Spark on Kubernetes 部署:spark-submit 与 Operator 取舍
对比原生 spark-submit 与 Spark Operator 两条路线,给出镜像化、podTemplate 与动态分配的落地要点
Spark on Kubernetes 的落地首先面临路线选择:原生 spark-submit 与 Spark Operator 各有明确的适用边界。简单说:spark-submit 适合少量、偏交互式、由脚本驱动的作业;Spark Operator 适合大规模、需要生命周期管理(自动重启、版本升级、与 Kubernetes 原生工具集成)的生产作业。两条路线不是二选一,可以共存。
两条路线的对比
spark-submit(原生提交):
- 命令式:一条命令拉起 driver 与 executor,退出即结束;
- 适合:临时分析、CI 跑批、少量定时任务;
- 劣势:无状态管理(失败不会自动重启)、日志与监控需自行接入、升级依赖改脚本。
Spark Operator(声明式):
- 以 SparkApplication 自定义资源描述作业,控制器负责创建/删除 Pod、失败重试、保留作业历史;
- 适合:生产调度、有 SLA 要求的作业、多租户场景;
- 劣势:多一层控制面组件、调试链路更长、版本兼容性需要关注。
选型的关键问题不是"哪个更高级",而是"你的作业需要谁来看管":如果作业失败后必须有人重跑,且重跑逻辑散落在脚本和定时器里,Operator 的自动重试与状态保留会显著降低运维成本;如果只是临时验证或低频跑批,spark-submit 更轻量。多数团队最终是两条路线并存:临时作业用 submit,核心生产作业迁到 Operator。
先想清楚要解决什么问题
对多数迁移场景,建议先回答三个问题再选路线:作业是常驻调度还是临时触发?失败后由谁负责重跑?团队更熟悉脚本还是声明式资源?如果作业数量少、触发方式简单,spark-submit 加外部调度器(如 cron 或工作流引擎)已经够用;如果作业数量上百、需要统一的失败重试、版本回滚与多环境管理,Operator 的声明式模型会显著降低维护成本。两条路线切换并不困难——spark-submit 的配置项在 Operator 的 SparkApplication 里大多有对应字段,先跑通原生模式再迁移到 Operator,是风险最低的落地路径。
镜像化要点
- 基础镜像:官方 Spark 镜像加自定义依赖(JDBC 驱动、Python 包、配置文件),driver 与 executor 使用同一镜像,避免行为不一致;
- 非 root 运行:显式指定 UID,避免 Pod 安全策略拦截;
- 入口脚本:镜像内包含 spark-submit 脚本入口(Operator 模式依赖它),原生模式则用
k8s://master 直接提交; - 版本对齐:Spark 二进制版本与镜像内版本一致,避免序列化与 RPC 协议不兼容导致的启动失败。
镜像构建时要注意依赖的"收敛":把常用 JDBC 驱动、Python 依赖打进基础镜像,作业运行时不再动态下载,既加快启动速度,也避免运行时网络问题导致 executor 启动失败。镜像要打版本标签,与 Spark 版本、作业代码版本一一对应,出问题时才能快速回滚到旧镜像。
大数据基础平台 EasyMR
统一存储、计算与运维,支撑批、流、OLAP 与湖仓一体的国产化大数据底座平台
podTemplate 要点
podTemplate 用于精细控制 Pod 调度:
- 资源:driver 与 executor 的 requests 与 limits 分开设置,executor 预留 limits 余量,避免因资源超限被驱逐;
- 亲和性/反亲和性:executor 通过 Pod 反亲和尽量分散到不同节点,降低单节点故障的影响面;
- 容忍与污点:配合专用节点池使用 toleration,保证资源隔离;
- 卷挂载:配置、密钥、shuffle 数据盘通过 PVC/emptyDir 挂载,注意非 root 用户的读写权限。
另一个经常被忽略的点是 driver Pod 的调度:driver 承担任务调度与 UI 服务,建议与 executor 反亲和,避免 driver 与大量 executor 挤在同一节点;如果使用 headless service 访问 driver UI 或进行 driver-executor 通信,还要确认网络策略放行相关端口。
动态分配(无外部 shuffle 服务)
在 Kubernetes 上不部署外部 shuffle 服务时,动态分配依赖 shuffle 文件跟踪:
spark.dynamicAllocation.enabled=true;spark.shuffle.service.enabled=false——Kubernetes 上没有外部 shuffle 服务,此时必须开启 shuffle 跟踪(spark.dynamicAllocation.shuffleTracking.enabled=true),否则 executor 缩减时会丢失 shuffle 文件导致任务失败;- 设置最小/最大 executor 数与闲置超时(
spark.dynamicAllocation.shuffleTracking.timeout); - executor 以 Pod 粒度伸缩,注意 Kubernetes 的 ResourceQuota 要覆盖峰值并发。
理解 shuffle 跟踪的原理有助于排错:动态分配允许闲置 executor 被回收,但如果某个 executor 的 shuffle 数据还被其他任务依赖,回收会导致 ShuffleFetchFailedException。开启 shuffle 跟踪后,Spark 会为"仍被引用 shuffle 数据"的 executor 保留足够长的超时,超时后才回收。因此 shuffleTracking.timeout 要根据最长 shuffle 依赖链估算,太短会误杀,太长会浪费资源。
关键配置示例(spark-defaults 或 spark-submit 参数):
spark.master=k8s://https://<api-server>
spark.kubernetes.container.image=<registry>/spark:3.5
spark.kubernetes.authenticate.driver.serviceAccountName=spark
spark.kubernetes.driver.podTemplateFile=/path/pod-template.yaml
spark.kubernetes.executor.podTemplateFile=/path/pod-template.yaml
spark.dynamicAllocation.enabled=true
spark.dynamicAllocation.shuffleTracking.enabled=true
spark.dynamicAllocation.initialExecutors=2
spark.dynamicAllocation.minExecutors=1
spark.dynamicAllocation.maxExecutors=50
spark.shuffle.service.enabled=false
spark.kubernetes.executor.request.cores=1
spark.kubernetes.executor.limit.cores=2Operator 路线的最小 SparkApplication 示例:
apiVersion: sparkoperator.k8s.io/v1beta2
kind: SparkApplication
metadata:
name: etl-hourly
spec:
type: Scala
mode: cluster
image: <registry>/spark:3.5
mainClass: com.example.EtlJob
sparkVersion: "3.5.1"
restartPolicy:
type: OnFailure
onFailureRetries: 3
driver:
cores: 1
coreLimit: "1200m"
memory: "2g"
serviceAccount: spark
executor:
instances: 4
cores: 1
memory: "2g"
dynamicAllocation:
enabled: true
shuffleTracking: true
initialExecutors: 2
minExecutors: 1
maxExecutors: 20网络、安全与多租户
Spark 作业在 Kubernetes 上运行还涉及三类基础配置。网络:driver 与 executor 通过 driver 的 headless service 通信,如果集群启用了网络策略,要放行 Spark 使用的端口段;跨命名空间访问(如读取其他命名空间的存储)还要处理服务账号权限。安全:作业访问对象存储或数据库的凭据通过 Kubernetes Secret 挂载,而不是写死在镜像或代码里;镜像仓库使用私有仓库时需要 imagePullSecret。多租户:不同团队用不同命名空间隔离,各自配置 ResourceQuota 与 LimitRange,避免一个团队的作业占满集群;服务账号按团队拆分,配合 RBAC 控制谁能提交作业、谁能查看日志。
国产信创
面向国产化替代与信创改造场景,支撑数据平台平滑迁移、统一纳管、性能保障与安全可控。
监控、日志与成本
Kubernetes 模式比传统 YARN 模式更容易丢失现场:Pod 被回收后日志随之消失,因此日志采集要在部署时一起做,把 driver 与 executor 的标准输出接入集中存储,并保留作业粒度的日志索引。指标方面,除了 Spark UI 的 executor 指标,还要关注 Pod 维度的资源使用(CPU 限流、内存 OOM 被杀、重启次数)——很多 Spark 作业"莫名其妙失败"其实是 executor Pod 被驱逐或 OOM。成本上,动态分配加合理的最小/最大 executor 是控制资源账单的主要手段,配合空闲超时避免作业结束后 executor 长时间空转;镜像体积也要控制,过大镜像会拖慢每个 Pod 的拉取时间,间接拉长作业启动延迟。
常见失败模式
- 镜像与集群版本不一致:executor 启动即失败。对策:版本对齐并做最小 smoke test。
- 配额/限额不足:executor 一直 Pending,作业无进展。对策:检查 ResourceQuota、LimitRange 与节点资源水位。
- 动态分配关闭 shuffle 跟踪:executor 缩减后任务报 shuffle 文件丢失。对策:明确
spark.shuffle.service.enabled=false且开启shuffleTracking。 - 日志随 Pod 回收丢失:作业失败后无日志可查。对策:配置日志采集到集中存储,driver 与 executor 日志都接。
- 非 root 挂载卷不可写:任务在写临时目录时报权限错误。对策:securityContext 与卷权限对齐。
- driver 单点:driver Pod 所在节点故障,整个作业失败。对策:配合 Operator 的重试策略,并监控 driver 节点的健康状态。
下一步
先在测试集群用 spark-submit 跑通一个最小作业,验证镜像、网络与配额三项基础条件;随后引入 Operator 接管 1 到 2 个核心作业,把"脚本驱动的提交"迁移为"声明式作业",并同步接上日志与指标采集,再逐步扩大迁移范围。迁移过程中把每次故障(Pod 驱逐、shuffle 丢失、日志缺失)记录进问题清单,作为后续完善镜像、模板与监控基线的依据。


