Linux内核 CRIU 检查点恢复:容器热迁移、快照调试与有状态工作负载容错的深度工程实战

在生产环境中,我们常常面临这样的困境:一个正在运行的有状态服务(比如长连接的WebSocket服务器、正在执行长任务的Worker、或者一个承载重要会话的容器)需要停机升级、迁移到其他节点、或者排查一个偶发的Bug —— 但直接杀死它意味着丢失内存中的上下文、断开客户端连接、甚至破坏数据一致性。CRIU(Checkpoint/Restore In Userspace)正是为此而生:它可以在用户态将一个完整运行的进程(或容器)冻结并序列化到磁盘,然后在另一台机器上精确恢复,就像什么都没发生过一样。

一、为什么需要 CRIU 检查点恢复

1.1 传统热迁移的局限

在虚拟机时代,热迁移(Live Migration)已经很成熟。但容器不同:它们共享宿主机内核,传统VM级别的迁移无法直接应用。我们需要一种"进程级快照"的技术。

传统方案及其局限:

1.2 CRIU 的独特价值

方案 局限
应用层序列化 需要应用自行实现,无法覆盖所有状态(打开的socket、文件描述符、IPC等)
外部重放(TCP Replay) 无法保证精确的状态一致性,对非确定性事件(时间戳、随机数)无能为力
主备切换 需要双份资源,standby 期间浪费一半算力

CRIU 在用户态接管了整个进程状态的采集与恢复。它能处理:

  • 进程树与命名空间(PID、Mount、UTS、IPC、Network、User namespace)
  • 内存页(包括共享内存、mmap 文件、匿名映射)
  • 文件描述符(普通文件、管道、Unix socket、Netlink socket)
  • 网络状态(TCP socket、UDP、UNIX domain socket)
  • 系统 V IPC(消息队列、信号量、共享内存)
  • 定时器(POSIX timer、itimer、alarm)
  • 信号处理器与挂起的信号
  • 凭证(UID/GID、Capabilities、SECCOMP 过滤器)
  • ptrace 附加的子进程(gdb 调试会话也可被冻结)
  • 这些能力使得 CRIU 成为了容器的"时间机器"。

    二、CRIU 核心架构深入

    2.1 检查点(Checkpoint)流程

    CRIU 的检查点流程是一个复杂的多阶段操作:

    
    ┌──────────────────────────────────────────────────────────┐
    │                    CRIU Checkpoint Flow                    │
    ├──────────────────────────────────────────────────────────┤
    │                                                          │
    │  1. 预冻结 (Pre-dump)                                    │
    │     └─ 使用 parasite code 注入到目标进程                  │
    │                                                          │
    │  2. 冻结 (Freeze)                                        │
    │     └─ 通过 cgroup freezer 暂停所有进程                   │
    │                                                          │
    │  3. 状态采集 (Dump)                                      │
    │     ├─ 遍历 /proc/<pid>/ 获取所有元数据                   │
    │     ├─ 通过 netlink 获取网络状态                          │
    │     ├─ 通过 ptrace 获取寄存器与内存映射                   │
    │     └─ 使用 PPM 协议压缩写入镜像文件                      │
    │                                                          │
    │  4. 镜像持久化                                            │
    │     └─ 生成 pages-<id>.img, files.img, fs-*.img 等      │
    │                                                          │
    │  5. 恢复唤醒 (Post-dump)                                  │
    │     └─ 允许进程继续运行(迭代式检查点)                   │
    │                                                          │
    └──────────────────────────────────────────────────────────┘
    

    2.2 恢复(Restore)流程

    恢复是检查点的逆过程,需要特别注意 PID 命名空间的精确重建:

    
    ┌──────────────────────────────────────────────────────────┐
    │                    CRIU Restore Flow                       │
    ├──────────────────────────────────────────────────────────┤
    │                                                          │
    │  1. 命名空间创建                                          │
    │     └─ unshare()/clone() 重建所有 ns                      │
    │                                                          │
    │  2. 进程树重建                                            │
    │     └─ 使用 CLONE_PARENT 重建父子关系                     │
    │                                                          │
    │  3. 文件描述符恢复                                        │
    │     ├─ 按原始 fd 编号打开文件                             │
    │     ├─ 重建管道与 socket                                  │
    │     └─ 重连 Unix domain socket                            │
    │                                                          │
    │  4. 内存页恢复                                            │
    │     ├─ 使用 userfaultfd 按需加载                          │
    │     └─ 恢复 mmap 映射与共享内存                           │
    │                                                          │
    │  5. 上下文恢复                                            │
    │     ├─ 设置寄存器状态                                     │
    │     ├─ 恢复定时器                                         │
    │     └─ 重注入信号处理器                                   │
    │                                                          │
    │  6. 运行态恢复                                            │
    │     └─ sigreturn 使进程从冻结点继续执行                   │
    │                                                          │
    └──────────────────────────────────────────────────────────┘
    

    2.3 Parasite Code 注入机制

    CRIU 的核心技术之一是"寄生代码注入"。由于检查点需要从目标进程的视角获取状态(比如 socket 的recvq),CRIU 需要将一小段代码注入到目标进程的地址空间中执行:

    
    // 简化的 parasite 注入流程 ( criu/parasite-syscall.c )
    int parasite_trap_cmd(int pid, int cmd) {
        struct regs regs;
        
        // 1. 附加到目标进程
        ptrace(PTRACE_ATTACH, pid);
        
        // 2. 保存原始寄存器状态
        get_regs(pid, &regs);
        
        // 3. 在目标进程的代码段中寻找可执行空间
        //    注入 sys_mmap() 来分配寄生块
        inject_syscall(pid, __NR_mmap, ...);
        
        // 4. 将寄生代码(get_fd info, dump socket 等)写入目标进程
        write_proc_memory(pid, trap_addr, parasite_blob, blob_size);
        
        // 5. 修改 RIP/RSP 使其执行寄生代码
        modify_regs(pid, trap_addr);
        
        // 6. 等待寄生代码执行完成
        wait_for_seized(pid, CMD_DONE);
        
        // 7. 恢复原始寄存器
        set_regs(pid, &regs);
        ptrace(PTRACE_DETACH, pid);
    }
    

    这种注入方式能够实现类似 gdb 的功能,但专门用于状态采集,性能开销极小(通常 < 50ms)。

    三、关键技术细节

    3.1 TCP socket 的精确恢复

    TCP 是最难处理的状态之一。CRIU 需要:

    
    # 检查点时需要捕获的关键 TCP 信息:
    # 1. 序列号与确认号
    # 2. 接收/发送缓冲区中的数据
    # 3. TCP 选项(window scaling, timestamps, SACK)
    # 4. 连接状态(ESTABLISHED, TIME_WAIT 等)
    
    # 恢复时关键配置:
    criu restore \
        --tcp-established \        # 恢复已建立的TCP连接
        --tcp-close \              # 关闭未引用的连接
        --ext-unix-sk \            # 恢复外部 unix socket
        --shell-job \              # 恢复 shell job 控制
        --manage-cgroups           # 重建 cgroup 层级
    

    3.2 内存页的迭代检查点(Iterative Migration)

    对于需要热迁移的场景,CRIU 支持迭代式检查点(pre-copy):

    
    # 第一次检查点:全量复制内存
    criu dump --tree=<pid> --images-dir=/tmp/checkpoint1 --leave-running
    
    # 后续检查点:仅复制脏页
    criu dump --tree=<pid> --images-dir=/tmp/checkpoint2 \
        --leave-running --track-mem --prev-images-dir=/tmp/checkpoint1
    
    # 最后一次:在很短的时间内冻结进程,复制剩余脏页
    criu dump --tree=<pid> --images-dir=/tmp/checkpoint_final \
        --prev-images-dir=/tmp/checkpoint2
    

    这种方式可以将停机时间从秒级降低到毫秒级,非常适合在线迁移场景。

    3.3 文件系统的处理

    对于 mount namespace 中的文件,CRIU 需要:

    
    # 处理 overlayfs 等复杂文件系统
    criu dump \
        --ext-mount-map auto \      # 自动处理外部挂载映射
        --exec-cmd -- /bin/mount -t tmpfs tmpfs /run
        
    # 对于 bind mount 需要手动映射
    echo "/var/lib/redis/data /var/lib/redis/data none bind" > /tmp/mount_map.txt
    criu dump --ext-mount-map /tmp/mount_map.txt
    

    四、Kubernetes 集成实战

    4.1 使用 CRI-O + CRIU 实现容器检查点

    CRI-O 从 1.25 版本开始原生支持 CRIU 检查点:

    
    apiVersion: v1
    kind: Pod
    metadata:
      name: redis-stateful
      annotations:
        # 启用 CRIU 检查点支持
        criu.kubernetes.io/checkpoint: "true"
    spec:
      containers:
      - name: redis
        image: redis:7.2
        resources:
          requests:
            memory: "512Mi"
            cpu: "500m"
        volumeMounts:
        - name: redis-data
          mountPath: /data
      volumes:
      - name: redis-data
        persistentVolumeClaim:
          claimName: redis-pvc
    

    通过 Kubernetes 子资源端点触发检查点:

    
    # 触发容器检查点并生成 OCI 镜像
    kubectl checkpoint create redis-checkpoint/redis-stateful \
        --namespace=default
    
    # 输出:
    # Created checkpoint: /var/lib/kubelet/checkpoints/redis-checkpoint_redis-stateful-2025-01-15.tar
    
    # 基于 checkpoint 镜像创建新 Pod
    kubectl checkpoint restore redis-checkpoint/redis-stateful \
        --namespace=production
    

    4.2 自定义 CRIU 检查点控制器

    对于更复杂的场景,我们可以编写自定义控制器:

    
    # criu_controller.py - 简化版 CRIU 检查点控制器
    import subprocess
    import time
    import json
    from pathlib import Path
    from datetime import datetime
    
    class CheckpointController:
        def __init__(self, image_dir="/var/lib/criu/images"):
            self.image_dir = Path(image_dir)
            self.image_dir.mkdir(parents=True, exist_ok=True)
        
        def checkpoint_pod(self, pod_name: str, namespace: str = "default"):
            """触发 Pod 检查点"""
            ts = datetime.now().strftime("%Y%m%d_%H%M%S")
            checkpoint_dir = self.image_dir / f"{pod_name}_{ts}"
            checkpoint_dir.mkdir()
            
            # 通过 CRI-O 的 checkpoint 端点
            pod = self._get_pod(pod_name, namespace)
            container_id = pod['status']['containerStatuses'][0]['containerID']
            
            # 构建检查点请求
            req = {
                "action": "checkpoint",
                "container_id": container_id,
                "image": str(checkpoint_dir),
                "preserve_workspace": True
            }
            
            result = subprocess.run(
                ["crictl", "checkpoint", json.dumps(req)],
                capture_output=True, text=True
            )
            
            if result.returncode != 0:
                raise RuntimeError(f"Checkpoint failed: {result.stderr}")
            
            # 压缩为 tar
            archive = checkpoint_dir.with_suffix(".tar.gz")
            subprocess.run(
                ["tar", "-czf", str(archive), "-C", str(self.image_dir), checkpoint_dir.name]
            )
            
            return archive
        
        def restore_pod(self, archive: str, pod_name: str, namespace: str = "default"):
            """从归档恢复 Pod"""
            # 解压
            extract_dir = Path(archive).with_suffix("").with_suffix("")
            subprocess.run(["tar", "-xzf", archive, "-C", self.image_dir])
            
            # 创建 Pod manifest
            restore_spec = {
                "apiVersion": "v1",
                "kind": "Pod",
                "metadata": {"name": pod_name},
                "spec": {
                    "containers": [{
                        "name": "app",
                        "image": "registry/criu-restored:latest",
                        "command": ["criu", "restore", "--images-dir", "/checkpoint"],
                        "volumeMounts": [{
                            "name": "checkpoint-vol",
                            "mountPath": "/checkpoint"
                        }]
                    }],
                    "volumes": [{
                        "name": "checkpoint-vol",
                        "hostPath": {"path": str(extract_dir)}
                    }]
                }
            }
            
            # 应用 manifest
            return kubectl_apply(restore_spec, namespace)
    

    4.3 OpenKruue + CRIU 的高级用法

    对于大规模部署,OpenKruue 提供了更完善的 CRIU 集成:

    
    # openkruue_checkpoint.yaml
    apiVersion: v1
    kind: CheckpointSchedule
    metadata:
      name: webapp-checkpoint
    spec:
      sourceRef:
        kind: Deployment
        name: webapp-deployment
      schedule: "*/30 * * * *"  # 每 30 分钟检查点一次
      historyLimit: 10          # 保留 10 个历史检查点
      retentionPolicy:
        maxAge: 7d
        maxCount: 20
      
      # 增量检查点配置
      incremental:
        enabled: true
        baseCheckpoint: auto
        
      # 检查点完成后自动导出到 S3
      export:
        enabled: true
        s3:
          bucket: my-checkpoint-store
          prefix: webapp/
          region: cn-north-1
    

    五、生产环境实践

    5.1 长连接 WebSocket 服务的无损升级

    场景:WebSocket 服务器承载了 10 万在线连接,需要升级应用代码而不踢出用户。

    
    #!/bin/bash
    # websocket_zero_downtime_upgrade.sh
    
    WS_PID=$(pgrep -f "nodejs.*websocket-server")
    CHECKPOINT_DIR="/var/ckpt/ws-$(date +%s)"
    
    echo "[+] 开始检查点: PID=$WS_PID"
    
    # 1. 设置 cgroup freezer 精确控制
    mkdir -p /sys/fs/cgroup/freezer/criu-migrate
    echo $WS_PID > /sys/fs/cgroup/freezer/criu-migrate/cgroup.procs
    
    # 2. 执行检查点
    criu dump \
        --tree=$WS_PID \
        --images-dir=$CHECKPOINT_DIR \
        --tcp-established \
        --shell-job \
        --ext-unix-sk \
        --ghost-limit=1048576 \
        --action-script=/scripts/notify_before_dump.sh \
        --leave-stopped
    
    echo "[+] 检查点完成: $CHECKPOINT_DIR ($(du -sh $CHECKPOINT_DIR | cut -f1))"
    
    # 3. 终止原进程
    kill $WS_PID
    
    # 4. 启动新版本(同一端口)
    systemctl start websocket-server-v2
    
    # 5. 恢复连接状态到新进程
    RESTORE_PID=$(systemctl show -p MainPID websocket-server-v2 | cut -d= -f2)
    
    # 通过 unix socket 传递 socket fd 给新进程
    criu restore \
        --tree=$RESTORE_PID \
        --images-dir=$CHECKPOINT_DIR \
        --tcp-established \
        --restore-detached \
        --action-script=/scripts/notify_after_restore.sh
    
    echo "[+] 连接恢复完成"
    

    5.2 机器学习训练任务容错

    对于长达数天的 ML 训练任务,检查点可以防止意外中断导致的全部重算:

    
    # train_with_criu_checkpoint.py
    import os
    import signal
    import subprocess
    import torch
    import torch.nn as nn
    
    class CRIUCheckpointManager:
        def __init__(self, checkpoint_dir="/mnt/nfs/training_ckpt"):
            self.checkpoint_dir = checkpoint_dir
            self.iteration = 0
            
        def setup_signal_handler(self):
            """设置 SIGUSR1 触发 CRIU 检查点"""
            def handler(signum, frame):
                self._do_criu_checkpoint()
            signal.signal(signal.SIGUSR1, handler)
        
        def _do_criu_checkpoint(self):
            """执行 CRIU 检查点,保存 PyTorch 训练状态"""
            pid = os.getpid()
            ckpt_subdir = os.path.join(
                self.checkpoint_dir, 
                f"iteration_{self.iteration}"
            )
            os.makedirs(ckpt_subdir, exist_ok=True)
            
            # 保存 PyTorch state dict 到共享存储
            # 确保 CRIU 可以访问完整状态
            torch.save({
                'iteration': self.iteration,
                'model_state': model.state_dict(),
                'optimizer_state': optimizer.state_dict(),
                'rng_state': torch.get_rng_state(),
                'cuda_rng_state': torch.cuda.get_rng_state(),
            }, os.path.join(ckpt_subdir, 'training_state.pt'))
            
            # 执行 CRIU dump
            subprocess.run([
                'criu', 'dump',
                '--tree', str(pid),
                '--images-dir', ckpt_subdir,
                '--leave-running',  # 检查点后继续运行
                '--ghost-limit', '4096',
                '-v4',  # 详细日志
            ], check=True)
            
            print(f"[CRIU] Iter {self.iteration} checkpointed to {ckpt_subdir}")
        
        def restore_from_checkpoint(self, ckpt_path):
            """从 CRIU 检查点恢复"""
            subprocess.run([
                'criu', 'restore',
                '--images-dir', ckpt_path,
                '--restore-detached',
            ], check=True)
    
    # 训练主循环
    model = MyModel()
    optimizer = torch.optim.Adam(model.parameters())
    ckpt_mgr = CKPTManager()
    ckpt_mgr.setup_signal_handler()
    
    for epoch in range(100):
        # ... training logic ...
        ckpt_mgr.iteration = epoch
        
        # 通过外部触发: kill -USR1 <pid>
        # 或者定时自动检查点
    

    5.3 调试偶发 Bug

    利用 CRIU 的"时间回溯"能力,可以反复调试难以复现的 Bug:

    
    # 1. 在异常状态即将发生时触发检查点
    criu dump --tree=$(pidof myapp) --images-dir=/tmp/bug_ckpt --leave-running
    
    # 2. 反复恢复观察
    for i in $(seq 1 10); do
        echo "=== Run $i ==="
        criu restore --images-dir=/tmp/bug_ckpt \
            --restore-detached \
            --pidfile=/tmp/restored_pid \
            -v4 2>&1 | tee /tmp/restore_$i.log
        
        # 使用 gdb 进行调试
        gdb -p $(cat /tmp/restored_pid) \
            -ex "break suspicious_function" \
            -ex "continue" \
            -ex "bt full" \
            -ex "quit"
    done
    

    六、高级技巧与故障排查

    6.1 性能优化

    检查点的主要开销在于内存页复制:

    
    # 使用 --lazy-pages 支持按需恢复
    criu restore --images-dir=/tmp/ckpt \
        --lazy-pages \
        --page-server 192.168.1.10:9999 &
        
    # page-server 在另一个终端运行
    criu page-server --images-dir=/tmp/ckpt --port=9999 --auto-dedup
    
    # 使用 zstd 压缩加速传输
    criu dump --compress=zstd:3 --images-dir=/tmp/ckpt
    

    检查点大小与停机时间对比表:

    6.2 常见问题排查

    
    # 问题 1: 恢复失败,提示 "Unable to restore namespace"
    # 原因: 目标系统内核版本不一致导致 ns 接口差异
    # 修复: 使用 --check-external 参数列出外部依赖
    criu dump --images-dir=/tmp/ckpt --check-external
    
    # 问题 2: TCP 连接恢复后 RTP 流中断
    # 原因: CRIU 无法恢复应用层缓存的 RTP 序号
    # 修复: 应用层需要参与恢复流程
    criu restore --images-dir=/tmp/ckpt \
        --action-script=/scripts/reinit_rtp.sh
    
    # /scripts/reinit_rtp.sh 示例:
    #!/bin/bash
    echo "Resuming RTP stream..."
    # 发送控制命令让应用重置 UDP 流
    echo "FLUSH_RTP" > /run/app_control.sock
    
    # 问题 3: 大mmap文件导致检查点缓慢
    # 修复: 使用 --ghost-file 跳过不必要的大文件
    criu dump --tree=$PID --images-dir=/tmp/ckpt \
        --ghost-file /data/large_db.sqlite \
        --ghost-limit 1073741824  # 1GB
    

    6.3 Kernel 版本兼容性

    
    # 检查系统是否支持 CRIU
    criu check --all
    
    # 输出示例:
    # Looks good.
    # 或:
    # Error: /proc/config.gz doesn't have CONFIG_SECCOMP_FILTER=y
    

    七、未来展望

    内存占用 压缩方式 检查点大小 恢复时间 (本地) 恢复时间 (网络)
    4 GiB none 4.0 GiB 1.2s 8.5s
    4 GiB zstd:3 0.8 GiB 1.8s 2.1s
    4 GiB zstd:9 0.6 GiB 4.2s 1.6s
    16 GiB zstd:3 3.2 GiB 5.8s 6.4s
    16 GiB zstd:9 2.4 GiB 14.2s 4.8s
    CRIU 版本 最低 Linux 内核 推荐内核 关键依赖
    3.18 4.18 5.15+ protobuf, libnl
    3.19 5.4 6.1+ 需 CONFIG_USERFAULTFD
    4.0 (dev) 5.10 6.6+ 需 CONFIG_CHECKPOINT_RESTORE

    CRIU 的发展正沿着几个关键方向:

  • GPU 状态支持: NVIDIA 与 CRIU 社区合作推进 CUDA 上下文的快照,目标是让 GPU 渲染/计算任务也能热迁移
  • SGX/TDX 机密计算: 恢复 SGX enclave 内的信任链,实现机密容器的在线迁移
  • 与 Checkpoint Scheduling Operator 深度集成: Kubernetes 层面的声明式检查点策略
  • 共享内存加速: 利用 CXL 内存池化,实现纳秒级跨节点内存恢复
  • 与 eBPF 联动: 通过 BPF 程序在检查点时自动清理敏感数据(密钥、Token),实现"安全的快照"
  • 总结

    CRIU 不是一个"银弹",但它是系统工程师工具箱中不可或缺的一环。无论是容器热迁移、无损升级、故障恢复还是 Bug 调试,CRIU 都能提供其他方案难以复现的精确状态保持能力。它的核心价值在于:在云原生时代,让进程像虚拟机一样获得"时间暂停与传送"的能力。

    掌握 CRIU 的关键不是记住所有参数,而是理解它如何处理不同类别的状态(内存、文件、网络、命名空间),这样才能在合适的场景做出正确的设计决策。下次当你面临"这个进程不能死,必须原地复活"的需求时,CRIU 就是那个答案。


    相关工具链: `criu`, `crictl checkpoint`, `OpenKruue`, `CRIU-K8s-Checkpoint`, `Podman checkpoint` (podman container checkpoint)

    参考文档: [CRIU 官方文档](https://criu.org/) | [CRIU GitHub](https://github.com/checkpoint-restore/criu)

    点赞(0) 打赏

    评论列表 共有 0 条评论

    暂无评论
    立即
    投稿

    微信公众账号

    微信扫一扫加关注

    发表
    评论
    返回
    顶部