资讯动态

Spark on Kubernetes Operator 高频故障排查:调度、证书与动态资源分配

发布时间:2026/10/13 8:26:18 来源:尧图企业网站定制
用过 spark-on-kubernetes-operator 的人大多有同感它把 Spark 作业包装成一个 K8s 原生资源之后提交和重跑确实规范了很多但排查问题的路径也变了。上一轮聊了安装、RBAC 和基本提交的问题这次继续把日常环境里更高频的坑拆开。这篇文章整理的几类问题都是我在真实集群里反复遇到的集中在调度、Webhook 证书、动态资源分配、镜像依赖和 Pod 安全几个方向适合已经能跑通 Demo、却开始被各种边界问题纠缠的运维和开发同学。开始之前建议先确认你的 operator 版本和 K8s 版本都处于互相支持的范围很多问题本质上是版本组合太老续了 operator 就好了一半。1. 一直 Running 却没有 Pod 被调度先查事件再查 Operator 日志1.1 一个典型的假 Running 场景刚接触 operator 的时候很容易被它的状态字段误导。创建一个 SparkApplication 后kubectl get sparkapplication显示RUNNING但kubectl get pods里连 driver 都没有。于是很多人下意识去翻 driver 的日志结果当然是空的什么都查不到。其实在 operator 的模型里applicationState.stateRUNNING并不等同于所有 Pod 都健康它更像是 operator 已经完成提交动作之后给出的状态。Spark 应用真正能跑起来还需要 K8s 调度器、镜像拉取、serviceAccount 权限、容器启动多道工序全部通过。中间任何一环卡住CR 的状态都可能停在 RUNNING而实际 Pod 却一直在等待。这种假 Running在资源受限的环境里特别常见。比如在一台只能匀出 1CPU、2G 内存的测试节点集群中如果 SparkApplication 没有显式写 driver 和 executor 的资源 requests镜像里的默认配置可能直接把节点资源吃满后续创建的 Pod 只能排队等待。这时把注意力放在kubectl logs上就完全走错了方向。判断的起点应该是这三步看 CR 的 statuskubectl get sparkapplication my-app -o yaml里面 driverInfo、executorState 能反映 operator 视角下的执行进度看 driver pod 是否存在kubectl get pods -l spark-roledriver -n namespace看 pod 事件如果 driver 已经创建但处于 Pendingkubectl describe pod driver-pod里 Events 会直接给出被调度器拒绝的原因。三步走完问题基本就收窄到了调度链路上。再往后才是看 operator 日志。1.2 调度失败的高频原因按出现概率排序资源不足排在第一位。kubectl describe nodes里能看到每个节点的 Allocatable 和当前已分配资源Pod 的 request 不满足时事件里会出现类似0/2 nodes are available: insufficient cpu, insufficient memory的提示。Spark 这种动辄申请 1CPU、1G 内存起的作业在小集群里很容易被卡住。节点选择条件不满足排在第二位。如果在 podTemplate 里配置了 nodeSelector 或 affinity但节点上并没有对应的标签事件会显示0/2 nodes are available: 2 node(s) didnt match node selector。这类问题最隐蔽的地方在于YAML 是从文档里复制的看起来完全没问题实际上节点标签根本没有打上。第三位是 serviceAccount 权限问题。driver pod 创建时使用的 serviceAccountName 如果不存在或者缺少相应的 RBAC 权限Pod 创建阶段就会被 API Server 拒绝。事件里会出现类似pods is forbidden: User system:serviceaccount:default:spark cannot create resource pods。这一点和普通业务 Pod 有本质区别Spark 在 K8s 上跑的时候driver 需要具备创建、查看、删除 executor pod 的权限否则动态分配和足够的 executor 数都无从谈起。还有一个容易被忽略的问题operator 的监视范围。如果 SparkApplication 被创建在 operator 没有 watch 的命名空间里CR 的 YAML 会一直不出现 status 字段所有提交都像打在水里。很多共享集群是从这一步开始埋雷的排查时先看一眼 operator 的WATCH_NAMESPACE环境变量能省下大量时间。1.3 手动把资源写清楚能省掉大半调度烦恼与其让 operator 用默认值去猜不如直接在 YAML 里把资源请求写清楚spec: driver: cores: 1 memory: 1g serviceAccount: spark executor: instances: 2 cores: 1 memory: 1g这个例子不算最优配比但足够直观。driver 和 executor 的 request 一旦明确调度器就能基于节点的剩余量快速判断能不能放进去。实际规划时单个 executor 使用的资源不只是executor.memory那部分还有 memoryOverhead。比如申请 1G 堆内加 200M overhead10 个 executor 带来的总占用就是 12G 左右节点可分配量不够时创建出来的 executor pod 会一直 Pending。这也是为什么很多用户在自建小集群上反复遇到卡住现象根本不是 Spark 引擎的问题是资源模型没有对齐。2. Webhook 证书过期安装很顺利提交后却静默失败2.1 现象有的报 x509有的什么都不报创建或更新 SparkApplication 时API Server 会调用 operator 注册的 ValidatingWebhookConfiguration 对 CR 做校验。如果 webhook service 的 TLS 证书过期就会出现完整报错Internal error occurred: failed calling webhook spark-operator-validator.sparkoperator.k8s.io: Post https://spark-operator-webhook.spark-operator.svc:443/validate: x509: certificate has expired or is not yet valid这种报错基本可以直接断定是证书问题namespace 和 service 名称会随部署方式变化但 x509 这句跑不了。更隐蔽的一种情况是webhook 调用失败但部署时把 failurePolicy 配成了 Ignore。API Server 会继续放行请求SparkApplication 也能创建成功但 operator 的默认值注入逻辑没有执行任务跑起来后行为各种诡异。这种问题看起来像随机故障实际是证书已经失效了系统在静默吞错误。2.2 确认证书状态和 caBundle 的对应关系先找到 operator 的 tls secret检查证书有效期kubectl get certificate -n spark-operator kubectl get secret tls-secret-name -n spark-operator -o jsonpath{.data.tls\.crt} | base64 -d | openssl x509 -noout -dates注意把 secret 名称替换成实际部署里的名称通常是spark-operator-tls这类名字。如果 NotAfter 日期已经过去了问题定位完成。还要检查 webhookconfiguration 里的 caBundle 与当前证书的 CA 是否一致kubectl get validatingwebhookconfiguration -l appspark-operator -o yaml找到 clientConfig.caBundle解码后和 tls secret 里的 ca.crt 对比。如果两者不一致即使证书没过期API Server 同样会拒绝调用 webhook。2.3 证书修复与轮换建议如果使用 cert-manager 管理证书最简单的一步是检查 Certificate 对象是否处于 Ready 状态。cert-manager 到期会自动续期但 webhook server 进程内部加载的证书不一定能热更新所以通常需要滚动重启让进程重新加载kubectl rollout restart deployment/spark-operator-controller-manager -n spark-operator如果 cert-manager 已经重新签发了 secret重启后 webhook 就能恢复正常。有一种容易误判的情况部署完成后立刻提交 SparkApplication报错是certificate is not yet valid。这通常不是过期而是签发还没完成等几秒再提交即可。从运维角度看证书过期应该进入日常巡检。可以写一个简单的定时任务检查 tls secret 的 NotAfter 时间提前通知。对于非生产测试环境如果不想维护 cert-manager可以考虑部署时关闭 webhook减少一个依赖项。但生产环境不建议关CR 校验和默认值注入都依赖它关掉之后灵异事件会变多。3. 动态分配应扩未扩executor 数量没有按预期增长的原因链3.1 动态分配的真正执行者是 driver不是 operator不少人对 operator 有个误解觉得 SparkApplication 的 executor 数量变化是 operator 控制的。实际上在 K8s 模式下executor 的动态创建和销毁由 driver 进程内部的 ExecutorAllocationManager 完成它直接调用 K8s API 去创建和删除 executor pod。operator 在这个环节更像是配置翻译官它负责把 CRD 里的 dynamicAllocation 字段渲染成 Spark 配置真正扩缩容的动作全部发生在 driver 侧。理解了这一点很多排查思路就顺了executor 数量不涨先问 driver 是否在申请新 executor再问 K8s 为什么没有让申请成功。3.2 最容易导致不扩容的配置口径第一个是 maxExecutors 没有显式设置。在共享集群里这不仅是配置问题还是安全隐患。如果没有 maxExecutors 兜底某个任务可能无限申请 executor把整个集群的资源吃干。而且 Spark 在 K8s 模式下对动态分配的默认参数并不都适合直接生产使用显式设置 maxExecutors 是基本要求。第二个是只开 dynamicAllocation 却没解决 shuffle 数据归属问题。动态分配会在 executor 空闲时把它删除删除前必须确保它的 shuffle 数据还能被其他 executor 读取。在 K8s 上要么部署外部 shuffle service要么开启 shuffle trackingspark.dynamicAllocation.shuffleTracking.enabledtrue spark.dynamicAllocation.shuffleTracking.timeout120000这两样至少占一样。否则 executor 被回收后某个 stage 去 fetch shuffle 数据时会直接失败任务反复重试现象看起来完全像网络问题。第三个是 executor 的资源请求太大导致新创建的 executor pod 卡在调度阶段。比如单 executor 申请 4G 内存集群里每个节点的剩余量都不到 4G扩容请求永远无法被满足。这种问题的特征是driver 日志里有Requesting a new executor但实际 executor pod 不是不存在就是永远 Pending。一个完整的动态分配配置参考spec: driver: cores: 1 memory: 1g executor: cores: 1 instances: 1 memory: 1g dynamicAllocation: enabled: true initialExecutors: 1 minExecutors: 1 maxExecutors: 8注意这里spec.executor.instances在动态分配开启后更多承担的是初始值的角色真正控制上下限的是 minExecutors 和 maxExecutors。两者同时出现时先把动态分配字段的语义理清楚能少踩很多坑。3.3 扩容问题怎么快速定位判断的最直接方法是看实际存在的 executor pod 数量kubectl get pods -l spark-roleexecutor -n namespace如果数量一直不变去 driver 日志里搜ExecutorAllocationManager相关的 INFO 日志看它是否在持续申请。如果根本没有申请动作大概率是配置问题如果一直在申请但 pod 始终 Pending接下来就该kubectl describe pod看调度器的拒绝原因。还有一种情况是 executor pod 已经 Running却迟迟没有开始计算。这通常不是动态分配的问题要查 driver 与 executor 之间的网络连通比如 NetworkPolicy 是否放行了 executors 与 driver 之间的端口或者 service 的 Endpoints 是否正常。此类问题在跨节点、异网段环境下尤其明显。4. 镜像、local:// 与启动入口本地能跑集群里不该这么配4.1 local:// 在 K8s 上的真实语义很多开发第一次用 operator 时以为 local:// 指向的是提交节点的本机路径习惯性地把 jar 放在自己电脑或跳板机上然后提交任务。结果任务能提交成功但 executor 跑起来后报找不到类或者找不到文件。local:// 在 K8s 上的语义是容器内部路径所有 driver 和 executor pod 都必须在这个路径上能找到对应文件。换句话说jar 必须提前放进了镜像或者挂载进了 Pod。每次代码改动都要重新构建镜像这是 K8s 模式下最反直觉的一点。如果不想频繁构建镜像可以走远程依赖把 jar 放到对象存储或内部仓库通过 SparkApplication 的 deps 字段声明。operator 提交阶段会让 Spark 从目标地址拉取依赖。这种方式更新灵活但要求集群到对象存储的网络稳定并且依赖文件本身不能太大。4.2 自定义镜像最常踩的启动入口坑基于openjdk:11这类基础镜像做 Spark 镜像不是不行但很多人只拷贝了 Spark 目录没有保留官方镜像里的启动脚本和系统依赖结果 driver pod 一启动就 CrashLoopBackOff日志提示找不到spark-submit或者干脆是exec: /bin/bash: stat /bin/bash: no such file or directory。Spark on K8s 的 Pod 启动流程默认是由entrypoint.sh引导的它会处理环境变量、日志目录、用户切换等逻辑然后调用 spark-submit。镜像裁剪时至少要保留 bash、coreutils、procpsps 命令Spark 进程检测依赖它、Java 环境以及 Spark 安装目录的写权限。还有一个隐藏很深的问题文件属主。如果自定义镜像把 Spark 目录放进 /opt/spark但目录属主是 root而 driver 容器以非 root 用户运行启动时就会因为目录不可写而失败。日志里经常只有一个含糊的No such file or directory实际原因是权限不足。检查时先确认运行用户的 UID 与 Spark 目录属主是否匹配。4.3 三种依赖分发方式的取舍方式优点缺点适用场景打进镜像启动快、稳定、离线可用每次改 jar 都要重新构建生产发布流程成熟的团队远程 URLjar 更新灵活不用重建镜像依赖网络质量和存储稳定性开发联调、快速迭代共享 PVC不重建镜像也支持本地路径要管理 PVC 生命周期和并发访问内网离线环境、无对象存储时我的建议是生产环境优先把业务 jar 固化进镜像依赖库用spec.deps里的远程 URL 拉取。这样既能保证基础镜像版本可追溯又不用每次为了一个小改动重新做整个镜像。5. Pod 安全准入与 securityContext集群规范收紧后被拒的解决路径5.1 症状与快速确认K8s 集群升级或管理员启用 Pod Security Admission 后原来正常的 SparkApplication 可能突然全部失败。driver 或 executor pod 创建被拒绝事件里会明确给出原因pods xxx is forbidden: violates PodSecurity restricted:latest快速确认时用kubectl get events | grep forbidden能看到是哪条安全策略拦住了。常见的都是 restricted 级别要求未满足比如缺少 seccompProfile、没有显式 drop capabilities、allowPrivilegeEscalation 没有设成 false 等。5.2 两条解决路径改命名空间还是改 Pod路径一在 Spark 专用命名空间降低 Pod Security 级别。如果这个 namespace 只跑 Spark 作业可以把 enforce 从 restricted 降到 baseline甚至 privileged。这是最省事的做法但会降低该命名空间的整体安全边界需要和管理员确认合规要求。路径二通过 podTemplate 给 driver 和 executor 补 securityContext。参考配置如下spec: driver: podTemplate: spec: securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault containers: - name: spark-kubernetes-driver securityContext: allowPrivilegeEscalation: false capabilities: drop: [ALL] executor: podTemplate: spec: securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault containers: - name: spark-kubernetes-executor securityContext: allowPrivilegeEscalation: false capabilities: drop: [ALL]注意容器名称要对应 operator 生成的容器名driver 是spark-kubernetes-driverexecutor 是spark-kubernetes-executor写错的话模板合并不会生效。采用 restricted 策略后镜像必须真的能以非 root 用户运行。如果镜像里的 Spark 目录是 root 属主或者运行 UID 没有写日志目录的权限即使策略放行了容器也会在启动阶段崩溃。所以改 YAML 之前先保证镜像本身通过了非 root 运行测试。5.3 别把 RBAC 和准入控制混在一起Pod 被拒绝的报错里如果出现了forbidden不一定都是安全准入策略的问题。driver 创建 executor pod 时用 own serviceAccount 去调用 K8s API如果这个 serviceAccount 没有绑定到允许 create pods 的 Role一样会被拒绝。区分方法很简单看事件里的具体信息。提到PodSecurity的是准入层问题提到cannot create resource pods的是 RBAC 层问题。两者的处理路径完全不同前者改 securityContext 或命名空间标签后者改 RoleBinding 或 serviceAccount。6. 排查顺序与日志留存把现场证据留到你真正需要的时候6.1 我固定使用的四步排查顺序接触这个 operator 的头几个月我踩过最多的坑就是先去翻 driver 日志。后来固定下来一套顺序效率高很多kubectl get sparkapplication name -o yaml看 status 和 conditionskubectl get pods -l spark-roledriver -n namespace确认 driver 在不在并 describe 看事件kubectl logs driver-pod -n namespace这一步才开始看 Spark 引擎日志kubectl logs -n operator-ns operator-pod最后排查 operator 侧的问题。这套顺序的关键在于operator 模式下有相当一部分问题发生在 driver 真正启动之前比如调度、准入、证书、RBAC、镜像启动。如果一开始就看 driver 日志看到的往往是被牵连的症状比如 executor 连不上、task 超时、重试爆炸反而把根因藏得更深。6.2 日志留存怎么配置Spark executor 是动态资源任务结束后会被回收日志也随之消失。如果没配历史服务排查一个已经结束的任务时就像在没有监控的情况下复盘事故现场。建议开启 Spark 的 event log 并落到持久存储spec: sparkConf: spark.eventLog.enabled: true spark.eventLog.dir: s3a://my-bucket/spark-events如果集群没有对象存储也可以把 event log 写到 PVC 或分布式文件系统关键是保证 Spark driver、history server 都能访问同一个路径。配合 Spark History Server任务结束之后仍然能看到 Spark UIexecutor 列表、stage 耗时、shuffle 量这些信息都还在。6.3 快速诊断对照表症状最先执行的命令常见根因CR 没有 status 字段kubectl get sparkapplication -o yamloperator 未 watch 该命名空间、CRD 未正确注册driver pod Pendingkubectl describe pod driver-pod节点资源不足、nodeSelector 不匹配、toleration 缺失提交时 x509 报错检查 tls secret 的 NotAfterwebhook 证书过期或未签发完成executor 数量不涨kubectl get pods -l spark-roleexecutor动态分配参数错误、单 executor 资源超节点上限pod 创建被 forbiddenkubectl get eventsgrep forbiddendriver 启动即 CrashLoop查看 driver pod 的 Previous 日志镜像入口脚本缺失、目录权限、非 root 兼容问题6.4 最后一点个人体会在共享集群里维护这套东西久了最大的感受是SparkApplication YAML 只是入口真正决定任务稳定性的往往是一堆看起来不起眼的 K8s 基础配置。ResourceQuota、LimitRange、Pod Security、RBAC、证书这些词在 Spark 环境里听起来像是平台团队的事可一旦出了问题第一负责人就是提交任务的你。我现在每接到一个 Spark on K8S 问题都会先按上面那张表把现场数据存成时间线记录等任务恢复之后再回头对比正常任务和异常任务的差异。很多时候所谓偶发问题其实都是配置边界没对齐只是之前没有把现场留全罢了。希望这篇能帮你少熬几个通宵。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑