编程 docker checkpoint 之后 restore,计数器从 2 接着数:CRIU 存下了哪些状态,哪些它存不了

2026-09-19 00:04:07

docker checkpoint 之后 restore,计数器从 2 接着数:CRIU 存下了哪些状态,哪些它存不了

项目信息

  • 官网:
  • GitHub:
  • Go binding / 在线迁移封装:
  • 版本:criu-4.2.1 "CRIUTIBILITY"(2026-07-21),Git tag v4.2.1
  • 许可证:GPLv2(lib/ 目录下为 LGPLv2.1),images/ 目录为 MIT(Expat)
  • 来源项目:起于 Virtuozzo,后由 OpenVZ、LXC/LXD/Incus、Docker、Podman、Kubernetes 集成,发行版打包
  • 参考:Red Hat RHEL9 第 22 章「创建并恢复容器检查点」、阿里云 CRIU 用户指南、博客园 Docker 热迁移实测、EPJ Web Conf 2024 论文

先给结论

跑一个每请求自增的计数器服务,podman container checkpoint 打点,重启机器后 podman container restore --keep,再 curl 拿到的是 2、3、4,不是从 0 重新开始。PID、文件描述符及其偏移、内存页映射地址、TCP 连接状态都在。

但它不是万能的。下面这些情况打不了或恢复不了:

  • GPU / 加速器类设备资源,CRIU 不支持,只能靠 plugin 机制扩展;
  • 容器不能给自己打检查点,必须通过 docker/podman 自己的接口,apptainer/singularity 没有这个接口,K8s 里直接调 CRIU 同样失败;
  • checkpoint 与 restore 两端 CPU family/type 不一致,跨型号恢复会挂;
  • 源和目标上被用到的库必须完全一致,否则恢复后的二进制会崩;
  • 内核不支持 AArch64 上的 pre-copy 检查点。

CRIU 是什么,三种用法

CRIU(Checkpoint and Restore in Userspace,念 "kree-oo")做的事:冻结正在运行的进程(或进程树、容器),把状态写成一堆磁盘镜像,之后据此恢复并从中断处继续执行。主要实现在用户空间,通过已有的内核接口完成,需要时才扩展或新增接口。

用法入口有三种:CLI、RPC、C API。

几个独立组件值得单独记一下:

  • P.Haul:在线迁移,Go 库封装,CRIU 的 Go binding 放在 go-criu 仓库;
  • libcompel:parasite code injection,让目标进程执行一段取信息的代码而不杀掉应用;
  • libsoccr:TCP socket 检查点恢复,可以不断开连接保存/恢复 TCP socket 状态。

wiki 侧的相关页面:Installation、Usage、FAQ & When C/R fails、What can change after C/R、What cannot be checkpointed、Images 格式、Plugins、ZDTM 测试套件。

进程级与容器级两条路径

手动做进程级保存/恢复:

criu dump
criu restore --shell-job --images-dir

结合 docker 做容器级:

docker checkpoint create
docker start --checkpoint

容器检查点镜像默认落在 /var/lib/docker/containers//checkpoints//。跨主机迁移就是把 /var/lib/docker//checkpoints/ 下的同名文件夹搬过去。

两个能跑通的例子

Red Hat RHEL9 文档里的计数器服务:counter.pyhttp.server,一个全局 counter,do_GET 返回当前值再自增,监听 8088;Containerfile 基于 ubi9COPY counter.pyuseradd counterdnf install python3USER counterENTRYPOINT

podman build . --tag counter
podman run --name criu-test --detach counter
podman ps
podman inspect criu-test --format "{{.NetworkSettings.IPAddress}}"   # 10.88.0.247
curl 10.88.0.247:8088        # 返回 0、1
podman container checkpoint criu-test
# 重启系统后
podman container restore --keep criu-test
curl 10.88.0.247:8088        # 返回 2、3、4

博客园那篇 Docker 热迁移实测用的是官方 looper 例子,环境为 Docker 17.06.0-ce、criu 3.12、kernel 3.10.0-957.el7 / 5.10.2、CentOS 7.9,作者结论是热迁移成功与否与 docker 和 criu 版本强相关。

sudo yum install criu -y

docker run -d --name looper2 --security-opt seccomp:unconfined busybox \
/bin/sh -c 'i=0; while true; do echo $i; i=$(expr $i + 1); sleep 1; done'

docker checkpoint create looper2 checkpoint2
docker logs looper2

docker create --name looper-clone --security-opt seccomp:unconfined busybox \
/bin/sh -c 'i=0; while true; do echo $i; i=$(expr $i + 1); sleep 1; done'

docker start --checkpoint=checkpoint2 looper-clone
docker logs looper-clone

三个必须满足的前提

seccomp 必须放开。 --security-opt seccomp:unconfined 是必需的,否则 CRIU 的 ptrace 类操作会被 seccomp 拦下来。

TCP 要显式声明。 原进程用 TCP 时,dump 需要加 --tcp-established

进程要能处理 EINTR。 checkpoint 可能打断系统调用(比如 poll),原进程需要支持系统调用返回 EINTR 的情况,否则这一刀切下去进程就回不来了。

恢复时发生了什么

恢复不是「启动一个新进程再填数据」,而是 CRIU 把执行恢复的自身进程「变身」成被恢复的进程。

  • PID 必须复现。 进程只能以 checkpoint 时的同一 PID 恢复。CRIU 把「目标 PID 减一」写进 /proc/sys/kernel/ns_last_pid,然后校验新建进程是否真拿到了那个 PID,拿不到就中止恢复。
  • 文件描述符以原标识重新打开,并调整回原偏移。
  • 内存页从 checkpoint 目录读回,映射回原地址。
  • 库是最硬的要求。 源与目标系统上被用到的库必须完全一致。库不会在目标重新加载,恢复后的二进制期望库函数出现在完全相同的内存地址,对不上就崩。容器场景相对宽松,因为镜像里自带依赖库。
  • 迁移流程就是 checkpoint → 传输镜像 → restore;容器迁移还要另外传文件系统,除非用共享存储。停机时间取决于进程大小,单进程内存越大,dump 阶段的写入量和恢复阶段的重映射量越大,实际成功率也越容易掉。

哪些存不了

  • GPU / 加速器。 CRIU 能保存 CPU 侧的进程运行时数据,但对进程依赖的设备资源(如加速器、GPU)通常无法直接处理,靠 plugin 机制扩展:把 .so 放到 /usr/lib/criu 默认查询路径,或用环境变量 CRIU_LIBS_DIR,或命令行参数指定。结合 docker 且用环境变量加 plugin 时,要在 docker 启动时设置,例如 systemd 的 [Service] 里写 Environment="CRIU_LIBS_DIR= "
  • 容器给自己打点。 在非特权 apptainer(原 singularity)内运行的用户作业,CRIU 打不了检查点,无论从容器外还是容器内调用;加 root 权限后仍然失败。换成 podman/docker 直接跑结果不变:CRIU 不能给「运行在容器运行时里」的用户作业打点,必须走 docker/podman 自己的接口,而 singularity 没有这个接口。在容器化(如 K8s)环境里调用 CRIU 同样失败。
  • 跨 CPU 型号。 checkpoint 与 restore 两端 CPU family/type 一致时可工作;否则用特殊编译 flag 编出的程序只能在同族/同型号 CPU 上恢复。
  • 目录路径。 恢复要求与 checkpoint 时使用相同的目录路径。
  • 权限。 CRIU 需要某种形式的 root:sudo、SUID bit 或 kernel capabilities(用 capabilities 需要特定版本的 CRIU 和 Linux)。
  • AArch64 的 pre-copy。 内核不支持 AArch64 上的预复制检查点。

排障时的排查顺序

  1. 出现了 ptrace 相关失败,先看 seccomp 有没有放开。
  2. restore 直接中止,检查 /proc/sys/kernel/ns_last_pid 是否可写、目标 PID 是否已被占用。
  3. 恢复后进程立刻崩,先比对两端的库是否完全一致;跨主机时优先用带依赖库的镜像。
  4. 提示找不到 devices 或加速器相关资源,确认 plugin 是否被加载到 /usr/lib/criu,或 CRIU_LIBS_DIR 是否在正确的层级设置(docker 场景要在 docker 启动时设)。
  5. 恢复路径与 checkpoint 路径不一致,先对齐目录。
  6. 上面都排掉还是失败,回头核对 docker 与 criu 的版本组合——实测里这一项直接决定热迁移成败。

尾声

K8s 原生的 checkpoint 能力目前仍在演进,具体形态和 CRIU 之间的关系还没有尘埃落定。

复制全文 生成海报 CRIU 容器热迁移 检查点恢复 Linux运维

推荐文章

程序员茄子在线接单