Chạy Apache Spark trên Kubernetes

Các độc giả thân mến, xin chào buổi chiều. Hôm nay chúng ta sẽ nói một chút về Apache Spark và triển vọng phát triển của nó.

Chạy Apache Spark trên Kubernetes

Trong thế giới Big Data hiện đại, Apache Spark là tiêu chuẩn thực tế để phát triển các tác vụ xử lý dữ liệu hàng loạt. Ngoài ra, nó còn được sử dụng để tạo các ứng dụng phát trực tuyến hoạt động theo khái niệm lô vi mô, xử lý và vận chuyển dữ liệu theo từng phần nhỏ (Spark Structured Streaming). Và theo truyền thống, nó là một phần của ngăn xếp tổng thể của Hadoop, sử dụng YARN (hoặc trong một số trường hợp là Apache Mesos) làm trình quản lý tài nguyên. Đến năm 2020, việc sử dụng nó ở dạng truyền thống đang bị nghi ngờ đối với hầu hết các công ty do thiếu các bản phân phối Hadoop phù hợp - sự phát triển của HDP và CDH đã dừng lại, CDH không được phát triển tốt và có chi phí cao, và các nhà cung cấp Hadoop còn lại đã ngừng phát triển. hoặc không còn tồn tại hoặc có một tương lai mờ mịt. Do đó, việc ra mắt Apache Spark sử dụng Kubernetes đang ngày càng nhận được sự quan tâm của cộng đồng và các công ty lớn - trở thành một tiêu chuẩn trong điều phối container và quản lý tài nguyên trong các đám mây riêng và công cộng, nó giải quyết vấn đề lập lịch tài nguyên bất tiện cho các tác vụ Spark trên YARN và cung cấp một nền tảng đang phát triển ổn định với nhiều bản phân phối thương mại và mở cho các công ty thuộc mọi quy mô và lĩnh vực. Ngoài ra, trước sự phổ biến, hầu hết đã cố gắng có được một vài bản cài đặt của riêng mình và nâng cao kiến ​​thức chuyên môn trong việc sử dụng nó, điều này giúp đơn giản hóa việc di chuyển.

Bắt đầu với phiên bản 2.3.0, Apache Spark đã nhận được hỗ trợ chính thức để chạy các tác vụ trong cụm Kubernetes và hôm nay, chúng ta sẽ nói về mức độ hoàn thiện hiện tại của phương pháp này, các tùy chọn khác nhau để sử dụng và những cạm bẫy sẽ gặp phải trong quá trình triển khai.

Trước hết, chúng ta hãy xem quá trình phát triển các tác vụ và ứng dụng dựa trên Apache Spark và nêu bật các trường hợp điển hình mà bạn cần chạy một tác vụ trên cụm Kubernetes. Khi chuẩn bị bài đăng này, OpenShift được sử dụng làm bản phân phối và các lệnh liên quan đến tiện ích dòng lệnh (oc) của nó sẽ được đưa ra. Đối với các bản phân phối Kubernetes khác, có thể sử dụng các lệnh tương ứng từ tiện ích dòng lệnh Kubernetes tiêu chuẩn (kubectl) hoặc các lệnh tương tự của chúng (ví dụ: đối với chính sách oc adm).

Trường hợp sử dụng đầu tiên - gửi tia lửa

Trong quá trình phát triển các tác vụ và ứng dụng, nhà phát triển cần chạy các tác vụ để gỡ lỗi chuyển đổi dữ liệu. Về mặt lý thuyết, sơ khai có thể được sử dụng cho những mục đích này, nhưng việc phát triển với sự tham gia của các phiên bản thực (mặc dù là thử nghiệm) của hệ thống cuối đã được chứng minh là nhanh hơn và tốt hơn trong loại nhiệm vụ này. Trong trường hợp chúng tôi gỡ lỗi trên các phiên bản thực của hệ thống cuối, có thể xảy ra hai trường hợp:

  • nhà phát triển chạy tác vụ Spark cục bộ ở chế độ độc lập;

    Chạy Apache Spark trên Kubernetes

  • nhà phát triển chạy tác vụ Spark trên cụm Kubernetes trong vòng thử nghiệm.

    Chạy Apache Spark trên Kubernetes

Tùy chọn đầu tiên có quyền tồn tại, nhưng kéo theo một số nhược điểm:

  • Mỗi nhà phát triển phải được cung cấp quyền truy cập từ nơi làm việc vào tất cả các phiên bản của hệ thống cuối mà anh ta cần;
  • cần có đủ lượng tài nguyên trên máy làm việc để chạy tác vụ đang được phát triển.

Tùy chọn thứ hai không có những nhược điểm này, vì việc sử dụng cụm Kubernetes cho phép bạn phân bổ nhóm tài nguyên cần thiết để chạy các tác vụ và cung cấp cho nó quyền truy cập cần thiết vào các phiên bản hệ thống cuối, cung cấp quyền truy cập linh hoạt vào nó bằng mô hình vai trò Kubernetes cho tất cả các thành viên của nhóm phát triển. Hãy đánh dấu nó là trường hợp sử dụng đầu tiên - khởi chạy các tác vụ Spark từ máy của nhà phát triển cục bộ trên cụm Kubernetes trong vòng thử nghiệm.

Hãy nói thêm về quá trình thiết lập Spark để chạy cục bộ. Để bắt đầu sử dụng Spark bạn cần cài đặt nó:

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

Chúng tôi thu thập các gói cần thiết để làm việc với Kubernetes:

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

Quá trình xây dựng đầy đủ mất rất nhiều thời gian và để tạo hình ảnh Docker và chạy chúng trên cụm Kubernetes, bạn thực sự chỉ cần các tệp jar từ thư mục “assembly/”, vì vậy bạn chỉ có thể xây dựng dự án con này:

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

Để chạy các công việc Spark trên Kubernetes, bạn cần tạo hình ảnh Docker để sử dụng làm hình ảnh cơ sở. Có 2 cách tiếp cận có thể ở đây:

  • Hình ảnh Docker được tạo bao gồm mã tác vụ Spark có thể thực thi được;
  • Hình ảnh được tạo chỉ bao gồm Spark và các phần phụ thuộc cần thiết, mã thực thi được lưu trữ từ xa (ví dụ: trong HDFS).

Trước tiên, hãy xây dựng hình ảnh Docker chứa ví dụ thử nghiệm về tác vụ Spark. Để tạo Docker image, Spark có một tiện ích tên là "docker-image-tool". Hãy nghiên cứu sự trợ giúp về nó:

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

Với sự trợ giúp của nó, bạn có thể tạo hình ảnh Docker và tải chúng lên cơ quan đăng ký từ xa, nhưng theo mặc định, nó có một số nhược điểm:

  • chắc chắn sẽ tạo 3 hình ảnh Docker cùng một lúc - cho Spark, PySpark và R;
  • không cho phép bạn chỉ định tên hình ảnh.

Do đó, chúng tôi sẽ sử dụng phiên bản sửa đổi của tiện ích này được đưa ra dưới đây:

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

Với sự trợ giúp của nó, chúng tôi tập hợp một hình ảnh Spark cơ bản chứa tác vụ thử nghiệm tính toán Pi bằng Spark (ở đây {docker-registry-url} là URL của sổ đăng ký hình ảnh Docker của bạn, {repo} là tên của kho lưu trữ bên trong sổ đăng ký, phù hợp với dự án trong OpenShift , {image-name} - tên của hình ảnh (ví dụ: nếu sử dụng tính năng phân tách ba cấp độ của hình ảnh, như trong sổ đăng ký tích hợp của hình ảnh Red Hat OpenShift), {tag} - thẻ này phiên bản của hình ảnh):

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

Đăng nhập vào cụm OKD bằng tiện ích bảng điều khiển (ở đây {OKD-API-URL} là URL API cụm OKD):

oc login {OKD-API-URL}

Hãy lấy mã thông báo của người dùng hiện tại để ủy quyền trong Sổ đăng ký Docker:

oc whoami -t

Đăng nhập vào Docker Register nội bộ của cụm OKD (chúng tôi sử dụng mã thông báo thu được bằng lệnh trước đó làm mật khẩu):

docker login {docker-registry-url}

Hãy tải hình ảnh Docker đã được lắp ráp lên Docker Đăng ký OKD:

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

Hãy kiểm tra xem hình ảnh được ghép có sẵn trong OKD hay không. Để thực hiện việc này, hãy mở URL trong trình duyệt có danh sách hình ảnh của dự án tương ứng (ở đây {project} là tên của dự án bên trong cụm OpenShift, {OKD-WEBUI-URL} là URL của bảng điều khiển Web OpenShift ) - https://{OKD-WEBUI-URL}/console /project/{project}/browse/images/{image-name}.

Để chạy các tác vụ, một tài khoản dịch vụ phải được tạo với các đặc quyền để chạy các nhóm với quyền root (chúng ta sẽ thảo luận về điểm này sau):

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

Hãy chạy lệnh spark-submit để xuất bản tác vụ Spark lên cụm OKD, chỉ định tài khoản dịch vụ đã tạo và hình ảnh 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

Đây:

—name - tên của nhiệm vụ sẽ tham gia vào việc hình thành tên của nhóm Kubernetes;

—class - lớp của tệp thực thi, được gọi khi tác vụ bắt đầu;

—conf - Tham số cấu hình Spark;

spark.executor.instances - số lượng trình thực thi Spark sẽ khởi chạy;

spark.kubernetes.authenticate.driver.serviceAccountName - tên của tài khoản dịch vụ Kubernetes được sử dụng khi khởi chạy nhóm (để xác định bối cảnh và khả năng bảo mật khi tương tác với API Kubernetes);

spark.kubernetes.namespace - Không gian tên Kubernetes trong đó nhóm trình điều khiển và người thực thi sẽ được khởi chạy;

spark.submit.deployMode - phương thức khởi chạy Spark (đối với “cụm” gửi tia lửa tiêu chuẩn được sử dụng cho Spark Operator và các phiên bản mới hơn của Spark “client”);

spark.kubernetes.container.image - Hình ảnh Docker được sử dụng để khởi chạy nhóm;

spark.master - URL API Kubernetes (bên ngoài được chỉ định để quyền truy cập diễn ra từ máy cục bộ);

local:// là đường dẫn đến tệp thực thi Spark bên trong hình ảnh Docker.

Chúng tôi đi đến dự án OKD tương ứng và nghiên cứu các nhóm đã tạo - https://{OKD-WEBUI-URL}/console/project/{project}/browse/pod.

Để đơn giản hóa quá trình phát triển, có thể sử dụng một tùy chọn khác, trong đó hình ảnh cơ sở chung của Spark được tạo, được tất cả các tác vụ sử dụng để chạy và ảnh chụp nhanh của các tệp thực thi được xuất bản lên bộ nhớ ngoài (ví dụ: Hadoop) và được chỉ định khi gọi spark-submit dưới dạng liên kết. Trong trường hợp này, bạn có thể chạy các phiên bản khác nhau của tác vụ Spark mà không cần xây dựng lại hình ảnh Docker, chẳng hạn như sử dụng WebHDFS để xuất bản hình ảnh. Chúng ta gửi yêu cầu tạo file (ở đây {host} là máy chủ của dịch vụ WebHDFS, {port} là cổng của dịch vụ WebHDFS, {path-to-file-on-hdfs} là đường dẫn mong muốn tới file trên HDFS):

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

Bạn sẽ nhận được phản hồi như thế này (ở đây {location} là URL cần được sử dụng để tải xuống tệp):

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

Tải tệp thực thi Spark vào HDFS (ở đây {path-to-local-file} là đường dẫn đến tệp thực thi Spark trên máy chủ hiện tại):

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

Sau này, chúng ta có thể thực hiện spark-submit bằng tệp Spark được tải lên HDFS (ở đây {class-name} là tên của lớp cần được khởi chạy để hoàn thành nhiệm vụ):

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

Cần lưu ý rằng để truy cập HDFS và đảm bảo tác vụ hoạt động, bạn có thể cần thay đổi tập lệnh Dockerfile và entrypoint.sh - thêm một lệnh vào Dockerfile để sao chép các thư viện phụ thuộc vào thư mục /opt/spark/jars và bao gồm tệp cấu hình HDFS trong SPARK_CLASSPATH trong entrypoint.sh.

Trường hợp sử dụng thứ hai - Apache Livy

Hơn nữa, khi một tác vụ được phát triển và kết quả cần được kiểm tra, câu hỏi đặt ra là khởi chạy nó như một phần của quy trình CI/CD và theo dõi trạng thái thực thi của nó. Tất nhiên, bạn có thể chạy nó bằng lệnh gọi spark-submit cục bộ, nhưng điều này làm phức tạp cơ sở hạ tầng CI/CD vì nó yêu cầu cài đặt và định cấu hình Spark trên tác nhân/trình chạy máy chủ CI và thiết lập quyền truy cập vào API Kubernetes. Trong trường hợp này, quá trình triển khai mục tiêu đã chọn sử dụng Apache Livy làm API REST để chạy các tác vụ Spark được lưu trữ bên trong cụm Kubernetes. Với sự trợ giúp của nó, bạn có thể chạy các tác vụ Spark trên cụm Kubernetes bằng cách sử dụng các yêu cầu cURL thông thường, dễ dàng triển khai dựa trên bất kỳ giải pháp CI nào và vị trí của nó trong cụm Kubernetes giải quyết vấn đề xác thực khi tương tác với API Kubernetes.

Chạy Apache Spark trên Kubernetes

Hãy đánh dấu nó như trường hợp sử dụng thứ hai - chạy các tác vụ Spark như một phần của quy trình CI/CD trên cụm Kubernetes trong vòng thử nghiệm.

Một chút về Apache Livy - nó hoạt động như một máy chủ HTTP cung cấp giao diện Web và API RESTful cho phép bạn khởi chạy spark-submit từ xa bằng cách chuyển các tham số cần thiết. Theo truyền thống, nó được vận chuyển như một phần của bản phân phối HDP, nhưng cũng có thể được triển khai tới OKD hoặc bất kỳ bản cài đặt Kubernetes nào khác bằng cách sử dụng tệp kê khai thích hợp và một bộ hình ảnh Docker, chẳng hạn như cái này - github.com/ttauveron/k8s-big-data-experiments/tree/master/livy-spark-2.3. Đối với trường hợp của chúng tôi, một hình ảnh Docker tương tự đã được tạo, bao gồm Spark phiên bản 2.4.5 từ Dockerfile sau:

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

Hình ảnh được tạo có thể được xây dựng và tải lên kho Docker hiện có của bạn, chẳng hạn như kho lưu trữ OKD nội bộ. Để triển khai nó, hãy sử dụng tệp kê khai sau ({registry-url} - URL của sổ đăng ký hình ảnh Docker, {image-name} - Tên hình ảnh Docker, {tag} - Thẻ hình ảnh Docker, {livy-url} - URL mong muốn nơi máy chủ sẽ có thể truy cập được Livy; bảng kê khai “Tuyến đường” được sử dụng nếu Red Hat OpenShift được sử dụng làm bản phân phối Kubernetes, nếu không thì bảng kê khai Ingress hoặc Service tương ứng của loại NodePort được sử dụng):

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

Sau khi áp dụng và khởi chạy thành công nhóm, giao diện đồ họa Livy có sẵn tại liên kết: http://{livy-url}/ui. Với Livy, chúng tôi có thể xuất bản tác vụ Spark của mình bằng cách sử dụng yêu cầu REST từ Người đưa thư chẳng hạn. Một ví dụ về tập hợp các yêu cầu được trình bày bên dưới (các đối số cấu hình với các biến cần thiết cho hoạt động của tác vụ đã khởi chạy có thể được truyền trong mảng “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": {}
}

Hãy thực hiện yêu cầu đầu tiên từ bộ sưu tập, đi tới giao diện OKD và kiểm tra xem tác vụ đã được khởi chạy thành công chưa - https://{OKD-WEBUI-URL}/console/project/{project}/browse/pod. Đồng thời, một phiên sẽ xuất hiện trong giao diện Livy (http://{livy-url}/ui), trong đó, bằng cách sử dụng API Livy hoặc giao diện đồ họa, bạn có thể theo dõi tiến trình của nhiệm vụ và nghiên cứu phiên nhật ký.

Bây giờ hãy cho thấy Livy hoạt động như thế nào. Để thực hiện việc này, hãy kiểm tra nhật ký của vùng chứa Livy bên trong nhóm bằng máy chủ Livy - https://{OKD-WEBUI-URL}/console/project/{project}/browse/pod/{livy-pod-name }?tab=log. Từ đó, chúng ta có thể thấy rằng khi gọi API Livy REST trong vùng chứa có tên “livy”, một spark-submit được thực thi, tương tự như API chúng ta đã sử dụng ở trên (ở đây {livy-pod-name} là tên của nhóm được tạo với máy chủ Livy). Bộ sưu tập cũng giới thiệu một truy vấn thứ hai cho phép bạn chạy các tác vụ lưu trữ từ xa tệp thực thi Spark bằng máy chủ Livy.

Trường hợp sử dụng thứ ba - Toán tử Spark

Bây giờ tác vụ đã được kiểm tra, câu hỏi về việc chạy nó thường xuyên sẽ nảy sinh. Cách gốc để thường xuyên chạy các tác vụ trong cụm Kubernetes là thực thể CronJob và bạn có thể sử dụng nó, nhưng hiện tại việc sử dụng toán tử để quản lý ứng dụng trong Kubernetes rất phổ biến và đối với Spark thì có một toán tử khá hoàn thiện, đó cũng là được sử dụng trong các giải pháp cấp doanh nghiệp (ví dụ: Nền tảng Lightbend FastData). Chúng tôi khuyên bạn nên sử dụng nó - phiên bản ổn định hiện tại của Spark (2.4.5) có các tùy chọn cấu hình khá hạn chế để chạy các tác vụ Spark trong Kubernetes, trong khi phiên bản chính tiếp theo (3.0.0) tuyên bố hỗ trợ đầy đủ cho Kubernetes, nhưng vẫn chưa rõ ngày phát hành . Spark Operator bù đắp cho thiếu sót này bằng cách thêm các tùy chọn cấu hình quan trọng (ví dụ: gắn Bản đồ cấu hình với cấu hình truy cập Hadoop vào nhóm Spark) và khả năng chạy tác vụ được lên lịch thường xuyên.

Chạy Apache Spark trên Kubernetes
Hãy đánh dấu nó như trường hợp sử dụng thứ ba - thường xuyên chạy các tác vụ Spark trên cụm Kubernetes trong vòng lặp sản xuất.

Spark Operator là mã nguồn mở và được phát triển trong Google Cloud Platform - github.com/GoogleCloudPlatform/spark-on-k8s-operator. Việc cài đặt nó có thể được thực hiện theo 3 cách:

  1. Là một phần của quá trình cài đặt Lightbend FastData Platform/Cloudflow;
  2. Sử dụng mũ bảo hiểm:
    helm repo add incubator http://storage.googleapis.com/kubernetes-charts-incubator
    helm install incubator/sparkoperator --namespace spark-operator
    	

  3. Sử dụng bảng kê khai từ kho lưu trữ chính thức (https://github.com/GoogleCloudPlatform/spark-on-k8s-operator/tree/master/manifest). Điều đáng chú ý sau - Cloudflow bao gồm một toán tử có phiên bản API v1beta1. Nếu loại cài đặt này được sử dụng, mô tả bảng kê khai ứng dụng Spark phải dựa trên các thẻ mẫu trong Git với phiên bản API thích hợp, ví dụ: "v1beta1-0.9.0-2.4.0". Bạn có thể tìm thấy phiên bản của toán tử trong phần mô tả CRD có trong toán tử trong từ điển “phiên bản”:
    oc get crd sparkapplications.sparkoperator.k8s.io -o yaml
    	

Nếu toán tử được cài đặt chính xác, một nhóm hoạt động có toán tử Spark sẽ xuất hiện trong dự án tương ứng (ví dụ: cloudflow-fdp-sparkoperator trong không gian Cloudflow để cài đặt Cloudflow) và loại tài nguyên Kubernetes tương ứng có tên “sparkapplications” sẽ xuất hiện . Bạn có thể khám phá các ứng dụng Spark có sẵn bằng lệnh sau:

oc get sparkapplications -n {project}

Để chạy các tác vụ bằng Spark Operator bạn cần thực hiện 3 việc:

  • tạo hình ảnh Docker bao gồm tất cả các thư viện cần thiết cũng như các tệp cấu hình và tệp thực thi. Trong ảnh đích, đây là ảnh được tạo ở giai đoạn CI/CD và được thử nghiệm trên cụm thử nghiệm;
  • xuất bản hình ảnh Docker lên sổ đăng ký có thể truy cập được từ cụm Kubernetes;
  • tạo một bảng kê khai có loại “SparkApplication” và mô tả về tác vụ sẽ được khởi chạy. Các bảng kê khai mẫu có sẵn trong kho lưu trữ chính thức (ví dụ: github.com/GoogleCloudPlatform/spark-on-k8s-operator/blob/v1beta1-0.9.0-2.4.0/examples/spark-pi.yaml). Có những điểm quan trọng cần lưu ý về bản tuyên ngôn:
    1. từ điển “apiVersion” phải chỉ ra phiên bản API tương ứng với phiên bản của nhà điều hành;
    2. từ điển “metadata.namespace” phải chỉ ra không gian tên mà ứng dụng sẽ được khởi chạy trong đó;
    3. từ điển “spec.image” phải chứa địa chỉ của hình ảnh Docker đã tạo trong sổ đăng ký có thể truy cập được;
    4. từ điển “spec.mainClass” phải chứa lớp tác vụ Spark cần được chạy khi quá trình bắt đầu;
    5. từ điển “spec.mainApplicationFile” phải chứa đường dẫn đến tệp jar thực thi;
    6. từ điển “spec.sparkVersion” phải cho biết phiên bản Spark đang được sử dụng;
    7. từ điển “spec.driver.serviceAccount” phải chỉ định tài khoản dịch vụ trong vùng tên Kubernetes tương ứng sẽ được sử dụng để chạy ứng dụng;
    8. từ điển “spec.executor” phải cho biết số lượng tài nguyên được phân bổ cho ứng dụng;
    9. từ điển "spec.volumeMounts" phải chỉ định thư mục cục bộ nơi các tệp tác vụ Spark cục bộ sẽ được tạo.

Một ví dụ về cách tạo tệp kê khai (ở đây {spark-service-account} là tài khoản dịch vụ bên trong cụm Kubernetes để chạy các tác vụ 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"

Tệp kê khai này chỉ định một tài khoản dịch vụ mà trước khi xuất bản tệp kê khai, bạn phải tạo các ràng buộc vai trò cần thiết để cung cấp các quyền truy cập cần thiết để ứng dụng Spark tương tác với API Kubernetes (nếu cần). Trong trường hợp của chúng tôi, ứng dụng cần có quyền để tạo Pod. Hãy tạo ràng buộc vai trò cần thiết:

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

Cũng cần lưu ý rằng thông số kỹ thuật của tệp kê khai này có thể bao gồm tham số "hadoopConfigMap", cho phép bạn chỉ định Bản đồ cấu hình với cấu hình Hadoop mà không cần phải đặt tệp tương ứng vào hình ảnh Docker trước tiên. Nó cũng thích hợp để chạy các tác vụ thường xuyên - bằng cách sử dụng tham số "lịch trình", có thể chỉ định lịch chạy một tác vụ nhất định.

Sau đó, chúng ta lưu tệp kê khai vào tệp spark-pi.yaml và áp dụng nó vào cụm Kubernetes:

oc apply -f spark-pi.yaml

Điều này sẽ tạo ra một đối tượng kiểu “sparkapplications”:

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

Trong trường hợp này, một nhóm có ứng dụng sẽ được tạo, trạng thái của ứng dụng này sẽ được hiển thị trong “ứng dụng tia lửa” đã tạo. Bạn có thể xem nó bằng lệnh sau:

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

Sau khi hoàn thành nhiệm vụ, POD sẽ chuyển sang trạng thái “Đã hoàn thành”, trạng thái này cũng sẽ cập nhật trong “ứng dụng spark”. Nhật ký ứng dụng có thể được xem trong trình duyệt hoặc sử dụng lệnh sau (ở đây {sparkapplications-pod-name} là tên của nhóm tác vụ đang chạy):

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

Các tác vụ Spark cũng có thể được quản lý bằng tiện ích sparkctl chuyên dụng. Để cài đặt nó, hãy sao chép kho lưu trữ với mã nguồn của nó, cài đặt Go và xây dựng tiện ích này:

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

Hãy kiểm tra danh sách các tác vụ Spark đang chạy:

sparkctl list -n {project}

Hãy tạo một mô tả cho tác vụ 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"

Hãy chạy tác vụ được mô tả bằng sparkctl:

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

Hãy kiểm tra danh sách các tác vụ Spark đang chạy:

sparkctl list -n {project}

Hãy kiểm tra danh sách các sự kiện của tác vụ Spark đã khởi chạy:

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

Hãy kiểm tra trạng thái của tác vụ Spark đang chạy:

sparkctl status spark-pi -n {project}

Để kết luận, tôi muốn xem xét những nhược điểm đã được phát hiện khi sử dụng phiên bản ổn định hiện tại của Spark (2.4.5) trong Kubernetes:

  1. Nhược điểm đầu tiên và có lẽ là chính là thiếu Địa phương dữ liệu. Bất chấp tất cả những thiếu sót của YARN, việc sử dụng nó cũng có những lợi thế, chẳng hạn như nguyên tắc phân phối mã tới dữ liệu (thay vì dữ liệu sang mã). Nhờ đó, các tác vụ Spark đã được thực thi trên các nút nơi chứa dữ liệu liên quan đến tính toán và thời gian phân phối dữ liệu qua mạng đã giảm đáng kể. Khi sử dụng Kubernetes, chúng ta phải đối mặt với nhu cầu di chuyển dữ liệu liên quan đến một tác vụ trên mạng. Nếu chúng đủ lớn, thời gian thực hiện tác vụ có thể tăng đáng kể và cũng yêu cầu một lượng không gian đĩa khá lớn được phân bổ cho các phiên bản tác vụ Spark để lưu trữ tạm thời. Nhược điểm này có thể được giảm thiểu bằng cách sử dụng phần mềm chuyên dụng đảm bảo tính cục bộ của dữ liệu trong Kubernetes (ví dụ: Alluxio), nhưng điều này thực sự có nghĩa là cần phải lưu trữ bản sao hoàn chỉnh của dữ liệu trên các nút cụm Kubernetes.
  2. Nhược điểm quan trọng thứ hai là bảo mật. Theo mặc định, các tính năng liên quan đến bảo mật liên quan đến việc chạy các tác vụ Spark bị tắt, việc sử dụng Kerberos không được đề cập trong tài liệu chính thức (mặc dù các tùy chọn tương ứng đã được giới thiệu trong phiên bản 3.0.0, sẽ yêu cầu công việc bổ sung) và tài liệu bảo mật cho sử dụng Spark (https://spark.apache.org/docs/2.4.5/security.html) chỉ YARN, Mesos và Cụm độc lập mới xuất hiện dưới dạng kho lưu trữ chính. Đồng thời, không thể chỉ định trực tiếp người dùng thực hiện các tác vụ Spark - chúng tôi chỉ chỉ định tài khoản dịch vụ mà nó sẽ hoạt động và người dùng được chọn dựa trên các chính sách bảo mật đã định cấu hình. Về vấn đề này, người dùng root được sử dụng, điều này không an toàn trong môi trường hiệu quả hoặc người dùng có UID ngẫu nhiên, điều này gây bất tiện khi phân phối quyền truy cập vào dữ liệu (điều này có thể được giải quyết bằng cách tạo PodSecurityPolicies và liên kết chúng với tài khoản dịch vụ tương ứng). Hiện tại, giải pháp là đặt tất cả các tệp cần thiết trực tiếp vào hình ảnh Docker hoặc sửa đổi tập lệnh khởi chạy Spark để sử dụng cơ chế lưu trữ và truy xuất các bí mật được áp dụng trong tổ chức của bạn.
  3. Chạy các công việc Spark bằng Kubernetes chính thức vẫn ở chế độ thử nghiệm và có thể có những thay đổi đáng kể về các tạo phẩm được sử dụng (tệp cấu hình, hình ảnh cơ sở Docker và tập lệnh khởi chạy) trong tương lai. Và thực sự, khi chuẩn bị tài liệu, phiên bản 2.3.0 và 2.4.5 đã được thử nghiệm, hoạt động khác biệt đáng kể.

Hãy chờ đợi các bản cập nhật - một phiên bản mới của Spark (3.0.0) đã được phát hành gần đây, mang lại những thay đổi đáng kể cho hoạt động của Spark trên Kubernetes, nhưng vẫn giữ trạng thái thử nghiệm hỗ trợ cho trình quản lý tài nguyên này. Có lẽ các bản cập nhật tiếp theo sẽ thực sự giúp bạn hoàn toàn có thể khuyến nghị từ bỏ YARN và chạy các tác vụ Spark trên Kubernetes mà không lo sợ về tính bảo mật của hệ thống và không cần phải sửa đổi độc lập các thành phần chức năng.

Vây.

Nguồn: www.habr.com

Thêm một lời nhận xét