课程目录(第 29 章 / 共 33 章)
课程/平台化与规模化

文件与对象存储:NFS、CephFS、JuiceFS 与对象网关

29 章 / 共 33·20 分钟·进阶存储NFSCephFSJuiceFS对象存储

把共享文件与对象存储接进集群:NFS CSI 与 CephFS 的 RWX 落地、JuiceFS 在对象存储上做 POSIX 文件系统,以及对象网关该怎么部署。

学完这一章,你将能够

  • 说得出块、文件、对象三种存储的差别,并能为业务选对类型
  • 用 csi-driver-nfs 或 CephFS 给多个 Pod 提供 RWX 共享卷并验证并发读写
  • 理解 JuiceFS 的元数据引擎与对象存储分工,会做元数据引擎选型
  • 知道对象存储为什么不挂 PVC,以及集群内网关与外部对象存储各自的适用场景

一个需求:多个 Pod 要写同一个目录

前面几章里,一个 PVC 基本只服务一个 Pod。但多副本应用常要往同一个上传目录写文件,训练任务要多个 Pod 同时读同一份数据集——这时 ReadWriteOnce(RWO)就不够用了:RWO 卷只能被一个节点挂载读写,多副本 Deployment 里的第二个 Pod 会一直起不来。你需要 ReadWriteMany(RWX),而它取决于后端类型:块存储本质是一块磁盘,只适合单写;文件存储是共享目录,天生支持多写;对象存储连「目录」都没有。这一章讲清后两者:什么时候用、怎么接进集群、踩坑时看哪里。

三种存储:块、文件、对象

三者不是新旧替代,而是三种抽象层次。选错层次,后面所有优化都是在补窟窿。

维度块存储文件存储对象存储
访问方式挂载成块设备再格式化挂载成目录HTTP API(S3)
典型协议iSCSI、NVMe-oF、Ceph RBDNFS、SMB、CephFSS3、OSS、COS、R2
并发能力通常单节点读写(RWO)多节点多 Pod 共享(RWX)海量客户端并发,无挂载概念
适合的数据数据库、虚拟机磁盘、单写工作负载共享上传目录、CI 缓存、多副本同时读图片视频、备份归档、日志、数据湖
K8s 接入PVC(块 CSI 驱动)PVC(NFS / CephFS / JuiceFS CSI)一般不用 PVC,应用用 SDK 直连
选型看的是「谁来并发写、要不要 POSIX 语义」,而不是性能数字。

从 PVC 到后端:一张图看清链路

不管选哪条路线,入口都是 PVC,区别只在 StorageClass 指向哪个 CSI 驱动、驱动背后接什么后端。

text
应用(Pod 里的进程)── 挂载到 /data
  ▼ PVC(声明访问模式 RWX/RWO 与容量)
  ▼ StorageClass(provisioner + parameters)
  ▼ CSI 驱动(Controller 管建卷 / Node 插件管挂载)
  ├─▶ NFS 服务端      csi-driver-nfs:server + share
  ├─▶ CephFS 集群     cephfs.csi.ceph.com:fsName + pool(需要 MDS)
  ├─▶ JuiceFS         csi.juicefs.com → 元数据引擎 + 对象存储
  └─▶ 对象网关(RustFS/MinIO)── 应用多用 S3 SDK 直连,不经过 PVC

看懂这张图就知道排错看哪一层:PVC 的问题看 CSI Controller 日志,挂载的问题看 CSI Node 插件与 kubelet。CSI 的完整架构(Controller / Node / sidecar 分工)见生产存储:CSI 与 Rook/Ceph,本章只讲文件与对象这两条线。

NFS CSI:最省事的共享文件存储

NFS 是最容易落地的 RWX 方案:服务端成熟、客户端在内核里、节点不用装额外程序。适合已有 NAS、多副本共享上传目录、多个 Pod 同时读一份数据;不适合要跑数据库、单点不可接受、锁语义要求严格的场景。先装驱动,再写 StorageClass(把 servershare 换成实际值):

bash
helm repo add csi-driver-nfs https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts && helm repo update
# 版本按 csi-driver-nfs 最新 release 替换
helm install csi-driver-nfs csi-driver-nfs/csi-driver-nfs -n kube-system --version v4.13.1
kubectl -n kube-system get pods | grep csi-nfs
yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-rwx
provisioner: nfs.csi.k8s.io
parameters:
  server: 10.0.0.20
  share: /srv/kube101/volumes
  subDir: ${pvc.metadata.namespace}-${pvc.metadata.name}
mountOptions:
  - nconnect=16
reclaimPolicy: Delete
allowVolumeExpansion: true

server 是服务端地址,share 是已导出的目录,subDir 决定每个 PVC 落在哪个子目录(留空则由驱动按 PV 名建)。nconnect=16 让一个客户端对服务端建多条 TCP 连接,是提升 NFS 带宽最省事的办法;hard(默认)保证写请求永不放弃,代价是服务端宕机时进程会一直卡住。NFS 没有可用区属性,volumeBindingMode: Immediate 即可。

动手练习:给两个 Pod 提供 RWX 共享卷

已有 NAS 就把上面的 server/share 填成实际值(PVC 基础写法见第 10 章 存储卷与 PV/PVC);本地实验可临时装一个(sudo apt-get install -y nfs-kernel-server,导出 /srv/kube101sudo exportfs -ra)。把下面清单保存为 nfs-rwx-demo.yaml

yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: shared-data
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: nfs-rwx
  resources:
    requests:
      storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: writer
spec:
  replicas: 2
  selector:
    matchLabels:
      app: writer
  template:
    metadata:
      labels:
        app: writer
    spec:
      containers:
        - name: app
          image: busybox:1.36
          command: ["sh", "-c", "while true; do echo $(hostname) >> /data/log.txt; sleep 5; done"]
          volumeMounts:
            - name: data
              mountPath: /data
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: shared-data
bash
kubectl create namespace demo --dry-run=client -o yaml | kubectl apply -f -
kubectl -n demo apply -f nfs-rwx-demo.yaml
kubectl -n demo get pvc shared-data
kubectl -n demo exec deploy/writer -- tail -n 6 /data/log.txt
kubectl -n demo exec deploy/writer -- df -h /data

tail 的输出里应交替出现两个不同的 Pod 名(hostname),说明两个节点上的 Pod 在写同一个目录;只有一个名字出现,说明第二个副本没挂上这个卷。df -h 的结果先记住,后面运维小节要用。清理用 kubectl -n demo delete -f nfs-rwx-demo.yaml

CephFS:带 MDS 的共享文件系统

已经在用 Rook/Ceph 做块存储的话,加一份共享文件能力是顺理成章的事:CephFS 与 RBD 共用同一个 Ceph 集群,只是抽象不同——RBD 是块设备(RWO,单节点挂载,适合数据库),CephFS 是 POSIX 文件系统(RWX,多 Pod 共享,适合共享目录与训练数据)。

MDS 是 CephFS 独有的一环,只负责目录树、权限、文件锁这些元数据,数据仍落在 Ceph 存储池里。Rook 把 MDS 部署成主备两个 Pod(rook-ceph-mds-<fs>-a / -b),主挂了备的顶上,所以「文件存储的可用性 = MDS 可用性 + 数据池可用性」。

yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: cephfs-rwx
provisioner: rook-ceph.cephfs.csi.ceph.com
parameters:
  clusterID: rook-ceph
  fsName: myfs
  pool: myfs-data0
  mounter: kernel
  csi.storage.k8s.io/provisioner-secret-name: rook-csi-cephfs-provisioner
  csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph
  csi.storage.k8s.io/node-stage-secret-name: rook-csi-cephfs-node
  csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph
reclaimPolicy: Delete
allowVolumeExpansion: true

关键参数只有四个:clusterID(Rook 里就是集群所在命名空间)、fsName(哪个 CephFS)、pool(数据落在哪个池)、mounterkernel 用内核客户端,性能更好;内核版本太老时才用 fuse)。要支持扩容还得补 controller-expand-secret-namecontroller-expand-secret-namespace

bash
kubectl -n rook-ceph get pods -l app=rook-ceph-mds
kubectl -n rook-ceph get cephfilesystem myfs -o jsonpath='{.status.phase}'
kubectl -n demo get pvc cephfs-data -o jsonpath='{.status.phase}'

验证三步:MDS 两个 Pod 都 RunningCephFilesystem 输出 Ready、PVC 输出 Bound。把 PVC 的 storageClassName 换成 cephfs-rwxaccessModes 写成 ReadWriteMany,就能用和 NFS 练习里完全一样的方式验证并发读写——对上层来说两条路线的区别只体现在 StorageClass 里。Rook/Ceph 的安装与 OSD 规划见生产存储:CSI 与 Rook/Ceph

JuiceFS:在对象存储上做 POSIX 文件系统

机房里已有一堆便宜机械盘搭的对象存储、或云上有 S3 时,更省成本的做法是让对象存储当数据盘、再叠一层文件系统——这就是 JuiceFS:元数据(文件名、目录树、权限、锁)放在低延迟数据库里(Metadata Engine),数据块存进对象存储,客户端把两者组合成 POSIX 文件系统。瓶颈因此很明确:小文件与元数据操作看元数据引擎,大文件吞吐看对象存储。

text
应用 ── /data ──▶ JuiceFS 客户端(CSI 挂载 Pod)
                    ├─ 元数据 ──▶ Metadata Engine(Redis / MySQL / TiKV)
                    └─ 数据块 ──▶ 对象存储(S3 / OSS / Ceph RGW / RustFS)
bash
helm repo add juicefs https://juicedata.github.io/charts/ && helm repo update
# 版本按 JuiceFS CSI 最新 release 替换
helm install juicefs-csi-driver juicefs/juicefs-csi-driver -n juicefs-csi --create-namespace --version 0.30.4
kubectl -n juicefs-csi get pods

驱动只是管道,真正定义文件系统的是 Secret 里的连接信息与引用它的 StorageClass:

yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: juicefs-rwx
provisioner: csi.juicefs.com
parameters:
  csi.storage.k8s.io/provisioner-secret-name: juicefs-secret
  csi.storage.k8s.io/provisioner-secret-namespace: juicefs-csi
  csi.storage.k8s.io/node-publish-secret-name: juicefs-secret
  csi.storage.k8s.io/node-publish-secret-namespace: juicefs-csi
  pathPattern: "${.pvc.namespace}-${.pvc.name}"
reclaimPolicy: Retain
allowVolumeExpansion: true

Secret 建在 juicefs-csi 命名空间,stringData 必须写全 name(文件系统名)、metaurl(元数据引擎连接串,如 redis://:密码@redis.demo.svc:6379/1)、storages3)、bucket(对象存储地址)、access-keysecret-key,并打上标签 juicefs.com/validate-secret: "true";要支持扩容还要加 controller-expand-secret-name/namespacepathPattern 决定每个 PVC 对应 JuiceFS 里的哪个子目录,不写会让多个 PVC 共用根目录。验证时先确认 PVC 是 Bound,再在挂了它的 Pod 里读写一次,最后看挂载 Pod 与元数据引擎状态:

bash
kubectl -n demo exec deploy/app -- sh -c 'echo hi > /data/t.txt && cat /data/t.txt'
kubectl get pods -A | grep juicefs-
kubectl -n juicefs-csi exec <mount-pod> -- juicefs status redis://:REPLACE_PASSWORD@redis.demo.svc:6379/1

挂载 Pod 名字形如 juicefs-<pvc 名>-<hash>juicefs status 会打印文件数、对象存储用量与连接状态——这是判断「JuiceFS 到底能不能用」最快的命令

元数据引擎的选型直接决定规模上限,下面是官方给的经验量级(实际以你的文件数与元数据操作压测为准):Redis(单机 / Sentinel / Cluster)适合文件数 1 亿以内,最省事,单机没有 HA;PostgreSQL / MySQL 适合 10 亿级,主从复制加事务;TiKV 适合百亿级,Raft 多副本、强一致,运维成本最高。一句话取舍:开发用单机 Redis,生产别用单机 Redis——它一挂整个文件系统就挂载不了,而换引擎意味着一次元数据迁移。

对象存储怎么接进集群

最常见的误区是「给对象存储也做个 PVC 挂给应用」,这条路通常走不通,也不该走:

  • 它的接口是 HTTP API,不是文件系统。挂成目录要靠 s3fsgeesefs 这类用户态网关,它们只实现 POSIX 子集:不支持随机写、rename 不是原子的、文件锁与 mmap 行为异常,很多程序会写出损坏数据。
  • 每次读写都是一次 HTTP 请求,延迟比本地文件系统高一个数量级;应用本来就更适合直连,S3 SDK 的分片上传、断点续传、预签名 URL 经 FUSE 包装后都用不上。唯一例外是遗留程序只会读写本地目录,这时正确做法是用 JuiceFS 这类「对象存储之上的 POSIX 层」,而不是 s3fs

所以集群里的对象存储通常是两种形态:集群内网关,或集群外的外部对象存储。先看网关,用 RustFS(S3 兼容,MinIO 的平替)举例——它需要一块 PVC 放数据,再用 Service 暴露 S3 与控制台端口,PVC 用普通块存储 StorageClass 即可:

yaml
# Deployment 关键片段(补全 metadata、selector、labels、volumes 后即为完整清单)
containers:
  - name: rustfs
    image: rustfs/rustfs:1.0.0-rc.4-preview.1  # tag 按最新 release 替换
    envFrom:
      - secretRef:
          name: rustfs-credentials
    ports:
      - name: s3
        containerPort: 9000
    volumeMounts:
      - name: data
        mountPath: /data
---
# Service 关键片段
spec:
  selector:
    app: rustfs
  ports:
    - name: s3
      port: 9000
      targetPort: 9000
yaml
# Ingress 关键片段:控制台再加一条同样的 rule 指向 9001
rules:
  - host: s3.rustfs.example.com
    http:
      paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: rustfs
              port:
                number: 9000

动手练习:在集群里跑一个对象存储网关

bash
kubectl -n demo create secret generic rustfs-credentials \
  --from-literal=RUSTFS_ACCESS_KEY=rustfsadmin --from-literal=RUSTFS_SECRET_KEY=rustfsadmin
kubectl -n demo apply -f rustfs.yaml
kubectl -n demo rollout status deploy/rustfs
kubectl -n demo run curl --rm -it --restart=Never --image=curlimages/curl:8.10.1 -- \
  curl -s -i http://rustfs.demo.svc:9000/

不带凭证访问根路径,返回 403 加一段 <Error><Code>AccessDenied</Code> 就是正常的:服务活着,并且要求鉴权。再用 S3 客户端建桶验证,make_bucket: demo-bucket 即成功(amazon/aws-cli 镜像,tag 按最新 release 替换):kubectl -n demo run awscli --rm -it --restart=Never --image=amazon/aws-cli:2.15.17 --env=AWS_ACCESS_KEY_ID=rustfsadmin --env=AWS_SECRET_ACCESS_KEY=rustfsadmin -- aws --endpoint-url http://rustfs.demo.svc:9000 s3 mb s3://demo-bucket--endpoint-url 必须写,否则客户端会去访问真实的 AWS S3。

网关能跑起来,但它不适合直接当生产方案:单副本 Deployment 是单点,数据落在单块 RWO 卷上,Pod 重建还可能起不来。生产上更常见的是外部对象存储 + 集群内凭证:用云上的 S3/OSS/COS 或机房里的 Ceph RGW,应用用 SDK 直连,凭证通过 Secret 注入,或在支持 OIDC 的云上给 ServiceAccount 绑定角色(EKS 的 IRSA、GKE 的 Workload Identity),完全不落地长期 AccessKey(见第 9 章 ConfigMap 与 Secret)。真要在集群里自建,就用 StatefulSet 多副本加独立数据盘,而不是 local-path 这种单节点目录;S3 API 尽量只在内网暴露,控制台才考虑用 Ingress 暴露并加认证。

性能与运维:几个容易搞错的点

用 fio 定基线。 网络文件存储的性能高度依赖网络与服务端,别凭感觉判断。在挂了 PVC 的 Pod 里跑一轮顺序写(kubectl -n demo exec -it fio-test -- fio --name=nfs-seqwrite --ioengine=libaio --direct=1 --rw=write --bs=1M --iodepth=16 --numjobs=4 --runtime=60 --time_based --group_reporting --filename=/data/testfile --size=4G),再和服务端本地裸盘对比。

重点看 BW=(带宽)与 clat percentiles 里的 99.00th(长尾延迟)。把 nconnect 从默认改成 16 再跑一遍:带宽明显提升说明瓶颈在单条 TCP 连接,没变化则多半在服务端磁盘或网络。fio 参数含义与块存储对比见生产存储:CSI 与 Rook/Ceph

`df -h` 看到的往往不是你的配额。 练习里 df -h /data 显示的容量大概率不是 5Gi:csi-driver-nfs 只是「在服务端建一个子目录」,它不设配额,所以你看到的是 NFS 服务端整个文件系统的大小,PVC 申请的 5Gi 只是一个数字。只有当后端支持并启用了目录配额(如 CephFS 的 ceph.quota.max_bytes、JuiceFS 的目录配额)时,PVC 容量才是真正的上限。所以容量告警要盯后端真实使用率,而不是 PVC 申请值。

NFS 卡住会让 Pod 删不掉。 hard 挂载下,服务端无响应时应用进程进入 D 状态(不可中断睡眠),kubelet 发 SIGTERM、再发 SIGKILL 都没用——它在等内核里的 RPC 返回,不会处理信号,于是 Pod 停在 Terminating。在 Pod 所在节点上确认:

bash
ps -eo pid,stat,wchan:24,cmd | awk '$2 ~ /D/'
dmesg -T | grep -i nfs | tail

处理顺序是先恢复 NFS 服务端,进程会自动解除阻塞;服务端确实回不来了,再 kubectl delete pod writer-xxx --grace-period=0 --force,并在该节点上 umount -f -l <挂载点> 清理残留,必要时重启节点。想避免「删不掉」,可以给非关键业务用 softtimeoretrans,但代价是写失败会返回错误、可能造成数据不一致;核心业务仍然推荐 hard 加高可用服务端(双控制器 NAS,或 DRBD/keepalived 这类方案)。

单点与高可用要提前想。 自建 NFS 的单点就是服务端本身;CephFS 的高可用来自 MDS 主备与数据池多副本;JuiceFS 的高可用来自元数据引擎与对象存储;集群内对象网关则要看副本数与数据后端。共享存储挂掉影响的是所有挂载它的 Pod,爆炸半径远大于单块 RWO 卷。 另外快照不等于备份:csi-driver-nfs 本身不支持 VolumeSnapshot,快照得靠 NAS 自己的能力;CephFS 可以走 external-snapshotter,但快照与源数据在同一个 Ceph 集群里;JuiceFS 要把元数据(juicefs dump)和数据一起备份,缺一半都恢复不出来;对象存储靠桶版本控制、生命周期策略与跨区域复制,防勒索还要开对象锁定。跨介质备份与恢复演练见第 20 章 备份与恢复

并行文件系统:3FS、GPFS、VAST、WEKA 各自解决什么

前面三条路线(NFS / CephFS / JuiceFS)解决的都是「多个 Pod 共享一个目录」,带宽上限通常在几 GB/s 量级。AI 训练、HPC、渲染这类负载要的是几百 GB/s 的聚合带宽和千万级 IOPS:同一个数据集被上百个 Pod 同时读,通用文件存储的元数据与网络会先成为瓶颈。这时上的是并行文件系统——数据被打散到多个存储节点,客户端并行访问,不再经过单一网关。

系统一句话定位典型场景
3FS用 SSD 加 RDMA 拼出的高性能分布式文件系统,为 AI 训练/推理的数据加载与 checkpoint 而生大模型训练数据集、checkpoint 落盘
GPFS(IBM Storage Scale)老牌企业级并行文件系统,多租户、配额、快照、审计最齐全HPC、需要严格配额与多租户隔离的平台
VAST全闪统一存储,文件、S3、块一套系统里都有,卖点是 QoS 与多租户多团队共享的全闪 AI 平台
WEKA全闪并行文件系统,客户端软件直连,吞吐与延迟都很低高吞吐训练、媒体渲染、基因测序
XSky国内厂商的分布式存储方案,块/文件/对象都有,硬件按厂商推荐配置已采购该厂商存储的企业
选它们的原因不是「更高级」,而是并发带宽:单台 NFS 或通用分布式文件系统先撑不住,再往上才是并行文件系统。

接进 K8s:CSI 建卷还是节点预挂载

两种接法的区别在「谁负责建卷、谁负责挂载」:

接法谁建卷谁挂载适用
官方 CSI 驱动CSI provisioner 按 PVC 建(如 GPFS 的 fileset、VAST 的 storagePath)CSI Node 插件在容器里挂需要每个 PVC 有配额、QoS、快照
节点预挂载 + 本地 provisioner节点上已挂好文件系统,K8s 只切子目录节点内核或厂商专用客户端客户端需要内核模块/RDMA、容器里装不了

为什么会有第二种?因为不少并行文件系统要求宿主机装专用客户端、加载内核模块、走 RDMA 或做 license 校验,这些在容器里做不了;多租户环境里原生 CSI 也常常拿不到建 fileset 的权限。这时常见做法是:节点上先挂好(如 /mnt/gpfs1/weka),再用 local-path-provisioner 把那个目录暴露成一个 StorageClass。

yaml
# local-path-provisioner 的 ConfigMap 片段:把已挂载的并行文件系统暴露成 StorageClass
apiVersion: v1
kind: ConfigMap
metadata:
  name: local-path-config
  namespace: local-path-storage
data:
  config.json: |-
    {
      "nodePathMap": [
        {"node": "DEFAULT_PATH_FOR_NON_LISTED_NODES", "paths": ["/mnt/gpfs1/volumes"]}
      ]
    }

代价要讲清楚:这条路没有配额和 QoS,PVC 申请的 100Gi 只是一个数字,多个 Pod 会争同一份后端带宽;要配额就得让后端先划好 fileset 或 storagePath,再让应用写到各自子目录。另外两种混合形态也常见:VAST 的文件接入走 NFS CSI(比社区版多 Quota 与 QoS,挂载加 nconnect=16),块接入走 NVMe-oF CSI,节点要先装 nvme-cli;WEKA 的官方 CSI 前置条件是节点已装 Weka 客户端。

GPFS 的 CSI 通过 REST API 建 fileset,所以前置条件比普通 CSI 多一步:要有 GUI 服务和一个 CsiAdmin 用户。

bash
# GPFS:所有节点先挂好文件系统,CSI 通过 GUI 的 REST API 建 fileset
mount | grep gpfs
/usr/lpp/mmfs/gui/cli/mkuser csiuser -p <密码> -g CsiAdmin
curl --insecure -u 'csiuser:<密码>' -X GET https://<gui-host>:443/scalemgmt/v2/nodes
kubectl label node gn01 scale=true --overwrite=true

选接法就问三个问题:后端能不能按 PVC 建出带配额的目录?能就用 CSI。客户端要不要装内核模块或 RDMA?要就预挂载。多租户下 CSI 有没有建目录的权限?没有就预挂载加后端分配。

参考:reference/k8s-in-action/storage/3fs/README.mdreference/k8s-in-action/storage/gpfs/README.mdreference/k8s-in-action/storage/gpfs-csi/README.mdreference/k8s-in-action/storage/vast/README.mdreference/k8s-in-action/storage/weka/README.mdreference/k8s-in-action/storage/weka/csi/README.mdreference/k8s-in-action/storage/xsky/README.md

常见坑速查表

文件存储的问题大多能归到「服务端不可达」「权限或导出策略不对」「访问模式理解错了」三类。

现象原因怎么确认怎么办
PVC 一直 PendingStorageClass 不存在或名字拼错、server/share 参数错、CSI Controller 未就绪kubectl get storageclasskubectl describe pvc 的 Events、kubectl -n kube-system logs deploy/csi-nfs-controller先修名字与参数,再看 Controller 日志里的具体报错
事件报 mount.nfs: access denied by serverexports 没包含 Pod 所在节点网段、导出目录权限不对、root_squash 与容器 root 用户冲突在节点上 showmount -e <server>exportfs -v,比对 Pod 所在节点 IP调整 exports 网段与 no_root_squash,或改用匹配的 uid
声明了 RWX,但只有一个 Pod 能写卷实际是 RWO 块存储、PV 的 AccessModes 不含 ReadWriteMany、NFS 导出是只读kubectl get pvACCESS MODES;容器里写文件是否报 Read-only file system换成支持 RWX 的 StorageClass,或检查导出参数
JuiceFS 的 PVC 挂载失败metaurl 写错、元数据引擎不可达、密码不对、Secret 标签缺失kubectl -n demo describe pvckubectl logs <juicefs-mount-pod>、容器内 nc -zv <host> 6379修 Secret 后重建 PVC,并确认网络策略放行
NFS 服务端宕机后 Pod 卡在 Terminatinghard 挂载下进程处于 D 状态,SIGKILL 无效节点上 ps -eo pid,stat,cmdDdmesg | grep nfs先恢复服务端;实在不行用 --grace-period=0 --force 删除并 umount -f -l
对象存储返回 403 AccessDeniedAccessKey/Secret 错、桶策略不含该操作、endpoint 或 Region 写错、HTTPS 写成 HTTP用同一凭证执行 aws --endpoint-url ... s3 ls 看原始报错核对 Secret、桶策略与 endpoint、Region、签名版本

三条最贵的教训

  • 别在 RWX 上跑数据库:SQLite 等嵌入式数据库依赖文件锁与 fsync 语义,放到 NFS/CephFS 上很容易损坏数据;数据库要块存储。
  • 别把 PVC 申请容量当配额:NFS 场景下 df -h 显示的是服务端总容量,容量告警要盯后端。
  • 别用单机 Redis 当 JuiceFS 元数据引擎跑生产:它一挂整个文件系统就挂载不了,而迁移引擎成本很高。

自测题

自测:一个业务要「多个 Pod 同时读写同一个上传目录」,另一个要「存 500TB 图片」,分别选什么存储?(点击展开答案)

共享上传目录要的是多写加 POSIX 目录语义,所以选文件存储:NFS CSI(简单,已有 NAS 就用)或 CephFS(要更高可用性与扩展性),判断依据是 ReadWriteMany 加普通文件读写。500TB 图片要的是海量对象加低成本加 HTTP 分发,所以选对象存储:应用用 S3 SDK 直连,配合 CDN 与生命周期策略,而不是挂 PVC。如果同时要「POSIX 路径」和「对象存储的成本」,就用 JuiceFS:对象存储存数据,元数据引擎提供目录语义,CSI 驱动挂成 RWX 卷。

自测:为什么对象存储通常不用 PVC 挂载,而是让应用直连 S3 API?(点击展开答案)

因为对象存储的抽象层级和文件系统不同:没有目录树、没有原地随机写、rename 不是原子操作、也没有可靠的文件锁。FUSE 网关(s3fsgeesefs)只能在 HTTP 之上模拟一个 POSIX 子集,很多程序会在这种「看起来像本地目录、语义却不同」的环境里写出损坏数据;同时每次读写都变成一次网络请求,延迟高一个数量级。直连 S3 API 拿到的才是这个存储真正的能力:分片上传、断点续传、预签名 URL、生命周期与版本控制。所以正确姿势是应用改代码用 SDK;只有遗留程序必须读本地路径时,才用 JuiceFS 这类专门为对象存储设计的 POSIX 层去包装。

自测:同样是 RWX,为什么还要特别关注并发写一致性?(点击展开答案)

因为 ReadWriteMany 只保证多个 Pod 能同时挂载这个卷,不保证它们同时写同一个文件不会互相破坏。文件系统的原子性限于单个 writerename 这类操作,而「读-改-写」这种复合操作不在保证范围内:两个 Pod 同时读同一个文件、各自修改再写回,后写的会覆盖先写的。不同后端的锁语义也不一样:NFSv3 的锁依赖 lockd/statd,NFSv4 把锁做进了协议,JuiceFS 靠元数据引擎实现锁。所以共享写场景要么由应用层加分布式锁,要么让每个 Pod 写独立文件再合并。共享存储解决的是「都能访问」,不是「都能安全地写」。

小结

  • 块、文件、对象是三种抽象层次:块存储单写、文件存储多 Pod 共享目录、对象存储走 HTTP API 给应用直连;选型看并发方式与 POSIX 需求,不看性能数字。
  • NFS CSI 落地最快,server + share 就能提供 RWX,但要接受单点风险,并用 nconnect 提带宽、理解 hard 挂载会卡住进程。
  • CephFS 与 RBD 共用 Ceph 集群,多了 MDS 这一环;StorageClass 关键是 fsNamepoolmounter。JuiceFS 则把元数据放数据库、数据放对象存储,用 CSI 挂成 POSIX 卷,元数据引擎选型(Redis / MySQL / TiKV)决定规模上限,生产别用单机 Redis。
  • 对象存储一般不挂 PVC:应用用 SDK 直连;集群内网关适合开发与内网隔离,生产优先外部对象存储加 IRSA/Workload Identity 或凭证 Secret。
  • 需要几百 GB/s 聚合带宽时用并行文件系统(3FS / GPFS / VAST / WEKA):客户端能容器化就用官方 CSI,必须装内核模块或走 RDMA 就节点预挂载加本地 provisioner,代价是没有配额与 QoS。
  • 运维盯三件事:fio 定基线、容量看后端真实使用率、共享存储的爆炸半径远大于单块卷。

练习

  1. csi-driver-nfs 建一个 RWX PVC,起两个 Pod 分别写不同文件名,再用第三个 Pod 列出目录,确认三者视图一致。
  2. 假设要给 AI 训练任务提供 50TB 数据集:写出你会选的文件存储或对象存储方案,以及如果用 JuiceFS,元数据引擎该选哪个、为什么。

文件与对象存储接上了,可它们健康不健康、延迟有没有变差,光靠 kubectl 看不出来,容量、延迟与错误率要交给指标和告警,见第 21 章 可观测性;如果已经出现挂载失败、Pod 起不来,按第 15 章 排障手册的顺序走一遍。