4、UDP校验和:计算开销与关闭策略。

UDP 校验和,说白了就是一段 16 位的冗余码。发送方算一遍,接收方再算一遍,对不上就丢包。就这么个简单机制,却让很多人在性能调优时纠结不已——关还是不关?

我个人习惯,先把校验和的计算开销搞清楚,再谈策略。否则你做的优化决策,心里没底。

4.1 校验和到底在算什么?

UDP 校验和覆盖的范围,不只是 UDP 头和数据。它还把 IP 伪头(pseudo header)也算进去了。伪头里包含源 IP、目的 IP、协议号、UDP 长度。这么设计是为了检测数据是否送错了地方。

算法本身很简单——16 位反码求和。发送方把所有 16 位字累加,进位回卷,最后取反。接收方做同样的计算,结果应该是全 1。如果不是,说明数据坏了。

关键点:校验和计算是纯 CPU 操作,不涉及 DMA 或硬件卸载(除非网卡支持)。这意味着每个 UDP 包,CPU 都要亲自下场算一遍。

我在项目中遇到过一种情况:某网关设备转发 UDP 流量,CPU 占用率居高不下。排查下来,发现每个包都要做两次校验和——一次入站验证,一次出站生成。对于 64 字节的小包,校验和计算占了将近 30% 的 CPU 开销。

4.2 计算开销到底有多大?

我们来拆解一下。假设一个 1472 字节的 UDP 数据(刚好填满标准以太网 MTU),校验和计算需要:

  • 遍历所有数据,按 16 位字读取
  • 执行加法 + 进位回卷
  • 最后取反

在 2.5 GHz 的 x86 处理器上,纯软件计算大约需要 0.5~1 微秒。听起来不多对吧?但你要想想看,如果每秒处理 100 万个包,光校验和就要吃掉 0.5~1 秒的 CPU 时间。这还只是一个核。

包大小 单包计算时间 (2.5GHz x86) 1M pps 总耗时 CPU 占用率 (单核)
64 字节 ~0.15 μs ~0.15 s 15%
256 字节 ~0.3 μs ~0.3 s 30%
1472 字节 ~0.8 μs ~0.8 s 80%

嗯,这里要注意:现代 CPU 有 SSE/AVX 指令集加速,实际耗时可能更低。但嵌入式平台或低端 ARM 处理器,这个数字可能翻 3~5 倍。

4.3 什么时候可以关闭校验和?

关闭校验和,意味着 UDP 层不再做数据完整性检查。接收方拿到什么就交给上层。这显然有风险,但某些场景下值得权衡。

我总结了几种可以考虑关闭的情况:

  • 链路层已经提供强校验:比如光纤通道、InfiniBand,或者启用了 CRC32 的以太网。链路层已经能检测到绝大多数比特错误。
  • 应用层自带校验:比如视频流、游戏同步数据,应用层有自己的序列号和 CRC。UDP 校验和就变成了冗余。
  • 性能敏感且丢包容忍度高:实时音视频、传感器数据流。偶尔一个坏包,比延迟抖动更容易接受。
  • 硬件卸载不可用:有些老旧网卡不支持校验和卸载,纯软件计算成为瓶颈。

警告:跨网络传输(尤其是公网)时,绝对不要关闭校验和。中间的路由器、交换机、光模块都可能引入比特翻转。我曾经见过一个案例,某公司内部视频会议系统关闭了 UDP 校验和,结果在跨机房传输时出现了大量花屏——链路层 CRC 没抓到,但 UDP 数据已经坏了。

4.4 关闭校验和的具体操作

在 Linux 上,关闭 UDP 校验和有两种方式:

方式一:socket 选项(仅限发送端)

int val = 0;
setsockopt(sock, SOL_SOCKET, SO_NO_CHECK, &val, sizeof(val));

设置 SO_NO_CHECK 后,内核在发送 UDP 包时会把校验和字段填为 0。接收端看到校验和为 0,就知道发送方没做校验,直接跳过验证。

方式二:全局关闭(不推荐)

# 通过 sysctl 关闭所有 UDP 校验和验证
sysctl -w net.ipv4.udp_no_check=1

这个参数会让内核跳过所有入站 UDP 包的校验和验证。但注意,它只影响接收端,不影响发送端。

我的建议:尽量用 socket 级别控制,别动全局参数。你永远不知道系统上其他应用是否依赖校验和保护。我曾经接手过一个线上事故,就是有人全局关了校验和,导致某个金融交易系统的 UDP 行情数据出现了静默损坏。

4.5 硬件卸载:两全其美的方案

其实,最好的策略不是关掉校验和,而是让硬件来做。现代网卡普遍支持 UDP 校验和卸载(Checksum Offload)。

开启卸载后,发送路径上,网卡硬件在数据发出前计算并填充校验和。接收路径上,网卡验证校验和,把结果标记在描述符里。CPU 只需要读一下标记,不用亲自算。

效果非常明显:

  • CPU 开销几乎降为 0
  • 校验和保护依然存在
  • 延迟没有增加

检查你的网卡是否开启了卸载:

ethtool -k eth0 | grep udp
# 输出示例:
# tx-udp-segmentation: on
# rx-udp-gro-hw: on
# tx-checksum-ip-generic: on
# tx-checksum-udp: on
# rx-checksum: on

如果 tx-checksum-udp 和 rx-checksum 都是 on,说明硬件卸载已经生效。你不需要做任何额外优化。

4.6 决策流程图

下面这张图,是我在实际项目中用来做决策的流程。你可以参考一下:

UDP 校验和关闭决策流程 开始评估 网卡支持校验和卸载? ethtool -k eth0 | grep checksum 是 开启硬件卸载 否 链路层有强校验? 光纤/InfiniBand/CRC32 是 应用层自带校验? 序列号/CRC/应用层ACK 是 校验和是性能瓶颈? perf top 确认 CPU 热点 是 关闭 UDP 校验和 否 否 否 保持校验和开启 说明:任何一步为“是”都可以考虑关闭;全部为“否”则必须保持开启

4.7 避坑指南

最后,分享几个我踩过的坑:

  • 不要在生产环境全局关闭。我曾经为了压测性能,在测试机全局关了校验和,结果忘了改回来。第二天线上出现间歇性数据损坏,排查了整整一个下午。
  • 硬件卸载不是万能的。有些廉价网卡的卸载实现有 bug,校验和算出来是错的。我遇到过一款 Realtek 网卡,开启卸载后偶尔会生成错误的校验和,导致接收端疯狂丢包。最后只能关掉卸载,用软件计算。
  • 虚拟化环境要小心。在 KVM/VMware 里,虚拟网卡的校验和卸载可能被透传到宿主机。如果宿主机和虚拟机都做了卸载,反而会出现重复计算或漏算。建议在虚拟机里关掉卸载,让宿主机统一处理。
  • IPv6 的校验和是强制的。IPv4 允许校验和为 0(表示不校验),但 IPv6 的 UDP 校验和是必填的。如果你在 IPv6 环境下试图关闭,内核会直接拒绝发送。

一句话总结:优先用硬件卸载,其次按场景选择性关闭,永远不要全局关闭。校验和不是性能的敌人,盲目优化才是。