1.3 机器测算:CVM 撑多少连接
场景
老板问我们要多少台机器、什么配置。预算会上不能拍脑袋,得算。IM 系统的特点是连接数巨大、单连接很闲:一万个用户挂在线上,可能一分钟都没几条消息,但每个连接都实实在在占着内存和文件描述符。所以我们测算的核心指标不是 QPS,而是单机能承载多少长连接。这个数字直接决定了网关层的机器数量和每月的固定成本。
方案
单机连接数的瓶颈有三个地方:文件描述符、内存、CPU。Linux 默认每个进程只给 1024 个 fd,不改这个参数,一万连接都撑不到。内存上每个连接在 Go 里大约占 8~20KB(goroutine 栈加读写 buffer 加心跳定时器),按 10 万连接算就是 1~2GB 的常驻开销,看着不多,但广播峰值还有临时分配。真正要命的是 CPU:一个万人群一条消息出去就是一万次网络写,全压在一个逻辑里就是广播风暴。这三个瓶颈哪一个先到顶,容量就是哪个数。
我们按两档规划:网关机要内存大、CPU 中等,跑足够多的连接;逻辑层和存储分离部署,避免广播把存储也拖垮。存储用 TDSQL-C MySQL,它本身是云数据库,扩容不用我们操心物理机。文档入口:CVM 的规格与选型清单见 cloud.tencent.com/document/product/213。
连接数、内存、广播能力三者不是独立的,我们撮合一个综合公式贴在墙上天天看:单机承载连接数约等于(可用内存减去系统预留)除以每连接常驻开销;安全广播峰值约等于 CPU 核数乘以每核每秒能处理的写次数。两个数取小的那个,就是这台机器的真实容量。任何单调指标都会误导,IM 的容量是多维约束。
线上还有个与测算相关的反直觉:高峰期最先到顶的往往不是平均资源,而是某个瞬间的尖峰。比如老板群里有人发了条带图的消息,几百人同时点开大图,回执和 ack 一起涌回网关。我们后来学会把容量按尖峰算再打七折,纸面数字才真正对得上线上观测。
另外各档资源配置存在明显木桶效应。我们一开始在网关机上挂了一块很小的系统盘,结果连接数还没到,日志先把磁盘写满,整个进程 crash。云服务器的系统盘、数据盘、带宽都是独立计费的,测算清单里要把它们逐项列出来,别只盯着 CPU 和内存两项。具体费用归集方式见 成本复盘:连接与存储怎么算钱。
| 机型配置 | 预估连接数 | 定位 | 备注 |
|---|---|---|---|
| 4核 8GB CVM | 15~20 万 | 网关(保守估) | 也够逻辑层混部 |
| 8核 16GB CVM | 30~40 万 | 网关(主力) | 广播峰值更从容 |
| 8核 16GB CVM | 不按连接算 | 逻辑层 | 按 QPS 扩 |
| TDSQL-C MySQL | 按容量计费 | 消息存储 | 读写分离见后文 |
| 云数据库 Redis | 按实例规格 | 未读数/会话 | 2GB 起步够用 |
注意表里的连接数都是调优后的乐观值,没调 fd 参数前这些数字一律打五折。
采购前建议先看一眼活动页折扣,我们第一次买就赶上 云服务器 CVM 特惠,新户价格比列表价便宜不少,省下的钱够多买两台网关。采购规格定下之后,先买一台做压测,别一次买齐,压测数字和测算差多少补多少,这样最省钱。
配置与代码
单机先做系统调优,这是撑连接的地基,不改参数一切测算都白搭:
# 文件描述符放开
ulimit -n 1048576
# 内核层面:连接队列与 TIME_WAIT 复用
sysctl -w net.core.somaxconn=65535
sysctl -w net.ipv4.tcp_max_syn_backlog=65535
sysctl -w net.ipv4.tcp_tw_reuse=1
Go 侧网关监听保持最朴素的 per-connection-per-goroutine 模型,初始栈给小一点省内存:
func main() {
ln, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
for {
conn, err := ln.Accept()
if err != nil {
continue // 单个 accept 错误不能拖垮整个网关
}
go serveConn(conn)
}
}
测算时还需要把带宽放进公式。一条 200 字节的文本消息,单聊只产生一次写,但万人群就是一万次写,瞬时出向约 2MB,听上去不多,可高峰期每秒几十条群消息叠加,Gbps 级别内也就到顶了。所以我们给网关机定的容量公式是:
// 单机容量粗算:连接数由内存决定,广播由 CPU 决定,峰值由带宽决定
func capacityPlan(memGB int, mbps int, perConnKB int) int {
connByMem := memGB * 1024 * 1024 / (perConnKB * 1024)
_ = mbps // 带宽单独评估,取三者最小
return connByMem
}
这只是纸面估算,真正签字买机器前必须压测一把。压测脚本的思路也很简单:开足 goroutine 不断建连,每个连接按真实频率发心跳和消息,同时盯三个指标——连接数、内存占用、出向带宽。我们的压测工具直接复用了网关的 codec(见 协议先定:WebSocket 还是 TCP),三天就写完。
踩坑
第一个坑是栽在 fd 上。压测到 1 万连接就报 too many open files,查了半天发现是 systemd 服务的 LimitNOFILE 没改,ulimit 只对当前 shell 生效,服务重启后又回去。后来在 systemd 单元文件里写死 LimitNOFILE=1048576 才彻底解决。这就是典型的纸上测算都对、一跑就错。
第二个坑是内存估算太理想。纸上算 10KB 每连接,实际跑起来每个连接的读 buffer、心跳定时器、会话对象加起来奔着 20KB 去,加上广播瞬间的临时分配,8GB 机器压到 30 万连接就开始抖动。后来我们把读 buffer 从默认 4KB 调到 2KB,并用 sync.Pool 复用消息对象,才稳在 40 万。
第三个坑是忘记算带宽。某次群消息风暴把网卡出向带宽打满,监控上连接数一切正常,就是消息延迟飙升,排查了两个小时才看到网卡包量曲线。从此我们把网卡出向带宽也纳入告警体系(见 监控告警:连接数与积压)。
第四个坑是容量规划没留 buffer。我们按 1:1 的在线率买机器,结果某次节假日活动在线用户翻倍,网关内存直接顶到 90%,临时扩容手忙脚乱。后来固定留 30% 的余量习惯,宁可平时闲着。
第五个坑是多机部署的连接分布不均。上线分组里有一台机器 health check 配置偏长,负载均衡持续往同一台灌连接,那台机器先爆。所有线性的容量测算都是按单机理想分布算的,实际上负载均衡的健康检查策略会显著影响分布,这事我们后来在 长连接网关:连接管理与会话表 里用一致性哈希和优雅退出解决了一半,剩下的一半靠监控发现再手动摘机。容量这件事说到底,测算是地图,监控才是路况。
小结
测算思路一句话:按连接数选内存,按广播压力选 CPU,按峰值出向带宽选网卡,先把 fd 和内核参数调好再谈容量。纸上算完一定要压测验证,单机压测:先摸清单台的底 给出了我们当时压到 40 万连接的完整过程。存储侧的容量测算是另一本账,参考 存储扩容:先走读写分离,最终的成本归集在 成本复盘 里。
容量测算这件事值得团队反复做。它逼着每个人把连接、内存、广播、带宽串成一条链路想清楚,而不是只盯着自己负责的模块。这份手算的习惯,后来在排查任何线上抖动时都派上用场。
容量规划不是一次性的活。我们把测算表固化成了季度复盘流程:每季度末把实际峰值(连接数、QPS、带宽)与三个月前的预测值逐项比对,偏差超两成的项要写归因,是业务增速变化还是架构变化。这个习惯跑了两个季度后,预测模型的误差收敛到百分之十五以内,扩容申请从此有了说服力而不是靠嗓门。顺带给一个数据库量测的经验:把线上高峰的十分钟流量录下来做对比回放,比看平均值曲线更能暴露瓶颈,平均数总是偏向你好的一面。
压测不是一次性的事。我们约定每次网关大版本、协议改动或依赖中间件升级后都重跑一次基线,用同一套压测脚本同样参数对比,偏差超过百分之十必须给出解释。另一个容易被忽略的指标是单机句柄数与内存的对应关系:一条连接在网关进程里大约占二十多 KB 的读缓冲与上下文,算上定时器与统计字段实际开销接近三倍,压测时内存涨得比预期快往往是缓冲区参数留大了。按实测反过来调 read buffer,能把单机连接数上限再往上抬一截,这比换高配机器便宜多了。
