必须限制长轮询并发数,因每个请求独占goroutine,易致调度延迟与gc压力;需用信号量或带缓冲channel控制(如限500),配合context超时与取消检查防泄漏,并合理设置事件channel容量。

长轮询请求会卡住goroutine,必须限制并发数
Go 的 HTTP handler 是同步执行的,每个长轮询请求都会独占一个 goroutine。如果客户端发起 1000 个长轮询连接,服务端就同时运行 1000 个 goroutine 在 select 上等待——不是“轻量”,而是实实在在的内存与调度开销。默认 runtime.GOMAXPROCS 下,过多 goroutine 会导致调度延迟上升、GC 压力陡增。
实操建议:
- 用
semaphore或带缓冲的 channel 控制并发长轮询请求数,例如只允许最多 500 个活跃连接 - 在 handler 开头加计数器 + defer 减计数,配合 prometheus 暴露
longpoll_active_connections指标 - 避免用
sync.WaitGroup等待所有连接退出——它不感知 context 取消,容易阻塞 shutdown
连接挂起时间越长,服务端资源占用越不可控
长轮询不是“等 30 秒”,而是“可能被 Nginx 断在 240 秒、被客户端关在 15 秒、被网络中间件 kill 在 90 秒”。服务端若只依赖 time.After 而忽略 ctx.Done(),就会出现 goroutine 泄漏:连接已断,但 goroutine 还在 channel 上死等。
常见错误现象:runtime: goroutine stack exceeds 1000000000-byte limit 或 pprof 显示大量 goroutine 停留在 select 或 chan receive 状态。
实操建议:
- 每个 handler 必须创建独立
context.WithTimeout(r.Context(), 230*time.Second),比 Nginx 的proxy_read_timeout小至少 10 秒 - 注册事件 channel 前先检查
if ctx.Err() != nil { return },尤其在w.(http.Flusher).Flush()之后 - 不要复用全局
context.Background(),它永远不会取消,泄漏风险极高
事件通道容量和推送方式直接影响吞吐与稳定性
用 chan []byte 接收推送数据时,若 channel 容量为 1,而高并发写入方(如消息总线回调)连续发两条消息,第二条就会阻塞在 ch ,拖慢整个推送链路;若容量过大(比如 10000),又可能导致内存暴涨或旧消息积压无法及时淘汰。
实操建议:
- channel 容量设为 100 左右较平衡:够缓冲突发推送,又不至于吃光内存
- 推送方必须用非阻塞写法:
select { case ch ,失败即丢弃或降级打日志 - 不要用
close(ch)清理 channel——这会导致接收方 panic,应从sync.Map中delete()后让 goroutine 自然退出
Nginx 配置不当会让 Go 服务“看起来正常却持续泄漏”
很多团队压测时发现 QPS 上不去、goroutine 数缓慢上涨,查了半天代码没毛病,最后发现是 Nginx 在背后默默断连:proxy_read_timeout 60 默认值让所有长轮询在 60 秒被切断,而 Go handler 还在等 time.After(240 * time.Second),根本收不到断连信号。
实操建议:
- Nginx 必须配置
proxy_read_timeout 240和proxy_buffering off - 禁用
proxy_http_version 1.1下的 keepalive timeout 干扰(某些版本会覆盖 read_timeout) - 上线前用
curl -v http://your-api/poll观察响应头是否含Connection: keep-alive,且无Transfer-Encoding: chunked干扰 flush
真实负载压力不在“有没有新数据”,而在“连接生命周期管理是否闭环”。哪怕只有 200 个长轮询客户端,只要 channel 注册没配锁、超时没对齐代理层、flush 后没检查 ctx,就足以让服务在一天内积累上千个僵尸 goroutine。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











