mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3761 字
11 分钟
容器在 Kubernetes 中
2026-06-16

Kubernetes 不直接运行容器。它通过 kubelet 把容器编排的意图翻译成 CRI gRPC 调用,交给 containerd 或 CRI-O 执行。一个 Pod 从 API Server 的 YAML 到真正跑起来的进程,中间要经历 Sandbox 创建、Namespace 共享、Init Container 串行执行、主容器启动、Sidecar 注入——每一步都对应着容器运行时的具体操作。

本章站在 Kubernetes 的视角,看容器运行时如何被编排系统驱动。理解了 CRI 接口和 Pod 创建流程,你就能从 kubelet 日志和 crictl 输出中定位问题,也能理解为什么 Pod 里的容器共享 Network Namespace、pause 容器为什么存在、Init Container 和普通容器到底有什么区别。

Kubernetes 与容器运行时的关系经历了一次重大解耦。Kubernetes 1.5 之前,kubelet 硬编码调用 Docker API——Docker 是唯一支持的运行时。2016 年,Kubernetes 1.5 引入 CRI(Container Runtime Interface),将容器运行时操作抽象为一组 gRPC 接口,kubelet 不再直接调用任何运行时的 API。2018 年,dockershim 被标记为弃用;2020 年,Kubernetes 1.24 正式移除 dockershim——从此 Docker 不再是 Kubernetes 的”默认运行时”,containerd 取而代之。这场解耦的核心思想是:Kubernetes 不关心你用什么容器运行时,只关心你遵循 CRI 接口。理解这段历史,有助于理解为什么 Kubernetes 的容器运行时配置如此灵活——CRI 让 containerd、CRI-O、Kata Containers、gVisor 都可以成为 Kubernetes 的运行时。

前置知识#

  • Ch07 containerd 架构:containerd 实现了 CRI 插件,是 Kubernetes 的默认运行时
  • Ch08 containerd-shim:Pod Sandbox 通过 shim 管理,pause 容器是第一个 shim 创建的进程
  • Ch09 容器完整流程:Kubernetes 中的容器创建流程更复杂,理解单容器流程是前提
  • Kubernetes 基础:Pod、Deployment、Service 等核心概念
Note

如果你还不熟悉 Kubernetes 基础概念,推荐先阅读 Kubernetes 官方文档的 Pod 概念 章节。

一、Kubernetes 与容器运行时:kubelet 通过 CRI 与 containerd/runc 交互#

1.1 Kubernetes 的容器运行时抽象#

Kubernetes 从 1.5 版本引入 CRI(Container Runtime Interface),把容器运行时操作抽象为一组 gRPC 接口。kubelet 不再直接调用 Docker 或 containerd 的 API,而是通过 CRI 与运行时通信。这种解耦让 Kubernetes 可以支持多种容器运行时——containerd、CRI-O、Mirantis Container Runtime,甚至 Kata Containers 和 gVisor。

graph TB subgraph K8s控制面["Kubernetes 控制面"] API["API Server"] SCHED["Scheduler"] CTRLMGR["Controller Manager"] end subgraph 节点["Worker 节点"] KUBELET["kubelet"] CRIRUNTIME["CRI 运行时<br/>(containerd / CRI-O)"] SHIM["shim"] RUNC["runc / runsc / kata"] PAUSE["pause 容器"] CONTAINER["业务容器"] end API --> KUBELET SCHED --> API CTRLMGR --> API KUBELET -->|"CRI gRPC"| CRIRUNTIME CRIRUNTIME --> SHIM SHIM --> RUNC RUNC --> PAUSE RUNC --> CONTAINER PAUSE -.->|"共享 Namespace"| CONTAINER style K8s控制面 fill:#e3f2fd,stroke:#1565c0 style 节点 fill:#e8f5e9,stroke:#2e7d32 style CRIRUNTIME fill:#fff3e0,stroke:#e65100

1.2 从 Docker 到 CRI 的演进#

Kubernetes 早期直接调用 Docker Engine 的 API。但 Docker 的 API 设计面向单机使用,与 Kubernetes 的 Pod 语义存在根本性冲突——Docker 没有Pod 的概念,一个 Pod 中的多个容器需要共享 Network Namespace,而 Docker 的容器模型是每个容器独立的 Namespace。

阶段时间运行时方案问题
Docker 直接集成K8s 1.0 - 1.5kubelet → dockerdDocker API 与 Pod 语义不匹配,每次 Docker 改 API 都要改 kubelet
CRI + dockershimK8s 1.5 - 1.24kubelet → dockershim → dockerd多一层转换,性能损耗,维护负担
CRI + containerdK8s 1.24+kubelet → containerd原生 CRI 支持,性能最优,当前主流
CRI + CRI-OK8s 1.24+kubelet → CRI-O专为 K8s 设计的轻量运行时,Red Hat 主推
Note

Kubernetes 1.24 正式移除了 dockershim(dockershim Removal)。如果你的集群还在用 Docker Engine 作为运行时,需要迁移到 containerd 或 CRI-O。Docker 构建的镜像仍然可以在任何 CRI 运行时上运行——因为镜像格式遵循 OCI 标准,与运行时无关。

1.3 kubelet 与容器运行时的交互方式#

kubelet 通过 Unix Socket 与 CRI 运行时通信。默认的 socket 路径:

/var/run/containerd/containerd.sock
/var/run/crio/crio.sock
kubectl get nodes -o wide
crictl --runtime-endpoint unix:///var/run/containerd/containerd.sock info

kubelet 在启动时通过 --container-runtime-endpoint 参数指定 CRI socket。每创建一个 Pod,kubelet 就通过这个 socket 发起一系列 CRI 调用。

二、CRI 接口详解:RuntimeService + ImageService,gRPC 协议#

2.1 CRI 的两个服务#

CRI 协议定义在 Kubernetes 仓库的 pkg/kubelet/cri/api 目录中,包含两个 gRPC 服务:

// RuntimeService - 容器和 Pod 的生命周期管理
service RuntimeService {
// Pod 管理
rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse);
rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse);
rpc RemovePodSandbox(RemovePodSandboxRequest) returns (RemovePodSandboxResponse);
rpc PodSandboxStatus(PodSandboxStatusRequest) returns (PodSandboxStatusResponse);
rpc ListPodSandbox(ListPodSandboxRequest) returns (ListPodSandboxResponse);
// 容器管理
rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse);
rpc StartContainer(StartContainerRequest) returns (StartContainerResponse);
rpc StopContainer(StopContainerRequest) returns (StopContainerResponse);
rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse);
rpc ContainerStatus(ContainerStatusRequest) returns (ContainerStatusResponse);
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse);
// 其他
rpc ExecSync(ExecSyncRequest) returns (ExecSyncResponse);
rpc Exec(ExecRequest) returns (ExecResponse);
rpc Attach(AttachRequest) returns (AttachResponse);
rpc PortForward(PortForwardRequest) returns (PortForwardResponse);
}
// ImageService - 镜像管理
service ImageService {
rpc ListImages(ListImagesRequest) returns (ListImagesResponse);
rpc ImageStatus(ImageStatusRequest) returns (ImageStatusResponse);
rpc PullImage(PullImageRequest) returns (PullImageResponse);
rpc RemoveImage(RemoveImageRequest) returns (RemoveImageResponse);
rpc ImageFsInfo(ImageFsInfoRequest) returns (ImageFsInfoResponse);
}

2.2 CRI 关键接口说明#

接口作用对应的底层操作
RunPodSandbox创建 Pod Sandbox(pause 容器)创建 Network Namespace、挂载 /etc/resolv.conf、启动 pause 进程
CreateContainer在 Sandbox 中创建容器准备 rootfs、生成 OCI Bundle、runc create
StartContainer启动已创建的容器runc start
StopContainer停止容器发送 SIGTERM,超时后 SIGKILL
RemoveContainer删除容器runc delete、清理 rootfs
PullImage拉取镜像从 Registry 下载镜像层、解压到 Snapshot
ExecSync在容器中执行命令并等待返回runc exec

2.3 CRI 与 containerd 的内部映射#

containerd 内部通过 CRI 插件(pkg/cri)实现 CRI 接口。每个 CRI 调用在 containerd 内部的映射关系:

// containerd CRI 插件的核心映射(简化)
// RunPodSandbox → 创建 pause 容器
func (c *criService) RunPodSandbox(ctx context.Context, r *runtime.RunPodSandboxRequest) (*runtime.RunPodSandboxResponse, error) {
// 1. 拉取 pause 镜像
image, err := c.ensureImageExists(ctx, sandboxImage, config)
// 2. 创建 containerd container(pause)
container, err := c.containerService.Create(ctx, containerdContainer)
// 3. 创建 task(启动容器进程)
task, err := container.NewTask(ctx, cio.NewCreator())
// 4. 设置网络(CNI)
if config.NetworkConfig.PodCidr != "" {
result, err := c.netPlugin.Setup(ctx, sandboxID, netnsPath)
}
// 5. 启动 task
err = task.Start(ctx)
return &runtime.RunPodSandboxResponse{PodSandboxId: sandboxID}, nil
}

这段代码展示了 containerd 如何把一个 RunPodSandbox CRI 调用翻译成内部操作:拉镜像 → 创建容器 → 创建 task → 配置网络 → 启动。关于 containerd 的完整架构,参见 Ch07 containerd 架构

三、Pod 创建流程:Sandbox → Init Container → 主容器 → Sidecar#

3.1 Pod 创建的完整时序#

当一个 Pod 被调度到节点上,kubelet 通过以下步骤创建它:

sequenceDiagram participant API as API Server participant KL as kubelet participant CRI as CRI Runtime participant CNI as CNI Plugin participant RUNC as runc API->>KL: 1. Pod 分配到节点 KL->>KL: 2. 创建 PodDirectory (/var/lib/kubelet/pods/<uid>) Note over KL,RUNC: 阶段一:创建 Sandbox KL->>CRI: 3. RunPodSandbox CRI->>CRI: 4. 拉取 pause 镜像 CRI->>RUNC: 5. 创建 pause 容器(新 Namespace) RUNC-->>CRI: pause 进程 PID CRI->>CNI: 6. Setup Pod 网络 CNI-->>CRI: Pod IP CRI-->>KL: SandboxID + Pod IP Note over KL,RUNC: 阶段二:创建 Init Container KL->>CRI: 7. PullImage (init container) KL->>CRI: 8. CreateContainer (init, 共享 Sandbox NS) CRI->>RUNC: 9. runc create KL->>CRI: 10. StartContainer CRI->>RUNC: 11. runc start KL->>KL: 12. 等待 Init Container 退出(exit code 0) Note over KL,RUNC: 阶段三:创建主容器和 Sidecar loop 每个容器(按 spec.containers 顺序) KL->>CRI: 13. PullImage KL->>CRI: 14. CreateContainer (共享 Sandbox NS) CRI->>RUNC: 15. runc create KL->>CRI: 16. StartContainer CRI->>RUNC: 17. runc start end KL->>API: 18. Pod Running

3.2 Pod 创建的关键步骤#

步骤操作失败处理
创建 SandboxRunPodSandbox,启动 pause 容器重试,记录事件
配置网络CNI 插件设置 veth pair、路由、iptables清理 Sandbox,重试
运行 Init Container按顺序逐个执行,必须成功退出Pod 失败,根据 restartPolicy 决定是否重启
创建主容器在 Sandbox 的 Namespace 中创建重试
启动主容器并行启动所有主容器(无依赖顺序)记录事件,可能触发重启

3.3 Pod 创建失败时的行为#

spec:
restartPolicy: Always # 总是重启(默认)

Pod 创建过程中任何阶段失败,kubelet 都会记录事件并按照 restartPolicy 决定是否重试。Init Container 的失败尤其需要注意——它会导致整个 Pod 重启,所有 Init Container 从头执行。

四、Pod Sandbox:基础设施容器(pause),共享 Namespace#

4.1 pause 容器的作用#

每个 Pod 启动时,CRI 运行时首先创建一个 pause 容器。pause 容器的镜像极小(约 300KB),它的唯一功能是持有 Namespace——创建 Network Namespace、IPC Namespace,然后无限休眠。

// pause 容器的完整源码(kubernetes/build/pause/linux/pause.c)
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#define STRINGIFY(x) #x
#define VERSION_STRING(x) STRINGIFY(x)
#ifndef VERSION
#define VERSION HEAD
#endif
static void sigdown(int signo) {
psignal(signo, "Shutting down, got signal");
exit(0);
}
static void sigreap(int signo) {
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}
int main(int argc, char **argv) {
int i;
for (i = 1; i < argc; ++i) {
if (!strcmp(argv[i], "-v")) {
printf("pause " VERSION_STRING(VERSION) "\n");
return 0;
}
}
if (getpid() != 1)
/* Not in a container, just sleep forever */
while (1) sleep(10000);
if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
return 1;
if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
return 1;
if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap, .sa_flags = SA_NOCLDSTOP},
NULL) < 0)
return 1;
for (;;)
pause();
fprintf(stderr, "Error: infinite loop terminated\n");
return 42;
}

pause 容器做三件事:

  1. 持有 Namespace:作为 Pod 中所有容器的 Namespace 锚点
  2. 回收僵尸进程:作为 PID 1,调用 waitpid 回收子进程
  3. 信号处理:响应 SIGINT/SIGTERM 优雅退出

4.2 Namespace 共享机制#

Pod 中所有容器共享 pause 容器的 Namespace。这是通过在创建容器时指定 PodSandboxId 实现的——CRI 运行时把 Sandbox 的 Namespace 路径传入 runc create 的配置中:

{
"linux": {
"namespaces": [
{"type": "pid"},
{"type": "ipc", "path": "/proc/3456/ns/ipc"},
{"type": "uts", "path": "/proc/3456/ns/uts"},
{"type": "mount"},
{"type": "network", "path": "/proc/3456/ns/net"}
]
}
}

这里 3456 是 pause 容器的 PID。通过 setns() 系统调用,新容器加入 pause 已有的 IPC、UTS、Network Namespace,但拥有独立的 PID 和 Mount Namespace。

NamespacePod 内共享说明
Network共享所有容器共享同一个 IP 和端口空间
UTS共享所有容器共享同一个 hostname
IPC共享所有容器可以通过 System V IPC 通信
PID独立每个容器有独立的进程树(可配置共享)
Mount独立每个容器有独立的文件系统视图
User独立每个容器有独立的用户映射
Cgroup独立每个容器有独立的 Cgroup(K8s 1.25+ 可配置共享)

4.3 为什么需要 pause 容器#

假设 Pod 中有容器 A 和容器 B。如果没有 pause 容器,容器 A 崩溃重启后,容器 B 的 Network Namespace 会怎样?

  • 没有 pause:容器 A 持有 Network Namespace,A 崩溃后 Namespace 被销毁,容器 B 失去网络
  • 有 pause:pause 持有 Network Namespace,A 崩溃重启后加入 pause 的 Namespace,B 不受影响
crictl ps --name POD
ls -la /proc/$(crictl inspect abc123 | jq '.info.pid')/ns/
crictl inspect def456 | jq '.info.pid'
# 5678
ls -la /proc/5678/ns/net

pause 容器是 Pod 稳定性的基石。只要 pause 不退出,Pod 的 Network/IPC/UTS Namespace 就一直存在,业务容器的崩溃重启不会影响其他容器。

五、多容器模式:Sidecar/Ambassador/Adapter 模式#

5.1 Sidecar 模式#

Sidecar 是 Kubernetes 中最常见的多容器模式。一个辅助容器与主容器共存于同一个 Pod,共享 Network Namespace,通过 localhost 通信。

apiVersion: v1
kind: Pod
metadata:
name: app-with-log-collector
spec:
containers:
- name: app
image: nginx:1.25
volumeMounts:
- name: logs
mountPath: /var/log/nginx
- name: log-collector
image: fluent/fluentd:v1.16
volumeMounts:
- name: logs
mountPath: /var/log/nginx
readOnly: true
volumes:
- name: logs
emptyDir: {}

Sidecar 的典型应用场景:

场景主容器Sidecar通信方式
日志收集业务应用Fluentd/Filebeat共享 Volume
代理/服务网格业务应用Envoy/Istio proxylocalhost 端口
监控采集业务应用Prometheus exporterHTTP localhost
配置热更新业务应用ConfigMap reloader信号 + 共享 Volume

5.2 Ambassador 模式#

Ambassador 容器作为 Pod 与外部服务的代理。主容器通过 localhost 访问 Ambassador,Ambassador 负责与外部服务的连接管理、认证、重试。

apiVersion: v1
kind: Pod
metadata:
name: app-with-db-proxy
spec:
containers:
- name: app
image: myapp:latest
env:
- name: DB_HOST
value: "127.0.0.1" # 连接 Ambassador
- name: DB_PORT
value: "3306"
- name: db-proxy
image: prom/mysqld-exporter:latest

5.3 Adapter 模式#

Adapter 容器对主容器的输出进行标准化转换。主容器输出自有格式的监控数据,Adapter 将其转换为 Prometheus 可以抓取的格式。

apiVersion: v1
kind: Pod
metadata:
name: app-with-metrics-adapter
spec:
containers:
- name: app
image: myapp:latest
- name: metrics-adapter
image: metrics-adapter:latest

5.4 三种模式对比#

模式辅助容器角色与主容器关系典型场景
Sidecar增强/扩展主容器功能主容器感知 Sidecar 存在日志收集、服务网格、监控
Ambassador代理外部服务连接主容器不感知外部服务细节数据库代理、外部 API 代理
Adapter标准化主容器输出主容器不感知输出格式要求监控格式转换、日志格式统一

三种模式的本质都是利用 Pod 内共享 Network Namespace 的特性——容器之间通过 localhost 通信,零网络开销,无需服务发现。

六、Init Container:初始化顺序、与普通容器的区别#

6.1 Init Container 的执行语义#

Init Container 在 Pod 的主容器启动之前按顺序执行,每个 Init Container 必须成功退出(exit code 0)后,下一个才会启动。所有 Init Container 执行完毕,主容器才开始并行启动。

apiVersion: v1
kind: Pod
metadata:
name: app-with-init
spec:
initContainers:
- name: wait-for-db
image: busybox:1.36
command: ['sh', '-c', 'until nc -z db-service 5432; do echo waiting; sleep 2; done']
- name: db-migrate
image: myapp:latest
command: ['sh', '-c', 'python manage.py migrate']
- name: generate-config
image: busybox:1.36
command: ['sh', '-c', 'echo "config from $(hostname)" > /config/app.conf']
volumeMounts:
- name: config
mountPath: /config
containers:
- name: app
image: myapp:latest
command: ['python', 'manage.py', 'runserver']
volumeMounts:
- name: config
mountPath: /etc/app/config
volumes:
- name: config
emptyDir: {}

6.2 Init Container 与普通容器的区别#

特性Init Container普通 Container
执行方式串行,按顺序逐个执行并行,同时启动
退出要求必须成功退出(exit 0)可以长期运行
重启行为失败后整个 Pod 重启,所有 Init 重新执行根据重启策略单独重启
资源请求取所有 Init Container 的最大值各容器独立计算
探针支持不支持 readiness/liveness 探针支持
生命周期Pod 启动阶段一次性执行Pod 运行期间持续运行
Namespace共享 Sandbox Namespace共享 Sandbox Namespace

6.3 Init Container 的资源计算#

Init Container 的资源请求和限制计算方式与普通容器不同:

# init-1: cpu=500m, memory=256Mi
# init-2: cpu=1000m, memory=128Mi
# app: cpu=500m, memory=512Mi
# sidecar: cpu=200m, memory=128Mi

这种计算方式的原因是 Init Container 串行执行,同一时刻只有一个在运行,所以取最大值;而普通容器并行运行,所以求和。调度器使用 Pod 的有效请求来选择节点。

6.4 Sidecar Container(Kubernetes 1.28+)#

Kubernetes 1.28 引入了原生 Sidecar Container,它是一种特殊的 Init Container——启动顺序与 Init Container 相同(串行),但不会因为退出而阻塞后续容器:

spec:
initContainers:
- name: log-collector
image: fluent/fluentd:v1.16
restartPolicy: Always # 关键:标记为 Sidecar
volumeMounts:
- name: logs
mountPath: /var/log
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: logs
mountPath: /var/log
volumes:
- name: logs
emptyDir: {}

restartPolicy: Always 的 Init Container 就是 Sidecar Container。它会在所有普通容器启动前启动,并在整个 Pod 生命周期内持续运行。如果 Sidecar Container 崩溃,它会被自动重启。

七、容器生命周期:PostStart/PreStop 钩子、终止流程#

7.1 容器生命周期钩子#

Kubernetes 为容器定义了两个生命周期钩子:

spec:
containers:
- name: app
image: nginx:1.25
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "echo 'Container started' > /tmp/started"]
preStop:
exec:
command: ["/bin/sh", "-c", "nginx -s quit; while pgrep nginx; do sleep 1; done"]
钩子触发时机用途注意事项
postStart容器创建后立即执行初始化配置、注册服务与容器 ENTRYPOINT 并行执行,不能保证在 ENTRYPOINT 之前完成
preStop容器终止前执行优雅关闭、注销服务同步阻塞,执行完毕后才发送 SIGTERM

7.2 容器终止流程#

flowchart TD A["删除 Pod / 缩容"] --> B["kubelet 调用 preStop 钩子"] B --> C["preStop 执行完毕<br/>(最长 terminationGracePeriodSeconds)"] C --> D["kubelet 调用 CRI StopContainer"] D --> E["CRI 运行时发送 SIGTERM"] E --> F{"容器是否在宽限期内退出?"} F -->|"是"| G["容器退出"] F -->|"否"| H["发送 SIGKILL"] H --> I["容器强制退出"] G --> J["kubelet 调用 CRI RemoveContainer"] I --> J J --> K["清理资源"] style A fill:#ffcdd2,stroke:#c62828 style E fill:#fff3e0,stroke:#e65100 style H fill:#ffcdd2,stroke:#c62828 style G fill:#c8e6c9,stroke:#2e7d32 style K fill:#c8e6c9,stroke:#2e7d32

容器终止的完整时序:

  1. API Server 收到删除 Pod 请求
  2. kubelet 触发 preStop 钩子(同步阻塞)
  3. preStop 执行完毕后,kubelet 调用 StopContainer
  4. CRI 运行时向容器进程发送 SIGTERM
  5. 等待 terminationGracePeriodSeconds(默认 30 秒)
  6. 超时后发送 SIGKILL 强制终止
  7. 容器退出后,kubelet 调用 RemoveContainer 清理
kubectl describe pod my-app
# Events:

7.3 容器状态机#

stateDiagram-v2 [*] --> Waiting: CreateContainer Waiting --> Running: StartContainer Waiting --> Terminated: 创建失败 Running --> Terminated: 退出 / StopContainer Terminated --> Waiting: 重启策略触发重启 Terminated --> [*]: restartPolicy=Never 且退出 state Waiting { [*] --> PullingImage: 拉取镜像 PullingImage --> ImagePulled: 拉取完成 ImagePulled --> CrashLoopBackOff: 上次退出码非0 } state Running { [*] --> Healthy: 健康检查通过 Healthy --> Unhealthy: 健康检查失败 Unhealthy --> Healthy: 健康检查恢复 } state Terminated { [*] --> ExitCode0: 正常退出 [*] --> ExitCodeNon0: 异常退出 [*] --> OOMKilled: 内存不足被杀 [*] --> Error: 执行错误 }

容器在 Kubernetes 中有三种状态:Waiting(等待启动)、Running(运行中)、Terminated(已终止)。每种状态都有原因和消息字段,可以通过 kubectl describe pod 查看。

八、动手实践#

8.1 用 crictl 观察 Pod 创建过程#

kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: demo-pod
spec:
initContainers:
- name: init-config
image: busybox:1.36
command: ['sh', '-c', 'echo "initialized" > /shared/config']
volumeMounts:
- name: shared
mountPath: /shared
containers:
- name: app
image: nginx:1.25
volumeMounts:
- name: shared
mountPath: /usr/share/nginx/html
readOnly: true
- name: sidecar
image: busybox:1.36
command: ['sh', '-c', 'while true; do echo sidecar; sleep 10; done']
volumes:
- name: shared
emptyDir: {}
EOF
crictl pods --name demo-pod
crictl ps -a --pod abc123
PAUSE_PID=$(crictl inspect def456 | jq '.info.pid')
ls -la /proc/$PAUSE_PID/ns/
APP_PID=$(crictl inspect jkl012 | jq '.info.pid')
readlink /proc/$APP_PID/ns/net
readlink /proc/$APP_PID/ns/mnt

8.2 观察 Init Container 的执行顺序#

kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: init-order-demo
spec:
initContainers:
- name: step-1
image: busybox:1.36
command: ['sh', '-c', 'echo "Step 1: $(date)" >> /log/init.log; sleep 3']
volumeMounts:
- name: log
mountPath: /log
- name: step-2
image: busybox:1.36
command: ['sh', '-c', 'echo "Step 2: $(date)" >> /log/init.log; sleep 3']
volumeMounts:
- name: log
mountPath: /log
- name: step-3
image: busybox:1.36
command: ['sh', '-c', 'echo "Step 3: $(date)" >> /log/init.log; sleep 3']
volumeMounts:
- name: log
mountPath: /log
containers:
- name: app
image: busybox:1.36
command: ['sh', '-c', 'cat /log/init.log; sleep 3600']
volumeMounts:
- name: log
mountPath: /log
volumes:
- name: log
emptyDir: {}
EOF
kubectl get pods init-order-demo -w
# init-order-demo 0/1 1/1 Running 0
# 查看执行日志
kubectl logs init-order-demo app
# Step 1: Sat Apr 25 10:00:01 UTC 2026
# Step 2: Sat Apr 25 10:00:04 UTC 2026
# Step 3: Sat Apr 25 10:00:07 UTC 2026

8.3 观察容器终止流程#

# 创建一个带 preStop 钩子的 Pod
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: graceful-shutdown
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
image: nginx:1.25
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "echo 'Graceful shutdown started' && nginx -s quit && sleep 10 && echo 'Graceful shutdown completed'"]
EOF
# 等待 Pod 运行
kubectl wait --for=condition=Ready pod/graceful-shutdown
# 删除 Pod 并观察事件
kubectl delete pod graceful-shutdown &
kubectl get events --field-selector involvedObject.name=graceful-shutdown -w
# 你会看到 preStop 钩子执行、SIGTERM 发送、容器退出的完整过程
# 对比:不带 preStop 的容器收到 SIGTERM 后可能直接退出
# Nginx 默认处理 SIGTERM 的方式是 worker 进程在处理完当前请求后退出

8.4 检查 CRI 运行时信息#

# 查看 containerd 版本和 CRI 支持情况
crictl info | jq '.config.cni'
crictl info | jq '.config.containerd.runtimes'
# {
# "runc": {
# "runtimeType": "io.containerd.runc.v2",
# "options": {"BinaryName": "/usr/bin/runc"}
# },
# "runsc": {
# "runtimeType": "io.containerd.runsc.v1",
# "options": {"BinaryName": "/usr/bin/runsc"}
# }
# }
# 查看容器运行时详情
kubectl get nodes -o jsonpath='{.items[0].status.nodeInfo.containerRuntimeVersion}'
# containerd://1.7.8
# 查看容器日志(通过 crictl)
CONTAINER_ID=$(crictl ps --name app -q | head -1)
crictl logs $CONTAINER_ID
# 在容器中执行命令
crictl exec $CONTAINER_ID cat /etc/hostname

九、本章小结#

上一章理解了Wasm 容器运行时。

本章从 Kubernetes 的视角审视了容器运行时的工作方式,核心要点:

主题核心要点关键词
CRI 接口kubelet 通过 RuntimeServiceImageService 两个 gRPC 服务与容器运行时交互,RunPodSandboxCreateContainerStartContainer 是三个最关键的调用RuntimeService、ImageService、gRPC
Pod Sandboxpause 容器持有 Pod 的 Network/IPC/UTS Namespace,是 Pod 稳定性的基石——只要 pause 不退出,业务容器的崩溃重启不影响其他容器pause 容器、Namespace 锚点、僵尸回收
Pod 创建流程Sandbox → Init Container(串行) → 主容器(并行),每个阶段失败有不同的处理策略RunPodSandbox、串行/并行、restartPolicy
多容器模式Sidecar、Ambassador、Adapter 三种模式都利用 Pod 内共享 Network Namespace 的特性,通过 localhost 通信Sidecar、Ambassador、Adapter、localhost
Init Container串行执行、必须成功退出、失败后整个 Pod 重启;Kubernetes 1.28+ 的原生 Sidecar Container 通过 restartPolicy: Always 标记串行、exit 0、Sidecar Container
容器生命周期postStart/preStop 钩子、SIGTERM/SIGKILL 终止流程、terminationGracePeriodSeconds 宽限期postStart、preStop、SIGTERM、宽限期
Warning

Pod 中共享 Network Namespace 意味着端口冲突是真实风险——两个容器不能监听同一个端口。在设计多容器 Pod 时,务必规划好端口分配。此外,共享 IPC Namespace 可能导致 System V IPC 资源泄漏,建议只在明确需要时启用 shareProcessNamespace

这些知识将前几章的容器运行时原理与 Kubernetes 的编排实践连接起来。Ch09 容器完整流程追踪了 docker run 的完整调用链,本章则追踪了 Pod 创建的完整调用链——两者在底层都是 runc 创建 Namespace、配置 Cgroup、挂载 OverlayFS,区别在于 Kubernetes 通过 CRI 和 Sandbox 机制增加了 Pod 级别的编排语义。Ch07 containerd 架构介绍了 containerd 的内部设计,本章展示了 kubelet 如何通过 CRI 驱动 containerd。Ch10 容器安全中的 seccomp/AppArmor/Capabilities 同样适用于 Kubernetes 中的容器——Pod 的 securityContext 就是这些安全机制的 K8s 抽象。


参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

容器在 Kubernetes 中
https://blog.souloss.com/posts/container-runtime/containers-in-k8s/
作者
Souloss
发布于
2026-06-16
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
容器网络
容器运行时 容器网络的核心问题是——隔离的 Network Namespace 如何与外部通信?详细解读 veth pair(虚拟网卡对)、bridge(虚拟网桥)、iptables/NAT(地址转换)、CNI(容器网络接口)的完整链路,以及 Docker 的四种网络模式和 Kubernetes 的 Pod 网络模型——从「容器能 ping 通外网」到「理解每一条网络规则」。
2
容器存储
容器运行时 容器的可写层随容器删除而丢失,数据持久化需要 Volume。一网打尽容器存储的完整方案——Volume(绑定挂载/命名卷/tmpfs)、存储驱动(overlay2/devicemapper/btrfs)、CSI(容器存储接口)插件机制,以及 Kubernetes 的 PV/PVC/StorageClass 体系——从「docker run -v」到「理解容器存储的每一条挂载规则」。
3
Wasm 容器:WasmEdge/WASI
容器运行时 WebAssembly(Wasm)正在从浏览器走向服务器——WASI(WebAssembly System Interface)定义了 Wasm 访问操作系统的标准接口,WasmEdge/runwasi 等运行时让 Wasm 模块可以作为容器运行。详细解读 Wasm 容器的架构、WASI 接口、与 OCI 的集成、与 Linux 容器的对比——从「Wasm 只能在浏览器运行」到「Wasm 是下一代容器运行时」。
4
OverlayFS:容器文件系统
容器运行时 OverlayFS 是容器分层文件系统的核心。全面剖析 OverlayFS 的 lowerdir/upperdir/workdir 三层结构、whiteout 标记文件机制、copy-up 写时复制语义、多层叠加规则,以及容器运行时如何用 OverlayFS 实现镜像层复用和容器可写层——理解「为什么容器启动这么快」和「镜像层是怎么共享的」。
5
容器安全:seccomp/AppArmor/Capabilities
容器运行时 容器的安全边界在哪里?Namespace 提供视图隔离,但不阻止特权操作。从零讲透 Linux 安全模块在容器中的应用——Capabilities(权限细分)、seccomp(系统调用过滤)、AppArmor(文件访问控制),以及 rootless 容器的实现,让你理解容器的安全边界和加固方法。