Kjører Apache Spark på Kubernetes

Kjære lesere, god ettermiddag. I dag skal vi snakke litt om Apache Spark og dens utviklingsmuligheter.

Kjører Apache Spark på Kubernetes

I den moderne verden av Big Data er Apache Spark de facto-standarden for utvikling av batchdatabehandlingsoppgaver. I tillegg brukes den også til å lage strømmeapplikasjoner som fungerer i mikrobatch-konseptet, behandle og sende data i små porsjoner (Spark Structured Streaming). Og tradisjonelt har det vært en del av den totale Hadoop-stabelen, ved å bruke YARN (eller i noen tilfeller Apache Mesos) som ressursansvarlig. Innen 2020 er bruken i sin tradisjonelle form et spørsmål for de fleste selskaper på grunn av mangelen på anstendige Hadoop-distribusjoner - utviklingen av HDP og CDH har stoppet, CDH er ikke godt utviklet og har høye kostnader, og de gjenværende Hadoop-leverandørene har enten sluttet å eksistere eller har en svak fremtid. Derfor er lanseringen av Apache Spark ved bruk av Kubernetes av økende interesse blant fellesskapet og store selskaper - ved å bli en standard innen containerorkestrering og ressursstyring i private og offentlige skyer, løser den problemet med upraktisk ressursplanlegging av Spark-oppgaver på YARN og gir en plattform i stadig utvikling med mange kommersielle og åpne distribusjoner for selskaper i alle størrelser og striper. I tillegg, i kjølvannet av populariteten, har de fleste allerede klart å skaffe seg et par egne installasjoner og har økt sin ekspertise på bruken, noe som forenkler flyttingen.

Fra og med versjon 2.3.0 skaffet Apache Spark offisiell støtte for å kjøre oppgaver i en Kubernetes-klynge, og i dag vil vi snakke om den nåværende modenheten til denne tilnærmingen, ulike alternativer for bruken og fallgruvene som vil bli møtt under implementeringen.

Først av alt, la oss se på prosessen med å utvikle oppgaver og applikasjoner basert på Apache Spark og fremheve typiske tilfeller der du trenger å kjøre en oppgave på en Kubernetes-klynge. Når du forbereder dette innlegget, brukes OpenShift som en distribusjon og kommandoer som er relevante for kommandolinjeverktøyet (oc) vil bli gitt. For andre Kubernetes-distribusjoner kan de tilsvarende kommandoene fra standard Kubernetes kommandolinjeverktøy (kubectl) eller deres analoger (for eksempel for oc adm policy) brukes.

Førstegangsbruk - gnist-send

Under utviklingen av oppgaver og applikasjoner må utvikleren kjøre oppgaver for å feilsøke datatransformasjon. Teoretisk sett kan stubber brukes til disse formålene, men utvikling med deltagelse av reelle (riktignok test) forekomster av sluttsystemer har vist seg å være raskere og bedre i denne klassen av oppgaver. I tilfellet når vi feilsøker på reelle forekomster av sluttsystemer, er to scenarier mulig:

  • utvikleren kjører en Spark-oppgave lokalt i frittstående modus;

    Kjører Apache Spark på Kubernetes

  • en utvikler kjører en Spark-oppgave på en Kubernetes-klynge i en testsløyfe.

    Kjører Apache Spark på Kubernetes

Det første alternativet har rett til å eksistere, men medfører en rekke ulemper:

  • Hver utvikler må gis tilgang fra arbeidsplassen til alle forekomster av sluttsystemene han trenger;
  • en tilstrekkelig mengde ressurser kreves på arbeidsmaskinen for å kjøre oppgaven som utvikles.

Det andre alternativet har ikke disse ulempene, siden bruken av en Kubernetes-klynge lar deg tildele den nødvendige ressurspoolen for å kjøre oppgaver og gi den den nødvendige tilgangen til sluttsystemforekomster, og gir fleksibel tilgang til den ved å bruke Kubernetes-rollemodellen for alle medlemmer av utviklingsteamet. La oss fremheve det som den første brukssaken – lansering av Spark-oppgaver fra en lokal utviklermaskin på en Kubernetes-klynge i en testkrets.

La oss snakke mer om prosessen med å sette opp Spark til å kjøre lokalt. For å begynne å bruke Spark må du installere det:

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

Vi samler inn de nødvendige pakkene for å jobbe med Kubernetes:

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

En full build tar mye tid, og for å lage Docker-bilder og kjøre dem på en Kubernetes-klynge trenger du egentlig bare jar-filer fra "assembly/"-katalogen, så du kan bare bygge dette underprosjektet:

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

For å kjøre Spark-jobber på Kubernetes, må du opprette et Docker-bilde som skal brukes som et basisbilde. Det er 2 mulige tilnærminger her:

  • Det genererte Docker-bildet inkluderer den kjørbare Spark-oppgavekoden;
  • Det opprettede bildet inkluderer bare Spark og de nødvendige avhengighetene, den kjørbare koden vert eksternt (for eksempel i HDFS).

Først, la oss bygge et Docker-bilde som inneholder et testeksempel på en Spark-oppgave. For å lage Docker-bilder har Spark et verktøy som heter "docker-image-tool". La oss studere hjelpen på det:

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

Med dens hjelp kan du lage Docker-bilder og laste dem opp til eksterne registre, men som standard har det en rekke ulemper:

  • skaper uten feil 3 Docker-bilder samtidig - for Spark, PySpark og R;
  • tillater deg ikke å angi et bildenavn.

Derfor vil vi bruke en modifisert versjon av dette verktøyet gitt nedenfor:

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

Med dens hjelp setter vi sammen et grunnleggende Spark-bilde som inneholder en testoppgave for å beregne Pi ved hjelp av Spark (her er {docker-registry-url} URL-en til Docker-bilderegisteret ditt, {repo} er navnet på depotet inne i registeret, som samsvarer med prosjektet i OpenShift , {image-name} — navn på bildet (hvis tre-nivå separasjon av bilder brukes, for eksempel som i det integrerte registret for Red Hat OpenShift-bilder), {tag} — tag for dette versjon av bildet):

./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

Logg på OKD-klyngen ved hjelp av konsollverktøyet (her er {OKD-API-URL} OKD-klyngen API URL):

oc login {OKD-API-URL}

La oss få gjeldende brukers token for autorisasjon i Docker Registry:

oc whoami -t

Logg på det interne Docker-registeret til OKD-klyngen (vi bruker tokenet oppnådd ved å bruke forrige kommando som passord):

docker login {docker-registry-url}

La oss laste opp det sammensatte Docker-bildet til Docker Registry OKD:

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

La oss sjekke at det sammensatte bildet er tilgjengelig i OKD. For å gjøre dette, åpne URL-en i nettleseren med en liste over bilder av det tilsvarende prosjektet (her er {project} navnet på prosjektet i OpenShift-klyngen, {OKD-WEBUI-URL} er URL-en til OpenShift-nettkonsollen ) - https://{OKD-WEBUI-URL}/console /project/{project}/browse/images/{image-name}.

For å kjøre oppgaver må det opprettes en tjenestekonto med rettighetene til å kjøre pods som root (vi vil diskutere dette punktet senere):

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

La oss kjøre spark-submit-kommandoen for å publisere en Spark-oppgave til OKD-klyngen, og spesifisere den opprettede tjenestekontoen og Docker-bildet:

 /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

her:

—navn — navnet på oppgaven som vil delta i dannelsen av navnet på Kubernetes-bekkene;

—klasse — klasse til den kjørbare filen, kalt når oppgaven starter;

—conf — Spark-konfigurasjonsparametere;

spark.executor.instances — antall Spark-utøvere som skal lanseres;

spark.kubernetes.authenticate.driver.serviceAccountName - navnet på Kubernetes-tjenestekontoen som brukes ved oppstart av pods (for å definere sikkerhetskonteksten og funksjonene når du samhandler med Kubernetes API);

spark.kubernetes.namespace — Kubernetes navneområde der driver- og eksekveringsmoduler vil bli lansert;

spark.submit.deployMode — metode for å starte Spark (for standard gnist-send brukes "cluster", for Spark Operator og senere versjoner av Spark "klient");

spark.kubernetes.container.image - Docker-bilde som brukes til å starte pods;

spark.master — Kubernetes API URL (ekstern er spesifisert slik at tilgang skjer fra den lokale maskinen);

local:// er banen til den kjørbare Spark-filen inne i Docker-bildet.

Vi går til det tilsvarende OKD-prosjektet og studerer de opprettede podene - https://{OKD-WEBUI-URL}/console/project/{project}/browse/pods.

For å forenkle utviklingsprosessen, kan et annet alternativ brukes, der et felles basisbilde av Spark opprettes, som brukes av alle oppgavene til å kjøre, og øyeblikksbilder av kjørbare filer publiseres til ekstern lagring (for eksempel Hadoop) og spesifiseres når du ringer spark-send inn som en lenke. I dette tilfellet kan du kjøre forskjellige versjoner av Spark-oppgaver uten å gjenoppbygge Docker-bilder, ved å bruke for eksempel WebHDFS for å publisere bilder. Vi sender en forespørsel om å opprette en fil (her er {host} verten for WebHDFS-tjenesten, {port} er porten til WebHDFS-tjenesten, {path-to-file-on-hdfs} er den ønskede banen til filen på HDFS):

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

Du vil motta et svar som dette (her {location} er nettadressen som må brukes for å laste ned filen):

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

Last inn den kjørbare Spark-filen til HDFS (her er {path-to-local-file} banen til den kjørbare Spark-filen på den gjeldende verten):

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

Etter dette kan vi gjøre spark-submit ved å bruke Spark-filen lastet opp til HDFS (her {class-name} er navnet på klassen som må startes for å fullføre oppgaven):

/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}

Det skal bemerkes at for å få tilgang til HDFS og sikre at oppgaven fungerer, kan det hende du må endre Dockerfile og entrypoint.sh-skriptet - legg til et direktiv til Dockerfile for å kopiere avhengige biblioteker til /opt/spark/jars-katalogen og inkludere HDFS-konfigurasjonsfilen i SPARK_CLASSPATH i inngangspunkt. sh.

Andre bruksboks - Apache Livy

Videre, når en oppgave er utviklet og resultatet må testes, oppstår spørsmålet om å starte den som en del av CI/CD-prosessen og spore statusen for utføringen av den. Selvfølgelig kan du kjøre det ved å bruke et lokalt spark-submit-kall, men dette kompliserer CI/CD-infrastrukturen siden det krever installasjon og konfigurering av Spark på CI-serveragentene/løperne og konfigurering av tilgang til Kubernetes API. For dette tilfellet har målimplementeringen valgt å bruke Apache Livy som et REST API for å kjøre Spark-oppgaver som er vert inne i en Kubernetes-klynge. Med dens hjelp kan du kjøre Spark-oppgaver på en Kubernetes-klynge ved å bruke vanlige cURL-forespørsler, som enkelt implementeres basert på enhver CI-løsning, og plasseringen i Kubernetes-klyngen løser problemet med autentisering når du samhandler med Kubernetes API.

Kjører Apache Spark på Kubernetes

La oss fremheve det som en andre brukssak – å kjøre Spark-oppgaver som en del av en CI/CD-prosess på en Kubernetes-klynge i en testsløyfe.

Litt om Apache Livy - den fungerer som en HTTP-server som gir et webgrensesnitt og en RESTful API som lar deg fjernstarte spark-submit ved å sende de nødvendige parameterne. Tradisjonelt har den blitt sendt som en del av en HDP-distribusjon, men kan også distribueres til OKD eller en hvilken som helst annen Kubernetes-installasjon ved å bruke det riktige manifestet og et sett med Docker-bilder, for eksempel denne - github.com/ttauveron/k8s-big-data-experiments/tree/master/livy-spark-2.3. For vårt tilfelle ble et lignende Docker-bilde bygget, inkludert Spark versjon 2.4.5 fra følgende 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"]

Det genererte bildet kan bygges og lastes opp til ditt eksisterende Docker-depot, for eksempel det interne OKD-depotet. For å distribuere det, bruk følgende manifest ({registry-url} - URL til Docker-bilderegisteret, {image-name} - Docker-bildenavn, {tag} - Docker-bildetag, {livy-url} - ønsket nettadresse der serveren vil være tilgjengelig Livy; "Rute"-manifestet brukes hvis Red Hat OpenShift brukes som Kubernetes-distribusjon, ellers brukes det tilsvarende Ingress- eller Service-manifestet av typen NodePort):

---
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

Etter å ha brukt den og vellykket lansering av poden, er Livy grafiske grensesnitt tilgjengelig på lenken: http://{livy-url}/ui. Med Livy kan vi publisere Spark-oppgaven vår ved å bruke en REST-forespørsel fra for eksempel Postman. Et eksempel på en samling med forespørsler er presentert nedenfor (konfigurasjonsargumenter med variabler som er nødvendige for driften av den lanserte oppgaven kan sendes i "args"-matrisen):

{
    "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": {}
}

La oss utføre den første forespørselen fra samlingen, gå til OKD-grensesnittet og sjekke at oppgaven har blitt lansert - https://{OKD-WEBUI-URL}/console/project/{project}/browse/pods. Samtidig vil en økt vises i Livy-grensesnittet (http://{livy-url}/ui), der du ved å bruke Livy API eller grafisk grensesnitt kan spore fremdriften til oppgaven og studere økten tømmerstokker.

La oss nå vise hvordan Livy fungerer. For å gjøre dette, la oss undersøke loggene til Livy-beholderen inne i poden med Livy-serveren - https://{OKD-WEBUI-URL}/console/project/{project}/browse/pods/{livy-pod-name }?tab=logger. Fra dem kan vi se at når du kaller Livy REST API i en beholder kalt "livy", blir en spark-submit utført, lik den vi brukte ovenfor (her er {livy-pod-name} navnet på den opprettede poden med Livy-serveren). Samlingen introduserer også en andre spørring som lar deg kjøre oppgaver som eksternt er vert for en Spark-kjørbar fil ved hjelp av en Livy-server.

Tredje brukstilfelle - Spark Operator

Nå som oppgaven er testet, oppstår spørsmålet om å kjøre den regelmessig. Den opprinnelige måten å regelmessig kjøre oppgaver i en Kubernetes-klynge er CronJob-enheten, og du kan bruke den, men for øyeblikket er bruken av operatører til å administrere applikasjoner i Kubernetes veldig populær, og for Spark er det en ganske moden operatør, som også er brukes i løsninger på bedriftsnivå (for eksempel Lightbend FastData Platform). Vi anbefaler å bruke den - den nåværende stabile versjonen av Spark (2.4.5) har ganske begrensede konfigurasjonsmuligheter for å kjøre Spark-oppgaver i Kubernetes, mens den neste hovedversjonen (3.0.0) erklærer full støtte for Kubernetes, men utgivelsesdatoen forblir ukjent . Spark Operator kompenserer for denne mangelen ved å legge til viktige konfigurasjonsalternativer (for eksempel å montere en ConfigMap med Hadoop-tilgangskonfigurasjon til Spark-pods) og muligheten til å kjøre en regelmessig planlagt oppgave.

Kjører Apache Spark på Kubernetes
La oss fremheve det som et tredje bruksområde – regelmessig kjører Spark-oppgaver på en Kubernetes-klynge i en produksjonssløyfe.

Spark Operator er åpen kildekode og utviklet innenfor Google Cloud Platform - github.com/GoogleCloudPlatform/spark-on-k8s-operator. Installasjonen kan gjøres på 3 måter:

  1. Som en del av Lightbend FastData Platform/Cloudflow-installasjonen;
  2. Bruke Helm:
    helm repo add incubator http://storage.googleapis.com/kubernetes-charts-incubator
    helm install incubator/sparkoperator --namespace spark-operator
    	

  3. Bruke manifester fra det offisielle depotet (https://github.com/GoogleCloudPlatform/spark-on-k8s-operator/tree/master/manifest). Det er verdt å merke seg følgende - Cloudflow inkluderer en operatør med API-versjon v1beta1. Hvis denne typen installasjon brukes, bør Spark-applikasjonsmanifestbeskrivelser være basert på eksempelkoder i Git med riktig API-versjon, for eksempel "v1beta1-0.9.0-2.4.0". Versjonen av operatøren kan finnes i beskrivelsen av CRD inkludert i operatøren i "versjoner"-ordboken:
    oc get crd sparkapplications.sparkoperator.k8s.io -o yaml
    	

Hvis operatøren er riktig installert, vil en aktiv pod med Spark-operatøren vises i det tilsvarende prosjektet (for eksempel cloudflow-fdp-sparkoperator i Cloudflow-området for Cloudflow-installasjonen) og en tilsvarende Kubernetes-ressurstype kalt "sparkapplications" vises . Du kan utforske tilgjengelige Spark-applikasjoner med følgende kommando:

oc get sparkapplications -n {project}

For å kjøre oppgaver med Spark Operator må du gjøre 3 ting:

  • lage et Docker-bilde som inkluderer alle nødvendige biblioteker, samt konfigurasjons- og kjørbare filer. I målbildet er dette et bilde laget på CI/CD-stadiet og testet på en testklynge;
  • publisere et Docker-bilde til et register tilgjengelig fra Kubernetes-klyngen;
  • generere et manifest med typen "SparkApplication" og en beskrivelse av oppgaven som skal lanseres. Eksempelmanifester er tilgjengelige i det offisielle depotet (f.eks. github.com/GoogleCloudPlatform/spark-on-k8s-operator/blob/v1beta1-0.9.0-2.4.0/examples/spark-pi.yaml). Det er viktige punkter å merke seg om manifestet:
    1. "apiVersion"-ordboken må angi API-versjonen som tilsvarer operatørversjonen;
    2. «metadata.namespace»-ordboken må angi navneområdet der applikasjonen skal startes;
    3. "spec.image"-ordboken må inneholde adressen til det opprettede Docker-bildet i et tilgjengelig register;
    4. "spec.mainClass"-ordboken må inneholde Spark-oppgaveklassen som må kjøres når prosessen starter;
    5. "spec.mainApplicationFile"-ordboken må inneholde banen til den kjørbare jar-filen;
    6. "spec.sparkVersion"-ordboken må angi hvilken versjon av Spark som brukes;
    7. «spec.driver.serviceAccount»-ordboken må spesifisere tjenestekontoen innenfor det tilsvarende Kubernetes-navneområdet som skal brukes til å kjøre programmet;
    8. «spec.executor»-ordboken må angi antall ressurser som er tildelt applikasjonen;
    9. "spec.volumeMounts"-ordboken må spesifisere den lokale katalogen der de lokale Spark-oppgavefilene skal opprettes.

Et eksempel på generering av et manifest (her er {spark-service-account} en tjenestekonto inne i Kubernetes-klyngen for å kjøre Spark-oppgaver):

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"

Dette manifestet spesifiserer en tjenestekonto som du, før du publiserer manifestet, må opprette de nødvendige rollebindingene som gir de nødvendige tilgangsrettighetene for at Spark-applikasjonen kan samhandle med Kubernetes API (om nødvendig). I vårt tilfelle trenger applikasjonen rettigheter for å lage Pods. La oss lage den nødvendige rollebindingen:

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

Det er også verdt å merke seg at denne manifestspesifikasjonen kan inneholde en "hadoopConfigMap"-parameter, som lar deg spesifisere et ConfigMap med Hadoop-konfigurasjonen uten å først plassere den tilsvarende filen i Docker-bildet. Den er også egnet for å kjøre oppgaver regelmessig - ved å bruke parameteren "planlegging", kan en tidsplan for å kjøre en gitt oppgave spesifiseres.

Etter det lagrer vi manifestet vårt i spark-pi.yaml-filen og bruker det på Kubernetes-klyngen vår:

oc apply -f spark-pi.yaml

Dette vil lage et objekt av typen "sparkapplications":

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

I dette tilfellet vil det bli opprettet en pod med en applikasjon, hvis status vil vises i de opprettede "sparkapplications". Du kan se den med følgende kommando:

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

Når oppgaven er fullført, vil POD flytte til "Fullført"-statusen, som også vil oppdateres i "sparkapplications". Applikasjonslogger kan vises i nettleseren eller ved å bruke følgende kommando (her er {sparkapplications-pod-name} navnet på poden til den kjørende oppgaven):

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

Spark-oppgaver kan også administreres ved å bruke det spesialiserte sparkctl-verktøyet. For å installere det, klone depotet med kildekoden, installer Go og bygg dette verktøyet:

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

La oss undersøke listen over kjørende Spark-oppgaver:

sparkctl list -n {project}

La oss lage en beskrivelse for Spark-oppgaven:

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"

La oss kjøre den beskrevne oppgaven ved å bruke sparkctl:

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

La oss undersøke listen over kjørende Spark-oppgaver:

sparkctl list -n {project}

La oss undersøke listen over hendelser for en lansert Spark-oppgave:

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

La oss undersøke statusen til den løpende Spark-oppgaven:

sparkctl status spark-pi -n {project}

Avslutningsvis vil jeg vurdere de oppdagede ulempene ved å bruke den nåværende stabile versjonen av Spark (2.4.5) i Kubernetes:

  1. Den første og kanskje største ulempen er mangelen på datalokalitet. Til tross for alle manglene til YARN, var det også fordeler ved å bruke det, for eksempel prinsippet om å levere kode til data (i stedet for data til kode). Takket være det ble Spark-oppgaver utført på nodene der dataene som var involvert i beregningene var lokalisert, og tiden det tok å levere data over nettverket ble betydelig redusert. Når vi bruker Kubernetes, står vi overfor behovet for å flytte data involvert i en oppgave over nettverket. Hvis de er store nok, kan oppgaveutførelsestiden øke betydelig, og også kreve en ganske stor mengde diskplass tildelt Spark-oppgaveforekomster for deres midlertidige lagring. Denne ulempen kan reduseres ved å bruke spesialisert programvare som sikrer datalokalitet i Kubernetes (for eksempel Alluxio), men dette betyr faktisk behovet for å lagre en fullstendig kopi av dataene på nodene til Kubernetes-klyngen.
  2. Den andre viktige ulempen er sikkerhet. Som standard er sikkerhetsrelaterte funksjoner for å kjøre Spark-oppgaver deaktivert, bruken av Kerberos er ikke dekket i den offisielle dokumentasjonen (selv om de tilsvarende alternativene ble introdusert i versjon 3.0.0, som vil kreve ekstra arbeid), og sikkerhetsdokumentasjonen for ved bruk av Spark (https ://spark.apache.org/docs/2.4.5/security.html) vises bare YARN, Mesos og Standalone Cluster som nøkkelbutikker. Samtidig kan brukeren som Spark-oppgaver startes under ikke spesifiseres direkte - vi spesifiserer kun tjenestekontoen den skal fungere under, og brukeren velges basert på de konfigurerte sikkerhetspolicyene. I denne forbindelse brukes enten rotbrukeren, som ikke er trygg i et produktivt miljø, eller en bruker med en tilfeldig UID, noe som er upraktisk ved distribusjon av tilgangsrettigheter til data (dette kan løses ved å lage PodSecurityPolicies og koble dem til tilsvarende tjenestekontoer). Foreløpig er løsningen enten å plassere alle nødvendige filer direkte inn i Docker-bildet, eller modifisere Spark-lanseringsskriptet for å bruke mekanismen for å lagre og hente hemmeligheter som er tatt i bruk i organisasjonen din.
  3. Å kjøre Spark-jobber med Kubernetes er offisielt fortsatt i eksperimentell modus, og det kan være betydelige endringer i artefaktene som brukes (konfigurasjonsfiler, Docker-basebilder og lanseringsskript) i fremtiden. Og faktisk, når du utarbeidet materialet, ble versjoner 2.3.0 og 2.4.5 testet, oppførselen var betydelig forskjellig.

La oss vente på oppdateringer - en ny versjon av Spark (3.0.0) ble nylig utgitt, som brakte betydelige endringer i arbeidet til Spark på Kubernetes, men beholdt den eksperimentelle statusen for støtte for denne ressursbehandleren. Kanskje vil de neste oppdateringene virkelig gjøre det mulig å fullt ut anbefale å forlate YARN og kjøre Spark-oppgaver på Kubernetes uten frykt for sikkerheten til systemet ditt og uten å måtte endre funksjonelle komponenter uavhengig.

Fin.

Kilde: www.habr.com

Legg til en kommentar