故障档案

一个 PVC 装所有用户,当时看不出有什么问题

K8s 共享 PVC + subPath 方案在实施中暴露的四个陷阱:权限不匹配、目录创建时机、节点级故障域、无 per-subPath 配额。

平台从 Docker Swarm 迁移到 Kubernetes,存储架构做了一个看似理想的调整:用共享 PVC + subPath 替代原来的"每用户独占 PVC"。方案优势显而易见——管理简单(1 个 PVC 对 574 个)、自动扩展、成本低。当时看不出有什么缺点。

但实施中遇到了四个隐蔽的问题。

权限错配:bind 型数据的属主陷阱

旧系统中,用户数据通过两种方式持久化:bind mount 和 Docker volume。迁移时需要从这两个来源完整导出数据。

对于 bind mount 型数据,文件属主通常是 10001(或其他固定 UID)。当我登上旧集群的宿主机,以普通用户身份尝试读取这些文件时,只能看到 13 个文件。权限拒绝。同一目录里其实有 4778 个文件,但大多数因为属主权限不匹配而不可见。

只有用 sudo 才能完整读到。所以迁移脚本必须用 sudo tar 来打包:

sudo tar -czf /tmp/mig/${uid}/data.tgz \
  -C /mnt/yun-claw/users/${uid} .

这不是脚本设计的问题,而是 Kubernetes 环境下容器权限与宿主权限映射的一个陷阱。容器内运行的网关进程可能以容器用户身份运行,但宿主上的文件属主是不同的 UID。当两个权限体系接不上,访问就会失败。

新方案中,用户数据通过 subPath 挂到容器的 /data 目录。如果 subPath 指向的目录是由 Kubernetes 在宿主机上创建的,属主可能是 root 或其他用户,容器内进程(以容器指定的 UID 运行)仍然会遭遇权限问题。这个风险需要在 Dockerfile 和容器启动时明确处理:容器内应该预先创建 /data 并设置正确的属主,或者使用 securityContext 的 fsGroup 或 runAsUser 来强制权限。

subPath 目录创建的时机与属主处理

Kubernetes 对 subPath 的处理有个微妙之处:如果挂载的 subPath 目录在宿主机上不存在,kubelet 会自动创建它。但 待补:这个自动创建的目录的属主是什么?是 root 还是其他用户?容器的 securityContext 能否保证挂载后的权限符合预期?

设计文档中没有明确说明这一点。在实施前,需要验证:

  • 首次 subPath 目录不存在时,kubelet 创建它并将其属主设置为谁
  • 容器的 securityContext(fsGroup、runAsUser)是否能在挂载时生效
  • 是否应该预先在共享 PVC 上创建所有 subPath 目录并设置好属主,而不是依赖 Kubernetes 的隐式创建

没有明确的答案,就容易踩坑。一个保险做法是在 destroy 用户账户时清空 subPath 目录,同时在 provision 时让一个 init-container 验证或修复目录属主,确保容器进程有写权限。

单 PVC 中的节点级故障域

这是最严重的问题,也是与 FA-015(节点假 Ready)的关联点。

当一个节点的 NFS 客户端或到 SFS 的网络链路发生 hang 时,所有依赖共享 PVC 的 Pod 都会受影响。这不是共享 PVC 本身的问题,而是故障隔离的问题。

具体现象:

  • 如果 NFS 挂载使用了硬 mount(hard 参数是 NFS 的默认值),而网络故障或存储服务中断,NFS 客户端会无限重试
  • 重试期间,所有试图访问 NFS 的进程都会被阻塞在 I/O 上(D 状态,不可中断)
  • 如果 kubelet 或 containerd 的某个操作(如创建容器的卷挂载步骤)陷入 I/O 阻塞,整个节点就会冻死
  • 该节点上的所有实例(无论是否在访问存储)都会因为无法创建或删除 Pod 而故障

这与独立 PVC 的效果截然不同。旧架构中,每个用户有独立 PVC,意味着如果某个 PVC 对应的存储有问题,只会影响这一个用户。其他用户的 PVC 可能分散在不同的存储卷甚至不同的节点上,相对独立。

新架构下,单个共享 PVC 承载所有用户,一旦这个 PVC 对应的网络链路或挂载出问题,同节点上所有实例都会被殃及。如果该节点碰巧堆积了 50 个实例,一次节点级的存储 hang 就会导致 50 个实例集体故障,且外表看起来是"节点 Ready,但实例卡住"——kubelet 的心跳正常,Pod 状态却卡在 ContainerCreating 或 Terminating。

这正是 FA-015 在 2026-06-05 观察到的现象:节点假 Ready,但大量实例网关无响应。防护措施包括:

  • NFS 挂载参数改为软 mount(soft,timeo=100,retrans=3),让 I/O 超时而不是无限等待
  • 节点加健康检测,检查 ContainerCreating 堆积数和网关探测失败率,及早发现节点冻死
  • 实例分散部署,用 topologySpreadConstraints 避免单节点堆积

无 per-subPath 硬配额

共享存储的架构决定了无法在存储层面按 subPath 限制用户配额。SFS(及 NFS 一般)没有 per-directory 的硬配额功能,只能在整个卷级别限制。

这意味着:

  • 无法阻止某个用户的数据膨胀而挤占其他用户的空间
  • 无法在存储层面实现"超额用户的写入失败"
  • 只能在应用层进行监控、告警和逻辑控制

新系统的方案是应用层监控:定期 du -sb /data 扫描每个用户的目录大小,落库到 Instance 表,/status 接口返回超额标志位。前端据此提示用户"存储已满"。这是纯监控,不阻断。如果用户无视提示继续写入,直到共享卷真的满了,才会出现"所有用户集体写入失败"的惨淡局面。

这个限制是方案选择的代价。

为什么仍然选了这个方案

尽管有这些问题,平台仍然采用了共享 PVC + subPath 的方案。原因是对比了替代方案:

方案优点缺点故障隔离
独立 PVC(旧架构)天然的每用户隔离;故障域清晰管理复杂(574 个 PVC);扩展性差;成本高✓ 最好
共享 PVC + subPath(新架构)管理简单(1 个 PVC);自动扩展;成本低故障隔离性差;无硬配额;权限管理复杂✗ 较差
对象存储(S3/OSS)真正的多租户隔离;天然分布延迟高;成本更高;应用改造大✓ 最好,但代价大

独立 PVC 的管理开销是关键问题。574 个用户意味着 574 个 PVC 对象、574 条 PV 绑定、574 个存储卷。每次实例创建、删除或迁移都要涉及 PVC 的生命周期管理。扩展到 5000 用户时,这种开销会成为瓶颈。对象存储虽然隔离性最好,但需要应用层改造(兼容 S3 API、处理延迟、调整备份策略),而且成本更高。

共享 PVC 方案的核心优势是运维简洁:增删用户只需改 subPath(一行 YAML),不涉及存储层操作。代价是故障隔离性从"用户级"降到"节点级"。

适用边界

这个方案适合以下场景:

  • 用户数量有上限(几百到几千)。用户过多时,共享卷的单点压力会成问题。
  • 用户数据量可控(单用户通常 GB 级)。如果单用户数据量达 TB,一次 du 扫描会拖累整体。
  • 可以接受节点级故障隔离。只要网络和存储配置足够稳定(硬化 NFS 参数、冗余链路),节点 hang 的概率不会很高。
  • 能够实施应用层监控和告警。无硬配额,就必须有实时监控。

不适合的场景包括:

  • 超大规模用户(万级以上)
  • 用户数据特别不均衡(少数用户占大头)的场景
  • 对故障隔离要求极高的系统(比如金融交易)

防护与调整

基于这四个问题,实施中做了以下调整:

  1. 权限处理:容器 Dockerfile 中预先创建 /data 并设置正确的属主;启动脚本检查并修复权限。
  2. NFS 参数硬化:StorageClass 的挂载选项改为 soft,timeo=100,retrans=3,intr,避免硬 mount 导致节点冻。
  3. 节点健康检测:cron 每 60 秒检查各节点的 ContainerCreating 堆积和网关探测失败率,及早发现故障。
  4. 实例分散:StatefulSet 加 topologySpreadConstraints,避免单节点堆积 50+ 个实例。
  5. 应用层配额:/status 接口返回 diskUsedMi 和 overQuota 标志位,前端告警用户。

这些措施不能消除风险,但可以大幅降低故障概率和影响范围。

星野的头像

星野 XINGYE

一个人维护 AI 平台的工程师。这里记录 63 篇复盘:18 份故障档案、OTA、架构演进与工作流。