课程目录(第 29 章 / 共 33 章)
文件与对象存储:NFS、CephFS、JuiceFS 与对象网关
把共享文件与对象存储接进集群: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 RBD | NFS、SMB、CephFS | S3、OSS、COS、R2 |
| 并发能力 | 通常单节点读写(RWO) | 多节点多 Pod 共享(RWX) | 海量客户端并发,无挂载概念 |
| 适合的数据 | 数据库、虚拟机磁盘、单写工作负载 | 共享上传目录、CI 缓存、多副本同时读 | 图片视频、备份归档、日志、数据湖 |
| K8s 接入 | PVC(块 CSI 驱动) | PVC(NFS / CephFS / JuiceFS CSI) | 一般不用 PVC,应用用 SDK 直连 |
选型看的是「谁来并发写、要不要 POSIX 语义」,而不是性能数字。
从 PVC 到后端:一张图看清链路
不管选哪条路线,入口都是 PVC,区别只在 StorageClass 指向哪个 CSI 驱动、驱动背后接什么后端。
应用(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(把 server 与 share 换成实际值):
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-nfsapiVersion: 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: trueserver 是服务端地址,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/kube101 并 sudo exportfs -ra)。把下面清单保存为 nfs-rwx-demo.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-datakubectl 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 /datatail 的输出里应交替出现两个不同的 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 可用性 + 数据池可用性」。
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(数据落在哪个池)、mounter(kernel 用内核客户端,性能更好;内核版本太老时才用 fuse)。要支持扩容还得补 controller-expand-secret-name 与 controller-expand-secret-namespace。
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 都 Running、CephFilesystem 输出 Ready、PVC 输出 Bound。把 PVC 的 storageClassName 换成 cephfs-rwx、accessModes 写成 ReadWriteMany,就能用和 NFS 练习里完全一样的方式验证并发读写——对上层来说两条路线的区别只体现在 StorageClass 里。Rook/Ceph 的安装与 OSD 规划见生产存储:CSI 与 Rook/Ceph。
JuiceFS:在对象存储上做 POSIX 文件系统
机房里已有一堆便宜机械盘搭的对象存储、或云上有 S3 时,更省成本的做法是让对象存储当数据盘、再叠一层文件系统——这就是 JuiceFS:元数据(文件名、目录树、权限、锁)放在低延迟数据库里(Metadata Engine),数据块存进对象存储,客户端把两者组合成 POSIX 文件系统。瓶颈因此很明确:小文件与元数据操作看元数据引擎,大文件吞吐看对象存储。
应用 ── /data ──▶ JuiceFS 客户端(CSI 挂载 Pod)
├─ 元数据 ──▶ Metadata Engine(Redis / MySQL / TiKV)
└─ 数据块 ──▶ 对象存储(S3 / OSS / Ceph RGW / RustFS)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:
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: trueSecret 建在 juicefs-csi 命名空间,stringData 必须写全 name(文件系统名)、metaurl(元数据引擎连接串,如 redis://:密码@redis.demo.svc:6379/1)、storage(s3)、bucket(对象存储地址)、access-key、secret-key,并打上标签 juicefs.com/validate-secret: "true";要支持扩容还要加 controller-expand-secret-name/namespace。pathPattern 决定每个 PVC 对应 JuiceFS 里的哪个子目录,不写会让多个 PVC 共用根目录。验证时先确认 PVC 是 Bound,再在挂了它的 Pod 里读写一次,最后看挂载 Pod 与元数据引擎状态:
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,不是文件系统。挂成目录要靠
s3fs、geesefs这类用户态网关,它们只实现 POSIX 子集:不支持随机写、rename不是原子的、文件锁与mmap行为异常,很多程序会写出损坏数据。 - 每次读写都是一次 HTTP 请求,延迟比本地文件系统高一个数量级;应用本来就更适合直连,S3 SDK 的分片上传、断点续传、预签名 URL 经 FUSE 包装后都用不上。唯一例外是遗留程序只会读写本地目录,这时正确做法是用 JuiceFS 这类「对象存储之上的 POSIX 层」,而不是
s3fs。
所以集群里的对象存储通常是两种形态:集群内网关,或集群外的外部对象存储。先看网关,用 RustFS(S3 兼容,MinIO 的平替)举例——它需要一块 PVC 放数据,再用 Service 暴露 S3 与控制台端口,PVC 用普通块存储 StorageClass 即可:
# 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# Ingress 关键片段:控制台再加一条同样的 rule 指向 9001
rules:
- host: s3.rustfs.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: rustfs
port:
number: 9000动手练习:在集群里跑一个对象存储网关
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 所在节点上确认:
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 <挂载点> 清理残留,必要时重启节点。想避免「删不掉」,可以给非关键业务用 soft 加 timeo、retrans,但代价是写失败会返回错误、可能造成数据不一致;核心业务仍然推荐 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。
# 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 用户。
# 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.md、reference/k8s-in-action/storage/gpfs/README.md、reference/k8s-in-action/storage/gpfs-csi/README.md、reference/k8s-in-action/storage/vast/README.md、reference/k8s-in-action/storage/weka/README.md、reference/k8s-in-action/storage/weka/csi/README.md、reference/k8s-in-action/storage/xsky/README.md
常见坑速查表
文件存储的问题大多能归到「服务端不可达」「权限或导出策略不对」「访问模式理解错了」三类。
| 现象 | 原因 | 怎么确认 | 怎么办 |
|---|---|---|---|
PVC 一直 Pending | StorageClass 不存在或名字拼错、server/share 参数错、CSI Controller 未就绪 | kubectl get storageclass、kubectl describe pvc 的 Events、kubectl -n kube-system logs deploy/csi-nfs-controller | 先修名字与参数,再看 Controller 日志里的具体报错 |
事件报 mount.nfs: access denied by server | exports 没包含 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 pv 看 ACCESS MODES;容器里写文件是否报 Read-only file system | 换成支持 RWX 的 StorageClass,或检查导出参数 |
| JuiceFS 的 PVC 挂载失败 | metaurl 写错、元数据引擎不可达、密码不对、Secret 标签缺失 | kubectl -n demo describe pvc、kubectl logs <juicefs-mount-pod>、容器内 nc -zv <host> 6379 | 修 Secret 后重建 PVC,并确认网络策略放行 |
NFS 服务端宕机后 Pod 卡在 Terminating | hard 挂载下进程处于 D 状态,SIGKILL 无效 | 节点上 ps -eo pid,stat,cmd 看 D、dmesg | grep nfs | 先恢复服务端;实在不行用 --grace-period=0 --force 删除并 umount -f -l |
对象存储返回 403 AccessDenied | AccessKey/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 网关(s3fs、geesefs)只能在 HTTP 之上模拟一个 POSIX 子集,很多程序会在这种「看起来像本地目录、语义却不同」的环境里写出损坏数据;同时每次读写都变成一次网络请求,延迟高一个数量级。直连 S3 API 拿到的才是这个存储真正的能力:分片上传、断点续传、预签名 URL、生命周期与版本控制。所以正确姿势是应用改代码用 SDK;只有遗留程序必须读本地路径时,才用 JuiceFS 这类专门为对象存储设计的 POSIX 层去包装。
自测:同样是 RWX,为什么还要特别关注并发写一致性?(点击展开答案)
因为 ReadWriteMany 只保证多个 Pod 能同时挂载这个卷,不保证它们同时写同一个文件不会互相破坏。文件系统的原子性限于单个 write、rename 这类操作,而「读-改-写」这种复合操作不在保证范围内:两个 Pod 同时读同一个文件、各自修改再写回,后写的会覆盖先写的。不同后端的锁语义也不一样:NFSv3 的锁依赖 lockd/statd,NFSv4 把锁做进了协议,JuiceFS 靠元数据引擎实现锁。所以共享写场景要么由应用层加分布式锁,要么让每个 Pod 写独立文件再合并。共享存储解决的是「都能访问」,不是「都能安全地写」。
小结
- 块、文件、对象是三种抽象层次:块存储单写、文件存储多 Pod 共享目录、对象存储走 HTTP API 给应用直连;选型看并发方式与 POSIX 需求,不看性能数字。
- NFS CSI 落地最快,
server+share就能提供 RWX,但要接受单点风险,并用nconnect提带宽、理解hard挂载会卡住进程。 - CephFS 与 RBD 共用 Ceph 集群,多了 MDS 这一环;StorageClass 关键是
fsName、pool、mounter。JuiceFS 则把元数据放数据库、数据放对象存储,用 CSI 挂成 POSIX 卷,元数据引擎选型(Redis / MySQL / TiKV)决定规模上限,生产别用单机 Redis。 - 对象存储一般不挂 PVC:应用用 SDK 直连;集群内网关适合开发与内网隔离,生产优先外部对象存储加 IRSA/Workload Identity 或凭证 Secret。
- 需要几百 GB/s 聚合带宽时用并行文件系统(3FS / GPFS / VAST / WEKA):客户端能容器化就用官方 CSI,必须装内核模块或走 RDMA 就节点预挂载加本地 provisioner,代价是没有配额与 QoS。
- 运维盯三件事:fio 定基线、容量看后端真实使用率、共享存储的爆炸半径远大于单块卷。
练习
- 用
csi-driver-nfs建一个 RWX PVC,起两个 Pod 分别写不同文件名,再用第三个 Pod 列出目录,确认三者视图一致。 - 假设要给 AI 训练任务提供 50TB 数据集:写出你会选的文件存储或对象存储方案,以及如果用 JuiceFS,元数据引擎该选哪个、为什么。
文件与对象存储接上了,可它们健康不健康、延迟有没有变差,光靠 kubectl 看不出来,容量、延迟与错误率要交给指标和告警,见第 21 章 可观测性;如果已经出现挂载失败、Pod 起不来,按第 15 章 排障手册的顺序走一遍。