我把 Jenkins 夜间批处理迁到 Argo Workflows 后,K8s 资源利用率从 23% 提到 71%
我把 Jenkins 夜间批处理迁到 Argo Workflows 后,K8s 资源利用率从 23% 提到 71%
说实话,我之前一直觉得 Jenkins 夜间跑批处理这事挺稳的。凌晨 0 点 30 分自动触发,三台固定 slave 节点吭哧吭哧跑 5 个多小时,第二天早上我上班看报告就行。
直到有天早上 7 点 15 分,我打开 Grafana 看到三条曲线:
- 三台 slave 的 CPU 平均 11%;
- 队列长度 47;
- 最长一个 job 在队列里躺了 4 小时 12 分钟。
那一刻我就意识到:我们不是缺资源,而是在把资源当摆设。白天三台机器空转,晚上任务又互相排队等锁。这种架构不改,成本只会越来越高。
01 问题:Jenkins 批处理就像租了三间常年空着的仓库
先交代一下我们的 nightly 流程,大概是给第二天 BI 报表准备数据:
- 00:30 从 6 个业务库抽取增量数据;
- 01:00 做清洗、脱敏、打标签,生成中间表;
- 02:00 跑三个模型训练任务,输出特征权重和预测结果;
- 03:30 汇总报告,生成 CSV 和 PDF;
- 04:30 推送到数据仓库和对象存储;
- 05:00 触发下游 BI 刷新。
全部写在一个 Jenkins pipeline 里,串在 3 台固定节点nightly-slave-01/02/03上。每台 8C16G,全年 24 小时开机,只为凌晨 6 小时服务。
更难受的是一次故障。1 号节点凌晨 2 点因为磁盘满了 crash,整个 pipeline 从头再来。早上 8 点业务上班,报表还没出来,老板直接在群里 @ 我。
我列了 5 个核心痛点:
- 资源分配和任务调度强耦合。slave 一旦绑定任务,其他 job 只能排队,即使旁边节点空闲。
- 失败重试粒度是整个 pipeline。一个步骤失败,前面 2 小时全部白跑。
- 没有真正的 DAG。步骤之间要么全串行,要么靠 shell 里写
&&自己拼,出错很难定位。 - 资源利用率极低。白天三台机器 95% 时间空转,凌晨又因为串行导致节点利用率起不来。
- 扩容成本线性。想加并发?加机器。机器加完,白天继续空转。
我算了一笔账:三台 8C16G 云主机,按包年包月每月 3200 块,一年接近 4 万。但这三台机器真正在干活的时间,每月不到 180 小时,利用率不到 25%。
02 选型:为什么是 Argo Workflows 而不是 Airflow / Temporal
我先看了一圈方案:
- Airflow:编排能力强,生态成熟。但对我们来说太重,需要单独维护 scheduler、webserver、metadata DB,而且任务最终还是要跑在 K8s Pod 上,多了一层抽象。
- Temporal:我之前做对账系统时用过,durable execution 很强,适合长状态、需要补偿的业务流程。但批处理场景我们更想要 “任务即容器”,不想常驻 worker 吃资源。
- Argo Workflows:直接基于 K8s CRD,Workflow 就是一组 Pod 的 DAG。天然能分时复用节点资源,失败可以按 step 重试,扩缩完全交给 K8s scheduler。
最终选 Argo Workflows 的核心原因就一句话:它让我把 “任务调度” 交给 K8s scheduler,把 “业务编排” 交给 YAML。没有额外中间层,也少了一套系统需要维护。
03 迁移:两周时间,分四步走
我没敢一次性全切,而是制定了为期两周的迁移计划:
- 第 1-2 天:在测试 namespace 部署 Argo Workflows controller,验证 WorkflowTemplate 和 CronWorkflow 基本功能;
- 第 3-5 天:把 nightly pipeline 拆成 5 个 DAG step,在测试环境跑通三次完整流程;
- 第 6-10 天:灰度切流,50% 日期用 Argo、50% 日期仍用 Jenkins,对比耗时和资源占用;
- 第 11-14 天:全量切到 Argo,回收 Jenkins slave 节点,补监控和告警。
下面说说拆分 DAG 时的具体做法。
整个流程拆成 5 个 step:
extract → transform → train → export → load-to-warehouse每个 step 运行在自己的 Pod 里,依赖关系通过 DAG 表达。transform 必须等 extract 完成,export 必须等 train 完成,但 extract 和后续一些前置准备可以并行。
3.1 用 WorkflowTemplate 沉淀可复用模板
我先写了一个通用模板,所有 step 都引用它:
apiVersion:argoproj.io/v1alpha1kind:WorkflowTemplatemetadata:name:nightly-batch-stepnamespace:batchspec:templates:-name:batch-containerinputs:parameters:-name:image-name:command-name:cpuvalue:"500m"-name:memoryvalue:"1Gi"-name:storagevalue:"10Gi"container:image:"{{inputs.parameters.image}}"command:["/bin/sh","-c"]args:["{{inputs.parameters.command}}"]resources:requests:cpu:"{{inputs.parameters.cpu}}"memory:"{{inputs.parameters.memory}}"limits:cpu:"{{inputs.parameters.cpu}}"memory:"{{inputs.parameters.memory}}"volumeMounts:-name:batch-datamountPath:/datavolumes:-name:batch-datapersistentVolumeClaim:claimName:batch-data-pvc统一模板的好处很明显:每个 step 只声明自己需要的资源,训练 step 给 4C8G,导出 step 给 500m/1Gi,谁也不多占。资源请求精确到 step 级别,K8s scheduler 才能把碎片时间利用起来。
3.2 CronWorkflow 定时触发
然后用 CronWorkflow 替代 Jenkins 的定时任务:
apiVersion:argoproj.io/v1alpha1kind:CronWorkflowmetadata:name:nightly-pipeline-v2namespace:batchspec:schedule:"30 0 * * *"timezone:"Asia/Shanghai"startingDeadlineSeconds:120concurrencyPolicy:ForbidsuccessfulJobsHistoryLimit:3failedJobsHistoryLimit:3workflowSpec:entrypoint:nightly-dagserviceAccountName:argo-batch-satemplates:-name:nightly-dagdag:tasks:-name:extracttemplateRef:name:nightly-batch-steptemplate:batch-containerarguments:parameters:-name:imagevalue:"registry/batch-extract:v2.1"-name:commandvalue:"python extract.py --date {{workflow.parameters.batch-date}}"-name:storagevalue:"50Gi"-name:transformdependencies:[extract]templateRef:name:nightly-batch-steptemplate:batch-containerarguments:parameters:-name:imagevalue:"registry/batch-transform:v2.1"-name:commandvalue:"python transform.py --stage full"-name:cpuvalue:"2"-name:memoryvalue:"4Gi"-name:storagevalue:"80Gi"-name:traindependencies:[transform]templateRef:name:nightly-batch-steptemplate:batch-containerarguments:parameters:-name:imagevalue:"registry/batch-train:v2.1"-name:commandvalue:"python train.py --models all"-name:cpuvalue:"4"-name:memoryvalue:"8Gi"-name:storagevalue:"100Gi"-name:exportdependencies:[train]templateRef:name:nightly-batch-steptemplate:batch-containerarguments:parameters:-name:imagevalue:"registry/batch-export:v2.1"-name:commandvalue:"python export.py"-name:load-to-warehousedependencies:[export]templateRef:name:nightly-batch-steptemplate:batch-containerarguments:parameters:-name:imagevalue:"registry/batch-load:v2.1"-name:commandvalue:"python load.py --warehouse prod"这里有两个细节我认为很关键:
concurrencyPolicy: Forbid:防止前一次没跑完又触发一次,造成资源踩踏;startingDeadlineSeconds: 120:如果 2 分钟内 scheduler 没把 workflow 调度起来,直接视为失败,避免静默错过调度窗口。
3.3 资源分时复用,训练 step 独占、其余 step 错峰填缝
我把训练 step 放在 01:30-03:00 这个窗口,给它nodeAffinity优先落到带高内存的节点上。其他导出、load 步骤用通用计算节点,跑完立刻释放。
affinity:nodeAffinity:preferredDuringSchedulingIgnoredDuringExecution:-weight:100preference:matchExpressions:-key:workload-typeoperator:Invalues:["batch-memory"]结果是:同一批节点,白天跑微服务 Pod,凌晨跑批处理 Pod,24 小时都有人干活。K8s 集群的夜间利用率从 23% 直接拉到 71%。
04 量化对比:23% → 71% 是怎么算的
改造前后各跑了两周,数据如下。需要说明的是,灰度阶段我用标签把 Jenkins 和 Argo 跑的日子区分开,确保对比的是同一业务负载:
| 指标 | 改造前(Jenkins) | 改造后(Argo) | 变化 |
|---|---|---|---|
| 夜间固定节点数 | 3 台 | 0 台 | 全部释放 |
| 夜间平均 CPU 利用率 | 23% | 71% | +48% |
| 夜间平均内存利用率 | 31% | 68% | +37% |
| pipeline 平均耗时 | 5h 40min | 2h 15min | -60% |
| 最长排队等待 | 4h 12min | 0 | 消除 |
| 单 step 失败重跑耗时 | 从头 5h+ | 平均 8min | 按 step 重试 |
| 月均节点成本 | 约 3,200 元 | 约 1,100 元 | -66% |
| 调度失败次数/周 | 2-3 次 | 0 次 | 消除 |
| 数据产出准时率 | 87% | 100% | 稳定 5:30 前产出 |
说明一下:
- 利用率计算的是凌晨 0-6 点批处理窗口内,所有实际运行 Pod 的
container_cpu_usage_seconds_total / kube_pod_container_resource_requests_cpu平均值; - 没有到 100% 是因为 K8s 预留了系统进程、kubelet 开销,以及我们故意保留了 20% buffer 防止某个 step 突发暴涨;
- 成本下降主要来自于白天不再保留空转节点,批处理 Pod 用集群现有容量,跑完即走。
最让我意外的是 pipeline 耗时从 5 小时 40 分降到 2 小时 15 分。原因不是机器变快了,而是 DAG 把能并行的步骤并行起来,并且 K8s scheduler 能根据资源实时填缝,不再受固定 slave 的锁限制。
05 踩坑记录:这 5 条建议能省你半天
1. 默认 Pod 起不来?检查 service account 权限
Argo controller 需要给 Pod 创建、查看日志等权限。如果 service account 没配,workflow 会一直 Pending。我建了一个专用 sa 和 role:
apiVersion:v1kind:ServiceAccountmetadata:name:argo-batch-sanamespace:batch---apiVersion:rbac.authorization.k8s.io/v1kind:Rolemetadata:name:argo-batch-rolenamespace:batchrules:-apiGroups:[""]resources:["pods","pods/log"]verbs:["get","list","watch"]-apiGroups:["argoproj.io"]resources:["workflows"]verbs:["get","list","watch"]---apiVersion:rbac.authorization.k8s.io/v1kind:RoleBindingmetadata:name:argo-batch-bindingnamespace:batchsubjects:-kind:ServiceAccountname:argo-batch-sanamespace:batchroleRef:kind:Rolename:argo-batch-roleapiGroup:rbac.authorization.k8s.io2. 大文件不要用默认 minio artifact 存储
我们一开始用 minio 做 artifact,transform 输出 60GB Parquet,每次上传下载要 20 分钟。后来改成 NFS 共享卷 +volumeClaimTemplates,同 namespace 下多个 step 直接挂载,省掉串行上传。
volumeClaimTemplates:-metadata:name:batch-dataspec:accessModes:["ReadWriteOnce"]storageClassName:nfs-batchresources:requests:storage:100Gi3. retryStrategy 别只写 count,要写 expression
无脑重试会把 OOM 也重试 3 次,浪费资源。我改成只重试非资源类失败:
retryStrategy:limit:3retryPolicy:"OnError"expression:"asInt(lastRetry.exitCode) != 137 && asInt(lastRetry.exitCode) != 143"137 是 OOMKilled,143 是 SIGTERM,这两种情况重试基本没用,应该直接告警人工介入。
4. CronWorkflow missed schedule 很难查
有天凌晨 pipeline 没触发,查了半天才发现 controller 当时重启了。建议配 Prometheus 告警:
-alert:ArgoCronWorkflowMissedScheduleexpr:|(argo_workflows_cronworkflows_info - argo_workflows_cronworkflows_triggered_total) > 0for:5mlabels:severity:warningannotations:summary:"CronWorkflow missed schedule"5. 监控 UI 只看 Argo UI 不够,要加 workflow_duration 和 pod_phase
我搭了 Grafana 看板,核心指标有三个:
argo_workflows_workflow_duration按 workflow 名分位,看整体耗时趋势;kube_pod_status_phase{phase=~"Pending|Failed"}按 workflow 标签过滤,快速定位卡住的 Pod;container_cpu_usage_seconds_total / kube_pod_container_resource_requests_cpu_cores按 Pod 聚合,算实际资源利用率。
另外我还加了一个指标:每个 CronWorkflow 的last_successful_time和last_scheduled_time差值,超过 10 分钟就触发告警。这个比单纯看 Pod 是否成功更直接,因为有时候 workflow 根本没被触发,Pod 监控是看不到的。
6. 别忘了给 Argo 组件本身做高可用
controller 默认只跑一个副本,某天节点维护时它重启了,导致 3 个 workflow 同时挂起。我后来给 controller 配了 2 个副本 + PodDisruptionBudget,并把它固定在两个不同可用区的节点上。虽然 Argo 号称 stateless,但 controller 重启时正在执行的 workflow 会短暂卡住,关键业务还是要考虑这一点。
07 迁移 checklist:如果你也准备动手,按这个顺序来
我把这次迁移的 checklist 整理出来,方便你复制粘贴:
- 梳理现有 pipeline:画出每个步骤的输入输出、执行时间、资源占用、失败重试点;
- 搭建 Argo Workflows:先在测试 namespace 部署 controller,确认版本 ≥ 3.5,开启 metrics 端点;
- 设计 WorkflowTemplate:把通用容器模板抽象出来,参数化 image、command、cpu、memory;
- 拆 DAG:用依赖关系替代时间 sleep,把能并行的步骤并行;
- 选 artifact 方案:小文件用 minio/S3,大文件用共享 PVC 或 NFS;
- 配 RBAC + service account:给 workflow Pod 最小权限,别用 default sa;
- 写 CronWorkflow:设置
concurrencyPolicy: Forbid和startingDeadlineSeconds; - 灰度对比:至少跑 5-7 天双跑,记录耗时、资源利用率、失败率;
- 加监控告警:workflow_duration、pod_phase、cron missed schedule、资源利用率;
- 回收旧资源:确认稳定后再下线 Jenkins slave 节点,省钱。
按这个顺序走,基本不会踩大坑。
06 架构讨论:不是 Jenkins 不好,是用错了地方
迁移完我复盘了一下,Jenkins 并不是被 “淘汰” 了,而是回归它该干的事:CI/CD 流水线、代码构建、镜像打包。这些场景 Jenkins 的插件生态和可视化流水线依然很能打。
批处理这种 “定时触发、DAG 编排、资源弹性” 的场景,交给 K8s-native 的 Argo Workflows 更对味。整个架构变成:
GitLab / GitHub -> Argo Workflows -> K8s Job Pod ^ | CronWorkflow 定时触发 | Prometheus + Grafana 监控这个架构的核心好处是:调度层和编排层分离。
- 调度层:K8s scheduler 根据节点资源实时决定 Pod 落在哪台机器上,不需要人工指定 slave;
- 编排层:Argo Workflows 用 DAG 表达依赖,失败重试可以精确到 step;
- 资源层:Pod 跑完即释放,不再占用固定节点,白天和晚上都能充分利用集群。
如果规模再大一点,我会考虑这几个方向:
- Argo Events做事件触发,替代部分 CronWorkflow。比如上游数据到达 S3 后自动触发 workflow,而不是死等半夜 0 点 30 分;
- Hera SDK让数据科学家用 Python 写 workflow,而不是手写 YAML。团队里大部分人不是 K8s 专家,降低门槛很重要;
- workflow-level 成本分摊,把每个批处理任务的资源成本打到业务线账上。这一步做好了,能反向推动业务优化自己的任务资源申请;
- archive 和 artifact GC 策略。workflow 历史记录默认保存在 etcd 里,跑久了会成为集群负担,需要定期归档到 S3 并清理。
还有一个很多人忽略的点是:Argo Workflows 让你的批处理变成了 “声明式” 的。以前 Jenkins pipeline 的改动是改 Groovy 脚本,现在改 YAML 并走 Git 流程。配合 Argo CD 或 Flux,批处理流程本身也可以被 GitOps 管理。
写在最后
这次迁移最值钱的一课,不是学会了 Argo Workflows 的 YAML 语法,而是让我重新理解了 “资源利用率” 这个词。
以前我以为利用率低就是机器买大了。其实很多时候,是调度模型太旧。把任务从固定节点里解放出来,让 K8s 根据实际资源去填缝,71% 并不是上限,而是我们刻意留的 buffer。如果胆子大一点,把训练任务进一步拆分并行,把 buffer 压到 10%,利用率还能再往上走。
对于还在用 Jenkins 跑定时批处理的团队,我的建议是分步走:先拿一条非核心 pipeline 试点,跑通 WorkflowTemplate 和 CronWorkflow;再逐步把高耗时的步骤拆成独立 Pod;最后把资源请求精确化,让 scheduler 真正发挥作用。不要一上来就追求全切,灰度对比数据才是说服老板和团队最好的材料。
Argo Workflows 的 YAML 乍看啰嗦,但写顺之后,你会爱上那种 “资源按任务走,失败按步回滚” 的清爽感。
下一篇我可能会写写怎么用 Argo Events 把这些批处理改成事件驱动,或者聊聊用 Hera SDK 让数据团队不写 YAML 也能跑 Argo。感兴趣的可以蹲一下。
参考链接
- Argo Workflows 官方文档:https://argoproj.github.io/argo-workflows/
- CronWorkflow 调度说明:https://argoproj.github.io/argo-workflows/cron-workflows/
- Hera Python SDK:https://github.com/argoproj-labs/hera