2. 容器运行时基础:OCI运行时规范、runc架构与生命周期管理、容器进程模型
2.1 OCI运行时规范:容器运行时的“宪法”
OCI(Open Container Initiative)运行时规范定义了容器运行时的最低行为标准。任何符合该规范的实现(如runc、crun、youki)都能保证一致的容器创建、启动、停止流程。规范核心包含两大组件:运行时规范(runtime-spec)和镜像规范(image-spec)。本章聚焦运行时规范。
| 规范组件 | 定义内容 | 关键文件/结构 |
|---|---|---|
| config.json | 容器配置清单,包含进程、根文件系统、挂载点、命名空间、资源限制等 | ociVersion、process、root、mounts、linux |
| runtime-spec | 定义容器生命周期操作:create、start、kill、delete、state、exec | 状态机转换、钩子(hooks)、标准输入输出 |
| 文件系统捆绑包 | 容器根文件系统目录,包含 config.json 和根文件系统内容 |
rootfs/ 目录 + config.json |
SRE 实战提示:OCI 规范的核心价值在于可互换性。当你在生产环境中遇到 runc 性能瓶颈时,可以无缝切换到 crun(C 语言实现,启动速度提升 30%-50%),只需保证 config.json 符合 OCI 规范即可。
2.2 runc架构:从OCI规范到内核系统调用的桥梁
runc 是 OCI 运行时规范的参考实现,也是 Docker、containerd、Podman 等主流容器引擎的默认底层运行时。其架构可划分为三层:CLI/API层、生命周期管理层、内核接口层。
2.3 容器生命周期管理:状态机与操作流程
runc 将容器生命周期定义为严格的状态机,每个状态转换对应一个明确的 CLI 操作。理解状态机是故障时间线还原的基础。
2.3.1 状态定义与转换
| 状态 | 描述 | 对应操作 | 典型持续时间 |
|---|---|---|---|
| creating | 容器正在创建,config.json 解析中,命名空间尚未完全就绪 | runc create 执行中 |
毫秒级 |
| created | 容器已创建,命名空间已隔离,但用户进程尚未启动 | runc create 完成 |
可停留(等待 start) |
| running | 用户进程(init 进程)正在执行 | runc start 完成 |
容器运行时长 |
| stopped | 容器进程已退出,资源尚未完全回收 | runc kill 或进程自然退出 |
短暂(等待 delete) |
2.3.2 核心操作详解
runc create <container-id>:解析 config.json,创建命名空间(clone()),配置 cgroups,挂载根文件系统,执行 prestart 钩子。容器进入 created 状态。runc start <container-id>:在已创建的命名空间中执行用户进程(execve),执行 poststart 钩子。容器进入 running 状态。runc kill <container-id> <signal>:向容器 init 进程发送信号。默认 SIGTERM,超时后 SIGKILL。runc delete <container-id>:清理容器残留资源(cgroups 目录、挂载点、网络接口)。只能在 stopped 状态执行。runc exec <container-id> <command>:在运行中的容器内执行新进程(通过 setns() 加入已有命名空间)。
运维避坑:runc create 和 runc start 是分离操作。在 Kubernetes + containerd 架构中,kubelet 调用 CRI 接口创建 sandbox 时,如果 runc create 成功但 runc start 失败(例如 seccomp 策略拒绝 execve),容器会卡在 created 状态。此时需要手动
runc delete 清理,否则 cgroups 残留会导致节点资源泄漏。
2.4 容器进程模型:PID 1、信号处理与僵尸进程
容器内的进程模型与宿主机有本质区别。理解这些差异对于排查“容器无法正常停止”、“容器内进程僵死”等经典故障至关重要。
2.4.1 容器 PID 1 的特殊性
在容器中,用户指定的入口进程(ENTRYPOINT/CMD)成为 PID 1。Linux 内核为 PID 1 赋予了特殊语义:
- 信号屏蔽:PID 1 默认不会处理 SIGTERM/SIGINT 等信号,除非显式注册信号处理器。
- 孤儿进程收养:容器内所有孤儿进程(父进程已退出的子进程)会被 PID 1 收养。如果 PID 1 不调用
wait(),孤儿进程将变成僵尸进程。 - init 进程职责:PID 1 必须负责回收子进程、处理信号转发。这也是 tini、dumb-init 等 init 系统存在的意义。
2.4.2 进程树与命名空间隔离
| 特性 | 宿主机进程 | 容器内进程 | 影响 |
|---|---|---|---|
| PID 命名空间 | 全局 PID 空间 | 独立 PID 空间,PID 从 1 开始 | 容器内无法看到宿主机进程 |
| 进程树根 | init (systemd) | 容器入口进程 | 孤儿进程被容器 PID 1 收养 |
| 信号传播 | 内核直接传递 | 需 PID 1 显式处理或使用 --init | SIGTERM 可能被忽略 |
| 僵尸回收 | systemd 自动 wait() | 依赖 PID 1 实现 | 无 init 系统时僵尸堆积 |
2.4.3 典型故障场景:僵尸进程风暴
当容器内应用频繁 fork 子进程且 PID 1 未正确回收时,僵尸进程会占满 PID 空间,导致 fork() 失败。runc 本身不提供僵尸回收能力,需要容器镜像自带 init 系统。
排查命令:
# 查看容器内僵尸进程
runc exec <container-id> ps aux | grep 'Z'
# 查看容器 PID 1 的 wait 状态
runc exec <container-id> cat /proc/1/status | grep -i state
# 宿主机视角查看容器 PID 1 的信号处理
cat /proc/$(runc state <container-id> | jq '.pid')/status | grep -i sig
2.5 本章小结:SRE 视角的关键要点
- OCI 规范是契约:config.json 的字段含义决定了容器的行为边界,故障排查时首先检查配置完整性。
- runc 状态机是故障时间线的基础:每个状态转换点都是潜在的故障注入点。creating 阶段关注资源,running 阶段关注信号和进程。
- 容器进程模型是运维盲区:PID 1 的信号处理和僵尸回收是容器化应用最常见的稳定性隐患,建议生产环境统一使用 tini 或 --init 参数。