go语言支撑百万级mqtt连接的关键在于非阻塞i/o、连接复用、内存池管理及系统层调优,而非简单堆goroutine;go-iot-platform通过禁用tls默认超时、手动解析mqtt头、sync.pool复用buffer、两级会话缓存等手段,将百万连接内存压至100mb内,并依赖somaxconn和ulimit等os参数保障底层承载能力。
go 语言能撑住百万级 mqtt 连接,但不是靠“开更多 goroutine”硬堆出来的——关键在于避免每连接独占 goroutine、控制文件描述符增长、绕过标准库的隐式阻塞行为。
为什么 net/http 风格的 for { conn, _ := listener.Accept(); go handle(conn) } 在百万连接下会崩
这种写法看似简洁,实则埋了三个雷:
- 每个
conn默认启用Read/Write阻塞调用,goroutine 会长期挂起在系统调用上,百万 goroutine = 百万线程栈(哪怕只 2KB,也近 2GB 内存) -
listener.Accept()返回的net.Conn是阻塞型,无法与epoll/kqueue统一调度,失去事件驱动优势 - Go runtime 对 goroutine 的调度器(M:P:G 模型)在高并发下会因频繁抢占和状态切换产生可观开销
真正可行的路径是:用 syscall.EpollWait 或封装好的 gobwas/ws / nhooyr.io/websocket 等非阻塞库接管连接生命周期,让单个 goroutine 轮询处理成百上千连接。
go-iot-platform 如何把 MQTT 连接数压到 100MB 内内存跑满百万
它没用 github.com/eclipse/paho.mqtt.golang 这类客户端风格库,而是直接基于 net + syscall 构建服务端,并做了三处关键裁剪:
- 禁用 TLS 握手的默认超时逻辑,改用固定 buffer + 手动解析 MQTT 固定头(
CONNECT,PUBLISH等),跳过crypto/tls的 goroutine 分配 - 连接复用读写 buffer:
sync.Pool管理[]byte,避免每次收发都 malloc;MQTT 报文长度字段明确,可预分配 - 心跳检测不依赖
time.Ticker,而是在每次读操作后更新 lastActive 时间戳,空闲连接由后台 goroutine 批量扫描清理
实测中,开启 SO_KEEPALIVE + TCP_USER_TIMEOUT 后,连接存活判断延迟从 2 分钟降至 30 秒以内,有效防止僵尸连接堆积。
Linux 系统层必须调的两个参数:否则连 10 万都卡死
Go 程序再优化,卡在 OS 层就全白搭。启动前务必确认:
-
/proc/sys/net/core/somaxconn至少设为65535(默认常为 128),否则listen()的完成队列溢出,新连接直接被内核丢弃,现象是客户端connect()随机超时 -
ulimit -n必须 > 实际连接数 × 2(每个连接至少占 1 个 fd,加上日志、监控等),建议设为1048576;且需在 systemd service 文件里加LimitNOFILE=1048576,仅 shell 里 ulimit 不生效
漏掉任一参数,你会看到 accept: too many open files 或 accept: invalid argument,而不是预期的业务错误。
MQTT 协议层不能省的轻量级会话管理
百万连接 ≠ 百万会话。真实场景中,95% 设备是低频上报(如每 5 分钟一次),go-iot-platform 用两级缓存规避全量 session 内存驻留:
- 内存中只保留最近活跃的 10 万 session(含 clientID、cleanSession 标志、未 ACK 的 QoS1 报文)
- 其余 session 元数据落盘到 LevelDB(或 SQLite WAL 模式),按需加载;QoS1 消息体本身不缓存,只存 offset 和 topic
- 重连时先查内存缓存,未命中再异步加载,同时允许新连接携带
cleanSession=false但无历史会话时静默创建空 session
这个设计让内存占用和连接数几乎解耦——连接数涨到 200 万,只要活跃会话仍控制在 10 万内,内存波动就很小。容易被忽略的是:MQTT Will Message 的存储必须同步落盘,否则断连瞬间崩溃会导致遗嘱丢失。











