تشغيل Apache Spark على Kubernetes

القراء الأعزاء ، مساء الخير. سنتحدث اليوم قليلاً عن Apache Spark وآفاق تطويرها.

تشغيل Apache Spark على Kubernetes

في العالم الحديث للبيانات الضخمة ، يعد Apache Spark المعيار الفعلي لتطوير مهام معالجة البيانات المجمعة. بالإضافة إلى ذلك ، يتم استخدامه أيضًا لإنشاء تطبيقات دفق تعمل في مفهوم الدُفعات الصغيرة ومعالجة وتحميل البيانات في أجزاء صغيرة (Spark Structured Streaming). وقد كان تقليديًا جزءًا من مكدس Hadoop الإجمالي ، باستخدام YARN (أو ، في بعض الحالات ، Apache Mesos) كمدير للموارد. بحلول عام 2020 ، يكون استخدامه في شكله التقليدي لمعظم الشركات محل سؤال كبير بسبب عدم وجود توزيعات Hadoop لائقة - توقف تطوير HDP و CDH ، CDH متخلف وله تكلفة عالية ، وبقية Hadoop لم يعد مقدمو الخدمة موجودون أو لديهم مستقبل غامض. لذلك ، يهتم المجتمع والشركات الكبيرة بشكل متزايد بتشغيل Apache Spark باستخدام Kubernetes - بعد أن أصبح المعيار في تنسيق الحاويات وإدارة الموارد في السحب الخاصة والعامة ، فإنه يحل مشكلة تخطيط الموارد غير المريح لمهام Spark على YARN ويوفر بشكل ثابت منصة تطوير مع العديد من التوزيعات التجارية ومفتوحة المصدر للشركات من جميع الأحجام. بالإضافة إلى ذلك ، في موجة الشعبية ، تمكن معظمهم بالفعل من الحصول على اثنين من التركيبات الخاصة بهم وبناء الخبرة في استخدامها ، مما يبسط هذه الخطوة.

بدءًا من الإصدار 2.3.0 ، تلقى Apache Spark دعمًا رسميًا لتشغيل المهام في مجموعة Kubernetes ، وسنتحدث اليوم عن النضج الحالي لهذا النهج ، والخيارات المختلفة لاستخدامه ، والمزالق التي ستواجهها أثناء التنفيذ.

أولاً وقبل كل شيء ، دعنا نلقي نظرة على عملية تطوير المهام والتطبيقات بناءً على Apache Spark وإبراز الحالات النموذجية التي تريد فيها تشغيل مهمة على مجموعة Kubernetes. في إعداد هذا المنشور ، يتم استخدام OpenShift كتوزيع وسيتم إعطاء الأوامر ذات الصلة بأداة سطر الأوامر (oc). بالنسبة لتوزيعات Kubernetes الأخرى ، يمكن استخدام الأوامر المناسبة لأداة سطر أوامر Kubernetes القياسية (kubectl) أو ما يعادلها (على سبيل المثال ، لسياسة oc adm).

حالة الاستخدام الأولى هي إرسال شرارة

أثناء تطوير المهام والتطبيقات ، يحتاج المطور إلى تشغيل المهام لتصحيح أخطاء تحويل البيانات. من الناحية النظرية ، يمكن استخدام بذرة لهذه الأغراض ، ولكن التطوير الذي يتضمن أمثلة حقيقية (وإن كانت اختبارية) للأنظمة النهائية أثبت أنه أسرع وأفضل في هذه الفئة من المهام. في الحالة التي نقوم فيها بتصحيح الأخطاء في حالات حقيقية للأنظمة النهائية ، هناك سيناريوهان للعمل:

  • يقوم المطور بتشغيل مهمة Spark محليًا في الوضع المستقل ؛

    تشغيل Apache Spark على Kubernetes

  • يقوم أحد المطورين بتشغيل مهمة Spark على مجموعة Kubernetes في حلقة اختبار.

    تشغيل Apache Spark على Kubernetes

الخيار الأول له الحق في الوجود ، ولكنه ينطوي على عدد من العيوب:

  • لكل مطور ، يلزم توفير الوصول من مكان العمل إلى جميع مثيلات الأنظمة النهائية التي يحتاجها ؛
  • مطلوب موارد كافية على آلة الإنتاج لتشغيل المهمة قيد التطوير.

الخيار الثاني يخلو من أوجه القصور هذه ، نظرًا لأن استخدام مجموعة Kubernetes يتيح لك تخصيص مجموعة الموارد اللازمة لتشغيل المهام وتزويدها بالوصول الضروري إلى مثيلات الأنظمة النهائية ، مما يتيح الوصول إليها بمرونة باستخدام نموذج Kubernetes جميع أعضاء فريق التطوير. دعنا نسلط الضوء عليها باعتبارها حالة الاستخدام الأولى - تشغيل مهام Spark من جهاز مطور محلي على مجموعة Kubernetes في دائرة اختبار.

دعنا نلقي نظرة فاحصة على عملية إعداد Spark للتشغيل محليًا. لبدء استخدام Spark ، تحتاج إلى تثبيته:

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

نجمع الحزم اللازمة للعمل مع Kubernetes:

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

يستغرق التصميم الكامل وقتًا طويلاً ، وفي الواقع ، لا يلزم سوى جرار من دليل "التجميع /" لإنشاء صور Docker وتشغيلها على مجموعة Kubernetes ، لذلك يمكن إنشاء هذا المشروع الفرعي فقط:

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

يتطلب تشغيل مهام Spark على Kubernetes إنشاء صورة Docker لاستخدامها كصورة أساسية. هناك طريقتان محتملتان هنا:

  • تشتمل صورة Docker التي تم إنشاؤها على التعليمات البرمجية القابلة للتنفيذ الخاصة بمهمة Spark ؛
  • تتضمن الصورة التي تم إنشاؤها فقط Spark والتبعيات الضرورية ، ويتم استضافة الكود القابل للتنفيذ عن بُعد (على سبيل المثال ، في HDFS).

أولاً ، دعنا نبني صورة Docker تحتوي على حالة اختبار مهمة Spark. لإنشاء صور Docker ، يحتوي Spark على أداة مساعدة مقابلة تسمى "أداة docker-image-tool". دعنا ندرسها للمساعدة:

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

يمكن استخدامه لإنشاء صور Docker وتحميلها إلى السجلات البعيدة ، ولكن افتراضيًا لها عدد من العيوب:

  • بدون فشل ، يقوم بإنشاء 3 صور Docker في وقت واحد - لـ Spark و PySpark و R ؛
  • لا يسمح لك بتحديد اسم الصورة.

لذلك ، سوف نستخدم نسخة معدلة من هذه الأداة ، كما هو موضح أدناه:

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

بمساعدتها ، قمنا ببناء صورة Spark أساسية تحتوي على مهمة اختبار لحساب الرقم Pi باستخدام Spark (هنا {docker-Registry-url} هو عنوان URL لسجل صور Docker الخاص بك ، {repo} هو اسم المستودع بالداخل السجل ، الذي يطابق المشروع في OpenShift ، {image-name} - اسم الصورة (إذا تم استخدام فصل ثلاثي المستويات للصور ، على سبيل المثال ، كما هو الحال في سجل الصور المتكامل Red Hat OpenShift) ، {tag} - علامة هذا نسخة الصورة):

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

التخويل في كتلة OKD باستخدام الأداة المساعدة لوحدة التحكم (هنا {OKD-API-URL} هو عنوان URL لواجهة برمجة تطبيقات مجموعة OKD):

oc login {OKD-API-URL}

دعنا نحصل على رمز المستخدم الحالي للترخيص في Docker Registry:

oc whoami -t

التفويض في Docker Registry الداخلي لمجموعة OKD (نستخدم الرمز المميز الذي تم الحصول عليه باستخدام الأمر السابق ككلمة مرور):

docker login {docker-registry-url}

قم بتحميل صورة Docker المدمجة إلى Docker Registry OKD:

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

دعنا نتحقق من أن الصورة المبنية متاحة في OKD. للقيام بذلك ، افتح عنوان URL مع قائمة صور المشروع المقابل في المتصفح (هنا {project} هو اسم المشروع داخل مجموعة OpenShift ، {OKD-WEBUI-URL} هو عنوان URL لوحدة تحكم الويب OpenShift ) - https: // {OKD-WEBUI-URL} / console / project / {project} / browse / images / {image-name}.

لتشغيل المهام ، يجب إنشاء حساب خدمة بامتيازات تشغيل البودات كجذر (سنناقش هذه النقطة لاحقًا):

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

لنقم بتشغيل الأمر spark-submit لنشر مهمة Spark إلى مجموعة OKD ، مع تحديد حساب الخدمة الذي تم إنشاؤه وصورة 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

هنا:

--name - اسم المهمة التي ستشارك في تشكيل اسم Kubernetes pods ؛

--class - فئة الملف القابل للتنفيذ تسمى عند بدء المهمة ؛

--conf - معلمات تكوين شرارة ؛

spark.executor.instances - عدد منفذي Spark المطلوب تشغيلهم

spark.kubernetes.authenticate.driver.serviceAccountName - اسم حساب خدمة Kubernetes المستخدم عند تشغيل Pods (لتحديد سياق الأمان والإمكانيات عند التفاعل مع Kubernetes API)

spark.kubernetes.namespace - مساحة اسم Kubernetes حيث سيتم تشغيل أقراص برنامج التشغيل والمنفذ ؛

spark.submit.deployMode - كيفية بدء تشغيل Spark (بالنسبة إلى استخدام شرارة الإرسال القياسي "الكتلة" ، لمشغل Spark والإصدارات الأحدث من Spark "العميل") ؛

spark.kubernetes.container.image - صورة عامل ميناء تُستخدم لتشغيل القرون

spark.master - عنوان URL الخاص بواجهة برمجة تطبيقات Kubernetes (يتم تحديد خارجي بحيث يتم إجراء الاستدعاء من الجهاز المحلي) ؛

local: // هو المسار إلى ملف Spark القابل للتنفيذ داخل صورة Docker.

نذهب إلى مشروع OKD المقابل وندرس البودات التي تم إنشاؤها - https: // {OKD-WEBUI-URL} / console / project / {project} / browse / pods.

لتبسيط عملية التطوير ، يمكن استخدام خيار آخر ، حيث يتم إنشاء صورة شرارة أساسية مشتركة ، وتستخدمها جميع المهام للتشغيل ، ويتم نشر لقطات من الملفات القابلة للتنفيذ في وحدة التخزين الخارجية (على سبيل المثال ، Hadoop) وتحديدها عند استدعاء شرارة -تقديمه كرابط. في هذه الحالة ، يمكنك تشغيل إصدارات مختلفة من مهام Spark دون إعادة إنشاء صور Docker باستخدام ، على سبيل المثال ، WebHDFS لنشر الصور. نرسل طلبًا لإنشاء ملف (هنا {host} هو مضيف خدمة WebHDFS ، {المنفذ} هو منفذ خدمة WebHDFS ، {path-to-file-on-hdfs} هو المسار المطلوب للملف على HDFS):

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

سيؤدي هذا إلى عرض استجابة النموذج (هنا {location} هو عنوان URL الذي سيتم استخدامه لتنزيل الملف):

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

قم بتحميل Spark القابل للتنفيذ إلى HDFS (حيث يكون {path-to-local-file} هو المسار إلى Spark القابل للتنفيذ على المضيف الحالي):

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

بعد ذلك ، يمكننا إرسال شرارة باستخدام ملف Spark الذي تم تحميله إلى HDFS (هنا {class-name} هو اسم الفصل الذي يجب تشغيله لإكمال المهمة):

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

في الوقت نفسه ، تجدر الإشارة إلى أنه من أجل الوصول إلى HDFS وجعل المهمة تعمل ، قد يكون من الضروري تغيير Dockerfile و entrypoint.sh النصي - إضافة توجيه إلى Dockerfile لنسخ المكتبات التابعة إلى / opt / spark / jars وقم بتضمين ملف تكوين HDFS في SPARK_CLASSPATH في نقطة الدخول.

حالة الاستخدام الثانية هي Apache Livy

علاوة على ذلك ، عندما يتم تطوير المهمة ويكون مطلوبًا لاختبار النتيجة التي تم الحصول عليها ، فإن السؤال الذي يطرح نفسه هو إطلاقها كجزء من عملية CI / CD وتتبع حالة تنفيذها. بالطبع ، يمكنك تشغيله باستدعاء محلي لإرسال شرارة ، ولكن هذا يعقد البنية التحتية CI / CD لأنه يتطلب تثبيت وتكوين Spark على وكلاء / مشغلي خادم CI وتكوين الوصول إلى Kubernetes API. لهذه الحالة ، اختار التنفيذ المستهدف استخدام Apache Livy باعتباره REST API لتشغيل مهام Spark المستضافة داخل مجموعة Kubernetes. باستخدامه ، يمكنك تشغيل مهام Spark على مجموعة Kubernetes باستخدام طلبات cURL العادية ، والتي يتم تنفيذها بسهولة بناءً على أي حل CI ، ووضعها داخل مجموعة Kubernetes يحل مشكلة المصادقة عند التفاعل مع Kubernetes API.

تشغيل Apache Spark على Kubernetes

لنفردها كحالة الاستخدام الثانية - تشغيل مهام Spark كجزء من عملية CI / CD على مجموعة Kubernetes في دائرة اختبار.

قليلا عن Apache Livy - إنه يعمل كخادم HTTP يوفر واجهة ويب وواجهة برمجة تطبيقات RESTful تتيح لك تشغيل إرسال شرارة عن بعد عن طريق تمرير المعلمات الضرورية. تم شحنه تقليديًا كجزء من توزيع HDP ، ولكن يمكن أيضًا نشره في OKD أو أي تثبيت Kubernetes آخر باستخدام البيان المناسب ومجموعة من صور Docker ، مثل هذا - github.com/ttauveron/k8s-big-data-experiments/tree/master/livy-spark-2.3. بالنسبة لحالتنا ، تم إنشاء صورة Docker مماثلة ، بما في ذلك إصدار Spark 2.4.5 من 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"]

يمكن إنشاء الصورة التي تم إنشاؤها وتحميلها إلى مستودع Docker لديك ، مثل مستودع OKD الداخلي. لنشره ، استخدم البيان التالي ({Registry-url} - عنوان URL لسجل Docker image ، {image-name} - Docker image name ، {tag} - علامة صورة Docker ، {livy-url} - عنوان URL المطلوب حيث سيقوم الخادم كن متاحًا Livy ؛ يتم استخدام بيان "المسار" إذا تم استخدام Red Hat OpenShift كتوزيع Kubernetes ، وإلا فسيتم استخدام بيان الدخول أو الخدمة المقابل لنوع 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

بعد تطبيقه وتشغيل الكبسولة بنجاح ، تتوفر واجهة المستخدم الرسومية Livy على: http: // {livy-url} / ui. مع Livy ، يمكننا نشر مهمة Spark الخاصة بنا باستخدام طلب REST من Postman ، على سبيل المثال. يتم تقديم مثال لمجموعة مع طلبات أدناه (يمكن تمرير وسيطات التكوين مع المتغيرات اللازمة لعمل المهمة التي تم إطلاقها في مصفوفة "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": {}
}

دعنا ننفذ الطلب الأول من المجموعة ، وانتقل إلى واجهة OKD وتحقق من بدء المهمة بنجاح - https: // {OKD-WEBUI-URL} / console / project / {project} / browse / pods. في الوقت نفسه ، ستظهر جلسة في واجهة Livy (http: // {livy-url} / ui) ، والتي من خلالها ، باستخدام واجهة برمجة تطبيقات Livy أو واجهة رسومية ، يمكنك مراقبة تقدم المهمة ودراسة سجلات الجلسة.

الآن دعنا نظهر كيف تعمل ليفي. للقيام بذلك ، دعنا نفحص سجلات حاوية Livy داخل الكبسولة باستخدام خادم Livy - https: // {OKD-WEBUI-URL} / console / project / {project} / browse / pods / {livy-pod-name }؟ علامة التبويب = السجلات. يمكنك أن ترى منهم أنه عند استدعاء Livy REST API في حاوية تسمى "livy" ، يتم إجراء إرسال شرارة ، على غرار ما استخدمناه أعلاه (هنا {livy-pod-name} هو اسم الكبسولة التي تم إنشاؤها مع خادم Livy). توفر المجموعة أيضًا استعلامًا ثانيًا يسمح لك بتشغيل المهام مع الاستضافة عن بُعد لبرنامج Spark القابل للتنفيذ باستخدام خادم Livy.

حالة الاستخدام الثالثة - عامل الشرارة

الآن وقد تم اختبار المهمة ، يطرح السؤال حول إطلاقها المنتظم. الطريقة الأصلية لتشغيل المهام بانتظام في مجموعة Kubernetes هي كيان CronJob ويمكنك استخدامه ، ولكن في الوقت الحالي ، يحظى استخدام المشغلين لإدارة التطبيقات في Kubernetes بشعبية كبيرة وهناك عامل ناضج إلى حد ما لـ Spark ، والذي من بين أشياء أخرى ، يتم استخدامها في حلول على مستوى المؤسسة (على سبيل المثال ، Lightbend FastData Platform). نوصي باستخدامه - يحتوي الإصدار الثابت الحالي من Spark (2.4.5) على خيارات محدودة نوعًا ما لتكوين إطلاق مهام Spark في Kubernetes ، بينما يطالب الإصدار الرئيسي التالي (3.0.0) بالدعم الكامل لـ Kubernetes ، ولكن تاريخ إصداره ما زال مجهولا. يعوض Spark Operator عن ذلك عن طريق إضافة خيارات تكوين مهمة (مثل تركيب ConfigMap مع تكوين وصول Hadoop إلى Spark Pods) والقدرة على تشغيل مهمة مجدولة على أساس منتظم.

تشغيل Apache Spark على Kubernetes
دعنا نفرزها كحالة استخدام ثالثة - تشغيل مهام Spark بانتظام على مجموعة Kubernetes في حلقة إنتاج.

Spark Operator مفتوح المصدر وتم تطويره ضمن Google Cloud Platform - github.com/GoogleCloudPlatform/spark-on-k8s-operator. يمكن تثبيته بثلاث طرق:

  1. كجزء من تثبيت Lightbend FastData Platform / Cloudflow ؛
  2. مع هيلمز:
    helm repo add incubator http://storage.googleapis.com/kubernetes-charts-incubator
    helm install incubator/sparkoperator --namespace spark-operator
    	

  3. استخدام البيانات من المستودع الرسمي (https://github.com/GoogleCloudPlatform/spark-on-k8s-operator/tree/master/manifest). في الوقت نفسه ، تجدر الإشارة إلى ما يلي - يتضمن Cloudflow مشغلًا بإصدار API من v1beta1. إذا تم استخدام هذا النوع من التثبيت ، فيجب أن تستند أوصاف بيان تطبيق Spark إلى أمثلة من العلامات في Git مع إصدار API المناسب ، على سبيل المثال ، "v1beta1-0.9.0-2.4.0". يمكن الاطلاع على إصدار المشغل في وصف CRD الذي يعد جزءًا من المشغل في قاموس "الإصدارات":
    oc get crd sparkapplications.sparkoperator.k8s.io -o yaml
    	

إذا تم تثبيت المشغل بشكل صحيح ، فسيكون للمشروع المقابل بود نشط مع مشغل Spark (على سبيل المثال ، cloudflow-fdp-sparkoperator في مساحة Cloudflow لتثبيت Cloudflow) وسيظهر نوع مورد Kubernetes المقابل المسمى "sparkapplications". يمكنك استكشاف تطبيقات Spark المتاحة باستخدام الأمر التالي:

oc get sparkapplications -n {project}

لتشغيل المهام باستخدام Spark Operator ، عليك القيام بثلاثة أشياء:

  • قم بإنشاء صورة Docker تتضمن جميع المكتبات الضرورية ، بالإضافة إلى ملفات التكوين والملفات القابلة للتنفيذ. في الصورة المستهدفة ، هذه صورة تم إنشاؤها في مرحلة CI / CD واختبارها على مجموعة اختبار ؛
  • نشر صورة Docker في سجل يمكن الوصول إليه من مجموعة Kubernetes ؛
  • إنشاء بيان من النوع "SparkApplication" ووصف المهمة التي سيتم إطلاقها. تتوفر عينات البيانات في المستودع الرسمي (على سبيل المثال ، github.com/GoogleCloudPlatform/spark-on-k8s-operator/blob/v1beta1-0.9.0-2.4.0/examples/spark-pi.yaml). تجدر الإشارة إلى النقاط المهمة المتعلقة بالبيان:
    1. يجب أن يحتوي قاموس "apiVersion" على إصدار API المطابق لإصدار المشغل ؛
    2. يجب أن يحتوي قاموس "metadata.namespace" على مساحة الاسم التي سيتم تشغيل التطبيق فيها ؛
    3. يجب أن يحتوي قاموس "spec.image" على عنوان صورة Docker التي تم إنشاؤها في التسجيل المتاح ؛
    4. يجب أن يحتوي قاموس "spec.mainClass" على فئة مهمة Spark التي تريد تشغيلها عند بدء العملية ؛
    5. يجب أن يحتوي قاموس "spec.mainApplicationFile" على المسار إلى ملف jar القابل للتنفيذ ؛
    6. يجب أن يكون قاموس "spec.sparkVersion" هو إصدار Spark المستخدم ؛
    7. يجب أن يحتوي قاموس "spec.driver.serviceAccount" على حساب الخدمة ضمن مساحة اسم Kubernetes المقابلة التي سيتم استخدامها لتشغيل التطبيق ؛
    8. يجب أن يشير قاموس "spec.executor" إلى مقدار الموارد المخصصة للتطبيق ؛
    9. يجب تعيين قاموس "spec.volumeMounts" على الدليل المحلي حيث سيتم إنشاء ملفات مهمة Spark المحلية.

مثال على إنشاء بيان (هنا {spark-service-account} هو حساب خدمة ضمن مجموعة Kubernetes لتشغيل مهام 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"

يحدد هذا البيان حساب الخدمة الذي تحتاج إلى إنشاء روابط الأدوار اللازمة له قبل نشر البيان ، مما يوفر حقوق الوصول الضرورية لتطبيق Spark للتفاعل مع Kubernetes API (إذا لزم الأمر). في حالتنا ، يحتاج التطبيق إلى حقوق إنشاء Pods. لنقم بإنشاء دور ملزم:

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

تجدر الإشارة أيضًا إلى أنه يمكن تحديد معلمة "hadoopConfigMap" في مواصفات هذا البيان ، والذي يسمح لك بتحديد ConfigMap بتكوين Hadoop دون الحاجة إلى وضع الملف المقابل أولاً في صورة Docker. كما أنها مناسبة لبدء المهام بانتظام - باستخدام معلمة "الجدول الزمني" ، يمكن تحديد الجدول الزمني لبدء هذه المهمة.

بعد ذلك ، نحفظ البيان الخاص بنا في ملف spark-pi.yaml ونطبقه على مجموعة Kubernetes الخاصة بنا:

oc apply -f spark-pi.yaml

سيؤدي هذا إلى إنشاء كائن من النوع "sparkapplications":

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

سيؤدي هذا إلى إنشاء جراب مع تطبيق سيتم عرض حالته في "تطبيق شرارات" الذي تم إنشاؤه. يمكن مشاهدته بالأمر التالي:

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

عند الانتهاء من المهمة ، سينتقل POD إلى الحالة "مكتمل" ، والتي سيتم تحديثها أيضًا في "تطبيقات الشرارة". يمكن عرض سجلات التطبيق في المتصفح أو باستخدام الأمر التالي (هنا {sparkapplications-pod-name} هو اسم جراب المهام قيد التشغيل):

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

أيضًا ، يمكن إدارة مهام Spark باستخدام الأداة المساعدة sparkctl المتخصصة. لتثبيته ، قمنا باستنساخ المستودع برمز المصدر الخاص به ، وقم بتثبيت Go وإنشاء هذه الأداة المساعدة:

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

دعنا نفحص قائمة مهام Spark قيد التشغيل:

sparkctl list -n {project}

لنقم بإنشاء وصف لمهمة 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"

لنقم بتشغيل المهمة الموصوفة باستخدام sparkctl:

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

دعنا نفحص قائمة مهام Spark قيد التشغيل:

sparkctl list -n {project}

دعنا نفحص قائمة أحداث مهمة Spark قيد التشغيل:

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

دعنا نفحص حالة مهمة Spark قيد التشغيل:

sparkctl status spark-pi -n {project}

في الختام ، أود النظر في العيوب المكتشفة لتشغيل الإصدار الثابت الحالي من Spark (2.4.5) في Kubernetes:

  1. العيب الأول ، وربما العيب الرئيسي ، هو الافتقار إلى منطقة البيانات. على الرغم من جميع أوجه القصور في YARN ، كانت هناك إيجابيات في استخدامه ، على سبيل المثال ، مبدأ تسليم الكود إلى البيانات (بدلاً من البيانات للتشفير). بفضله ، تم تنفيذ مهام Spark على العقد حيث تم تحديد البيانات المتضمنة في الحسابات ، وتم تقليل الوقت اللازم لتسليم البيانات عبر الشبكة بشكل ملحوظ. عند استخدام Kubernetes ، نواجه الحاجة إلى نقل البيانات المتضمنة في عمل المهمة عبر الشبكة. إذا كانت كبيرة بما يكفي ، فيمكن زيادة وقت تنفيذ المهمة بشكل كبير ، ويلزم تخصيص مساحة كبيرة بما فيه الكفاية لمثيلات مهمة Spark لتخزينها المؤقت. يمكن تقليل هذا العيب باستخدام أدوات برمجية متخصصة توفر موقع البيانات في Kubernetes (على سبيل المثال ، Alluxio) ، ولكن هذا يعني في الواقع الحاجة إلى تخزين نسخة كاملة من البيانات على عقد مجموعة Kubernetes.
  2. الجانب السلبي الرئيسي الثاني هو الأمن. بشكل افتراضي ، يتم تعطيل الميزات المتعلقة بالأمان فيما يتعلق بتشغيل مهام Spark ، ولا يتم تغطية استخدام Kerberos في الوثائق الرسمية (على الرغم من ظهور الخيارات المقابلة في الإصدار 3.0.0 ، والتي ستتطلب مزيدًا من العمل) ، وفي وثائق الأمان عندما باستخدام Spark (https: //spark.apache.org/docs/2.4.5/security.html) تظهر فقط YARN و Mesos و Standalone Cluster كمخازن مفاتيح. في الوقت نفسه ، لا يمكن تحديد المستخدم الذي يتم بموجبه تشغيل مهام Spark بشكل مباشر - فنحن نعين فقط حساب الخدمة الذي سيعمل بموجبه ، ويتم تحديد المستخدم بناءً على سياسات الأمان التي تم تكوينها. في هذا الصدد ، يتم استخدام إما المستخدم الجذر ، وهو ليس آمنًا في بيئة إنتاجية ، أو مستخدمًا له UID عشوائي ، وهو أمر غير مريح عند توزيع حقوق الوصول إلى البيانات (يتم تحديده من خلال إنشاء PodSecurityPolicies وربطها بحسابات الخدمة المقابلة) . في الوقت الحالي ، يتمثل الحل إما في وضع جميع الملفات الضرورية مباشرةً في صورة Docker ، أو تعديل برنامج Spark لبدء التشغيل لاستخدام آلية تخزين واسترداد الأسرار المقبولة في مؤسستك.
  3. لا يزال تشغيل مهام Spark باستخدام Kubernetes رسميًا في الوضع التجريبي وقد تكون هناك تغييرات كبيرة في العناصر الأثرية المستخدمة (ملفات التكوين وصور قاعدة Docker ونصوص بدء التشغيل) في المستقبل. وبالفعل - عند تحضير المواد ، تم اختبار الإصدارين 2.3.0 و 2.4.5 ، كان السلوك مختلفًا بشكل كبير.

دعنا ننتظر التحديثات - تم إصدار إصدار جديد من Spark (3.0.0) مؤخرًا ، والذي أدخل تغييرات ملموسة على عمل Spark على Kubernetes ، لكنه احتفظ بالحالة التجريبية لدعم مدير الموارد هذا. ربما ستتيح التحديثات التالية حقًا التوصية بشكل كامل بالتخلي عن YARN وتشغيل مهام Spark على Kubernetes دون خوف على أمان نظامك ودون الحاجة إلى تحسين المكونات الوظيفية بنفسك.

نهاية

المصدر: www.habr.com

إضافة تعليق