当前位置: 首页 > news >正文

我把 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 个核心痛点:

  1. 资源分配和任务调度强耦合。slave 一旦绑定任务,其他 job 只能排队,即使旁边节点空闲。
  2. 失败重试粒度是整个 pipeline。一个步骤失败,前面 2 小时全部白跑。
  3. 没有真正的 DAG。步骤之间要么全串行,要么靠 shell 里写&&自己拼,出错很难定位。
  4. 资源利用率极低。白天三台机器 95% 时间空转,凌晨又因为串行导致节点利用率起不来。
  5. 扩容成本线性。想加并发?加机器。机器加完,白天继续空转。

我算了一笔账:三台 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 40min2h 15min-60%
最长排队等待4h 12min0消除
单 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.io

2. 大文件不要用默认 minio artifact 存储

我们一开始用 minio 做 artifact,transform 输出 60GB Parquet,每次上传下载要 20 分钟。后来改成 NFS 共享卷 +volumeClaimTemplates,同 namespace 下多个 step 直接挂载,省掉串行上传。

volumeClaimTemplates:-metadata:name:batch-dataspec:accessModes:["ReadWriteOnce"]storageClassName:nfs-batchresources:requests:storage:100Gi

3. 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_timelast_scheduled_time差值,超过 10 分钟就触发告警。这个比单纯看 Pod 是否成功更直接,因为有时候 workflow 根本没被触发,Pod 监控是看不到的。

6. 别忘了给 Argo 组件本身做高可用

controller 默认只跑一个副本,某天节点维护时它重启了,导致 3 个 workflow 同时挂起。我后来给 controller 配了 2 个副本 + PodDisruptionBudget,并把它固定在两个不同可用区的节点上。虽然 Argo 号称 stateless,但 controller 重启时正在执行的 workflow 会短暂卡住,关键业务还是要考虑这一点。

07 迁移 checklist:如果你也准备动手,按这个顺序来

我把这次迁移的 checklist 整理出来,方便你复制粘贴:

  1. 梳理现有 pipeline:画出每个步骤的输入输出、执行时间、资源占用、失败重试点;
  2. 搭建 Argo Workflows:先在测试 namespace 部署 controller,确认版本 ≥ 3.5,开启 metrics 端点;
  3. 设计 WorkflowTemplate:把通用容器模板抽象出来,参数化 image、command、cpu、memory;
  4. 拆 DAG:用依赖关系替代时间 sleep,把能并行的步骤并行;
  5. 选 artifact 方案:小文件用 minio/S3,大文件用共享 PVC 或 NFS;
  6. 配 RBAC + service account:给 workflow Pod 最小权限,别用 default sa;
  7. 写 CronWorkflow:设置concurrencyPolicy: ForbidstartingDeadlineSeconds
  8. 灰度对比:至少跑 5-7 天双跑,记录耗时、资源利用率、失败率;
  9. 加监控告警:workflow_duration、pod_phase、cron missed schedule、资源利用率;
  10. 回收旧资源:确认稳定后再下线 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
http://www.jsqmd.com/news/1314682/

相关文章:

  • 虚拟机单盘多开:链接克隆技术原理与VMware/VirtualBox实战指南
  • 插座式室温采集器:室内温度实时监测终端
  • 从怀疑到真香,我实测半年2026视频音频转文字工具哪个性价比更高?
  • 2026西安厂区沥青修补公司/球场沥青垫层工程公司施工推荐:4个坑5条硬标准,选错返工太糟心 - geo88
  • 肌电信号采集与处理:从传感器原理到Arduino实战应用
  • 433MHz射频模块入门:从ASK调制到Arduino无线通信实战
  • Grove SPDT继电器模块详解:从原理到智能控制实战
  • 跨平台玩家的终极解决方案:WorkshopDL实现Steam创意工坊模组自由下载
  • 【计算机毕业设计单片机案例】双按键可调阈值单片机酒精检测断电保护硬件实现 基于 STM32/51 单片机的车辆酒驾自动切断电源系统开发(020501)
  • 基于RDA5807M的Grove I2C FM接收器:从原理到Arduino驱动实战
  • 如何优雅解决Windows自动锁屏问题?Mouse Jiggler技术深度解析
  • 2026 年 8 月最新 —— 桂林多雨高湿期防水攻略:老房 / 高层 / 低洼院落三种渗漏解决办法 - 超人防水
  • Android输入法调试指南:adb ime命令详解与实战应用
  • 2026 年安徽单招复读班哪个公办大专学校里面有?正规校内办学院校详解 - cc江江
  • 2026福建想考教师资格证?成人大专学前教育!怎么报名?在哪报名?联系方式多少? - 最新资讯
  • AI资本开支竞赛转向“谁收得回”:微软、Meta财报迥异,市场定价逻辑生变
  • Java内存马检测实战:从原理到排查的完整安全指南
  • 解放双手的FGO全自动助手:智能战斗与任务管理的终极解决方案
  • 2026深圳搬家干货大全:收费明细、选公司标准、打包技巧一次讲透 - 深圳顺风搬迁
  • 如何快速部署免费开源的中文字体解决方案:PingFangSC跨平台排版终极指南
  • 终极指南:BiliTools如何用AI智能总结彻底改变你的B站学习方式
  • 谷歌Genie 3世界模型实测:AI如何从草图生成可玩2D游戏场景
  • 20260801-04-观点分析-2026年数字孪生年中盘点
  • 河源紫金黄金回收避坑指南:这家10年合规连锁门店太靠谱了? - 紫金的金
  • SpringBoot3+Vue3+MySQL 社区志愿服务管理系统源码 前后端分离实战
  • 2026河南人口大省找工作难?成人大专提升竞争力!怎么报名?在哪报名?联系方式多少? - 最新资讯
  • 逻辑回归实战:从原理到代码实现与调优全解析
  • 5分钟快速实现Android Studio界面中文化:告别英文障碍,开启母语开发新时代
  • Unity移动端UI性能优化:Sprite管理与图集打包实战指南
  • Seeeduino开发板入门指南:从硬件解析到智能小车项目实战