1. 这个方案到底解决什么问题
先聊点实在的。过去几年我经手过不少大数据平台的搭建和改造,从最早的纯物理机部署 Hadoop,到后来用云主机搭 CDH 集群,再到现在完全容器化的 Spark、Flink 跑在 Kubernetes 上。每一次架构变迁,本质上都是被同一个问题逼的——资源利用率。
传统大数据集群有个很尴尬的处境:为了应对业务高峰,你得按峰值申请资源。但业务不是每天都高峰,比如数据部门白天跑批处理,晚上集群基本闲着,可这时候 ClickHouse 又需要资源做预聚合。你拆两个集群?机器翻倍,成本翻倍。你共用一个集群?YARN 和 K8s 怎么共处,调度策略根本是两套逻辑,运维复杂度直接起飞。
那能不能让一套基础设施同时扛住离线批处理、实时流计算、即席查询?这就是 K8s 上跑大数据这个方向的价值所在。Kubernetes 本身就是做资源编排和弹性伸缩的,它能把计算资源池化,让不同计算框架共享同一批机器,再配合节点自动扩缩容,真正做到“按需分配,按量计费”。
我这次在项目中实践的方案,核心思路是:把所有数据计算组件全部容器化,由 Kubernetes 统一调度,前端业务流量低峰期,节点池缩容,把机器让给在线业务;凌晨离线任务高峰期,节点池扩容,几百个 Spark Executor 同时跑起来也不卡。从效果看,最直观的变化是:这个集群整体计算资源成本降了大概三成,而这套经验的通用性很高,只要你手头有 K8s 环境,就能直接参考。
这套设计适合谁来参考?一类是已经在用 K8s、想把 Spark/Flink 迁进来的团队;另一类是还在用传统 YARN 集群、被资源利用率问题折腾得头疼的运维或架构同学,你想知道 K8s 到底能不能替代大数据底座,这篇文章会给你一个比较完整的答案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 整体设计思路:为什么是 Kubernetes 而不是 YARN
聊架构方案必须先说清楚选型逻辑,不然很容易变成“拿着锤子看什么都是钉子”。Kubernetes 替代 YARN 做大数据底座,到底动了谁的蛋糕,又解决什么真问题,这里头有不少门道。
2.1 传统 YARN 架构的三个痛点
先说 YARN 的优势,它毕竟是专门为大数据设计的资源调度系统,对 MapReduce、Spark 的调度做了很多深度优化,比如延迟调度、容量抢占。这些机制相当成熟,这也是为什么很多生产集群仍然跑在 YARN 上。
但数据这行变化太快。YARN 的瓶颈在三个层面:
- 资源隔离粒度粗糙。YARN 的容器本质上还是进程级隔离,对 CPU、内存的约束主要靠 CGroup 实现,磁盘 IO、网络带宽的隔离基本处于缺失状态。某些时候一个跑得很野的任务就把 NameNode 所在的物理机 IO 打满了,排障很难受。
- 组件间资源无法共享。YARN 管不了 HBase、Kafka、Flink 这些组件,因为它们不在同一套调度体系内。实际上,一个大数据平台往往有十几套计算引擎并存,每套都各自管理资源,最终必然造成严重的资源碎片化。
- 弹性能力先天不足。YARN 的设计理念是管理“长期存活”的集群节点,虽然它也支持节点上线、下线,但节点粒度太粗,无法实现秒级伸缩。你要让一个 Hadoop 集群根据实时负载自动伸缩节点,配置和维护成本非常大。
2.2 Kubernetes 的资源池化优势
Kubernetes 的资源管理思路就不一样了。它追求的是把整个数据中心的计算资源变成一个统一的“资源池”,所有应用(不管你是大数据任务还是在线微服务)都从同一个池子里申请资源。
这里面最核心的机制是 Namespace + ResourceQuota + LimitRange 的组合。不同业务团队划分到不同 Namespace,通过 ResourceQuota 限制每个团队能使用的总资源上限,避免某个团队把集群资源吃干抹净。配合 LimitRange,可以限制单个 Pod 的资源规格范围,防止有人申请一个 128G 内存的“庞然大物”,导致集群资源碎片化。
这种模式下,你的 Hadoop 集群、Spark 集群、Flink 集群之间不再有物理边界。所有计算任务都以 Pod 的形式运行,共享同一个资源池。
我实际测过一个场景:早上 8 点到 10 点,在线业务流量上来,大数据批处理任务还没跑完,两类负载同时存在,Kubernetes 靠优先级(PriorityClass)保证在线业务的 Pod 优先调度,大数据任务使用低优先级,一旦资源紧张,调度器会优先驱逐低优先级的 Pod,给在线业务让路。这一套在 YARN 里面没法实现,因为 YARN 根本管不了在线业务容器,你只能靠人工拆集群。
2.3 选型 K8s 之后的架构分层
具体到这套方案,我把整个大数据平台分成了四层:
第一层是基础设施层。底层节点采用混合部署,一部分是 CPU 密集型节点,跑 Spark、MapReduce 等计算任务;一部分是内存优化型节点,跑 Flink、Presto 这些吃内存的引擎。底层存储用的是云盘加对象存储的组合,热数据放云盘,冷数据、备份数据放对象存储,成本能低一大截。
第二层是资源调度层,这里就是 Kubernetes 本身。所有组件都以 Deployment、StatefulSet 或 Operator 管理的自定义资源方式运行。调度策略上,我用 nodeSelector 和 nodeAffinity 把不同计算引擎固定到对应规格的节点上,再用 taint 和 toleration 保证大数据任务不会占用到在线业务的专属节点。
第三层是数据计算层。跑批处理用 Spark,流计算用 Flink,即席查询用 Presto(Trino),消息队列用 Kafka。每个引擎都有对应的 Kubernetes Operator 管理,比如 Spark 用 Spark Operator,Flink 用 Flink Kubernetes Operator。
第四层是服务与治理层,包括数据开发平台、调度系统(Apache Airflow)、元数据管理(DataHub)、权限控制(Ranger)等,统一以服务方式接入。
这个分层的好处是:每一层都可以独立扩展,比如计算引擎要加新版本,直接更新对应的 Operator 和镜像即可,底层的节点和调度策略不用变。数据平台的可维护性就是这样一点点撑起来的。
3. 核心组件选型与配置要点
架构设计的下一步就是落实到具体选型。这套方案里有几块核心组件,选型和配置直接决定了后续能不能落地。我把我配置过的参数和经验逐一展开。
3.1 Spark Operator:在 K8s 上跑 Spark 的关键插件
Spark 从 2.3 开始就支持 Kubernetes 作为调度器,但原生支持比较“裸”:你得自己写 Pod 模板,处理 Driver 和 Executor 的各类细节。Spark Operator 的价值在于,它把 SparkApplication 定义成了一个 CRD(自定义资源),你只需要写一个 YAML 描述应用,剩下的创建、监控、删除全部由 Operator 自动完成。
配置上,我建议重点关注这几个参数:
yaml复制apiVersion: sparkoperator.k8s.io/v1beta2
kind: SparkApplication
metadata:
name: spark-etl-job
namespace: data-platform
spec:
type: Scala
mode: cluster
image: registry.internal/spark/spark:v3.5.1
imagePullPolicy: Always
mainClass: com.example.etl.DailyJob
mainApplicationFile: local:///opt/spark/examples/jars/etl-job.jar
sparkVersion: 3.5.1
restartPolicy:
type: OnFailure
onFailureRetries: 3
driver:
cores: 1
coreLimit: "1200m"
memory: "2g"
serviceAccount: spark
executor:
instances: 20
cores: 2
coreLimit: "2400m"
memory: "4g"
memoryOverhead: "1g"
dynamicAllocation:
enabled: true
initialExecutors: 5
minExecutors: 1
maxExecutors: 50
一些细节集中说明一下:
coreLimit这个参数很关键。Spark 在 Kubernetes 模式里默认只设置 CPU requests,如果没有显式设置 limits,容器可以使用节点上的全部 CPU,非常容易造成噪音干扰。coreLimit一般设置为 requests 的 1.2~1.5 倍,比如上面配置了 2 核 requests,limit 就给 2400m。memoryOverhead务必手动指定。默认值是 executor memory 的 10%,对有些内存密集型任务来说不够。经验值建议:如果 executor 是 4g,overhead 给 1g;如果 executor 是 8g,overhead 给 1.5g 起。这个空间用于存放 JVM 之外的内存消耗,比如 PySpark 的 Python 进程或者直接内存。没有给够的话,任务跑一段时间会报 Container Memory Limit Exceeded,极其折磨人。dynamicAllocation一定要开。开了之后,Spark 会根据 shuffle 文件量和任务积压情况动态调整 executor 数量,高峰期自动扩,空闲期自动缩。这是我们整套弹性方案里最核心的一环。注意,动态分配依赖 shuffle service,需要在集群里额外部署一个spark-shuffle-service的 DaemonSet。这个是集群层面的组件,在新版的 Spark Operator 中,可以通过配置spark.shuffle.service.enabled=true配合预装的 shuffle service 使用。
3.2 Flink Kubernetes Operator:流任务的“自动驾驶”
Flink 的流任务有一个特性:它是长时运行的服务,不像 Spark 那样跑完就走。所以 Flink on K8s 的难点在于,JobManager 和 TaskManager 都需要持久化保证,出问题了需要自动恢复。Flink Kubernetes Operator 就是用来管这摊子事的,它支持 Job 的自动升级、Savepoint 管理、失败恢复等能力。
关键配置参考:
yaml复制apiVersion: flink.apache.org/v1beta1
kind: FlinkDeployment
metadata:
name: rta-flink-job
namespace: data-platform
spec:
image: registry.internal/flink/flink:1.18.1
flinkVersion: v1_18
serviceAccount: flink
jobManager:
resource:
memory: "2048m"
cpu: 1
taskManager:
resource:
memory: "4096m"
cpu: 2
replicas: 4
podTemplate:
spec:
containers:
- name: flink-main-container
env:
- name: HADOOP_USER_NAME
value: hdfs
flinkConfiguration:
state.checkpoints.dir: s3://bucket/flink-rta/checkpoints
state.savepoints.dir: s3://bucket/flink-rta/savepoints
execution.checkpointing.interval: 60s
execution.checkpointing.min-pause: 30s
high-availability: kubernetes
high-availability.storageDir: s3://bucket/flink-rta/ha
taskmanager.numberOfTaskSlots: "4"
job:
jarURI: local:///opt/flink/usrlib/rta-job.jar
entryClass: com.example.realtime.RealtimeJob
parallelism: 8
upgradeMode: savepoint
Flink 这块有几点比较容易踩坑:
- Checkpoint 目录一定要跟 Savepoint 目录分开,不要混用。否则 Flink 在做 Savepoint 时会读到 Checkpoint 的数据,状态恢复会大概率出问题。
- HA 存储推荐用 S3 或者其他兼容对象存储,高可用本质上是把 JobManager 的当前状态存到外部,一旦 JobManager 挂了,新的 JobManager 能恢复状态继续跑。Kubernetes 模式下,Flink 会自动帮你创建新的 JobManager Pod,这个 HA 模式必须配好。
- 如果要做 Job 升级,建议用 savepoint 模式。任务代码变了,要从状态恢复继续跑,它默认会先做一次 Savepoint,然后用新镜像基于 Savepoint 恢复,以实现精确一次的语义。
这套配置跑下来,我用 Flink Operator 替换掉原来的 Flink Standalone 集群之后,最明显的感受就是升级省事了。以前升级 Flink 任务,流程是“停作业 -> 手动做 Savepoint -> 换 jar -> 起新作业 -> 指定 Savepoint 恢复”,现在直接在 YAML 里改 jar 版本或者镜像标签,Operator 自动完成整个过程,而且失败可以回滚。
3.3 K8s 集群层面的大小资源调优
组件选好了,K8s 集群自身的配置也马虎不得。这里列几个我实测调优过的参数,值得抄作业:
节点池设计:核心是两层——用 nodeSelector 把大数据任务调度到计算节点池,计算节点池的实例类型通常选择 CPU 主频高、内网带宽大的机型;使用 taint 给节点打上污点,让大数据 Pod 不会“流出”到在线业务节点上。注意,这个污点不是为了隔离而隔离,它的真实目的是让弹性伸缩互相不受干扰,你的节点池才能在业务低峰时安全释放。
举一个实际配置:
bash复制# 给大数据专用节点打污点
kubectl taint nodes node-pool-bigdata bigdata=true:NoSchedule
# 给 Spark 和 Flink 的 namespace 打上对应的 toleration(写在各自 Operator 配置中)
集群自动伸缩:使用 Kubernetes 的 cluster-autoscaler。我给大数据节点池设置的扩容条件是“有 pending Pod 超过 2 分钟”,缩容条件是“节点利用率连续 20 分钟低于 50%”。
设置成 2 分钟是一个经验值。设置太短,集群会频繁扩缩容,容易形成一个 Pod 刚起来又要调度走的抖动问题;设置太长,任务高峰期的调度延迟会变得很尴尬。
Pod 驱逐策略:大数据任务被驱逐是很致命的,所以千万别用默认驱逐。我建议给 Spark、Flink 这类有状态任务的 Pod 设置 topologySpreadConstraints,让 Pod 尽量均匀分布在多个节点上。这样即便某个节点出现故障,也不会同时把大规模的计算任务全部拖垮。
yaml复制topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: spark-executor
另外一个特别容易忽略的地方是 PDB(PodDisruptionBudget)。如果你们用了节点自动回收或者定期替换,那 PDB 是保命用的,建议给 Flink 这类长时服务设置 minAvailable: 1,节点维护时,K8s 会等 Pod 在其他节点恢复后才继续驱逐,以免把整个 Flink 集群搞到“全军覆没”。
3.4 分层存储:热数据与冷数据的成本平衡
大数据场景下数据量很大,全放云盘注定扛不住成本,所以我是把存储拆分成三层的:
- HDFS 或 JuiceFS 作为数据湖的底层存储,存放核心业务数据。我这里选择了 JuiceFS,因为它的元数据存在 Redis 或 MySQL,数据本身在对象存储上,天然适合跑在 K8s 上。
- 高并发读取的热数据放 SSD 云盘。比如 ClickHouse 的本地表、Spark Shuffle 的临时盘。
- 不常访问的大文件直接走对象存储(S3/OSS),通过 Presto/Trino 的
s3://协议直接查询。
给一个比较实惠的组合建议:如果你的数据量没有大到非要 HDFS 不可,可以直接使用对象存储作为数据湖底座,配合 JuiceFS 做缓存加速。这样做的好处是底层根本不需要维护 NameNode 和 DataNode 这两类长期运行的组件,运维负担瞬间减半。我们实际测试下来,800 并发、平均文件大小 128MB 的场景下,JuiceFS 的读性能跟 HDFS 基本持平,而存储成本大约是原来的 40%。
4. 实操过程:从零搭建一套弹性数据处理环境
理论说了不少,下面给一套可以直接照做的实操过程。这里以一套最小可用的完整环境为例,包括 K8s 集群、Spark、Flink 和简单的数据入湖入仓场景。
4.1 环境准备与工具链安装
假设你的环境里已经有一套 Kubernetes 集群(v1.28+),下面这些工具的安装步骤比较关键:
bash复制# 安装 spark-operator
helm repo add spark-operator https://kubeflow.github.io/spark-operator
helm install spark-operator spark-operator/spark-operator \
--namespace spark-operator \
--create-namespace \
--set webhook.enable=true \
--set serviceAccounts.spark.name=spark
# 创建 Spark 运行所需的 ServiceAccount
kubectl create serviceaccount spark -n data-platform
kubectl create clusterrolebinding spark-role \
--clusterrole=edit \
--serviceaccount=data-platform:spark
# 安装 flink-kubernetes-operator
helm repo add flink-operator https://apache.github.io/flink-kubernetes-operator
helm install flink-kubernetes-operator flink-operator/flink-kubernetes-operator \
--namespace flink-operator \
--create-namespace
装完之后验证一下:
bash复制kubectl get crd | grep -E "sparkapplication|flinkdeployment"
看到 sparkapplications.sparkoperator.k8s.io 和 flinkdeployments.flink.apache.org 两个 CRD 都存在,说明插件装好了。
4.2 核心配置链路完整走查
Spark Operator 和 Flink Operator 装好,接下来最重要的就是验证整个提交链路的连通性。我的经验是先从一个简单的 Spark Pi 任务开始,跑通之后再做复杂的 ETL。
第一个要验证的是一个空跑任务能否正常提交,以此判断 Operator 到 API Server 的网络链路是否畅通:
bash复制kubectl apply -f - <<EOF
apiVersion: sparkoperator.k8s.io/v1beta2
kind: SparkApplication
metadata:
name: spark-pi
namespace: data-platform
spec:
type: Scala
mode: cluster
image: registry.internal/spark/spark:v3.5.1
mainClass: org.apache.spark.examples.SparkPi
sparkVersion: 3.5.1
driver:
cores: 1
memory: "1g"
serviceAccount: spark
executor:
instances: 1
cores: 1
memory: "1g"
EOF
提交一个 Flink 示例任务验证流处理链路:
bash复制kubectl apply -f - <<EOF
apiVersion: flink.apache.org/v1beta1
kind: FlinkDeployment
metadata:
name: basic-example
namespace: data-platform
spec:
image: flink:1.18.1
flinkVersion: v1_18
serviceAccount: flink
jobManager:
resource:
memory: "1024m"
cpu: 0.5
taskManager:
resource:
memory: "2048m"
cpu: 1
flinkConfiguration:
taskmanager.numberOfTaskSlots: "2"
job:
jarURI: local:///opt/flink/examples/streaming/TopSpeedWindowing.jar
parallelism: 1
upgradeMode: stateless
EOF
跑完之后检查 Pod 状态,看到 driver、executor、jobmanager、taskmanager 全部进入 Running 状态,整条链路就通了。
接着验证弹性。用一个会产生大量 Task 的 Spark 任务,把 dynamicAllocation 开启,观察 executor 数量能否自动扩展。用 kubectl get pods -n data-platform -l spark-role=executor 可以看到 Pod 数量变化。
这里要特别提醒一个我踩过的坑:如果任务一直默认不扩容,去看 Spark Driver 的日志,大概率是 spark.dynamicAllocation.enabled 没有真正生效,或者 shuffle service 没有安装。这个前提条件需要保证。
4.3 监控与告警配置:Prometheus + Grafana
大数据任务上了 K8s,监控比之前复杂得多。以前 YARN 的 ResourceManager 就是天然的监控中心,现在你用 K8s,就得自己搭一套。我用的组合是 Prometheus + Grafana + 自定义告警规则。
核心指标包括采集三类:
- K8s 资源层:节点 CPU、内存、磁盘 IO、网络流量。直接用 node-exporter 采集。
- Pod 资源层:每个 executor/taskmanager 的 CPU、内存使用率。这些数据来自 K8s Metrics API,Prometheus 配好 kubelet 的 cAdvisor 端口即可采集。
- 应用层:Spark 的
spark.metrics.prometheus.enabled=true配置可以把 Driver/Executor 的指标暴露出来;Flink 则通过metrics.reporter.prom.factory.class把作业延迟、Checkpoint 时长等推到 Prometheus。
自己写了一段比较实用的告警规则,发生问题能及时感知:
yaml复制groups:
- name: bigdata-alerts
rules:
- alert: ExecutorOOMKilled
expr: kube_pod_container_status_terminated_reason{reason="OOMKilled", namespace="data-platform"} > 0
for: 1m
labels:
severity: warning
annotations:
summary: "{{ $labels.pod }} 被 OOM Kill"
- alert: FlinkCheckpointFailed
expr: flink_jobmanager_job_numberOfFailedCheckpoints > 0
for: 2m
labels:
severity: critical
annotations:
summary: "{{ $labels.job_name }} Checkpoint 连续失败"
- alert: SparkAppFailed
expr: spark_application_spark_app_status{state="failed"} > 0
for: 1m
labels:
severity: critical
annotations:
summary: "Spark 应用 {{ $labels.app_name }} 运行失败"
我实际用下来,这套告警规则里最给力的是 FlinkCheckpointFailed。以前流任务出问题,经常是业务方反馈数据延迟了才发现,现在 Checkpoint 连续失败 5 分钟就会收到报警,能在事态扩大前介入。
5. 数据接入与处理:贴源、入湖、实时计算一体化
平台搭建完成之后,真正决定成败的是数据能不能顺畅的接入和处理。这个章节我用四个最常见的场景来拆解。
5.1 批式 ETL 入湖
5.2 实时流计算链路
实时链路的核心是 Kafka + Flink。Flink 消费 Kafka 的数据,做实时 ETL,结果落到 Kafka、对象存储或者 OLAP 引擎。
这里给出 Flink 连接 Kafka 时常用的几个核心配置思路:
- 提交 offset 的策略用
initial模式,也就是从上次记录的位置恢复,服务重启不会丢数据。 - 设置 checkpoint 间隔 60 秒,checkpoint 的存储放到对象存储或者远程卷,不要存放在本地磁盘。假如 JobManager 或 TaskManager Pod 被驱逐,数据状态还能照常恢复。
- 每个 TaskManager 的 slot 数量尽量不要设太高。slot 数量越高,单个 TaskManager 挂掉后的影响面越大。一般建议 2~4 个。
我之前遇到过最典型的一个问题:Flink 任务在高峰期因为 backpressure,消费 lag 逐渐变大。检查发现是 Sink 下游的 ClickHouse 写入性能跟不上。后来把 Flink 的 Sink 改成批量写入模式,配合 ClickHouse 的批量 Buffer 表,整个链路瞬间顺滑了很多。
5.3 离线数仓分层加工
离线数仓的加工任务主要有三类:一是每天的定时批处理 (T+1 报表);二是按小时或者 15 分钟级别的小批量任务;三是临时跑的数据分析或模型训练样本。
这三类任务,用一套 Airflow 做编排就可以统一调度。Airflow 在 K8s 上最常见的部署方式是用官方的 Helm Chart,需要给它的 worker 设置一个 helm 特性——worker.spec.dynamicPodsReplicas。这样做的好处是 Airflow 调度到的每个任务都以单独的 Pod 运行,跑完即焚。这样依赖关系、失败重试、日志查看全部由 Airflow 管理,弹性也好,忙的时候多并行跑几个 Pod,不忙就不用常驻那么多资源。
KubernetesExecutor 的执行模式,配合上面说到的 Spark Operator,有一个常见打法:Airflow 的任务里写一个 PythonOperator 或者 KubernetesPodOperator,调 spark-submit 到集群上执行 SparkApplication,本质上是从 Airflow 的调度向 Spark Operator 提交任务,让不同的调度系统发挥各自的优势。
这里有个亲测有效的经验:如果 Airflow worker 本身也是跑在 K8s 上,需要给它的 RBAC 权限加上创建 SparkApplication 的能力,否则 Airflow 调 K8s API 的 Spark Operator 提交时会 API 权限不够。
5.4 多引擎资源冲突排查案例
最后分享一个我们在灰度阶段遇到的冲突排查过程,这个问题比较有代表性。
状态:K8s 集群里同时跑着一个跑批的 Spark 任务和若干个 Flink 实时任务,突然收到告警,Flink 的 checkpoint 开始超时,Spark 任务的执行效率也明显下降。
排查过程:
第一反应看节点监控。发现 CPU 使用率并没有满,但是磁盘 IO 到了一个非常高的水位,说明问题可能出在存储竞争上。
后来把目标定到 shuffle 文件路径,发现 Spark 的 shuffle 临时目录写在默认的本地磁盘上,而 Flink 的 RocksDB 状态后端也写在本地磁盘上。两个引擎的临时盘是同一块数据盘,IO 冲突自然不可避免。
处理办法是:给 Spark 的 shuffle 和 Flink RocksDB 各自挂载独立的卷,并分别设置 spark.local.dir 和 state.backend.rocksdb.localdir 两个参数;同时调整了节点的 IO 调度策略。此后 IO 冲突就再也没有出现过。
这个问题在物理机时代非常常见,反而到了云原生时代容易被忽略,因为大家默认 Pod 是隔离的,但实际上底层存储是共享的,IO 竞争依然存在。所以如果你在 K8s 上跑混合负载,尤其是 Flink 做有状态计算时,这个存储隔离策略一定要提前设计好。
6. 常见问题与排查技巧实录
架构真正上线后,问题一定会以另一种方式出现。我把实际项目里踩过的一些坑和排查方法集中在这里,给有需要的人参考。
6.1 Spark on K8s 的 Pod 一直处于 Pending
现象:SparkApplication 提交后,Driver Pod 或 Executor Pod 一直停留在 Pending 状态,超过十几分钟没有任何变化。
排查思路:
bash复制# 查看 Pod 当前状态和最近事件
kubectl describe pod <executor-pod-name> -n data-platform
常见原因和对应方案:
- 集群资源不够。看 Events 里面是不是有
Insufficient cpu、Insufficient memory。如果是,说明集群总资源不足,cluster-autoscaler 没有及时扩容。这里要确认下节点池的 autoscaler 是否绑定了正确的扩展策略。 - 调度约束太苛刻。比如 nodeSelector 匹配不到任何节点,或者污点容忍没配对。检查 Spark 配置里有没有设置
spark.kubernetes.node.selector。 - 镜像拉取失败。如果用的私有镜像仓库,Pod 一直在拉镜像,请检查 imagePullSecrets 是否配置正确。
6.2 Executor 经常被 OOMKilled
现象:Executor Pod 以状态 Error 退出,kubectl describe 看到容器最后状态是 OOMKilled。
这个是最常见也最让人头疼的。大多数情况下不是说 JVM 堆不够大,而是 JVM 之外的 native memory(直接内存、线程栈、Metaspace)超了。
常规处理方法:
- 检查 Spark UI 里的 Executor 内存曲线,看堆内内存峰值是多少。如果堆内峰值低于你给的内存值,说明是堆外内存不够,要加
spark.executor.memoryOverhead。 - 如果你是跑 PySpark,Python 进程的内存消耗相对大头,
memoryOverhead相对要多留一些,Spark 3.x 里推荐配置spark.executor.pyspark.memory。 - 另外,如果你的任务是访问 S3/OSS 并使用较大的 Parquet 文件,JNI 层分配的内存偶尔也会显得偏高,如果反复出现,在 Spark 配置里适当把
spark.sql.adaptive.coalescePartitions.enabled等分区合并开关调大,减少一次读入的数据量也能缓解。
6.3 Flink Checkpoint 一直超时或失败
Checkpoint 超时了,最直接的思路是查看 Flink UI 中 Checkpoint 的详细指标,特别是 Alignment Duration 和 Start Delay。下面几种情况基本覆盖了我遇到过的绝大多数:
- 网络抖动造成的反压。 检查下游 Sink 的写入速率,如果数据源和 Sink 不在同可用区,需要看下跨机房带宽是否被打满。
- RocksDB 的写放大。 Flink 的增量 Checkpoint 机制默认是异步的,如果状态量特别大,单次 Checkpoint 可能耗时就会超过默认的超时时间。建议把 checkpoint 超时时间调大(默认 10 分钟,可以改成 20~30 分钟)。
- TaskManager 老是频繁重启。 如果 TM Pod 总是在重启,Checkpoint 很难成功。这时候要往上排查,看是不是 TaskManager 的内存配置不够导致被 OOM。
- K8s 滚动更新问题。 如果你对 FlinkDeployment 做了镜像版本更新,并且同时还在跑着大量实时任务,可能出现新 TM 启动后大量旧 TM 同时被下线的情况,这个瞬间很容易把 Checkpoint 干失败。所以建议配置
spec.job.upgradeMode: savepoint,并确保每次滚动只替换总 TM 数的 25% 左右。
6.4 写任务常见问题速查表
| 问题表现 | 可能原因 | 建议解法 |
|---|---|---|
| Spark 任务执行速度逐步变慢 | Shuffle 文件太多产生小文件 | 开启动态资源分配或调整 shuffle partition 数 |
| Executor 启动失败 NoSuchMethodError | Spark 版本与依赖库冲突 | 统一镜像中的 Spark 版本,用 shade 解决依赖冲突 |
| Flink 作业 Kafka lag 持续增加 | Sink 写入性能不足或者并行度不够 | 检查是否反压,提高 Sink 并行度或改为批量写入 |
| Pod 频繁被驱逐 | 节点内存碎片化或资源超分 | 检查节点内存水位,调整 request 与 limit 比例 |
| K8s 节点扩缩容节奏跟不上业务 | cluster-autoscaler 的扩容策略阈值设置不合理 | 收紧扩容统计时间,建议 1~2 分钟触发扩容 |
| 访问 S3/OSS 的读取速度太低 | 存在大量小文件请求,或者没有使用数据本地化 | 开启 S3A 的快速列表模式,或者做文件合并 |
6.5 独家避坑:K8s 任务冷启动时间优化
大数据任务容器化之后的冷启动时间一般比 YARN 环境长。YARN 里 NodeManager 已经预先把需要用到的 jar 包放在本地,而 K8s 里每次从镜像拉取,加上 JVM 启动,整个 Spark Driver 从提交到编译完成,往往要多个几秒到几十秒。
这类冷启动问题可以通过三个手段优化:
- 优化节点池的镜像预热能力。如果你们用的是自建 K8s,用 DaemonSet 做镜像预热,或者开启 Kubelet 的
serializeImagePulls=false配合按需拉取,能让并发创建 Executor 的等待时间大幅缩短。 - 开启 Spark 的
spark.kubernetes.allocation.driver.readinessTimeout,并适当调高 Executor 的抢占超时时间。初次启动的时候 Timeout 太小,Executor 创建速度跟不上时会误报超时。 - 如果任务对延迟要求很高,直接用 K8s 的原地升级机制,尽量不要每次部署都换新的节点,要让 Pod 优先调度到已缓存镜像的节点上。
按我实测的经验,经过这几项优化后,一个包含 100 个 Executor 的 Spark 批任务,从提交到全部 Pod 启动,时间从原来的约 3 分钟压缩到了 1 分钟多一点。对用 K8s 跑短任务的场景来说,影响很大。
7. 扩展与微调经验
到了这一步,整套平台基本能稳定运作了。但还有几个值得往外延展的方向,结合我自己的体会简单说一下。
7.1 引入 Service Mesh 优化流量管控?
有不少人问过我,大数据组件上 K8s 是不是需要把 Flink、Kafka 全部纳入 Service Mesh 的治理。从我的观点看,这倒未必。大数据组件内部节点间通信大多走的是高吞吐、低延迟的内网通道,如果让 Envoy sidecar 介入,会额外引入几毫秒的延迟,对高吞吐量的 Flink TaskManager 来说并不划算。所以我们在 Flink 的 podTemplate 中显式排除了对 sidecar 的注入需求——在大数据内部链路上没必要做那么重的微服务治理。
7.2 Serverless 化:ECI / Virtual Kubelet
如果你们公司的业务量本身就带有比较强烈的潮汐特征,可以引入 Virtual Kubelet,把超出物理节点承载范围的 Pod 调度到 Serverless 容器实例上。比如大促当天或者月底结算系统跑批量数据的时候,核心 K8s 集群内节点固定,流量暴增后再调度一批 Pod 直接跑在 Serverless 容器里,实现真正意义上的“无限弹性”。
逻辑很简单,配置起来主要是在 K8s 的调度器上增加一个 provider。亲测如果跑数据量很大的任务,Serverless 容器间的网络带宽没有物理机那么好,如果不特别在意延迟,这确实是一种性价比很高的大数据处理方案。
7.3 真正的“云原生”不只是容器化
最后说一个容易产生的认知误区:上了 K8s 不代表就是云原生。真正的云原生大数据,核心应该是你的架构可以跟基础设施完全解耦:你的计算引擎能不能用一种声明式的方式定义,能不能由数据平台自动伸缩、调度、自愈。
我把这套方案落地的整个过程走下来之后,最大的体会就是:云原生大数据不是某一种软件或某一个组件的功能,而是一种把这些组件像拼积木一样组合管理的能力。理解到这一层,后续做资源优化、架构演进,甚至跟团队讨论选型决策,都能站在更高的维度去看问题,而不是陷在某个具体组件的排障中出不来。
希望这篇文章对有相同需求的团队有一点参考价值。如果你们在迁移过程中遇到有意思的问题,欢迎交流。
