生产环境中,A服务器上的Docker容器出现文件上传接口延迟异常,B/C服务器上的同服务容器无此问题。本文完整记录从现象发现到根因定位的排查全过程。

一、问题现象

  • A服务器(node210)容器内文件上传延迟极高
  • B/C服务器同服务容器上传正常
  • 上传代码使用FileOutputStream同步写磁盘,代码逻辑无差异
1
2
3
4
5
6
7
8
9
10
11
12
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性能。因此排查方向锁定在存储层

二、时间线

以下为本次故障排查与恢复的关键时间节点,便于复盘业务影响时长与排查效率。
时间阶段说明
1发现告警文件上传接口P99延迟飙升至8s以上,监控告警触发
2初步定位完成docker inspectdd裸测,确认问题在NFS存储层
3精准锚定完成四象限对比测试(A容器UDP/TCP、A宿主机、B/C容器),锁定node210容器网络对UDP的处理存在异常
4排除干扰项完成内存、conntrack、内核日志检查,全部排除
5根因确认确认NFS v3 mountproto=udp为根本原因
6业务恢复修改PV配置mountproto=tcp并重建Pod,接口延迟恢复正常

三、排查过程

第一阶段:定位问题层级

步骤1:确认容器存储挂载类型

排查思路: 首先需要明确文件上传路径使用的是哪种存储类型,是本地磁盘、NFS还是其他分布式存储。不同的存储类型,问题排查方向完全不同。

命令:

1
docker inspect <container_id> --format '{{json .Mounts}}' | python -m json.tool

作用: docker inspect输出容器的完整配置信息,--format '{{json .Mounts}}'只提取挂载配置部分,python -m json.tool将JSON格式化输出便于阅读。

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
[
{
"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 本身慢还是其他环节的问题。

步骤2:容器内 dd 裸写性能测试

排查思路: 使用dd工具绕过应用代码,直接测试底层存储的读写性能。通过对比本地存储(overlay层)和NFS路径的写入耗时,可以判断延迟是否由NFS引起。同时,通过dsync标志可以判断延迟是否与同步写(fsync)有关。

命令:

1
2
3
4
5
6
7
8
9
10
11
# 测试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输出解读:

1
2
3
4
5
6
7
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),包含所有等待时间。关键判断: 如果real远大于写入耗时之和,说明进程在等待某些I/O操作完成(如close阶段的flush、或网络传输)
  • user/sys: 用户态/内核态CPU时间

测试结果:

测试写入耗时real耗时分析
overlay /tmp + dsync0.42s0.42s本地磁盘,正常
NFS + dsync 100MB0.53s56s写入快但real极慢
NFS 无dsync 100MB0.25s29.6s不dsync也慢
NFS dsync 1KB0.0007s0.003s小文件正常(单次RPC完成,未触发分片)

阶段性结论:

  1. overlay正常 → 容器本地存储无问题
  2. NFS不带dsync也慢(29.6s)→ 不是sync/fsync的问题,是数据传输本身在某个环节被阻塞
  3. 小文件正常 → 小文件未触发NFS分片,单次RPC即可完成;大文件被拆分为多个RPC请求,延迟被累积放大。说明问题与RPC交互次数相关
  4. 写入耗时正常但real极慢 → 延迟不在write()系统调用,而在其他环节(如close时的flush、或网络传输)

进一步诊断建议: 如需区分是"RPC交互次数过多导致的RTT累积"还是"MTU/分片/缓冲区问题",可补充一组bs=4K的小块大文件测试(dd if=/dev/zero of=... bs=4K count=25600 oflag=dsync)。如果小块写入也慢,说明是RPC次数过多;如果只有大块慢,更指向MTU/分片问题。

步骤3:宿主机对比测试

排查思路: 为了确认问题是出在容器层还是宿主机层,直接在宿主机上对同一个NFS挂载路径执行相同的dd测试。如果宿主机也慢,说明问题在NFS服务端或物理网络;如果宿主机快,说明问题在容器层。

命令:

1
2
# 在宿主机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

结果: 宿主机dd很快

阶段性结论: 宿主机访问同一NFS正常,说明NFS服务端和物理网络没问题。问题被隔离在容器网络层。

第二阶段:定位协议差异

步骤4:确认NFS挂载参数

排查思路: NFS的挂载参数(版本、协议、传输模式等)直接影响性能。需要对比慢路径(upload)和快路径(upload1)的挂载参数差异,找出关键变量。

命令:

1
2
# 容器内查看NFS挂载参数
cat /proc/mounts | grep nfs

作用: /proc/mounts列出当前系统所有活跃的挂载点及其参数,grep nfs过滤出NFS相关的挂载信息。

结果:

1
2
3
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=3vers=4.1
数据协议proto=tcpproto=tcp
挂载协议mountproto=udp无(v4不需要)
挂载模式hardsoft
rsize/wsize1MB1MB

关键发现: upload使用mountproto=udp,upload1使用NFS v4.1(纯TCP,无UDP)。

阶段性结论: mountproto=udp意味着NFS挂载/卸载操作使用UDP协议。UDP无拥塞控制,不可靠,丢包后依赖RPC层超时重传(指数退避);而TCP有快速重传和拥塞控制机制,抗丢包能力更强。需要进一步验证是否是UDP导致的问题。

步骤5:跨容器对比验证

排查思路: 通过控制变量法,在A容器内测试TCP协议的NFS(upload1),并与B/C容器的UDP协议NFS进行对比,逐步缩小问题范围。

命令:

1
2
3
4
5
# 在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包的处理

第三阶段:排除干扰项

步骤6:排除内存限制

排查思路: 内存不足可能导致页缓存频繁回收,进而阻塞文件写入。需要确认容器内存是否充足。

命令:

1
2
3
4
5
# 容器内存限制(字节)
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,使用14GB,内存充足

阶段性结论: 排除内存压力导致的页缓存回写阻塞。

步骤7:排除conntrack表满

排查思路: nf_conntrack是Linux内核的连接跟踪表,当条目数达到最大值时,新的网络连接会被丢弃,导致UDP包丢失。需要确认conntrack是否成为瓶颈。

命令:

1
2
3
4
5
6
7
8
# 当前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

结果: 当前24529,最大1048576,使用率2.3%,UDP条目0

阶段性结论: 排除conntrack表满导致UDP丢包的可能。

步骤8:检查网络插件与容器网络,并抓包验证

排查思路: 容器网络插件(Calico)通过iptables规则进行Pod网络路由。UDP包从容器出来后,会经过一系列网络栈处理。需要确认网络插件类型以及是否存在网卡层面的丢包。为进一步确认UDP链路的实际状况,本次排查增加了抓包验证环节。

命令:

1
2
3
4
5
6
7
8
9
10
11
12
# 宿主机上查看所有网络接口
ip link show | grep -E "^[0-9]+"

# 查看网卡丢包统计(需使用实际网卡名)
ethtool -S ens192 | grep -i drop
ethtool -S ens192 | grep -i error

# 容器内抓取NFS UDP流量,验证是否存在丢包/重传
tcpdump -i eth0 udp port 2049 or port 111 -c 500 -w /tmp/nfs_udp.pcap

# 分析抓包结果:统计重传和RTT(需安装tshark)
tshark -r /tmp/nfs_udp.pcap -Y "nfs" -T fields -e nfs.procedure -e frame.time_delta | tail -20

作用:

  • ip link show列出所有网络接口,确认网络插件类型(cali*前缀=Calico插件)
  • ethtool -S输出网卡硬件级别的收发统计,包括丢包和错误计数
  • tcpdump抓取NFS UDP流量,tshark分析抓包文件,统计重传和RTT变化

结果:

  • node210使用Calico网络插件(大量cali*接口,MTU=1450)
  • B服务器同样使用Calico,cali接口数量相当
  • 抓包结果显示:UDP链路上存在明显的NFS RPC重传,RTT呈指数增长;TCP链路无重传,RTT稳定
阶段性结论: Calico本身不是直接原因(B/C也使用Calico且正常),但抓包证实了UDP链路存在丢包和重传,这是导致延迟的直接原因。

步骤9:检查内核日志与进程状态

排查思路: 检查是否有进程处于不可中断睡眠状态(D状态),以及内核日志中是否有NFS超时或网络错误,这些都能反映底层存储或网络的异常状态。

命令:

1
2
3
4
5
# 查看D状态进程(不可中断睡眠,通常在等待I/O)
ps aux | grep " D "

# 查看内核最近日志(查找存储/网络相关错误)
dmesg | tail -50

作用:

  • D状态进程表示进程处于不可中断睡眠状态,通常是在等待I/O完成。如果有大量D状态进程,说明底层存储有问题
  • dmesg输出内核环形缓冲区日志,NFS超时、网络断开等会在内核日志中留下记录

结果: 无D状态进程,dmesg只有Docker网络接口的正常日志

阶段性结论: 排除存储层I/O hang和内核层面的异常。

四、根因定位

通过逐步排除法,最终确认:

排除项依据
NFS服务端故障宿主机/B/C容器访问正常
挂载参数差异三节点参数完全一致
容器内存限制36GB限制,使用14GB
conntrack表满使用率2.3%
Calico规则链差异A/B节点cali接口数量相当
磁盘空间/内核错误均正常

最终根因:node210容器网络命名空间到NFS服务端192.168.1.220的NFS v3 mountproto=UDP链路存在异常丢包/重传,疑似与Calico网络策略或网卡Offload特性交互有关。

核心证据链:

  1. A容器内upload1(NFS v4.1 TCP)快 → 容器网络本身没问题
  2. A宿主机upload(NFS v3 UDP)快 → 物理网络没问题
  3. B/C容器upload(NFS v3 UDP)快 → NFS服务端没问题
  4. 只有A容器 + upload(NFS v3 UDP)慢 → 问题在node210容器网络对UDP的处理
  5. tcpdump抓包证实UDP链路存在重传 → 确认为丢包+超时重传,而非单纯的规则遍历延迟

技术原理补充:
NFS v3 over UDP 与 Calico iptables 的交互机制:

  • NFS v3 的 mountproto=udp 使用 UDP 协议传输 RPC 请求
  • UDP无拥塞控制,丢包后依赖RPC层超时重传,超时时间指数退避
  • Calico 的 iptables 规则链在特定条件下可能导致UDP包被静默丢弃
  • 每次丢包触发RPC重传,重传超时不断累积(如首次超时200ms、第二次400ms、第三次800ms……)
  • 大文件(100MB+)包含大量RPC分片,一旦发生几次丢包,总延迟就会被严重放大
  • 而TCP有快速重传和拥塞控制机制,对丢包的容忍度和恢复速度远优于UDP
  • 宿主机直接访问NFS不经过Calico iptables规则链(走宿主机网络栈),所以宿主机快

五、修复方案

方案1(推荐):NFS PV mountOptions将mountproto改为tcp

修改K8S NFS PersistentVolume配置,将挂载协议从UDP改为TCP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: v1
kind: PersistentVolume
metadata:
name: new-nfs-client-home
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteMany
mountOptions:
- nfsvers=3
- mountproto=tcp # 将mount协议从udp改为tcp
- proto=tcp # 数据协议也改为tcp(需与服务端匹配)
- hard
- rsize=1048576
- wsize=1048576
nfs:
server: 192.168.1.220
path: /zhiyun-longhorn

执行前预检:确认NFS服务端是否支持TCP mount

1
2
3
4
5
# 验证NFS服务端是否支持TCP mount协议
rpcinfo -p 192.168.1.220 | grep mountd
# 预期输出应包含 tcp 协议行,如:
# 100005 3 tcp 2050 mountd
# 如果只有udp行,需先排查防火墙或NFS服务端配置

执行步骤:

1
2
3
4
5
6
7
8
9
10
11
# 1. 应用PV变更
kubectl apply -f pv.yaml

# 2. 删除Pod让其重建(注意:修改mountOptions后需重建Pod才生效)
kubectl delete pod <pod-name> -n <namespace>

# 3. 验证新Pod的挂载参数,确认已改为tcp
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丢包

1
2
3
# node210宿主机查看网卡丢包统计
ethtool -S ens192 | grep -i drop
ethtool -S ens192 | grep -i error

如果drop/error计数持续增长,说明物理层存在UDP丢包,需排查交换机端口或网卡驱动。但从本次排查的证据链来看,此方案优先级最低。

六、排查思路总结

本次排查遵循逐层隔离、对比验证的方法论:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
应用层(代码)
↓ 排除(dd裸测也慢)
存储层(NFS挂载)
↓ 确认是NFS
容器内 vs 宿主机
↓ 容器内慢,宿主机快 → 问题在容器层
容器内 NFS(v3 UDP) vs NFS(v4.1 TCP)
↓ UDP慢,TCP快 → 问题与UDP相关
A容器 vs B/C容器 vs 宿主机
↓ 只有A容器慢 → 问题在node210容器网络
抓包验证
↓ 证实UDP链路存在重传
内存/conntrack/内核日志
↓ 全部排除
最终定位:node210容器网络对NFS v3 UDP包存在异常丢包,触发RPC超时重传,导致大文件写入延迟

核心原则:

  1. 先量化再定位: 用dd测试量化延迟,区分"慢"和"卡死"。没有量化数据,就无法精准定位。
  2. 逐层隔离: 应用层 → 存储层 → 容器层 → 网络层,每步只变一个变量,避免被表象迷惑。
  3. 横向对比: 同一服务在不同节点的表现差异是最好的线索。如果没有B/C节点的对比,这个问题可能要被归咎于NFS服务端。
  4. 控制变量: upload(UDP)vs upload1(TCP)的对比直接锁定了UDP协议。这个对比之所以有效,是因为两个路径共享相同的容器网络环境,唯一的变量是协议。
  5. 抓包验证: 网络问题不能只靠猜测,抓包是证实丢包、重传等行为的铁证。网络栈的异常行为必须通过包级别的证据来锁定,而不是依赖对iptables规则链的推测。
https://juejin.cn/post/7664994459843706921
AI