引言上周踩了个能把人逼疯的坑:在 K8S 集群里部署 Kafka,挂载了 NFS 存储存日志和数据,结果下班关机的时候,终端突然蹦出一行红字 ——“[***] A stop job is running for libcontainer container f36087a11c26… (2min 58s /no limit)”。

我盯着屏幕等了 15 分钟,终端纹丝不动,Ctrl+C 没反应、远程连接也断不开,最后只能硬拔服务器电源。第二天重启 Kafka,日志里全是 “数据分区不一致” 的报错,差点没把我吓出冷汗。

折腾了 3 小时才搞明白:罪魁祸首是 NFS 默认的 “hard 挂载” 模式—— 今天把我踩的坑、原理和解决方案全说透,让你再也不用遭这罪!

一、关机卡住的底层原因:NFS hard 挂载的 “死等” 逻辑

先搞懂为什么会卡住:K8S 挂载 NFS 时,默认用的是 “hard 挂载” 模式,这个模式的核心行为是 ——只要 NFS 服务器没返回响应,客户端就会无限阻塞等待,绝不主动终止请求。

而 Kafka 是有状态应用,关机时 K8S 会触发 Pod 优雅终止,但如果 Kafka 还在往 NFS 写数据(比如落盘未完成的日志),NFS 客户端就会卡在 “等待服务器确认写成功” 的步骤里;同时 systemd 在关机时,会强制等待所有服务的 “stop job” 完成,这就导致 systemd 被 NFS 的阻塞请求绑死,根本关不了机。

反观 “soft 挂载” 模式:如果 NFS 请求超时,客户端会直接返回错误,不会无限阻塞 —— 这正是解决问题的关键!

二、解决方案:给 PV 加 soft+timeo 挂载选项(注意:位置别写错!)

核心是给每个 PV 配置mountOptions,把 NFS 挂载从 “死等” 改成 “超时终止”。这里有个极易踩的坑:mountOptions是 PV 的spec一级字段,不是nfs的子字段(我一开始写错位置,导致配置完全不生效)。

下面是修正后的kafka-pv.yaml(以 3 个 Kafka 节点为例):

yaml

# kafka-pv.yaml:给每个PV加soft+timeo挂载选项
apiVersion: v1
kind: PersistentVolume
metadata:
  name: kafka-pv-0  # 对应kafka-0节点
spec:
  capacity:
    storage: 20Gi  # 按需分配存储
  accessModes:
    - ReadWriteOnce  # Kafka单节点读写,用RWO即可
  persistentVolumeReclaimPolicy: Retain  # 保留NFS数据,避免误删
  storageClassName: nfs  # 对应PVC的storageClassName
  # 关键挂载选项:解决关机阻塞的核心配置
  mountOptions:
    - soft        # 软挂载:超时后返回错误,不无限阻塞
    - timeo=30    # 超时时间(单位是0.1秒,30=3秒,可根据网络调整)
    - retrans=2   # 超时后重试2次,避免单次网络抖动导致失败
    - noatime     # 禁用文件访问时间更新,减少NFS的IO请求(优化性能)
  nfs:
    path: /data/kafka/kafka-0  # NFS服务器上的子目录
    server: 192.168.142.141    # 你的NFS服务器IP
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: kafka-pv-1  # 对应kafka-1节点
spec:
  capacity:
    storage: 20Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs
  mountOptions:  # 每个PV都要加,确保所有节点的挂载模式一致
    - soft
    - timeo=30
    - retrans=2
    - noatime
  nfs:
    path: /data/kafka/kafka-1
    server: 192.168.142.141
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: kafka-pv-2  # 对应kafka-2节点
spec:
  capacity:
    storage: 20Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs
  mountOptions:
    - soft
    - timeo=30
    - retrans=2
    - noatime
  nfs:
    path: /data/kafka/kafka-2
    server: 192.168.142.141
三、操作步骤:替换旧 PV + 验证配置生效

旧的 PV 还在绑定状态,直接改 yaml 没用,得先清理旧资源(NFS 数据会保留,别担心丢数据):

步骤 1:清理旧的 PV 和 PVC

先查当前 PV 状态,确认哪些是绑定中的:

bash

运行

kubectl get pv | grep kafka
# 输出示例:kafka-pv-0   20Gi       RWO            Bound   kafka/kafka-data-kafka-0   nfs

然后删除对应的 PVC(PVC 绑着 PV,得先删 PVC):

bash

运行

# 删除kafka命名空间下的所有PVC
kubectl delete pvc -n kafka --all
# 等待PVC删除完成后,删除旧PV
kubectl delete pv kafka-pv-0 kafka-pv-1 kafka-pv-2

如果删 PVC 时提示 “PVC is bound to PV xxx”,说明对应的 Kafka Pod 还在运行,得先删 Pod:

bash

运行

kubectl delete pod -n kafka kafka-0 kafka-1 kafka-2 --force --grace-period=0
步骤 2:应用新的 PV 配置

bash

运行

kubectl apply -f kafka-pv.yaml

查新 PV 的状态,确认是 “Available”(待绑定):

bash

运行

kubectl get pv | grep kafka
# 输出示例:kafka-pv-0   20Gi       RWO            Available   10s   nfs
步骤 3:重启 Kafka Pod,验证挂载选项生效

重启 Kafka StatefulSet,让 Pod 重新绑定新 PV:

bash

运行

kubectl rollout restart statefulset -n kafka kafka

等 Pod 启动后,登录 Kafka 所在节点,验证 NFS 挂载选项:

bash

运行

# 1. 查Kafka Pod所在节点
kubectl get pod -n kafka -o wide | grep kafka
# 输出示例:kafka-0   Running   0     2m   10.244.1.10   node-1   <none>

# 2. 登录节点
ssh node-1

# 3. 查NFS挂载详情
mount | grep kafka

如果输出里包含soft,timeo=30,retrans=2,说明配置生效了:

bash

运行

# 输出示例(关键看括号里的选项)
192.168.142.141:/data/kafka/kafka-0 on /var/lib/kubelet/pods/029b664b-e666-47e0-aea3-0cd46f2ed76a/volumes/kubernetes.io~nfs/kafka-pv-0 type nfs (rw,soft,timeo=30,retrans=2,noatime,vers=3,...)
四、关机测试:终于能秒关了!

执行关机命令:

bash

运行

sudo shutdown -h now

这次终端不会再卡 stop job,系统会在 NFS 请求超时后(3 秒)正常终止资源,1 分钟内就能完成关机 —— 亲测有效!

总结K8S+NFS+Kafka 关机卡住,本质是 NFS hard 挂载的 “死等” 逻辑和 systemd 的 stop job 机制冲突。只要给 PV 加上soft+timeo的挂载选项,就能从根源解决阻塞问题。记住:mountOptions要写在 PV 的spec一级,别写错位置!

更多推荐