
UDP丢包时WriteTo不报错,但对方收不到
Go 的 net.Conn.WriteTo(或 UDPConn.WriteTo)调用成功,只代表数据进了内核发送队列,不等于对方收到了。UDP 本身无确认、无重传,写入成功 ≠ 投递成功。
常见错误现象:WriteTo 返回 nil 错误,日志里一切正常,但业务层发现消息“消失”了;尤其在高丢包率网络(如弱网模拟、容器间跨节点通信)下更明显。
- 别依赖
WriteTo的返回值判断消息是否送达 - 如果需要可靠性,必须自己实现应用层 ACK + 重传逻辑,不能靠系统调用兜底
- 注意:Linux 内核 UDP 发送缓冲区满时,
WriteTo才会返回ENOSPC或ENOBUFS,但这种错误极少触发,不能作为丢包探测手段
用SetReadDeadline和SetWriteDeadline控制超时,但别设太短
UDP 是无连接协议,SetReadDeadline 控制的是从 socket 接收缓冲区读出数据的阻塞等待时间,SetWriteDeadline 控制的是把数据拷贝进内核发送队列的超时 —— 它们都不涉及网络往返,所以设成几毫秒(比如 5ms)反而容易误判失败。
使用场景:用于防止协程卡死,比如做请求-响应式交互时,等 ACK 的合理窗口通常在几十到几百毫秒之间。
立即学习“go语言免费学习笔记(深入)”;
-
SetReadDeadline要配合循环ReadFrom使用,超时后需主动 continue,否则一次超时就退出接收循环 - 不要对每个
WriteTo都设WriteDeadline,除非你明确知道发送队列可能长期拥塞 - 实测中,局域网内设
100ms读超时较稳妥;公网或移动网络建议 ≥500ms
自己实现重传逻辑时,避免用time.After启动 goroutine
高频发包场景下,每条消息都起一个 time.After + goroutine,会导致 goroutine 泄漏和调度压力。典型错误写法:go func() { 。
性能影响:每秒发 1000 条消息,每条重试 3 次,就会累积 3000 个待唤醒定时器,大量 goroutine 处于休眠态,GC 和调度开销陡增。
- 改用单个
time.Ticker或heap维护待重传任务,统一驱动 - 给每条待重传消息打上序列号和时间戳,避免重复 ACK 导致乱序重传
- 重传间隔建议用指数退避(如
base * 2^attempt),首重传可设50ms,上限封顶到1s
MTU 和分片导致的静默丢包
UDP 包超过路径 MTU(通常 1500 字节)会被 IP 层分片;任一片丢失,整个 UDP 包就被接收方内核丢弃,且不通知上层 —— 这是真正的“静默丢包”,连 ReadFrom 都收不到。
容易踩的坑:本地测试用 localhost 或 Docker 网络,MTU 往往是 65535,一跑生产环境(特别是云厂商 VPC、K8s CNI)立刻掉包。
- 保守起见,应用层限制单包 ≤
1200字节(留足 IP+UDP 头部和可能的隧道封装空间) - 不要依赖
conn.LocalAddr().(*net.UDPAddr).Zone获取 MTU,它返回的是接口配置值,不是实际路径 MTU - 若必须发大包,得自己做应用层分片 + 组帧,并带校验和与偏移标识,比用 TCP 成本还高
真正难处理的不是丢包本身,而是丢包不可见、不可区分 —— 你永远不知道是没发出去、中途丢了、还是对方根本没调用 ReadFrom。所有优化都得围绕“让不可见变得可探测”来设计。










