Kubernetes 基础体系 · 第 12/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes CRI、CNI 与 CSI:运行时、网络、存储插件责任边界

Kubernetes 并不直接实现“启动容器、配置 Pod 网络、挂载云盘”这三类具体能力,而是分别通过三套接口与外部组件协作:

  • CRI(Container Runtime Interface):Kubelet 与容器运行时之间的接口,负责 Pod Sandbox、容器、镜像、日志、执行命令和运行时状态。
  • CNI(Container Network Interface):容器运行时为网络命名空间配置网络的插件接口,负责网卡、IP、路由和网络资源清理。
  • CSI(Container Storage Interface):Kubernetes 存储控制器与存储插件之间的接口,负责卷的创建、附加、格式化、挂载、发布、扩容和快照等能力。

三者经常被并列称为 Kubernetes 的“插件接口”,但它们解决的问题不同:

CRI进程如何运行\text{CRI} \rightarrow \text{进程如何运行}

CNI进程所在网络命名空间如何联网\text{CNI} \rightarrow \text{进程所在网络命名空间如何联网}

CSI进程如何获得持久化存储\text{CSI} \rightarrow \text{进程如何获得持久化存储}

这个关系不是严格的串行调用链。CRI 是 Kubelet 与运行时的直接边界;CNI 通常由容器运行时在创建 Pod Sandbox 时调用;CSI 则主要由 Kubernetes 控制器和 Kubelet 的存储管理路径调用。三者共同影响一个 Pod,但调用方、状态来源和故障表现都不同。


一、先建立对象模型:Pod、Sandbox、容器、网络命名空间和卷

1. Pod 不是一个容器

Kubernetes 的 Pod 是一组共享部分运行环境的容器。至少有三个概念需要区分:

  1. Pod 对象:存储在 API Server 中的声明,例如镜像、命令、卷和资源请求。
  2. Pod Sandbox:容器运行时为 Pod 建立的运行环境,通常对应一个网络命名空间和一个 pause/infra 容器。
  3. 业务容器:Pod 中真正运行应用的容器。

典型结构如下:

Pod
├── Pod Sandbox
│   ├── Network Namespace
│   ├── Pod IP
│   └── pause/infra container
├── app container
└── sidecar container

业务容器通常加入 Sandbox 已经创建好的网络命名空间,因此同一个 Pod 内的容器共享 Pod IP 和端口空间。两个容器不能分别监听同一个端口,即使它们是不同的容器;这是共享网络命名空间的直接结果。

2. Pod Sandbox 为什么存在

如果每个业务容器都独立创建网络命名空间,Pod 内多个容器就无法自然地共享同一个 IP。Sandbox 把 Pod 级别的网络生命周期从业务容器生命周期中分离出来:

  1. 创建 Sandbox;
  2. 创建 Sandbox 的网络命名空间;
  3. 为该网络命名空间配置 Pod IP 和路由;
  4. 创建业务容器并让它们加入该命名空间;
  5. 业务容器退出时,Sandbox 仍可暂时存在;
  6. Sandbox 被删除时,才清理整个 Pod 级网络资源。

这也是为什么“容器启动失败”和“Pod 网络创建失败”可能是两个独立问题。


二、CRI:Kubelet 与容器运行时的责任边界

1. CRI 是什么

CRI 是 Kubelet 调用容器运行时的 gRPC 接口。它主要分为两个服务:

  • RuntimeService:管理 Pod Sandbox、容器、执行命令、日志、状态和统计信息;
  • ImageService:拉取、删除、列出镜像并查询镜像信息。

逻辑关系可以表示为:

Kubelet
  │
  │ CRI gRPC
  ▼
Container Runtime
  ├── containerd
  ├── CRI-O
  └── 其他实现 CRI 的运行时

Kubelet 不应该依赖某个具体运行时的内部目录或私有命令。它通过 CRI 请求运行时完成操作,例如:

RunPodSandbox
CreateContainer
StartContainer
StopContainer
RemoveContainer
PullImage
ExecSync
ContainerStatus
PodSandboxStatus

CRI 定义的是 Kubernetes 需要的抽象,而不是运行时内部必须如何实现。containerd、CRI-O 可以使用不同的内部组件、存储布局和监控方式,只要对 Kubelet 提供兼容的 CRI 行为即可。

2. CRI 的典型启动流程

一个 Pod 从 API Server 中出现到容器真正运行,简化后经过以下步骤:

sequenceDiagram
    participant A as API Server
    participant K as Kubelet
    participant R as CRI Runtime
    participant C as CNI Plugin
    participant I as Image Registry

    A->>K: PodSpec
    K->>R: RunPodSandbox
    R->>C: ADD(network namespace, config)
    C-->>R: Pod IP, routes, interfaces
    R-->>K: Sandbox ID

    K->>R: PullImage
    R->>I: Pull image
    I-->>R: Image layers
    R-->>K: Image available

    K->>R: CreateContainer
    K->>R: StartContainer
    R-->>K: Container running

实际实现可能有更多检查、重试和事件,但因果顺序通常是:

  1. Kubelet 根据 PodSpec 创建 Sandbox;
  2. 运行时建立 Pod 的网络命名空间;
  3. 运行时配置 Pod 网络;
  4. Kubelet 确认 Sandbox 可用;
  5. 拉取镜像;
  6. 创建并启动业务容器;
  7. Kubelet 持续检查状态、探针和资源信息。

3. CRI 与 CNI 的连接点

在典型实现中,Kubelet 调用:

RunPodSandbox()

容器运行时随后调用 CNI 插件,为 Sandbox 的网络命名空间配置网络。

因此,严格来说:

Kubelet ──CRI──> Runtime ──CNI──> Network Plugin

而不是:

Kubelet ──直接──> CNI

不同运行时的内部调用路径可能有差异,但 Kubernetes 依赖的边界是 CRI;Kubelet 不需要知道 CNI 二进制如何执行。

4. CRI 的生命周期与幂等性

Kubelet 是声明式控制循环,而不是只执行一次的安装脚本。它会反复观察实际状态,并尝试使实际状态接近期望状态。

例如,Kubelet 发现 Pod 需要一个容器:

期望状态:容器 running
实际状态:容器不存在

它可能执行:

PullImage
CreateContainer
StartContainer

如果在 StartContainer 后 Kubelet 进程重启,恢复后不会简单地再创建一个重复容器,而是通过 Sandbox 和 ContainerStatus 等接口重新发现已有对象,并根据状态继续协调。

这要求运行时接口具备可重复调用的语义。实际是否严格幂等,取决于 CRI 实现和具体调用;Kubelet 也会通过状态查询、ID 和错误处理避免无条件重复创建。

5. PLEG、Probe 和资源状态属于哪一层

PLEG

PLEG,即 Pod Lifecycle Event Generator,是 Kubelet 内部用于感知容器运行时状态变化的机制。它不是 CRI,也不是独立插件。

粗略的数据流是:

Runtime 状态
    ↓
Kubelet 通过 CRI 查询或接收事件
    ↓
PLEG 生成 Pod 生命周期事件
    ↓
Pod Sync Loop 重新协调 Pod

如果 PLEG 长时间没有更新,常见表现是:

NodeNotReady
Container runtime is down
PLEG is not healthy

这通常指向运行时响应慢、运行时卡死、节点负载过高或 Kubelet 与运行时通信异常,而不是直接证明 CNI 或 CSI 有问题。

Probe

Liveness、Readiness 和 Startup Probe 是 Kubelet 对容器执行的健康检查:

  • Startup Probe:应用是否完成启动;
  • Readiness Probe:是否应接收 Service 流量;
  • Liveness Probe:是否需要重启容器。

Probe 失败通常由 Kubelet 触发容器重启或更新就绪状态,不是 CRI 运行时决定探针语义。Kubelet 可能通过 CRI 的 ExecSync 执行 exec probe,也可能由 Kubelet 自己发起 HTTP/TCP 检查。

例如,应用因为数据库连接失败而 Readiness Probe 失败:

容器仍然 Running
Pod 可能是 Running
Pod 不一定 Ready
Service EndpointSlice 通常不会把它作为就绪后端

这与容器进程崩溃导致的 CrashLoopBackOff 不同。前者是健康状态问题,后者是进程和重启策略问题。

资源状态

Kubelet 从容器运行时和节点系统收集 CPU、内存、文件系统等信息,并将部分状态用于:

  • Pod 和容器状态;
  • 驱逐决策;
  • 节点条件;
  • Metrics 相关组件的数据来源。

CRI 提供运行时可见的容器统计接口,但完整资源观测还涉及 cgroup、节点文件系统、内核和 kubelet 自身。不能把所有资源指标都归因于 CRI。


三、CRI 不负责什么

明确边界比记住接口名称更重要。

1. CRI 不负责调度

Scheduler 决定 Pod 被放到哪个节点。CRI 只在 Pod 已经被调度到某个节点后参与运行。

2. CRI 不负责 Service 和集群路由

Service、EndpointSlice、kube-proxy 或 eBPF 数据面负责服务发现和服务转发。CRI 只负责运行时层面的容器和 Sandbox 生命周期。

3. CRI 不定义网络插件算法

运行时可以调用 CNI,但 CRI 不规定:

  • Pod IP 从哪个地址池分配;
  • 是否使用 Overlay;
  • 是否使用 Underlay;
  • 是否由 eBPF 转发;
  • NetworkPolicy 如何执行。

这些属于 CNI 插件及其数据面实现。

4. CRI 不负责持久卷生命周期

容器可以看到一个挂载后的目录,但“云盘如何创建、卷如何附加、设备如何格式化”属于 CSI 和存储系统。运行时只负责按照 Kubelet 传入的容器配置启动进程,并不理解 PVC 的业务语义。


四、CNI:为网络命名空间配置 Pod 网络

1. CNI 的基本模型

CNI 是一组面向容器网络配置的规范。运行时创建或删除网络命名空间时,调用 CNI 插件完成网络操作。

抽象调用如下:

ADD:
  输入:网络命名空间、容器 ID、网络配置、运行时上下文
  输出:接口、IP、路由、DNS 等网络结果

DEL:
  输入:同一个容器和网络上下文
  输出:删除网络资源

CHECK:
  输入:已有网络状态
  输出:检查结果

具体插件通常包括:

  • 主网络插件:创建 veth、虚拟接口、路由或其他数据面;
  • IPAM 插件:分配和回收 IP;
  • 其他 meta 插件:链路、带宽、端口映射或多网络编排。

CNI 的核心目标是“配置网络”,不是管理 Kubernetes API 中的 Pod 状态。

2. 一个 Pod 网络是如何建立的

以最常见的 veth 模型为例:

Node Network Namespace
┌────────────────────────────────────────┐
│  host-side veth ── bridge / dataplane  │
└──────────────┬─────────────────────────┘
               │ veth pair
┌──────────────▼─────────────────────────┐
│ Pod Network Namespace                  │
│  eth0: Pod IP                          │
│  default route                         │
└────────────────────────────────────────┘

创建过程可以拆成以下步骤:

  1. 运行时创建 Pod Network Namespace;
  2. CNI 创建一对 veth;
  3. 一端放入 Pod 命名空间并命名为 eth0
  4. 另一端留在节点网络命名空间;
  5. IPAM 分配 Pod IP;
  6. CNI 为 eth0 配置 IP;
  7. CNI 配置默认路由和必要的邻居、规则;
  8. 插件将网络结果返回给运行时;
  9. 运行时把该 Sandbox 作为业务容器的网络环境。

eth0 只是常见结果,不是所有 CNI 实现必须采用的内部方式。

3. Pod IP、节点 IP 和 Service IP 不是同一个概念

假设:

Node IP:    192.168.10.20
Pod IP:     10.244.2.17
Service IP: 10.96.0.10

它们的职责不同:

  • Node IP:节点对外通信和节点级组件使用的地址;
  • Pod IP:Pod 网络命名空间中的地址,通常由 CNI 分配;
  • Service IP:虚拟服务地址,由 Service 数据面转发到后端 Pod。

当应用监听 0.0.0.0:8080 时,它通常监听的是 Pod 网络命名空间中的所有地址,而不是直接监听节点地址。访问 Node IP 是否能到达 Pod,还取决于 Service 类型、端口映射、路由和数据面规则。

4. Overlay 与 Underlay 的区别

Overlay

Overlay 使用额外的封装网络承载 Pod 流量。例如节点之间传输的是封装后的包:

原始包:Pod A IP → Pod B IP
封装后:Node A IP → Node B IP
         [外层头部][内层 Pod 包]

优点是可以在底层网络不理解 Pod 网段时构建独立的 Pod 网络。代价包括:

  • 封装带来额外头部;
  • MTU 需要扣除封装开销;
  • 故障诊断同时涉及内层和外层;
  • 节点 CPU、网络设备和流量路径可能受到影响。

Underlay

Underlay 让 Pod 地址直接使用底层网络可路由的地址,或让底层网络显式学习 Pod 网段。

优点是路径更直接、封装更少。代价是:

  • 需要底层网络支持 Pod 地址;
  • IP 地址规划、路由发布和安全边界更复杂;
  • 云厂商 VPC、子网和安全组规则可能成为限制。

Overlay 和 Underlay 不是“谁一定更好”的关系,而是网络可达性、地址规划、MTU、运维能力和云平台限制之间的取舍。

5. IPAM 只是 CNI 的一个组成部分

CNI 主插件负责把网络接入 Pod,但 IP 地址从哪里来通常由 IPAM 插件管理。

典型过程为:

主插件请求 IPAM
    ↓
IPAM 从地址池分配 10.244.2.17
    ↓
主插件配置接口和路由
    ↓
删除 Pod 时 IPAM 回收 10.244.2.17

如果 IPAM 状态丢失或回收失败,可能出现:

  • Pod 创建失败并提示地址池耗尽;
  • Pod 删除后地址没有回收;
  • 节点重启后地址状态与实际接口不一致;
  • 同一地址被错误复用。

因此,“Pod 网络不通”不一定是路由问题,也可能是 IPAM 分配、回收或状态文件问题。

6. NetworkPolicy 不等于 CNI 的基础联网

NetworkPolicy API 描述允许和拒绝规则,但具体执行必须由支持 NetworkPolicy 的网络实现完成。仅仅安装一个能分配 Pod IP 的 CNI,不代表 NetworkPolicy 一定生效。

例如:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: only-from-frontend
  namespace: app
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              purpose: frontend

这份对象由 API Server 接收,控制器或网络数据面读取它,但是否真正阻断流量取决于网络插件是否实现相应策略。对象存在而流量未被阻断,并不能证明 Kubernetes API 失效,可能是 CNI 数据面不支持该能力或配置不完整。


五、CNI 的失败路径与诊断方法

1. Sandbox 创建失败

如果 CNI ADD 失败,常见状态是:

Pod phase: Pending
事件:FailedCreatePodSandBox

这时业务容器通常还没有启动。典型原因包括:

  • CNI 配置文件缺失或格式错误;
  • CNI 二进制不存在或无执行权限;
  • IPAM 地址池耗尽;
  • 节点网络插件 DaemonSet 未运行;
  • 内核模块、路由或 eBPF 前置条件不满足;
  • Pod 网卡创建成功但路由配置失败;
  • Overlay MTU 或封装参数错误。

应先检查 Pod 事件和节点上的运行时日志:

kubectl describe pod <pod> -n <namespace>

kubectl get pods -n kube-system -o wide

crictl pods
crictl ps -a

kubectl describe pod 中如果持续出现 FailedCreatePodSandBox,优先检查 CRI 到 CNI 的调用链,而不是先看应用日志,因为应用容器可能尚未存在。

2. 网络命名空间存在但业务不通

如果容器已经运行,问题可能发生在 CNI 之后:

  • Service 规则未生成;
  • NetworkPolicy 阻断;
  • Pod 到节点有路由,但跨节点路由缺失;
  • 云安全组或底层 ACL 阻断;
  • DNS 配置错误;
  • MTU 导致大包或特定协议失败;
  • 应用只监听 127.0.0.1
  • 端口没有被应用监听。

基础检查示例:

kubectl exec -n <namespace> <pod> -- ip addr
kubectl exec -n <namespace> <pod> -- ip route
kubectl exec -n <namespace> <pod> -- cat /etc/resolv.conf
kubectl get pod -n <namespace> -o wide

镜像没有 ipcat 时,命令本身失败并不意味着网络故障。应使用临时调试容器、节点网络命名空间检查或网络插件提供的诊断工具。

3. CNI DEL 失败

删除 Pod 时,运行时应调用 CNI DEL 清理接口、IP 和路由。如果 DEL 失败,可能产生:

  • 残留 veth;
  • IP 地址泄漏;
  • 网络命名空间或插件状态残留;
  • 节点后续创建 Pod 时地址或接口冲突。

强制删除 Pod 可能让 API 对象迅速消失,但不保证底层网络资源已经被正确清理。生产环境中应先确认节点和插件状态,再决定是否手工清理,避免同时存在多个控制路径修改网络状态。


六、CSI:从 PVC 声明到卷挂载

1. CSI 解决什么问题

CSI 将存储系统的操作抽象成标准接口。Kubernetes 不需要为每种云盘、SAN、分布式文件系统实现一套内置逻辑,而是通过 CSI 驱动调用外部存储系统。

CSI 驱动通常由两类组件组成:

CSI Controller Plugin
  ├── CreateVolume
  ├── DeleteVolume
  ├── ControllerPublishVolume
  ├── ControllerUnpublishVolume
  ├── ValidateVolumeCapabilities
  └── CreateSnapshot / DeleteSnapshot 等

CSI Node Plugin
  ├── NodeStageVolume
  ├── NodeUnstageVolume
  ├── NodePublishVolume
  ├── NodeUnpublishVolume
  └── NodeGetInfo / NodeGetVolumeStats 等

Kubernetes 还会部署一组 sidecar 容器,例如:

  • external-provisioner;
  • external-attacher;
  • external-resizer;
  • external-snapshotter;
  • livenessprobe。

sidecar 负责观察 Kubernetes API 对象并调用 CSI 驱动。sidecar 不是 CSI 规范本身,但它们是 Kubernetes 中常见的集成实现。

2. 动态供给的完整路径

下面以动态创建块存储卷为例。

用户提交:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data
  namespace: demo
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: fast
  resources:
    requests:
      storage: 10Gi

再提交使用该 PVC 的 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: app
  namespace: demo
spec:
  containers:
    - name: app
      image: nginx:stable
      volumeMounts:
        - name: data
          mountPath: /var/lib/data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: data

过程不是“创建 PVC 就立刻挂载磁盘”,而是多个阶段:

sequenceDiagram
    participant U as User
    participant A as API Server
    participant P as external-provisioner
    participant D as CSI Controller
    participant S as Storage Backend
    participant K as Kubelet
    participant N as CSI Node Plugin

    U->>A: Create PVC
    A-->>P: PVC watch event
    P->>D: CreateVolume
    D->>S: Create volume
    S-->>D: Volume ID
    D-->>P: Volume created
    P->>A: Create PV / bind PVC

    U->>A: Create Pod
    A-->>K: Pod assigned to node
    K->>A: Observe PVC/PV
    K->>D: ControllerPublishVolume
    D->>S: Attach volume to node
    S-->>D: Device available
    D-->>K: Volume attached

    K->>N: NodeStageVolume
    N->>N: Format and mount to staging path
    K->>N: NodePublishVolume
    N->>N: Bind mount into Pod path
    K->>K: Start container through CRI

其中最后一步很重要:CSI 挂载完成后,Kubelet 才能让容器运行时创建一个包含该挂载的容器。

3. Controller 和 Node 的区别

Controller 侧

Controller 侧面向存储系统和节点级附加操作,通常负责:

  • 创建或删除卷;
  • 将卷附加到节点;
  • 从节点解除附加;
  • 扩展控制器侧卷容量;
  • 创建和删除快照。

它不应该直接进入每个 Pod 的网络命名空间或容器文件系统。

Node 侧

Node 插件运行在具体节点上,负责:

  • 识别块设备或远端文件系统;
  • 格式化卷;
  • 将卷挂载到节点 staging 目录;
  • 将 staging 目录发布到 Pod 目录;
  • 在 Pod 删除时解除发布和卸载。

常见的两阶段挂载关系为:

设备或远端卷
    ↓ NodeStageVolume
节点 staging path
    ↓ NodePublishVolume
Pod volume path
    ↓
容器内 /var/lib/data

NodeStageVolume 常用于在节点上完成一次设备级挂载,多个 Pod 可能再通过 NodePublishVolume 使用该 staging 路径。是否能被多个 Pod 使用,仍受访问模式和驱动能力约束。

4. Attach、Mount 和 Publish 不是同一个动作

以块存储为例:

  • Attach:云平台把云盘连接到某个节点,节点可能出现 /dev/... 设备;
  • Stage/Mount:CSI Node 插件识别设备、格式化并挂载到节点目录;
  • Publish:把节点上的卷路径绑定挂载到某个 Pod 的目录;
  • Container start:CRI 创建容器时使用这个已准备好的挂载点。

如果 Pod 卡在:

FailedAttachVolume

问题通常在控制器、云平台附加权限、卷状态或节点绑定关系。

如果出现:

FailedMount

问题更可能在 CSI Node 插件、设备识别、文件系统、挂载参数或节点内核。

如果挂载成功但应用无法写入,则还要检查:

  • 文件系统只读;
  • UID/GID 和权限;
  • fsGroup 处理;
  • SELinux/AppArmor;
  • 容器内路径是否正确;
  • 应用是否使用了错误的子路径。

5. 访问模式不是存储能力的实现

PVC 中的访问模式表达工作负载要求,例如:

  • ReadWriteOnce:卷可被一个节点以读写方式使用;
  • ReadOnlyMany:多个节点可只读使用;
  • ReadWriteMany:多个节点可读写使用;
  • ReadWriteOncePod:限制为单个 Pod 使用,具体支持依赖版本和驱动能力。

访问模式不是把块设备自动变成共享文件系统。一个普通云块盘即使 Kubernetes 对象写了 ReadWriteMany,如果底层设备和 CSI 驱动不支持多节点并发读写,仍然不能安全使用。

反例:

节点 A:把同一个普通块盘格式化并挂载为 ext4
节点 B:也把同一块盘挂载为 ext4 并读写

这通常会造成文件系统损坏。Kubernetes 不会把不具备共享语义的块设备变成安全的分布式文件系统。


七、CSI 快照、扩容和故障恢复

1. 快照不是 PV 的简单复制

Kubernetes 的 VolumeSnapshot 使用快照相关 API 对象表达快照请求。实际快照由 CSI 驱动和存储后端完成。

基本关系是:

VolumeSnapshot
    ↓
VolumeSnapshotContent
    ↓
存储系统中的 snapshot ID

从快照恢复 PVC 时,控制器向 CSI 驱动请求基于快照创建新卷。快照的一致性取决于:

  • 存储系统是否支持崩溃一致快照;
  • 应用是否在快照前暂停写入;
  • 数据库是否完成 flush 或使用专用备份机制;
  • CSI 驱动是否正确实现快照语义。

因此,“快照成功”不自动等于“应用级备份成功”。

2. 扩容分为控制器和节点阶段

卷扩容通常包括:

控制器扩容:
  存储后端容量变大

节点扩容:
  文件系统或设备在节点上识别新的容量

如果只完成后端扩容,应用容器内看到的文件系统容量可能仍未增加。必须确认 CSI 驱动、文件系统和 Kubernetes 版本共同支持对应流程。

3. 节点故障时的 Attach 风险

假设节点 A 失联,但云平台仍认为卷附着在节点 A:

Pod 被重新调度到节点 B
ControllerPublishVolume(B)
    ↓
云平台拒绝:volume already attached to A

这不是 Kubelet 单独能够解决的问题。控制器、CSI 驱动和云平台需要确认旧节点上的卷已经安全解除附加。强行 detach 可能导致数据未刷盘、文件系统损坏或双写。

生产环境必须把以下事实区分开:

  • Kubernetes API 中 Pod 是否已删除;
  • Kubelet 是否已停止容器;
  • 节点是否仍可访问;
  • 存储平台是否已完成 detach;
  • 文件系统是否允许在新节点重新挂载。

删除 Pod 对象不等于存储设备已经安全释放。


八、三套接口如何共同影响一个 Pod

一个带网络和持久卷的 Pod,大致经过如下路径:

flowchart LR
    API[Pod/PVC/PV API 对象]
    S[Scheduler]
    K[Kubelet]
    CRI[CRI Runtime]
    CNI[CNI Plugin]
    CSIc[CSI Controller]
    CSIn[CSI Node Plugin]
    NET[Pod Network Namespace]
    VOL[Mounted Volume]
    APP[Application Container]

    API --> S
    S --> API
    API --> K

    K --> CSIc
    CSIc --> VOL
    K --> CSIn
    CSIn --> VOL

    K --> CRI
    CRI --> CNI
    CNI --> NET
    K --> CRI
    VOL --> APP
    NET --> APP
    CRI --> APP

更严格地看,卷准备通常发生在容器启动前:

Pod 被调度
  ↓
Kubelet 发现卷需求
  ↓
CSI Attach/Stage/Publish
  ↓
CRI RunPodSandbox
  ↓
CNI ADD
  ↓
CRI CreateContainer/StartContainer

具体顺序可能因卷类型、运行时和 Kubelet 内部协调细节有所不同,但应用容器必须在其所需的网络和卷环境准备好后才能正常启动。

三者的状态来源也不同:

能力 主要调用方 主要状态 典型失败事件
CRI Kubelet Sandbox、Container、Image 状态 CreateContainerErrorCrashLoopBackOff
CNI Runtime 调用网络插件 网卡、IP、路由、网络命名空间 FailedCreatePodSandBox
CSI Controller 控制器/sidecar Volume、Attachment、Snapshot ProvisioningFailedFailedAttachVolume
CSI Node Kubelet 调用 Node 插件 设备、staging、publish、mount FailedMountFailedUnmount

九、一个完整的故障定位顺序

情况一:Pod 一直 Pending,且没有 Pod IP

先执行:

kubectl get pod -n demo app -o wide
kubectl describe pod -n demo app

若事件类似:

Failed to create pod sandbox:
failed to setup network for sandbox ...

应沿以下路径排查:

Kubelet
  → CRI Runtime socket
  → CNI 配置
  → CNI 二进制
  → IPAM
  → 节点路由/内核

此时不应先检查应用进程,因为业务容器可能根本没有被创建。

情况二:Pod 有 IP,但 PVC 未挂载

检查:

kubectl get pvc,pv -n demo
kubectl describe pod -n demo app
kubectl get volumeattachment

诊断分层:

  • PVC 未绑定:看 StorageClass、provisioner 和配额;
  • Attach 失败:看 CSI Controller、云平台卷状态和权限;
  • Mount 失败:看 CSI Node、节点设备和文件系统;
  • 容器内目录为空:检查 volumeMounts、路径和子路径。

情况三:Pod Running 但应用不可访问

此时 CRI、CNI、CSI 都可能已经“部分成功”:

  1. 容器进程是否真的监听目标端口;
  2. Readiness Probe 是否成功;
  3. Pod IP 是否可达;
  4. Service selector 是否选中了 Pod;
  5. EndpointSlice 是否包含该 Pod;
  6. NetworkPolicy 是否允许流量;
  7. CNI 跨节点路径和 MTU 是否正确;
  8. 应用是否读取到了正确的卷路径和数据。

“Pod 是 Running”只表示容器进程处于运行状态,不保证网络可达、服务就绪或存储内容正确。


十、常见误解与反例

误解一:安装 containerd 就等于安装了 Kubernetes 网络

不成立。containerd 是容器运行时的一种实现。它通常需要可用的 CRI 集成,并在创建 Sandbox 时调用配置好的 CNI。没有正确的 CNI 配置时,镜像可能能够拉取,但 Pod Sandbox 仍然创建失败。

误解二:Pod IP 由 Kubelet 直接分配

通常不成立。Kubelet 通过 CRI 请求运行时创建 Sandbox,运行时再调用 CNI 和 IPAM。Kubelet 会观察并报告 Pod 网络结果,但不负责实现地址池分配算法。

误解三:PVC 已经 Bound 就代表容器已经可以读写

不成立。PVC Bound 表示 PVC 与 PV 已建立绑定关系,不代表:

  • 卷已附加到目标节点;
  • 设备已被识别;
  • 文件系统已挂载;
  • 卷已发布到 Pod;
  • 应用用户有读写权限。

误解四:CSI 驱动只负责创建磁盘

不完整。动态供给只是 CSI 的一部分。一个可用的块存储 CSI 集成还可能涉及 Attach、Detach、Stage、Publish、Unmount、扩容、快照和节点故障恢复。

误解五:删掉 Pod 就一定清理网络和磁盘

不保证立即完成。删除动作会触发异步控制循环:

删除 Pod
  ↓
Kubelet 停止容器
  ↓
CRI 删除容器和 Sandbox
  ↓
CNI DEL 清理网络
  ↓
CSI Node Unpublish/Unstage
  ↓
CSI Controller Unpublish

任何一步失败,都可能暂时留下网络接口、挂载点或云盘附着状态。应观察事件、控制器日志和节点实际状态,而不是只看 API 对象是否消失。


十一、版本和实现边界

1. CRI 版本

Kubernetes 已移除内置 dockershim;较新的 Kubernetes 集群需要使用实现 CRI 的运行时,例如 containerd 或 CRI-O。CRI 的 API 版本、运行时版本和 Kubernetes 版本必须兼容,不能仅凭“能启动进程”判断兼容性。

诊断运行时可以使用:

crictl info
crictl version
crictl pods
crictl ps -a

crictl 的 endpoint 配置必须指向实际 CRI socket。把它指向普通 containerd socket、错误的 CRI-O socket,或在多运行时节点上选错 endpoint,都会产生误导性结果。

2. CNI 版本和插件能力

CNI 规范版本、具体网络插件版本与 Kubernetes 版本是三个不同维度。插件是否支持:

  • NetworkPolicy;
  • 多网络;
  • IPv6 或双栈;
  • Windows;
  • eBPF 数据面;
  • 特定 MTU 或云网络模式;

不能只根据“它是 CNI 插件”推断。云厂商托管 Kubernetes 还可能把 CNI 与 VPC、ENI、路由表和安全组绑定,升级或切换插件的风险高于修改一个普通 DaemonSet。

3. CSI 驱动和 sidecar 必须匹配

CSI 驱动版本、sidecar 版本、Kubernetes API 版本和存储后端能力共同决定最终行为。尤其要注意:

  • Attach 是否由驱动支持;
  • ReadWriteMany 是否有真实共享语义;
  • 在线扩容是否支持;
  • 快照 CRD 和控制器是否安装;
  • 拓扑感知是否正确;
  • 节点重启和强制 detach 是否有恢复机制。

某些旧的内置存储插件已经被 CSI Migration 替代或弃用。生产集群不应把旧教程中的 in-tree provisioner 名称直接复制到当前集群。

4. 云厂商差异

云平台可能改变以下语义:

  • Pod IP 是否直接来自 VPC;
  • 节点是否能直接访问 Pod 网段;
  • 云盘是否支持跨可用区附加;
  • 安全组是否作用于 Pod ENI;
  • 网络和存储插件是否由云平台托管;
  • 是否允许用户替换默认 CNI 或 CSI。

因此,Kubernetes API 中相同的 Pod、PVC 和 Service,不一定对应相同的数据路径。需要同时核对 Kubernetes 对象、节点内核和云平台控制面状态。


十二、如何设计清晰的责任边界

可以用三个问题快速判断组件归属:

问题一:谁创建和管理进程?

看 CRI:

镜像、Sandbox、容器、启动、停止、执行命令、容器状态

问题二:谁让网络命名空间获得地址并可达?

看 CNI:

网卡、IPAM、路由、网络策略数据面、网络资源清理

问题三:谁让节点和 Pod 获得持久化卷?

看 CSI:

创建卷、附加设备、格式化、挂载、发布、扩容、快照

一个故障也可能跨越边界。例如:

CSI 挂载目录存在
  ↓
CRI 成功启动容器
  ↓
应用向卷写入失败

这时问题可能是文件系统权限,而不是 CSI 控制器;又如:

CNI 成功分配 Pod IP
  ↓
应用仍然无法访问 Service

这时应检查 Service、EndpointSlice、kube-proxy/eBPF 数据面和 NetworkPolicy,而不是重复安装 CNI。

最终可以把三者压缩成一条判断链:

容器是否被创建和运行?        → CRI
Pod 是否有正确的网络环境?    → CNI
卷是否被正确提供和挂载?      → CSI

CRI、CNI 和 CSI 的价值不只是“插件化”,而是把不同资源域的状态、失败路径和升级责任分开。理解这些边界后,PendingContainerCreatingCrashLoopBackOffFailedCreatePodSandBoxFailedAttachVolumeFailedMount 就不再是相似的错误字符串,而是能够映射到不同控制循环和不同组件的数据流。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。