[K8S] On-Demand Spark Cluster on AKS

最近在研究如何在 K8S 上面跑一個 On-Demand 的 Spark Cluster 服務,基本上有兩條路可以走,一條是利用 k8s 的 Deployment 來自建 Spark Cluster,另外一條路則是利用 Kubernetes 既有與 Spark 對接的介面 (這邊是利用 spark-submit) 來實作,概念上就是直接執行一個類似下方的指令,所以想要擁有一個 On-Demand Spark Cluster on AKS 這兩種方法個有什麼優劣?

$ ./bin/spark-submit \
    --master k8s://https://<k8s-apiserver-host>:<k8s-apiserver-port> \
    --deploy-mode cluster \
    --name spark-pi \
    --class org.apache.spark.examples.SparkPi \
    --conf spark.executor.instances=5 \
    --conf spark.kubernetes.container.image=<spark-image> \
    local:///path/to/examples.jar
 
方法一:利用 K8S 中的 Deployment 與 Services 自建 Spark Cluster

自建 Spark Cluster 的方法又可以分成兩個方法,分別是 Single-Pod 與 Multiple-Pods 兩種,Single-Pods 的部分,🍋 爸嘗試去利用以下的 Yaml 設定檔去產生一個Pod 包含多個 containers,但是由於使用的是 Azure Standard_D13_v2 的 VM 所以以下 Pod 索要的資源超過 8 cpus,因此無法成功開啟。備註:Microsoft 的工程師曾經實作部署一個 Pod 跨在不同的節點上,這邊還無法釐清是否能夠將同一個 Pod 的不同 container 部署在不同的節點上?

備註:後來從 Microsoft 的 Documentation 得知同一個 Pod 雖然是可以有多個 container 但是只能夠在同一個 node 上面。

apiVersion: v1
kind: Pod
metadata:
  name: spark-test
spec:
  containers:
    - name: master
      image: docker.io/spark:3.0.1
      ports:
        - containerPort: 8080
        - containerPort: 7077
      resources:
        limits:
          cpu: "3"
        requests:
          cpu: "3"
      imagePullPolicy: IfNotPresent
      command: ["/bin/sh", "-ec", "while :; do echo '.'; sleep 5 ; done"]
    - name: worker-1
      image: docker.io/spark:3.0.1
        - containerPort: 8081
      resources:
        limits:
          cpu: "3"
        requests:
          cpu: "3"
      imagePullPolicy: IfNotPresent
      command: ["/bin/sh", "-ec", "while :; do echo '.'; sleep 5 ; done"]
    - name: worker-2
      image: docker.io/spark:3.0.1
      ports:
        - containerPort: 8081
      resources:
        limits:
          cpu: "3"
        requests:
          cpu: "3"
      imagePullPolicy: IfNotPresent
      command: ["/bin/sh", "-ec", "while :; do echo '.'; sleep 5 ; done"]
  imagePullSecrets:
    - name: acr-secret

關於 Multiple-Pods 檸檬爸主要是參考連結,https://testdriven.io/blog/deploying-spark-on-kubernetes/ 實作了一個由一個 Master 與多個 Worker 所組成的 Spark Cluster,基本上是使用多個 Pods,然後再由 Ingress 將 Spark WebUI 導出。

其他參考利用 minikube 去做的範例

流程簡單可以分成幾個步驟:

  1. kubectl create -f ./spark-master-deployment.yaml 建立 Master Node
  2. kubectl create -f ./spark-master-service.yaml 將 master node 的 port 建立成一個 Service 
  3. kubectl create -f ./spark-worker-deployment.yaml 建立 worker node 並且利用 Service 去連結 Master Node

分別列出 spark-master-deployment.yaml

kind: Deployment
apiVersion: apps/v1
metadata:
  name: spark-master-1
spec:
  replicas: 1
  selector:
    matchLabels:
      component: spark-master
  template:
    metadata:
      labels:
        component: spark-master
    spec:
      containers:
        - name: spark-master
          image: docker.io/spark:3.0.1
          command: ["/bin/sh", "-ec"]
          args: ["/home/spark-current/sbin/start-master.sh && while :; do echo '.'; sleep 5 ; done"]
          ports:
            - containerPort: 7077
            - containerPort: 8080
          resources:
            requests:
              cpu: 100m

spark-master-service.yaml

kind: Service
apiVersion: v1
metadata:
  name: spark-master
spec:
  ports:
    - name: webui
      port: 8080
      targetPort: 8080
    - name: spark
      port: 7077
      targetPort: 7077
  selector:
    component: spark-master

spark-worker-deployment.yaml

kind: Deployment
apiVersion: apps/v1
metadata:
  name: spark-worker
spec:
  replicas: 6
  selector:
    matchLabels:
      component: spark-worker
  template:
    metadata:
      labels:
        component: spark-worker
    spec:
      containers:
        - name: spark-worker-1
          image: docker.io/spark:3.0.1
          env:
            - name: MASTER_SPARK_URL
              value: spark-master
          command: ["/bin/sh", "-c"]
          args: ["/home/spark-current/sbin/start-slave.sh spark://spark-master:7077 && while true; do sleep 3000; done"]
          ports:
            - containerPort: 8081
          resources:
            requests:
              cpu: 100m
      imagePullSecrets:
        - name: acr-secret

最後利用 kubectl port-forward spark-master-7f548444d6-vdk9s 8080:8080 可以看到 Spark Master 的 WebUI.

備註:這邊需要特別注意,在 spark cluster 建置完成之後,下達執行工作之前,需要特別標注兩個 configurations (spark.driver.bindAddress, spark.driver.host),如以下指令所示:

kubectl exec -it spark-master-xxxxxx -- spark-submit \
--conf spark.driver.bindAddress=10.244.3.16 \
--conf spark.driver.host=10.244.3.16 \
--conf spark.dynamicAllocation.enabled=false \
--class org.apache.spark.examples.SparkPi \
--master spark://spark-master:7077 \
/opt/spark/examples/jars/spark-examples_2.12-3.0.2.jar \
1000

否則會一直出現 TaskSchedulerImple 的錯誤訊息:

WARN TaskSchedulerImpl: Initial job has not accepted any resources; check your cluster UI to ensure that workers are registered and have sufficient resources
方法二:使用 spark-submit 交付工作到 Kubernetes

因應 k8s 社群裡面想要使用 Spark 技術的需求,k8s 也有支援直接從 spark-submit 將 spark job 送到 k8s 平台的選項,有興趣的讀者可以參考下面的 Youtube 影片介紹 spark + kubernetes。🍋 爸在參考官網上的資料之後(主要是AWS的服務的範例),在這邊主要記錄一些實作 spark-submit + azure kubernetes services 遇到的問題。

讀者可以直接使用成功送到 AKS 上的指令:
spark-submit --master k8s://http://127.0.0.1:8001 \
  --deploy-mode cluster \
  --name spark-pi \
  --class org.apache.spark.examples.SparkPi \
  --packages org.apache.hadoop:hadoop-azure:3.3.1,org.apache.hadoop:hadoop-common:3.3.1 \
  --conf spark.kubernetes.authenticate.driver.serviceAccountName=spark \
  --conf spark.hadoop.fs.azure.account.auth.type=OAuth \
  --conf spark.hadoop.fs.azure.account.oauth.provider.type=org.apache.hadoop.fs.azurebfs.oauth2.ClientCredsTokenProvider \
  --conf spark.hadoop.fs.azure.account.oauth2.client.id=xxxx\
  --conf spark.hadoop.fs.azure.account.oauth2.client.secret=xxxx \
  --conf spark.hadoop.fs.azure.account.oauth2.client.endpoint=https://login.microsoftonline.com/xxxx/oauth2/token \
  --conf spark.executor.instances=3 \
  --conf spark.kubernetes.file.upload.path=abfss://test@testaccount.dfs.core.windows.net/\
  --conf spark.kubernetes.container.image=docker.io/xxxx/spark:3.0.3 \
  --conf spark.driver.extraJavaOptions="-Divy.cache.dir=/tmp -Divy.home=/tmp" \
  ~/spark-3.0.3-bin-hadoop3.2/examples/jars/spark-examples_2.12-3.0.3.jar

Notes:

  1. 首先使用 kubectl proxy 將 localhost:8001 指向遠端的 Azure Kubernetes Services。
  2. 須提供 spark.kubernetes.file.upload.path 否則會報錯,由於我們是使用 AKS,所以提供以 abfss:// 開頭的路徑,要跑 Spark Job 所需要用到的 Jar 檔,最終會傳到這個 upload 的路徑。
  3. 加上 –package 並附上 hadoop-azure, hadoop-common 兩個函式庫。
  4. 配合第二點需要加上 auth.type, oauth.provider.type, cliend.id, client.secret, client.endpoint 等五個憑據權限。
  5. –conf 一定要有 spark.kubernetes.container.image 並且一定要是 spark package 裡面 build 出來的映像檔
  6. –conf 要加 spark.driver.extraJavaOptions=”-Divy.cache.dir=/tmp -Divy.home=/tmp”
Apache Spark on Kubernetes

Pros and Cons of these two methods:

    Method 1   Method 2
Preparation of Docker Image  👍🏻   Compatible with existing
docker images
👎🏻  Need to rebuild all docker images based on specific rules
 Control Flexibity 👍🏻 Concept of Worker Node is clear 👎🏻  Concept of Worker Node is not clear
Complexity of Workflow Management 👎🏻   Creation, Deletion, Monitoring of each Cluster needs to be well managed. 👍🏻   Easy to manage. Just submit and wait.