长连接存活靠心跳而非setreaddeadline设一次就完事:该方法仅对下一次read()生效,每次成功读(含ping/pong/业务数据)后必须立即重置;心跳间隔需小于中间设备空闲阈值(推荐30–45秒),且须配对收发、独立预检、写操作配setwritedeadline防卡死。

长连接存活靠心跳,不是靠SetReadDeadline设一次就完事
很多人以为在连接建立后调用一次 conn.SetReadDeadline 就能永久保活,结果连接静默几分钟就断了。根本原因是:该方法只对下一次 Read() 生效,读完不重置,下次读直接返回 io.EOF 或阻塞超时。
- 每次成功读到数据(包括心跳包
"ping"、"pong"或业务消息)后,必须立刻重置读超时:conn.SetReadDeadline(time.Now().Add(45 * time.Second)) - 心跳间隔要小于中间设备(NAT、SLB、防火墙)的空闲回收阈值,实测推荐 30–45 秒;服务端收到
ping必须回pong,且客户端收到pong后也要重置自己的读超时 - 别把心跳和业务逻辑混在一个 buffer 里解析——心跳失败不能导致主协议解析崩溃,建议用独立小 buffer 预检前 4 字节判断是否为心跳帧
goroutine泄漏比连接泄漏更危险
每个连接起一个 readLoop + 一个 writeLoop 是常见模式,但一旦连接已断,写 goroutine 还在往 conn.Write() 发数据,就会永久卡住、持续占内存和 fd。
-
Write()必须配SetWriteDeadline,超时即退出,不能无限制等待 -
readLoop退出时,必须通过chan struct{}或context.WithCancel显式通知writeLoop停止,不能依赖defer conn.Close()——它不会中断正在阻塞的Write() - 用
sync.WaitGroup跟踪活跃 goroutine 数,服务重启或关闭时可等待它们自然退出,避免强制 kill 导致数据丢失
连接数上万时,系统限制先于 Go 代码生效
Go 能轻松跑 10w+ goroutine,但 net.Conn 底层是文件描述符(fd),Linux 默认单进程最多 1024 个 fd。不调系统参数,连接数卡在几千就爆 too many open files。
- 启动前执行
ulimit -n 65535;若用 systemd,需在 service 文件里加LimitNOFILE=65535 - 内核参数也要调:
net.core.somaxconn(全连接队列长度)、net.ipv4.tcp_max_syn_backlog(半连接队列),否则 SYN 包直接被丢弃 - 连接元数据(如 user_id、device_id)别存全局 map,改用
sync.Map或分片 map,避免锁竞争成为瓶颈
消息分发不能靠遍历,得按标识哈希路由
给 1000 个用户推消息,如果每次都遍历所有连接筛选目标,CPU 和延迟会随连接数线性增长。这不是并发问题,是算法问题。
- 写入时根据
user_id或topic哈希到固定 shard(比如 64 个),每个 shard 独立chan *Message+ 消费 goroutine - 订阅关系用
map[string][]*Conn维护,增删时加RWMutex,但锁内禁止 DB 查询、HTTP 调用等耗时操作 - 推送关键链路必须落库(如 Badger 或 WAL 日志),客户端 ack 后才标记送达;超时未 ack 触发重试(最多 2 次,指数退避),再失败转离线消息进 Kafka
真正卡住高并发长连接的,往往不是 Go 语法或 goroutine 数量,而是 fd 上限、心跳配对缺失、写 goroutine 卡死、以及消息路由没做分片。这些点漏掉任意一个,连接数上到几千就开始掉连接、OOM 或响应延迟飙升。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











