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 kubeflow1.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-operatorkubeflow是一个面向机器学习工作流的平台它不是一个单体应用而是一堆CRD和Operator的集合。其中tf-operator负责管理TensorFlow任务它定义了TFJob这个自定义资源。你提交一个TFJob的YAMLtf-operator会帮你创建出对应的Pod、Service并处理好分布式训练中PSParameter 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分支的manifeststf-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混部项目里跑TensorFlowGPU几乎是刚需。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 YAMLapiVersion: 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-nametfjob-mnist-single看到tfjob状态变为SucceededPod以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的经典分布式架构是PSParameter Server和Worker。tf-operator对这两种角色的支持非常完善你不需要自己在代码里写任何服务发现的逻辑只需要在YAML里声明两个ReplicaSpecsapiVersion: 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: 4Gitf-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的副本数改为1prometheus的resources调低避免监控本身消耗太多资源。我在这类项目里prometheus的requests只给了1核2G足够处理几百个Pod的指标量了。4.3 GPU指标采集dcgm-exporterprometheus默认采集不到GPU指标需要额外部署dcgm-exporter。这个组件由NVIDIA开源通过DCGMData Center GPU Manager库采集每个GPU的利用率、显存、温度、功率等数据。部署方式依然很简单用helmhelm repo add nvdp https://nvidia.github.io/dcgm-exporter/helm-charts helm install dcgm nvdp/dcgm-exporter --namespace monitoringdcgm-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-operatorkube-prometheus-stack默认配置你可以在helm values里改掉别用默认密码上生产。登录后第一步就是添加Prometheus数据源在Configuration - Data Sources里填上prometheus服务的地址比如http://prometheus-operated.monitoring.svc:9090点击SaveTest显示成功即可。接着就是导入面板。kube-prometheus-stack自带了一组kubernetes的默认面板比如Kubernetes / Compute Resources / NodeKubernetes / 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{namespaceonline-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面板搭配几个时间序列图组成的观察效率非常高。
返回列表