我把 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 和 PDF04: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 CRDWorkflow 就是一组 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 个 stepextract → 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:10Gicontainer: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/ShanghaistartingDeadlineSeconds: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凌晨跑批处理 Pod24 小时都有人干活。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 和 roleapiVersion: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 做 artifacttransform 输出 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:OnErrorexpression:asInt(lastRetry.exitCode) ! 137 asInt(lastRetry.exitCode) ! 143137 是 OOMKilled143 是 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 schedule5. 监控 UI 只看 Argo UI 不够要加 workflow_duration 和 pod_phase我搭了 Grafana 看板核心指标有三个argo_workflows_workflow_duration按 workflow 名分位看整体耗时趋势kube_pod_status_phase{phase~Pending|Failed}按 workflow 标签过滤快速定位卡住的 Podcontainer_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 SDKhttps://github.com/argoproj-labs/hera
我把 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 和 PDF04: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 CRDWorkflow 就是一组 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 个 stepextract → 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:10Gicontainer: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/ShanghaistartingDeadlineSeconds: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凌晨跑批处理 Pod24 小时都有人干活。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 和 roleapiVersion: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 做 artifacttransform 输出 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:OnErrorexpression:asInt(lastRetry.exitCode) ! 137 asInt(lastRetry.exitCode) ! 143137 是 OOMKilled143 是 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 schedule5. 监控 UI 只看 Argo UI 不够要加 workflow_duration 和 pod_phase我搭了 Grafana 看板核心指标有三个argo_workflows_workflow_duration按 workflow 名分位看整体耗时趋势kube_pod_status_phase{phase~Pending|Failed}按 workflow 标签过滤快速定位卡住的 Podcontainer_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 SDKhttps://github.com/argoproj-labs/hera