把 Spark 从传统的 YARN 集群迁移到 Kubernetes,听起来只是换了个运行平台,但实际操作中会碰到不少细节问题:镜像怎么构建才够精简,Executor 的内存配置如何映射到容器的资源限制,Pod 被驱逐了怎么办。本文从架构原理讲起,一步步覆盖部署流程和资源调优的核心要点。

Spark on Kubernetes 的运行架构
Spark 从 2.3 版本开始原生支持 Kubernetes 作为资源管理器,到了 3.x 版本已经相当成熟。与 YARN 模式不同,Spark on Kubernetes 中没有常驻的资源守护进程,每次提交作业时,spark-submit 会先在集群中创建一个 Driver Pod,Driver 启动后再通过 Kubernetes API 动态创建 Executor Pod,作业结束后这些 Pod 全部销毁。
这种设计的好处是资源隔离更彻底,每个作业独享自己的 Pod,通过 Namespace 可以实现多租户隔离。同时 Executor Pod 可以直接使用 Kubernetes 的 Service、ConfigMap、Secret 等能力,访问集群内服务变得非常方便。缺点是冷启动比 YARN 略慢,因为每次都要拉取镜像并调度 Pod。
在角色划分上,Driver Pod 对应 Spark 的 driver 角色,Executor Pod 对应 executor 角色。Kubernetes 本身并不理解 Spark 的概念,它只负责按照 Pod 模板调度容器,真正的资源协商由 Spark 内部完成,这也是理解后续资源配置的关键。
镜像构建与作业提交
容器化部署的第一步是准备镜像。官方提供了 spark 基础镜像,也可以自己构建。一个实用的做法是基于官方发行版打镜像,把依赖的 jar 包统一放进 jars 目录,这样每次作业运行时不需要额外分发依赖,启动速度明显提升。
# 进入 Spark 发行包目录后执行打包 ./bin/docker-image-tool.sh -r ipipp.com/spark -t v3.5.0 build # 推送镜像到私有仓库 ./bin/docker-image-tool.sh -r ipipp.com/spark -t v3.5.0 push
提交作业时使用 --master k8s://https://<apiserver地址> 指定 Kubernetes 集群,并通过 --conf 传递一系列容器相关参数。下面是一个典型示例:
spark-submit \ --master k8s://https://192.168.0.1:6443 \ --deploy-mode cluster \ --name etl-job \ --class com.ipipp.etl.Main \ --conf spark.kubernetes.container.image=ipipp.com/spark:v3.5.0 \ --conf spark.kubernetes.namespace=spark-jobs \ --conf spark.executor.instances=5 \ --conf spark.executor.cores=2 \ --conf spark.executor.memory=4g \ --conf spark.executor.memoryOverhead=1g \ --conf spark.kubernetes.authenticate.driver.serviceAccountName=spark \ local:///opt/spark/workdir/etl-job.jar
注意 local:// 前缀表示 jar 包已经在镜像内部,避免每次从远端拉取。如果是 Python 作业,还需要设置 spark.kubernetes.container.image 为包含 Python 环境的镜像,并用 spark.kubernetes.pyspark.pythonVersion 指定版本。
资源配置的映射关系
这是容器化部署中最容易出错的部分。Spark 的资源配置会被转换成 Pod 的 request 和 limit:spark.executor.cores 加上 memoryOverhead 对应容器的 CPU 和内存请求。具体来说,Executor Pod 的内存 request 等于 spark.executor.memory + spark.executor.memoryOverhead,CPU request 等于 spark.executor.cores。
如果 Pod 的实际内存使用超过 limit,Kubernetes 会直接杀掉容器(OOMKilled),因此在估算 memoryOverhead 时要格外注意堆外内存的占用,包括 Netty 缓冲区、Python 进程内存(PySpark 场景)以及本地_shuffle_文件的开销。经验值是堆内存的 10% 到 20%,PySpark 重计算场景可以适当再调高。
另一个建议是使用 Pod Template 自定义更多细节,比如挂载主机路径、设置亲和性、配置安全上下文等。通过 spark.kubernetes.executor.podTemplateFile 指定一个 YAML 文件即可:
apiVersion: v1
kind: Pod
metadata:
labels:
app: spark-executor
spec:
containers:
- name: executor
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
hostPath:
path: /mnt/nas/data动态资源分配与常见问题排查
静态指定 Executor 数量往往浪费资源,开启动态资源分配(Dynamic Allocation)可以让 Spark 根据负载伸缩。在 Kubernetes 上需要配合 Shuffle Tracking 使用,不再依赖外部 shuffle 服务:
--conf spark.dynamicAllocation.enabled=true \ --conf spark.shuffle.service.enabled=false \ --conf spark.dynamicAllocation.shuffleTracking.enabled=true \ --conf spark.dynamicAllocation.minExecutors=2 \ --conf spark.dynamicAllocation.maxExecutors=20
生产环境中常见的问题有几类。一是 Executor Pod 一直 Pending,通常是集群资源不足或者 Namespace 配额用完,可以通过 kubectl describe pod 查看事件定位。二是 Pod 被驱逐,多发生在内存 request 设置过低、实际使用超标的情况下,Node 压力驱逐会直接终止容器,需要调整 request 与实际负载匹配。三是镜像拉取失败,建议在集群节点上预拉取镜像或配置镜像预热。
监控方面,建议通过 spark.kubernetes.executor.annotation 打上标签,结合 Prometheus 抓取 Spark 指标,重点观察 Executor 的内存使用峰值和 GC 时间。如果发现 Full GC 频繁,优先排查数据倾斜;如果内存峰值贴近 limit,就应该上调 memoryOverhead 而不是盲目加堆内存,因为容器场景下堆外内存超限同样会导致 OOMKilled。
总体来看,Spark 容器化部署的核心在于理解 Spark 配置与 Kubernetes 资源模型的映射关系,把镜像、资源配置、动态伸缩这三块基础打牢,大部分运行不稳定的问题都能提前规避。搭配 Kubernetes 的弹性能力,Spark 作业可以更灵活地共享集群资源,这也是越来越多团队选择它替代 YARN 的原因。
Spark容器化部署KubernetesSpark资源管理修改时间:2026-09-10 01:06:38