StorageClass 配了 PV 还 Pending?CSI 驱动没启动

场景:应用部署创建 PVC 后 PV 一直 Pending,StorageClass 配置检查了 3 遍没问题——问题不在 YAML,在 CSI 驱动侧 路径:PVC 状态 → Events → provisioner 日志 → CSI driver socket → 驱动修复

以下排查基于 K8s v1.28,CSI 组件使用 storage.k8s.io/v1 API

坐标 — PV 创建卡住的现场

上篇讲了 emptyDir 不设 sizeLimit 导致整节点 30 个 Pod 被驱逐,这篇我们来看另一个存储场景——PV 创建卡住了,根因不是 StorageClass 写错,是 CSI 驱动没起来。

新存储上线后 PVC 一直 Pending——"StorageClass 名字写错了吧"团队第一反应。kubectl describe pvc 一看,Events 段显示:

  Warning ProvisioningFailed  3m  external-provisioner
    Failed to provision volume with StorageClass "csi-fast-sc":
    rpc error: code = Unavailable desc = connection closed

StorageClass 名字没错——是 CSI 驱动侧 gRPC socket 没开,external-provisioner 连不上 driver,PV 没法创建。以后你发现 PVC Pending 且 StorageClass 配置没问题:先查 CSI 驱动组件状态,别在 YAML 字段上死磕。

表象:PVC 一直 Pending

创建一个 PVC 指定 StorageClass csi-fast-sc

kubectl get pvc data-pvc

输出:

NAME       STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
data-pvc   Pending                                      csi-fast-sc    5m

PVC(PersistentVolumeClaim,Pod 使用的存储请求)停留在 Pending 状态,VOLUME 列为空——说明底层 PV(PersistentVolume,集群级存储资源)尚未创建。

如果你不熟悉 CSI 架构,第一反应是 StorageClass 写错了。检查了一遍 YAML:provisioner 名字对、parameters 格式对、reclaimPolicy 也设了——问题不在声明侧。

那问题在哪?

Events:provisioner 给出了明确错误

kubectl describe pvc data-pvc

Events 段:

Events:
  Type     Reason                Age   From                     Message
  ----     ------                ----  ----                     -------
  Warning  ProvisioningFailed    3m    external-provisioner     Failed to provision volume with StorageClass "csi-fast-sc": rpc error: code = Unavailable desc = connection closed
  Warning  ProvisioningFailed    2m    external-provisioner     Failed to provision volume with StorageClass "csi-fast-sc": rpc error: code = Unavailable desc = connection closed

关键线索在 From: external-provisionercode = Unavailable desc = connection closed

external-provisioner(K8s 的 CSI 边车容器,负责监听 PVC 变化并调用 CSI 驱动的 gRPC 接口创建卷)反复报 connection closed——这说明:

  1. StorageClass 的 provisioner 名字是对的(否则它连 driver 都找不到)
  2. external-provisioner 找到了 CSI driver 的注册信息
  3. 但它在建立 gRPC 连接时被拒绝了

gRPC 的 Unavailable 状态码表示服务端不可达——不是网络不通,是 Unix socket 文件不存在或进程没在监听。

K8s 版本

Server Version: v1.28.7
CSI 组件 API: storage.k8s.io/v1

PVC Pending 状态 + Events

分层 — CSI 存储链路拓扑

PVC 创 PV 不是单个组件的事,是一条完整调用链。分层看才能理解"断在哪":

CSI 存储链路拓扑

这条链路上发生了什么

从用户创建 PVC 到 PV 绑定,K8s 依次走以下步骤:

  1. PVC 控制器看到新的未绑定 PVC,找到它引用的 StorageClass
  2. external-provisioner(运行在 kube-controller-manager 内部或作为独立 Pod)监听到 PVC 创建事件,根据 StorageClass 的 provisioner 字段找到对应的 CSI Driver 注册信息
  3. external-provisioner 通过 Unix socket(/var/lib/kubelet/plugins/<driver>/csi.sock)向 CSI driver 发起 gRPC CreateVolume 调用
  4. CSI driver 收到请求后,调用后端存储 API 创建实际存储卷
  5. 卷创建成功后,CSI driver 返回 PV 对象信息,K8s 将 PV 绑定到 PVC

这条链路上任何一个环节断了,PV 就创建不出来。而 connection closed 说明断在第 3 步——external-provisioner 发出了 gRPC 调用,但 socket 对面没人接。

三层排查顺序

为什么提倡分层排查?因为不同层的根因对应不同的修复方式:

层级 查什么 关键命令 典型根因
Pod 层 PVC/PV 状态 + Events kubectl describe pvc StorageClass 名字错、quota 不足
CSI 层 driver pod 状态 + socket + 日志 kubectl get pods -n kube-systemkubectl logs driver 没启动、socket 目录未挂载
后端层 存储系统可达性 存储侧管理面检查 存储 API 不可达、容量耗尽

先查 Pod 层排除配置问题,再查 CSI 层看驱动状态,最后查后端——不跳层、不漏层。

路径 — 核心排查命令队列

第 1 步:确认 PVC 的 provisioner 和 StorageClass

kubectl get sc csi-fast-sc -o yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: csi-fast-sc
provisioner: csi-fast.example.com    # ← 这个 provisioner 决定了调哪个 CSI 驱动
parameters:
  type: ssd

注意 provisioner 字段的值——external-provisioner 根据这个值找到对应的 CSI driver 注册信息。这个字段不是随便写的,它必须和 CSIDriver 对象的名字完全一致。

第 2 步:查 CSI driver 相关 Pod 状态

kubectl get pods -n kube-system -o wide | grep csi

输出:

csi-fast-controller-6b9d75c9d8-4z5m7   0/1  CrashLoopBackOff   5   10m
csi-fast-node-8h3j2                     2/3  Running            0   2d
csi-fast-node-l9p4m                     2/3  Running            0   2d

csi-fast-controller(CSI controller 组件,处理 CreateVolume/DeleteVolume 等控制面操作)CrashLoopBackOff——这是根因的信号。

注意这里有两种 CSI 组件: - Controller 组件:处理控制面操作,以 Deployment 形式运行,需要 Socket 目录来接受 gRPC 调用 - Node 组件:处理挂载/卸载操作,以 DaemonSet 形式运行在每个节点上

PVC 创建走的是 Controller 组件,所以即使 Node 组件都 Running,Controller 挂了也白搭。

第 3 步:检查 CSIDriver 对象

kubectl get csidriver

CSIDriver(CSI 驱动在集群中的注册对象,告诉 K8s 这个驱动支持哪些能力):

NAME                    ATTACHREQUIRED   PODINFOONMOUNT   MODES        AGE
csi-fast.example.com    true             false            Persistent   2d

CSIDriver 对象存在,说明驱动之前在集群中注册过。一个常见的排查误区是认为 CSIDriver 存在就等于 driver 在正常工作——不,它只是一个注册声明,不保证对应的 driver Pod 在跑。

第 4 步:看 CSI driver 日志(根因)

kubectl logs -n kube-system csi-fast-controller-6b9d75c9d8-4z5m7
I1023 10:15:23.001  csi-driver.go:42] Starting CSI driver...
I1023 10:15:23.002  identity.go:28] Opening gRPC server on unix:///csi/csi.sock
E1023 10:15:23.015  csi-driver.go:55] Failed to listen on socket: listen unix /csi/csi.sock: bind: no such file or directory

CSI driver 启动时试图在 /csi/csi.sock 监听 gRPC 请求,但 /csi/ 目录不存在——这是容器挂载卷(Volume)映射问题。

日志揭示了一个关键信息:CSI driver 用 Unix socket 通信。unix:// 协议意味着它需要一个实际的文件系统路径来创建 socket 文件。如果 /csi/ 目录没挂载进来,driver 进程一启动就卡死在 socket 创建这一步。

第 5 步:验证 socket 文件

kubectl exec -n kube-system csi-fast-controller-6b9d75c9d8-4z5m7 -- ls -la /csi/
ls: cannot access '/csi/': No such file or directory

CSI 驱动 Pod 状态 + 日志

socket 目录根本不存在——CSI driver Deployment 的 Pod spec 中没有声明 VolumeMount,容器内没有 /csi 这个目录,driver 进程无法创建 socket 文件,启动即崩溃,provisioner 连不上,PV 创建卡死。

这是一个 CSI driver 部署清单本身的质量问题。它出现在生产环境,通常是因为集群管理员从社区拉了一个 CSI Driver manifest,但 Helm Chart 升级时 volumes 字段被覆盖、或者手动编辑 Deployment 时漏掉了 volumeMounts。

核心排查命令

定位 — 最常误判的方向

大多数人不熟悉 CSI 架构时会反复排查的错误方向——每一个我都见过真实案例。

❌ 错误方向 1:反复检查 StorageClass YAML

"provisioner 名字写对了吗?parameters 类型对吗?"——provisioner 字段只是一个标识符,external-provisioner 用它在集群中找到对应 CSIDriver 对象。名字正确不代表 CSI driver 在跑。

实际上,provisioner 名字错了 Events 根本不会出现 ProvisioningFailed——它是 StorageClassNotFound 或不触发任何 Event(因为 controller 找不到谁处理这个 provisioner)。

判断依据:如果你在 Events 中看到 external-provisioner 这个来源,说明 StorageClass 配置是通的。external-provisioner 能定位到说明名字正确,它的报错是它自己的工作和 CSI driver 通信,和你的 YAML 没关系。

❌ 错误方向 2:检查 PVC 的 accessModes

"ReadWriteOnce 还是 ReadWriteMany?会不会是 access mode 写错了?"——accessModes 不影响 PV 创建。它是挂载阶段的约束条件,告诉 K8s "这个卷应该以什么模式挂载到 Pod",不是 provisioning 阶段的检查项。

accessModes 错误会导致 Pod 启动时挂载失败(MountVolume.SetUp failed),不是 PV 创建失败。

❌ 错误方向 3:怀疑资源配额

"ResourceQuota 限制了吧?Namespace 资源用完了?"——配额限制会报 exceeded quota,Events 会明确写 "exceeded quota",不会写 connection closed

ResourceQuota 检查发生在 Pod 创建阶段(Admission Controller),不是 PV provisioning 阶段。PV 是集群资源,不受 Namespace 级别的 ResourceQuota 限制。

✅ 正确排查思路

CSI 驱动的架构决定了排查顺序:

PVC Pending + ProvisioningFailed 事件 → 不用看 StorageClass 了,直接从 CSI 层入手:

  1. CSI driver Pod 在不在 Running(第 2 步)
  2. Driver 日志有没有 socket 错误(第 4 步)
  3. Socket 文件存不存在(第 5 步)
  4. VolumeMounts 配没配(看 Pod spec)

StorageClass 只是一个声明(告诉 K8s "找哪个 provisioner"),CSI driver 是一个运行中的组件(gRPC 服务端)。你在排查一个运行组件的问题,却一直在看一份静态 YAML——方向错了。

这条原则不仅适用 CSI,也适用于 K8s 中其他"配置+组件"分离的场景(Ingress Controller、CNI 插件、CoreDNS 等):配置正确不代表背后的控制器在正常工作。

标点 — 修复 + Check-list

修复

CSI controller 容器挂载卷配置缺失——CSI driver Deployment 的 Pod spec 中缺少 hostPath 卷映射 /csi

apiVersion: apps/v1
kind: Deployment
metadata:
  name: csi-fast-controller
  namespace: kube-system
spec:
  template:
    spec:
      containers:
      - name: csi-driver
        volumeMounts:
        - name: socket-dir
          mountPath: /csi              # driver socket 目录
      - name: csi-provisioner
        volumeMounts:
        - name: socket-dir
          mountPath: /csi              # provisioner 与 driver 共享 socket
      volumes:
      - name: socket-dir
        emptyDir: {}                   # 容器间共享的临时目录

注意两个容器都需要挂载同一个 socket-dir——external-provisioner 和 CSI driver 通过这个共享的 emptyDir 卷交换 gRPC 消息。emptyDir 是 Pod 级别的临时目录,两个容器通过它实现本地 IPC。

修复后重启 CSI driver Pod,PV 创建成功:

kubectl get pvc data-pvc
NAME       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
data-pvc   Bound    pvc-8f2b4e12-7c3d-4a5b-9e1f-6d2a3b4c5d6e  10Gi       RWO            csi-fast-sc    30s

CSI Driver 修复 YAML

预防措施

这类问题修复起来简单(加几行 YAML),但预防更重要。生产环境的 CSI 驱动部署应该做三件事:

  1. CSI driver 加 startupProbe:检查 socket 文件是否创建成功再 readiness——避免 deployment 滚动更新时短暂中断
  2. 监控告警:对 PersistentVolumeClaim ProvisioningFailed 事件设置告警,第一时间发现
  3. 部署清单验收:CSI Driver 部署后自动检查 Pod spec 中 socket-dir volumeMounts 是否存在——可以在 CI 中加个 kubectl 命令验证

金句

"排查 PVC 创建问题不要在 StorageClass YAML 上反复横跳——Events 已经告诉你是 provisioner 连不上 CSI driver,查 driver pod 才是正解。"

Check-list(每条对应一个 kubectl 命令)

PVC Pending 且疑似 CSI 问题时,按此队列排查:

序号 命令 预期输出 异常判断
1 kubectl describe pvc <name> Events 段有 ProvisioningFailed 详情 看 Message 是否含 connection closedUnavailable
2 kubectl get pods -n kube-system -o wide \| grep csi CSI controller + node 均 Running controller CrashLoopBackOff 或 0/1
3 kubectl logs -n kube-system <csi-controller> gRPC server started Failed to listen on socketno such file
4 kubectl exec -n kube-system <csi-controller> -- ls -la /csi/ socket 文件存在 (csi.sock) No such file or directory
5 kubectl get csidriver 列出已注册的 CSI 驱动 目标 provisioner 对应的条目缺失
6 kubectl describe pod -n kube-system <csi-controller> VolumeMounts 包含 socket 目录 缺少 /csi 挂载

C6 下篇预告

下篇我们聊另一个存储踩坑点——动态存储类 StorageClass 参数配置错误,看看 parameters 字段写错一个 key 会导致什么后果。

附:完整命令清单

# 1. PVC 状态
kubectl get pvc data-pvc -o wide
kubectl describe pvc data-pvc

# 2. StorageClass 配置
kubectl get sc csi-fast-sc -o yaml

# 3. CSI 组件 Pod 状态
kubectl get pods -n kube-system -o wide | grep csi

# 4. CSI driver 日志
kubectl logs -n kube-system csi-fast-controller-6b9d75c9d8-4z5m7
kubectl logs -n kube-system -l app=csi-fast-controller

# 5. CSIDriver 注册信息
kubectl get csidriver -o wide
kubectl describe csidriver csi-fast.example.com

# 6. CSI socket 文件验证
kubectl exec -n kube-system csi-fast-controller-6b9d75c9d8-4z5m7 \
  -- ls -la /csi/

# 7. CSI driver 容器挂载卷配置
kubectl describe pod -n kube-system csi-fast-controller-6b9d75c9d8-4z5m7 \
  | grep -A5 VolumeMounts