AI Infra 训练营
总览
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
  • 心智模型
  • 看懂命令输出
  • 容器网络底层
  • K8s 网络深入
  • DNS 全套
  • 故障排查方法论
  • 心智模型
  • 容器挂载完整指南
  • K8s Volumes 大全
  • PV/PVC/CSI 深入
  • NFS 深入
  • 分布式存储概览
  • 故障排查 runbook
HiHuo 主站
GitHub
总览
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
  • 心智模型
  • 看懂命令输出
  • 容器网络底层
  • K8s 网络深入
  • DNS 全套
  • 故障排查方法论
  • 心智模型
  • 容器挂载完整指南
  • K8s Volumes 大全
  • PV/PVC/CSI 深入
  • NFS 深入
  • 分布式存储概览
  • 故障排查 runbook
HiHuo 主站
GitHub
  • 存储专题

    • 存储心智模型:从一次 write() 到磁盘
    • 容器挂载完整指南:bind mount / volume / mount propagation / 多视角
    • K8s Volume 类型大全 + subPath / 多视角
    • PV / PVC / StorageClass / CSI 深入
    • NFS 深入:server / client / K8s NFS / 经典坑
    • 分布式存储概览:Ceph / Longhorn / Rook
    • 存储故障排查 runbook

NFS 深入:server / client / K8s NFS / 经典坑

NFS 是企业里最普及的共享存储——简单、便宜、随处可用。但 NFS 也是 K8s 集群里最容易踩坑的存储——pod 卡 D 状态、stale handle、性能踩坑、节点挂了所有 pod 跟着挂。

这一篇把 NFS 从协议到 K8s 集成讲透。

这篇要回答什么

  1. NFSv3 / NFSv4 / NFSv4.1 区别?K8s 该用哪个?
  2. NFS server 怎么搭、有哪些关键参数?
  3. NFS mount options 那么多、哪些是关键的?
  4. 为啥 NFS server 一挂 pod 全卡 D 状态?
  5. K8s 怎么用 NFS PV?什么时候不该用?
  6. soft vs hard mount、sync vs async、tcp vs udp 怎么选?
  7. stale file handle 是什么?怎么修?

1. NFS 协议简介

graph LR
    subgraph 客户端节点
        App["应用<br>open() / read() / write()"]
        VFS["VFS"]
        NFSClient["NFS Client<br>(kernel)"]
    end

    subgraph 网络[网络 RPC over TCP/UDP]
        Net["mount/rpcbind/nfs/lockd"]
    end

    subgraph NFS服务端
        NFSD["nfsd<br>(kernel)"]
        ExportFS["导出的目录<br>/exports/data"]
    end

    App --> VFS --> NFSClient
    NFSClient -.RPC.-> Net
    Net -.RPC.-> NFSD
    NFSD --> ExportFS

    style 客户端节点 fill:#e1f5ff
    style NFS服务端 fill:#ffe1f5

NFS 把远端目录"投射"到本地文件系统——应用以为在本地操作、实际走网络。

NFSv3 vs NFSv4 vs NFSv4.1

维度NFSv3NFSv4NFSv4.1
端口多端口(111 rpcbind + lockd + nfs)单端口 2049单端口 2049
防火墙友好❌ 难配✅✅
状态无状态有状态(更可靠)有状态
锁机制单独 lockd内嵌内嵌
ACL弱POSIX 兼容 + Win同 v4
并行 I/O❌❌✅ pNFS
K8s 推荐简单场景生产推荐大规模

结论:K8s 集群里用 NFSv4 / v4.1——单端口、防火墙好配、有状态更可靠。

看 NFS 协议版本

$ mount | grep nfs
nfs.example.com:/exports/data on /mnt/data type nfs4 (...,vers=4.2,...)
                                                    ^^^^^^^^^
                                                    协议版本

强制版本:

mount -t nfs -o vers=4.2 nfs.example.com:/exports/data /mnt/data

2. NFS server 搭建 (Ubuntu)

# 1. 装 NFS server
apt install -y nfs-kernel-server

# 2. 创建导出目录
mkdir -p /exports/data
chown nobody:nogroup /exports/data
chmod 0777 /exports/data                    # 简化(实际要细粒度)

# 3. 配置导出
cat > /etc/exports <<'EOF'
/exports/data 10.0.24.0/24(rw,sync,no_subtree_check,no_root_squash,fsid=0)
EOF

# 4. 应用
exportfs -ra
systemctl enable --now nfs-kernel-server

# 5. 验证
exportfs -v
showmount -e localhost

/etc/exports 字段解读

/exports/data 10.0.24.0/24(rw,sync,no_subtree_check,no_root_squash,fsid=0)
^^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(1)           (2)          (3)
标记含义
(1)导出的目录
(2)允许哪些客户端(CIDR / hostname / *)
(3)选项

关键选项:

选项含义建议
rw / ro可写 / 只读看需求
sync / asyncserver 写盘 sync vs async生产用 sync(数据可靠)
no_subtree_check关闭子树检查推荐开(性能 + 避免边缘 bug)
root_squash (默认)把 root 用户映射成 nobody安全推荐
no_root_squash允许 root 真 root慎用(K8s pod 用 root 写 NFS 需要)
all_squash所有用户映射 nobody公共只读场景
anonuid=N / anongid=N指定 squash 用户的 UID/GID
secure (默认)客户端必须用 <1024 端口防火墙严要求
insecure允许 >1024 端口容器场景常需要
fsid=N / fsid=root文件系统 IDNFSv4 root 必加

多个导出

/exports/shared    10.0.24.0/24(rw,sync,no_subtree_check)
/exports/readonly  *(ro,sync,no_subtree_check)
/exports/configs   10.0.24.5(rw,sync,no_subtree_check,no_root_squash) 10.0.24.6(ro,sync)

注意客户端和 (选项) 之间没空格:

/exports 10.0.24.0/24(rw)        ✅
/exports 10.0.24.0/24 (rw)       ❌ 不一样!第二种 = 默认 ro

少一个细节让你权限全错。

exportfs 操作

exportfs -v                       # 看当前 exports
exportfs -ra                       # reload 配置
exportfs -u <client>:<path>        # 卸下某客户端
exportfs -au                        # 全卸

showmount —— 列 server 的 exports

# 本机
$ showmount -e localhost
Export list for localhost:
/exports/data    10.0.24.0/24

# 远端
$ showmount -e nfs.example.com

K8s 排错快速验证 NFS server 健康用。


3. NFS client mount

# 临时挂
mount -t nfs -o vers=4.2,rw,hard,timeo=600,retrans=2 \
  nfs.example.com:/exports/data /mnt/data

# 持久(/etc/fstab)
echo "nfs.example.com:/exports/data /mnt/data nfs vers=4.2,rw,hard,timeo=600,retrans=2,_netdev,nofail 0 0" \
  >> /etc/fstab

mount options 关键讲解

hard vs soft
hard       client 重试到天荒地老(默认)
soft       超时后报错给应用

生产 99% 用 hard——soft 在 NFS server 重启 / 网络抽风时会让应用拿到 EIO 错误、数据可能丢。

代价:server 真挂了 client 卡死、umount 也卡。

tcp vs udp
proto=tcp  (默认 v4+)
proto=udp  (老 v3 默认)

用 TCP——更可靠、更稳。UDP 在拥塞时丢包多。

sync vs async
sync       client 同步写、慢但安全
async      client 异步写、快但断电可能丢

mount 选项的 sync 和 server 端的 sync 含义不同。client 的 async(默认)+ server 的 sync = 数据安全的常见组合。

timeo / retrans
timeo=600        每次 RPC 超时秒数 (默认 60 = 6秒)
retrans=2         重试次数 (默认 3)

timeo 是十分之一秒、所以 timeo=600 = 60 秒。

NFS server 偶尔卡几秒 → 不希望立即失败。 但 hard mount 下 timeo 大不会影响最终行为、只影响内核 log 输出节奏。

noatime / nodiratime
noatime         访问文件不更新 access time
nodiratime      访问目录不更新 access time
relatime        relative atime (默认)

NFS 上 atime 更新 = 每次 read 也变 write → 性能差 + 不必要写。生产几乎都加 noatime,nodiratime。

_netdev / nofail
_netdev         告诉 systemd 这是网络依赖
nofail          挂不上不让系统启动失败

/etc/fstab 里 NFS 行必加 _netdev,nofail——见 fstab.md。

标准生产 mount options

mount -t nfs -o \
  vers=4.2,proto=tcp,\
  hard,timeo=600,retrans=2,\
  rsize=1048576,wsize=1048576,\
  noatime,nodiratime,\
  _netdev,nofail \
  nfs.example.com:/exports/data /mnt/data
选项作用
vers=4.2NFSv4.2
proto=tcpTCP(v4 默认)
hard卡死也不放弃
timeo=60060s 超时
retrans=2重试 2 次
rsize/wsize=1M单次读写大小 1M(大文件大幅提升)
noatime不更新 atime
_netdev,nofailsystemd 友好

4. K8s 用 NFS 的几种方式

方式 1:直接 NFS volume(不推荐)

volumes:
  - name: data
    nfs:
      server: nfs.example.com
      path: /exports/data
      readOnly: false

不经过 PV / PVC、每个 pod 自己挂。问题:

  • 不能 K8s 管理(扩缩 / 监控 / 配额)
  • 每个 pod 都自己写 server / path、易错
  • 没有 StorageClass 抽象

只在快速测试时用。

方式 2:静态 NFS PV(推荐)

apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv-shared
spec:
  capacity:
    storage: 100Gi
  accessModes: [ReadWriteMany]            # NFS 天生 RWX
  persistentVolumeReclaimPolicy: Retain
  nfs:
    server: nfs.example.com
    path: /exports/data
  mountOptions:                            # 设 mount options
    - vers=4.2
    - hard
    - timeo=600
    - rsize=1048576
    - wsize=1048576
    - noatime
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: shared-data
spec:
  accessModes: [ReadWriteMany]
  resources:
    requests:
      storage: 100Gi
  volumeName: nfs-pv-shared                # 显式绑定

方式 3:NFS CSI driver(生产推荐)

# 装 csi-driver-nfs
helm install csi-driver-nfs csi-driver-nfs/csi-driver-nfs \
  --namespace kube-system \
  --set kubeletDir=/var/lib/kubelet
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-sc
provisioner: nfs.csi.k8s.io
parameters:
  server: nfs.example.com
  share: /exports/data                     # 父目录
  subDir: ${pvc.metadata.namespace}-${pvc.metadata.name}   # 每个 PVC 一个子目录
reclaimPolicy: Retain
volumeBindingMode: Immediate
mountOptions:
  - vers=4.2
  - hard
  - timeo=600
  - noatime

效果:

$ kubectl apply -f pvc.yaml
# StorageClass 自动:
#   - 在 NFS server 的 /exports/data 下创建子目录 default-my-pvc/
#   - 创建 PV 包装这子目录
#   - bind PVC

方式 4:NFS Subdir External Provisioner(更早期方案)

类似 csi-driver-nfs、但用老式 external-provisioner。已经被 csi-driver-nfs 替代、新部署用 CSI。


5. NFS 在 K8s 里的"幽灵 pod" 问题

生产事故 #1:NFS server 挂导致所有 pod 卡 D

1. 集群 N 个 pod 用 NFS PV
2. NFS server 突然宕机(或网络断 5 分钟)
3. 客户端 hard mount → 应用 syscall 卡内核 D 状态
4. kubelet 健康检查也卡(因为也访问 NFS 文件)
5. **kubelet 失去和 apiserver 心跳** → 节点 NotReady
6. 所有 pod(不只是用 NFS 的)被影响

为什么 hard mount 还是用 hard?因为 soft 时应用直接拿 EIO、可能数据丢更糟。两害相权取其轻。

解决思路(不能完美、只能缓解):

  1. NFS server 高可用(双机热备 / DRBD / Ceph 等)
  2. 应用容忍 NFS 卡死(重试 / 降级)
  3. 优先用云 NFS(AWS EFS / Aliyun NAS)—— 底层有高可用
  4. 限制 NFS PV 用途(不要给系统组件用、只给业务)

检测 D 状态 pod

# 节点上
$ ps aux | awk '$8 ~ /D/ {print}'
USER    PID  ... STAT  ...  COMMAND
root    1234 ... D     ...  java -jar app.jar
                     ^ 卡 D

D 状态 = 不可中断的睡眠(通常等 I/O)。kill -9 也杀不掉。

# 看进程在等啥
$ cat /proc/1234/wchan
nfs_wait_on_request                          ← NFS 在等

唯一办法:等 NFS 恢复 + 进程自己继续。或者 reboot 节点。

umount -l 救命招

NFS server 没救了、本地 mount 卡死,怎么办?

$ umount /mnt/data
umount: /mnt/data: target is busy

$ umount -f /mnt/data
umount.nfs: ...  Stale file handle

$ umount -l /mnt/data                       # lazy umount
# 立即"断开"、但已经持有 fd 的进程继续等 NFS
# server 恢复后这些进程才能完成

umount -l 不会立即释放——已有 fd 的进程仍卡。但新进程不再访问 NFS,节点能恢复其它工作。


6. NFS 性能调优

1. rsize / wsize 调大

mount -o rsize=1048576,wsize=1048576 ...

默认很小(4K-64K)、网络好的话调到 1M。大文件传输性能 10x 提升。

注意:太大 + 网络丢包率高 = 重传整 chunk、反而慢。1M 是 sweet spot。

2. server 端 async(慎用)

/exports/data 10.0.0.0/24(rw,async,no_subtree_check)
                                   ^^^^^

async = NFS server 收到 write、放内存就返回成功、后台慢慢落盘。性能数倍。

代价:server 断电 = 这些数据丢。仅限"可以重新计算的数据"(缓存 / 中间结果)。

3. server 端线程数

# /etc/default/nfs-kernel-server
RPCNFSDCOUNT=64                              # 默认 8 太少

并发客户端多时调大。看 /proc/net/rpc/nfsd 第 1 行:

th 8 0 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000
   ^ 8 个线程、0 次满负载、其它桶都 0

如果某桶非 0、特别是最后几个桶,说明线程经常满 → 调大 RPCNFSDCOUNT。

4. K8s 节点参数

# /etc/sysctl.d/99-nfs.conf
sunrpc.tcp_max_slot_table_entries = 128       # 默认 65、调大并发
sunrpc.tcp_slot_table_entries = 128

# 应用
sysctl --system

5. 监控 NFS client 性能

nfsstat -m                                    # 看每个挂载点的统计
$ nfsstat -m
/mnt/data from nfs.example.com:/exports/data
  Flags: rw,relatime,vers=4.2,...
  ...

nfsstat -c                                    # client 操作统计
$ nfsstat -c
Client rpc stats:
calls      retrans    authrefrsh
123456     45         123456
            ^^^
            重传数大 → 网络问题

Client nfs v4:
null     read    write   commit  open  ...
0%       45%     30%     5%      2%   ...

retrans 持续涨 = 网络丢包。

详见 nfsstat.md。


7. stale file handle —— 经典错误

$ ls /mnt/data
ls: cannot access '/mnt/data': Stale file handle

含义:这个 NFS 句柄已失效。常见原因:

  1. server 端文件被删 / 移动——client 还在用旧 inode
  2. server 重建文件系统(mkfs)后挂回来 → 所有 inode 重新分配
  3. server 改了 fsid(/etc/exports 里的 fsid= 改了)

修:重新挂:

umount /mnt/data
mount -t nfs ... /mnt/data

或者 lazy umount:

umount -l /mnt/data
mount -t nfs ... /mnt/data

K8s pod 里出现 stale → pod 重启就好(重新 mount)。


8. K8s NFS 用例 / 反用例

✅ 适合 NFS 的场景
  • 配置共享:多个 pod 读同一份配置 / 模板
  • 静态资源:图片 / 静态 HTML(多个 web server pod 共享)
  • 共享数据集:多个训练任务读同一数据
  • 临时共享:CI / 构建产物共享
  • 小文件 + 不需高 IOPS:日志归档、文档站点
❌ 不适合 NFS
  • 数据库(MySQL / PostgreSQL)——NFS 没真正的文件锁、写性能差、断网灾难
  • 高 IOPS 应用——NFS 远低于 SSD
  • etcd / 关键有状态服务——NFS 单点会让 K8s 控制面挂
  • 大量并发写同一文件——NFS 锁有 bug 历史
  • OS-level cgroup / 系统文件——明显跨网络不合理

数据库为啥不能放 NFS

MySQL / PostgreSQL 假设底层有:

  1. 真正的 fsync 语义(写真落盘后才返回)
  2. 可靠的文件锁(pid file / row lock)
  3. 稳定低延迟

NFS 三条都不满足。生产经常听到 "把 MySQL 放 NFS 上数据库损坏"——根因都在这。

数据库永远用块存储(云盘 / Local PV / Ceph RBD)+ RWO PV。


9. 反面教材合集

反面 1:用默认 sync mount option(搞混了 client vs server)

mount -t nfs -o sync ...                     # client sync

client sync = 每次 write 都等 server ACK 才返回 = 超慢。

正确:client 默认 async(不写 sync)、server 端 export 设 sync。

反面 2:/etc/exports 客户端和括号之间有空格

/exports/data 10.0.0.0/24 (rw)
                          ^^^^
                          有空格 = 给 10.0.0.0/24 默认权限(ro) + 给其它客户端 (rw)

exportfs -v 看实际生效:

/exports/data 10.0.0.0/24
   (ro,sync,...)                  ← 实际 ro
/exports/data *
   (rw,sync,...)                  ← 反而给所有人 rw

安全大坑。

正确:

/exports/data 10.0.0.0/24(rw,sync,no_subtree_check)
                         ^
                         没空格

反面 3:K8s pod 写 NFS 报 EACCES

$ kubectl exec my-pod -- touch /data/test
touch: /data/test: Permission denied

K8s pod 用 root(UID 0)写、NFS server 默认 root_squash 把 root 映射成 nobody → nobody 没写权限。

修法 A:server 关 squash(有安全风险):

/exports/data 10.0.0.0/24(rw,no_root_squash,...)

修法 B:pod 用非 root 用户、确保 NFS 目录有对应权限:

securityContext:
  runAsUser: 1000
  fsGroup: 1000

server 上:

chown -R 1000:1000 /exports/data

修法 C:用 all_squash + anonuid=1000:

/exports/data 10.0.0.0/24(rw,all_squash,anonuid=1000,anongid=1000,...)

所有客户端写入都变成 UID 1000。

反面 4:NFS PV reclaim policy 设 Delete

spec:
  persistentVolumeReclaimPolicy: Delete
  nfs: ...

PVC 删 → K8s 试图删 PV → NFS PV 没有底层 driver 知道怎么"删 NFS 数据" → PV 状态卡 Released。

NFS PV 永远用 Retain。要清数据手动操作 NFS server。

反面 5:不设 mountOptions 默认挂

spec:
  nfs:
    server: ...
    path: ...
  # 没 mountOptions

默认 mount options 可能 server 端协商出最差组合:

  • vers=3 而不是 4
  • rsize=4096 而不是 1M
  • 没 noatime

永远显式设 mountOptions。

反面 6:NFS PV 跨集群迁移

kubectl apply -f nfs-pv.yaml                # 把 PV yaml 应用到新集群

PV claimRef 含旧集群的 PVC UID → 新集群里 PVC 创建 UID 不同 → bind 失败。

修:删除 yaml 里的 claimRef、uid 字段、status 段。

反面 7:用 * 暴露 NFS

/exports/data *(rw,sync,no_root_squash)
              ^
              所有 IP!

NFSv3 没认证、NFSv4 默认 sys auth(仅 IP+UID)。* 暴露 = 任何能访问 server 的人能读写数据。

修:

  • 限制 IP / CIDR
  • 用 NFSv4 + Kerberos 真认证(复杂、生产很少)
  • 防火墙做 IP 白名单

10. 排查 cheatsheet

"K8s pod 卡 ContainerCreating + NFS PV"

# 1. PV / PVC 状态
$ kubectl get pv,pvc
# Bound ?

# 2. NFS server 通吗
$ ssh m4 'showmount -e nfs.example.com'
Export list for nfs.example.com:
/exports/data    10.0.24.0/24

# 3. 节点能挂吗(手动测)
$ ssh m4 'mkdir -p /tmp/nfs-test && mount -t nfs nfs.example.com:/exports/data /tmp/nfs-test && ls /tmp/nfs-test'

# 4. K8s describe pod 看具体错
$ kubectl describe pod my-pod
Events:
  FailedMount  ...  mount failed: ...

# 5. kubelet 日志
$ ssh m4 'journalctl -u kubelet --since "5 min ago" | grep -i nfs'

"NFS 慢"

# 1. 网络
$ mtr -rn -c 30 nfs.example.com

# 2. client 重传率
$ nfsstat -c | grep retrans

# 3. server 线程满
$ ssh nfs-server 'cat /proc/net/rpc/nfsd | head -1'
# th 8 1234 ...     ← 1234 次满载

# 4. mount options
$ mount | grep nfs
# 看 rsize / wsize / vers

# 5. server 磁盘 / CPU
$ ssh nfs-server 'iostat -x 1 5'

"stale file handle"

# 客户端
$ ls /mnt/data
ls: Stale file handle

# server 端
$ ssh nfs-server 'ls -li /exports/data'   # 看 inode 还在不

# 重新挂
$ umount -l /mnt/data
$ mount -t nfs ... /mnt/data

11. 下一步

篇内容
00-storage-mental-model.md心智模型
01-container-volumes.md容器挂载
02-k8s-volumes.mdK8s Volume 大全
03-pv-pvc-storageclass.mdPV / PVC / CSI
本篇NFS 深入
05-distributed-storage.mdCeph / Longhorn
06-storage-troubleshooting.md故障 runbook

相关命令

  • mount / umount
  • fstab
  • findmnt
  • nfsstat
  • iostat
  • mtr
在 GitHub 上编辑此页
Prev
PV / PVC / StorageClass / CSI 深入
Next
分布式存储概览:Ceph / Longhorn / Rook