اجرای Apache Spark در Kubernetes

خوانندگان عزیز، ظهر بخیر. امروز کمی در مورد آپاچی اسپارک و چشم انداز توسعه آن صحبت خواهیم کرد.

اجرای Apache Spark در Kubernetes

در دنیای مدرن بیگ دیتا، آپاچی اسپارک استاندارد واقعی برای توسعه وظایف پردازش دسته ای داده است. علاوه بر این، از آن برای ایجاد برنامه‌های پخش جریانی استفاده می‌شود که در مفهوم میکرو دسته‌ای، پردازش و ارسال داده‌ها در بخش‌های کوچک (جریان‌سازی ساختار یافته جرقه) کار می‌کنند. و به طور سنتی بخشی از پشته کلی Hadoop بوده و از YARN (یا در برخی موارد Apache Mesos) به عنوان مدیر منابع استفاده می‌کند. تا سال 2020، استفاده از آن به شکل سنتی آن برای اکثر شرکت ها به دلیل عدم توزیع مناسب Hadoop زیر سوال می رود - توسعه HDP و CDH متوقف شده است، CDH به خوبی توسعه نیافته و هزینه بالایی دارد، و بقیه تامین کنندگان Hadoop یا دیگر وجود ندارد یا آینده ای مبهم خواهد داشت. بنابراین، راه‌اندازی آپاچی اسپارک با استفاده از Kubernetes مورد توجه جامعه و شرکت‌های بزرگ است - تبدیل شدن به استانداردی در ارکستراسیون کانتینر و مدیریت منابع در ابرهای خصوصی و عمومی، مشکل زمان‌بندی منابع نامناسب وظایف Spark در YARN را حل می‌کند و ارائه می‌کند. یک پلت فرم به طور پیوسته در حال توسعه با بسیاری از توزیع های تجاری و باز برای شرکت ها در هر اندازه و راه راه. علاوه بر این، در پی محبوبیت، بسیاری از آنها قبلاً موفق شده اند چندین نصب را از خود به دست آورند و تخصص خود را در استفاده از آن افزایش داده اند، که این حرکت را ساده می کند.

با شروع نسخه 2.3.0، Apache Spark پشتیبانی رسمی برای اجرای وظایف در یک خوشه Kubernetes به دست آورد و امروز، ما در مورد بلوغ فعلی این رویکرد، گزینه های مختلف برای استفاده از آن و مشکلاتی که در حین پیاده سازی با آن مواجه می شوند صحبت خواهیم کرد.

اول از همه، بیایید به روند توسعه وظایف و برنامه های کاربردی بر اساس Apache Spark نگاهی بیندازیم و موارد معمولی را که در آن شما نیاز به اجرای یک کار در یک کلاستر Kubernetes دارید را برجسته کنید. در تهیه این پست از OpenShift به عنوان توزیع استفاده می شود و دستورات مربوط به ابزار خط فرمان (oc) آن داده می شود. برای سایر توزیع‌های Kubernetes، می‌توان از دستورات مربوطه از ابزار خط فرمان استاندارد Kubernetes (kubectl) یا آنالوگ‌های آنها (مثلاً برای oc adm policy) استفاده کرد.

اولین مورد استفاده - spark-submit

در طول توسعه وظایف و برنامه های کاربردی، توسعه دهنده نیاز به اجرای وظایف برای اشکال زدایی تبدیل داده ها دارد. از نظر تئوری، خرده‌ها را می‌توان برای این اهداف استفاده کرد، اما توسعه با مشارکت نمونه‌های واقعی (البته آزمایشی) سیستم‌های نهایی ثابت کرده است که در این دسته از وظایف سریع‌تر و بهتر است. در موردی که ما در نمونه های واقعی سیستم های پایانی اشکال زدایی می کنیم، دو سناریو ممکن است:

  • توسعه دهنده یک وظیفه 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، شما واقعاً فقط به فایل های jar از دایرکتوری "assembly/" نیاز دارید، بنابراین فقط می توانید این زیر پروژه را بسازید:

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

برای اجرای Spark Jobs در Kubernetes، باید یک تصویر Docker ایجاد کنید تا از آن به عنوان تصویر پایه استفاده کنید. در اینجا 2 رویکرد ممکن وجود دارد:

  • تصویر 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 API خوشه OKD است):

oc login {OKD-API-URL}

بیایید توکن کاربر فعلی را برای مجوز در رجیستری Docker دریافت کنیم:

oc whoami -t

به رجیستری داخلی Docker خوشه 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} نشانی اینترنتی کنسول وب 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

در اینجا:

- نام - نام وظیفه ای که در شکل گیری نام غلاف های Kubernetes شرکت می کند.

—class — کلاس فایل اجرایی که هنگام شروع کار فراخوانی می شود.

—conf — پارامترهای پیکربندی Spark.

spark.executor.instances - تعداد اجراکنندگان Spark برای راه‌اندازی.

spark.kubernetes.authenticate.driver.serviceAccountName - نام حساب سرویس Kubernetes که هنگام راه‌اندازی پادها استفاده می‌شود (برای تعریف زمینه و قابلیت‌های امنیتی هنگام تعامل با Kubernetes API).

spark.kubernetes.namespace - فضای نام Kubernetes که در آن پادهای درایور و اجرا کننده راه اندازی می شوند.

spark.submit.deployMode - روش راه‌اندازی Spark (برای Spark-submit استاندارد از "cluster" استفاده می‌شود، برای Spark Operator و نسخه‌های بعدی Spark "client").

spark.kubernetes.container.image - تصویر داکر که برای راه اندازی غلاف ها استفاده می شود.

spark.master — URL API Kubernetes (خارجی مشخص شده است تا دسترسی از دستگاه محلی انجام شود).

local:// مسیری است که به Spark قابل اجرا در داخل تصویر داکر می رسد.

ما به پروژه OKD مربوطه می رویم و پادهای ایجاد شده را مطالعه می کنیم - https://{OKD-WEBUI-URL}/console/project/{project}/browse/pods.

برای ساده‌تر کردن فرآیند توسعه، می‌توان از گزینه دیگری استفاده کرد که در آن یک تصویر پایه مشترک از Spark ایجاد می‌شود که توسط همه وظایف برای اجرا استفاده می‌شود و عکس‌های فوری فایل‌های اجرایی در حافظه خارجی (مثلا Hadoop) منتشر می‌شوند و هنگام فراخوانی مشخص می‌شوند. spark-submit به عنوان لینک. در این حالت، می‌توانید نسخه‌های مختلف وظایف Spark را بدون بازسازی تصاویر Docker اجرا کنید، مثلاً از WebHDFS برای انتشار تصاویر استفاده کنید. ما درخواستی برای ایجاد فایل ارسال می کنیم (در اینجا {host} میزبان سرویس WebHDFS است، {port} پورت سرویس WebHDFS است، {path-to-file-on-hdfs} مسیر مورد نظر برای فایل است. در HDFS):

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

شما پاسخی مانند این دریافت خواهید کرد (در اینجا {location} آدرس اینترنتی است که باید برای دانلود فایل استفاده شود):

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-submit را با استفاده از فایل 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 و اسکریپت enterpoint.sh را تغییر دهید - یک دستورالعمل به Dockerfile اضافه کنید تا کتابخانه های وابسته را در پوشه /opt/spark/jars کپی کنید. فایل پیکربندی HDFS را در SPARK_CLASSPATH در نقطه ورودی قرار دهید.

مورد استفاده دوم - آپاچی لیوی

علاوه بر این، هنگامی که یک کار توسعه می یابد و نتیجه نیاز به آزمایش دارد، این سوال مطرح می شود که آن را به عنوان بخشی از فرآیند CI/CD راه اندازی کنید و وضعیت اجرای آن را ردیابی کنید. البته، می‌توانید آن را با استفاده از یک تماس محلی spark-submit اجرا کنید، اما این زیرساخت CI/CD را پیچیده می‌کند، زیرا نیاز به نصب و پیکربندی Spark روی عوامل/رانرهای سرور CI و راه‌اندازی دسترسی به Kubernetes API دارد. برای این مورد، پیاده‌سازی هدف استفاده از Apache Livy را به‌عنوان یک API REST برای اجرای وظایف Spark که در داخل یک خوشه Kubernetes میزبانی می‌شوند، انتخاب کرده است. با کمک آن می‌توانید وظایف Spark را روی یک خوشه Kubernetes با استفاده از درخواست‌های cURL معمولی اجرا کنید که به راحتی بر اساس هر راه‌حل CI پیاده‌سازی می‌شود و قرار دادن آن در داخل خوشه Kubernetes مشکل احراز هویت را هنگام تعامل با Kubernetes API حل می‌کند.

اجرای Apache Spark در Kubernetes

بیایید آن را به عنوان مورد دوم برجسته کنیم - اجرای وظایف Spark به عنوان بخشی از فرآیند CI/CD در یک خوشه Kubernetes در یک حلقه آزمایشی.

کمی در مورد Apache Livy - به عنوان یک سرور HTTP کار می کند که یک رابط وب و یک API RESTful ارائه می دهد که به شما امکان می دهد با عبور پارامترهای لازم، Spark-submit را از راه دور راه اندازی کنید. به‌طور سنتی به‌عنوان بخشی از توزیع HDP ارسال می‌شود، اما همچنین می‌تواند با استفاده از مانیفست مناسب و مجموعه‌ای از تصاویر Docker، مانند این، در OKD یا هر نصب دیگر Kubernetes مستقر شود. 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-name} - Docker image name, {tag} - Docker image tag, {livy-url} - URL دلخواه که در آن سرور قابل دسترسی Livy خواهد بود؛ اگر Red Hat OpenShift به عنوان توزیع Kubernetes استفاده شود، از مانیفست "Route" استفاده می شود، در غیر این صورت از مانیفست Ingress یا Service مربوطه از نوع 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 API یا رابط گرافیکی، می توانید پیشرفت کار را پیگیری کرده و جلسه را مطالعه کنید. سیاهههای مربوط

حالا بیایید نشان دهیم لیوی چگونه کار می کند. برای انجام این کار، اجازه دهید سیاهههای مربوط به کانتینر Livy را در داخل غلاف با سرور Livy بررسی کنیم - https://{OKD-WEBUI-URL}/console/project/{project}/browse/pods/{livy-pod-name }?tab=logs. از آن‌ها می‌توانیم ببینیم که هنگام فراخوانی Livy REST API در ظرفی به نام «livy»، یک spark-submit اجرا می‌شود، مشابه آنچه در بالا استفاده کردیم (در اینجا {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 pod) و توانایی اجرای یک کار برنامه ریزی شده منظم، این نقص را جبران می کند.

اجرای Apache Spark در Kubernetes
بیایید آن را به عنوان یک مورد استفاده سوم برجسته کنیم - به طور منظم وظایف Spark را در یک خوشه Kubernetes در یک حلقه تولید اجرا می کنیم.

Spark Operator منبع باز است و در Google Cloud Platform توسعه یافته است - github.com/GoogleCloudPlatform/spark-on-k8s-operator. نصب آن به 3 روش قابل انجام است:

  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
    	

اگر اپراتور به درستی نصب شده باشد، یک pod فعال با عملگر Spark در پروژه مربوطه ظاهر می شود (به عنوان مثال، cloudflow-fdp-sparkoperator در فضای Cloudflow برای نصب Cloudflow) و یک نوع منبع Kubernetes مربوطه با نام "sparkapplications" ظاهر می شود. . با دستور زیر می توانید برنامه های Spark موجود را کاوش کنید:

oc get sparkapplications -n {project}

برای اجرای وظایف با استفاده از Spark Operator باید 3 کار را انجام دهید:

  • یک تصویر 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

در این حالت، یک pod با یک برنامه ایجاد می شود که وضعیت آن در "sparkapplications" ایجاد شده نمایش داده می شود. با دستور زیر می توانید آن را مشاهده کنید:

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

پس از اتمام کار، POD به وضعیت "تکمیل" منتقل می شود، که در "sparkapplications" نیز به روز می شود. گزارش های برنامه را می توان در مرورگر یا با استفاده از دستور زیر مشاهده کرد (در اینجا {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 و پیوند دادن آنها به حساب های خدمات مربوطه). در حال حاضر، راه حل این است که یا تمام فایل های لازم را مستقیماً در تصویر داکر قرار دهید، یا اسکریپت راه اندازی Spark را برای استفاده از مکانیسم ذخیره و بازیابی اسرار مورد استفاده در سازمان خود تغییر دهید.
  3. اجرای کارهای Spark با استفاده از Kubernetes به طور رسمی هنوز در حالت آزمایشی است و ممکن است در آینده تغییرات قابل توجهی در مصنوعات مورد استفاده (فایل های پیکربندی، تصاویر پایه Docker و اسکریپت های راه اندازی) ایجاد شود. و در واقع، هنگام تهیه مواد، نسخه های 2.3.0 و 2.4.5 آزمایش شدند، رفتار به طور قابل توجهی متفاوت بود.

بیایید منتظر به روزرسانی ها باشیم - اخیراً نسخه جدیدی از Spark (3.0.0) منتشر شد که تغییرات قابل توجهی در کار Spark در Kubernetes ایجاد کرد ، اما وضعیت آزمایشی پشتیبانی را برای این مدیر منابع حفظ کرد. شاید به‌روزرسانی‌های بعدی واقعاً این امکان را به شما بدهد که به طور کامل از YARN و اجرای وظایف Spark در Kubernetes بدون ترس از امنیت سیستم خود و بدون نیاز به تغییر مستقل اجزای عملکردی استفاده کنید.

پایان.

منبع: www.habr.com

اضافه کردن نظر