百万节点级超算的残酷现实是:故障不是例外,而是常态。以单节点每年 1 次的故障率推算,一万节点集群平均每 52 分钟就有节点宕机——而很多科学作业要连续运行数周。如果每次故障都从零重算,大规模计算将寸步难行。容错(Fault Tolerance)由此成为 HPC 的核心基础设施:它保证「坏了能恢复」,并且把恢复成本控制到最小。本文从故障模型出发,覆盖 Checkpoint/Restart、ULFM 与生产容错策略。
1. 大规模系统故障特征
理解故障的统计规律,才能设计正确的容错策略:
| 故障特征 | 典型表现 | 对策略的影响 |
|---|---|---|
| MTTF(平均无故障时间) | 万节点集群分钟级 | 必须频繁检查点 |
| Fail-stop(停摆型) | 节点宕机、进程消失 | 检测 + 重启即可,无需处理脏状态 |
| 非 fail-stop(拜占庭式) | 数据静默损坏、错误结果 | 需要校验与冗余计算 |
| 偏置故障 | 故障常集中在特定节点/时段 | 健康管理、退役热点 |
| 升级式故障 | 散热→宕机→起火 | 分级响应、自动迁移 |
Fail-stop 是最主流的假设:计算节点崩溃时,OS 与 MPI 层能检测到进程消失,节点上的内存状态全部丢失(除非做了检查点),但磁盘上的共享文件系统(Lustre)不丢。容错系统因此分两步:保存可恢复状态(检查点)+ 检测故障并重组(恢复)。
故障生命周期:
[正常运行] ──周期检查点──> [节点宕机] ──检测──> [重启恢复] ──加载检查点──> [继续运行]
↑ │
└────────────────────── 损失 = 检查点间隔的一半平均 ──────────────────┘
一句话:HPC 容错的基本盘是「周期性保存 + 快速检测 + 从检查点重启」,性能优化集中在让这三步尽量便宜。
2. 检查点类型:周期 / 增量 / 分层
检查点(Checkpoint)按保存策略分三类,成本和粒度各不相同:
| 类型 | 保存内容 | 开销 | 恢复粒度 | 适用场景 |
|---|---|---|---|---|
| 周期性全量 | 每 N 时间全内存状态 | 大(写全量) | 粗(回到最近周期) | 通用、实现简单 |
| 增量 | 仅保存变化页(diff) | 小(数据量小) | 细(任意时刻) | 状态变化慢的长跑 |
| 分层(多级) | 本地盘 + 远端两级 | 中 | 分级(近的快、远的稳) | 大规模、大状态 |
增量检查点的核心是追踪脏页:
# 依赖页表 dirty-bit 的增量检查点(类 CRIU 思路)
# 首轮全量保存,之后每轮只保存被写过的页
criu dump --tcp-established --track-mem
criu dump --track-mem # 后续轮次仅输出增量镜像
criu restore --restore-detached
分层检查点把「恢复速度」与「持久安全」解耦——快层放本地 NVMe(秒级恢复),慢层放并行文件系统(跨故障域安全):
分层检查点(Multi-level):
应用进程 ──> L1: 本地 NVMe(每 10 min,秒级写)────┐
│ │ 后台异步推进
L2: Lustre/PFS(每 60 min,慢但稳)─┴─> 故障时优先从 L1 恢复
L1 丢失再回退 L2
检查点频率的最优解是著名的 Young 公式:最佳间隔 ≈ √(2 × 检查点开销 × 故障间隔)。开销 10 分钟、故障间隔 1 小时的场景,最优约每 34 分钟一次。频率过高浪费写带宽,过低则故障损失放大。
Young 公式算例:
检查点耗时 C = 600s,MTTF = 3600s
最佳间隔 T* = √(2 × C × MTTF) = √(2×600×3600) = 2078s ≈ 34.6 min
总损失 L = C/T + T/(2·MTTF) = 600/2078 + 2078/(2×3600)
= 0.289 + 0.289 = 0.58 → 占作业时间约 58% 的“相对损失”
(实际取整到 30min 间隔,损失差异可忽略;这是“近似最优”的关键)
一个完整的周期检查点循环(应用内实现,适合中等规模):
// checkpoint_loop.c —— 周期检查点 + 崩溃恢复的骨架
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
static volatile sig_atomic_t checkpoint_flag = 0;
void on_alarm(int s) { checkpoint_flag = 1; }
void save_state(const char* path, const void* state, size_t bytes) {
FILE* f = fopen(path, "wb");
fwrite(state, 1, bytes, f);
fclose(f); // 落盘
// 生产级还会先写临时文件再 rename,避免半写状态
}
int main(int argc, char** argv) {
double* state = malloc(sizeof(double) * 1024 * 1024);
const char* ckpt = "sim.ckpt";
// 崩溃后从检查点恢复:main(argc, argv, restart=1)
if (argc > 1 && !strcmp(argv[1], "--restart")) {
FILE* f = fopen(ckpt, "rb");
fread(state, 1, sizeof(double) * 1024 * 1024, f);
fclose(f);
printf("[restart] loaded from %s\n", ckpt);
}
// 定时触发检查点(这里用 SIGALRM 演示)
signal(SIGALRM, on_alarm);
alarm(300); // 每 300s
unsigned long step = 0;
while (1) {
if (checkpoint_flag) {
save_state(ckpt, state, sizeof(double) * 1024 * 1024);
printf("[ckpt] step %lu saved\n", step);
checkpoint_flag = 0;
alarm(300);
}
// 模拟一个计算步
compute_step(state);
step++;
}
}
配合调度层:这个程序只需在 SLURM 作业中声明 --requeue,崩溃后调度器自动重启并以 --restart 参数拉起,即可完成「透明恢复」。
3. 检查点库:SCR、DMTCP 与 BLCR
成熟的检查点库让应用不必自己实现保存/恢复。按「应用感知」程度可分为三类:
SCR(Scalable Checkpoint/Restart,LLNL)——面向 MPI 应用的分层检查点库,自动管理多级缓存与冗余副本:
# 构建并链接 SCR(作为 MPI 库注入,无需改应用代码)
module load scr
export SCR_CACHE_BASE=/scratch # 本地暂存层
export SCR_CACHE_TYPE=BURST # 或 NODE_LOCAL(本节点盘)
export SCR_FLAVOR=GLOBAL # 或 NODE_LOCAL
export SCR_PREFIX=/shared/chk # 最终持久化位置
export SCR_COPY_TYPE=FILE # 向 PFS 异步复制
# 应用只需调用 MPI_Init + 每步调 SCR_Checkpoint()
mpirun -np 256 ./sim
SCR 的价值:它在节点本地盘暂存检查点,多个副本间自动冗余(SCR_COPY=2 以上),再异步刷向 PFS——把「每节点写 2GB 到共享盘」的昂贵路径变成「写本地 + 后台聚合复制」。
SCR 的关键配置参数:
| 参数 | 取值示例 | 作用 |
|---|---|---|
SCR_CACHE_BASE | /scratch | 本地暂存根目录 |
SCR_CACHE_TYPE | BURST / NODE_LOCAL | 暂存介质类型 |
SCR_COPY_TYPE | FILE / XOR | 向 PFS 复制或纠删码 |
SCR_COPY | 2 | 冗余副本数 |
SCR_FLAVOR | GLOBAL / NODE_LOCAL | 检查点分布模式 |
SCR_PREFIX | /shared/chk | 持久检查点目录 |
SCR_COPY_TYPE=XOR 用异或纠删码取代整副本,以较少存储提供冗余——适合「副本太多存不下、但又要抗节点故障」的场景。LLNL 的 VeloC(后继项目)进一步把压缩、增量、异构存储统一进同一框架。
DMTCP(Distributed MultiThreaded CheckPointing)——透明用户态检查点,无需改代码,对整个进程树做检查点:
dmtcp_checkpoint --checkpoint=300 ./app # 每 300 秒自动检查点
# 运行中手动触发:向进程发送 SIGUSR2
dmtcp_restart ckpt_app_*.dmtcp # 重启恢复到检查点时刻
DMTCP 通过 LD_PRELOAD 拦截系统调用,保存 CPU 寄存器、内存、打开文件状态。优点是零侵入;缺点是体积大、对大状态应用开销高、MPI 场景需配合专用插件。
**BLCR(Berkeley Lab Checkpoint/Restart)**是历史最悠久的 Linux 内核级检查点(基于内核模块),单进程/进程组粒度,配合 SLURM 的 --requeue 使用。因内核版本维护成本,现在多数集群用用户态方案(DMTCP/SCR)或应用自带检查点取代。
4. ULFM:面向故障的 MPI 扩展
传统 MPI 的容错模型是「一个进程死了,全体自杀」。ULFM(User-Level Failure Mitigation)改变这一范式:允许 MPI 作业在部分进程失败后收缩(shrink)存活进程集合并继续运行。ULFM 已在 Open MPI 中实验性实现(--with-ulfm)。
核心 API:
| API | 作用 |
|---|---|
MPI_Comm_revoke(comm) | 宣告当前通信上下文失效,终止一切未完成通信 |
MPI_Comm_shrink(comm, &newcomm) | 丢弃失败进程,返回仅含存活进程的新通信子 |
MPI_Comm_agree(comm, &flag) | 所有存活进程达成一致(哪些进程失败) |
MPI_Comm_failure_get_acked(comm, &grp) | 查询已确认失败进程组 |
应用侧最小容错循环:
// ULFM 容错主循环(伪代码)
while (running) {
rc = MPI_Allreduce(..., comm);
if (rc != MPI_SUCCESS) {
// 1. 停止一切未决通信
MPI_Comm_revoke(comm);
// 2. 确定失败进程集合
MPI_Comm_failure_ack(comm);
MPI_Comm_failure_get_acked(comm, &failed);
// 3. 收缩:移除失败进程,重建通信子
MPI_Comm_shrink(comm, &newcomm);
MPI_Comm_agree(newcomm, &flag);
// 4. 从最近检查点恢复计算状态,在 newcomm 上继续
load_checkpoint("latest.ckpt");
comm = newcomm;
}
// 正常计算 + 定期 checkpoint
}
一个可编译的 ULFM 收缩循环(Open MPI + ULFM 补丁):
// ulfm_shrink.c —— 失败后收缩通信子继续运行
#include <mpi.h>
#include <stdio.h>
int try_allreduce(MPI_Comm comm, double* buf, int n) {
return MPI_Allreduce(MPI_IN_PLACE, buf, n, MPI_DOUBLE, MPI_SUM, comm);
}
int main(int argc, char** argv) {
MPI_Init(&argc, &argv);
MPI_Comm comm = MPI_COMM_WORLD;
int rank, size, step = 0;
MPI_Comm_rank(comm, &rank);
MPI_Comm_size(comm, &size);
double buf[1024] = {0};
while (step < 1000) {
int rc = try_allreduce(comm, buf, 1024);
if (rc != MPI_SUCCESS) {
// 1. 集体进入失败恢复模式
MPI_Comm_revoke(comm);
// 2. 确认失败并获取失败集合
MPI_Comm_failure_ack(comm);
MPI_Group failed;
MPI_Comm_failure_get_acked(comm, &failed);
// 3. 收缩:newcomm 只含存活 rank
MPI_Comm_shrink(comm, &newcomm);
MPI_Comm_agree(newcomm, &flag);
// 4. 存活 rank 从最近检查点恢复计算状态
restore_checkpoint(); // 应用实现
comm = newcomm;
printf("[rank %d] shrunk, continuing at step %d\n", rank, step);
}
compute_step(buf, step++);
}
MPI_Comm_free(&comm);
MPI_Finalize();
return 0;
}
开发注意:ULFM 是实验性特性,Open MPI 需 --with-ulfm 编译;收缩后的通信子在拓扑上「紧凑化」,应用要准备好在更小的规模上继续(数据重分布逻辑)。ULFM 适合与「应用内检查点 + 冗余数据副本」配合,形成「少节点、快恢复」的组合拳。
ULFM 的核心收益:不重启整个作业。在 1024 rank 中挂 8 个 rank 的场景下,传统方案全队重启损失 30 分钟,ULFM 收缩后只重算失败分区,损失降到检查点间隔量级。代价是应用必须把「怎么收缩」的逻辑写进主循环——这比透明检查点更复杂。
一句话:ULFM 把 MPI 从「全体故障即全体失败」升级为「部分故障局部恢复」,用应用内联的收缩逻辑换取大规模长作业的存活率。
5. 异步复制与恢复
恢复(Restart)本身也可能成为瓶颈:单节点 2GB 检查点 × 千节点 = 2TB 回放,瞬间压垮 PFS。异步复制与流水线恢复是控制恢复成本的关键。
异步复制:检查点先在本地生成,再由后台线程/独立进程向持久层复制,避免应用写检查点时停顿(checkpoint 开销趋近于零)。SCR 的 SCR_COPY_TYPE=FILE 正是这一机制;应用层也可显式双写:
# 用 rsync 在检查点生成后异步推送(配合 SLURM epilog)
cp state.ckpt /scratch/local/ # 快层:本地
rsync -a state.ckpt /shared/pfs/ # 慢层:异步刷 PFS(可后台)
流水线恢复:恢复时不是「全部进程同步加载再开算」,而是让每个 rank 一恢复完数据就立刻开始计算——早期完成的 rank 边算边等,把恢复等待隐藏在计算中。
双写与版本控制:检查点文件采用「写临时 → fsync → rename」的原子替换,并保留最近 N 个版本(sim.ckpt.1、sim.ckpt.2…),防止恢复时读到半写状态。文件系统层的技巧包括:
# 原子写检查点(避免恢复时读到截断文件)
cp state.tmp state.ckpt.new && mv state.ckpt.new state.ckpt
# 保留双版本轮换,崩溃现场始终有可回退的上一份
for v in 2 1; do mv sim.ckpt.$v sim.ckpt.$((v+1)); done
mv sim.ckpt.new sim.ckpt.1
恢复正确性验证:恢复后并不立即信任状态,生产系统通常在重启作业的 head 段对「检查点摘要」做校验(CRC/校验和),不一致则自动回退上一版本检查点并告警。这属于非 fail-stop 故障(静默损坏)的最后防线。
**故障注入测试(Fault Injection)**是检验容错系统的唯一可靠方法:
#!/bin/bash
# 在 SLURM 作业中随机 kill 一个 rank,验证 ULFM/检查点恢复路径
# 依赖 SLURM --requeue 自动重启被 kill 的作业
#SBATCH --requeue
#SBATCH --time=12:00:00
srun --kill-on-bad-exit=0 ./sim --restart-from-last-checkpoint
# 配合混沌测试脚本:随机挑节点执行
# scontrol terminate <jobid> 或直接 ssh 节点 kill 进程
一句话:恢复成本与检查点成本同样关键——异步复制让保存不阻塞计算,流水线恢复让回放不阻塞计算。
6. 容错与性能权衡
容错不是免费的:检查点消耗写带宽、占用存储、拉长作业时间。量化这笔账:
| 开销来源 | 量级估算 | 缓解手段 |
|---|---|---|
| 检查点写带宽 | 全量状态 / 间隔 | 增量、压缩、分层 |
| 存储占用 | 状态 × 副本数 × 周期数 | 副本复用、删除旧检查点 |
| 恢复时间 | 状态量 / 恢复带宽 | 本地层恢复、流水线 |
| 计算损失 | 间隔 / 2 × 节点故障率 | 缩短间隔、ULFM 局部恢复 |
经验数值:合理设计的容错系统应把容错总开销压到作业时间的 5-10%。超过 20% 说明检查点太频繁或路径太贵;低于 1% 往往是检查点太少,故障损失反而更大。
开销工程分解(千节点、每节点 4GB 状态、间隔 30min):
每次检查点写 4TB → 本地 NVMe 层 <1s/节点(并行写),异步刷 PFS
容错总开销 ≈ 5% 作业时间 + 存储 <1TB(循环覆盖)
故障损失 ≈ 平均 15min 重算(半个间隔)
合计可控
权衡决策表:
| 场景 | 推荐配置 | 理由 |
|---|---|---|
| 短作业(<1h) | 不检查点,--requeue 重跑 | 容错开销 > 重跑成本 |
| 中作业(小时级) | 周期全量 + SLURM requeue | 简单可靠 |
| 长作业(天级) | 分层检查点 + 增量 + 异步复制 | 控制写带宽与损失 |
| 超大规模(万核+) | ULFM + 应用内检查点 | 部分恢复,避免全队重启 |
7. 生产容错策略:从单点到体系
成熟超算的容错不是单个工具,而是分层的防御体系:
L0 硬件层 ECC 内存、冗余电源、RAID/纠删码 → 静默损坏最小化
L1 系统层 看门狗、健康检查、节点退役 → 坏节点及时下线
L2 调度层 SLURM --requeue、作业重启策略 → 无状态/轻状态作业自动恢复
L3 应用层 Checkpoint/Restart、ULFM 收缩 → 有状态作业断点续跑
L4 数据层 副本、纠删码、版本控制 → 输入输出数据不丢
生产 Checklist:
- SLURM 侧:作业头加
#SBATCH --requeue;SlurmctldParameters=...配置节点健康检查自动排障。 - 检查点频率动态化:按「当前作业已跑时长」动态调整间隔——早期稀疏、后期加密(故障概率随时间上升)。
- 检查点验证:定期做「恢复演练」——从最新检查点恢复并在校验数据集上比对结果,确保检查点本身没坏。
- 故障感知的调度:把检查点文件放在 Lustre,同时保留节点本地副本;
scontrol show node监控热点节点,主动迁移作业。 - 记录与复盘:
sacct+ 系统日志留痕,月度复盘故障模式,调整健康策略与退役名单。
一句话:生产容错是分层防御:调度层兜底轻作业、应用层断点续跑重作业、数据层保结果不丢,每层只解决自己那一层能解决的问题。
总结
| 层次 | 手段 | 代表工具 | 损失控制 |
|---|---|---|---|
| 故障检测 | 健康检查、看门狗 | SLURM、Nagios 监控 | 尽早发现 |
| 状态保存 | 周期/增量/分层检查点 | SCR、DMTCP、BLCR | 重算量→间隔/2 |
| 通信容错 | ULFM 收缩 | Open MPI ULFM | 部分故障局部恢复 |
| 恢复加速 | 异步复制、流水线 | SCR 副本、rsync | 恢复不阻塞计算 |
| 体系防御 | 分层容错策略 | 调度 + 应用 + 数据 | 总开销 5-10% |
容错是让 HPC 规模得以扩展的幕后工程。理解故障统计、检查点类型与 ULFM 的收缩模型,你就能为任何规模的作业设计出「坏了能快速回来」的体系。与 Slurm 集群调度 的 --requeue、Lustre 并行文件系统 的持久层配合,构成超算中心完整的韧性底座。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。