SetWriteDeadline 和 SetNoDelay 是平滑广播延迟最关键的两步:前者防慢连接阻塞,后者禁用 Nagle 避免小包延迟叠加;缺失任一,其余优化收益几近归零。

netpoll 本身不解决广播延迟震荡,它只是 Go 运行时对 epoll/kqueue/iocp 的封装,只管监听 socket 是否可读可写;真正导致延迟忽高忽低的,是广播路径中未控制的系统调用抖动、缓冲区分配毛刺、慢连接阻塞和调度抢占。
为什么 SetWriteDeadline 能压住延迟尖刺
不设写超时,一个接收窗口卡死或 NAT 断连的客户端会让对应 goroutine 长期 park 在 conn.Write() 上,既不失败也不返回,整批广播就被拖住。pprof 里能看到大量状态为 IO wait 的 goroutine 堆在 runtime.gopark 下。
- 必须每次调用
Write()前重置:conn.SetWriteDeadline(time.Now().Add(2 * time.Second)) - 超时值不宜低于 1s(太短误杀正常弱网),也不宜高于 5s(失去保护意义)
- 只对
os.IsTimeout(err)做降级处理(如标记离线),不要panic或盲目重试 - 该 deadline 不影响读操作,需单独配
SetReadDeadline
批量写 + ringBuffer 是平滑延迟的核心
逐个 conn.Write() 会触发 N 次 epoll_wait 唤醒和内核拷贝,网络抖动时唤醒间隔拉长,延迟直接震荡。高频小包尤其明显——Nagle 算法会攒包,TCP 延迟确认又等 ACK,两头叠加出 200ms 级毛刺。
- 禁用 Nagle:
conn.SetNoDelay(true),否则再快的 netpoll 也救不了 - 每个连接配一个无锁环形缓冲区(如
github.com/chenzhuoyu/ringbuffer),所有广播消息先入 buffer - 由固定数目的 worker(建议 2–4 个)统一轮询各连接的 buffer,用
writev批量刷出 - 避免在广播 hot path 分配新
[]byte,用sync.Pool复用缓冲区
Kubernetes 部署下最容易被忽略的调度陷阱
容器里 --cpus=1 但 Go 默认 GOMAXPROCS=8,导致多个 P 抢一个 CPU 时间片,广播 goroutine 频繁被挤出运行队列——trace 图里表现为“runnable”时间长但实际没执行,延迟曲线锯齿状跳变。
- 启动时显式设置:
runtime.GOMAXPROCS(1)或通过环境变量GOMAXPROCS=1 - 若需更高吞吐,改用
--cpus=2+GOMAXPROCS=2,但禁止跨 NUMA 节点绑核 -
cloudwego/netpoll的EventLoop.Runner可注册非阻塞回调,绕过 goroutine 调度开销,但仅适用于自研长连接服务(如 WebSocket 网关),不兼容http.Server - 检查是否启用了
cgo:DNS 解析或 TLS 握手若走 cgo,会阻塞整个 M 线程,放大调度不均
延迟震荡从来不是单点问题,而是 netpoll、TCP 栈、Go 调度器和容器编排四层耦合的结果;最常被跳过的动作是重置 WriteDeadline 和关闭 SetNoDelay,这两步不做,其他优化收益几乎被抵消。


















