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不可达,但客户端没处理,游戏就卡住了。

排查步骤:

  1. 确认对端端口是否监听:用ss -uln或netstat -uln查看。
  2. 抓包看ICMP回复:在发送端抓包,过滤icmp,看有没有Destination Unreachable。
  3. 检查防火墙规则:确保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丢包原因与排查体系 传输层UDP丢包:三大原因与排查路径 UDP传输层丢包 ① 校验和错误 ② 端口不可达 ③ 接收缓冲区溢出 排查手段 抓包看checksum字段 ethtool关闭硬件卸载 排查手段 抓ICMP不可达包 ss -uln确认监听状态 排查手段 netstat -s看丢包计数 ss -unlp看Recv-Q 修复方案 更换网卡/驱动 关闭硬件校验和卸载 修复方案 确保服务端端口监听 放开ICMP防火墙策略 修复方案 调大rmem_max/rmem_default 优化应用读取速度 排查顺序建议:先看缓冲区溢出 → 再查校验和 → 最后确认端口不可达

传输层的这三个问题,排查起来其实有套路。我个人习惯按概率排序:先看缓冲区溢出(最常见),再看校验和(硬件相关),最后查端口不可达(配置问题)。按这个顺序走,大部分UDP丢包问题都能快速定位。