Đăng vào

Triển khai Apache Spark trên Kubernetes bằng ArgoCD

Apache Spark là một hệ thống xử lý dữ liệu phân tán mã nguồn mở, được sử dụng rộng rãi để xử lý lớn lượng dữ liệu và tính toán phức tạp. Nếu chúng ta cài đặt Spark trên Kubernetes, bạn có thể tận dụng containerization và khả năng chạy ứng dụng Spark trong môi trường cô lập hoàn toàn, đồng thời tiết kiệm chi phí bằng việc sử dụng cơ sở hạ tầng chung.

Khi triển khai Apache Spark trên Kubernetes chúng ta nên sử dụng Operator để dễ dàng thiết lập và cài đặt trên cluster. Ỏ thời điêm hiện tại, mình thấy rằng có 2 nhà phát triển Spark Operator:

Operator của Apache có vẻ như đang phát triển và chưa hoàn thiện, nên trong ví dụ dưới đây mình sử dụng Operator do Kubeflow cung cấp.

Cài đặt Spark Operator

argocd.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: apache-spark
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
  labels:
    name: apache-spark
spec:
  syncPolicy:
    automated:
      selfHeal: true
      prune: true
      allowEmpty: false
    syncOptions:
      - Validate=false
      - CreateNamespace=true
      - PrunePropagationPolicy=foreground
      - PruneLast=true
      - RespectIgnoreDifferences=true
      - Replace=true
  project: default
  source:
    chart: spark-operator
    repoURL: https://kubeflow.github.io/spark-operator
    targetRevision: 2.*
    helm:
      releaseName: spark-operator
      passCredentials: false
      parameters:
        - name: controller.workers
          value: "1"
        - name: controller.resources.limits.cpu
          value: "0.4"
        - name: controller.resources.limits.memory
          value: 1Gi
        - name: controller.resources.requests.cpu
          value: "0.2"
        - name: controller.resources.requests.memory
          value: 512Mi
        - name: controller.uiIngress.enable
          value: "true"
        - name: controller.uiIngress.urlFormat
          value: <spark-domain.example.com>/{{$$appNamespace}}/{{$$appName}}
        - name: controller.uiIngress.ingressClassName
          value: nginx
        - name: controller.serviceAccount.name
          value: spark-operator-controller
        - name: webhook.workers
          value: "1"
        - name: webhook.resources.limits.cpu
          value: "0.25"
        - name: webhook.resources.limits.memory
          value: 1Gi
        - name: webhook.resources.requests.cpu
          value: "0.1"
        - name: webhook.resources.requests.memory
          value: 256Mi
        - name: spark.jobNamespaces[0]
          value: spark
        - name: spark.serviceAccount.name
          value: spark-operator-spark
  destination:
    server: "https://kubernetes.default.svc"
    namespace: spark-operator
  revisionHistoryLimit: 3

Sau khi đã khởi tạo file YAML ở trên, chúng ta tiến hành deploy:

kubectl create ns spark # Job sẽ chạy ở 1 namespace khác, và ở đây đặt tên là spark
kubectl apply -f argocd.yaml

Sau một lúc chúng ta kiểm tra namespace spark-oprator, kết quả trả về như sau

kubectl -n spark-operator get all
NAME                                             READY   STATUS    RESTARTS   AGE
pod/spark-operator-controller-84ccd46f9b-9z4n4   1/1     Running   0          32m
pod/spark-operator-webhook-7f4dc759bd-gnfz6      1/1     Running   0          32m

NAME                                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/spark-operator-webhook-svc   ClusterIP   10.233.28.186   <none>        9443/TCP   32m

NAME                                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/spark-operator-controller   1/1     1            1           32m
deployment.apps/spark-operator-webhook      1/1     1            1           32m

NAME                                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/spark-operator-controller-84ccd46f9b   1         1         1       32m
replicaset.apps/spark-operator-webhook-7f4dc759bd      1         1         1       32m

Ví dụ

Sau khi đã thiết lập thành công Spark-Operator, chúng ta thử tạo một ứng dụng đơn giản

spark-test.yaml
apiVersion: sparkoperator.k8s.io/v1beta2
kind: SparkApplication
metadata:
  name: spark-pi
  namespace: spark
spec:
  type: Python
  pythonVersion: "3"
  mode: cluster
  image: docker.io/spark:3.5.3
  imagePullPolicy: IfNotPresent
  mainApplicationFile: local:///opt/spark/examples/src/main/python/pi.py
  sparkVersion: 3.5.3
  driver:
    cores: 1
    coreLimit: 1000m
    memory: 512m
    memoryOverhead: 1024m
    serviceAccount: spark-operator-spark
  executor:
    instances: 1
    cores: 1
    coreLimit: 1000m
    memory: 512m
    memoryOverhead: 1024m
  # sparkUIOptions:
  #   ingressTLS:
  #     - hosts:
  #       - spark-domain.example.com
  #       secretName: ssl-spark-domain

Sau đó triển khai spark application bằng cách: kubectl apply -f spark-test.yaml. Nếu kiểm tra log của pod controller, chúng ta sẽ thấy

2025-01-11T16:17:34.909Z	INFO	sparkapplication/event_handler.go:168	SparkApplication created	{"name": "spark-pi", "namespace": "spark", "state": ""}
2025-01-11T16:17:34.915Z	INFO	sparkapplication/controller.go:177	Reconciling SparkApplication	{"name": "spark-pi", "namespace": "spark", "state": ""}
2025-01-11T16:17:34.915Z	INFO	sparkapplication/controller.go:648	Submitting SparkApplication	{"name": "spark-pi", "namespace": "spark", "state": ""}
2025-01-11T16:17:34.924Z	INFO	sparkapplication/controller.go:696	Created web UI service for SparkApplication	{"name": "spark-pi", "namespace": "spark"}
2025-01-11T16:17:34.925Z	INFO	sparkapplication/driveringress.go:148	Creating networking.v1/Ingress for SparkApplication web UI	{"name": "spark-pi", "namespace": "spark", "ingressName": "spark-pi-ui-ingress"}
2025-01-11T16:17:34.964Z	INFO	sparkapplication/controller.go:719	Created web UI ingress for SparkApplication	{"name": "spark-pi", "namespace": "spark"}
2025-01-11T16:17:34.964Z	INFO	sparkapplication/controller.go:756	Running spark-submit for SparkApplication	{"name": "spark-pi", "namespace": "spark", "arguments": ["--master", "k8s://https://10.233.0.1:443", "--deploy-mode", "cluster", "--name", "spark-pi", "--conf", "spark.kubernetes.namespace=spark", "--conf", "spark.kubernetes.container.image=docker.io/spark:3.5.3", "--conf", "spark.kubernetes.container.image.pullPolicy=IfNotPresent", "--conf", "spark.kubernetes.pyspark.pythonVersion=3", "--conf", "spark.kubernetes.submission.waitAppCompletion=false", "--conf", "spark.ui.proxyRedirectUri=/", "--conf", "spark.ui.proxyBase=/spark/spark-pi", "--conf", "spark.kubernetes.driver.pod.name=spark-pi-driver", "--conf", "spark.kubernetes.driver.label.sparkoperator.k8s.io/app-name=spark-pi", "--conf", "spark.kubernetes.driver.label.sparkoperator.k8s.io/launched-by-spark-operator=true", "--conf", "spark.kubernetes.driver.label.sparkoperator.k8s.io/mutated-by-spark-operator=true", "--conf", "spark.kubernetes.driver.label.sparkoperator.k8s.io/submission-id=043027ca-253f-48a1-b71e-1f1701a839e2", "--conf", "spark.kubernetes.driver.container.image=docker.io/spark:3.5.3", "--conf", "spark.driver.cores=1", "--conf", "spark.kubernetes.driver.limit.cores=1000m", "--conf", "spark.driver.memory=512m", "--conf", "spark.driver.memoryOverhead=1024m", "--conf", "spark.kubernetes.authenticate.driver.serviceAccountName=spark-operator-spark", "--conf", "spark.kubernetes.executor.label.sparkoperator.k8s.io/app-name=spark-pi", "--conf", "spark.kubernetes.executor.label.sparkoperator.k8s.io/launched-by-spark-operator=true", "--conf", "spark.kubernetes.executor.label.sparkoperator.k8s.io/mutated-by-spark-operator=true", "--conf", "spark.kubernetes.executor.label.sparkoperator.k8s.io/submission-id=043027ca-253f-48a1-b71e-1f1701a839e2", "--conf", "spark.executor.instances=1", "--conf", "spark.kubernetes.executor.container.image=docker.io/spark:3.5.3", "--conf", "spark.executor.cores=1", "--conf", "spark.kubernetes.executor.limit.cores=1000m", "--conf", "spark.executor.memory=512m", "--conf", "spark.executor.memoryOverhead=1024m", "local:///opt/spark/examples/src/main/python/pi.py"]}
2025-01-11T16:17:52.118Z	INFO	sparkapplication/event_handler.go:60	Spark pod created	{"name": "spark-pi-driver", "namespace": "spark", "phase": "Pending"}
2025-01-11T16:17:54.118Z	INFO	sparkapplication/controller.go:186	Finished reconciling SparkApplication	{"name": "spark-pi", "namespace": "spark"}
2025-01-11T16:17:54.119Z	INFO	sparkapplication/event_handler.go:188	SparkApplication updated	{"name": "spark-pi", "namespace": "spark", "oldState": "", "newState": "SUBMITTED"}
2025-01-11T16:17:54.124Z	INFO	sparkapplication/controller.go:177	Reconciling SparkApplication	{"name": "spark-pi", "namespace": "spark", "state": "SUBMITTED"}
2025-01-11T16:17:54.133Z	INFO	sparkapplication/event_handler.go:188	SparkApplication updated	{"name": "spark-pi", "namespace": "spark", "oldState": "SUBMITTED"

Ngoài ra khi job đang chạy, chúng ta có thể truy cập vào webUI theo định dạng: <spark-domain.example.com>/{{$$appNamespace}}/{{$$appName}}. Trong trường hợp này là: http://spark-domain.example.com/spark/spark-pi

Lưu ý: Nếu job đã chạy xong thì bạn sẽ không truy cập vào được đường dẫn ở trên.

Nếu muốn xem lịch sử của các job đã chạy thì cần phải triển khai trêm Spark History Server.

Chúc thành công,

Anh Nguyễn

Hiện tại mình đang tìm kiếm công việc mới với nhiều thử thách để chinh phục.

Mọi thông tin có thể gửi về me@nvtienanh.info.