消息积压是gorilla/websocket中readmessage阻塞与业务同步处理共同引发的连接雪崩前兆;必须将读、写、处理三步拆分为独立goroutine,严禁在readmessage后直接执行db写入或http调用等耗时操作,而应仅做轻量解包校验后立即投递至业务channel。

消息积压不是“卡一下就过去”的小问题,而是 gorilla/websocket 里 ReadMessage 阻塞 + 业务同步处理共同导致的连接雪崩前兆。必须拆开读、写、处理三步,各自独立运行。
ReadMessage 不能和 DB 写入写在同一个 goroutine 里
这是最常踩的坑:在 for 循环里直接调 conn.ReadMessage(),然后立刻 db.Exec() 或 http.Post()。一旦数据库慢或第三方接口超时,整个连接就挂起,后续消息全堆在 TCP 缓冲区,read: connection timed out 几乎必然发生。
- 只做轻量操作:解包 JSON、校验
msgType和userID、提取关键字段 - 构造好结构体后,立刻发到业务 channel,例如:
c.msgChan - 绝对不要在读循环里做任何阻塞 I/O,包括日志同步写、文件写、锁竞争高的 map 操作
每个连接必须配独立的 writePump goroutine + send channel
writePump 是 gorilla/websocket 官方示例里就强调的模式,但很多人只复制了代码,没理解它为什么非得是单独 goroutine —— 因为 conn.WriteMessage() 是同步阻塞的,如果多个地方并发往一个连接写,会 panic;如果写逻辑里有耗时操作,又会拖垮读协程。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- channel 类型建议用
chan []byte或带序列号的chan Message,避免反复 JSON 序列化 - buffer 长度设为 64 或 128:太小(如 8)容易触发
select { case c.send 导致连接被误杀;太大(如 1024)会让积压延迟暴露不出来 - 务必加
select { case c.send ,防止 goroutine 卡死
积压水位监控比扩容更关键
靠加机器扛不住突发流量,但没人看 len(c.send) 就等于闭眼开车。真实积压往往不报错,只是内存悄悄涨、GC 频繁、响应变慢。
- 在
Client结构体里加两个字段:recvQueueLen int64和lastRecvTime time.Time - 每收到一条消息,更新这两个字段,并定期上报到 Prometheus:
websocket_client_recv_queue_length{client_id="x"} - 配合心跳检测:若
lastRecvTime超过 30s 且len(c.send) > 0,标记为“疑似卡死”,主动conn.WriteMessage(websocket.CloseMessage, ...) - 不要只依赖
conn.SetReadDeadline()—— 它只管网络层空闲,不管业务层是不是早卡死了
二进制消息混大 JSON 时内存增长最隐蔽
很多压测只发 "hello" 字符串,上线后一推真实 payload(比如 50KB 的 base64 图片 + 嵌套 10 层的 JSON),send channel 里的每个元素都占一大块内存,runtime.ReadMemStats 会突然飙升,但错误日志里连 warning 都没有。
- 上线前必须用真实 payload 压测:构造含二进制字段的 struct,走一遍完整编解码路径
- 考虑对大消息做预检:超过 16KB 的
messageType == websocket.BinaryMessage直接拒绝或走单独通道 - 避免在
writePump中重复json.Marshal()—— 接收端就该把 JSON 序列化好再塞进sendchannel
真正难处理的从来不是“怎么发出去”,而是“发不出去时系统还知不知道”。只要 len(c.send) 可观测、lastRecvTime 可联动、select+default 不漏掉满队列,积压就从故障变成指标。










