背景
生产环境中,A 服务器上的 Docker 容器出现文件上传接口延迟异常,B/C 服务器上的同服务容器无此问题。本文完整记录从现象发现到根因定位的排查全过程,包含每一步使用的命令、命令作用、以及排查思路的推演逻辑。
问题现象
- A 服务器(node210)容器内文件上传延迟极高
- B/C 服务器同服务容器上传正常
- 上传代码使用
FileOutputStream同步写磁盘,代码逻辑无差异
java
public static void uploadFile(byte[] file, String filePath, String fileName) {
File targetFile = new File(filePath);
if (!targetFile.exists()) {
targetFile.mkdirs();
}
try (FileOutputStream out = new FileOutputStream(filePath + fileName)) {
out.write(file);
out.flush();
} catch (Exception e) {
throw new DefaultException("uploadFile file error", e);
}
}
代码本身是标准的同步文件写入,延迟完全取决于底层存储 I/O 性能。因此排查方向锁定在存储层。
第一步:确认容器存储挂载类型
命令
bash
# 查看容器的挂载详情
docker inspect <container_id> --format '{{json .Mounts}}' | python -m json.tool
作用
docker inspect 输出容器的完整配置信息,--format '{{json .Mounts}}' 只提取挂载配置部分,python -m json.tool 将 JSON 格式化输出便于阅读。
执行结果
json
[
{
"Destination": "/home/admin/upload",
"Source": "/var/lib/kubelet/pods/.../volumes/kubernetes.io~nfs/new-nfs-client-home",
"Type": "bind"
},
{
"Destination": "/home/admin/upload1",
"Source": "/var/lib/kubelet/pods/.../volumes/kubernetes.io~csi/pvc-fd79d6c3.../mount",
"Type": "bind"
}
]
分析
/home/admin/upload的 Source 路径包含kubernetes.io~nfs,确认是 NFS 挂载/home/admin/upload1的 Source 路径包含kubernetes.io~csi,确认是 CSI PVC 挂载- 文件上传写入的是
/home/admin/upload,即 NFS 存储
排查思路:既然确认是 NFS 挂载,问题可能出在 NFS 服务端、网络链路、或挂载参数上。接下来需要在容器内做裸写测试,量化延迟程度。
第二步:容器内 dd 裸写性能测试
命令
bash
# 测试1:写入 overlay 层(本地磁盘),作为基准
time dd if=/dev/zero of=/tmp/container_test bs=1M count=100 oflag=dsync
# 测试2:写入 NFS 路径,带 dsync(每次写入都同步到磁盘)
time dd if=/dev/zero of=/home/admin/upload/prd/app/temp/nfs_test bs=1M count=100 oflag=dsync
# 测试3:写入 NFS 路径,不带 dsync(走 OS 缓存)
time dd if=/dev/zero of=/home/admin/upload/prd/app/temp/nfs_nosync bs=1M count=100
# 测试4:小文件 dsync 测试
time dd if=/dev/zero of=/home/admin/upload/prd/app/temp/nfs_small bs=1K count=1 oflag=dsync
命令参数说明
| 参数 | 作用 |
|---|---|
if=/dev/zero |
输入源,持续产生零字节 |
of=路径 |
输出目标文件路径 |
bs=1M |
每次读写块大小 1MB |
count=100 |
读写 100 次,共 100MB |
oflag=dsync |
每次写入都同步到物理存储(绕过 OS 缓存) |
time |
测量命令执行总耗时 |
dd 输出解读
sql
100+0 records in
100+0 records out
104857600 bytes (105 MB, 100 MiB) copied, 0.534649 s, 196 MB/s
real 0m56.652s
user 0m0.000s
sys 0m0.087s
- 写入耗时(0.53s):dd 进程实际执行 write() 系统调用的 CPU 时间
- real (56.652s):从命令开始到结束的挂钟时间(wall clock),包含所有等待时间
- user/sys:用户态/内核态 CPU 时间
关键判断 :如果 real 远大于写入耗时之和,说明进程在等待某些 I/O 操作完成(如 close/fsync 阶段的 sync 等待)。
测试结果
| 测试 | 写入耗时 | real 耗时 | 分析 |
|---|---|---|---|
| overlay /tmp + dsync | 0.42s | 0.42s | 本地磁盘,正常 |
| NFS + dsync 100MB | 0.53s | 56s | 写入快但 real 极慢 |
| NFS 无 dsync 100MB | 0.25s | 29.6s | 不 dsync 也慢 |
| NFS dsync 1KB | 0.0007s | 0.003s | 小文件正常 |
排查思路:
- overlay 正常 → 容器本地存储无问题
- NFS 不带 dsync 也慢(29.6s)→ 不是 sync/fsync 的问题,是数据传输本身在某个环节被阻塞
- 小文件正常 → 问题只在大文件传输时出现
- 写入耗时正常但 real 极慢 → 延迟不在 write() 系统调用,而在其他环节(如 close 时的 flush、或 NFS 属性缓存刷新)
第三步:宿主机对比测试
命令
bash
# 在宿主机 node210 上直接对 NFS 挂载路径 dd 测试
time dd if=/dev/zero of=/var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~nfs/new-nfs-client-home/host_test bs=1M count=100 oflag=dsync
作用
排除容器层因素。如果宿主机也慢,问题在 NFS 服务端或网络;如果宿主机快,问题在容器层。
结果
宿主机 dd 很快。
排查思路 :宿主机访问同一 NFS 正常,说明 NFS 服务端和物理网络没问题。问题被隔离在容器网络层。
第四步:确认 NFS 挂载参数
命令
bash
# 容器内查看 NFS 挂载参数
cat /proc/mounts | grep nfs
作用
/proc/mounts 列出当前系统所有活跃的挂载点及其参数。grep nfs 过滤出 NFS 相关的挂载信息。
结果
ruby
192.168.1.220:/zhiyun-longhorn /home/admin/upload nfs rw,relatime,vers=3,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,mountaddr=192.168.1.220,mountvers=3,mountport=2050,mountproto=udp,local_lock=none,addr=192.168.1.220 0 0
10.43.128.222:/pvc-... /home/admin/upload1 nfs4 rw,relatime,vers=4.1,rsize=1048576,wsize=1048576,namlen=255,soft,noresvport,proto=tcp,timeo=30,retrans=3,sec=sys,clientaddr=192.168.1.159,local_lock=none,addr=10.43.128.222 0 0
关键参数解读
| 参数 | upload(慢) | upload1(快) |
|---|---|---|
| NFS 版本 | vers=3 | vers=4.1 |
| 数据协议 | proto=tcp | proto=tcp |
| 挂载协议 | mountproto=udp | 无(v4 不需要) |
| 挂载模式 | hard | soft |
| rsize/wsize | 1MB | 1MB |
关键发现 :upload 使用 mountproto=udp,upload1 使用 NFS v4.1(纯 TCP,无 UDP)。
排查思路:mountproto=udp 意味着 NFS 挂载/卸载操作使用 UDP 协议。UDP 不可靠,丢包后需要应用层重传。需要验证是否是 UDP 导致的问题。
第五步:跨容器对比验证
命令
bash
# 在 B/C 容器内执行同样的挂载参数检查
cat /proc/mounts | grep nfs
# 在 A 容器内测试 upload1(NFS v4.1 TCP)
time dd if=/dev/zero of=/home/admin/upload1/v4_test bs=1M count=100
结果
| 测试 | 表现 |
|---|---|
| A 容器 upload(NFS v3 UDP) | 慢 |
| A 容器 upload1(NFS v4.1 TCP) | 快 |
| B/C 容器 upload(NFS v3 UDP) | 快 |
| A 宿主机 upload(NFS v3 UDP) | 快 |
排查思路:
- A 容器内 TCP 的 NFS 快、UDP 的 NFS 慢 → 问题与 UDP 相关
- B/C 容器 UDP 也快 → 不是 NFS 服务端问题
- A 宿主机 UDP 快 → 不是物理网络问题
- 问题锁定在 node210 容器网络命名空间对 UDP 包的处理
第六步:排除内存限制
命令
bash
# 容器内存限制(字节)
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
# 容器当前内存使用(字节)
cat /sys/fs/cgroup/memory/memory.usage_in_bytes
作用
cgroup v1 的 memory 子系统通过 /sys/fs/cgroup/memory/ 下的文件暴露内存限制和使用情况。
结果
- 限制:36GB(38654705664 字节)
- 使用:14GB(14931759104 字节)
内存充足,排除内存压力导致的页缓存回写阻塞。
第七步:排除 conntrack 表满
命令
bash
# 当前 conntrack 条目数
cat /proc/sys/net/netfilter/nf_conntrack_count
# conntrack 表最大容量
cat /proc/sys/net/netfilter/nf_conntrack_max
# UDP 连接跟踪条目数
conntrack -L -p udp 2>/dev/null | wc -l
作用
nf_conntrack 是 Linux 内核的连接跟踪表。当条目数达到 max 时,新的网络连接会被丢弃,导致 UDP 包丢失。
结果
- 当前:24529
- 最大:1048576
- 使用率:2.3%
- UDP 条目:0
conntrack 远未满,排除。
第八步:检查网络插件与容器网络
命令
bash
# 宿主机上查看所有网络接口
ip link show | grep -E "^[0-9]+"
# 查看网卡丢包统计(需使用实际网卡名)
ethtool -S ens192 | grep -i drop
ethtool -S ens192 | grep -i error
作用
ip link show列出所有网络接口,确认网络插件类型(cali* 前缀 = Calico 插件)ethtool -S输出网卡硬件级别的收发统计,包括丢包和错误计数
结果
- node210 使用 Calico 网络插件(大量 cali* 接口,MTU=1450)
- B 服务器同样使用 Calico,cali 接口数量相当
排查思路
Calico 通过 iptables 规则进行 Pod 网络路由。NFS 的 UDP 包从容器出来后,经过 cali veth pair → Calico iptables 规则 → 物理网卡 ens192 → NFS 服务端。
宿主机直接访问 NFS 不经过 Calico iptables 规则链(走宿主机网络栈),所以宿主机快。B/C 容器虽然也经过 Calico,但可能因为节点网络配置差异(如 iptables 规则数量、内核参数等)表现不同。
第九步:检查内核日志与进程状态
命令
bash
# 查看 D 状态进程(不可中断睡眠,通常在等待 I/O)
ps aux | grep " D "
# 查看内核最近日志(查找存储/网络相关错误)
dmesg | tail -50
作用
- D 状态进程表示进程处于不可中断睡眠状态,通常是在等待 I/O 完成。如果有大量 D 状态进程,说明底层存储有问题
dmesg输出内核环形缓冲区日志,NFS 超时、网络断开等会在内核日志中留下记录
结果
- 无 D 状态进程
- dmesg 只有 Docker 网络接口的正常日志,无存储异常
根因定位
通过逐步排除法,最终确认:
| 排除项 | 依据 |
|---|---|
| NFS 服务端故障 | 宿主机/B/C 容器访问正常 |
| 挂载参数差异 | 三节点参数完全一致 |
| 容器内存限制 | 36GB 限制,使用 14GB |
| conntrack 表满 | 使用率 2.3% |
| Calico 规则链差异 | A/B 节点 cali 接口数量相当 |
| 磁盘空间/内核错误 | 均正常 |
最终根因:node210 容器网络命名空间到 NFS 服务端 192.168.1.220 的 NFS v3 mountproto=UDP 链路存在延迟。
核心证据链:
- A 容器内 upload1(NFS v4.1 TCP)快 → 容器网络本身没问题
- A 宿主机 upload(NFS v3 UDP)快 → 物理网络没问题
- B/C 容器 upload(NFS v3 UDP)快 → NFS 服务端没问题
- 只有 A 容器 + upload(NFS v3 UDP)慢 → 问题在 node210 容器网络对 UDP 的处理
NFS v3 的 mountproto=UDP 在 node210 的 Calico 容器网络中,UDP 包经过 iptables 规则链处理时产生延迟,导致大文件写入时 real 时间异常。
修复方案
方案1(推荐):NFS PV mountOptions 将 mountproto 改为 tcp
修改 K8S NFS PersistentVolume 配置:
yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: new-nfs-client-home
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteMany
mountOptions:
- nfsvers=3
- mountproto=tcp
- proto=tcp
- hard
- rsize=1048576
- wsize=1048576
nfs:
server: 192.168.1.220
path: /zhiyun-longhorn
执行步骤:
bash
# 1. 应用 PV 变更
kubectl apply -f pv.yaml
# 2. 删除 Pod 让其重建(新 Pod 将使用 TCP 挂载)
kubectl delete pod <pod-name> -n <namespace>
# 3. 验证新 Pod 的挂载参数
kubectl exec -it <new-pod-name> -n <namespace> -- cat /proc/mounts | grep nfs
# 4. dd 测试验证
time dd if=/dev/zero of=/home/admin/upload/prd/app/temp/tcp_test bs=1M count=100
预期结果:real 时间从 29.6s 降至 1s 以内。
方案2:迁移到 NFS v4.1
将 upload 路径改用 Longhorn NFS v4.1(与 upload1 同一服务端 10.43.128.222),彻底避免 NFS v3 的 UDP 问题。
方案3(根因深挖):排查 node210 网卡/交换机 UDP 丢包
bash
# node210 宿主机查看网卡丢包统计
ethtool -S ens192 | grep -i drop
ethtool -S ens192 | grep -i error
如果 drop/error 计数持续增长,说明物理层存在 UDP 丢包,需排查交换机端口或网卡驱动。
排查思路总结
本次排查遵循逐层隔离、对比验证的方法论:
css
应用层(代码)
↓ 排除(dd 裸测也慢)
存储层(NFS 挂载)
↓ 确认是 NFS
容器内 vs 宿主机
↓ 容器内慢,宿主机快 → 问题在容器层
容器内 NFS(v3 UDP) vs NFS(v4.1 TCP)
↓ UDP 慢,TCP 快 → 问题与 UDP 相关
A 容器 vs B/C 容器 vs 宿主机
↓ 只有 A 容器慢 → 问题在 node210 容器网络
内存/conntrack/内核日志
↓ 全部排除
最终定位:node210 容器网络对 NFS v3 UDP 包处理延迟
核心原则:
- 先量化再定位:用 dd 测试量化延迟,区分"慢"和"卡死"
- 逐层隔离:应用层 → 存储层 → 容器层 → 网络层,每步只变一个变量
- 横向对比:同一服务在不同节点的表现差异是最好的线索
- 控制变量:upload(UDP)vs upload1(TCP)的对比直接锁定了 UDP 协议