AI 服务器集群中最让人紧张的状态,不是某一个 GPU 节点掉线,而是同一时刻大量节点集体瘫痪。Astra 集群这次故障发生时,监控页面上出现了一排红色,多个训练任务在几秒内从 Running 变成 Failed,GPU 可用卡数直接归零。第一时间所有人都会怀疑是不是 AI 服务器硬件大批量烧毁,但复盘之后发现,真正的根因并不是显卡或服务器本身,而是存储控制器切换后触发的一次连锁故障。
这篇文章以 Astra 集群的这次故障为线索,完整还原故障现象、信息采集、根因定位、恢复操作和高可用设计。内容重点不是渲染“天命将至”式的焦虑,而是说明 AI 服务器集群为什么会集体瘫痪,以及如何通过架构冗余、参数调优、健康检查设计和调度策略,避免同样的事件再次发生。适合私有化 AI 平台运维、GPU 集群管理员、Kubernetes 平台工程师和负责 AI 基础设施稳定性的人阅读。
1. 先看清AI服务器集群的依赖结构:为什么会出现“集体瘫痪”
1.1 AI服务器集群不是一堆GPU服务器并联
很多初学者会把 AI 服务器集群理解成“多台带 GPU 的服务器放在机柜里,通过网络连在一起”。这种理解在单机实验时成立,但在生产环境里远远不够。AI 训练和推理任务不只是使用 GPU 计算,还需要共享文件系统加载数据集、保存检查点、写日志,需要通过分布式调度器分配资源,需要通过高速网络完成多机通信。任何一个共享依赖出问题,都可能影响大量节点。
更关键的是,集群里的“节点状态”不仅仅由服务器自身硬件决定,还由 Kubernetes、Slurm 这类调度系统的健康检查逻辑决定。一个节点即使物理上完全健康,只要健康检查失败,调度器就会把它标记为不可用。因此,当某个共享组件出现短暂抖动,就可能让一批节点同时从集群状态中消失,表现为“AI服务器集体瘫痪”。
1.2 “集体瘫痪”通常不是所有硬件同时损坏
服务器硬件大批量同时损坏的概率很低。更常见的是以下链路:共享控制面异常、存储系统切换、网络分区、调度器误判、健康检查脚本产生假阳性,或者任务失败后的重试风暴。Astra 集群这次就属于“存储控制器切换 + 健康检查脚本假失败 + 任务重试风暴”三者叠加。
理解这一点非常重要,因为排查思路会完全不同。如果先入为主认为是 GPU 硬件故障,就会逐台去跑nvidia-smi、更换板卡,而忽略共享存储和健康检查脚本的问题,最终延误恢复时间。
1.3 Astra集群的简化拓扑
Astra 集群的内部结构可以简化为四层:
| 层次 | 典型组件 | 故障扩散方式 |
|---|---|---|
| 控制面 | 调度器、控制器、etcd、API Server | 控制面异常会导致节点状态无法更新、任务无法调度 |
| 计算面 | GPU 服务器、Kubelet、设备插件 | 节点健康检查失败会导致节点被标记为 NotReady |
| 存储面 | 分布式存储、共享文件系统、NAS、SAN | IO 挂起会让所有依赖数据的任务阻塞,进而触发健康检查假失败 |
| 网络面 | 业务网络、RDMA/IB、DNS | 网络分区会让节点心跳丢失,导致调度器误判节点离线 |
Astra 集群的存储面采用两台存储控制器做主备切换,计算节点通过 NFS 协议挂载共享数据集目录。控制面部署在三个控制节点上,通过 etcd 维持一致状态。计算节点数量约 30 台,每台装有 8 张 GPU 卡。故障发生时,存储控制器 A 出现异常,自动切换到了控制器 B,切换时间大约 3 分钟。
问题在于 NFS 客户端默认的超时和重试参数适合普通 Web 业务,不一定适合高负载的 AI 训练场景。控制器切换期间,所有挂载在共享目录上的计算节点同时进入 IO 等待,健康检查脚本又因为无法写入临时文件而判定 GPU 异常,最终把大量节点推入了 NotReady 状态。
2. 故障现场信息采集:不要先猜原因,先取证
2.1 第一次看到的异常现象
Astra 集群故障发生时,监控大屏出现以下现象:
- 多个节点状态在 Running 和 NotReady 之间反复跳动;
- GPU 可用卡数从 200 多张降到 0;
- 训练任务大面积失败,调度队列中出现大量 Pending;
- 告警平台同时触发节点心跳告警、任务失败告警和存储延迟告警;
- 进入服务器执行命令时,部分
ls、df、cat操作卡住超过 10 秒。
这些现象很容易让人误以为是计算节点集体死机。但注意到存储延迟告警也同时出现,说明故障很可能从存储层开始向上扩散。因此在做任何重启操作之前,先按节点、存储、控制面、网络四层采集现场信息。
2.2 采集节点状态的命令集合
在 Kubernetes 环境中,第一步是查看节点整体状态:
kubectl get nodes -o wide kubectl describe nodes | grep -E "Conditions:|Ready|MemoryPressure|DiskPressure|PIDPressure" -A 5 kubectl top nodes如果节点状态为 NotReady,需要进一步查看 Kubelet 日志:
journalctl -u kubelet --since "30 minutes ago" --no-pager逐台进入服务器查看硬件和内核日志:
dmesg -T --level=err,warn | tail -100 nvidia-smi systemctl status docker df -hIO 挂起时,df或ls可能卡住,这本身就是一个重要证据。要把“命令卡住”记录下来,而不是直接杀进程。
2.3 采集存储状态和硬件日志
存储侧需要确认控制器切换事件、卷挂载状态和服务延迟:
mount | grep nfs nfsstat -m cat /etc/fstab cat /proc/mounts | grep nfs如果共享存储有独立管理界面,还要查看以下信息:
- 控制器之间的心跳日志;
- 存储卷的读写延迟曲线;
- 存储控制器的 CPU、缓存、磁盘状态;
- 是否有磁盘重构或控制器切换事件。
网络侧也要同步检查:
ping <控制面IP> ping <存储IP> ibstatus | grep -A 3 state网络检查的目的是区分“节点真的离线”和“网络不可达”。如果业务网络正常,但 RDMA 网络失败,训练任务同样会失败,节点状态却可能保持 Ready。
2.4 把现象分成三类:计算节点、控制节点、存储/网络
现场信息采集完成后,不要急于定位单一根因。先把证据分类:
| 现象类别 | 可能原因 | 需要确认的证据 |
|---|---|---|
| 节点 NotReady | Kubelet 探活失败、运行时异常、存储挂载卡住 | Kubelet 日志、容器运行时状态、mount 状态 |
| 任务大量失败 | 共享文件系统不可用、GPU 卡被标记异常、调度器驱逐 | 任务日志、事件、GPU 状态 |
| 控制面异常 | etcd 性能下降、控制器崩溃 | etcd 慢查询、API Server 超时日志 |
Astra 集群的证据归类后,计算节点本身没有硬件损坏,GPU 卡都正常,问题集中在“存储挂载超时导致 Kubelet 探活失败”这条线上。
3. 根因复盘:存储控制器切换引发了GPU健康检查假失败
3.1 存储控制器发生了什么
Astra 集群的共享数据集目录通过 NFS 挂载到所有计算节点。正常情况下,数据集目录由存储控制器 A 提供服务,控制器 B 处于备用状态。故障当天,控制器 A 的磁盘固件报出介质错误,存储系统根据预设策略自动切换到控制器 B。
控制器切换过程中,NFS 服务会中断一段时间。Astra 集群使用的 NFS 挂载参数并没有为中断场景做特殊优化,默认的timeo和retrans让客户端长期处于重试状态。所有计算节点上的训练进程在读写检查点文件时被阻塞,无法及时释放 IO 栈。
真正的问题出现在节点健康检查环节。Astra 集群为 GPU 节点配置了一个自定义健康检查脚本,脚本会在共享目录中写入一个临时文件,用来验证 GPU 和存储是否正常。当共享目录 IO 卡住时,脚本无法创建临时文件,直接返回异常状态。Kubelet 收到健康检查失败后,把节点标记为 NotReady,调度器随后开始驱逐节点上的训练任务。
3.2 故障扩散的完整链路
从时间线看,故障扩散过程可以分为五个阶段:
- 存储控制器 A 故障,触发切换到控制器 B;
- NFS 服务中断约 3 分钟,所有计算节点进入 IO 等待;
- 自定义健康检查脚本因无法写临时文件而失败;
- Kubelet 上报节点 NotReady,调度器标记节点不可用,驱逐训练任务;
- 被驱逐的任务在其他节点重新调度,但其他节点仍无法读写共享目录,任务再次失败并重试,形成重试风暴。
这个链路中,最危险的设计是把“GPU 健康检查”和“存储 IO 探活”耦合在同一个脚本里。健康检查原本应该回答“GPU 是否可用”,却因为存储抖动而得出“节点不可用”的结论。这就是典型的假阴性健康检查。
3.3 日志和证据
故障处置过程中,最有力的证据来自以下位置:
# Kubelet 日志片段 MountVolume.SetUp failed for volume "data-volume" : mount failed: exit status 32# 健康检查脚本日志片段 [2025-06-20 10:23:15] ERROR: cannot write temp file to /data/checkpoints/.healthcheck [2025-06-20 10:23:16] ERROR: healthcheck failed for node astra-gpu-07# 存储控制器切换日志(管理界面导出) Controller A: Disk firmware error on disk 5 Controller A: Starting failover to Controller B Controller B: Received failover request Controller B: VIP changed from 10.0.0.10 to 10.0.0.11三条日志合在一起,才能还原完整链路。单看 Kubelet 日志会认为是网络挂载问题,单看健康检查脚本会认为是节点问题。这也是故障复盘时最容易忽略的部分:必须把所有组件日志放在同一个时间轴上看。
3.4 为什么不是AI服务器硬件故障
判断 AI 服务器是否硬件故障,需要看几个关键指标:
- GPU 是否在
nvidia-smi中正常显示; - 服务器 CPU、内存、PCIe 链路是否有硬件报错;
- 是否多个节点同时出现相同错误;
- 硬件报错是否与共享存储事件时间上重合。
Astra 集群中,所有 GPU 在nvidia-smi下都正常,dmesg中也没有 PCIe AER 报错,多个节点的错误同时发生在存储控制器切换之后。因此可以排除“GPU 批量烧毁”和“服务器主板故障”这两类假设。真正的故障点是共享存储切换后引发的连锁反应。
4. 修复与恢复:按控制面、存储、计算节点顺序操作
4.1 第一步:隔离故障,禁止新任务调度
故障恢复最忌讳在主链路尚未稳定时,就让所有节点和任务同时回归。Astra 集群恢复操作的第一步,是把所有计算节点设置为不可调度,避免新任务进入后继续重试。
kubectl cordon astra-gpu-01 kubectl cordon astra-gpu-02 # 批量操作可以用循环 for node in $(kubectl get nodes -o name); do kubectl cordon "$node" done执行后要确认调度器不再把新任务调度到这些节点上。可以使用以下命令查看节点状态:
kubectl get nodes | grep SchedulingDisabled此时不需要马上驱逐已有任务,因为任务可能还在等待存储恢复。先让集群进入“静止状态”,是避免重试风暴的关键。
4.2 第二步:恢复存储控制器和挂载
确认存储控制器 B 正常后,先不要急着解封节点。要先手动验证共享目录是否可读写。
在控制节点上执行:
mkdir -p /mnt/check mount -t nfs 10.0.0.11:/data/checkpoints /mnt/check touch /mnt/check/test_$(date +%s) cat /mnt/check/test_$(date +%s) umount /mnt/check验证通过后,再对所有计算节点执行重新挂载。如果节点上的 NFS 挂载已经失效,可以先卸载再挂载:
umount -l /data mount /data这里使用umount -l是为了处理“目录处于 busy 状态”的情况。但要注意,强制卸载可能会让正在写的进程收到 IO 错误,因此一定要先确认训练任务已停止,或者已经处于失败状态。
如果所有节点都要重新挂载,可以准备一个脚本在集群中批量执行:
ansible gpu_nodes -m shell -a "umount -l /data; mount /data; df -h /data"4.3 第三步:清理异常状态,恢复健康检查
存储恢复后,Astra 集群的自定义健康检查脚本会恢复写临时文件的能力。但已经被 Kubelet 标记的错误状态不会自动消失,需要重启 Kubelet 或在节点上删除异常状态记录。
比较稳妥的方法是重启 Kubelet,而不是重启整台服务器:
systemctl restart kubelet重启后确认节点状态:
kubectl get nodes如果节点仍然没有恢复 Ready,查看 Kubelet 日志并确认健康检查脚本返回结果:
/usr/local/bin/gpu-healthcheck.sh echo $?在确认节点状态恢复 Ready 后,再逐步解除调度限制:
kubectl uncordon astra-gpu-01 # 建议每批 3 到 5 台,确认资源使用率稳定后再继续4.4 第四步:验证恢复效果
恢复操作完成不等于故障结束。Astra 集群需要做四层验证:
| 层级 | 验证命令 | 预期结果 |
|---|---|---|
| 节点 | kubectl get nodes | 所有节点 Ready 且 SchedulingEnabled |
| GPU | nvidia-smi | 每张卡状态正常,利用率开始变化 |
| 存储 | df -h /data; dd 测试 | 共享目录可读写,延迟恢复 |
| 任务 | 提交一个测试任务 | 任务进入 Running 且不失败 |
生产环境不建议用真实训练任务做恢复验证,可以准备一个轻量任务镜像,只做 GPU 初始化和共享目录写入:
kubectl apply -f test-gpu-job.yaml测试任务代码核心逻辑如下:
import os import torch print("GPU count:", torch.cuda.device_count()) path = "/data/checkpoints/health_" + str(os.getpid()) + ".txt" with open(path, "w") as f: f.write("ok") print("write ok")如果测试任务能完成并写入文件,说明计算、GPU、存储三条链路已经恢复。
5. 如何让Astra集群不再“集体瘫痪”:高可用和降级设计
5.1 控制面高可用:避免调度器再次全面误判
Astra 集群的控制面已经采用三节点部署,但仍然需要关注 etcd 的性能和调度器的故障容忍策略。控制面节点不仅要保证数量,还要保证磁盘和网络性能。etcd 的写入延迟过高,会导致所有控制器状态更新变慢,进而引发更长的故障恢复时间。
生产环境建议检查以下参数:
- etcd 数据目录使用本地 SSD,避免与业务存储共用;
- 控制节点之间网络延迟保持在较低水平;
- 对 etcd 设置独立的告警阈值,例如 fsync 延迟超过 100ms 就触发告警;
- API Server 和调度器设置合理的
--node-monitor-grace-period和--node-monitor-period。
学习环境可以单节点跑 Kubernetes,但生产环境必须至少部署 3 个控制节点。Astra 集群这次故障中,控制面没有成为瓶颈,但如果控制面也部署在同一套共享存储上,就会反过来影响 etcd 的写入,情况会更复杂。
5.2 存储面:超时、重试、本地盘降级
NFS 挂载参数是这次故障的放大器。Astra 集群在存储控制器切换时,没有配置合理的超时和重试策略,导致所有节点同时被 IO 阻塞。NFS 客户端的关键参数如下:
| 参数 | 含义 | 默认值 | Astra 集群建议 | 调大影响 |
|---|---|---|---|---|
| timeo | 等待响应的超时时间,单位 0.1 秒 | 600 | 1500 到 2000 | 容忍更长恢复时间,但单次阻塞更久 |
| retrans | 超时后重试次数 | 2 | 3 到 5 | 增加重试穿透率,减少失败返回 |
| soft vs hard | 超过重试次数后的行为 | hard | 视场景而定 | soft 可能返回 IO 错误,hard 可能永久阻塞 |
| actimeo | 文件和目录属性缓存时间 | 3 | 30 到 60 | 减少元数据请求,但缓存一致性变弱 |
在训练场景中,如果共享数据集以只读方式挂载,建议使用软挂载加超时限制:
/data 10.0.0.11:/data nfs4 rw,soft,timeo=1500,retrans=3,actimeo=60 0 0如果任务对数据完整性要求极高,则建议使用hard挂载,但必须在应用层加入超时控制,避免进程永远卡死。
此外,Astra 集群还应该在每个计算节点保留一定比例的本地 SSD 空间,用于存放临时日志和 checkpoints 缓存。共享存储故障时,训练进程可以先把检查点写入本地盘,等共享存储恢复后再同步,这样能避免健康检查脚本因为共享目录不可写而误判 GPU 异常。
5.3 GPU健康检查:分级、幂等、快速失败
健康检查脚本是整个故障扩散的关键节点。一个可靠的健康检查脚本需要满足以下要求:
- 不依赖外部共享存储判断 GPU 状态;
- 写临时文件时必须指定本地目录;
- 单次执行时间不能过长,超过 5 秒就应该主动超时;
- 多次执行结果应幂等,不能因为上一次残留文件而误判;
- 失败后需要输出明确原因,方便定位。
推荐将健康检查拆成两个阶段:
#!/bin/bash # /usr/local/bin/gpu-healthcheck.sh set -e # 第一阶段:GPU 状态检查 GPU_READY=$(nvidia-smi --query-gpu=index,count --format=csv,noheader | wc -l) if [ "$GPU_READY" -lt 1 ]; then echo "FAIL: no gpu detected" exit 1 fi # 第二阶段:本地临时文件检查 LOCAL_TMP="/var/tmp/gpu-health-$$" if ! echo "ok" > "$LOCAL_TMP"; then echo "FAIL: local tmp write failed" exit 1 fi rm -f "$LOCAL_TMP" echo "PASS" exit 0注意,临时文件必须写入本地/var/tmp,不要写到/data共享目录。健康检查脚本的职责是确认节点计算资源可用,共享目录是否可用应该由存储监控独立负责。两者耦合在一起,就会把存储故障错误地放大为计算节点故障。
5.4 调度侧:节点故障容忍、任务恢复策略、限流
调度器在节点被标记为 NotReady 后,默认会驱逐上面的 Pod。大批量驱逐会放大故障,因此需要配置合理的 PodDisruptionBudget 和任务重试策略。
Kubernetes 调度侧可以调整以下配置:
# scheduler-config.yaml 片段 apiVersion: kubescheduler.config.k8s.io/v1 kind: KubeSchedulerConfiguration profiles: - schedulerName: default-scheduler plugins: multiPoint: enabled: - name: NodeResourceFit - name: NodeAffinity这里不展开所有调度插件。更关键的是在任务清单中加入节点故障容忍:
spec: tolerations: - key: "node.kubernetes.io/not-ready" operator: "Exists" effect: "NoExecute" tolerationSeconds: 60这个配置表示允许任务在节点 NotReady 后最多容忍 60 秒,给调度器留出重新调度时间,但不会立即驱逐。任务重试还需要设置restartPolicy和backoffLimit,避免失败任务无限重试。
spec: restartPolicy: Never backoffLimit: 3对于训练任务,建议在应用层实现持久化的参数服务器或定期 checkpoints。任务失败后从最近一个 checkpoint 恢复,而不是从零开始。
6. 从故障中沉淀的检查清单和最佳实践
6.1 故障前:巡检检查清单
Astra 集群这类 AI 基础设施,可以按以下清单做月度巡检:
| 检查项 | 命令/操作 | 预期结果 |
|---|---|---|
| NFS 挂载参数 | cat /etc/fstab | 已配置超时和重试参数 |
| GPU 健康检查 | bash /usr/local/bin/gpu-healthcheck.sh | 返回 PASS 且不依赖共享目录 |
| 节点状态 | kubectl get nodes | 全部 Ready |
| etcd 性能 | etcd metrics 查看 fsync 延迟 | 延迟稳定 |
| 存储控制器 | 管理界面查看主备状态 | 主备正常,无切换告警 |
| 任务恢复依赖 | 训练任务是否配置 checkpoints | 每个任务都有关键步骤保存点 |
学习环境可以简化到只检查 GPU 和节点状态。生产环境必须把这些检查项纳入自动巡检系统,不能依赖人工在故障后补查。
6.2 故障中:快速取证清单
故障发生时,最容易犯的错误是急着重启。正确顺序是:
- 先确认监控告警、存储事件、节点事件三个数据源的时间戳;
- 执行
kubectl get nodes -o wide,记录当前节点状态; - 逐台采集
dmesg、nvidia-smi、journalctl -u kubelet日志; - 查看共享存储控制器切换事件;
- 检查 NFS 挂载状态和健康检查脚本日志;
- 先隔离调度,再恢复存储,最后恢复计算节点;
- 复盘时把日志按时间线排列,确认故障扩散路径。
如果现场日志已经丢失,后续复盘会非常被动。建议集群层面的日志采集始终开启,至少保留 7 天。
6.3 故障后:复盘检查清单
故障恢复后的复盘会,建议按以下结构输出:
| 阶段 | 核心问题 | Astra 集群的结论 |
|---|---|---|
| 现象 | 用户看到了什么 | 多个节点 NotReady,任务全失败 |
| 根因 | 触发因素是什么 | 存储控制器切换 |
| 放大 | 为什么故障迅速扩散 | 健康检查脚本依赖共享目录 |
| 修复 | 如何恢复的 | 隔离节点、恢复存储、重建状态 |
| 预防 | 如何避免再发生 | 拆分健康检查、调整挂载参数、增加本地盘降级 |
复盘会议要产出可执行动作,不要只写“加强监控”“提升稳定性”这类空话。每个预防措施都要指定负责人和完成时间。
6.4 三个最容易忽略的坑
第一个坑:健康检查脚本把存储探活写进 GPU 探活逻辑里。Astra 集群的教训证明,共享目录抖动会让 GPU 状态被误判,最终触发节点集体下线。
第二个坑:NFS 挂载使用默认参数。默认timeo和retrans对普通 Web 服务够用,但对高负载训练任务不够弹性。控制器切换期间,默认参数可能让所有节点同步阻塞。
第三个坑:故障恢复时过早解封节点。如果在共享存储没有验证完成之前就把节点设为可调度,新任务会继续写失败,故障时间会被拉长。恢复操作应该一批一批验证,而不是一次性放开所有节点。
收尾:把故障复盘变成架构能力
Astra 集群这次“集体瘫痪”并不是 AI 服务器硬件的末日,而是一次架构脆弱点的集中暴露。真正应该保留的不是“天命将至”的焦虑,而是一套能快速取证、能判断故障扩散链路、能通过参数和脚本设计消除假阴性健康检查的运维能力。
下一步可以做的扩展方向有三个:一是把训练任务全部改为支持 checkpoint 断点续训,从任务侧降低存储故障带来的影响;二是为共享存储搭建独立的健康探测和告警机制,让存储延迟和节点状态解耦;三是把巡检清单、恢复流程和复盘模板脚本化,形成平台自带的故障演练能力。把这几点落地后,AI 服务器集群的稳定性就会从“靠运气”变成“靠设计”。