平台从 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 的概率不会很高。
- 能够实施应用层监控和告警。无硬配额,就必须有实时监控。
不适合的场景包括:
- 超大规模用户(万级以上)
- 用户数据特别不均衡(少数用户占大头)的场景
- 对故障隔离要求极高的系统(比如金融交易)
防护与调整
基于这四个问题,实施中做了以下调整:
- 权限处理:容器 Dockerfile 中预先创建
/data并设置正确的属主;启动脚本检查并修复权限。 - NFS 参数硬化:StorageClass 的挂载选项改为
soft,timeo=100,retrans=3,intr,避免硬 mount 导致节点冻。 - 节点健康检测:cron 每 60 秒检查各节点的 ContainerCreating 堆积和网关探测失败率,及早发现故障。
- 实例分散:StatefulSet 加 topologySpreadConstraints,避免单节点堆积 50+ 个实例。
- 应用层配额:/status 接口返回 diskUsedMi 和 overQuota 标志位,前端告警用户。
这些措施不能消除风险,但可以大幅降低故障概率和影响范围。
■