udp在workerman中比tcp更易丢包卡顿,因net_rx软中断单核饱和或net.core.rmem_max过小导致内核直接丢包(udpinerrors上涨),而tcp有重传机制兜底;workerman的onmessage无法触发,多进程对收包无直接帮助。

UDP 高并发在 Workerman 里不能靠堆进程数硬扛,核心瓶颈是内核收包路径的软中断(NET_RX)单核饱和和 UDP socket 接收缓冲区溢出,必须从网卡中断绑定、socket 参数、Worker 进程协同三方面一起调。
为什么 UDP 在 Workerman 里比 TCP 更容易丢包卡顿
Workerman 的 UDP 支持基于 socket_recvfrom + select/epoll 轮询,它本身不维护连接状态,但收包效率完全依赖内核能否及时把网卡数据搬进 socket 缓冲区。一旦 NET_RX 软中断全挤在 CPU0 上,或 net.core.rmem_max 太小,新包直接被内核丢弃(netstat -s | grep "packet receive errors" 会显示 UdpInErrors 持续上涨),Workerman 根本收不到——这不是代码问题,是系统层漏包。
- 现象:压测时客户端发包正常,服务端
ss -uul显示 recv-q 长期 >0,netstat -s中UdpInErrors每秒涨几十上百 - 关键区别:TCP 有重传机制兜底,UDP 丢一个就少一个;Workerman 的
onMessage回调不会触发,你连日志都打不出来 - 不要迷信
$worker->count:UDP 不走连接队列,多进程对收包无直接帮助,反而可能因竞争加剧缓存失效
必须改的三个内核参数和 socket 设置
Workerman 启动前要显式调大 UDP socket 的接收缓冲区,并确保内核允许应用突破默认限制。否则即使设了 SO_RCVBUF,也会被内核 silently 截断。
- 临时生效(验证用):
sysctl -w net.core.rmem_max=26214400(25MB),sysctl -w net.core.rmem_default=4194304(4MB) - 永久生效:写入
/etc/sysctl.conf并执行sysctl -p - Workerman 代码中必须显式设置(放在
new Worker()之后、Worker::runAll()之前):if (function_exists('socket_set_option')) { $worker->setSocketOption(SOL_SOCKET, SO_RCVBUF, 4194304); } - 注意:Linux 默认
net.core.rmem_max=212992(208KB),远低于高并发 UDP 场景需求;不调这个,SO_RCVBUF设再大也白搭
网卡中断必须手动绑到多个 CPU,且禁用 irqbalance
UDP 收包比 TCP 更吃软中断,因为没连接状态要维护,纯靠中断驱动搬运数据。现代多队列网卡(如 ixgbe、i40e)的每个 RX 队列对应一个 IRQ,必须把它们分散到不同 CPU,否则 CPU0 一满,所有包等死。
- 确认多队列启用:
ethtool -l eth0查Combined最大值(比如 8),再ls /sys/class/net/eth0/device/msi_irqs/看是否真有 8 个 IRQ 目录 - 查当前分布:
cat /proc/interrupts | grep eth0 | head -5,观察每行末尾各 CPU 计数是否严重倾斜 - 手动绑定(以 IRQ 40-47 绑定到 CPU0-7 为例):
for i in {40..47}; do echo 0-$((i-40)) > /proc/irq/$i/smp_affinity_list; done - 关掉
irqbalance:systemctl stop irqbalance && systemctl disable irqbalance,自动均衡在高吞吐下反而抖动 - 验证:
watch -n1 'cat /proc/interrupts | grep eth0',压测时各 CPU 计数应基本均匀
Workerman 进程数与 reusePort 协同策略
UDP 场景下 $worker->reusePort = true 是刚需,但它和进程数配置必须匹配,否则反而引发端口争抢或负载不均。
-
$worker->count应等于你绑定的 CPU 核数(比如绑了 CPU0-3,就设为 4),让每个 Worker 进程独占一个核处理收包+业务逻辑,避免跨核缓存同步开销 - 必须开启
$worker->reusePort = true,否则所有进程竞争同一个 socket,内核无法做负载分发 - 禁止用
SO_REUSEADDR替代:reusePort是 Linux 3.9+ 特性,能真正实现内核级分发;SO_REUSEADDR只解决 TIME_WAIT 端口复用,对 UDP 分发无效 - 示例:
$udp_worker = new Worker("udp://0.0.0.0:9502"); $udp_worker->count = 4; $udp_worker->reusePort = true; $udp_worker->onMessage = function($connection, $data) { // 业务逻辑要极轻量,避免阻塞事件循环 $connection->send("ACK"); };
最容易被忽略的是:UDP 优化效果没法靠 ab 或 wrk 测,得用真实发包工具(如 nping 或自研 client)持续发包并监控 UdpInErrors;另外,业务逻辑哪怕只加一行 sleep(0.001),都会让整个事件循环卡住,导致后续包堆积在 socket 缓冲区直至溢出——UDP 的脆弱性远超 TCP,容错空间几乎为零。











