拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

离线混部实战:Kubeflow TFJob + Prometheus + Grafana监控全流程

离线混部实战:Kubeflow TFJob + Prometheus + Grafana监控全流程

离线混部场景下跑TensorFlow任务,整套流程我都帮你踩过一遍了:Kubeflow + Prometheus + Grafana实战记录

最近在搞一个比较典型的离线混部项目:在线业务占用大量k8s集群节点,但深夜和低峰期的资源利用率惨不忍睹,老板要求把离线机器学习任务也塞进同一批集群节点里跑,把CPU和GPU余量吃干榨净。这个需求落到实操层面,就是把TensorFlow训练任务以kubeflow的tf-operator方式部署到k8s,再配套一套prometheus + grafana监控体系,随时能看到混部任务到底吃了多少资源,会不会跟在线业务打架。整套流程踩了不少坑,这里完整记录一下。

这篇内容适合三类人看:一是刚接触k8s和kubeflow、想弄清楚TFJob到底怎么用的;二是已经在用k8s跑业务、准备把机器学习任务混部上线的运维或平台工程师;三是自己搭了一套AI训练环境、想补上监控这块短板的同学。我会按真实项目推进的顺序来讲,从环境准备到任务提交,再到监控部署和面板配置,每一段都是我实际操作过的步骤,不是网上抄来的。

先说一下我的一套方案选型结论:混部场景下,调度层用k8s原生能力,任务层用kubeflow的TFJob CRD,监控指标采集用prometheus生态,可视化用grafana。这个组合不是唯一解,但在我这个项目里是最省事、最容易让团队其他人接手的一条路。

1. 先搞清楚:离线混部为什么一定要上k8s + kubeflow

1.1 混部到底解决什么问题

在线业务通常以Deployment、StatefulSet的形式跑在k8s里,为了保证高峰期的服务质量,资源requests往往比实际使用量高出一大截,这直接导致集群整体的CPU和内存利用率常年徘徊在20%到30%之间。离线训练任务的特点是“可容忍延迟、可抢占、时段性强”,正好可以填进在线业务留下的资源缝隙里。

把两类任务放到同一个k8s集群,就是混部(co-location)。但混部有个前提:离线任务不能影响在线业务的稳定性。这就需要有资源配额限制、优先级控制、可抢占机制,并且要能实时监控每个任务用掉了多少资源。k8s本身就提供LimitRange、ResourceQuota、PriorityClass这些能力,等于基础底座已经有了。你在裸机上直接起一个TensorFlow进程也能训练,但要做到“随时来一个任务、随时调度到合适的节点、随时被驱逐也不怕、训练进度能恢复”,那还是得上k8s。

很多人一开始分不清k8s和docker。简单说,docker解决的是“怎么把TensorFlow和它的依赖环境打包起来、在任意机器上一键运行”的问题,k8s解决的是“一堆跑着容器的机器,如何统一调度、保证资源利用率、实现服务自愈和水平扩展”的问题。单机训练你可以只靠docker,但一旦你想把十几个训练任务按优先级塞进一个几十台机器的集群,就离不开k8s了。

1.2 为什么任务层选用kubeflow + tf-operator

kubeflow是一个面向机器学习工作流的平台,它不是一个单体应用,而是一堆CRD和Operator的集合。其中tf-operator负责管理TensorFlow任务,它定义了TFJob这个自定义资源。你提交一个TFJob的YAML,tf-operator会帮你创建出对应的Pod、Service,并处理好分布式训练中PS(Parameter Server)和Worker之间互相发现的逻辑。

你完全可以手动写StatefulSet或Deployment去跑TensorFlow,但分布式训练会非常痛苦——手动管理每个Worker的地址、处理挂掉之后的重新拉起、协调训练版本,这些全要自己造轮子。tf-operator把这些事情全包了,而且它原生支持TensorFlow的TF_CONFIG环境变量注入,Worker启动后自动就知道自己该连哪个PS。

选型时我也看了kueue、Volcano这些调度增强组件,它们对排队、抢占确实有优势。但如果你的核心诉求是“先把TensorFlow任务跑起来,再慢慢优化调度”,tf-operator是最短路径,跟k8s原生Job一样通过kubectl apply就能提交,团队上手成本极低。

1.3 监控为什么选prometheus + grafana

混部项目上线第一天,如果没监控,负责人心里非常没底。在线业务和离线任务在同一台物理机上,谁的CPU飙了、谁的GPU显存把卡打满了,必须一眼能看出来。

prometheus在k8s监控领域几乎是事实标准,它通过拉模型采集指标,配合k8s的服务发现机制,能够自动找到每一个节点、每一个Pod的metrics端点。grafana则负责把prometheus里的数据变成能看的图表。放到混部场景里,我关心的指标有:节点CPU/内存使用率、在线业务容器和离线训练容器的资源占用对比、GPU利用率、显存占用、训练任务的状态变化。这一整套诉求,prometheus + grafana几分钟就能搭起来,而且不需要在业务容器里埋什么SDK,只要暴露标准的/metrics端点即可。

2. 环境准备:从k8s集群到kubeflow全家桶

2.1 k8s版本选型建议

kubeflow和tf-operator的版本兼容性是很多新手第一个“滑铁卢”。我在项目里用的k8s是1.26版本,kubeflow用的是v1.7.0分支的manifests,tf-operator对应v1.7.0版本。这个组合跑得很稳。

如果你准备从零搭集群,建议Kubernetes版本不要低于1.25,尽量别追最新大版本,因为kubeflow官方的测试节奏跟不上k8s的发布速度。选太新的k8s版本,很可能遇到kubeflow里某些组件(比如webhook、admission controller)跟API版本不兼容的问题。

另外提醒一下:kubeflow全家桶包含很多组件——中央Dashboard、Notebook、Katib、Pipelines等,如果只是跑TensorFlow任务,你没有必要全部安装。我当时的做法是直接从kubeflow manifests里抽取tf-operator相关部分安装,这样可以省掉一大半组件,减少对集群资源的占用和潜在的故障点。

2.2 GPU节点准备:驱动、container runtime、device plugin

混部项目里跑TensorFlow,GPU几乎是刚需。GPU节点要正常工作,需要依次确认三件事:

第一,GPU驱动已安装。用nvidia-smi检查驱动版本,比如驱动是535.xx或者545.xx,这是容器调用GPU的基础。

第二,容器运行时支持GPU。现在主流的k8s集群用containerd作为运行时,先确保节点上装了nvidia-container-toolkit,然后在/etc/containerd/config.toml里配置好nvidia runtime。这一步没做对,后面Pod即使调度到了GPU节点,容器启动也会报“could not select device driver”。

第三,安装NVIDIA device plugin。这个插件以DaemonSet的方式运行在每个GPU节点上,它负责把GPU资源上报给kubelet。装完之后,你会在节点上看到类似nvidia.com/gpu: 8这样的可分配资源。到这一步,k8s调度器才知道哪台节点有GPU、还剩下多少GPU可以调度。

验证GPU节点就绪的方式很简单,执行kubectl describe node ,在Allocatable里看看有没有nvidia.com/gpu。如果这个字段不存在,说明device plugin有问题;如果字段存在但显存信息不对,大概率是驱动和toolkit的版本匹配出了问题。

2.3 安装tf-operator并验证CRD

如果你不想装完整kubeflow,可以单独部署tf-operator。最简单的办法是直接用官方manifests里的tf-operator部署清单:

# 克隆kubeflow官网manifests仓库,选取v1.7.0标签 git clone -b v1.7.0 https://github.com/kubeflow/manifests.git cd manifests # 只需要安装tf-operator相关目录 kustomize build contrib/tf-operator/ | kubectl apply -f -

如果你更喜欢helm,也可以直接用kubeflow组织维护的tf-operator helm chart。安装完成后,执行kubectl get crd、kubectl get pods -n kubeflow,应该能看到tfjobs.kubeflow.org这个CRD,以及tf-operator的controller-manager Pod处在Running状态。

在这里我要重点说一个踩坑点:tf-operator部署后,默认监听所有命名空间,但需要RBAC权限。如果之前安装过其他版本的kubeflow,很可能因为CRD结构冲突导致TFJob提交后没有反应。遇到这种情况,把旧的CRD和controller删干净再重新apply,别图省事直接覆盖。

3. 部署TensorFlow训练任务:从单机到分布式的完整过程

3.1 第一个TFJob:单机单卡,先把链路跑通

万事开头难,难在链路不通。我建议你的第一个TFJob不要一上来就搞分布式,先跑一个单Worker单GPU的MNIST训练。下面是一个最简可用的TFJob YAML:

apiVersion: kubeflow.org/v1 kind: TFJob metadata: name: tfjob-mnist-single namespace: ml-dev spec: tfReplicaSpecs: Worker: replicas: 1 restartPolicy: OnFailure template: spec: containers: - name: tensorflow image: tensorflow/tensorflow:2.13.0-gpu command: - python - /app/train.py resources: limits: nvidia.com/gpu: "1" cpu: "4" memory: 8Gi requests: nvidia.com/gpu: "1" cpu: "2" memory: 4Gi

提交命令很简单:

kubectl apply -f tfjob-mnist-single.yaml kubectl get tfjob -n ml-dev kubectl get pods -n ml-dev -l job-name=tfjob-mnist-single

看到tfjob状态变为Succeeded,Pod以Completed结束,说明链路通了。这里有两个关键细节:

第一,restartPolicy。tf-operator要求TFReplicaSpecs里的restartPolicy不能是Always,一般用OnFailure或者ExitCode。这个设计参考的是批量任务的语义,训练失败后Controller自动重启Pod,但不会像在线服务那样一直保活。

第二,resources里的requests和limits。混部场景下千万不要只写limits不写requests,否则调度器对资源预估会失真,在线业务和离线任务容易互相挤占。requests是给调度器看的,limits是给运行时压制的,两者写清楚是混部的基本素养。

3.2 让训练容器能跑起来:镜像里的hidden bug

在第一个TFJob里,大家最容易翻车的是镜像问题。如果你直接使用tensorflow/tensorflow这个官方镜像,它默认的工作目录、Python路径和你训练的脚本路径可能对不上。

我当时在镜像里放了一个train.py,但TFJob里command直接写python /app/train.py,容器启动就报“No module named xxx”。排查到最后发现是镜像里TensorFlow是预装在一个虚拟环境里的,直接用系统python跑不对。这里给大家一个避坑建议:在构建训练镜像时,用dockerfile把入口封装成shell脚本,显式传入需要设置的环境变量和Python路径,例如:

FROM tensorflow/tensorflow:2.13.0-gpu WORKDIR /app COPY train.py /app/train.py ENTRYPOINT ["python", "/app/train.py"]

镜像在本地机器上能运行,不代表在k8s里能运行,因为你少了Pod的运行时环境约束。每次改完镜像,先docker run试一遍,确认无误再推到镜像仓库,能省掉大量Pod反复CrashLoopBackOff的时间。

3.3 分布式训练:PS和Worker怎么配合

单机训练验证完,就可以上分布式了。TensorFlow的经典分布式架构是PS(Parameter Server)和Worker。tf-operator对这两种角色的支持非常完善,你不需要自己在代码里写任何服务发现的逻辑,只需要在YAML里声明两个ReplicaSpecs:

apiVersion: kubeflow.org/v1 kind: TFJob metadata: name: tfjob-distributed namespace: ml-dev spec: tfReplicaSpecs: PS: replicas: 2 restartPolicy: OnFailure template: spec: containers: - name: tensorflow image: registry.example.com/ml/train:latest resources: requests: cpu: "2" memory: 4Gi Worker: replicas: 4 restartPolicy: OnFailure template: spec: containers: - name: tensorflow image: registry.example.com/ml/train:latest resources: limits: nvidia.com/gpu: "1" cpu: "4" memory: 8Gi requests: nvidia.com/gpu: "1" cpu: "2" memory: 4Gi

tf-operator会为每个Worker和PS生成一个Service,并把集群拓扑信息写入TF_CONFIG环境变量。Worker 0拿到的是一个chief节点的角色,负责保存checkpoint。如果你的训练代码带了分布式策略,像tf.distribute.experimental.ParameterServerStrategy,那基本不需要改代码就能直接跑起来。

分布式训练有一个经验性的资源配比:PS不一定需要GPU,但CPU和内存要给足,因为参数汇总和下发都是它干的;Worker则需要按GPU卡数去配。在我这个项目里,4个Worker配2卡PS,跑一个中型推荐模型,训练吞吐比单机提升了大约2.8倍。

3.4 混部专属:用PriorityClass给离线任务“降级”

混部场景最有挑战的是如何保证在线业务稳定。我的做法很直白:给所有TFJob分配一个低优先级的PriorityClass。

apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: offline-training value: 100 globalDefault: false description: "离线训练任务优先级"

在TFJob的PodTemplate里指定priorityClassName: offline-training。这样在线业务节点资源紧张时,kubelet驱逐Pod会优先驱逐低优先级的离线任务。你可能担心训练被中断怎么办?所以训练代码里要配合checkpoint机制,定期保存模型状态。被驱逐的Pod由tf-operator自动拉起后,加载最新checkpoint继续训练。整体看,混部项目要让训练任务具备“断点续训”能力,这不是一个额外需求,而是必备需求。

再补充一点:混部时建议给训练Pod加一个主动驱逐时的优雅退出时间。可以通过Pod的terminationGracePeriodSeconds设置,给训练代码充足的保存checkpoint时间。我一般设置60秒左右,太短保存不完,太长在线资源被占用过久。

4. Prometheus监控部署:让混部资源使用有迹可循

4.1 监控目标的拆解

监控要分三层来看:

第一层是节点层。每个节点的CPU、内存、网络、磁盘IO,这部分数据主要由node-exporter提供。

第二层是Kubernetes对象层。比如Pod的运行状态、容器的CPU/内存实际使用量、GPU调度数量、事件变化,这部分主要依赖cAdvisor和kube-state-metrics。

第三层是任务层。TFJob训练到了第几个epoch、GPU利用率是多少、显存是否溢出,这需要借助NVIDIA的dcgm-exporter来采集。

混部场景下,我最常盯的一个视图是:同时显示某个节点的CPU总体利用率、在线服务Pod占用量、离线训练Pod占用量。这个视图能直接告诉我们,混部到底有没有把资源“吃满”,在线服务有没有被干扰。

4.2 部署kube-prometheus-stack:用helm一键装

部署prometheus的方式非常多,如果不想自己拼装一堆组件,建议直接用kube-prometheus-stack这个helm chart。它把prometheus、alertmanager、node-exporter、kube-state-metrics、grafana打包在一起,一条命令就能装好。

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo update helm install monitor prometheus-community/kube-prometheus-stack \ --namespace monitoring \ --create-namespace \ --version 55.5.0

安装后检查Pod状态,你会看到prometheus-operator、alertmanager、grafana等Pod都跑起来了。这里有一点要说:kube-prometheus-stack默认配置比较重,对于一个小规模混部集群,建议把grafana的副本数改为1,prometheus的resources调低,避免监控本身消耗太多资源。我在这类项目里,prometheus的requests只给了1核2G,足够处理几百个Pod的指标量了。

4.3 GPU指标采集:dcgm-exporter

prometheus默认采集不到GPU指标,需要额外部署dcgm-exporter。这个组件由NVIDIA开源,通过DCGM(Data Center GPU Manager)库采集每个GPU的利用率、显存、温度、功率等数据。部署方式依然很简单,用helm:

helm repo add nvdp https://nvidia.github.io/dcgm-exporter/helm-charts helm install dcgm nvdp/dcgm-exporter --namespace monitoring

dcgm-exporter会通过Service在每个GPU节点上暴露9400端口,prometheus需要配置抓取目标。在kube-prometheus-stack里,可以用PodMonitor对象来定义抓取规则。安装dcgm-exporter的helm chart时,官方已经自动创建了PodMonitor,如果你的prometheus开了PodMonitor支持,指标会直接进入prometheus。

验证方法:在prometheus的web界面里,执行一个简单查询,如nvidia_gpu_utilization,如果返回了数据,说明GPU监控链路已经通了。

4.4 告警规则配置详解

prometheus监控不只是看图,真正有价值的是告警。混部场景里,下面几个告警规则我认为必须配上:

第一,节点磁盘或内存不足。这种告警提前发现,能避免在线业务No Space Left的错误。

第二,GPU利用率持续过低且任务仍在运行。可能意味着代码中数据加载瓶颈严重,GPU大部分时间在空转。训练任务占着显卡不干活,在混部项目里是非常巨大的浪费。

第三,训练Pod频繁重启。tf-operator拉起Pod以后,如果反复Crash,很可能是资源不足或者镜像问题,这个需要立刻告警。

告警规则的编写采用prometheus的rules语法。在kube-prometheus-stack里,你可以通过additionalPrometheusRules配置一个额外的PrometheusRule对象。举一个GPU利用率告警的例子:

apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: name: gpu-alerts namespace: monitoring spec: groups: - name: gpu-training rules: - alert: GPUIdleLongTime expr: avg(nvidia_gpu_utilization) < 10 for: 30m labels: severity: warning annotations: summary: "GPU利用率低于10%超过30分钟" description: "当前GPU平均利用率 {{ $value }}%,请检查训练任务是否陷入瓶颈。"

告警触发后会送到Alertmanager,你可以配置它转发到钉钉、企业微信或邮件。实际项目里,我建议告警先发给运维群,等运营稳定了再逐步加上自动化的自愈动作,不要一开始就把告警搞得太激进,否则全是噪音。

5. Grafana可视化:把十几个面板浓缩成一块“驾驶舱”

5.1 数据源接入与官方面板导入

grafana部署完成后,默认账号密码是admin/prom-operator(kube-prometheus-stack默认配置,你可以在helm values里改掉,别用默认密码上生产)。登录后第一步就是添加Prometheus数据源,在Configuration -> Data Sources里,填上prometheus服务的地址,比如http://prometheus-operated.monitoring.svc:9090,点击Save&Test,显示成功即可。

接着就是导入面板。kube-prometheus-stack自带了一组kubernetes的默认面板,比如Kubernetes / Compute Resources / Node,Kubernetes / Compute Resources / Pod等,这些直接用官方默认就很不错。GPU相关的面板,我推荐去grafana.com的dashboard市场搜“NVIDIA DCGM Exporter”,选择一个高下载量的面板ID(比如12219),导入后选对数据源,GPU利用率、显存占用、温度等信息就能直接展示。

5.2 自己拼一个混部资源视角的面板

官方面板大多是通用型的,但混部项目里你最想看的其实是“在线和离线的资源对比”。这种面板通常官方不会给你调好,需要自己写promql。

一个很实用的Panel是“节点CPU使用率构成”饼图,用下面这个查询:

sum(rate(container_cpu_usage_seconds_total{namespace="online-business"}[5m]))

再来一个“当前训练任务GPU利用率曲线”:

avg(nvidia_gpu_utilization{pod=~"tfjob-.*"}) by (pod)

grafana的变量功能非常强大。我在面板上设置了一个namespace变量,下拉框里可以切换不同的命名空间,这样一来同一个面板既能看在线业务也能看离线训练。

混部独有的一个功能是“资源水位”视图。你可以在grafana里做一个表格Panel,展示每个节点上的全部Pod名称、请求量、实际使用量、所在QoS等级,一眼就能看出哪个节点资源要爆了。

5.3 将面板拷贝到另一个实例或分享给同事

grafana面板从一个实例迁移到另一个实例,只需要三步:导出JSON、在另一个实例导入JSON、重新选择数据源。这个操作特别适合团队协作场景,比如我在测试环境调好了一组打分面板,导出JSON后直接发给同事,他在生产环境grafana里导入就能用,不需要重新一个个拉图表。

面板的JSON文件会包含所有Panel的配置、变量定义、告警规则。如果你准备跨团队分享,建议把数据源uid替换成“${DS_PROMETHEUS}”这种变量,避免导入后提示数据源找不到。我一开始没注意这个细节,结果同事导入后所有图表全是No Data,排查了半天才发现是数据源uid不匹配。

还有个小技巧:grafana的Library Panel支持把常用Panel保存为一个公共组件,后续新建Dashboard直接引用,不用反复复制粘贴。混部场景里的“CPU水位”Panel我就做成了Library Panel,新项目接进来直接拖拽使用,效率高出不少。

6. 常见问题排查与避坑清单

6.1 TFJob提交后Pod一直Pending

这是最常见的问题。先执行kubectl describe pod看事件,十有八九是“0/8 nodes available: 4 Insufficient nvidia.com/gpu, 4 Insufficient memory”。这说明GPU或内存资源不够。在混部项目里,还经常遇到资源实际上剩余很多,但因为配额限制导致无法调度。我的建议是优先看ResourceQuota和LimitRange是否把命名空间的资源限制得太死。

另外检查一下PriorityClass有没有生效。如果低优先级任务的Pod因为没有抢占逻辑而一直排队,可以用k8s的抢占机制来缓解。优先级的配置需要你去确认PodTemplate里的priorityClassName写对了,而且这个PriorityClass确实存在。

6.2 Pod起来后一直CrashLoopBackOff

大概率是镜像问题,按我前面说的先在本地docker run一遍。如果本地没问题,就kubectl logs看容器日志。GPU训练常见的错误是“CUDA_ERROR_NO_DEVICE”,这通常是device plugin没部署好,或者运行时没有nvidia runtime配置。在混部场景里还要注意一点:Pod被调度到GPU节点后,如果节点上有其他在线业务已经占满显存,但k8s的device plugin没有正确感知显存占用,就可能出现这个错。遇到这种,去确认dcgm-exporter监控图里的显存占用情况。

6.3 prometheus监控数据空白

首先确认prometheus的targets页面里,对应的采集目标是否都在UP状态。GPU指标如果查不到,去9100端口验证node-exporter有没有数据,去9400端口验证dcgm-exporter有没有响应。然后检查PodMonitor的网络选择器是否包含了目标Pod的标签。

grafana面板上如果No Data,最常见的坑是数据源选错、时间范围不对、promql里变量匹配不到数据。记住一个排查顺序:先prometheus Graph页面查询原始数据,有数据再回grafana调面板,别一上来就改图表样式。

6.4 混部后在线业务延迟明显上升

监控图会告诉你答案。查看在线业务Pod的CPU使用率曲线,如果它长期贴近limits上限,说明资源配额给得不到位。混部的核心就是弹性,我在项目里给在线业务和离线任务之间加了一层rubust调度策略——比如给离线任务加PodDisruptionBudget,让它在在线业务需要扩容时自动让出资源。本质上,混部是调度策略和资源配额的博弈,成功的标准是“在线业务的P99延迟几乎不变,离线任务能吃满闲暇资源”。

6.5 关于告警疲劳,多说一句

prometheus默认的一堆告警规则,在混部环境下很容易误报。比如节点内存使用率高,不一定是故障,有可能只是离线任务在高峰期填满了内存。我建议上线初期先关掉大部分告警,只保留节点级和GPU级的关键告警,等跑了一两周,摸清了资源使用的正常水位,再慢慢加规则的阈值。grafana里的告警规则可以直接在面板上配置,配好之后发到Alertmanager统一处理,这样不会告警轰炸。

7. 一些实操经验和心得

这套方案,从环境准备到监控上线,我们实际花了大概三个工作日。其中最耗时间的不是部署,而是调试分布式训练任务和排查GPU环境问题。如果是第一次接触kubeflow,建议先在测试集群里完整跑一遍单机任务,把GPU、镜像、存储这些底层依赖都确认好,再上正式环境。

我个人的一个体会是:离线混部项目里的监控不能只做“资源监控”,还应该把“训练任务状态”也纳入监控范围。TFJob的状态(Running、Succeeded、Failed)、每个训练任务的进度、GPU利用率的变化趋势,这些都是和资源监控同等重要的信号。你可以通过tf-operator自己暴露的metrics接口采集这些数据,也可以在grafana里用Kube状态指标来展示,甚至可以把训练日志接到Loki里做全链路追踪。第一步先把prometheus和grafana这套基础体系跑通,后面再逐步扩展。

最后分享一个小技巧:面向混部场景的grafana面板,不要把每个Panel的图例堆得太满,用几个关键数字把整体状态概括出来,比如“当前在线业务CPU用量、当前离线任务CPU用量、当前GPU利用率、GPU卡数剩余量”。一张面板解决一个核心问题,比塞十几个图表更好用。grafana的Stat面板特别适合做这种摘要式展示,我现在的运维驾驶舱就是由三四个Stat面板搭配几个时间序列图组成的,观察效率非常高。

返回列表