Menjalankan Apache Spark di Kubernetes

Pembaca yang dihormati, selamat petang. Hari ini kita akan bercakap sedikit tentang Apache Spark dan prospek pembangunannya.

Menjalankan Apache Spark di Kubernetes

Dalam dunia Data Besar moden, Apache Spark ialah piawaian de facto untuk membangunkan tugas pemprosesan data kelompok. Selain itu, ia juga digunakan untuk mencipta aplikasi penstriman yang berfungsi dalam konsep kumpulan mikro, pemprosesan dan penghantaran data dalam bahagian kecil (Spark Structured Streaming). Dan secara tradisinya ia telah menjadi sebahagian daripada timbunan Hadoop keseluruhan, menggunakan YARN (atau dalam beberapa kes Apache Mesos) sebagai pengurus sumber. Menjelang 2020, penggunaannya dalam bentuk tradisional menjadi persoalan bagi kebanyakan syarikat kerana kekurangan pengedaran Hadoop yang baik - pembangunan HDP dan CDH telah berhenti, CDH tidak dibangunkan dengan baik dan mempunyai kos yang tinggi, dan pembekal Hadoop yang tinggal telah sama ada tidak wujud atau mempunyai masa depan yang malap. Oleh itu, pelancaran Apache Spark menggunakan Kubernetes semakin menarik minat masyarakat dan syarikat besar - menjadi standard dalam orkestrasi kontena dan pengurusan sumber dalam awan peribadi dan awam, ia menyelesaikan masalah dengan penjadualan sumber yang menyusahkan tugas Spark pada YARN dan menyediakan platform yang terus berkembang dengan banyak pengedaran komersial dan terbuka untuk syarikat dari semua saiz dan jalur. Di samping itu, berikutan populariti, kebanyakannya telah berjaya memperoleh beberapa pemasangan mereka sendiri dan telah meningkatkan kepakaran mereka dalam penggunaannya, yang memudahkan langkah itu.

Bermula dengan versi 2.3.0, Apache Spark memperoleh sokongan rasmi untuk menjalankan tugas dalam kelompok Kubernetes dan hari ini, kita akan bercakap tentang kematangan semasa pendekatan ini, pelbagai pilihan untuk penggunaannya dan perangkap yang akan dihadapi semasa pelaksanaan.

Pertama sekali, mari kita lihat proses membangunkan tugasan dan aplikasi berdasarkan Apache Spark dan menyerlahkan kes biasa di mana anda perlu menjalankan tugas pada gugusan Kubernetes. Dalam menyediakan siaran ini, OpenShift digunakan sebagai pengedaran dan arahan yang berkaitan dengan utiliti baris arahannya (oc) akan diberikan. Untuk pengedaran Kubernetes lain, arahan yang sepadan daripada utiliti baris arahan Kubernetes standard (kubectl) atau analognya (contohnya, untuk dasar oc adm) boleh digunakan.

Kes penggunaan pertama - serah percikan

Semasa pembangunan tugasan dan aplikasi, pembangun perlu menjalankan tugas untuk menyahpepijat transformasi data. Secara teorinya, stub boleh digunakan untuk tujuan ini, tetapi pembangunan dengan penyertaan contoh sebenar (walaupun ujian) sistem akhir telah terbukti lebih pantas dan lebih baik dalam kelas tugasan ini. Dalam kes apabila kita nyahpepijat pada contoh sebenar sistem penamat, dua senario adalah mungkin:

  • pembangun menjalankan tugas Spark secara tempatan dalam mod kendiri;

    Menjalankan Apache Spark di Kubernetes

  • pembangun menjalankan tugas Spark pada gugusan Kubernetes dalam gelung ujian.

    Menjalankan Apache Spark di Kubernetes

Pilihan pertama mempunyai hak untuk wujud, tetapi memerlukan beberapa kelemahan:

  • Setiap pembangun mesti diberikan akses dari tempat kerja kepada semua keadaan sistem akhir yang dia perlukan;
  • jumlah sumber yang mencukupi diperlukan pada mesin kerja untuk menjalankan tugas yang sedang dibangunkan.

Pilihan kedua tidak mempunyai kelemahan ini, kerana penggunaan kluster Kubernetes membolehkan anda memperuntukkan kumpulan sumber yang diperlukan untuk menjalankan tugas dan memberikannya akses yang diperlukan untuk menamatkan contoh sistem, secara fleksibel menyediakan akses kepadanya menggunakan model peranan Kubernetes untuk semua ahli pasukan pembangunan. Mari kita serlahkannya sebagai kes penggunaan pertama - melancarkan tugas Spark daripada mesin pembangun tempatan pada gugusan Kubernetes dalam litar ujian.

Mari bercakap lebih lanjut tentang proses menyediakan Spark untuk dijalankan secara tempatan. Untuk mula menggunakan Spark anda perlu memasangnya:

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

Kami mengumpul pakej yang diperlukan untuk bekerja dengan Kubernetes:

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

Binaan penuh memerlukan banyak masa, dan untuk mencipta imej Docker dan menjalankannya pada kluster Kubernetes, anda benar-benar hanya memerlukan fail jar daripada direktori "himpunan/", jadi anda hanya boleh membina subprojek ini:

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

Untuk menjalankan kerja Spark pada Kubernetes, anda perlu mencipta imej Docker untuk digunakan sebagai imej asas. Terdapat 2 pendekatan yang mungkin di sini:

  • Imej Docker yang dihasilkan termasuk kod tugas Spark boleh laku;
  • Imej yang dicipta hanya termasuk Spark dan kebergantungan yang diperlukan, kod boleh laku dihoskan dari jauh (contohnya, dalam HDFS).

Mula-mula, mari kita bina imej Docker yang mengandungi contoh ujian tugas Spark. Untuk mencipta imej Docker, Spark mempunyai utiliti yang dipanggil "docker-image-tool". Mari kita kaji bantuan mengenainya:

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

Dengan bantuannya, anda boleh membuat imej Docker dan memuat naiknya ke pendaftaran jauh, tetapi secara lalai ia mempunyai beberapa kelemahan:

  • tanpa gagal mencipta 3 imej Docker sekaligus - untuk Spark, PySpark dan R;
  • tidak membenarkan anda menentukan nama imej.

Oleh itu, kami akan menggunakan versi ubah suai utiliti ini yang diberikan di bawah:

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

Dengan bantuannya, kami mengumpulkan imej Spark asas yang mengandungi tugas ujian untuk mengira Pi menggunakan Spark (di sini {docker-registry-url} ialah URL pendaftaran imej Docker anda, {repo} ialah nama repositori di dalam pendaftaran, yang sepadan dengan projek dalam OpenShift , {image-name} - nama imej (jika pemisahan tiga peringkat imej digunakan, contohnya, seperti dalam pendaftaran bersepadu imej Red Hat OpenShift), {tag} - tag ini versi imej):

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

Log masuk ke kluster OKD menggunakan utiliti konsol (di sini {OKD-API-URL} ialah URL API kluster OKD):

oc login {OKD-API-URL}

Mari dapatkan token pengguna semasa untuk kebenaran dalam Pendaftaran Docker:

oc whoami -t

Log masuk ke Pendaftaran Docker dalaman kluster OKD (kami menggunakan token yang diperoleh menggunakan arahan sebelumnya sebagai kata laluan):

docker login {docker-registry-url}

Mari muat naik imej Docker yang dipasang ke OKD Docker Registry:

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

Mari semak sama ada imej yang dipasang tersedia dalam OKD. Untuk melakukan ini, buka URL dalam penyemak imbas dengan senarai imej projek yang sepadan (di sini {project} ialah nama projek di dalam kelompok OpenShift, {OKD-WEBUI-URL} ialah URL konsol Web OpenShift ) - https://{OKD-WEBUI-URL}/console /project/{project}/browse/images/{image-name}.

Untuk menjalankan tugas, akaun perkhidmatan mesti dibuat dengan keistimewaan untuk menjalankan pod sebagai root (kita akan membincangkan perkara ini kemudian):

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

Mari jalankan perintah serah percikan untuk menerbitkan tugas Spark ke kelompok OKD, dengan menyatakan akaun perkhidmatan yang dibuat dan imej Docker:

 /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

Di sini:

β€”nama β€” nama tugas yang akan mengambil bahagian dalam pembentukan nama pod Kubernetes;

β€”class β€” kelas fail boleh laku, dipanggil apabila tugas bermula;

β€”conf β€” Parameter konfigurasi percikan;

spark.executor.instances β€” bilangan pelaksana Spark untuk dilancarkan;

spark.kubernetes.authenticate.driver.serviceAccountName - nama akaun perkhidmatan Kubernetes yang digunakan semasa melancarkan pod (untuk menentukan konteks dan keupayaan keselamatan semasa berinteraksi dengan API Kubernetes);

spark.kubernetes.namespace β€” Ruang nama Kubernetes di mana pod pemacu dan pelaksana akan dilancarkan;

spark.submit.deployMode β€” kaedah melancarkan Spark (untuk "kluster" penghantaran percikan standard digunakan, untuk Pengendali Spark dan versi "klien" Spark yang lebih baru);

spark.kubernetes.container.image - Imej docker digunakan untuk melancarkan pod;

spark.master β€” URL API Kubernetes (luaran ditentukan supaya akses berlaku daripada mesin tempatan);

local:// ialah laluan ke Spark boleh laku di dalam imej Docker.

Kami pergi ke projek OKD yang sepadan dan mengkaji pod yang dibuat - https://{OKD-WEBUI-URL}/console/project/{project}/browse/pods.

Untuk memudahkan proses pembangunan, pilihan lain boleh digunakan, di mana imej asas biasa Spark dicipta, digunakan oleh semua tugas untuk dijalankan, dan syot kilat fail boleh laku diterbitkan ke storan luaran (contohnya, Hadoop) dan dinyatakan semasa memanggil percikan-serahkan sebagai pautan. Dalam kes ini, anda boleh menjalankan versi tugas Spark yang berbeza tanpa membina semula imej Docker, menggunakan, sebagai contoh, WebHDFS untuk menerbitkan imej. Kami menghantar permintaan untuk mencipta fail (di sini {host} ialah hos perkhidmatan WebHDFS, {port} ialah port perkhidmatan WebHDFS, {path-to-file-on-hdfs} ialah laluan yang dikehendaki ke fail pada HDFS):

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

Anda akan menerima respons seperti ini (di sini {location} ialah URL yang perlu digunakan untuk memuat turun fail):

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

Muatkan fail boleh laku Spark ke dalam HDFS (di sini {path-to-local-file} ialah laluan ke fail boleh laku Spark pada hos semasa):

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

Selepas ini, kita boleh melakukan penyerahan percikan menggunakan fail Spark yang dimuat naik ke HDFS (di sini {class-name} ialah nama kelas yang perlu dilancarkan untuk menyelesaikan tugasan):

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

Perlu diingatkan bahawa untuk mengakses HDFS dan memastikan tugasan berfungsi, anda mungkin perlu menukar Dockerfile dan skrip entrypoint.sh - tambah arahan pada Dockerfile untuk menyalin perpustakaan bergantung ke direktori /opt/spark/jars dan sertakan fail konfigurasi HDFS dalam SPARK_CLASSPATH dalam titik masuk. sh.

Kes penggunaan kedua - Apache Livy

Selanjutnya, apabila tugasan dibangunkan dan hasilnya perlu diuji, timbul persoalan untuk melancarkannya sebagai sebahagian daripada proses CI/CD dan menjejaki status pelaksanaannya. Sudah tentu, anda boleh menjalankannya menggunakan panggilan hantar percikan tempatan, tetapi ini merumitkan infrastruktur CI/CD kerana ia memerlukan pemasangan dan konfigurasi Spark pada ejen/pelari pelayan CI dan menyediakan akses kepada API Kubernetes. Untuk kes ini, pelaksanaan sasaran telah memilih untuk menggunakan Apache Livy sebagai API REST untuk menjalankan tugas Spark yang dihoskan dalam kelompok Kubernetes. Dengan bantuannya, anda boleh menjalankan tugas Spark pada gugusan Kubernetes menggunakan permintaan cURL biasa, yang mudah dilaksanakan berdasarkan mana-mana penyelesaian CI dan peletakannya di dalam gugusan Kubernetes menyelesaikan isu pengesahan apabila berinteraksi dengan API Kubernetes.

Menjalankan Apache Spark di Kubernetes

Mari kita serlahkannya sebagai kes penggunaan kedua - menjalankan tugas Spark sebagai sebahagian daripada proses CI/CD pada gugusan Kubernetes dalam gelung ujian.

Sedikit tentang Apache Livy - ia berfungsi sebagai pelayan HTTP yang menyediakan antara muka Web dan API RESTful yang membolehkan anda melancarkan penyerahan percikan dari jauh dengan melepasi parameter yang diperlukan. Secara tradisinya ia telah dihantar sebagai sebahagian daripada pengedaran HDP, tetapi juga boleh digunakan ke OKD atau mana-mana pemasangan Kubernetes lain menggunakan manifes yang sesuai dan satu set imej Docker, seperti yang ini - github.com/ttauveron/k8s-big-data-experiments/tree/master/livy-spark-2.3. Untuk kes kami, imej Docker yang serupa telah dibina, termasuk Spark versi 2.4.5 daripada Dockerfile berikut:

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

Imej yang dijana boleh dibina dan dimuat naik ke repositori Docker sedia ada anda, seperti repositori OKD dalaman. Untuk melaksanakannya, gunakan manifes berikut ({registry-url} - URL pendaftaran imej Docker, {image-name} - Nama imej Docker, {tag} - Tag imej Docker, {livy-url} - URL yang dikehendaki di mana pelayan akan dapat diakses Livy; manifes "Laluan" digunakan jika Red Hat OpenShift digunakan sebagai pengedaran Kubernetes, jika tidak manifes Ingress atau Perkhidmatan yang sepadan jenis NodePort digunakan):

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

Selepas menggunakannya dan berjaya melancarkan pod, antara muka grafik Livy tersedia di pautan: http://{livy-url}/ui. Dengan Livy, kami boleh menerbitkan tugas Spark kami menggunakan permintaan REST daripada, contohnya, Posman. Contoh koleksi dengan permintaan dibentangkan di bawah (argumen konfigurasi dengan pembolehubah yang diperlukan untuk pengendalian tugas yang dilancarkan boleh dihantar dalam tatasusunan "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": {}
}

Mari laksanakan permintaan pertama daripada koleksi, pergi ke antara muka OKD dan semak sama ada tugasan telah berjaya dilancarkan - https://{OKD-WEBUI-URL}/console/project/{project}/browse/pods. Pada masa yang sama, sesi akan muncul dalam antara muka Livy (http://{livy-url}/ui), di mana, menggunakan API Livy atau antara muka grafik, anda boleh menjejak kemajuan tugas dan mengkaji sesi balak.

Sekarang mari tunjukkan cara Livy berfungsi. Untuk melakukan ini, mari kita periksa log bekas Livy di dalam pod dengan pelayan Livy - https://{OKD-WEBUI-URL}/console/project/{project}/browse/pods/{livy-pod-name }?tab=log. Daripada mereka, kita dapat melihat bahawa apabila memanggil Livy REST API dalam bekas bernama "livy", penyerahan percikan dilaksanakan, serupa dengan yang kami gunakan di atas (di sini {livy-pod-name} ialah nama pod yang dibuat dengan pelayan Livy). Koleksi ini juga memperkenalkan pertanyaan kedua yang membolehkan anda menjalankan tugas yang menjadi hos dari jauh Spark boleh laku menggunakan pelayan Livy.

Kes penggunaan ketiga - Spark Operator

Sekarang tugas itu telah diuji, persoalan menjalankannya dengan kerap timbul. Cara asli untuk menjalankan tugas secara kerap dalam kelompok Kubernetes ialah entiti CronJob dan anda boleh menggunakannya, tetapi pada masa ini penggunaan operator untuk mengurus aplikasi dalam Kubernetes sangat popular dan untuk Spark terdapat pengendali yang agak matang, yang juga digunakan dalam penyelesaian peringkat Perusahaan (contohnya, Lightbend FastData Platform). Kami mengesyorkan menggunakannya - versi stabil semasa Spark (2.4.5) mempunyai pilihan konfigurasi yang agak terhad untuk menjalankan tugas Spark dalam Kubernetes, manakala versi utama seterusnya (3.0.0) mengisytiharkan sokongan penuh untuk Kubernetes, tetapi tarikh keluarannya masih tidak diketahui . Operator Spark mengimbangi kekurangan ini dengan menambahkan pilihan konfigurasi penting (contohnya, memasang ConfigMap dengan konfigurasi akses Hadoop ke pod Spark) dan keupayaan untuk menjalankan tugas yang dijadualkan secara tetap.

Menjalankan Apache Spark di Kubernetes
Mari kita serlahkannya sebagai kes penggunaan ketiga - kerap menjalankan tugas Spark pada gugusan Kubernetes dalam gelung pengeluaran.

Spark Operator ialah sumber terbuka dan dibangunkan dalam Platform Awan Google - github.com/GoogleCloudPlatform/spark-on-k8s-operator. Pemasangannya boleh dilakukan dalam 3 cara:

  1. Sebagai sebahagian daripada pemasangan Lightbend FastData Platform/Cloudflow;
  2. Menggunakan Helm:
    helm repo add incubator http://storage.googleapis.com/kubernetes-charts-incubator
    helm install incubator/sparkoperator --namespace spark-operator
    	

  3. Menggunakan manifes daripada repositori rasmi (https://github.com/GoogleCloudPlatform/spark-on-k8s-operator/tree/master/manifest). Perlu diperhatikan perkara berikut - Cloudflow termasuk pengendali dengan versi API v1beta1. Jika jenis pemasangan ini digunakan, penerangan manifes aplikasi Spark hendaklah berdasarkan contoh teg dalam Git dengan versi API yang sesuai, contohnya, "v1beta1-0.9.0-2.4.0". Versi pengendali boleh didapati dalam perihalan CRD yang disertakan dalam operator dalam kamus "versi":
    oc get crd sparkapplications.sparkoperator.k8s.io -o yaml
    	

Jika operator dipasang dengan betul, pod aktif dengan operator Spark akan muncul dalam projek yang sepadan (contohnya, cloudflow-fdp-sparkoperator dalam ruang Cloudflow untuk pemasangan Cloudflow) dan jenis sumber Kubernetes yang sepadan bernama "sparkapplications" akan muncul. . Anda boleh meneroka aplikasi Spark yang tersedia dengan arahan berikut:

oc get sparkapplications -n {project}

Untuk menjalankan tugas menggunakan Spark Operator anda perlu melakukan 3 perkara:

  • buat imej Docker yang merangkumi semua perpustakaan yang diperlukan, serta konfigurasi dan fail boleh laku. Dalam gambar sasaran, ini ialah imej yang dicipta pada peringkat CI/CD dan diuji pada kluster ujian;
  • menerbitkan imej Docker ke pendaftaran yang boleh diakses daripada gugusan Kubernetes;
  • jana manifes dengan jenis "SparkApplication" dan penerangan tentang tugas yang akan dilancarkan. Contoh manifes tersedia dalam repositori rasmi (cth. github.com/GoogleCloudPlatform/spark-on-k8s-operator/blob/v1beta1-0.9.0-2.4.0/examples/spark-pi.yaml). Terdapat perkara penting yang perlu diberi perhatian mengenai manifesto:
    1. kamus "apiVersion" mesti menunjukkan versi API yang sepadan dengan versi pengendali;
    2. kamus "metadata.namespace" mesti menunjukkan ruang nama di mana aplikasi akan dilancarkan;
    3. kamus "spec.image" mesti mengandungi alamat imej Docker yang dibuat dalam pendaftaran yang boleh diakses;
    4. kamus "spec.mainClass" mesti mengandungi kelas tugas Spark yang perlu dijalankan apabila proses bermula;
    5. kamus "spec.mainApplicationFile" mesti mengandungi laluan ke fail jar boleh laku;
    6. kamus "spec.sparkVersion" mesti menunjukkan versi Spark yang digunakan;
    7. kamus "spec.driver.serviceAccount" mesti menyatakan akaun perkhidmatan dalam ruang nama Kubernetes yang sepadan yang akan digunakan untuk menjalankan aplikasi;
    8. kamus "spec.executor" mesti menunjukkan bilangan sumber yang diperuntukkan kepada aplikasi;
    9. kamus "spec.volumeMounts" mesti menentukan direktori tempatan di mana fail tugas Spark tempatan akan dibuat.

Contoh menjana manifes (di sini {spark-service-account} ialah akaun perkhidmatan dalam kelompok Kubernetes untuk menjalankan tugas 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"

Manifes ini menentukan akaun perkhidmatan yang mana, sebelum menerbitkan manifes, anda mesti membuat pengikatan peranan yang diperlukan yang menyediakan hak akses yang diperlukan untuk aplikasi Spark untuk berinteraksi dengan API Kubernetes (jika perlu). Dalam kes kami, aplikasi memerlukan hak untuk mencipta Pod. Mari kita cipta pengikatan peranan yang diperlukan:

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

Perlu juga diperhatikan bahawa spesifikasi manifes ini mungkin termasuk parameter "hadoopConfigMap", yang membolehkan anda menentukan ConfigMap dengan konfigurasi Hadoop tanpa perlu meletakkan fail yang sepadan terlebih dahulu dalam imej Docker. Ia juga sesuai untuk menjalankan tugas dengan kerap - menggunakan parameter "jadual", jadual untuk menjalankan tugas tertentu boleh ditentukan.

Selepas itu, kami menyimpan manifes kami pada fail spark-pi.yaml dan menggunakannya pada gugusan Kubernetes kami:

oc apply -f spark-pi.yaml

Ini akan mencipta objek jenis "sparkapplications":

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

Dalam kes ini, pod dengan aplikasi akan dibuat, statusnya akan dipaparkan dalam "sparkapplications" yang dibuat. Anda boleh melihatnya dengan arahan berikut:

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

Setelah selesai tugasan, POD akan beralih ke status "Selesai", yang juga akan dikemas kini dalam "sparkapplications". Log aplikasi boleh dilihat dalam penyemak imbas atau menggunakan arahan berikut (di sini {sparkapplications-pod-name} ialah nama pod tugas yang sedang dijalankan):

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

Tugas percikan juga boleh diurus menggunakan utiliti sparkctl khusus. Untuk memasangnya, klon repositori dengan kod sumbernya, pasang Go dan bina utiliti ini:

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

Mari kita periksa senarai menjalankan tugas Spark:

sparkctl list -n {project}

Mari buat penerangan untuk tugas 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"

Mari jalankan tugas yang diterangkan menggunakan sparkctl:

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

Mari kita periksa senarai menjalankan tugas Spark:

sparkctl list -n {project}

Mari kita periksa senarai acara tugas Spark yang dilancarkan:

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

Mari kita periksa status tugas Spark yang sedang berjalan:

sparkctl status spark-pi -n {project}

Sebagai kesimpulan, saya ingin mempertimbangkan kelemahan yang ditemui menggunakan versi stabil semasa Spark (2.4.5) dalam Kubernetes:

  1. Kelemahan pertama dan, mungkin, utama ialah kekurangan Lokasi Data. Walaupun semua kekurangan BENANG, terdapat juga kelebihan untuk menggunakannya, sebagai contoh, prinsip penghantaran kod kepada data (bukannya data kepada kod). Terima kasih kepadanya, tugas Spark telah dilaksanakan pada nod tempat data yang terlibat dalam pengiraan terletak, dan masa yang diambil untuk menghantar data melalui rangkaian telah dikurangkan dengan ketara. Apabila menggunakan Kubernetes, kami berhadapan dengan keperluan untuk memindahkan data yang terlibat dalam tugas merentasi rangkaian. Jika ia cukup besar, masa pelaksanaan tugas boleh meningkat dengan ketara, dan juga memerlukan jumlah ruang cakera yang agak besar yang diperuntukkan kepada kejadian tugas Spark untuk storan sementara mereka. Kelemahan ini boleh dikurangkan dengan menggunakan perisian khusus yang memastikan lokaliti data dalam Kubernetes (contohnya, Alluxio), tetapi ini sebenarnya bermakna keperluan untuk menyimpan salinan lengkap data pada nod gugusan Kubernetes.
  2. Kelemahan penting kedua ialah keselamatan. Secara lalai, ciri berkaitan keselamatan mengenai menjalankan tugas Spark dilumpuhkan, penggunaan Kerberos tidak diliputi dalam dokumentasi rasmi (walaupun pilihan yang sepadan telah diperkenalkan dalam versi 3.0.0, yang memerlukan kerja tambahan), dan dokumentasi keselamatan untuk menggunakan Spark (https ://spark.apache.org/docs/2.4.5/security.html) hanya YARN, Mesos dan Standalone Cluster muncul sebagai stor utama. Pada masa yang sama, pengguna yang menjalankan tugas Spark tidak boleh ditentukan secara langsung - kami hanya menentukan akaun perkhidmatan yang akan berfungsi dan pengguna dipilih berdasarkan dasar keselamatan yang dikonfigurasikan. Dalam hal ini, sama ada pengguna root digunakan, yang tidak selamat dalam persekitaran yang produktif, atau pengguna dengan UID rawak, yang menyusahkan apabila mengedarkan hak akses kepada data (ini boleh diselesaikan dengan mencipta PodSecurityPolicies dan memautkannya ke akaun perkhidmatan yang sepadan). Pada masa ini, penyelesaiannya ialah sama ada meletakkan semua fail yang diperlukan terus ke dalam imej Docker, atau mengubah suai skrip pelancaran Spark untuk menggunakan mekanisme untuk menyimpan dan mendapatkan semula rahsia yang diterima pakai dalam organisasi anda.
  3. Menjalankan kerja Spark menggunakan Kubernetes secara rasmi masih dalam mod percubaan dan mungkin terdapat perubahan ketara dalam artifak yang digunakan (fail konfigurasi, imej asas Docker dan skrip pelancaran) pada masa hadapan. Dan sememangnya, semasa menyediakan bahan, versi 2.3.0 dan 2.4.5 telah diuji, tingkah lakunya berbeza dengan ketara.

Mari tunggu untuk kemas kini - versi baharu Spark (3.0.0) telah dikeluarkan baru-baru ini, yang membawa perubahan ketara pada kerja Spark pada Kubernetes, tetapi mengekalkan status percubaan sokongan untuk pengurus sumber ini. Mungkin kemas kini seterusnya benar-benar akan membolehkan anda mengesyorkan sepenuhnya meninggalkan YARN dan menjalankan tugas Spark pada Kubernetes tanpa rasa takut terhadap keselamatan sistem anda dan tanpa perlu mengubah suai komponen berfungsi secara bebas.

Akhir

Sumber: www.habr.com

Tambah komen