CSI 存储卷挂载通信流程

目标

这篇文档只回答一个核心问题:kubelet 如何通过 CSI (Container Storage Interface) 完成 Pod 存储卷的挂载?

重点放在 VolumeManager 与 CSI driver 的交互,以及 attach/mount 的完整流程。

一句话摘要

kubelet 的 VolumeManager 通过 CSINode 和 CSIDriver 对象发现 CSI driver,然后通过 gRPC 调用 CSI driver 的 NodeStageVolume/NodePublishVolume 完成卷的挂载。


1. 流程总览

从通信视角看,这条链路可以拆成 3 个阶段:

  1. 卷准备阶段:CSI controller 完成 attach (对于块存储)。
  2. 节点挂载阶段:kubelet 通过 CSI driver 完成 stage 和 publish。
  3. 容器挂载阶段:kubelet 将卷 bind mount 到容器内。

架构总览图

flowchart TB
    subgraph 控制面["控制面"]
        direction TB
        PV[PersistentVolume]
        PVC[PersistentVolumeClaim]
        ATTACH[CSI Attacher<br/>外部控制器]
    end

    subgraph Kubelet["Kubelet VolumeManager"]
        direction TB
        VM[VolumeManager]
        DESIRED[DesiredStateOfWorld]
        ACTUAL[ActualStateOfWorld]
        EXEC[OperationExecutor]
        CSI[CSIPlugin]
    end

    subgraph CSI["CSI Driver"]
        direction TB
        NODE_REG[CSINode<br/>节点注册信息]
        DRIVER[CSIDriver<br/>驱动配置]
        PLUGIN[CSI Plugin<br/>gRPC 服务]
    end

    subgraph 存储["存储后端"]
        direction TB
        CLOUD[云存储<br/>EBS/CBS/etc]
        LOCAL[本地存储]
        NAS[网络存储<br/>NFS/Ceph/etc]
    end

    PVC --> PV
    PV --> VM
    VM --> DESIRED
    VM --> ACTUAL
    DESIRED --> EXEC
    EXEC --> CSI
    CSI --> NODE_REG
    CSI --> DRIVER
    CSI --> PLUGIN

    ATTACH --> CLOUD
    PLUGIN --> CLOUD
    PLUGIN --> LOCAL
    PLUGIN --> NAS

CSI 接口层次图

flowchart TB
    subgraph CSI接口["CSI gRPC 接口"]
        direction TB
        subgraph Controller["Controller Service"]
            C1[CreateVolume]
            C2[DeleteVolume]
            C3[ControllerPublishVolume<br/>Attach]
            C4[ControllerUnpublishVolume<br/>Detach]
            C5[ControllerExpandVolume]
        end

        subgraph Node["Node Service"]
            N1[NodeStageVolume<br/>临时挂载]
            N2[NodeUnstageVolume]
            N3[NodePublishVolume<br/>最终挂载]
            N4[NodeUnpublishVolume]
            N5[NodeExpandVolume]
            N6[NodeGetCapabilities]
        end

        subgraph Identity["Identity Service"]
            I1[GetPluginInfo]
            I2[GetPluginCapabilities]
            I3[Probe]
        end
    end

    subgraph Kubelet调用["Kubelet 调用流程"]
        K1[检查 CSIDriver]
        K2[发现 CSI Plugin]
        K3[调用 NodeStage]
        K4[调用 NodePublish]
    end

    K1 --> I1
    K2 --> N6
    K3 --> N1
    K4 --> N3

这张图想表达什么

  • CSI 将存储操作标准化为 gRPC 接口。
  • kubelet 只负责节点侧操作 (Stage/Publish)。
  • attach 操作由外部控制器完成 (对于需要 attach 的存储)。

2. 分阶段通信流程


阶段一:卷准备 (Attach)

这个阶段的本质是:对于需要 attach 的存储类型,将存储卷附加到节点。

步骤 1.1:PV 绑定到 PVC

用户创建 PVC 后,PV Controller 会找到或创建匹配的 PV 并绑定。

通信方向:PV Controller → API Server

步骤 1.2:AD Controller 调用 Attach

AttachDetach Controller 检测到 Pod 使用了 PV,会调用 CSI Attacher 进行 attach。

通信方向:AD Controller → CSI Attacher

步骤 1.3:CSI Attacher 调用 ControllerPublishVolume

CSI Attacher 通过 gRPC 调用 CSI driver 的 ControllerPublishVolume 方法。

通信方向:CSI Attacher → CSI Controller Plugin (gRPC)

步骤 1.4:存储卷附加到节点

CSI driver 与存储后端通信,将卷附加到目标节点。

通信方向:CSI Driver → 存储后端 (云 API/存储协议)

步骤 1.5:更新 Node.Status.VolumesAttached

Attach 完成后,AD Controller 更新 Node 对象的 status.volumesAttached 字段。

通信方向:AD Controller → API Server


阶段二:节点挂载 (Stage & Publish)

这个阶段的本质是:kubelet 通过 CSI driver 将卷挂载到节点目录。

步骤 2.1:VolumeManager 获取 Pod 的卷信息

kubelet 收到 Pod 后,VolumeManager 解析 Pod 的 volume 配置,包括 PVC。

通信方向:kubelet → Pod spec 解析

步骤 2.2:检查 CSIDriver 和 CSINode

CSIPlugin 检查 CSIDriver 和 CSINode 对象,确定:

  • CSI driver 是否支持 NodeStage
  • CSI driver 的挂载目录
  • CSI driver 的 gRPC 端点

通信方向:kubelet → API Server (读取 CSIDriver/CSINode)

步骤 2.3:调用 NodeStageVolume (如果支持)

如果 CSI driver 支持 STAGE_UNSTAGE_VOLUME 能力,kubelet 会先调用 NodeStageVolume,将卷挂载到一个临时目录(staging path)。

通信方向:kubelet → CSI Node Plugin (gRPC)

步骤 2.4:调用 NodePublishVolume

然后 kubelet 调用 NodePublishVolume,将卷从 staging path bind mount 到最终的 target path。

通信方向:kubelet → CSI Node Plugin (gRPC)

步骤 2.5:更新 ActualStateOfWorld

挂载成功后,VolumeManager 更新 ActualStateOfWorld,记录卷已挂载。

通信方向:VolumeManager 内部状态更新


阶段三:容器挂载

这个阶段的本质是:将节点上的挂载点 bind mount 到容器内。

步骤 3.1:容器创建时传入挂载配置

kubelet 调用 CRI 创建容器时,会将卷的挂载配置传给容器运行时。

通信方向:kubelet → CRI Runtime

步骤 3.2:容器运行时执行 bind mount

容器运行时将节点上的 target path bind mount 到容器内的 mount path。

通信方向:CRI Runtime → Linux 内核 (mount)

步骤 3.3:容器启动

容器启动后,可以通过容器内的路径访问存储卷内容。

通信方向:容器进程 → 文件系统


3. 详细流程图

3.1 卷挂载完整流程图

flowchart TD
    subgraph 初始化["初始化阶段"]
        A1[Pod 创建] --> A2[VolumeManager 解析卷]
        A2 --> A3[添加到 DesiredStateOfWorld]
    end

    subgraph 调度["调度阶段"]
        A3 --> B1[等待 Pod 绑定到节点]
        B1 --> B2[等待 PV 绑定到 PVC]
    end

    subgraph Attach["Attach 阶段 (可选)"]
        B2 --> C1{需要 Attach?}
        C1 -->|是| C2[AD Controller 调用 Attach]
        C2 --> C3[CSI ControllerPublishVolume]
        C3 --> C4[存储后端 Attach]
        C4 --> C5[更新 Node.VolumesAttached]
        C1 -->|否| D1
        C5 --> D1
    end

    subgraph Mount["Mount 阶段"]
        D1[D1: 检查 CSIDriver] --> D2[获取 CSI Plugin 地址]
        D2 --> D3{支持 Stage?}

        D3 -->|是| E1[NodeStageVolume]
        E1 --> E2[挂载到 staging path]
        E2 --> E3[NodePublishVolume]
        E3 --> E4[bind mount 到 target path]

        D3 -->|否| E3

        E4 --> F1[更新 ActualStateOfWorld]
    end

    subgraph 容器["容器阶段"]
        F1 --> G1[创建容器]
        G1 --> G2[bind mount 到容器内]
        G2 --> G3[启动容器]
    end

3.2 卷卸载流程图

flowchart TD
    subgraph 触发["卸载触发"]
        A1[Pod 删除] --> A2[VolumeManager 检测]
        A2 --> A3[从 DesiredStateOfWorld 移除]
    end

    subgraph Unmount["Unmount 阶段"]
        A3 --> B1[NodeUnpublishVolume]
        B1 --> B2[umount target path]

        B2 --> C1{支持 Stage?}
        C1 -->|是| C2[NodeUnstageVolume]
        C2 --> C3[umount staging path]
        C1 -->|否| D1

        C3 --> D1
    end

    subgraph Detach["Detach 阶段 (可选)"]
        D1 --> D2{需要 Detach?}
        D2 -->|是| D3[AD Controller 调用 Detach]
        D3 --> D4[CSI ControllerUnpublishVolume]
        D4 --> D5[存储后端 Detach]
        D5 --> D6[更新 Node.VolumesAttached]
        D2 -->|否| E1
        D6 --> E1
    end

    subgraph 清理["清理阶段"]
        E1[E1: 清理目录]
        E1 --> E2[删除 target path]
        E2 --> E3[删除 staging path]
        E3 --> E4[从 ActualStateOfWorld 移除]
    end

3.3 CSI Plugin 与 kubelet 交互图

sequenceDiagram
    autonumber
    participant KL as Kubelet
    participant VM as VolumeManager
    participant OE as OperationExecutor
    participant CP as CSIPlugin
    participant API as kube-apiserver
    participant CSI as CSI Node Plugin
    participant FS as 文件系统

    KL->>VM: Pod 更新
    VM->>VM: 添加到 DesiredStateOfWorld

    loop reconciler 循环
        VM->>VM: 对比 Desired vs Actual
        VM->>OE: MountVolume(volume, pod)

        OE->>CP: Mount(device, target)
        CP->>API: Get CSIDriver
        API-->>CP: CSIDriver 对象

        CP->>API: Get CSINode
        API-->>CP: CSINode 对象

        CP->>CP: 获取 CSI Plugin socket

        Note over CP,CSI: NodeStageVolume (可选)

        alt 支持 STAGE_UNSTAGE_VOLUME
            CP->>CSI: NodeStageVolume(staging_path, volume_context)
            CSI->>FS: mount /dev/xxx /var/lib/kubelet/plugins/kubernetes.io/csi/pv/<pv>/globalmount
            FS-->>CSI: 成功
            CSI-->>CP: 成功
        end

        Note over CP,CSI: NodePublishVolume

        CP->>CSI: NodePublishVolume(target_path, staging_path, volume_context)
        CSI->>FS: mount --bind staging_path target_path
        FS-->>CSI: 成功
        CSI-->>CP: 成功

        CP-->>OE: 成功
        OE-->>VM: 成功
        VM->>VM: 更新 ActualStateOfWorld
    end

4. 关键数据结构

4.1 CSIDriver 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: storage.k8s.io/v1
kind: CSIDriver
metadata:
name: ebs.csi.aws.com
spec:
attachRequired: true # 是否需要 attach
podInfoOnMount: false # 是否在 NodePublishVolume 传递 Pod 信息
volumeLifecycleModes: # 支持的生命周期模式
- Persistent
- Ephemeral
storageCapacity: true # 是否支持存储容量追踪
fsGroupPolicy: File # fsGroup 处理策略
requiresRepublish: false # 是否需要定期重新发布
seLinuxMount: true # SELinux 挂载支持
supportsSELinuxContext: true # SELinux 上下文支持
tokenRequests: # Token 请求配置
- audience: "sts.amazonaws.com"
ephemeral: false # 是否支持内联 ephemeral 卷

4.2 CSINode 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: storage.k8s.io/v1
kind: CSINode
metadata:
name: node-1
spec:
drivers:
- name: ebs.csi.aws.com
nodeID: i-1234567890abcdef0 # CSI driver 的节点 ID
topologyKeys: # 节点拓扑键
- topology.ebs.csi.aws.com/zone
allocatable: # 可分配容量
- name: storage
capacity: 100Gi

4.3 挂载路径结构

1
2
3
4
5
6
7
8
9
/var/lib/kubelet/
├── pods/<pod-uid>/
│ └── volumes/kubernetes.io~csi/
│ └── <pv-name>/ # target_path
│ ├── mount # 实际挂载点
│ └── data # 卷数据
└── plugins/kubernetes.io/csi/
└── <pv-name>/
└── globalmount/ # staging_path

5. 详细时序图

5.1 动态供应 PV 完整时序图

sequenceDiagram
    autonumber
    participant User as 用户
    participant API as kube-apiserver
    participant PVC as PVC Controller
    participant PROV as CSI Provisioner
    participant CSI as CSI Controller
    participant AD as AD Controller
    participant KL as Kubelet
    participant NODE as CSI Node Plugin
    participant STORAGE as 存储后端

    User->>API: 创建 PVC
    API-->>PVC: watch PVC ADDED
    PVC->>PVC: 检查 StorageClass
    PVC->>API: 绑定等待

    Note over PROV: External Provisioner watch PVC

    API-->>PROV: watch PVC (Pending)
    PROV->>CSI: CreateVolume
    CSI->>STORAGE: 创建存储卷
    STORAGE-->>CSI: Volume ID
    CSI-->>PROV: Volume ID
    PROV->>API: 创建 PV
    PVC->>API: 绑定 PV 到 PVC

    Note over AD: AD Controller watch Pod

    User->>API: 创建 Pod (使用 PVC)
    AD->>AD: 检测到 Pod 使用 PV

    alt attachRequired: true
        AD->>CSI: ControllerPublishVolume
        CSI->>STORAGE: Attach 卷到节点
        STORAGE-->>CSI: 成功
        CSI-->>AD: 成功
        AD->>API: 更新 Node.VolumesAttached
    end

    API-->>KL: watch Pod (已调度)
    KL->>KL: VolumeManager 处理

    alt 支持 STAGE_UNSTAGE_VOLUME
        KL->>NODE: NodeStageVolume
        NODE->>STORAGE: 挂载到 staging path
        STORAGE-->>NODE: 成功
        NODE-->>KL: 成功
    end

    KL->>NODE: NodePublishVolume
    NODE->>STORAGE: bind mount 到 target path
    STORAGE-->>NODE: 成功
    NODE-->>KL: 成功

    KL->>KL: 创建容器
    KL->>KL: 启动 Pod

5.2 卷扩容时序图

sequenceDiagram
    autonumber
    participant User as 用户
    participant API as kube-apiserver
    participant RESIZE as Resize Controller
    participant CSI as CSI Controller
    participant KL as Kubelet
    participant NODE as CSI Node Plugin
    participant STORAGE as 存储后端

    User->>API: 更新 PVC.spec.resources.requests.storage
    API-->>RESIZE: watch PVC UPDATE

    RESIZE->>RESIZE: 检测扩容请求
    RESIZE->>CSI: ControllerExpandVolume
    CSI->>STORAGE: 扩容存储卷
    STORAGE-->>CSI: 成功
    CSI-->>RESIZE: 成功

    RESIZE->>API: 更新 PV.spec.capacity.storage

    Note over KL: 等待 Pod 重启或在线扩容

    API-->>KL: watch PV UPDATE
    KL->>KL: 检测到卷容量变化

    alt 节点扩容
        KL->>NODE: NodeExpandVolume
        NODE->>STORAGE: resize2fs / xfs_growfs
        STORAGE-->>NODE: 成功
        NODE-->>KL: 成功
    end

    KL->>API: 更新 PVC.status.capacity.storage

6. 故障排查指南

6.1 常见问题与诊断方法

问题 1:Pod 卡在 ContainerCreating (卷挂载失败)

症状

1
2
3
4
5
6
kubectl get pods
NAME READY STATUS RESTARTS AGE
pod-1 0/1 ContainerCreating 0 5m

Events:
Warning FailedMount Unable to attach or mount volumes

排查流程图

flowchart TD
    A[卷挂载失败] --> B{检查 PV/PVC 状态}

    B --> B1[PVC Pending]
    B1 --> B1a[检查 StorageClass]
    B1a --> B1b[检查 Provisioner]

    B --> B2[PV Available]
    B2 --> B2a[检查 PV 绑定]

    B --> B3[PV Bound]
    B3 --> C{检查 Attach 状态}

    C --> C1[检查 Node.VolumesAttached]
    C1 --> C1a{有 Attach 记录?}

    C1a -->|否| D{检查 AD Controller}
    D --> D1[检查 AD Controller 日志]
    D --> D2[检查 CSI Controller 日志]

    C1a -->|是| E{检查 Mount 状态}
    E --> E1[检查 kubelet 日志]
    E --> E2[检查 CSI Node Plugin 日志]

    A --> F{检查 CSI Driver}
    F --> F1[检查 CSIDriver 对象]
    F --> F2[检查 CSINode 对象]
    F --> F3[检查 CSI Pod 状态]

诊断命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 检查 PVC 状态
kubectl get pvc -n <namespace>
kubectl describe pvc <pvc-name> -n <namespace>

# 检查 PV 状态
kubectl get pv
kubectl describe pv <pv-name>

# 检查 Attach 状态
kubectl get node <node-name> -o jsonpath='{.status.volumesAttached}'

# 检查 CSIDriver
kubectl get csidriver
kubectl describe csidriver <driver-name>

# 检查 CSINode
kubectl get csinode <node-name> -o yaml

# 检查 kubelet 日志
journalctl -u kubelet | grep -i "volume\|mount\|csi"

# 检查节点上的挂载点
ls -la /var/lib/kubelet/pods/<pod-uid>/volumes/
mount | grep <pv-name>

问题 2:卷卸载失败导致 Pod 无法删除

症状:Pod 一直处于 Terminating 状态

排查流程图

flowchart TD
    A[Pod Terminating] --> B{检查 kubelet 日志}

    B --> B1[Unmount 失败]
    B1 --> B1a[检查挂载点是否存在]
    B1a --> B1b[检查是否有进程占用]

    B --> B2[NodeUnpublishVolume 失败]
    B2 --> B2a[检查 CSI Node Plugin 日志]
    B2a --> B2b[检查 CSI Plugin 状态]

    B --> B3[Detach 失败]
    B3 --> B3a[检查 AD Controller 日志]
    B3a --> B3b[检查存储后端状态]

    A --> C{手动清理}
    C --> C1[umount 挂载点]
    C --> C2[删除挂载目录]
    C --> C3[强制删除 Pod]

诊断命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 检查 Pod 状态
kubectl describe pod <pod-name> -n <namespace>

# 检查挂载点
mount | grep <pod-uid>

# 检查是否有进程占用
lsof /var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~csi/

# 手动卸载
sudo umount /var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~csi/<pv-name>/mount

# 强制删除 Pod
kubectl delete pod <pod-name> -n <namespace> --force --grace-period=0

问题 3:CSI Driver 注册失败

症状:CSINode 对象不存在或 driver 未注册

排查流程图

flowchart TD
    A[CSI Driver 未注册] --> B{检查 CSI Driver Pod}

    B --> B1[Pod 未运行]
    B1 --> B1a[检查 Pod 事件]
    B1a --> B1b[检查镜像拉取]
    B1a --> B1c[检查资源限制]

    B --> B2[Pod 运行中]
    B2 --> C{检查 Driver 日志}

    C --> C1[注册失败]
    C1 --> C1a[检查 KUBELET_DIR 权限]
    C1a --> C1b[检查 socket 文件]

    C --> C2[gRPC 启动失败]
    C2 --> C2a[检查端口占用]
    C2 --> C2b[检查 SELinux/AppArmor]

    A --> D{检查 CSIDriver 对象}
    D --> D1[对象不存在]
    D1 --> D1a[创建 CSIDriver CRD]

诊断命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 检查 CSI Driver Pod
kubectl get pods -n <csi-namespace> -l app=<csi-driver>
kubectl describe pod <csi-pod> -n <csi-namespace>

# 检查 CSINode
kubectl get csinode
kubectl describe csinode <node-name>

# 检查 CSI socket
ls -la /var/lib/kubelet/plugins/<driver-name>/
ls -la /var/lib/kubelet/plugins_registry/

# 检查 CSI driver 日志
kubectl logs -n <csi-namespace> <csi-node-pod> -c node-driver-registrar
kubectl logs -n <csi-namespace> <csi-node-pod> -c <csi-driver>

6.2 关键日志关键词

组件 日志关键词 含义
kubelet WaitForAttachAndMount 等待卷挂载
kubelet NodeStageVolume 调用 CSI Stage
kubelet NodePublishVolume 调用 CSI Publish
kubelet UnmountVolume 卸载卷
CSI Plugin GRPC gRPC 调用
AD Controller AttachVolume 卷附加
AD Controller DetachVolume 卷分离

6.3 配置参数参考

参数 默认值 说明
--enable-controller-attach-detach true 启用 AD Controller
--volume-plugin-dir /usr/libexec/kubernetes/kubelet-plugins/volume 卷插件目录
--volume-stats-egress-interval 10s 卷统计间隔

6.4 一键诊断命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# CSI 卷问题诊断脚本
PVC_NAME=${1:-"<pvc-name>"}
NAMESPACE=${2:-"default"}
POD_NAME=$(kubectl get pvc $PVC_NAME -n $NAMESPACE -o jsonpath='{.spec.volumeName}')

echo "=== PVC Status ===" && \
kubectl get pvc $PVC_NAME -n $NAMESPACE -o wide && \
echo -e "\n=== PV Status ===" && \
kubectl get pv $POD_NAME -o wide && \
echo -e "\n=== CSIDriver ===" && \
kubectl get csidriver && \
echo -e "\n=== CSINode ===" && \
kubectl get csinode && \
echo -e "\n=== CSI Pods ===" && \
kubectl get pods -A -l app.kubernetes.io/name | grep csi && \
echo -e "\n=== Volume Attachments ===" && \
kubectl get volumeattachments | grep $POD_NAME

7. 版本差异说明

1.27 → 1.28 变化

变化点 说明
SELinux 挂载支持 CSI driver 可以支持 SELinux 上下文
存储容量追踪 GA 存储容量追踪功能正式发布

1.28 → 1.29 变化

变化点 说明
fsGroupPolicy 增强 更灵活的 fsGroup 处理策略
在线卷扩容改进 更多存储类型支持在线扩容

8. 代码入口(精简版)

如果读者想从流程跳回实现,可从下面几个入口开始:


9. 面试题与详细解答

问题 1:CSI(Container Storage Interface)的设计目的是什么?与 in-tree 存储插件相比有什么优势?

回答要点

CSI 设计目的

  1. 解耦存储驱动:将存储驱动从 Kubernetes 核心代码中分离
  2. 标准化接口:定义统一的 gRPC 接口规范
  3. 支持多种存储:同一 CSI 驱动可用于不同容器编排系统
  4. 独立发布:存储厂商可独立发布和更新驱动

与 in-tree 插件对比

特性 in-tree 插件 CSI 驱动
代码位置 Kubernetes 核心代码 独立仓库
发布周期 跟随 K8s 版本 独立发布
维护责任 K8s 社区 存储厂商
功能扩展 需要 K8s 代码变更 只需修改驱动
兼容性 强绑定 K8s 版本 向前兼容

CSI 三类服务

  1. Identity Service:插件身份和能力

    • GetPluginInfo:获取插件信息
    • GetPluginCapabilities:获取插件能力
    • Probe:健康检查
  2. Controller Service:卷生命周期管理

    • CreateVolume/DeleteVolume
    • ControllerPublishVolume/ControllerUnpublishVolume(Attach/Detach)
    • ControllerExpandVolume(扩容)
  3. Node Service:节点侧卷操作

    • NodeStageVolume/NodeUnstageVolume(临时挂载)
    • NodePublishVolume/NodeUnpublishVolume(最终挂载)
    • NodeExpandVolume(文件系统扩容)

CSI 迁移

  • Kubernetes 正逐步将 in-tree 插件迁移到 CSI
  • 使用 CSIMigration feature gate 启用
  • 透明转换:用户无需修改 PV 配置

问题 2:CSI 的 NodeStageVolume 和 NodePublishVolume 有什么区别?为什么需要两个阶段?

回答要点

两阶段设计目的

  1. NodeStageVolume:将卷挂载到临时目录(staging path)

    • 路径:/var/lib/kubelet/plugins/kubernetes.io/csi/<pv>/globalmount
    • 只执行一次(即使多个 Pod 使用同一 PV)
    • 用于需要特殊初始化的存储(如块设备格式化)
  2. NodePublishVolume:bind mount 到 Pod 目录(target path)

    • 路径:/var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~csi/<pv>/mount
    • 每个 Pod 执行一次
    • 轻量级操作(bind mount)

为什么需要两阶段

  1. 性能优化

    • 块设备挂载、格式化等耗时操作只在 Stage 阶段执行一次
    • 多 Pod 共享同一 PV 时避免重复操作
  2. 语义清晰

    • Stage:卷级别的操作
    • Publish:Pod 级别的操作
  3. 支持共享卷

    • NAS 等存储支持多个 Pod 同时访问
    • Stage 一次,Publish 多次

流程图

1
2
3
4
5
6
NodeStageVolume          NodePublishVolume
↓ ↓
块设备挂载 bind mount
文件系统格式化 mount --bind
↓ ↓
/globalmount → /pods/<uid>/volumes/.../mount

CSI Driver 能力声明

1
2
3
4
// 如果驱动支持 STAGE_UNSTAGE_VOLUME 能力
// kubelet 会先调用 NodeStageVolume
// 否则直接调用 NodePublishVolume
NodeGetCapabilities() []*NodeServiceCapability

问题 3:PV 的动态供应(Dynamic Provisioning)完整流程是什么?涉及哪些组件?

回答要点

动态供应流程

sequenceDiagram
    participant User
    participant API as kube-apiserver
    participant PVC as PVC Controller
    participant PROV as External Provisioner
    participant CSI as CSI Controller
    participant Storage as 存储后端

    User->>API: 创建 PVC (StorageClass)
    API-->>PVC: watch PVC ADDED
    PVC->>PVC: 检查 StorageClass
    PVC->>API: 绑定等待 (Pending)

    API-->>PROV: watch PVC (provisioner=xxx)
    PROV->>PROV: 检查是否需要供应
    PROV->>CSI: CreateVolume
    CSI->>Storage: 创建存储卷
    Storage-->>CSI: Volume ID
    CSI-->>PROV: Volume ID
    PROV->>API: 创建 PV
    PVC->>API: 绑定 PV 到 PVC (Bound)

    Note over User,Storage: 动态供应完成

涉及组件

  1. PVC Controller(kube-controller-manager):

    • 检查 PVC 的 StorageClass
    • 处理 PV 绑定逻辑
  2. External Provisioner(Sidecar 容器):

    • 监听 PVC 变化
    • 调用 CSI CreateVolume
    • 创建 PV 对象
  3. CSI Controller(CSI 驱动):

    • 实现 CreateVolume gRPC
    • 与存储后端通信

关键配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-storage
provisioner: ebs.csi.aws.com # CSI 驱动名称
parameters:
type: gp3
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer # 延迟绑定
allowedTopologies:
- matchLabelExpressions:
- key: topology.ebs.csi.aws.com/zone
values: ["us-west-2a", "us-west-2b"]

VolumeBindingMode 说明

模式 行为 使用场景
Immediate 立即创建 PV 不关心拓扑
WaitForFirstConsumer Pod 调度后创建 需要拓扑感知

问题 4:如何排查 Pod 因为卷挂载失败而卡在 ContainerCreating 的问题?

回答要点

排查流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
Pod ContainerCreating

检查 Pod Events

检查 PVC 状态

检查 PV 状态

检查 CSI Driver 状态

检查 kubelet 日志

检查节点挂载状态

详细排查步骤

步骤 1:检查 Pod 事件

1
2
3
4
5
6
kubectl describe pod <pod-name> -n <namespace>

# 常见错误信息:
# - Unable to attach or mount volumes
# - Volume is already exclusively attached to another node
# - timed out waiting for the condition

步骤 2:检查 PVC/PV 状态

1
2
3
4
5
6
7
8
9
10
# 检查 PVC
kubectl get pvc -n <namespace>
kubectl describe pvc <pvc-name> -n <namespace>

# 检查 PV
kubectl get pv
kubectl describe pv <pv-name>

# 检查绑定状态
kubectl get pvc <pvc-name> -o jsonpath='{.status.phase}'

步骤 3:检查 CSI Driver

1
2
3
4
5
6
7
8
9
10
11
# 检查 CSIDriver 对象
kubectl get csidriver
kubectl describe csidriver <driver-name>

# 检查 CSINode 对象
kubectl get csinode <node-name> -o yaml

# 检查 CSI Pod 状态
kubectl get pods -n <csi-namespace> -l app=<csi-driver>
kubectl logs -n <csi-namespace> <csi-controller-pod> -c csi-provisioner
kubectl logs -n <csi-namespace> <csi-node-pod> -c csi-node-driver

步骤 4:检查 Attach 状态

1
2
3
4
5
6
# 检查 VolumeAttachment
kubectl get volumeattachments
kubectl describe volumeattachment <va-name>

# 检查 Node 的 VolumesAttached
kubectl get node <node-name> -o jsonpath='{.status.volumesAttached}'

步骤 5:在节点上检查

1
2
3
4
5
6
7
8
9
10
11
12
13
# 检查 kubelet 日志
journalctl -u kubelet | grep -i "volume\|mount\|csi"

# 检查挂载点
ls -la /var/lib/kubelet/pods/<pod-uid>/volumes/
mount | grep <pv-name>

# 检查 CSI socket
ls -la /var/lib/kubelet/plugins/<driver-name>/
ls -la /var/lib/kubelet/plugins_registry/

# 手动测试 CSI gRPC
grpcurl -plaintext unix:///var/lib/kubelet/plugins/<driver-name>/csi.sock csi.v1.Identity/GetPluginInfo

常见问题及解决

问题 原因 解决方法
PVC Pending StorageClass 不存在 检查 StorageClass
PV 绑定失败 容量不匹配 检查 storage 字段
Attach 失败 卷已被其他节点使用 等待 Detach 完成
Mount 失败 CSI 驱动未就绪 重启 CSI Pod
格式化失败 块设备已格式化 检查卷内容

一键诊断脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash
PVC_NAME=${1}
NAMESPACE=${2:-default}

PV_NAME=$(kubectl get pvc $PVC_NAME -n $NAMESPACE -o jsonpath='{.spec.volumeName}')

echo "=== PVC Status ===" && \
kubectl get pvc $PVC_NAME -n $NAMESPACE -o wide && \
echo -e "\n=== PV Status ===" && \
kubectl get pv $PV_NAME -o wide && \
echo -e "\n=== VolumeAttachment ===" && \
kubectl get volumeattachments | grep -E "NAME|$PV_NAME" && \
echo -e "\n=== CSI Driver ===" && \
kubectl get csidriver && \
echo -e "\n=== CSINode ===" && \
kubectl get csinode && \
echo -e "\n=== StorageClass ===" && \
kubectl get storageclass $(kubectl get pvc $PVC_NAME -n $NAMESPACE -o jsonpath='{.spec.storageClassName}') -o yaml

问题 5:CSI 卷扩容(Volume Expansion)是如何实现的?在线扩容和离线扩容有什么区别?

回答要点

卷扩容流程

1
2
3
4
5
6
7
1. 用户更新 PVC.spec.resources.requests.storage
2. Resize Controller 检测扩容请求
3. 调用 CSI ControllerExpandVolume(存储后端扩容)
4. 更新 PV.spec.capacity.storage
5. kubelet 检测到容量变化
6. 调用 CSI NodeExpandVolume(文件系统扩容)
7. 更新 PVC.status.capacity.storage

在线扩容 vs 离线扩容

特性 在线扩容 离线扩容
Pod 状态 必须运行 必须停止
文件系统 可以在线扩展 需要先卸载
支持的 FS xfs, ext4(部分) 所有类型
风险 较低
业务影响 有停机时间

扩容条件

  1. StorageClass 必须允许扩容

    1
    2
    3
    4
    5
    6
    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
    name: expandable
    provisioner: ebs.csi.aws.com
    allowVolumeExpansion: true # 必须设置为 true
  2. CSI 驱动必须支持扩容

    1
    2
    3
    4
    5
    6
    7
    8
    # CSIDriver 对象
    apiVersion: storage.k8s.io/v1
    kind: CSIDriver
    metadata:
    name: ebs.csi.aws.com
    spec:
    # 支持节点扩容
    fsGroupPolicy: File
  3. 文件系统限制

    • xfs:只支持在线扩容,不支持缩小
    • ext4:支持在线扩容,缩小需要离线

扩容操作示例

1
2
3
4
5
6
7
8
# 1. 修改 PVC 请求大小
kubectl patch pvc my-pvc -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'

# 2. 检查扩容状态
kubectl describe pvc my-pvc | grep -A 5 Conditions

# 3. 检查文件系统扩容日志
journalctl -u kubelet | grep -i "resize\|expand"

扩容失败处理

1
2
3
4
5
6
7
# 扩容失败时 PVC 状态
status:
conditions:
- type: Resizing
status: "False"
reason: ControllerResizeFailed
message: "volume expansion failed"

恢复方法

1
2
3
4
5
6
7
# 1. 查看失败原因
kubectl describe pvc <pvc-name>

# 2. 如果是永久失败,可能需要回滚大小
kubectl patch pvc <pvc-name> -p '{"spec":{"resources":{"requests":{"storage":"10Gi"}}}}'

# 3. 删除并重建 PVC(数据会丢失)

问题 6:什么是 CSI 拓扑(Topology)?如何实现跨可用区的存储调度?

回答要点

CSI 拓扑概念

拓扑(Topology)用于表达存储资源的可用位置,帮助调度器将 Pod 调度到存储可用的节点。

拓扑键示例

  • topology.ebs.csi.aws.com/zone:AWS 可用区
  • topology.gke.io/zone:GKE 可用区
  • topology.kubernetes.io/zone:标准 zone 标签

实现方式

  1. CSINode 注册拓扑信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    apiVersion: storage.k8s.io/v1
    kind: CSINode
    metadata:
    name: node-1
    spec:
    drivers:
    - name: ebs.csi.aws.com
    nodeID: i-1234567890abcdef0
    topologyKeys:
    - topology.ebs.csi.aws.com/zone
    allocatable:
    - name: storage
    capacity: 100Gi
  2. Node 标签

    1
    kubectl label node node-1 topology.ebs.csi.aws.com/zone=us-west-2a
  3. StorageClass 配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
    name: zone-aware
    provisioner: ebs.csi.aws.com
    volumeBindingMode: WaitForFirstConsumer # 关键:等待调度决策
    allowedTopologies:
    - matchLabelExpressions:
    - key: topology.ebs.csi.aws.com/zone
    values:
    - us-west-2a
    - us-west-2b

调度流程

1
2
3
4
5
6
7
8
1. Pod 创建,带有 PVC
2. 调度器评估节点
3. 选择最优节点(考虑资源、拓扑等)
4. 触发 PV 动态供应
5. CSI Provisioner 从节点获取拓扑信息
6. 在正确的可用区创建存储卷
7. PV 绑定到 PVC
8. Pod 继续启动

WaitForFirstConsumer vs Immediate

模式 PV 创建时机 拓扑感知
Immediate PVC 创建时立即创建
WaitForFirstConsumer Pod 调度后创建

跨可用区最佳实践

  1. 使用 WaitForFirstConsumer:确保 PV 在正确的可用区创建
  2. 配置 allowedTopologies:限制存储创建区域
  3. Pod 反亲和性:分散 Pod 到不同可用区
  4. 存储复制:使用支持跨区复制的存储(如 Ceph)

问题 7:如何实现 CSI 卷的只读挂载?多 Pod 共享同一个 PV 时有什么注意事项?

回答要点

只读挂载实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- name: app
image: nginx
volumeMounts:
- name: data
mountPath: /data
readOnly: true # 只读挂载
volumes:
- name: data
persistentVolumeClaim:
claimName: my-pvc

只读挂载实现原理

  1. CSI 层面NodePublishVolume 请求中设置 readonly: true
  2. 挂载选项mount -o ro,bind /source /target
  3. 容器层面:容器运行时传入 readonly 配置

多 Pod 共享 PV 注意事项

  1. 访问模式

    • ReadWriteOnce (RWO):单节点读写
    • ReadOnlyMany (ROX):多节点只读
    • ReadWriteMany (RWX):多节点读写
  2. 文件系统限制

    • ext4/xfs:不支持多节点并发写
    • NFS:支持 RWX
    • 块存储:只支持 RWO
  3. 数据一致性

    • 多写需要应用层协调(如分布式锁)
    • 建议使用单一写入者模式

共享 PV 配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# PV 配置
apiVersion: v1
kind: PersistentVolume
metadata:
name: shared-nfs
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteMany # 支持多节点读写
nfs:
server: nfs-server.example.com
path: /export/data

# 多 Pod 使用同一 PVC
apiVersion: apps/v1
kind: Deployment
metadata:
name: reader-deployment
spec:
replicas: 3 # 多副本共享
template:
spec:
containers:
- name: reader
volumeMounts:
- name: shared-data
mountPath: /data
readOnly: true
volumes:
- name: shared-data
persistentVolumeClaim:
claimName: shared-pvc

共享存储最佳实践

  1. 明确读写角色:一个 Pod 写,多个 Pod 读
  2. 使用文件锁:应用层实现并发控制
  3. 选择合适的存储:NFS、CephFS 支持 RWX
  4. 监控 IO 竞争:关注存储性能指标