文档中心>容器服务>Data 应用实践>TKE 集群混部 Spark 任务实践

TKE 集群混部 Spark 任务实践

最近更新时间:2025-11-19 16:50:02

我的收藏

1. 背景

Spark on Kubernetes 结合了 Spark 强大的数据处理能力与 Kubernetes 卓越的容器编排能力,实现了资源的统一调度和弹性扩展。这不仅显著提升了集群的利用率,还简化了运维管理,为企业提供高效、低成本的大数据云原生解决方案。
针对许多在线业务集群 CPU 利用率偏低或存在显著的潮汐特性,可以利用 TKE 集群混部技术,实现 Spark 离线任务与在线任务的混合部署。在不影响在线业务 QoS 的前提下,进一步提升节点资源的利用率,从而有效降低整体成本。
具体来说,Spark 首先通过客户端构建 driver pod 对象,并向 Kubernetes 的 apiserver 发送请求以创建 driver pod。Spark 的 driver 进程运行在 driver pod 中。driver 启动后,会在内部构建并创建这些 executor pod,同时通过 watch 和 list 等机制持续监听各 executor pod 的状态。任务执行结束后,executor pod 会被自动清理,而 driver pod 则会以 completed 状态继续保留。


2. 环境准备

参考文档 TKE 集群混部原理与部署介绍,准备 TKE 集群,确保集群包含可用的原生节点,并正确安装 craned 和 QoSAgent 组件。请确保 QoSAgent 组件已勾选 “CPU 使用优先级”、 “CPU 超线程隔离”、“网络 QoS 增强”三项能力。

3. 使用原生 spark-submit

3.1 安装 Spark 环境

在准备好的 TKE 集群的一台节点上,或任一可访问集群 ApiServer 的 CVM 上部署 Spark 环境:
mkdir /usr/local/service/
cd /usr/local/service/
wget https://archive.apache.org/dist/spark/spark-3.3.2/spark-3.3.2-bin-hadoop3.tgz
tar -xzvf spark-3.3.2-bin-hadoop3.tgz
mv spark-3.3.2-bin-hadoop3 spark
在集群上部署 Spark 任务使用的 ServiceAccount:
首先创建名为 “workspark” 的 namespace。这里我们假设 Spark 相关的 pod 运行在该 namespace 中。
apiVersion: v1
kind: ServiceAccount
metadata:
name: spark
namespace: workspark
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: spark-cluster-admin-binding-workspark
subjects:
- kind: ServiceAccount
name: spark
namespace: workspark
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io

3.2 准备 podTemplate 文件

假设我们需要为 driver pod 申请2核2G的资源,创建 driver-pod-template.yaml:
apiVersion: v1
kind: Pod
spec:
containers:
- name: spark-kubernetes-driver
resources:
limits:
gocrane.io/cpu: "2"
gocrane.io/memory: 2Gi
requests:
gocrane.io/cpu: "2"
gocrane.io/memory: 2Gi
注意:
这里需要使用 gocrane.io/cpugocrane.io/memory 扩展资源,K8s 才会把相关资源只调度到有离线资源可用的节点上。
假设我们需要为 executor pod 申请2核2G的资源,创建 executor-pod-template.yaml:
apiVersion: v1
kind: Pod
spec:
containers:
- name: spark-kubernetes-executor
resources:
limits:
gocrane.io/cpu: "2"
gocrane.io/memory: 2Gi
requests:
gocrane.io/cpu: "2"
gocrane.io/memory: 2Gi
注意:
这里需要使用 gocrane.io/cpugocrane.io/memory 扩展资源,K8s 才会把相关资源只调度到有离线资源可用的节点上。

3.3 准备 PodQOS 规则

apiVersion: ensurance.crane.io/v1alpha1
kind: PodQOS
metadata:
name: podqos-spark
spec:
labelSelector:
matchExpressions:
- key: spark-role
operator: In
values:
- driver
- executor
resourceQOS:
cpuQOS:
cpuPriority: 7
说明:
这里通过匹配 driver pod 和 executor pod 的 label 来区分 spark-submit 提交后创建的 spark pod。

3.4 准备 spark-submit 命令

可以通过以下方式查看 Apiserver 的 URL:
$ kubectl cluster-info
Kubernetes control plane is running at https://10.2.16.107
......
在上述情况下,我们可以通过将 --master k8s://http://10.2.16.107 作为参数传递给 spark-submit 来将 spark 任务提交至该 Kubernetes 集群。
通过 spark-submit 提交以下任务:
/usr/local/service/spark/bin/spark-submit \\
--master k8s://https://10.2.16.107 \\
--deploy-mode cluster \\
--name spark-pi \\
--class org.apache.spark.examples.SparkPi \\
--conf spark.executor.instances=2 \\
--conf spark.kubernetes.namespace=workspark \\
--conf spark.kubernetes.driver.container.image=ccr.ccs.tencentyun.com/use-test/spark:v3.3.2 \\
--conf spark.kubernetes.executor.container.image=ccr.ccs.tencentyun.com/use-test/spark:v3.3.2 \\
--conf spark.kubernetes.authenticate.driver.serviceAccountName=spark \\
--conf spark.kubernetes.driver.podTemplateFile=./driver-pod-template.yaml \\
--conf spark.kubernetes.executor.podTemplateFile=./executor-pod-template.yaml \\
--conf spark.kubernetes.driver.limit.cores=2 \\
--conf spark.kubernetes.driver.request.cores=0 \\
--conf spark.kubernetes.executor.limit.cores=2 \\
--conf spark.kubernetes.executor.request.cores=0 \\
--conf spark.driver.memory=2g \\
--conf spark.executor.memory=2g \\
local:///opt/spark/examples/jars/spark-examples_2.12-3.3.2.jar 20000
命令参数说明如下:

3.4.1 基础部分

1. 通过 --conf spark.executor.instances 指定 executor pod 的数量。
2. 通过 --conf spark.kubernetes.namespace 指定的 namespace 应与创建 ServiceAccount 时一致,此处使用 “workspark”。

3.4.2 镜像部分

通过 --conf spark.kubernetes.driver.container.image--conf spark.kubernetes.executor.container.image 指定自己编译的要运行的镜像。

3.4.3 Pod 模板部分

我们需要让 spark pod 使用集群的离线资源。因为 gocrane.io/cpugocrane.io/memory 属于扩展资源,spark-submit 要求这类扩展资源放在 pod 模板文件中。请根据实际资源需求,变更相关 pod 模板中资源的值。

3.4.4 resources 要求说明

1. CPU 资源部分:
(1)通过 --conf spark.kubernetes.{driver,executor}.request.cores 指定为0。
(2)通过 --conf spark.kubernetes.{driver,executor}.limit.cores 指定为规划的 limit 的值。
2. 内存资源部分:
需要主动通过 --conf spark.driver.memory 指定内存 limit 要求的大小。

3.5 提交命令并查看 pod 状态

1. 查看运行的 spark pod 的 yaml,可以确认 driver pod 和 executor pod 是否按预期使用了 “gocrane.io/cpu” 和 “gocrane.io/memory” 的资源。
2. 查看 spark pod 是否包含 key 为 “gocrane.io/cpu-qos” 和 “gocrane.io/qos” 的 Annotation。当 pod 有了这些特定的 Annotation,代表已经正确使用了对应的离线资源。

3.6 Spark-submit 配合 Volcano 提交任务

3.6.1 部署 Volcano

参考 Volcano 官网,可以通过如下方式对 Volcano 进行安装。
helm repo add volcano-sh https://volcano-sh.github.io/helm-charts
helm repo update
helm install volcano volcano-sh/volcano -n volcano-system --create-namespace
说明:
安装完成后通过 kubectl get all -n volcano-system 确认 volcano-controllers、volcano-scheduler 等 pod 已经处于 Running 状态。

3.6.2 准备支持 Volcano 的 Spark 版本

因为需要通过 spark-submit 使用 Volcano 相关高级特性,需要参阅 Apache Spark 文档中 https://dlcdn.apache.org/spark/docs/3.3.2/running-on-kubernetes.html#build 小节,重新编译部署的 spark 版本:

说明:
为通过实验验证功能,我们也可以通过将 spark-kubernetes_2.12-3.3.2.jar、volcano-client-5.12.2.jar、volcano-model-v1beta1-5.12.2.jar 三个 jar 包拷贝进我们对应 jars 目录(如 /usr/local/service/spark/jars)的方法,来满足这里 Spark 版本需求。

3.6.3 准备支持 Volcano 的 Spark 镜像

因为需要使用 Volcano 高级特性,Spark 镜像需包含相关 jar 包。
这里作为实验,我们可以通过以下 Dockerfile.patch 来构建我们测试使用的支持 Volcano 的镜像:
FROM ccr.ccs.tencentyun.com/use-test/spark:v3.3.2
COPY spark-kubernetes_2.12-3.3.2.jar /opt/spark/jars/
COPY volcano-client-5.12.2.jar /opt/spark/jars/
COPY volcano-model-v1beta1-5.12.2.jar /opt/spark/jars/

3.6.4 准备 Volcano 队列

按照实际情况,创建我们要使用的 Volcano 队列。这里假设我们的 Volcano 队列名字为 “sparkqueue”。
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
name: sparkqueue
spec:
weight: 1
reclaimable: false
capability:
gocrane.io/cpu: 20
gocrane.io/memory: 100Gi

3.6.5 准备 podGroup 模板文件

apiVersion: scheduling.volcano.sh/v1beta1
kind: PodGroup
spec:
# Specify minMember to 1 to make a driver pod
minMember: 1
# Specify minResources to support resource reservation (the driver pod resource and executors pod resource should be considered)
# It is useful for ensource the available resources meet the minimum requirements of the Spark job and avoiding the
# situation where drivers are scheduled, and then they are unable to schedule sufficient executors to progress.
minResources:
gocrane.io/cpu: "6"
gocrane.io/memory: "6Gi"
# Specify the priority, help users to specify job priority in the queue during scheduling.
priorityClassName: system-node-critical
# Specify the queue, indicates the resource queue which the job should be submitted to
queue: sparkqueue
说明:
1. 如官方注释所要求:minMember 这里需要设置为 1,请勿修改。
2. minResources 中 gocrane.io/cpugocrane.io/memory 可以按实际情况进行配置。这里指定为 driver pod 和 executor pod 对资源需求的和。
3. 通过 queue 可以指定使用的 Volcano 队列。这里我们指定使用我们创建的名字为 “sparkqueue” 的队列。

3.6.6 准备 spark-submit 命令

/usr/local/service/spark/bin/spark-submit \\
--master k8s://https://10.2.16.107 \\
--deploy-mode cluster \\
--name spark-pi \\
--class org.apache.spark.examples.SparkPi \\
--conf spark.executor.instances=2 \\
--conf spark.kubernetes.namespace=workspark \\
--conf spark.kubernetes.driver.container.image=ccr.ccs.tencentyun.com/use-test/spark:v3.3.2-volcano-patch3 \\
--conf spark.kubernetes.executor.container.image=ccr.ccs.tencentyun.com/use-test/spark:v3.3.2-volcano-patch3 \\
--conf spark.kubernetes.authenticate.driver.serviceAccountName=spark \\
--conf spark.kubernetes.driver.podTemplateFile=./driver-pod-template.yaml \\
--conf spark.kubernetes.executor.podTemplateFile=./executor-pod-template.yaml \\
--conf spark.kubernetes.driver.limit.cores=2 \\
--conf spark.kubernetes.driver.request.cores=0 \\
--conf spark.kubernetes.executor.limit.cores=2 \\
--conf spark.kubernetes.executor.request.cores=0 \\
--conf spark.driver.memory=2g \\
--conf spark.executor.memory=2g \\
--conf spark.kubernetes.scheduler.name=volcano \\
--conf spark.kubernetes.driver.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep \\
--conf spark.kubernetes.executor.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep \\
--conf spark.kubernetes.scheduler.volcano.podGroupTemplateFile=./podgroup-template-gocrane.yaml \\
local:///opt/spark/examples/jars/spark-examples_2.12-3.3.2.jar 20000
说明:
这里增加了如下几行配置:
--conf spark.kubernetes.scheduler.name=volcano \\
--conf spark.kubernetes.driver.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep \\
--conf spark.kubernetes.executor.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep \\
--conf spark.kubernetes.scheduler.volcano.podGroupTemplateFile=./podgroup-template-gocrane.yaml
其中这里最后一行配置需要指定上文中准备好的 podGroup 模板文件的路径。
另外,需要注意的是,这里我们需要使用新构建的支持 Volcano 的镜像。

3.6.7 提交命令并查看 pod 状态

提交命令,可以看到首先会创建一个 driver pod,然后会创建出来两个 executor pod。我们依次验证相关状态,可以看到:
1. 在driver pod 和 executor pod 的 yaml 中会指定 schedulerName: volcano。表明 spark pod 已经在使用 Volcano 调度器了。
2. 通过 kubectl describe 命令查看 driver pod 和 executor pod,可以看到 “Normal Scheduled 29m volcano Successfully assigned workspark/xxxx-xxxxx to xxxxNode” 类似的 Event(事件)。
3. 在 driver pod 和 executor pod 的 Annotation中,都可以看到 key 为 “scheduling.k8s.io/group-name” 的行,类似如下:
scheduling.k8s.io/group-name: spark-d41b280c285741b9b6d3a2d3f3cf6eec-podgroup
这表明 driver pod 和 executor pod 共同使用的是同一个 podGroup。查看该 podGroup,类似如下,相关参数正是我们 podGroup 模板指定的:
apiVersion: scheduling.volcano.sh/v1beta1
kind: PodGroup
metadata:
name: spark-0ed23d5712a04c76ab15935d7fd6c49b-podgroup
namespace: workspark
spec:
minMember: 1
minResources:
gocrane.io/cpu: "6"
gocrane.io/memory: 6Gi
priorityClassName: system-node-critical
queue: sparkqueue
status:
conditions:
- lastTransitionTime: "2025-11-04T08:53:06Z"
reason: tasks in gang are ready to be scheduled
status: "True"
transitionID: f8e70311-3b30-4a01-8b00-4c0de38fced4
type: Scheduled
phase: Completed
succeeded: 1
在 driver pod 和 executor pod 运行期间,查看 “sparkqueue” 这个Volcano 队列的状态,表明我们的 spark pod 已经占用了本队列的部分资源:
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
annotations:
name: sparkqueue
resourceVersion: "1305744284"
uid: b88f3034-0e5f-487d-9ad1-a3e9b9781f66
spec:
capability:
gocrane.io/cpu: 10
gocrane.io/memory: 20Gi
parent: root
reclaimable: false
weight: 4
status:
allocated:
gocrane.io/cpu: "6"
gocrane.io/memory: "6442450944"
memory: 7296Mi
pods: "3"
tke.cloud.tencent.com/eni-ip: "3"
reservation: {}
state: Open

4. 使用 Spark on K8s Operator

Spark Operator 是专为在 Kubernetes 集群中运行 Spark 工作负载而设计,旨在自动化管理 Spark 作业的生命周期。通过 SparkApplication 等 CRD 资源,您可以灵活提交和管理 Spark 作业。利用 Kubernetes 的自动扩展、健康检查和资源管理等特性,Spark Operator 可以更有效地监控和优化 Spark 作业的运行。
相比 spark-submit 的使用优势:
简化管理:通过 Kubernetes 的声明式作业配置,自动化部署 Spark 作业并管理作业的生命周期。
弹性资源供给:利用原生节点池等弹性资源,可在业务高峰期快速获得大量弹性资源,平衡性能和成本。
适用场景:
数据分析:数据科学家可以利用 Spark 进行交互式数据分析和数据清洗等。
批量数据计算:运行定时批处理作业,处理大规模数据集。
实时数据处理:Spark Streaming 库提供了对实时数据进行流式处理的能力。

4.1 部署 Spark Operator 环境

依次执行如下命令,部署 Spark Operator 运行相关资源。
helm repo add spark-operator https://kubeflow.github.io/spark-operator
helm repo update
helm install spark-operator spark-operator/spark-operator --version 2.3.0 --namespace spark-operator --create-namespace --set controller.batchScheduler.enable=true
部署后,检查确认 spark-operator-controller、spark-operator-webhook pod 已经处于 Running 状态。
注意:
这里有可能遇到镜像拉取的问题,原始镜像地址为 ghcr.io/kubeflow/spark-operator/controller:2.3.0,如果该地址不能访问,可以变更为 ccr.ccs.tencentyun.com/use-test/spark-operator:controller-2.3.0 用于测试。

4.2 准备 SparkApplication 任务

apiVersion: "sparkoperator.k8s.io/v1beta2"
kind: SparkApplication
metadata:
name: spark-pi
namespace: default
spec:
arguments:
- "10000"
type: Scala
mode: cluster
image: "apache/spark:v3.3.2"
imagePullPolicy: Always
mainClass: org.apache.spark.examples.SparkPi
mainApplicationFile: "local:///opt/spark/examples/jars/spark-examples_2.12-3.3.2.jar"
sparkVersion: "3.3.2"
restartPolicy:
type: Never
volumes:
- name: "test-volume"
hostPath:
path: "/tmp"
type: Directory
driver:
memory: "2g"
template:
spec:
containers:
- name: spark-kubernetes-driver
resources:
limits:
gocrane.io/cpu: "2"
gocrane.io/memory: "2Gi"
requests:
gocrane.io/cpu: "2"
gocrane.io/memory: "2Gi"
labels:
version: 3.3.2
serviceAccount: spark
volumeMounts:
- name: "test-volume"
mountPath: "/tmp"
executor:
instances: 2
memory: "2g"
template:
spec:
containers:
- name: spark-kubernetes-executor
resources:
limits:
gocrane.io/cpu: "2"
gocrane.io/memory: "2Gi"
requests:
gocrane.io/cpu: "2"
gocrane.io/memory: "2Gi"
labels:
version: 3.3.2
volumeMounts:
- name: "test-volume"
mountPath: "/tmp"
部署该应用后:
1. 查看 spark pod 的资源已经按预期使用了 “gocrane.io/cpu” 和 “gocrane.io/memory” 相关扩展资源。
2. 查看 driver pod 和 executor pod 是否有 key 为 “gocrane.io/cpu-qos” 和 “gocrane.io/qos” 的Annotation,如果有这些 Annotation,代表已经正确使用了相关的离线资源。

4.3 Spark Operator 配合 Volcano 运行 Spark 任务

4.3.1 部署 Volcano

参考 章节 3.6.1 的方式部署 Volcano。
helm repo add spark-operator https://kubeflow.github.io/spark-operator
helm install my-release spark-operator/spark-operator --version 2.3.0 \\
--namespace spark-operator \\
--create-namespace \\
--set webhook.enable=true \\
--set controller.batchScheduler.enable=true
注意:
这里需要依赖 Volcano 打开 batchScheduler 特性:controller.batchScheduler.enable=true

4.3.2 准备 Volcano 队列

参考 章节 3.6.4,创建名为“sparkqueue”的 Volcano 队列。

4.3.3 准备使用 Volcano 的 SparkApplication 任务

apiVersion: "sparkoperator.k8s.io/v1beta2"
kind: SparkApplication
metadata:
name: spark-pi
namespace: default
spec:
arguments:
- "10000"
type: Scala
mode: cluster
image: "apache/spark:v3.3.2"
imagePullPolicy: Always
mainClass: org.apache.spark.examples.SparkPi
mainApplicationFile: "local:///opt/spark/examples/jars/spark-examples_2.12-3.3.2.jar"
sparkVersion: "3.3.2"
batchScheduler: volcano # 新增
batchSchedulerOptions: # 新增
queue: "sparkqueue"
restartPolicy:
type: Never
volumes:
- name: "test-volume"
hostPath:
path: "/tmp"
type: Directory
driver:
memory: "2g"
template:
spec:
containers:
- name: spark-kubernetes-driver
resources:
limits:
gocrane.io/cpu: "2"
gocrane.io/memory: "2Gi"
requests:
gocrane.io/cpu: "2"
gocrane.io/memory: "2Gi"
labels:
version: 3.3.2
serviceAccount: spark
volumeMounts:
- name: "test-volume"
mountPath: "/tmp"
executor:
instances: 2
memory: "2g"
template:
spec:
containers:
- name: spark-kubernetes-executor
resources:
limits:
gocrane.io/cpu: "2"
gocrane.io/memory: "2Gi"
requests:
gocrane.io/cpu: "2"
gocrane.io/memory: "2Gi"
labels:
version: 3.3.2
volumeMounts:
- name: "test-volume"
mountPath: "/tmp"
说明:
其中以下部分为新增:
1. batchScheduler: Volcano 指定了 driver pod 和 executor pod 使用 Volcano 调度器。
2. batchSchedulerOptions: queue: "sparkqueue" 部分,指定了使用名字为 “sparkqueue” 的 Volcano queue。

4.3.4 部署 SparkApplication 任务并查看 pod 状态

1. 在 driver pod 和 executor pod 的 yaml 可发现 schedulerName 已经设置为 Volcano,表明已经在使用 Volcano 调度器了。
2. 通过 kubectl describe 命令查看 driver pod 和 executor pod,可以看到 “Normal Scheduled 10m Volcano Successfully assigned workspark/xxxx-xxxxx to xxxxNode” 类似的 Event(事件)。
3. 在 driver pod 和 executor pod 的 Annotation中,都可以看到 key 为 “scheduling.k8s.io/group-name” 的行:
scheduling.k8s.io/group-name: spark-spark-pi-pg
这表明 driver pod 和 executor pod 已经共同使用了同一个 podGroup。
查看这个 podGroup,类似如下,相关参数正是我们 podGroup 模板指定的:
apiVersion: scheduling.volcano.sh/v1beta1
kind: PodGroup
metadata:
name: spark-spark-pi-pg
namespace: default
spec:
minMember: 1
minResources: {}
queue: sparkqueue
status:
conditions:
- lastTransitionTime: "2025-11-04T10:07:20Z"
reason: tasks in gang are ready to be scheduled
status: "True"
transitionID: 28ba9ce7-c84c-438e-a71d-a02ef3459011
type: Scheduled
phase: Running
running: 3
4. 在 driver pod 和 executor pod 运行期间,查看 “sparkqueue” 这个 Volcano 队列的状态,表明我们的 spark pod 已经占用了本队列的部分资源:
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
name: sparkqueue
spec:
capability:
gocrane.io/cpu: 10
gocrane.io/memory: 20Gi
parent: root
reclaimable: false
weight: 4
status:
allocated:
gocrane.io/cpu: "6"
gocrane.io/memory: "6442450944"
memory: 7296Mi
pods: "3"
tke.cloud.tencent.com/eni-ip: "3"
reservation: {}
state: Open