3. 传输层丢包:UDP校验和错误、端口不可达、接收缓冲区溢出
传输层是UDP通信的“命门”。数据到了这一层,如果出问题,基本就是三个原因:校验和错了、端口没人要、或者缓冲区满了。我排查过不少UDP丢包案例,十有八九都绕不开这三座大山。
咱们一个一个说清楚。
3.1 UDP校验和错误
UDP头部有个校验和字段,用来检测数据在传输过程中有没有被篡改或损坏。发送端计算一个值填进去,接收端重新算一遍,对不上就丢包。
很多人以为UDP不保证可靠,所以校验和可有可无。其实不是。IPv4下UDP校验和是可选的,但IPv6下是强制的。而且,就算在IPv4里,很多网卡和驱动默认也会开启校验和检查。
核心逻辑:校验和错误 = 数据损坏 = 直接丢弃。没有重传,没有通知。
我在项目中遇到过一个问题:某台服务器的网卡硬件故障,导致发出的UDP包校验和总是算错。对端收一个丢一个,业务完全跑不起来。当时抓包一看,校验和字段全是错的,但应用层死活找不到原因。
排查方法其实很简单:
- 抓包对比:用tcpdump或Wireshark抓包,看校验和字段是否显示"incorrect"。
- 关闭硬件校验和卸载:有些网卡会硬件计算校验和,但驱动有bug。可以试试关闭这个功能。
# 查看网卡校验和卸载状态
ethtool -k eth0 | grep checksum
# 关闭TX校验和卸载(以eth0为例)
ethtool -K eth0 tx-checksum-ip-generic off
我的习惯:排查UDP丢包时,第一件事就是看校验和。抓包5秒钟就能确认是不是这个问题,别绕弯路。
3.2 端口不可达(ICMP Destination Unreachable)
UDP是无连接的。发送端把数据扔出去,根本不管对端有没有进程在监听那个端口。如果目标端口上没有应用程序在收,操作系统就会回复一个ICMP包,告诉你“端口不可达”。
但问题来了——发送端收到这个ICMP包后,默认什么都不做。UDP没有机制去重发或者通知应用层。说白了,这个包就这么丢了。
注意:ICMP端口不可达不是一定会被发送端收到。很多防火墙会直接过滤ICMP包。你收不到通知,但包确实丢了。
我曾经帮一个游戏团队排查问题。他们的客户端发UDP包给服务器,偶尔会丢。查了半天,发现是服务器端某个服务重启时,端口还没注册好,客户端正好在这个窗口期发数据。服务器回了ICMP不可达,但客户端没处理,游戏就卡住了。
排查步骤:
- 确认对端端口是否监听:用
ss -uln或netstat -uln查看。 - 抓包看ICMP回复:在发送端抓包,过滤
icmp,看有没有Destination Unreachable。 - 检查防火墙规则:确保ICMP包没有被中间设备拦截。
# 在发送端抓ICMP包
tcpdump -i eth0 icmp and host 192.168.1.100
# 在接收端查看UDP监听端口
ss -uln | grep 12345
避坑指南:我曾经见过一个案例,防火墙把ICMP全封了,结果UDP丢包查了三天。后来放开ICMP,问题立刻定位。所以,排查时别忘了检查防火墙策略。
3.3 接收缓冲区溢出
这是UDP丢包最常见的原因,没有之一。
每个UDP socket都有一个接收缓冲区。应用程序从缓冲区里读数据,如果读得不够快,缓冲区满了,新来的包就会被丢掉。内核不会通知你,直接扔。
为什么会满?说白了就是“生产者太快,消费者太慢”。
| 场景 | 原因 | 典型表现 |
|---|---|---|
| 应用层处理慢 | 业务逻辑复杂、阻塞I/O、GC停顿 | 丢包率随CPU负载升高而增加 |
| 突发流量 | 瞬间大量UDP包涌入 | 丢包集中在流量峰值时刻 |
| 缓冲区太小 | 系统默认值不够用 | 持续丢包,但应用负载不高 |
排查方法:
- 查看丢包统计:用
netstat -s或ss -s看UDP的丢包计数。 - 查看当前缓冲区使用量:用
ss -unlp看Recv-Q列。 - 调整缓冲区大小:通过
setsockopt或系统参数修改。
# 查看UDP丢包统计
netstat -s | grep -i "packet receive errors"
# 查看具体socket的接收队列
ss -unlp | grep 12345
# 调整系统级UDP接收缓冲区最大值(单位:字节)
sysctl -w net.core.rmem_max=26214400
sysctl -w net.core.rmem_default=26214400
我的经验:很多开发者觉得缓冲区越大越好。其实不是。缓冲区太大,会导致延迟增加,而且内存占用飙升。我一般建议从默认值开始,监控丢包率,逐步调大,找到平衡点。
另外,应用程序里也可以设置socket缓冲区大小:
#include <sys/socket.h>
int sock = socket(AF_INET, SOCK_DGRAM, 0);
int rcvbuf = 1048576; // 1MB
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
注意:内核会对setsockopt设置的值做上限限制,不能超过net.core.rmem_max。所以要先调大系统参数,再在应用里设置。
本章知识体系
下面这张图把传输层UDP丢包的三个原因、排查手段和修复方向串起来了。你可以把它当作排查时的“作战地图”。
传输层的这三个问题,排查起来其实有套路。我个人习惯按概率排序:先看缓冲区溢出(最常见),再看校验和(硬件相关),最后查端口不可达(配置问题)。按这个顺序走,大部分UDP丢包问题都能快速定位。