如果你写过高并发实时系统,一定经历过这种绝望:每来一个连接就开一个协程,内存暴涨,垃圾回收器时不时卡死整个世界。作者花了几年时间踩遍这些坑,最后把5万个并发连接塞进500个协程,延迟压到1毫秒以下。他是怎么做到的?
从"每个连接一个协程"到"共享工人池"
传统做法的问题很直观。WebSocket连接是长连接,可能空闲几小时,但协程一直占着栈内存。一千个连接还好,十万个连接时,光是协程栈就能把机器吃光。
作者的解法:让多个连接共享一小群"工人协程"。
他设计了一个连接池结构体,核心是一个缓冲通道:
workers chan *ConnectionWorker
通道里预放着固定数量的工人对象。新连接到来时,从通道取出一个工人;连接关闭时,工人归还。作者通常把池子设成几百到几千,视并发量而定。
关键代码在handleConnection里:<-ms.connPool.workers 会阻塞,直到有工人可用。这意味着同一时刻,一个工人只服务一个连接,但工人可以在连接间快速切换。500个工人就能扛住5万个空闲连接——调度器切换开销远低于创建销毁协程。
零拷贝:砍掉两次内存分配
标准WebSocket库的性能杀手是拷贝。数据从网络缓冲区拷进新的字节切片,如果要转发,再拷一次。两次分配,两次拷贝,高吞吐时垃圾回收压力爆炸。
作者用sync.Pool预分配4KB缓冲区池。读取时直接写进预分配的buf,然后把指针传给处理器。处理器要么当场处理,要么自己决定要不要拷贝留存。原文没说具体转发场景,但逻辑很清楚:避免不必要的复制,把控制权交给业务层。
bufPtr := ms.msgRouter.bufferPool.Get().(*[]byte)
这行代码是性能分水岭。Get()复用旧内存,Put()归还,循环使用。
为什么这套方案能跑起来?
两个优化是互相配合的。工人池解决了"协程爆炸",零拷贝解决了"内存分配爆炸"。垃圾回收器的压力从两个方向被削减:更少的协程意味着更少的栈扫描,更少的分配意味着更频繁的GC触发被延后。
作者没提具体硬件配置,只说是"modest machine"(普通机器)。5万并发、1毫秒延迟,这两个数字是在这种环境下测出来的。
这套设计的边界在哪?
工人池本质是资源受限的并发控制。如果所有工人都被占满,新连接会阻塞在取工人环节——这是背压机制,防止系统过载崩溃。但这也意味着极端突发流量下,连接建立会有延迟。
零拷贝要求处理器不能长期持有缓冲区指针,否则池子会枯竭。原文说处理器要么立即处理数据,要么自己拷贝留存,把责任推给业务层,这是性能换灵活性的典型取舍。
作者没讨论消息广播场景。5万个连接如果要做群推,500个工人串行处理会不会成为瓶颈?这是留给读者的思考题。
值得动手试一把
如果你正在用Go写实时系统,这套模式可以直接搬。代码结构清晰:池化、复用、显式资源管理,没有黑魔法。作者的开源实现没有给出完整仓库链接,但核心逻辑已经在片段里——工人池、缓冲池、阻塞式获取,几十行代码就能搭出骨架。
下一步:拿你的业务场景压测。工人池大小该配多少?4KB缓冲区够不够用?这些数字没有银弹,但有了基准,调参就有方向。
热门跟贴