golang高性能消息推送核心是控制连接层保活限流、分发层路由批处理、存储层异步落库与状态幂等;用错一道qps上不去,用对4c8g可撑30w+活跃pv。

直接说结论:Golang 做高性能消息推送,核心不是堆协程数量,而是控制好三道闸门——连接层的保活与限流、分发层的路由与批处理、存储层的异步落库与状态幂等。用错一道,QPS 上不去,用对了,4c8g 机器撑住 30w+ 活跃 PV 是常态。
长连接管理:别让 net.Conn 泄漏或卡死
WebSocket 连接不是“建完就完”,不加管控的 net.Conn 在高并发下会迅速耗尽文件描述符或触发 TCP TIME_WAIT 暴涨。真实场景里,90% 的连接异常都发生在心跳和读写超时没设对。
- 必须为每个连接配独立
readLoop和writeLoopgoroutine,禁止共用一个循环做读写——conn.Write()可能阻塞,导致心跳 ping 发不出 - 心跳间隔建议设为
30s,服务端用time.AfterFunc定时发websocket.PingMessage,客户端 pong 回复后重置定时器;超时未响应(如 90s)直接conn.Close() - 连接元数据(
userID、deviceID、tags)存进sync.Map,不要查 DB 或 Redis——热路径上一次远程调用就是延迟放大器 - 连接对象本身建议用
sync.Pool复用,避免高频分配导致 GC 压力;池中对象要显式清空字段(如userID = 0),否则可能残留脏数据
消息分发:按 channel 分片 + 批量 publish,别遍历全量连接
把所有在线用户塞进一个 map[userID]*Conn 然后 for-range 推送,是典型性能反模式。360w 用户里哪怕只有 10% 在线,单次广播也得遍历数万连接,CPU 直接打满。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 采用两级路由:先按业务 topic(如
"order_update")哈希到 64 个 shard,再在 shard 内按userID % 16路由到子队列,每个子队列配独立chan *Message和固定 worker 数(比如 4 个) - Redis PubSub 不要每条消息都
Publish,改用内存缓冲:同一 channel 的消息攒够 50 条或 100ms 触发一次redis.Client.Publish,QPS 降 90%,延迟反而更稳 - 客户端订阅关系用
map[string]map[uintptr]struct{}存(key 是 topic,value 是 conn 指针集合),增删用sync.RWMutex,但锁内只做指针操作,绝不调 DB 或 RPC - 若需支持动态标签(如
tag=ios_vip),用布隆过滤器预筛 + 后置校验,避免每次推送都全量匹配字符串
投递可靠性:ack 必须幂等,失败必须进 retry_queue
“已发送”不等于“已送达”。线上最常踩的坑是:没做 ack 去重,导致用户收到 3 条一模一样的订单通知;或者重试逻辑写在主线程里,一条慢请求拖垮整个分发链路。
- 每条消息带唯一
messageID(用uuid.NewV7()或时间戳+seq),客户端收到后主动上报ACK {messageID, userID},服务端用redis.SetNX("ack:"+messageID, "1", time.Hour)保证幂等 - 未收到 ack 的消息,30s 后触发第一次重试(指数退避,最多 2 次),重试任务丢进内存延迟队列(
timer.AfterFunc),失败则写入 Kafka/RocketMQ 的retry_topic,由独立消费者兜底 - 消息落库必须异步:先写 WAL 日志(如 Badger),再发到分发 channel;DB 写入走
context.WithTimeout(ctx, 3*time.Second),超时直接丢弃并告警,不阻塞主流程 - 离线消息不能只靠 Redis List 缓存——容量不可控,要用带 TTL 的
zset存未读消息 ID,score 设为过期时间戳,定时用ZRANGEBYSCORE清理
真正难的从来不是“怎么推”,而是“推错之后怎么收场”。比如重试队列堆积时要不要自动降级?用户连续断连 3 次后该不该暂停推送?这些边界逻辑没写进代码,压测时永远看不出问题。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










