Menjalankan Apache Spark di Kubernetes

Pembaca yang budiman, selamat siang. Hari ini kita akan berbicara sedikit tentang Apache Spark dan prospek pengembangannya.

Menjalankan Apache Spark di Kubernetes

Di dunia Big Data modern, Apache Spark adalah standar de facto untuk mengembangkan tugas pemrosesan data batch. Selain itu juga digunakan untuk membuat aplikasi streaming yang bekerja dalam konsep micro batch, memproses dan mengirimkan data dalam porsi kecil (Spark Structured Streaming). Dan secara tradisional ini telah menjadi bagian dari keseluruhan tumpukan Hadoop, menggunakan YARN (atau dalam beberapa kasus Apache Mesos) sebagai pengelola sumber daya. Pada tahun 2020, penggunaannya dalam bentuk tradisional dipertanyakan oleh sebagian besar perusahaan karena kurangnya distribusi Hadoop yang layak - pengembangan HDP dan CDH telah terhenti, CDH tidak berkembang dengan baik dan berbiaya tinggi, dan pemasok Hadoop yang tersisa telah entah sudah tidak ada lagi atau memiliki masa depan yang suram. Oleh karena itu, peluncuran Apache Spark menggunakan Kubernetes semakin menarik minat komunitas dan perusahaan besar - menjadi standar dalam orkestrasi container dan pengelolaan sumber daya di cloud privat dan publik, ini memecahkan masalah penjadwalan sumber daya yang tidak nyaman pada tugas Spark di YARN dan menyediakan platform yang terus berkembang dengan banyak distribusi komersial dan terbuka untuk perusahaan dari semua ukuran dan lini. Selain itu, setelah popularitas, sebagian besar telah berhasil memperoleh beberapa instalasi sendiri dan meningkatkan keahlian mereka dalam penggunaannya, sehingga menyederhanakan perpindahan.

Dimulai dengan versi 2.3.0, Apache Spark memperoleh dukungan resmi untuk menjalankan tugas-tugas di kluster Kubernetes dan hari ini, kita akan membahas tentang kematangan pendekatan ini saat ini, berbagai opsi untuk penggunaannya, dan kendala yang akan dihadapi selama implementasi.

Pertama-tama, mari kita lihat proses pengembangan tugas dan aplikasi berdasarkan Apache Spark dan soroti kasus-kasus umum di mana Anda perlu menjalankan tugas di cluster Kubernetes. Dalam mempersiapkan posting ini, OpenShift digunakan sebagai distribusi dan perintah yang relevan dengan utilitas baris perintah (oc) akan diberikan. Untuk distribusi Kubernetes lainnya, perintah yang sesuai dari utilitas baris perintah Kubernetes standar (kubectl) atau analognya (misalnya, untuk kebijakan oc adm) dapat digunakan.

Kasus penggunaan pertama - pengiriman percikan

Selama pengembangan tugas dan aplikasi, pengembang perlu menjalankan tugas untuk men-debug transformasi data. Secara teoritis, stub dapat digunakan untuk tujuan ini, namun pengembangan dengan partisipasi contoh nyata (walaupun pengujian) dari sistem akhir telah terbukti lebih cepat dan lebih baik dalam kelas tugas ini. Dalam kasus ketika kita melakukan debug pada contoh nyata dari sistem akhir, ada dua skenario yang mungkin terjadi:

  • pengembang menjalankan tugas Spark secara lokal dalam mode mandiri;

    Menjalankan Apache Spark di Kubernetes

  • pengembang menjalankan tugas Spark pada kluster Kubernetes dalam loop pengujian.

    Menjalankan Apache Spark di Kubernetes

Opsi pertama berhak untuk ada, tetapi memiliki sejumlah kelemahan:

  • Setiap pengembang harus diberikan akses dari tempat kerja ke semua contoh sistem akhir yang dia butuhkan;
  • sejumlah sumber daya yang cukup diperlukan pada mesin yang berfungsi untuk menjalankan tugas yang sedang dikembangkan.

Opsi kedua tidak memiliki kelemahan ini, karena penggunaan cluster Kubernetes memungkinkan Anda mengalokasikan kumpulan sumber daya yang diperlukan untuk menjalankan tugas dan menyediakan akses yang diperlukan ke instance sistem akhir, secara fleksibel menyediakan akses ke sana menggunakan model peran Kubernetes untuk semua anggota tim pengembangan. Mari kita soroti ini sebagai kasus penggunaan pertama - meluncurkan tugas Spark dari mesin pengembang lokal di cluster Kubernetes dalam loop pengujian.

Mari kita bahas lebih lanjut tentang proses pengaturan Spark agar berjalan secara lokal. Untuk mulai menggunakan Spark, Anda perlu menginstalnya:

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 mengumpulkan paket yang diperlukan untuk bekerja dengan Kubernetes:

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

Pembangunan penuh membutuhkan banyak waktu, dan untuk membuat image Docker dan menjalankannya di kluster Kubernetes, Anda hanya memerlukan file jar dari direktori “assembly/”, jadi Anda hanya dapat membangun subproyek ini:

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

Untuk menjalankan pekerjaan Spark di Kubernetes, Anda perlu membuat image Docker untuk digunakan sebagai image dasar. Ada 2 kemungkinan pendekatan di sini:

  • Gambar Docker yang dihasilkan menyertakan kode tugas Spark yang dapat dieksekusi;
  • Gambar yang dibuat hanya menyertakan Spark dan dependensi yang diperlukan, kode yang dapat dieksekusi dihosting dari jarak jauh (misalnya, di HDFS).

Pertama, mari buat image Docker yang berisi contoh pengujian tugas Spark. Untuk membuat image Docker, Spark memiliki utilitas yang disebut "docker-image-tool". Mari kita pelajari bantuannya:

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

Dengan bantuannya, Anda dapat membuat image Docker dan mengunggahnya ke registry jarak jauh, tetapi secara default ia memiliki sejumlah kelemahan:

  • tanpa gagal membuat 3 image Docker sekaligus - untuk Spark, PySpark, dan R;
  • tidak mengizinkan Anda menentukan nama gambar.

Oleh karena itu, kami akan menggunakan versi modifikasi dari utilitas yang diberikan di bawah ini:

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 merakit gambar Spark dasar yang berisi tugas pengujian untuk menghitung Pi menggunakan Spark (di sini {docker-registry-url} adalah URL registri gambar Docker Anda, {repo} adalah nama repositori di dalam registri, yang cocok dengan proyek di OpenShift , {image-name} - nama gambar (jika pemisahan gambar tiga tingkat digunakan, misalnya, seperti dalam registri terintegrasi gambar Red Hat OpenShift), {tag} - tag ini versi gambar):

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

Masuk ke kluster OKD menggunakan utilitas konsol (di sini {OKD-API-URL} adalah URL API kluster OKD):

oc login {OKD-API-URL}

Mari kita dapatkan token pengguna saat ini untuk otorisasi di Docker Registry:

oc whoami -t

Masuk ke Docker Registry internal cluster OKD (kami menggunakan token yang diperoleh menggunakan perintah sebelumnya sebagai kata sandi):

docker login {docker-registry-url}

Mari kita unggah gambar Docker yang telah dirakit ke OKD Docker Registry:

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

Mari kita periksa apakah gambar rakitan tersedia di OKD. Untuk melakukan ini, buka URL di browser dengan daftar gambar proyek terkait (di sini {project} adalah nama proyek di dalam cluster OpenShift, {OKD-WEBUI-URL} adalah URL konsol Web OpenShift ) - https://{OKD-WEBUI-URL}/console /project/{project}/browse/images/{nama-gambar}.

Untuk menjalankan tugas, akun layanan harus dibuat dengan hak istimewa untuk menjalankan pod sebagai root (kita akan membahas poin ini nanti):

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

Mari kita jalankan perintah spark-submit untuk menerbitkan tugas Spark ke kluster OKD, dengan menentukan akun layanan dan image Docker yang dibuat:

 /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

Berikut:

—name — nama tugas yang akan berpartisipasi dalam pembentukan nama pod Kubernetes;

—class — kelas dari file yang dapat dieksekusi, dipanggil saat tugas dimulai;

—conf — Parameter konfigurasi Spark;

spark.executor.instances — jumlah eksekutor Spark yang akan diluncurkan;

spark.kubernetes.authenticate.driver.serviceAccountName - nama akun layanan Kubernetes yang digunakan saat meluncurkan pod (untuk menentukan konteks keamanan dan kemampuan saat berinteraksi dengan API Kubernetes);

spark.kubernetes.namespace — Namespace Kubernetes tempat pod driver dan eksekutor akan diluncurkan;

spark.submit.deployMode — metode peluncuran Spark (untuk "cluster" pengiriman percikan standar digunakan, untuk Operator Spark dan versi "klien" Spark yang lebih baru);

spark.kubernetes.container.image - Gambar Docker yang digunakan untuk meluncurkan pod;

spark.master — URL API Kubernetes (eksternal ditentukan sehingga akses terjadi dari mesin lokal);

local:// adalah jalur ke Spark yang dapat dieksekusi di dalam image Docker.

Kami pergi ke proyek OKD yang sesuai dan mempelajari pod yang dibuat - https://{OKD-WEBUI-URL}/console/project/{project}/browse/pods.

Untuk menyederhanakan proses pengembangan, opsi lain dapat digunakan, di mana gambar dasar umum Spark dibuat, digunakan oleh semua tugas untuk dijalankan, dan snapshot dari file yang dapat dieksekusi dipublikasikan ke penyimpanan eksternal (misalnya, Hadoop) dan ditentukan saat memanggil spark-kirim sebagai tautan. Dalam hal ini, Anda dapat menjalankan versi tugas Spark yang berbeda tanpa membuat ulang image Docker, misalnya menggunakan WebHDFS untuk menerbitkan image. Kami mengirimkan permintaan untuk membuat file (di sini {host} adalah host layanan WebHDFS, {port} adalah port layanan WebHDFS, {path-to-file-on-hdfs} adalah jalur yang diinginkan ke file pada HDFS):

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

Anda akan menerima respon seperti ini (di sini {location} adalah URL yang perlu digunakan untuk mendownload file):

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

Muat file Spark yang dapat dieksekusi ke HDFS (di sini {path-to-local-file} adalah jalur ke file Spark yang dapat dieksekusi pada host saat ini):

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

Setelah ini, kita dapat melakukan pengiriman percikan menggunakan file Spark yang diunggah ke HDFS (di sini {nama-kelas} adalah nama kelas yang perlu diluncurkan untuk menyelesaikan tugas):

/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 dicatat bahwa untuk mengakses HDFS dan memastikan tugas berfungsi, Anda mungkin perlu mengubah Dockerfile dan skrip entrypoint.sh - tambahkan arahan ke Dockerfile untuk menyalin perpustakaan dependen ke direktori /opt/spark/jars dan sertakan file konfigurasi HDFS di SPARK_CLASSPATH di entrypoint.sh.

Kasus penggunaan kedua adalah Apache Livy

Selanjutnya, ketika suatu tugas dikembangkan dan hasilnya perlu diuji, muncul pertanyaan untuk meluncurkannya sebagai bagian dari proses CI/CD dan melacak status pelaksanaannya. Tentu saja, Anda dapat menjalankannya menggunakan panggilan pengiriman percikan lokal, tetapi hal ini mempersulit infrastruktur CI/CD karena memerlukan instalasi dan konfigurasi Spark pada agen/runner server CI dan menyiapkan akses ke API Kubernetes. Untuk kasus ini, implementasi target telah memilih untuk menggunakan Apache Livy sebagai REST API untuk menjalankan tugas Spark yang dihosting di dalam cluster Kubernetes. Dengan bantuannya, Anda dapat menjalankan tugas Spark di cluster Kubernetes menggunakan permintaan cURL biasa, yang mudah diimplementasikan berdasarkan solusi CI apa pun, dan penempatannya di dalam cluster Kubernetes memecahkan masalah otentikasi saat berinteraksi dengan API Kubernetes.

Menjalankan Apache Spark di Kubernetes

Mari kita soroti ini sebagai kasus penggunaan kedua - menjalankan tugas Spark sebagai bagian dari proses CI/CD pada cluster Kubernetes dalam loop pengujian.

Sedikit tentang Apache Livy - ia berfungsi sebagai server HTTP yang menyediakan antarmuka Web dan API RESTful yang memungkinkan Anda meluncurkan spark-submit dari jarak jauh dengan meneruskan parameter yang diperlukan. Secara tradisional, ini dikirimkan sebagai bagian dari distribusi HDP, tetapi juga dapat diterapkan ke OKD atau instalasi Kubernetes lainnya menggunakan manifes yang sesuai dan sekumpulan image Docker, seperti ini - github.com/ttauveron/k8s-big-data-experiments/tree/master/livy-spark-2.3. Untuk kasus kami, image Docker serupa dibuat, termasuk Spark versi 2.4.5 dari 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"]

Gambar yang dihasilkan dapat dibuat dan diunggah ke repositori Docker Anda yang ada, seperti repositori OKD internal. Untuk menerapkannya, gunakan manifes berikut ({registry-url} - URL registri image Docker, {image-name} - Nama image Docker, {tag} - Tag image Docker, {livy-url} - URL yang diinginkan tempat server akan dapat diakses oleh Livy; manifes "Rute" digunakan jika Red Hat OpenShift digunakan sebagai distribusi Kubernetes, jika tidak, manifes Ingress atau Service yang sesuai dengan tipe NodePort akan 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

Setelah menerapkannya dan berhasil meluncurkan pod, antarmuka grafis Livy tersedia di tautan: http://{livy-url}/ui. Dengan Livy, kita dapat memublikasikan tugas Spark menggunakan permintaan REST, misalnya, dari Tukang Pos. Contoh koleksi dengan permintaan disajikan di bawah ini (argumen konfigurasi dengan variabel yang diperlukan untuk pengoperasian tugas yang diluncurkan dapat diteruskan dalam array “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 jalankan permintaan pertama dari koleksi, buka antarmuka OKD dan periksa apakah tugas telah berhasil diluncurkan - https://{OKD-WEBUI-URL}/console/project/{project}/browse/pods. Pada saat yang sama, sebuah sesi akan muncul di antarmuka Livy (http://{livy-url}/ui), di mana, menggunakan Livy API atau antarmuka grafis, Anda dapat melacak kemajuan tugas dan mempelajari sesi tersebut log.

Sekarang mari kita tunjukkan cara kerja Livy. Untuk melakukannya, mari kita periksa log kontainer Livy di dalam pod dengan server Livy - https://{OKD-WEBUI-URL}/console/project/{project}/browse/pods/{livy-pod-name }?tab=log. Dari sini kita dapat melihat bahwa ketika memanggil Livy REST API dalam sebuah container bernama “livy”, sebuah spark-submit dijalankan, mirip dengan yang kita gunakan di atas (di sini {livy-pod-name} adalah nama pod yang dibuat dengan server Livy). Koleksi ini juga memperkenalkan kueri kedua yang memungkinkan Anda menjalankan tugas yang menghosting Spark yang dapat dieksekusi dari jarak jauh menggunakan server Livy.

Kasus penggunaan ketiga adalah Operator Spark

Sekarang setelah tugas tersebut diuji, pertanyaan tentang cara menjalankannya secara teratur muncul. Cara asli untuk menjalankan tugas secara teratur di cluster Kubernetes adalah entitas CronJob dan Anda dapat menggunakannya, tetapi saat ini penggunaan operator untuk mengelola aplikasi di Kubernetes sangat populer dan untuk Spark terdapat operator yang cukup matang, yaitu juga digunakan dalam solusi tingkat Perusahaan (misalnya, Lightbend FastData Platform). Kami merekomendasikan untuk menggunakannya - versi stabil Spark (2.4.5) saat ini memiliki opsi konfigurasi yang agak terbatas untuk menjalankan tugas Spark di Kubernetes, sedangkan versi utama berikutnya (3.0.0) menyatakan dukungan penuh untuk Kubernetes, tetapi tanggal rilisnya masih belum diketahui . Spark Operator mengkompensasi kekurangan ini dengan menambahkan opsi konfigurasi penting (misalnya, memasang ConfigMap dengan konfigurasi akses Hadoop ke pod Spark) dan kemampuan untuk menjalankan tugas yang dijadwalkan secara rutin.

Menjalankan Apache Spark di Kubernetes
Mari kita soroti ini sebagai kasus penggunaan ketiga - menjalankan tugas Spark secara teratur di kluster Kubernetes dalam loop produksi.

Spark Operator adalah open source dan dikembangkan dalam Google Cloud Platform - github.com/GoogleCloudPlatform/spark-on-k8s-operator. Pemasangannya dapat dilakukan dengan 3 cara:

  1. Sebagai bagian dari instalasi 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 dari repositori resmi (https://github.com/GoogleCloudPlatform/spark-on-k8s-operator/tree/master/manifest). Perlu diperhatikan hal berikut - Cloudflow menyertakan operator dengan API versi v1beta1. Jika jenis instalasi ini digunakan, deskripsi manifes aplikasi Spark harus didasarkan pada contoh tag di Git dengan versi API yang sesuai, misalnya, "v1beta1-0.9.0-2.4.0". Versi operator dapat ditemukan dalam deskripsi CRD yang disertakan dalam operator di kamus “versi”:
    oc get crd sparkapplications.sparkoperator.k8s.io -o yaml
    	

Jika operator diinstal dengan benar, pod aktif dengan operator Spark akan muncul di proyek terkait (misalnya, cloudflow-fdp-sparkoperator di ruang Cloudflow untuk instalasi Cloudflow) dan jenis sumber daya Kubernetes yang sesuai bernama “sparkapplications” akan muncul . Anda dapat menjelajahi aplikasi Spark yang tersedia dengan perintah berikut:

oc get sparkapplications -n {project}

Untuk menjalankan tugas menggunakan Spark Operator, Anda perlu melakukan 3 hal:

  • buat image Docker yang mencakup semua perpustakaan yang diperlukan, serta konfigurasi dan file yang dapat dieksekusi. Dalam gambar target, ini adalah gambar yang dibuat pada tahap CI/CD dan diuji pada cluster pengujian;
  • mempublikasikan image Docker ke registri yang dapat diakses dari cluster Kubernetes;
  • menghasilkan manifes dengan tipe "SparkApplication" dan deskripsi tugas yang akan diluncurkan. Contoh manifes tersedia di repositori resmi (mis. github.com/GoogleCloudPlatform/spark-on-k8s-operator/blob/v1beta1-0.9.0-2.4.0/examples/spark-pi.yaml). Ada beberapa hal penting yang perlu diperhatikan mengenai manifesto tersebut:
    1. kamus “apiVersion” harus menunjukkan versi API yang sesuai dengan versi operator;
    2. kamus “metadata.namespace” harus menunjukkan namespace tempat aplikasi akan diluncurkan;
    3. kamus “spec.image” harus berisi alamat image Docker yang dibuat dalam registri yang dapat diakses;
    4. kamus “spec.mainClass” harus berisi kelas tugas Spark yang perlu dijalankan saat proses dimulai;
    5. kamus “spec.mainApplicationFile” harus berisi jalur ke file jar yang dapat dieksekusi;
    6. kamus “spec.sparkVersion” harus menunjukkan versi Spark yang digunakan;
    7. kamus “spec.driver.serviceAccount” harus menentukan akun layanan dalam namespace Kubernetes terkait yang akan digunakan untuk menjalankan aplikasi;
    8. kamus “spec.executor” harus menunjukkan jumlah sumber daya yang dialokasikan untuk aplikasi;
    9. kamus "spec.volumeMounts" harus menentukan direktori lokal tempat file tugas Spark lokal akan dibuat.

Contoh pembuatan manifes (di sini {spark-service-account} adalah akun layanan di dalam cluster 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 akun layanan yang, sebelum menerbitkan manifesnya, Anda harus membuat pengikatan peran yang diperlukan yang memberikan hak akses yang diperlukan untuk aplikasi Spark untuk berinteraksi dengan API Kubernetes (jika diperlukan). Dalam kasus kami, aplikasi memerlukan hak untuk membuat Pod. Mari buat pengikatan peran yang diperlukan:

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

Perlu diperhatikan juga bahwa spesifikasi manifes ini mungkin menyertakan parameter "hadoopConfigMap", yang memungkinkan Anda menentukan ConfigMap dengan konfigurasi Hadoop tanpa harus menempatkan file terkait terlebih dahulu di image Docker. Ini juga cocok untuk menjalankan tugas secara teratur - menggunakan parameter "jadwal", jadwal untuk menjalankan tugas tertentu dapat ditentukan.

Setelah itu, kami menyimpan manifes kami ke file spark-pi.yaml dan menerapkannya ke cluster Kubernetes kami:

oc apply -f spark-pi.yaml

Ini akan membuat objek bertipe "sparkapplications":

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

Dalam hal ini, sebuah pod dengan aplikasi akan dibuat, yang statusnya akan ditampilkan di “sparkapplications” yang dibuat. Anda dapat melihatnya dengan perintah berikut:

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

Setelah tugas selesai, POD akan berpindah ke status "Selesai", yang juga akan diperbarui di "sparkapplications". Log aplikasi dapat dilihat di browser atau menggunakan perintah berikut (di sini {sparkapplications-pod-name} adalah nama pod dari tugas yang sedang berjalan):

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

Tugas Spark juga dapat dikelola menggunakan utilitas sparkctl khusus. Untuk menginstalnya, clone repositori dengan kode sumbernya, instal Go dan buat utilitas 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 daftar tugas Spark yang sedang berjalan:

sparkctl list -n {project}

Mari buat deskripsi 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 dijelaskan menggunakan sparkctl:

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

Mari kita periksa daftar tugas Spark yang sedang berjalan:

sparkctl list -n {project}

Mari kita periksa daftar peristiwa tugas Spark yang diluncurkan:

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 ditemukan saat menggunakan versi stabil Spark (2.4.5) saat ini di Kubernetes:

  1. Kerugian pertama dan mungkin yang utama adalah kurangnya Lokalitas Data. Terlepas dari semua kekurangan YARN, ada juga keuntungan menggunakannya, misalnya prinsip penyampaian kode ke data (bukan data ke kode). Berkat itu, tugas Spark dijalankan pada node tempat data yang terlibat dalam penghitungan berada, dan waktu yang diperlukan untuk mengirimkan data melalui jaringan berkurang secara signifikan. Saat menggunakan Kubernetes, kita dihadapkan pada kebutuhan untuk memindahkan data yang terlibat dalam suatu tugas ke seluruh jaringan. Jika ukurannya cukup besar, waktu eksekusi tugas dapat meningkat secara signifikan, dan juga memerlukan jumlah ruang disk yang cukup besar yang dialokasikan ke instans tugas Spark untuk penyimpanan sementara. Kerugian ini dapat diatasi dengan menggunakan perangkat lunak khusus yang memastikan lokalitas data di Kubernetes (misalnya, Alluxio), tetapi ini sebenarnya berarti perlunya menyimpan salinan data lengkap di node cluster Kubernetes.
  2. Kerugian penting kedua adalah keamanan. Secara default, fitur terkait keamanan terkait menjalankan tugas Spark dinonaktifkan, penggunaan Kerberos tidak tercakup dalam dokumentasi resmi (walaupun opsi terkait diperkenalkan di versi 3.0.0, yang memerlukan pekerjaan tambahan), dan dokumentasi keamanan untuk menggunakan Spark (https ://spark.apache.org/docs/2.4.5/security.html) hanya YARN, Mesos, dan Standalone Cluster yang muncul sebagai penyimpanan kunci. Pada saat yang sama, pengguna yang menjalankan tugas Spark tidak dapat ditentukan secara langsung - kami hanya menentukan akun layanan yang akan menjalankannya, dan pengguna dipilih berdasarkan kebijakan keamanan yang dikonfigurasi. Dalam hal ini, pengguna root digunakan, yang tidak aman di lingkungan produktif, atau pengguna dengan UID acak, yang merepotkan saat mendistribusikan hak akses ke data (ini dapat diselesaikan dengan membuat PodSecurityPolicies dan menautkannya ke akun layanan terkait). Saat ini, solusinya adalah dengan menempatkan semua file yang diperlukan langsung ke image Docker, atau memodifikasi skrip peluncuran Spark untuk menggunakan mekanisme penyimpanan dan pengambilan rahasia yang diterapkan di organisasi Anda.
  3. Menjalankan pekerjaan Spark menggunakan Kubernetes secara resmi masih dalam mode eksperimental dan mungkin ada perubahan signifikan pada artefak yang digunakan (file konfigurasi, image dasar Docker, dan skrip peluncuran) di masa mendatang. Dan memang, saat menyiapkan materi, diuji versi 2.3.0 dan 2.4.5, perilakunya berbeda secara signifikan.

Mari kita tunggu pembaruannya - versi baru Spark (3.0.0) baru-baru ini dirilis, yang membawa perubahan signifikan pada kinerja Spark di Kubernetes, tetapi tetap mempertahankan status eksperimental dukungan untuk pengelola sumber daya ini. Mungkin pembaruan berikutnya akan benar-benar memungkinkan untuk sepenuhnya merekomendasikan untuk meninggalkan YARN dan menjalankan tugas Spark di Kubernetes tanpa mengkhawatirkan keamanan sistem Anda dan tanpa perlu memodifikasi komponen fungsional secara mandiri.

Sirip.

Sumber: www.habr.com

Tambah komentar