Repository navigation
UDP写操作阻塞导致读操作停滞及内核接收缓冲区满溢 #26
Description
所使用的 tnet 版本
v1.0.1
当前程序GO版本:
go1.23.2
操作系统和处理器架构
AlmaLinux 8.10 (Cerulean Leopard) x86_64 Intel(R) Xeon(R) Platinum 8255C CPU @ 2.50GHz Core(s) per socket: 8
操作步骤
服务器使用tnet的UDP模块,开启多路复用,配置为isIONonblocking=false, useBusinessRoutinePool=true
监听多个客户端向服务器发送的UDP数据包,在正常逻辑中,按一定频率固定返回消息给同组的客户端。
期望结果
即使写操作失败(EAGAIN),writing锁应合理释放,并通知底层再次读取需要发送的数据继续发送
实际观察到的结果
通过ss -numpa查看服务器UDP连接状态,程序多次出现UDP监听端口接收数据缓存溢出现象
UNCONN 46214656 0 0.0.0.0:25000 0.0.0.0:* users:(("server",pid=724130,fd=42))
skmem:(r46214656,rb46214400,t0,tb26214400,f512,w0,o0,bl0,d16490)
以下为dlv工具捕获数据:
inBuffer: trpc.group/trpc-go/tnet/internal/buffer.Buffer {
head: ("trpc.group/trpc-go/tnet/internal/buffer.node")(0xc164e6ec30),
tail: ("trpc.group/trpc-go/tnet/internal/buffer.node")(0xc164e6ec30),
rnode: ("trpc.group/trpc-go/tnet/internal/buffer.node")(0xc164e6ec30),
wnode: ("trpc.group/trpc-go/tnet/internal/buffer.node")(0xc164e6ec30),
rlock: (*sync.Mutex)(0xc003274b98),
wlock: (sync.Mutex)(0xc003274ba0),
rlen: 0,
wlen: 92,
nodeBlockSize: 4096,
enableAutoNodeBlockSize: true,},
outBuffer: trpc.group/trpc-go/tnet/internal/buffer.Buffer {
head: ("trpc.group/trpc-go/tnet/internal/buffer.node")(0xc0c9f78e70),
tail: ("trpc.group/trpc-go/tnet/internal/buffer.node")(0xc13fcc2b70),
rnode: ("trpc.group/trpc-go/tnet/internal/buffer.node")(0xc0c9f78e70),
wnode: ("trpc.group/trpc-go/tnet/internal/buffer.node")(0xc13fcc2b70),
rlock: (sync.Mutex)(0xc003274bd8),
wlock: (sync.Mutex)(0xc003274be0),
rlen: 4370476,
wlen: 0,
nodeBlockSize: 16384,
enableAutoNodeBlockSize: true,}
rtimer: trpc.group/trpc-go/tnet/internal/timer.Timer nil,
wtimer: trpc.group/trpc-go/tnet/internal/timer.Timer nil,
nfd: trpc.group/trpc-go/tnet.netFD {
desc: ("trpc.group/trpc-go/tnet/internal/poller.Desc")(0xc000430e60),
sock: trpc.group/trpc-go/tnet.goSockCloser(net.UDPConn) ...,
laddr: net.Addr(net.UDPAddr) ...,
raddr: net.Addr nil,
network: "udp",
fd: 42,
fdtype: fdUDP (1),
closed: ("go.uber.org/atomic.Bool")(0xc003274c60),
locker: (sync.Mutex)(0xc003274c68),
udpBufferSize: 548,},
closer: trpc.group/trpc-go/tnet.closer {
sysReadJob: ("trpc.group/trpc-go/tnet/internal/safejob.ExclusiveUnblockJob")(0xc003274c78),
sysWriteJob: ("trpc.group/trpc-go/tnet/internal/safejob.ExclusiveUnblockJob")(0xc003274c88),
apiReadJob: ("trpc.group/trpc-go/tnet/internal/safejob.ExclusiveBlockJob")(0xc003274c98),
apiWriteJob: ("trpc.group/trpc-go/tnet/internal/safejob.ConcurrentJob")(0xc003274ca8),
apiCtrlJob: ("trpc.group/trpc-go/tnet/internal/safejob.ExclusiveBlockJob")(0xc003274cc8),
closeAllJob: ("trpc.group/trpc-go/tnet/internal/safejob.OnceJob")(0xc003274cd8),},
postpone: trpc.group/trpc-go/tnet/internal/autopostpone.PostponeWrite {
prevPacketsNum: 0,
readingTryLockFailed: ("go.uber.org/atomic.Uint32")(0xc003274ce8),
loopCnt: 0,
consecutiveSamePacketsNumCounts: 0,
enable: true,},
reading: 0,
writing: 1,
nonblocking: false,}
outBuffer:持续堆积数据outBuffer.rlen≈4.3MB;
由于outBuffer始终非空,writing锁(writing=1)被一直持有,底层epoll未再次收到可写事件,导致数据一直驻留在outBuffer缓存中;
因为IO操作都在同一个poller协程中,因为发送阻塞了,引起了接受缓冲区溢出?
Activity
[-]UDP 同步模式下写操作阻塞导致读操作停滞及内核接收缓冲区满溢[/-][+]UDP写操作阻塞导致读操作停滞及内核接收缓冲区满溢[/+]silencelbl commentedon Aug 11, 2025
@WineChord 大佬有空看看呢
Rememorio commentedon Jun 18, 2026
感谢反馈。看现象和代码路径,这个问题不像是单纯使用方式问题,
WithNonBlocking(false)+ 业务协程池处理 UDP 请求本身是 tnet 支持的模式。我初步看了下 UDP 写路径:当
WriteTo无法立即写出时,数据会进入outBuffer,并通过ModReadWriteable由 poller 后续触发udpOnWrite发送;udpOnWrite遇到EAGAIN/EWOULDBLOCK时当前实现会直接返回,并保持writing状态,只有outBuffer清空后才会切回只读事件并释放writing。所以你看到的outBuffer堆积和writing=1与当前实现路径是吻合的。不过
writing=1本身不一定等价于死锁,它也表示当前连接已经进入“由 poller 负责继续刷写”的状态。真正需要确认的是:在 UDP 发送侧持续背压时,是否会导致写事件长期占用/反复触发,从而拖慢读事件处理,最终造成内核接收缓冲区溢出。我倾向于把这个问题按 tnet UDP 写侧背压/公平性问题继续看,后续可能需要补充:
udpOnWrite遇到 EAGAIN 后的事件处理策略;如果方便的话,也麻烦补充一下:
UDPSendMMsgFails/UDPSendMMsgPackets等指标是否有异常;silencelbl commentedon Jun 18, 2026
包不大,500字节以内,当时频率有点高。
不是客户端不读,是数据根本没进入网络底层进行发包。
当时观察UDPSendMMsgFails / UDPSendMMsgPackets没见异常。
有开启 reuseport,poller当时是8个。
时间有点久远了,当时不止遇到一次这个问题,表现就是outBuffer持续堆积数据,writing锁(writing=1)被一直持有,底层不在处理读写事件。