生产环境中,A服务器上的Docker容器出现文件上传接口延迟异常,B/C服务器上的同服务容器无此问题。本文完整记录从现象发现到根因定位的排查全过程。
一、问题现象
- A服务器(node210)容器内文件上传延迟极高
- B/C服务器同服务容器上传正常
- 上传代码使用
FileOutputStream同步写磁盘,代码逻辑无差异
1 | public static void uploadFile(byte[] file, String filePath, String fileName) { |
代码本身是标准的同步文件写入,延迟完全取决于底层存储I/O性能。因此排查方向锁定在存储层。
二、时间线
以下为本次故障排查与恢复的关键时间节点,便于复盘业务影响时长与排查效率。
| 时间 | 阶段 | 说明 |
|---|---|---|
| 1 | 发现告警 | 文件上传接口P99延迟飙升至8s以上,监控告警触发 |
| 2 | 初步定位 | 完成docker inspect和dd裸测,确认问题在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 | [ |
分析:
/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 | # 测试1:写入overlay层(本地磁盘),作为基准 |
参数说明:
| 参数 | 作用 |
|---|---|
if=/dev/zero | 输入源,持续产生零字节 |
of=路径 | 输出目标文件路径 |
bs=1M | 每次读写块大小1MB |
count=100 | 读写100次,共100MB |
oflag=dsync | 每次写入都同步到物理存储(绕过OS缓存) |
time | 测量命令执行总耗时 |
dd输出解读:
1 | 100+0 records in |
- 写入耗时(0.53s): dd进程实际执行
write()系统调用的CPU时间,反映写入操作本身的速度 - real(56.652s): 从命令开始到结束的挂钟时间(wall clock),包含所有等待时间。关键判断: 如果
real远大于写入耗时之和,说明进程在等待某些I/O操作完成(如close阶段的flush、或网络传输) - user/sys: 用户态/内核态CPU时间
测试结果:
| 测试 | 写入耗时 | 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 | 小文件正常(单次RPC完成,未触发分片) |
阶段性结论:
- overlay正常 → 容器本地存储无问题
- NFS不带dsync也慢(29.6s)→ 不是sync/fsync的问题,是数据传输本身在某个环节被阻塞
- 小文件正常 → 小文件未触发NFS分片,单次RPC即可完成;大文件被拆分为多个RPC请求,延迟被累积放大。说明问题与RPC交互次数相关
- 写入耗时正常但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 | # 在宿主机node210上直接对NFS挂载路径dd测试 |
结果: 宿主机dd很快
阶段性结论: 宿主机访问同一NFS正常,说明NFS服务端和物理网络没问题。问题被隔离在容器网络层。
第二阶段:定位协议差异
步骤4:确认NFS挂载参数
排查思路: NFS的挂载参数(版本、协议、传输模式等)直接影响性能。需要对比慢路径(upload)和快路径(upload1)的挂载参数差异,找出关键变量。
命令:
1 | # 容器内查看NFS挂载参数 |
作用: /proc/mounts列出当前系统所有活跃的挂载点及其参数,grep nfs过滤出NFS相关的挂载信息。
结果:
1 | 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 |
关键参数解读:
| 参数 | 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无拥塞控制,不可靠,丢包后依赖RPC层超时重传(指数退避);而TCP有快速重传和拥塞控制机制,抗丢包能力更强。需要进一步验证是否是UDP导致的问题。步骤5:跨容器对比验证
排查思路: 通过控制变量法,在A容器内测试TCP协议的NFS(upload1),并与B/C容器的UDP协议NFS进行对比,逐步缩小问题范围。
命令:
1 | # 在B/C容器内执行同样的挂载参数检查 |
结果:
| 测试 | 表现 |
|---|---|
| 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 | # 容器内存限制(字节) |
作用: cgroup v1的memory子系统通过/sys/fs/cgroup/memory/下的文件暴露内存限制和使用情况。
结果: 限制36GB,使用14GB,内存充足
阶段性结论: 排除内存压力导致的页缓存回写阻塞。
步骤7:排除conntrack表满
排查思路: nf_conntrack是Linux内核的连接跟踪表,当条目数达到最大值时,新的网络连接会被丢弃,导致UDP包丢失。需要确认conntrack是否成为瓶颈。
命令:
1 | # 当前conntrack条目数 |
结果: 当前24529,最大1048576,使用率2.3%,UDP条目0
阶段性结论: 排除conntrack表满导致UDP丢包的可能。
步骤8:检查网络插件与容器网络,并抓包验证
排查思路: 容器网络插件(Calico)通过iptables规则进行Pod网络路由。UDP包从容器出来后,会经过一系列网络栈处理。需要确认网络插件类型以及是否存在网卡层面的丢包。为进一步确认UDP链路的实际状况,本次排查增加了抓包验证环节。
命令:
1 | # 宿主机上查看所有网络接口 |
作用:
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 | # 查看D状态进程(不可中断睡眠,通常在等待I/O) |
作用:
- 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特性交互有关。
核心证据链:
- 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的处理
- 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 | apiVersion: v1 |
执行前预检:确认NFS服务端是否支持TCP mount
1 | # 验证NFS服务端是否支持TCP mount协议 |
执行步骤:
1 | # 1. 应用PV变更 |
预期结果: 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 | # node210宿主机查看网卡丢包统计 |
如果drop/error计数持续增长,说明物理层存在UDP丢包,需排查交换机端口或网卡驱动。但从本次排查的证据链来看,此方案优先级最低。
六、排查思路总结
本次排查遵循逐层隔离、对比验证的方法论:
1 | 应用层(代码) |
核心原则:
- 先量化再定位: 用dd测试量化延迟,区分"慢"和"卡死"。没有量化数据,就无法精准定位。
- 逐层隔离: 应用层 → 存储层 → 容器层 → 网络层,每步只变一个变量,避免被表象迷惑。
- 横向对比: 同一服务在不同节点的表现差异是最好的线索。如果没有B/C节点的对比,这个问题可能要被归咎于NFS服务端。
- 控制变量: upload(UDP)vs upload1(TCP)的对比直接锁定了UDP协议。这个对比之所以有效,是因为两个路径共享相同的容器网络环境,唯一的变量是协议。
- 抓包验证: 网络问题不能只靠猜测,抓包是证实丢包、重传等行为的铁证。网络栈的异常行为必须通过包级别的证据来锁定,而不是依赖对iptables规则链的推测。
https://juejin.cn/post/7664994459843706921
AI
评论已关闭