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层、生命周期管理层、内核接口层。

runc 架构分层图 CLI / API 层 runc create | runc start | runc kill | runc exec 生命周期管理层 状态机:creating → created → running → stopped 钩子系统:prestart | poststart | poststop cgroups 配置 | 命名空间创建 | 挂载点处理 内核接口层 clone() | unshare() | setns() | pivot_root() | mount() cgroups v1/v2 文件系统接口 | seccomp BPF | capabilities namespace: pid | net | mnt | uts | ipc | user | cgroup Linux 系统调用 + /proc + /sys 文件系统 架构要点: runc 本身是一个轻量级 CLI 工具(~5MB),不包含守护进程。每次容器操作都通过 fork+exec 方式创建子进程, 子进程负责执行实际的命名空间创建和系统调用,父进程负责监控和状态管理。

2.3 容器生命周期管理:状态机与操作流程

runc 将容器生命周期定义为严格的状态机,每个状态转换对应一个明确的 CLI 操作。理解状态机是故障时间线还原的基础。

2.3.1 状态定义与转换

状态 描述 对应操作 典型持续时间
creating 容器正在创建,config.json 解析中,命名空间尚未完全就绪 runc create 执行中 毫秒级
created 容器已创建,命名空间已隔离,但用户进程尚未启动 runc create 完成 可停留(等待 start)
running 用户进程(init 进程)正在执行 runc start 完成 容器运行时长
stopped 容器进程已退出,资源尚未完全回收 runc kill 或进程自然退出 短暂(等待 delete)
runc 容器生命周期状态机 creating created running stopped create start exit/kill 状态转换关键点: 1. creating → created:runc 完成命名空间创建、cgroups 配置、根文件系统挂载,但用户进程尚未启动。 2. created → running:runc 执行 execve() 启动用户进程(PID 1),此时容器开始消耗 CPU/内存。 3. running → stopped:用户进程退出或收到 SIGKILL,内核清理进程资源,runc 记录退出码。 4. 故障场景:creating 阶段卡住通常意味着内核资源不足;running 阶段异常退出需检查 OOM、seccomp 或信号。

2.3.2 核心操作详解

  1. runc create <container-id>:解析 config.json,创建命名空间(clone()),配置 cgroups,挂载根文件系统,执行 prestart 钩子。容器进入 created 状态。
  2. runc start <container-id>:在已创建的命名空间中执行用户进程(execve),执行 poststart 钩子。容器进入 running 状态。
  3. runc kill <container-id> <signal>:向容器 init 进程发送信号。默认 SIGTERM,超时后 SIGKILL。
  4. runc delete <container-id>:清理容器残留资源(cgroups 目录、挂载点、网络接口)。只能在 stopped 状态执行。
  5. 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 参数。