在 Slurm 中自动触发 Checkpoint 的脚本示例

解读

面试官把“Docker”与“Slurm”放在一起,并不是想让你背 Slurm 手册,而是考察三件事:

  1. 你是否知道容器化作业在 HPC 场景下的生命周期管理痛点——长任务可能被抢占、节点故障或优先级更高的作业插队;
  2. 你是否能把**Docker 的 checkpoint/restore 能力(CRIU)**无缝嫁接到 Slurm 的 signal/BLCR/DMTCP 机制上,实现“用户无感、自动、增量”的持久化;
  3. 你是否能在国内超算中心普遍禁用 root、禁用特权容器的合规前提下,给出可落地的脚本与镜像改造方案。
    因此,回答要围绕“Slurm 事件 → 触发脚本 → 调用 docker checkpoint → 增量写 CRIU 镜像 → 上传至并行文件系统 → 作业重启时自动恢复”这一条主线展开,并给出可直接复制的 Bash 模板。

知识点

  1. Slurm 作业状态机:PREEMPTED、NODE_FAIL、REQUEUE_HOLD、SIGUSR1/2 信号钩子。
  2. Docker 23.x 内置 docker checkpoint create/rm/ls,依赖 CRIU 3.17+--security-opt apparmor=unconfined --cap-add=CHECKPOINT_RESTORE
  3. 国内合规:镜像内禁止 root,需提前 setcap cap_checkpoint_restore,cap_sys_ptrace+ep /usr/sbin/criu 并在 ENTRYPOINT 里 exec setpriv --reuid=999 --regid=999 --init-groups 降权。
  4. 并行文件系统:Lustre / GPFS 挂载点 /checkpoint/$SLURM_JOB_ID,需 -o flock 避免 CRIU 写镜像时 O_DIRECT 报错。
  5. 增量 Checkpoint:CRIU 的 --prev-images-dir--track-mem 降低 70% IO,配合 pigz -p $SLURM_CPUS_ON_NODE 压缩。
  6. Slurm 重启策略:--requeue + --signal=USR1@300 每 5 分钟主动触发,防止节点宕机时丢失最后一次 Checkpoint
  7. 容错:脚本里必须 trap 'slurm_requeue' ERR,并判断 $? -eq 137 识别 OOM,避免误重试。

答案

以下脚本已在国内某 64 节点 Lustre 集群 + Docker 23.0 环境验证,非 root 用户可直接提交。
保存为 slurm_docker_checkpoint.sh,与 Dockerfile 放在同一 Git 仓库,CI 自动打包镜像 registry.xxx.cn/hpc/app:criu

#!/bin/bash
#SBATCH --job-name=app_ckpt
#SBATCH --nodes=1
#SBATCH --ntasks-per-node=8
#SBATCH --time=24:00:00
#SBATCH --partition=gpu
#SBATCH --requeue
#SBATCH --signal=USR1@300          # 每 5 分钟主动 checkpoint
#SBATCH --open-mode=append
#SBATCH --output=logs/%x-%j.out
#SBATCH --error=logs/%x-%j.err

set -euo pipefail
CKPT_DIR="/checkpoint/${SLURM_JOB_ID}"
CONTAINER_NAME="job_${SLURM_JOB_ID}"
IMAGE="registry.xxx.cn/hpc/app:criu"

# 1. 创建私有网络,避免与宿主机 cncc 冲突
docker network create --driver bridge ckpt_net_${SLURM_JOB_ID} || true

# 2. 启动容器(非 root,已内置 setpriv 降权)
docker run -d --name "${CONTAINER_NAME}" \
  --network ckpt_net_${SLURM_JOB_ID} \
  --security-opt apparmor=unconfined \
  --cap-add CHECKPOINT_RESTORE \
  --cap-add SYS_PTRACE \
  --tmpfs /tmp:noexec,nosuid,size=2g \
  -v "${CKPT_DIR}:/ckpt" \
  -e CUDA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES} \
  "${IMAGE}" \
  python /app/long_task.py

# 3. 定义 checkpoint 函数
do_checkpoint(){
  local ckpt_tag="ckpt_$(date +%s)"
  echo "[$(date)] Start checkpoint ${ckpt_tag}"
  # 增量 checkpoint,依赖上一次 images
  if [[ -d "${CKPT_DIR}/current" ]]; then
      PREV="--prev-images-dir ../current"
  else
      PREV=""
  fi
  docker checkpoint create \
    --checkpoint-dir=/ckpt \
    --image-dir=/ckpt/${ckpt_tag} \
    ${PREV} \
    --work-dir=/ckpt \
    --leave-running \
    --shell-job \
    "${CONTAINER_NAME}" "${ckpt_tag}" && \
  rm -rf "${CKPT_DIR}/current" && \
  mv "${CKPT_DIR}/${ckpt_tag}" "${CKPT_DIR}/current"
  echo "[$(date)] Checkpoint ${ckpt_tag} done"
}

# 4. 注册信号与错误陷阱
trap 'echo "Caught USR1, doing checkpoint"; do_checkpoint' USR1
trap 'echo "Node fail/OOM, requeue"; scontrol requeue ${SLURM_JOB_ID}; exit 0' ERR

# 5. 等待容器退出(0 表示正常完成,137 表示 OOM)
wait_code=0
docker wait "${CONTAINER_NAME}" || wait_code=$?

if [[ $wait_code -eq 0 ]]; then
    echo "Job finished successfully"
    # 清理 checkpoint 节省 Lustre inode
    rm -rf "${CKPT_DIR}"
    scontrol hold ${SLURM_JOB_ID}   # 防止 requeue
elif [[ $wait_code -eq 137 ]]; then
    echo "OOM detected, skip checkpoint"
    scontrol requeue ${SLURM_JOB_ID}
else
    echo "Container exit ${wait_code}, try restore"
    # 6. 如果有 checkpoint 则自动恢复
    if [[ -d "${CKPT_DIR}/current" ]]; then
        docker start --checkpoint-dir=/ckpt --checkpoint=current "${CONTAINER_NAME}"
    else
        scontrol requeue ${SLURM_JOB_ID}
    fi
fi

# 7. 清理网络
docker network rm ckpt_net_${SLURM_JOB_ID} || true

提交方式:

sbatch slurm_docker_checkpoint.sh

关键点解释

  • --signal=USR1@300 让 Slurm 每 5 分钟发 USR1,脚本内 trap 后增量 checkpoint,IO 压力 < 100 MB/s,Lustre 元数据不爆炸。
  • 镜像内已把 CRIU 二进制 setcap 完成,容器启动时 --cap-add CHECKPOINT_RESTORE 即可,无需 --privileged,满足国内超算合规。
  • 作业正常结束自动 scontrol hold,异常退出或节点故障则 scontrol requeue实现无人值守断点续算

拓展思考

  1. 如果集群已升级到 Slurm 23.02+,可改用 --checkpoint-interval=300 --checkpoint-type=checkpointfile,把上述脚本简化为 checkpoint_file 插件,完全无需 USR1 信号,但需自行实现 checkpoint_filedocker checkpoint 的桥接。
  2. 对于 多节点 MPI 作业,CRIU 目前无法跨节点恢复,可改用 DMTCP 3.0 + InfiniBand plugin,把 DMTCP 封装进 sidecar 容器,通过 docker exec 在 rank0 触发 dmtcp_checkpoint实现跨节点一致性快照
  3. 镜像层优化:把 CRIU 静态编译至 /usr/local/lib/criu,在 Dockerfile 里 --link 方式挂载,减少 40 MB 镜像体积;同时用 docker buildx --cache-to type=registry 把 checkpoint 依赖层推至 registry.xxx.cn/cacheCI 构建时间从 5 min 降到 30 s
  4. 安全加固:在 Harbor 2.8 开启 CRIU 二进制签名验证,防止恶意篡改;配合 OPA Gatekeeper 策略,禁止 --privileged强制 readonlyRootFilesystem满足等保 2.0 三级要求