资讯动态

UDP协议详解:报文结构、MTU分片与缓冲区排障

发布时间:2026/10/26 13:07:15 来源:尧图企业网站定制
前阵子帮一个做实时语音的团队看线上问题抓包文件摊开来一半是UDP包一半是TCP重传。服务跑了两周都算平稳某天开始用户投诉“说话有回音、断断续续”而监控面板上CPU、内存、带宽全是绿色。折腾到第三天有人翻出团队内部网络笔记里标着“13.16、UDP”的那一节才意识到问题根本不在应用层代码——是发送端一直在发超过链路MTU的UDP数据报被IP层拆成了碎片任何一片丢了整个语音帧就废了。这件事之后我把UDP相关的东西重新梳理了一遍从报文结构到内核参数到应用层重传整理成下面这些内容。UDPUser Datagram Protocol用户数据报协议是传输层里最“省事”的那个协议它只做多路复用和一点点差错检测剩下的事全交给上层。这篇文章会从协议本身讲到代码落地再讲到实际部署里最容易翻车的地方适合已经写过socket但被UDP坑过的人也适合刚接触网络编程、想知道“什么时候该用UDP”的读者。看完你至少能搞清楚三件事UDP到底保证什么不保证什么、你的业务场景该不该选它、选完之后要在哪些地方自己补课。1. 无连接这三个字比你想的要重得多很多人对UDP的第一印象是“快”这个印象对但不够准确。UDP真正区别于TCP的地方不是快慢而是它压根不维护连接状态。理解这一点后面所有的取舍才有依据。1.1 无连接意味着两端互不知情TCP连接建立之后双方内核里各有一份连接控制块记录着序列号、窗口大小、拥塞状态、重传队列。这些状态是有成本的内存、CPU、以及连接建立和拆除的往返时间。UDP把这份账本整个删掉了发送方调用sendto数据报就被扔进网络收不收到、什么时候到、到的时候顺序对不对协议层面一概不管。这个设计带来的直接后果是服务端不需要为每个客户端分配资源。一个UDP服务端处理十万个客户端内存占用和十个客户端几乎没区别因为它只认“数据报里带的源地址”不认“连接”。这对那些客户端数量巨大、单次交互又很轻的场景是决定性的优势。代价也很明显。TCP的连接状态顺便承担了“对端还活着”的探测功能UDP没有这层你的应用必须自己设计心跳、自己判断对端是不是已经消失。我见过不少项目在上线半年后才想起来补心跳原因是NAT表项超时把映射删了客户端还在发服务端一个包都收不到双方都以为对方还活着。1.2 快慢只是表象责任划分才是本质拿UDP和TCP比吞吐在理想网络下差距其实没想象中大。真正的差距出现在网络变差的时候TCP会因为丢包触发重传和拥塞窗口收缩吞吐断崖式下跌UDP该发多少发多少丢的包就丢了应用层看到的是“质量下降”而不是“延迟飙升”。这两种降级方式没有绝对优劣取决于业务对“延迟”和“完整性”的偏好。传文件、传配置、传订单一个字节都不能错宁可慢一点也要完整那就该用TCP或者基于UDP自己实现可靠传输。传实时语音、传视频帧、传游戏里的位置更新晚到一秒钟的数据等于废数据那丢掉反而比等待更划算。我通常用一个判断标准如果这份数据晚到500毫秒就失去意义那它是UDP的候选如果这份数据晚到5秒钟但必须完整那老老实实用TCP。这个标准粗糙但好用能挡掉大部分选型错误。1.3 八字节报头里塞下的全部信息UDP头部固定8字节四个16位字段源端口、目的端口、长度、校验和。就这么多没有选项字段没有可变长度部分。对比TCP头部最短20字节、带选项能到60字节UDP的极简程度一眼可见。这8字节的结构决定了UDP的上限和下限。上限是单个数据报最大65535字节受长度字段16位限制扣掉IP头和UDP头之后IPv4下实际能承载的载荷是65507字节。下限是它什么额外能力都没有序号、确认、窗口、拥塞控制、连接管理全都不存在。有人会问既然这么简陋为什么还要设计它因为在某些场景下“简陋”本身就是需求。把复杂度从协议层挪到应用层意味着应用可以根据自己的业务特点做最合适的取舍而不是被协议层的通用策略绑住。QUIC就是这条思路的极致体现它把TCP那一整套可靠性机制在用户态重写了一遍为的就是能针对HTTP场景做更激进的优化。2. 把一个UDP报文拆到字节级光知道有四个字段不够实际排查问题时经常需要对着十六进制看报文。这一节把每个字段的作用和容易踩的边界说清楚。2.1 端口字段十六位撑起的多路复用源端口和目的端口各16位范围0到65535。目的端口决定数据报交给哪个应用进程源端口让接收方知道该回给谁。这两个字段是多路复用的全部基础一台机器上跑几十个UDP服务靠的就是端口区分。有个细节经常被忽略源端口可以是0。当发送方不需要接收回复时可以把源端口设成0表示“别回我”。某些扫描工具和单向通知场景会用这个技巧。反过来服务端在回包时必须用recvfrom拿到的完整地址结构而不是自己拼一个否则在多网卡机器上很容易回错地址。还有一类问题出在端口复用上。TCP有SO_REUSEADDR处理TIME_WAITUDP没有TIME_WAIT状态但多个进程绑定同一端口的场景下内核的行为取决于是否设置了SO_REUSEPORT。在Linux 3.9之后SO_REUSEPORT可以让多个进程各绑一个socket到同一端口内核按四元组哈希分流这是做UDP服务横向扩展的常见手段。不设这个选项的话第二个进程直接报EADDRINUSE。2.2 长度字段与那个绕不开的65507长度字段是16位包含UDP头部本身所以单个UDP数据报理论最大值是65535字节载荷最大65535减8等于65527字节。再往下扣IP头IPv4头最短20字节得到65507字节IPv6头固定40字节得到65487字节。但这是理论值实际能发多大取决于路径MTU。以太网默认MTU是1500扣掉20字节IP头和8字节UDP头单个UDP数据报的安全载荷是1472字节。超过这个数IP层要么分片要么在DF位Dont Fragment置位的情况下直接丢弃并回一个ICMP“需要分片”消息。我见过太多项目在这里翻车。发送端在本机测试走回环接口MTU是65536怎么发都没事一上生产走真实链路超过1472字节的包全部被分片或者丢弃表现就是“小包正常、大包不通”。所以应用层的UDP协议设计单包载荷最好控制在1200字节以内这个数留出了IPv6头、各种隧道封装VXLAN、GRE的余量是目前业界比较通行的保守值。2.3 校验和一个经常被关掉的字段校验和字段16位覆盖UDP头部、载荷以及一个“伪首部”——包含源IP、目的IP、协议号、UDP长度。伪首部的存在是为了验证数据报确实送到了正确的目的地址而不只是端口号对上了。这里有个历史遗留的坑IPv4下UDP校验和是可选的填0表示不校验IPv6下是强制的。早年有些网卡和驱动为了省CPU会把校验和计算卸载到硬件或者在驱动层直接关掉导致IPv4环境下大量UDP包的校验和是0。这在纯内网环境问题不大一旦跨公网、经过中间设备某些实现会直接丢弃校验和为0的包。提示如果你在做跨公网的UDP服务务必确认发送端开启了校验和。用tcpdump -vv看包的时候留意一下cksum 0x0000出现这个值就要警惕。校验和是16位反码求和只能检测差错不能纠错也不能防篡改。别指望它承担任何安全职责需要完整性保护就上应用层的HMAC或者直接走TLS。2.4 抓一次DNS查询看看真实结构DNS是最典型的UDP应用用它举例最直观。查询example.com的A记录UDP载荷大概是这样的结构前2字节是事务ID比如0x1a2b接着2字节标志位0x0100表示标准查询、期望递归然后2字节问题数0x00012字节回答数0x00002字节权威数0x00002字节附加数0x0000这12字节是DNS自己的头部。后面跟着查询名格式是“长度标签”的序列0x07加example七个字节0x03加com三个字节最后0x00结尾。再往后2字节查询类型0x0001表示A记录2字节查询类0x0001表示IN互联网。用tcpdump -X -s0 udp port 53能看到这串十六进制。之所以要熟悉这个结构是因为线上排查DNS问题时你经常会看到“查询发出去了但没响应”这时候需要判断是查询本身构造有问题还是响应在路上丢了。DNS默认超时是5秒重试2次总共3个查询包这个行为在/etc/resolv.conf里可以调但很多人不知道options timeout:1 attempts:2这行的存在。3. 这些场景里UDP是正确答案而不是妥协选UDP不是“退而求其次”在下面这些场景里它才是那个对的选择。判断依据始终是业务能不能容忍丢包以及能不能接受自己补齐协议层的能力。3.1 实时音视频晚到的包等于坏包语音编码器每20毫秒产生一个数据包播放端有一个几十毫秒的抖动缓冲区。如果某个包在路上多耽搁了200毫秒才到它到达时播放位置早就过去了只能被丢弃。这种情况下TCP的重传机制完全是负资产它不仅让这个包更晚到还因为队头阻塞把后面本来能按时到的包一起卡住。所以实时音视频清一色走UDP应用层自己做前向纠错FEC、丢包隐藏PLC、以及基于NACK的选择性重传。丢1%到2%的包人耳基本听不出来但如果因为等一个重传包导致整体延迟增加300毫秒通话体验会直接崩掉。3.2 DNS和DHCP这类一问一答的短事务DNS查询是典型的“一个小请求换一个小响应”交互轮次少、数据量小、对延迟敏感。用TCP的话三次握手就要一个往返查询本身再一个往返开销翻倍。UDP把这个过程压缩到一个往返代价是响应丢了要重试但重试的成本比握手低。DHCP更典型客户端在还没拿到IP地址的时候根本没条件建立TCP连接只能用UDP广播。这类“协议引导阶段”的场景UDP几乎是唯一选择。类似的还有NTP时间同步、SNMP设备管理、syslog日志上报都是一问一答或者单向推送用UDP最经济。3.3 游戏状态同步新状态永远比旧状态值钱射击游戏里服务端每秒给客户端推送20到60次玩家位置。某个位置包丢了下一个包带来的新位置会覆盖它所以丢包的影响只是“画面稍微顿一下”而不是“角色瞬移”。如果为了不丢包去用TCP队头阻塞会让所有后续的位置更新都卡在重传队列后面表现出来就是“人物卡住不动然后突然跳过去”体验比丢包差得多。这类场景的设计要点是每个状态包都是自包含的、幂等的、可丢弃的。客户端收到任何一个包都能独立还原出当前状态不需要依赖之前的包。这个设计原则反过来也约束了协议格式不能设计成“增量更新”必须是全量快照或者带足够上下文的增量。3.4 隧道与覆盖网络把可靠性推给上层各种Overlay网络、容器网络、服务网格的数据面底层通常是UDP封装。原因是隧道里跑的可能本来就是TCP流量如果隧道本身再用TCP就出现了“TCP over TCP”两层重传和拥塞控制互相干扰性能会急剧恶化。用UDP做封装内层流量的可靠性由内层协议自己保证隧道层只负责搬运。这个思路在VXLAN、Geneve、以及各种私有隧道协议里都能看到。做这类系统时要注意封装开销外层IP头20字节、UDP头8字节、隧道头8到16字节加起来三四十字节MTU要相应调小否则内层的大包会被分片。3.5 什么时候老老实实用TCP说完UDP的好得说说什么情况别硬上。如果你的业务是下面这几种选UDP基本是给自己找麻烦数据完整性要求高丢一个字节就出错比如文件传输、配置下发、数据库复制交互模式是长连接、请求响应严格配对比如HTTP API团队里没人有精力维护重传、拥塞控制、连接管理这套逻辑网络环境复杂中间有大量NAT和防火墙UDP穿透成功率不保证最后这条尤其值得强调。很多企业网络对UDP的限制比TCP严格得多某些防火墙直接封掉除53端口外的所有UDP。你的服务在测试环境跑得好好的一到客户内网就不通排查半天发现是被策略挡了。做选型时要把这个因素算进去。4. 把服务跑起来UDP的socket用法与并发模型协议层面聊完该动手了。UDP的socket API比TCP简单但“简单”不等于“没坑”几个参数设不对性能能差一个数量级。4.1 socket创建时那几个容易漏的参数先看Python版本的最小服务端import socket sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 关键接收缓冲区要在bind之前设置且内核会把值翻倍 sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4 * 1024 * 1024) sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 4 * 1024 * 1024) sock.bind((0.0.0.0, 9000)) while True: data, addr sock.recvfrom(65535) sock.sendto(data.upper(), addr)SOCK_DGRAM这个参数决定了是UDPSOCK_STREAM是TCP写错了后面全错。SO_RCVBUF值得单独说它控制内核为该socket排队的最大字节数默认值在Linux上通常是212992字节约208KB这个量在现代服务里远远不够。缓冲区一满后续到达的包会被内核直接丢弃而且不会通知应用层你只能通过/proc/net/snmp里的RcvbufErrors计数发现问题。设置这个值有两个约束一是必须在bind之前设二是实际生效值是设定值的两倍内核会翻倍且不能超过net.core.rmem_max。想设成4MB得先确认系统参数够大否则设了也白设。4.2 服务端单线程循环能撑到多少上面那个单线程循环在本地回环上跑小包echo实测能到每秒十几万次请求。这个数字看起来不错但要注意它成立的前提每个包的处理逻辑极短且没有阻塞操作。一旦处理逻辑里加了一次数据库查询或者一次磁盘写整个循环就被卡住了后面排队的包只能等。UDP服务端的并发模型大致有三条路模型适用场景注意事项单线程循环处理逻辑极轻包量中等任何阻塞操作都会拖垮整体多线程/线程池处理逻辑有IO等待线程数别超过CPU核数太多epoll事件循环高并发、多socket需要处理边缘触发和缓冲区读取我倾向于先用单线程跑用压测数据说话。如果单核跑不满说明瓶颈在业务逻辑而不是IO模型加线程也没用。真正需要上epoll的是单机几十万QPS并且要管理多个socket的场景普通业务到不了这个量级。Go的实现更省心一些运行时帮你处理了调度package main import net func main() { addr, _ : net.ResolveUDPAddr(udp, :9000) conn, _ : net.ListenUDP(udp, addr) // 加大读写缓冲区 conn.SetReadBuffer(4 * 1024 * 1024) conn.SetWriteBuffer(4 * 1024 * 1024) buf : make([]byte, 65535) for { n, remote, err : conn.ReadFromUDP(buf) if err ! nil { continue // 别因为一个错包退出循环 } conn.WriteToUDP(buf[:n], remote) } }这里有个细节ReadFromUDP返回错误时不要return或者panic。UDP的错误大多数是单个包级别的比如ICMP端口不可达回传不影响后续收包。我见过一个服务因为对端发了个畸形包触发错误代码直接退出循环整个服务假死。4.3 客户端超时、重试与幂等UDP客户端必须自己处理超时。settimeout之后recvfrom会抛异常这是唯一能感知“对方没回”的手段import socket sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(0.3) for attempt in range(3): try: sock.sendto(bping, (127.0.0.1, 9000)) data, _ sock.recvfrom(65535) print(got:, data) break except socket.timeout: print(fattempt {attempt} timeout)重试次数和超时时间要一起设计。超时设太短网络稍微抖一下就重试会造成服务端收到大量重复请求设太长用户等待时间不可接受。经验值是超时取预估RTT的2到3倍重试2到3次整体等待控制在秒级以内。重试带来的直接问题是幂等性。同一个请求可能被服务端处理两次如果你的处理逻辑是“扣库存”或者“创建订单”重复处理就是事故。解决办法是在请求里带一个唯一ID服务端维护一个短期的去重窗口。这个窗口不用太大覆盖最大重试时间就够了比如10秒。4.4 本地实测的一组数据在四核机器上跑回环echo收发各100万次不同包大小下的结果大致是这样载荷大小单线程QPS平均延迟64字节约14万7微秒512字节约12万8微秒1200字节约9万11微秒8000字节约2.5万40微秒这个数据能看出两件事。第一小包的处理成本主要花在系统调用上不是数据拷贝所以包大小从64涨到1200QPS只掉了三分之一。第二超过MTU之后性能断崖式下跌因为触发了分片每个大包变成六个小包收发两端的软中断次数翻了好几倍。这也是为什么前面强调要把应用层载荷控制在1200字节以内。注意回环测试的数字只能用来对比相对性能不能当作线上容量规划的依据。真实链路上还有网卡中断、协议栈处理、交换机转发等开销实际QPS通常只有回环的三到五成。5. 问题最集中的地方MTU、分片和缓冲区UDP的线上问题八成出在这三个地方。它们的共同点是本地测试一切正常上生产就出问题而且报错信息往往模棱两可。5.1 MTU与IP分片1500这个数字的分量以太网默认MTU 1500字节这是链路层能承载的最大帧载荷。IP层要把一个超过MTU的数据报发出去只能分片拆成多个不超过MTU的片段每个片段带自己的IP头接收端再重组。重组需要所有片段都到齐缺一片就整个数据报作废而且接收端要等一个重组超时Linux默认30秒才能释放缓冲区。这个机制对UDP极其不友好。TCP的路径MTU发现机制能自动调整分段大小UDP如果应用层不注意就是源源不断制造分片。更麻烦的是某些中间设备防火墙、NAT网关、负载均衡对分片包的处理策略不一致有的直接丢有的只放行第一个片段导致“小包能通、大包不通”的诡异现象。判断方法很简单发不同大小的包做二分# 配合前面的echo服务端 for size in 500 1000 1400 1472 1473 2000; do payload$(head -c $size /dev/zero | tr \0 a) timeout 1 bash -c echo -n $payload /dev/udp/127.0.0.1/9000 echo $size ok || echo $size fail done如果在1472和1473之间出现分界基本可以确认是MTU问题。解决方式不是去调大MTU链路设备不受你控制而是把应用层协议的单包大小降下来或者自己在应用层做分片和重组。5.2 接收缓冲区满了之后内核做了什么这是UDP最“沉默”的一类故障。应用层处理速度跟不上收包速度缓冲区逐渐填满之后到达的包会被内核直接丢弃。整个过程没有任何异常、没有返回值、socket上也不会报错应用层完全无感。唯一的线索是内核计数器。netstat -su或者读/proc/net/snmp看RcvbufErrors这一项netstat -su | grep -A2 Udp: # Udp: # 1234567 packets received # 890 in-errors # 4321 receive buffer errorsreceive buffer errors持续增长就说明在丢包。这时候有两条路一是加大缓冲区SO_RCVBUF配合net.core.rmem_max二是提升消费速度多线程、批量读、减少单包处理耗时。加大缓冲区是有极限的它只是把“丢包”推迟到流量峰值之后。真正的解法还是让消费速度跟上生产速度。我的经验是缓冲区别超过8MB再大就是在掩盖问题而且会显著增加内存占用和延迟包在缓冲区里排队也是有时间的。5.3 UDP没有粘包但会截断TCP有粘包问题UDP没有因为UDP保留了消息边界一个recvfrom对应一个数据报。这个特性让协议解析简单不少但也带来另一个坑如果传入的缓冲区比数据报小多出来的部分会被直接丢弃而且recvfrom只返回缓冲区大小的字节数你根本不知道原来那个包有多大。# 错误写法缓冲区太小大包会被静默截断 data, addr sock.recvfrom(1024) # 收到一个1500字节的包只拿到1024字节 # 正确写法用足够大的缓冲区 data, addr sock.recvfrom(65535)这个坑隐蔽的地方在于对某些协议来说截断后的数据仍然“看起来合法”。比如一个JSON包被截断解析会失败你能发现但如果是一个定长二进制协议截断后字段错位可能解析出一个完全错误但结构合法的结果问题会飘到很远的地方才暴露。5.4 一次“发了但没收到”的完整排查链回到开头那个语音团队的问题把当时的排查过程完整还原一下这个链路对大多数UDP故障都适用。第一步确认包到底发出去没有。在发送端抓包tcpdump -i eth0 -n udp port 9000 -c 100。如果发送端能看到包说明应用层和socket没问题问题在链路或接收端。第二步看包的大小和分片标志。tcpdump -vv输出里如果看到flags [DF]或者frag字样就说明涉及分片。当时抓到的包是1600多字节明显超过MTU。第三步在接收端抓包对照。接收端只看到了一部分包缺失的正好是那些大包。这时候方向基本确定了大包在路径上被丢弃。第四步验证假设。把发送端的包大小临时改成1000字节问题消失。确认是MTU/分片问题。第五步定位根因。查代码发现语音帧的打包逻辑在检测到网络状况好时会自动增大帧长以提升音质但打包逻辑用的是“理论最大载荷”没考虑链路MTU。这是个典型的设计缺陷协议设计时假定链路能承载任意大小的数据报。修复方案是双管齐下打包层加一个1200字节的硬上限超过就拆成两帧发同时在接收端增加对分片的容忍度即使偶尔收到分片也能正确重组。上线后问题消失。这个案例的经验是UDP问题一定要在两端同时抓包单看一端永远只能看到一半的事实。发送端看到的是“我发了”接收端看到的是“我没收到”中间那段才是真相所在。6. 自己造可靠性从应用层ACK到QUIC当你决定用UDP又需要一定程度的可靠性时就得在应用层把这套机制补上。这一节讲最小可行的实现思路以及现成方案能做到什么程度。6.1 序号、确认与重传定时器最小可靠的UDP协议需要三个东西每个包带序号、接收方回ACK、发送方超时重传。序号用32位足够从0开始递增回绕也没关系只要比较时用模运算就行。ACK可以设计成累积确认“我收到序号N及之前的全部包”或者选择确认“我收到了这些序号”。累积确认简单但效率低一个包丢了后面全得重传选择确认复杂但省带宽实际系统大多用两者的结合。重传定时器的取值是个技术活。固定超时比如200毫秒实现简单但在高延迟链路上会造成大量不必要的重传。自适应超时的做法和TCP一样测量RTT用指数加权移动平均算平滑RTT再乘以一个系数得到超时时间。RFC 6298给了参考算法直接照抄就行别自己发明。# 简化版的自适应RTO计算 srtt None rttvar None def update_rto(rtt_sample): global srtt, rttvar if srtt is None: srtt rtt_sample rttvar rtt_sample / 2 else: rttvar 0.75 * rttvar 0.25 * abs(srtt - rtt_sample) srtt 0.875 * srtt 0.125 * rtt_sample return srtt max(4 * rttvar, 0.01) # 下限10毫秒6.2 去重与乱序重排网络不保证顺序同一个包也可能因为重传而到达两次。接收端需要一个滑动窗口来管理“已收到但还没交付给应用”的包。窗口大小决定了能容忍多少乱序太小会导致频繁等待太大占内存。窗口的实现通常用一个位图或者一个有序集合。收到序号N的包检查窗口内是否已存在不存在就插入并回ACK然后检查窗口左边界能不能往前推进能推进多少就把连续的包按序交付给应用。去重的关键是ACK必须带序号而且接收端要维护一个“已确认的最大序号”。对于重复包直接丢弃但重新回一次ACK因为原ACK可能在路上丢了发送方正在等。6.3 拥塞控制为什么躲不掉这点很多人想不通我用UDP就是为了避开TCP的拥塞控制为什么自己实现的时候还得加回来因为拥塞控制不是TCP的“额外功能”而是网络稳定的必要条件。如果所有UDP应用都无节制地全速发送中间路由器的队列会被填满所有人都开始丢包整个网络进入拥塞崩溃。TCP的拥塞控制本质上是一种“社会契约”UDP应用不遵守短期能抢到带宽长期会把链路搞垮包括自己。业界的做法是参考TCP的实现做简化版慢启动阶段指数增长到达阈值后线性增长检测到丢包时减半。QUIC用的是类似的CUBIC算法。如果你的应用是在可控的内网环境可以简化一些但外网场景一定要有拥塞控制否则对己对人都是灾难。6.4 QUIC把哪些事替你做了如果不想从头造轮子QUIC是目前最成熟的选择。它在UDP之上实现了可靠传输、拥塞控制、加密握手、多路复用而且把连接建立和加密握手合并比TCP加TLS快一个往返。QUIC相比自己造轮子的优势在于经过了大规模线上验证各种边界情况都处理过有成熟的库比如各种语言的quic-go、quiche、msquic封装支持连接迁移客户端换IP连接不断。代价是复杂度高调试困难。QUIC的流量全是加密的中间设备看不到内容抓包分析基本靠应用层日志。如果你的场景只是简单的请求响应用QUIC有点杀鸡用牛刀但如果是移动端长连接、需要多路复用、需要连接迁移那QUIC几乎是目前最优解。7. 上线前值得逐条过一遍的东西前面讲的都是原理和设计这最后一节是实操层面的检查清单都是我实际部署时踩过或者见过别人踩的。7.1 内核参数与缓冲区设置UDP服务的性能对这三个参数极其敏感# 接收缓冲区上限应用层设置的值不能超过这个 net.core.rmem_max 16777216 # 发送缓冲区上限 net.core.wmem_max 16777216 # 网卡接收队列长度高流量下小了会丢包 net.core.netdev_max_backlog 5000rmem_max不调大应用层setsockopt设多大都没用会被截断到系统上限。这个问题特别隐蔽因为setsockopt不报错返回成功但getsockopt读回来是个小值。养成设置完之后读一遍确认的习惯。另外netdev_max_backlog在万兆网卡、小包高吞吐的场景下很容易成为瓶颈。网卡收包速度超过内核处理速度时包会堆在这个队列里队列满了就丢。这个计数在/proc/net/softnet_stat里能看到第二列持续增长就是丢了。7.2 该盯的监控指标UDP服务的监控和TCP不太一样TCP有连接数、重传率这些天然指标UDP什么都没有得自己造。至少要盯这几个指标来源异常含义收包速率应用层计数突然归零可能是链路或端口问题缓冲区溢出计数/proc/net/snmp消费速度跟不上发送失败计数应用层sendto返回值本地资源或路由问题超时重试率应用层统计网络质量下降或对端异常包大小分布应用层统计出现大包要警惕分片其中“超时重试率”最能反映真实体验。这个值通常在0.1%以下升到1%以上就说明网络有问题了。我们当时设的告警阈值是0.5%触发后自动降级到更保守的发送策略。7.3 反射放大与源地址校验UDP有一个TCP没有的安全隐患源地址可以伪造。发送方随便填一个源IP包照样能发出去接收方如果按这个地址回响应就变成了对第三方的攻击。放大攻击就是利用这一点伪造受害者IP发大量请求让服务端把响应全打到受害者身上。防御的核心是源地址校验。最简单的方式是要求客户端在第一个包里带一个token服务端回一个带token的响应客户端第二个包必须带上这个token才被处理。这样伪造源IP的攻击者收不到第一个响应也就拿不到有效token。QUIC的握手设计里就内置了这套机制。除了协议层校验还要在运维层面做限速对单IP的请求速率做限制对异常大的响应做限制DNS的ANY查询、NTP的monlist这类历史问题都是响应远大于请求。这些措施不需要做到极致挡住大部分自动化攻击就够了。我在实际做UDP服务这些年最深的体会是UDP给你的自由最后都会变成你的工作量。TCP帮你把重传、顺序、拥塞、连接管理全处理了你不需要想这些换成UDP这些账全部记在你头上。所以选型的时候别只看“UDP快”要看你的团队有没有能力把省下来的这部分补回去。另一个常被忽略的点是测试环境的保真度。回环测试跑得再顺也测不出MTU、NAT超时、防火墙策略这些问题。我的做法是至少搭一个跨网段的测试环境中间放一台能做丢包和延迟注入的机器用tc netem模拟5%丢包、100毫秒延迟能提前暴露八成的问题。最后分享一个小技巧给每个UDP服务加一个/debug/udpstats之类的内部接口把收发包数、缓冲区溢出、超时重试、包大小分布这些计数器吐出来。线上出问题的时候这个接口的价值比任何日志都高因为它反映的是“协议层发生了什么”而不是“业务代码以为发生了什么”。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑