HPC 容错与检查点:Checkpoint/Restart、ULFM 与恢复

解析大规模系统故障特征与容错架构,深入 Checkpoint/Restart 的周期/增量/分层策略、SCR/DMTCP 检查点库、ULFM 消息传递容错与异步复制恢复的生产实践。

百万节点级超算的残酷现实是:故障不是例外,而是常态。以单节点每年 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_TYPEBURST / NODE_LOCAL暂存介质类型
SCR_COPY_TYPEFILE / XOR向 PFS 复制或纠删码
SCR_COPY2冗余副本数
SCR_FLAVORGLOBAL / 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:

  1. SLURM 侧:作业头加 #SBATCH --requeue;SlurmctldParameters=... 配置节点健康检查自动排障。
  2. 检查点频率动态化:按「当前作业已跑时长」动态调整间隔——早期稀疏、后期加密(故障概率随时间上升)。
  3. 检查点验证:定期做「恢复演练」——从最新检查点恢复并在校验数据集上比对结果,确保检查点本身没坏。
  4. 故障感知的调度:把检查点文件放在 Lustre,同时保留节点本地副本;scontrol show node 监控热点节点,主动迁移作业。
  5. 记录与复盘:sacct + 系统日志留痕,月度复盘故障模式,调整健康策略与退役名单。

一句话:生产容错是分层防御:调度层兜底轻作业、应用层断点续跑重作业、数据层保结果不丢,每层只解决自己那一层能解决的问题。

总结

层次手段代表工具损失控制
故障检测健康检查、看门狗SLURM、Nagios 监控尽早发现
状态保存周期/增量/分层检查点SCR、DMTCP、BLCR重算量→间隔/2
通信容错ULFM 收缩Open MPI ULFM部分故障局部恢复
恢复加速异步复制、流水线SCR 副本、rsync恢复不阻塞计算
体系防御分层容错策略调度 + 应用 + 数据总开销 5-10%

容错是让 HPC 规模得以扩展的幕后工程。理解故障统计、检查点类型与 ULFM 的收缩模型,你就能为任何规模的作业设计出「坏了能快速回来」的体系。与 Slurm 集群调度 的 --requeue、Lustre 并行文件系统 的持久层配合,构成超算中心完整的韧性底座。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「hpc」更多文章

  1. 科学工作流引擎:Nextflow/Snakemake 与管线编排
  2. HPC 集群管理与软件栈运维:模块、Spack 与监控
  3. HPC 性能剖析与调优工具链:perf/gprof/VTune/Nsight