Pagpapatakbo ng Apache Spark sa Kubernetes

Dear readers, magandang hapon. Ngayon ay pag-uusapan natin ang tungkol sa Apache Spark at ang mga prospect ng pag-unlad nito.

Pagpapatakbo ng Apache Spark sa Kubernetes

Sa modernong mundo ng Big Data, ang Apache Spark ay ang de facto na pamantayan para sa pagbuo ng mga gawain sa pagproseso ng batch data. Bilang karagdagan, ginagamit din ito upang lumikha ng mga streaming application na gumagana sa micro batch na konsepto, pagproseso at pagpapadala ng data sa maliliit na bahagi (Spark Structured Streaming). At ayon sa kaugalian ito ay naging bahagi ng pangkalahatang Hadoop stack, gamit ang YARN (o sa ilang mga kaso Apache Mesos) bilang resource manager. Sa pamamagitan ng 2020, ang paggamit nito sa tradisyonal na anyo nito ay pinag-uusapan para sa karamihan ng mga kumpanya dahil sa kakulangan ng disenteng mga pamamahagi ng Hadoop - ang pagbuo ng HDP at CDH ay tumigil, ang CDH ay hindi mahusay na binuo at may mataas na gastos, at ang natitirang mga supplier ng Hadoop ay may alinman ay tumigil sa pag-iral o magkaroon ng isang madilim na hinaharap. Samakatuwid, ang paglulunsad ng Apache Spark gamit ang Kubernetes ay nagpapalaki ng interes sa komunidad at malalaking kumpanya - nagiging pamantayan sa container orchestration at resource management sa pribado at pampublikong ulap, nilulutas nito ang problema sa hindi maginhawang pag-iiskedyul ng mapagkukunan ng mga gawain sa Spark sa YARN at nagbibigay ng isang patuloy na umuunlad na platform na may maraming komersyal at bukas na pamamahagi para sa mga kumpanya sa lahat ng laki at guhit. Bilang karagdagan, sa kalagayan ng katanyagan, karamihan ay nakakuha na ng ilang sariling pag-install at nadagdagan ang kanilang kadalubhasaan sa paggamit nito, na nagpapadali sa paglipat.

Simula sa bersyon 2.3.0, nakuha ng Apache Spark ang opisyal na suporta para sa pagpapatakbo ng mga gawain sa isang Kubernetes cluster at ngayon, pag-uusapan natin ang tungkol sa kasalukuyang kapanahunan ng diskarteng ito, iba't ibang mga opsyon para sa paggamit nito at mga pitfalls na makakaharap sa panahon ng pagpapatupad.

Una sa lahat, tingnan natin ang proseso ng pagbuo ng mga gawain at application batay sa Apache Spark at i-highlight ang mga tipikal na kaso kung saan kailangan mong magpatakbo ng isang gawain sa isang Kubernetes cluster. Sa paghahanda ng post na ito, ang OpenShift ay ginagamit bilang isang pamamahagi at ang mga command na nauugnay sa command line utility (oc) nito ay ibibigay. Para sa iba pang mga distribusyon ng Kubernetes, ang mga kaukulang command mula sa karaniwang Kubernetes command line utility (kubectl) o ang kanilang mga analogue (halimbawa, para sa oc adm policy) ay maaaring gamitin.

Unang kaso ng paggamit - spark-submit

Sa panahon ng pagbuo ng mga gawain at application, kailangan ng developer na magpatakbo ng mga gawain upang i-debug ang pagbabago ng data. Sa teorya, ang mga stub ay maaaring gamitin para sa mga layuning ito, ngunit ang pagbuo na may partisipasyon ng mga tunay (kahit na pagsubok) na mga pagkakataon ng mga end system ay napatunayang mas mabilis at mas mahusay sa klase ng mga gawaing ito. Sa kaso kapag nagde-debug kami sa mga totoong instance ng end system, dalawang sitwasyon ang posible:

  • ang developer ay nagpapatakbo ng isang gawaing Spark nang lokal sa standalone mode;

    Pagpapatakbo ng Apache Spark sa Kubernetes

  • nagpapatakbo ang isang developer ng isang Spark task sa isang Kubernetes cluster sa isang test loop.

    Pagpapatakbo ng Apache Spark sa Kubernetes

Ang unang opsyon ay may karapatang umiral, ngunit may kasamang ilang disadvantages:

  • Ang bawat developer ay dapat bigyan ng access mula sa lugar ng trabaho sa lahat ng pagkakataon ng mga end system na kailangan niya;
  • isang sapat na halaga ng mga mapagkukunan ay kinakailangan sa gumaganang makina upang patakbuhin ang gawain na binuo.

Ang pangalawang opsyon ay walang ganitong mga disbentaha, dahil ang paggamit ng Kubernetes cluster ay nagbibigay-daan sa iyo na maglaan ng kinakailangang resource pool para sa pagpapatakbo ng mga gawain at bigyan ito ng kinakailangang access upang tapusin ang mga instance ng system, na madaling magbigay ng access dito gamit ang Kubernetes role model para sa lahat ng miyembro ng development team. I-highlight natin ito bilang unang use case - paglulunsad ng mga gawain sa Spark mula sa isang lokal na developer machine sa isang Kubernetes cluster sa isang test circuit.

Pag-usapan natin ang higit pa tungkol sa proseso ng pag-set up ng Spark upang tumakbo nang lokal. Upang simulan ang paggamit ng Spark kailangan mong i-install ito:

mkdir /opt/spark
cd /opt/spark
wget http://mirror.linux-ia64.org/apache/spark/spark-2.4.5/spark-2.4.5.tgz
tar zxvf spark-2.4.5.tgz
rm -f spark-2.4.5.tgz

Kinokolekta namin ang mga kinakailangang pakete para sa pagtatrabaho sa Kubernetes:

cd spark-2.4.5/
./build/mvn -Pkubernetes -DskipTests clean package

Ang isang buong build ay tumatagal ng maraming oras, at upang lumikha ng mga imahe ng Docker at patakbuhin ang mga ito sa isang Kubernetes cluster, kailangan mo lang ng mga jar file mula sa direktoryo ng "assembly/", kaya maaari mo lamang buuin ang subproject na ito:

./build/mvn -f ./assembly/pom.xml -Pkubernetes -DskipTests clean package

Upang patakbuhin ang mga trabaho sa Spark sa Kubernetes, kailangan mong lumikha ng isang Docker na imahe upang magamit bilang isang batayang imahe. Mayroong 2 posibleng diskarte dito:

  • Kasama sa nabuong larawan ng Docker ang executable task code ng Spark;
  • Ang nilikhang imahe ay kinabibilangan lamang ng Spark at ang mga kinakailangang dependency, ang executable code ay naka-host nang malayuan (halimbawa, sa HDFS).

Una, bumuo tayo ng imahe ng Docker na naglalaman ng isang pagsubok na halimbawa ng isang gawain sa Spark. Upang lumikha ng mga imahe ng Docker, ang Spark ay may isang utility na tinatawag na "docker-image-tool". Pag-aralan natin ang tulong dito:

./bin/docker-image-tool.sh --help

Sa tulong nito, maaari kang lumikha ng mga imahe ng Docker at i-upload ang mga ito sa mga malalayong rehistro, ngunit bilang default ay mayroon itong ilang mga kawalan:

  • walang sablay na lumilikha ng 3 larawan ng Docker nang sabay-sabay - para sa Spark, PySpark at R;
  • ay hindi nagpapahintulot sa iyo na tumukoy ng pangalan ng larawan.

Samakatuwid, gagamit kami ng binagong bersyon ng utility na ito na ibinigay sa ibaba:

vi bin/docker-image-tool-upd.sh

#!/usr/bin/env bash

function error {
  echo "$@" 1>&2
  exit 1
}

if [ -z "${SPARK_HOME}" ]; then
  SPARK_HOME="$(cd "`dirname "$0"`"/..; pwd)"
fi
. "${SPARK_HOME}/bin/load-spark-env.sh"

function image_ref {
  local image="$1"
  local add_repo="${2:-1}"
  if [ $add_repo = 1 ] && [ -n "$REPO" ]; then
    image="$REPO/$image"
  fi
  if [ -n "$TAG" ]; then
    image="$image:$TAG"
  fi
  echo "$image"
}

function build {
  local BUILD_ARGS
  local IMG_PATH

  if [ ! -f "$SPARK_HOME/RELEASE" ]; then
    IMG_PATH=$BASEDOCKERFILE
    BUILD_ARGS=(
      ${BUILD_PARAMS}
      --build-arg
      img_path=$IMG_PATH
      --build-arg
      datagram_jars=datagram/runtimelibs
      --build-arg
      spark_jars=assembly/target/scala-$SPARK_SCALA_VERSION/jars
    )
  else
    IMG_PATH="kubernetes/dockerfiles"
    BUILD_ARGS=(${BUILD_PARAMS})
  fi

  if [ -z "$IMG_PATH" ]; then
    error "Cannot find docker image. This script must be run from a runnable distribution of Apache Spark."
  fi

  if [ -z "$IMAGE_REF" ]; then
    error "Cannot find docker image reference. Please add -i arg."
  fi

  local BINDING_BUILD_ARGS=(
    ${BUILD_PARAMS}
    --build-arg
    base_img=$(image_ref $IMAGE_REF)
  )
  local BASEDOCKERFILE=${BASEDOCKERFILE:-"$IMG_PATH/spark/docker/Dockerfile"}

  docker build $NOCACHEARG "${BUILD_ARGS[@]}" 
    -t $(image_ref $IMAGE_REF) 
    -f "$BASEDOCKERFILE" .
}

function push {
  docker push "$(image_ref $IMAGE_REF)"
}

function usage {
  cat <<EOF
Usage: $0 [options] [command]
Builds or pushes the built-in Spark Docker image.

Commands:
  build       Build image. Requires a repository address to be provided if the image will be
              pushed to a different registry.
  push        Push a pre-built image to a registry. Requires a repository address to be provided.

Options:
  -f file               Dockerfile to build for JVM based Jobs. By default builds the Dockerfile shipped with Spark.
  -p file               Dockerfile to build for PySpark Jobs. Builds Python dependencies and ships with Spark.
  -R file               Dockerfile to build for SparkR Jobs. Builds R dependencies and ships with Spark.
  -r repo               Repository address.
  -i name               Image name to apply to the built image, or to identify the image to be pushed.  
  -t tag                Tag to apply to the built image, or to identify the image to be pushed.
  -m                    Use minikube's Docker daemon.
  -n                    Build docker image with --no-cache
  -b arg      Build arg to build or push the image. For multiple build args, this option needs to
              be used separately for each build arg.

Using minikube when building images will do so directly into minikube's Docker daemon.
There is no need to push the images into minikube in that case, they'll be automatically
available when running applications inside the minikube cluster.

Check the following documentation for more information on using the minikube Docker daemon:

  https://kubernetes.io/docs/getting-started-guides/minikube/#reusing-the-docker-daemon

Examples:
  - Build image in minikube with tag "testing"
    $0 -m -t testing build

  - Build and push image with tag "v2.3.0" to docker.io/myrepo
    $0 -r docker.io/myrepo -t v2.3.0 build
    $0 -r docker.io/myrepo -t v2.3.0 push
EOF
}

if [[ "$@" = *--help ]] || [[ "$@" = *-h ]]; then
  usage
  exit 0
fi

REPO=
TAG=
BASEDOCKERFILE=
NOCACHEARG=
BUILD_PARAMS=
IMAGE_REF=
while getopts f:mr:t:nb:i: option
do
 case "${option}"
 in
 f) BASEDOCKERFILE=${OPTARG};;
 r) REPO=${OPTARG};;
 t) TAG=${OPTARG};;
 n) NOCACHEARG="--no-cache";;
 i) IMAGE_REF=${OPTARG};;
 b) BUILD_PARAMS=${BUILD_PARAMS}" --build-arg "${OPTARG};;
 esac
done

case "${@: -1}" in
  build)
    build
    ;;
  push)
    if [ -z "$REPO" ]; then
      usage
      exit 1
    fi
    push
    ;;
  *)
    usage
    exit 1
    ;;
esac

Sa tulong nito, nag-iipon kami ng isang pangunahing larawan ng Spark na naglalaman ng isang pagsubok na gawain para sa pagkalkula ng Pi gamit ang Spark (narito ang {docker-registry-url} ay ang URL ng iyong rehistro ng larawan ng Docker, ang {repo} ay ang pangalan ng repositoryo sa loob ng registry, na tumutugma sa proyekto sa OpenShift , {image-name} - pangalan ng larawan (kung ginamit ang tatlong antas na paghihiwalay ng mga larawan, halimbawa, tulad ng sa pinagsama-samang pagpapatala ng mga larawan ng Red Hat OpenShift), {tag} - tag nito bersyon ng larawan):

./bin/docker-image-tool-upd.sh -f resource-managers/kubernetes/docker/src/main/dockerfiles/spark/Dockerfile -r {docker-registry-url}/{repo} -i {image-name} -t {tag} build

Mag-log in sa OKD cluster gamit ang console utility (dito ang {OKD-API-URL} ay ang OKD cluster API URL):

oc login {OKD-API-URL}

Kunin natin ang token ng kasalukuyang user para sa awtorisasyon sa Docker Registry:

oc whoami -t

Mag-log in sa panloob na Docker Registry ng OKD cluster (ginagamit namin ang token na nakuha gamit ang nakaraang command bilang password):

docker login {docker-registry-url}

I-upload natin ang naka-assemble na imahe ng Docker sa Docker Registry OKD:

./bin/docker-image-tool-upd.sh -r {docker-registry-url}/{repo} -i {image-name} -t {tag} push

Tingnan natin kung ang naka-assemble na imahe ay available sa OKD. Upang gawin ito, buksan ang URL sa browser na may listahan ng mga larawan ng kaukulang proyekto (dito ang {project} ay ang pangalan ng proyekto sa loob ng OpenShift cluster, {OKD-WEBUI-URL} ay ang URL ng OpenShift Web console ) - https://{OKD-WEBUI-URL}/console /project/{project}/browse/images/{image-name}.

Upang magpatakbo ng mga gawain, dapat gumawa ng isang account ng serbisyo na may mga pribilehiyong magpatakbo ng mga pod bilang ugat (tatalakayin natin ang puntong ito sa ibang pagkakataon):

oc create sa spark -n {project}
oc adm policy add-scc-to-user anyuid -z spark -n {project}

Patakbuhin natin ang spark-submit command para mag-publish ng Spark task sa OKD cluster, na tumutukoy sa ginawang service account at Docker image:

 /opt/spark/bin/spark-submit --name spark-test --class org.apache.spark.examples.SparkPi --conf spark.executor.instances=3 --conf spark.kubernetes.authenticate.driver.serviceAccountName=spark --conf spark.kubernetes.namespace={project} --conf spark.submit.deployMode=cluster --conf spark.kubernetes.container.image={docker-registry-url}/{repo}/{image-name}:{tag} --conf spark.master=k8s://https://{OKD-API-URL}  local:///opt/spark/examples/target/scala-2.11/jars/spark-examples_2.11-2.4.5.jar

Narito:

—pangalan — ang pangalan ng gawain na lalahok sa pagbuo ng pangalan ng mga pod ng Kubernetes;

—class — klase ng executable file, na tinatawag kapag nagsimula ang gawain;

—conf — Mga parameter ng pagsasaayos ng spark;

spark.executor.instances — ang bilang ng mga Spark executor na ilulunsad;

spark.kubernetes.authenticate.driver.serviceAccountName - ang pangalan ng account ng serbisyo ng Kubernetes na ginagamit kapag naglulunsad ng mga pod (upang tukuyin ang konteksto ng seguridad at mga kakayahan kapag nakikipag-ugnayan sa Kubernetes API);

spark.kubernetes.namespace — Kubernetes namespace kung saan ilulunsad ang mga driver at executor pod;

spark.submit.deployMode — paraan ng paglulunsad ng Spark (para sa karaniwang spark-submit na "cluster" ay ginagamit, para sa Spark Operator at mga susunod na bersyon ng Spark "client");

spark.kubernetes.container.image - Docker image na ginamit upang ilunsad ang mga pod;

spark.master — Kubernetes API URL (panlabas ay tinukoy kaya ang pag-access ay nangyayari mula sa lokal na makina);

local:// ay ang path sa Spark executable sa loob ng Docker image.

Pumunta kami sa kaukulang proyekto ng OKD at pag-aralan ang mga ginawang pod - https://{OKD-WEBUI-URL}/console/project/{project}/browse/pods.

Upang pasimplehin ang proseso ng pag-develop, maaaring gumamit ng isa pang opsyon, kung saan ang isang karaniwang base na imahe ng Spark ay nilikha, na ginagamit ng lahat ng mga gawain upang tumakbo, at ang mga snapshot ng mga executable na file ay nai-publish sa panlabas na imbakan (halimbawa, Hadoop) at tinukoy kapag tumatawag spark-submit bilang isang link. Sa kasong ito, maaari kang magpatakbo ng iba't ibang bersyon ng mga gawain sa Spark nang hindi muling itinatayo ang mga imahe ng Docker, gamit, halimbawa, ang WebHDFS upang mag-publish ng mga larawan. Nagpapadala kami ng kahilingan para gumawa ng file (dito ang {host} ay ang host ng serbisyo ng WebHDFS, ang {port} ay ang port ng serbisyo ng WebHDFS, ang {path-to-file-on-hdfs} ay ang gustong path patungo sa file sa HDFS):

curl -i -X PUT "http://{host}:{port}/webhdfs/v1/{path-to-file-on-hdfs}?op=CREATE

Makakatanggap ka ng tugon na tulad nito (narito ang {location} ang URL na kailangang gamitin upang i-download ang file):

HTTP/1.1 307 TEMPORARY_REDIRECT
Location: {location}
Content-Length: 0

I-load ang Spark executable file sa HDFS (dito ang {path-to-local-file} ay ang path patungo sa Spark executable file sa kasalukuyang host):

curl -i -X PUT -T {path-to-local-file} "{location}"

Pagkatapos nito, maaari tayong gumawa ng spark-submit gamit ang Spark file na na-upload sa HDFS (dito ang {class-name} ay ang pangalan ng klase na kailangang ilunsad upang makumpleto ang gawain):

/opt/spark/bin/spark-submit --name spark-test --class {class-name} --conf spark.executor.instances=3 --conf spark.kubernetes.authenticate.driver.serviceAccountName=spark --conf spark.kubernetes.namespace={project} --conf spark.submit.deployMode=cluster --conf spark.kubernetes.container.image={docker-registry-url}/{repo}/{image-name}:{tag} --conf spark.master=k8s://https://{OKD-API-URL}  hdfs://{host}:{port}/{path-to-file-on-hdfs}

Dapat tandaan na upang ma-access ang HDFS at matiyak na gumagana ang gawain, maaaring kailanganin mong baguhin ang Dockerfile at ang entrypoint.sh script - magdagdag ng isang direktiba sa Dockerfile upang kopyahin ang mga umaasa na aklatan sa /opt/spark/jars na direktoryo at isama ang HDFS configuration file sa SPARK_CLASSPATH sa entrypoint. sh.

Pangalawang kaso ng paggamit - Apache Livy

Dagdag pa, kapag ang isang gawain ay binuo at ang resulta ay kailangang masuri, ang tanong ay bumangon sa paglulunsad nito bilang bahagi ng proseso ng CI/CD at pagsubaybay sa katayuan ng pagpapatupad nito. Siyempre, maaari mo itong patakbuhin gamit ang isang lokal na spark-submit na tawag, ngunit ginagawa nitong kumplikado ang imprastraktura ng CI/CD dahil nangangailangan ito ng pag-install at pag-configure ng Spark sa mga ahente/runner ng CI server at pag-set up ng access sa Kubernetes API. Para sa kasong ito, pinili ng target na pagpapatupad na gamitin ang Apache Livy bilang REST API para sa pagpapatakbo ng mga gawain sa Spark na naka-host sa loob ng isang Kubernetes cluster. Sa tulong nito, maaari kang magpatakbo ng mga gawain sa Spark sa isang cluster ng Kubernetes gamit ang mga regular na kahilingan sa cURL, na madaling ipatupad batay sa anumang solusyon sa CI, at ang paglalagay nito sa loob ng cluster ng Kubernetes ay malulutas ang isyu ng pagpapatunay kapag nakikipag-ugnayan sa Kubernetes API.

Pagpapatakbo ng Apache Spark sa Kubernetes

I-highlight natin ito bilang pangalawang kaso ng paggamit - pagpapatakbo ng mga gawain sa Spark bilang bahagi ng proseso ng CI/CD sa isang cluster ng Kubernetes sa isang test loop.

Kaunti tungkol sa Apache Livy - gumagana ito bilang isang HTTP server na nagbibigay ng Web interface at isang RESTful API na nagbibigay-daan sa iyong malayuang ilunsad ang spark-submit sa pamamagitan ng pagpasa sa mga kinakailangang parameter. Ayon sa kaugalian, ito ay ipinadala bilang bahagi ng isang pamamahagi ng HDP, ngunit maaari ding i-deploy sa OKD o anumang iba pang pag-install ng Kubernetes gamit ang naaangkop na manifest at isang hanay ng mga larawan ng Docker, tulad ng isang ito - github.com/ttauveron/k8s-big-data-experiments/tree/master/livy-spark-2.3. Para sa aming kaso, binuo ang isang katulad na imahe ng Docker, kasama ang Spark na bersyon 2.4.5 mula sa sumusunod na Dockerfile:

FROM java:8-alpine

ENV SPARK_HOME=/opt/spark
ENV LIVY_HOME=/opt/livy
ENV HADOOP_CONF_DIR=/etc/hadoop/conf
ENV SPARK_USER=spark

WORKDIR /opt

RUN apk add --update openssl wget bash && 
    wget -P /opt https://downloads.apache.org/spark/spark-2.4.5/spark-2.4.5-bin-hadoop2.7.tgz && 
    tar xvzf spark-2.4.5-bin-hadoop2.7.tgz && 
    rm spark-2.4.5-bin-hadoop2.7.tgz && 
    ln -s /opt/spark-2.4.5-bin-hadoop2.7 /opt/spark

RUN wget http://mirror.its.dal.ca/apache/incubator/livy/0.7.0-incubating/apache-livy-0.7.0-incubating-bin.zip && 
    unzip apache-livy-0.7.0-incubating-bin.zip && 
    rm apache-livy-0.7.0-incubating-bin.zip && 
    ln -s /opt/apache-livy-0.7.0-incubating-bin /opt/livy && 
    mkdir /var/log/livy && 
    ln -s /var/log/livy /opt/livy/logs && 
    cp /opt/livy/conf/log4j.properties.template /opt/livy/conf/log4j.properties

ADD livy.conf /opt/livy/conf
ADD spark-defaults.conf /opt/spark/conf/spark-defaults.conf
ADD entrypoint.sh /entrypoint.sh

ENV PATH="/opt/livy/bin:${PATH}"

EXPOSE 8998

ENTRYPOINT ["/entrypoint.sh"]
CMD ["livy-server"]

Ang nabuong imahe ay maaaring itayo at i-upload sa iyong umiiral na Docker repository, tulad ng panloob na OKD repository. Upang i-deploy ito, gamitin ang sumusunod na manifest ({registry-url} - URL ng Docker image registry, {image-name} - Docker image name, {tag} - Docker image tag, {livy-url} - gustong URL kung saan ang server ay maa-access Livy; ang "Ruta" na manifest ay ginagamit kung ang Red Hat OpenShift ay ginagamit bilang distribusyon ng Kubernetes, kung hindi ang kaukulang Ingress o Service manifest ng uri ng NodePort ay gagamitin):

---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    component: livy
  name: livy
spec:
  progressDeadlineSeconds: 600
  replicas: 1
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      component: livy
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        component: livy
    spec:
      containers:
        - command:
            - livy-server
          env:
            - name: K8S_API_HOST
              value: localhost
            - name: SPARK_KUBERNETES_IMAGE
              value: 'gnut3ll4/spark:v1.0.14'
          image: '{registry-url}/{image-name}:{tag}'
          imagePullPolicy: Always
          name: livy
          ports:
            - containerPort: 8998
              name: livy-rest
              protocol: TCP
          resources: {}
          terminationMessagePath: /dev/termination-log
          terminationMessagePolicy: File
          volumeMounts:
            - mountPath: /var/log/livy
              name: livy-log
            - mountPath: /opt/.livy-sessions/
              name: livy-sessions
            - mountPath: /opt/livy/conf/livy.conf
              name: livy-config
              subPath: livy.conf
            - mountPath: /opt/spark/conf/spark-defaults.conf
              name: spark-config
              subPath: spark-defaults.conf
        - command:
            - /usr/local/bin/kubectl
            - proxy
            - '--port'
            - '8443'
          image: 'gnut3ll4/kubectl-sidecar:latest'
          imagePullPolicy: Always
          name: kubectl
          ports:
            - containerPort: 8443
              name: k8s-api
              protocol: TCP
          resources: {}
          terminationMessagePath: /dev/termination-log
          terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      serviceAccount: spark
      serviceAccountName: spark
      terminationGracePeriodSeconds: 30
      volumes:
        - emptyDir: {}
          name: livy-log
        - emptyDir: {}
          name: livy-sessions
        - configMap:
            defaultMode: 420
            items:
              - key: livy.conf
                path: livy.conf
            name: livy-config
          name: livy-config
        - configMap:
            defaultMode: 420
            items:
              - key: spark-defaults.conf
                path: spark-defaults.conf
            name: livy-config
          name: spark-config
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: livy-config
data:
  livy.conf: |-
    livy.spark.deploy-mode=cluster
    livy.file.local-dir-whitelist=/opt/.livy-sessions/
    livy.spark.master=k8s://http://localhost:8443
    livy.server.session.state-retain.sec = 8h
  spark-defaults.conf: 'spark.kubernetes.container.image        "gnut3ll4/spark:v1.0.14"'
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: livy
  name: livy
spec:
  ports:
    - name: livy-rest
      port: 8998
      protocol: TCP
      targetPort: 8998
  selector:
    component: livy
  sessionAffinity: None
  type: ClusterIP
---
apiVersion: route.openshift.io/v1
kind: Route
metadata:
  labels:
    app: livy
  name: livy
spec:
  host: {livy-url}
  port:
    targetPort: livy-rest
  to:
    kind: Service
    name: livy
    weight: 100
  wildcardPolicy: None

Pagkatapos ilapat ito at matagumpay na ilunsad ang pod, available ang Livy graphical interface sa link: http://{livy-url}/ui. Sa Livy, maaari naming i-publish ang aming gawain sa Spark gamit ang isang REST na kahilingan mula sa, halimbawa, Postman. Ang isang halimbawa ng isang koleksyon na may mga kahilingan ay ipinakita sa ibaba (ang mga argumento ng configuration na may mga variable na kinakailangan para sa pagpapatakbo ng inilunsad na gawain ay maaaring maipasa sa array na "args"):

{
    "info": {
        "_postman_id": "be135198-d2ff-47b6-a33e-0d27b9dba4c8",
        "name": "Spark Livy",
        "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
    },
    "item": [
        {
            "name": "1 Submit job with jar",
            "request": {
                "method": "POST",
                "header": [
                    {
                        "key": "Content-Type",
                        "value": "application/json"
                    }
                ],
                "body": {
                    "mode": "raw",
                    "raw": "{nt"file": "local:///opt/spark/examples/target/scala-2.11/jars/spark-examples_2.11-2.4.5.jar", nt"className": "org.apache.spark.examples.SparkPi",nt"numExecutors":1,nt"name": "spark-test-1",nt"conf": {ntt"spark.jars.ivy": "/tmp/.ivy",ntt"spark.kubernetes.authenticate.driver.serviceAccountName": "spark",ntt"spark.kubernetes.namespace": "{project}",ntt"spark.kubernetes.container.image": "{docker-registry-url}/{repo}/{image-name}:{tag}"nt}n}"
                },
                "url": {
                    "raw": "http://{livy-url}/batches",
                    "protocol": "http",
                    "host": [
                        "{livy-url}"
                    ],
                    "path": [
                        "batches"
                    ]
                }
            },
            "response": []
        },
        {
            "name": "2 Submit job without jar",
            "request": {
                "method": "POST",
                "header": [
                    {
                        "key": "Content-Type",
                        "value": "application/json"
                    }
                ],
                "body": {
                    "mode": "raw",
                    "raw": "{nt"file": "hdfs://{host}:{port}/{path-to-file-on-hdfs}", nt"className": "{class-name}",nt"numExecutors":1,nt"name": "spark-test-2",nt"proxyUser": "0",nt"conf": {ntt"spark.jars.ivy": "/tmp/.ivy",ntt"spark.kubernetes.authenticate.driver.serviceAccountName": "spark",ntt"spark.kubernetes.namespace": "{project}",ntt"spark.kubernetes.container.image": "{docker-registry-url}/{repo}/{image-name}:{tag}"nt},nt"args": [ntt"HADOOP_CONF_DIR=/opt/spark/hadoop-conf",ntt"MASTER=k8s://https://kubernetes.default.svc:8443"nt]n}"
                },
                "url": {
                    "raw": "http://{livy-url}/batches",
                    "protocol": "http",
                    "host": [
                        "{livy-url}"
                    ],
                    "path": [
                        "batches"
                    ]
                }
            },
            "response": []
        }
    ],
    "event": [
        {
            "listen": "prerequest",
            "script": {
                "id": "41bea1d0-278c-40c9-ad42-bf2e6268897d",
                "type": "text/javascript",
                "exec": [
                    ""
                ]
            }
        },
        {
            "listen": "test",
            "script": {
                "id": "3cdd7736-a885-4a2d-9668-bd75798f4560",
                "type": "text/javascript",
                "exec": [
                    ""
                ]
            }
        }
    ],
    "protocolProfileBehavior": {}
}

Isagawa natin ang unang kahilingan mula sa koleksyon, pumunta sa OKD interface at tingnan kung matagumpay na nailunsad ang gawain - https://{OKD-WEBUI-URL}/console/project/{project}/browse/pods. Kasabay nito, lalabas ang isang session sa interface ng Livy (http://{livy-url}/ui), kung saan, gamit ang Livy API o graphical na interface, maaari mong subaybayan ang pag-usad ng gawain at pag-aralan ang session mga log.

Ngayon ipakita natin kung paano gumagana si Livy. Upang gawin ito, suriin natin ang mga log ng Livy container sa loob ng pod gamit ang Livy server - https://{OKD-WEBUI-URL}/console/project/{project}/browse/pods/{livy-pod-name }?tab=logs. Mula sa kanila, makikita natin na kapag tinatawag ang Livy REST API sa isang lalagyan na pinangalanang "livy", ang isang spark-submit ay isinasagawa, katulad ng ginamit namin sa itaas (dito ang {livy-pod-name} ay ang pangalan ng ginawang pod gamit ang Livy server). Ang koleksyon ay nagpapakilala rin ng pangalawang query na nagbibigay-daan sa iyong magpatakbo ng mga gawain na malayuang nagho-host ng isang Spark na maipapatupad gamit ang isang Livy server.

Pangatlong kaso ng paggamit - Spark Operator

Ngayon na ang gawain ay nasubok, ang tanong ng pagpapatakbo nito nang regular ay lumitaw. Ang katutubong paraan upang regular na magpatakbo ng mga gawain sa isang kumpol ng Kubernetes ay ang CronJob entity at magagamit mo ito, ngunit sa ngayon ang paggamit ng mga operator upang pamahalaan ang mga aplikasyon sa Kubernetes ay napakapopular at para sa Spark ay mayroong isang medyo mature na operator, na isa ring ginagamit sa mga solusyon sa antas ng Enterprise (halimbawa, Lightbend FastData Platform). Inirerekumenda namin ang paggamit nito - ang kasalukuyang matatag na bersyon ng Spark (2.4.5) ay medyo limitado ang mga opsyon sa pagsasaayos para sa pagpapatakbo ng mga gawain sa Spark sa Kubernetes, habang ang susunod na pangunahing bersyon (3.0.0) ay nagdedeklara ng buong suporta para sa Kubernetes, ngunit ang petsa ng paglabas nito ay nananatiling hindi alam . Binabayaran ng Spark Operator ang pagkukulang na ito sa pamamagitan ng pagdaragdag ng mahahalagang opsyon sa configuration (halimbawa, pag-mount ng ConfigMap na may Hadoop access configuration sa Spark pods) at ang kakayahang magpatakbo ng regular na nakaiskedyul na gawain.

Pagpapatakbo ng Apache Spark sa Kubernetes
I-highlight natin ito bilang pangatlong kaso ng paggamit - regular na nagpapatakbo ng mga gawain sa Spark sa isang cluster ng Kubernetes sa isang production loop.

Ang Spark Operator ay open source at binuo sa loob ng Google Cloud Platform - github.com/GoogleCloudPlatform/spark-on-k8s-operator. Ang pag-install nito ay maaaring gawin sa 3 paraan:

  1. Bilang bahagi ng pag-install ng Lightbend FastData Platform/Cloudflow;
  2. Paggamit ng Helm:
    helm repo add incubator http://storage.googleapis.com/kubernetes-charts-incubator
    helm install incubator/sparkoperator --namespace spark-operator
    	

  3. Paggamit ng mga manifest mula sa opisyal na repository (https://github.com/GoogleCloudPlatform/spark-on-k8s-operator/tree/master/manifest). Ito ay nagkakahalaga ng pagpuna sa mga sumusunod - Cloudflow ay may kasamang operator na may bersyon ng API v1beta1. Kung gagamitin ang ganitong uri ng pag-install, ang mga paglalarawan ng manifest ng Spark application ay dapat na nakabatay sa mga halimbawang tag sa Git na may naaangkop na bersyon ng API, halimbawa, "v1beta1-0.9.0-2.4.0." Ang bersyon ng operator ay matatagpuan sa paglalarawan ng CRD na kasama sa operator sa diksyunaryo ng "mga bersyon":
    oc get crd sparkapplications.sparkoperator.k8s.io -o yaml
    	

Kung na-install nang tama ang operator, lalabas ang isang aktibong pod na may operator ng Spark sa kaukulang proyekto (halimbawa, cloudflow-fdp-sparkoperator sa Cloudflow space para sa pag-install ng Cloudflow) at lalabas ang isang kaukulang uri ng mapagkukunan ng Kubernetes na pinangalanang "sparkapplications" . Maaari mong galugarin ang mga magagamit na application ng Spark gamit ang sumusunod na command:

oc get sparkapplications -n {project}

Upang magpatakbo ng mga gawain gamit ang Spark Operator kailangan mong gawin ang 3 bagay:

  • lumikha ng Docker image na kinabibilangan ng lahat ng kinakailangang library, pati na rin ang configuration at executable na mga file. Sa target na larawan, ito ay isang imahe na nilikha sa yugto ng CI/CD at sinubukan sa isang test cluster;
  • mag-publish ng imahe ng Docker sa isang registry na naa-access mula sa cluster ng Kubernetes;
  • bumuo ng isang manifest na may uri ng "SparkApplication" at isang paglalarawan ng gawaing ilulunsad. Available ang mga halimbawang manifest sa opisyal na repositoryo (hal. github.com/GoogleCloudPlatform/spark-on-k8s-operator/blob/v1beta1-0.9.0-2.4.0/examples/spark-pi.yaml). Mayroong mahahalagang punto na dapat tandaan tungkol sa manifesto:
    1. dapat ipahiwatig ng diksyunaryo ng "apiVersion" ang bersyon ng API na naaayon sa bersyon ng operator;
    2. dapat ipahiwatig ng diksyunaryo ng "metadata.namespace" ang namespace kung saan ilulunsad ang application;
    3. ang diksyunaryo ng "spec.image" ay dapat maglaman ng address ng ginawang Docker na imahe sa isang naa-access na registry;
    4. ang diksyunaryo ng "spec.mainClass" ay dapat maglaman ng klase ng gawain ng Spark na kailangang patakbuhin kapag nagsimula ang proseso;
    5. ang diksyonaryo ng "spec.mainApplicationFile" ay dapat maglaman ng landas patungo sa executable jar file;
    6. dapat ipahiwatig ng diksyunaryo ng "spec.sparkVersion" ang bersyon ng Spark na ginagamit;
    7. dapat tukuyin ng diksyunaryo ng “spec.driver.serviceAccount” ang account ng serbisyo sa loob ng kaukulang namespace ng Kubernetes na gagamitin upang patakbuhin ang application;
    8. dapat ipahiwatig ng diksyunaryo ng "spec.executor" ang bilang ng mga mapagkukunang inilalaan sa aplikasyon;
    9. dapat tukuyin ng diksyunaryo ng "spec.volumeMounts" ang lokal na direktoryo kung saan gagawin ang mga lokal na Spark task file.

Isang halimbawa ng pagbuo ng manifest (dito ang {spark-service-account} ay isang service account sa loob ng Kubernetes cluster para sa pagpapatakbo ng mga gawain sa Spark):

apiVersion: "sparkoperator.k8s.io/v1beta1"
kind: SparkApplication
metadata:
  name: spark-pi
  namespace: {project}
spec:
  type: Scala
  mode: cluster
  image: "gcr.io/spark-operator/spark:v2.4.0"
  imagePullPolicy: Always
  mainClass: org.apache.spark.examples.SparkPi
  mainApplicationFile: "local:///opt/spark/examples/jars/spark-examples_2.11-2.4.0.jar"
  sparkVersion: "2.4.0"
  restartPolicy:
    type: Never
  volumes:
    - name: "test-volume"
      hostPath:
        path: "/tmp"
        type: Directory
  driver:
    cores: 0.1
    coreLimit: "200m"
    memory: "512m"
    labels:
      version: 2.4.0
    serviceAccount: {spark-service-account}
    volumeMounts:
      - name: "test-volume"
        mountPath: "/tmp"
  executor:
    cores: 1
    instances: 1
    memory: "512m"
    labels:
      version: 2.4.0
    volumeMounts:
      - name: "test-volume"
        mountPath: "/tmp"

Tinutukoy ng manifest na ito ang isang account ng serbisyo kung saan, bago i-publish ang manifest, dapat kang lumikha ng mga kinakailangang papel na nagbubuklod na nagbibigay ng mga kinakailangang karapatan sa pag-access para sa Spark application upang makipag-ugnayan sa Kubernetes API (kung kinakailangan). Sa aming kaso, ang application ay nangangailangan ng mga karapatan upang lumikha ng mga Pod. Gumawa tayo ng kinakailangang papel na nagbubuklod:

oc adm policy add-role-to-user edit system:serviceaccount:{project}:{spark-service-account} -n {project}

Dapat ding tandaan na ang manifest specification na ito ay maaaring may kasamang parameter na "hadoopConfigMap", na nagbibigay-daan sa iyong tumukoy ng ConfigMap na may configuration ng Hadoop nang hindi kinakailangang ilagay muna ang kaukulang file sa Docker image. Angkop din ito para sa regular na pagpapatakbo ng mga gawain - gamit ang parameter na "iskedyul", maaaring tukuyin ang isang iskedyul para sa pagpapatakbo ng isang naibigay na gawain.

Pagkatapos nito, ise-save namin ang aming manifest sa spark-pi.yaml file at ilalapat ito sa aming Kubernetes cluster:

oc apply -f spark-pi.yaml

Ito ay lilikha ng isang object ng uri na "sparkapplications":

oc get sparkapplications -n {project}
> NAME       AGE
> spark-pi   22h

Sa kasong ito, ang isang pod na may isang application ay malilikha, ang katayuan kung saan ay ipapakita sa nilikha na "sparkapplications". Maaari mong tingnan ito gamit ang sumusunod na utos:

oc get sparkapplications spark-pi -o yaml -n {project}

Sa pagkumpleto ng gawain, lilipat ang POD sa status na "Nakumpleto", na mag-a-update din sa "sparkapplications". Maaaring tingnan ang mga log ng application sa browser o gamit ang sumusunod na command (dito ang {sparkapplications-pod-name} ay ang pangalan ng pod ng tumatakbong gawain):

oc logs {sparkapplications-pod-name} -n {project}

Ang mga gawain sa spark ay maaari ding pamahalaan gamit ang espesyal na utility ng sparkctl. Para i-install ito, i-clone ang repository gamit ang source code nito, i-install ang Go at buuin ang utility na ito:

git clone https://github.com/GoogleCloudPlatform/spark-on-k8s-operator.git
cd spark-on-k8s-operator/
wget https://dl.google.com/go/go1.13.3.linux-amd64.tar.gz
tar -xzf go1.13.3.linux-amd64.tar.gz
sudo mv go /usr/local
mkdir $HOME/Projects
export GOROOT=/usr/local/go
export GOPATH=$HOME/Projects
export PATH=$GOPATH/bin:$GOROOT/bin:$PATH
go -version
cd sparkctl
go build -o sparkctl
sudo mv sparkctl /usr/local/bin

Suriin natin ang listahan ng pagpapatakbo ng mga gawain sa Spark:

sparkctl list -n {project}

Gumawa tayo ng paglalarawan para sa gawain ng Spark:

vi spark-app.yaml

apiVersion: "sparkoperator.k8s.io/v1beta1"
kind: SparkApplication
metadata:
  name: spark-pi
  namespace: {project}
spec:
  type: Scala
  mode: cluster
  image: "gcr.io/spark-operator/spark:v2.4.0"
  imagePullPolicy: Always
  mainClass: org.apache.spark.examples.SparkPi
  mainApplicationFile: "local:///opt/spark/examples/jars/spark-examples_2.11-2.4.0.jar"
  sparkVersion: "2.4.0"
  restartPolicy:
    type: Never
  volumes:
    - name: "test-volume"
      hostPath:
        path: "/tmp"
        type: Directory
  driver:
    cores: 1
    coreLimit: "1000m"
    memory: "512m"
    labels:
      version: 2.4.0
    serviceAccount: spark
    volumeMounts:
      - name: "test-volume"
        mountPath: "/tmp"
  executor:
    cores: 1
    instances: 1
    memory: "512m"
    labels:
      version: 2.4.0
    volumeMounts:
      - name: "test-volume"
        mountPath: "/tmp"

Patakbuhin natin ang inilarawang gawain gamit ang sparkctl:

sparkctl create spark-app.yaml -n {project}

Suriin natin ang listahan ng pagpapatakbo ng mga gawain sa Spark:

sparkctl list -n {project}

Suriin natin ang listahan ng mga kaganapan ng isang inilunsad na gawain sa Spark:

sparkctl event spark-pi -n {project} -f

Suriin natin ang katayuan ng tumatakbong gawain ng Spark:

sparkctl status spark-pi -n {project}

Sa konklusyon, gusto kong isaalang-alang ang mga natuklasang disadvantages ng paggamit ng kasalukuyang stable na bersyon ng Spark (2.4.5) sa Kubernetes:

  1. Ang una at, marahil, ang pangunahing kawalan ay ang kakulangan ng Lokalidad ng Data. Sa kabila ng lahat ng mga pagkukulang ng YARN, mayroon ding mga pakinabang sa paggamit nito, halimbawa, ang prinsipyo ng paghahatid ng code sa data (sa halip na data sa code). Salamat dito, ang mga gawain ng Spark ay naisakatuparan sa mga node kung saan matatagpuan ang data na kasangkot sa mga kalkulasyon, at ang oras na kinuha upang maghatid ng data sa network ay makabuluhang nabawasan. Kapag gumagamit ng Kubernetes, nahaharap tayo sa pangangailangang ilipat ang data na kasangkot sa isang gawain sa buong network. Kung ang mga ito ay sapat na malaki, ang oras ng pagpapatupad ng gawain ay maaaring tumaas nang malaki, at nangangailangan din ng medyo malaking halaga ng puwang sa disk na inilaan sa mga pagkakataon ng gawain ng Spark para sa kanilang pansamantalang imbakan. Ang kawalan na ito ay maaaring mabawasan sa pamamagitan ng paggamit ng espesyal na software na nagsisiguro sa lokalidad ng data sa Kubernetes (halimbawa, Alluxio), ngunit nangangahulugan ito ng pangangailangang mag-imbak ng kumpletong kopya ng data sa mga node ng Kubernetes cluster.
  2. Ang pangalawang mahalagang kawalan ay seguridad. Bilang default, ang mga tampok na nauugnay sa seguridad tungkol sa pagpapatakbo ng mga gawain sa Spark ay hindi pinagana, ang paggamit ng Kerberos ay hindi saklaw sa opisyal na dokumentasyon (bagama't ang mga kaukulang opsyon ay ipinakilala sa bersyon 3.0.0, na mangangailangan ng karagdagang trabaho), at ang dokumentasyon ng seguridad para sa gamit ang Spark (https ://spark.apache.org/docs/2.4.5/security.html) tanging YARN, Mesos at Standalone Cluster ang lalabas bilang mga pangunahing tindahan. Kasabay nito, ang user kung saan inilunsad ang mga gawain ng Spark ay hindi maaaring direktang tukuyin - tinutukoy lang namin ang account ng serbisyo kung saan ito gagana, at ang user ay pinili batay sa mga naka-configure na patakaran sa seguridad. Kaugnay nito, ginagamit ang root user, na hindi ligtas sa isang produktibong kapaligiran, o isang user na may random na UID, na hindi maginhawa kapag namamahagi ng mga karapatan sa pag-access sa data (maaari itong malutas sa pamamagitan ng paglikha ng PodSecurityPolicies at pag-link sa kanila sa kaukulang account ng serbisyo). Sa kasalukuyan, ang solusyon ay ilagay ang lahat ng kinakailangang file nang direkta sa imahe ng Docker, o baguhin ang script ng paglulunsad ng Spark upang magamit ang mekanismo para sa pag-iimbak at pagkuha ng mga lihim na pinagtibay sa iyong organisasyon.
  3. Ang pagpapatakbo ng mga trabaho sa Spark gamit ang Kubernetes ay opisyal na nasa experimental mode pa rin at maaaring may mga makabuluhang pagbabago sa mga artifact na ginamit (mga configuration file, Docker base na imahe, at launch script) sa hinaharap. At sa katunayan, kapag inihahanda ang materyal, ang mga bersyon 2.3.0 at 2.4.5 ay nasubok, ang pag-uugali ay makabuluhang naiiba.

Maghintay tayo ng mga update - isang bagong bersyon ng Spark (3.0.0) ang inilabas kamakailan, na nagdala ng makabuluhang pagbabago sa gawain ng Spark sa Kubernetes, ngunit napanatili ang pang-eksperimentong katayuan ng suporta para sa resource manager na ito. Marahil ang mga susunod na pag-update ay talagang gagawing posible na ganap na irekomenda ang pag-abandona sa YARN at pagpapatakbo ng mga gawain sa Spark sa Kubernetes nang walang takot para sa seguridad ng iyong system at nang hindi nangangailangan na independiyenteng baguhin ang mga functional na bahagi.

Wakas.

Pinagmulan: www.habr.com

Magdagdag ng komento