2028 字
约 6 分钟
5
S02-J03 · 实时通信工程:连接数、心跳、消息可靠性与水平扩展

适用读者:要把长连接系统做到"十万级"并对容量负责的人。
前置:S02-B08(心跳与重连)。
实测环境:Ubuntu 24.04.1 / Python 3.12 asyncio / Linux 6.8;连接与内存为 2026-11-05 实测。
覆盖旧文:—(E 驱动;B08 的机制在此工程化)
覆盖扩展:E-S02-024(fd 上限与连接数)、E-S02-025(扇出与消息队列)。
上一篇:S02-J02 · TLS 深水区:握手细节、证书链、双向认证与常见错误
下一篇:S02-A01 · 实时通信架构选型。


1. 一句话结论

单机连接数由三个硬约束决定:文件描述符、内存、端口(仅客户端侧)。 三者的最小值就是上限。

单机上限 = min( fd 上限 / 每连接 fd 数,
                可用内存 / 每连接内存,
                可用端口数(仅主动连接方) )

2. 实测:2000 条长连接的真实成本

同一个进程内建服务端 + 2000 个客户端(loopback):

建立 2000 条连接,耗时 1.90s
RSS: 19.9 MB -> 39.2 MB,增量 19.4 MB
每条连接约 9.9 KB(含客户端与服务端两侧)

# 关闭后内核侧
TCP: 2043 (estab 7, closed 2010, timewait 2006)

解读:

观察 含义 推论
建立速率 ≈ 1,050 连接/秒(单进程 Python) accept 与握手是串行成本 启动风暴时先限速(B08 §4 的抖动)
每连接 ≈ 9.9 KB(两侧合计) 应用层缓冲 + 对象开销 单侧约 5 KB;但业务缓冲才是大头(见 §3)
关闭后出现 2,006 条 TIME_WAIT 主动关闭方留下 TIME_WAIT 服务端主动踢连接会堆 TIME_WAIT(B03 §4)

注意:这是"空连接"的成本。真实业务里每条连接还要挂发送缓冲(未确认消息)、订阅关系、会话状态——那才是内存主要来源。


3. 容量估算:把三个约束算清楚

3.1 约束一:文件描述符

$ ulimit -n
524288                      # 本机(容器内往往只有 1024!)
$ cat /proc/sys/fs/file-max
9223372036854775807
检查项 命令 说明
进程 soft 上限 ulimit -n 生产要调到 10 万+
进程 hard 上限 ulimit -Hn 不能超过 hard
系统级上限 cat /proc/sys/fs/file-max 通常足够
当前已用 ls /proc/<pid>/fd | wc -l 定位泄漏

每个连接占 1 个 fd(服务端),客户端每连接也占 1 个。

3.2 约束二:内存

内存 ≈ 连接数 × (内核 socket 缓冲 + 应用每连接对象 + 业务缓冲)
组成 量级 说明
内核 socket 收发缓冲 数 KB–数十 KB/连接 与 rmem/wmem 有关,可调小以省内存
应用每连接对象 实测约 5 KB/侧(空连接) 语言/框架差异大
业务缓冲(未确认消息) 主要项 B08 §6 的例子:10 万 × 100 条 × 200 B ≈ 2 GB

估算示例(10 万连接):

项 计算 结果
应用对象 100,000 × 5 KB 500 MB
业务缓冲 100,000 × 20 条 × 200 B 400 MB
内核缓冲 100,000 × 4 KB(调小后) 400 MB
合计 ≈ 1.3 GB

结论:10 万连接不是靠"大内存"堆出来的,是靠"把每连接成本压到几 KB"设计出来的。

3.3 约束三:端口(仅主动连接方)

项 数值
本地临时端口范围 net.ipv4.ip_local_port_range(默认 32768–60999,约 2.8 万)
每个出向连接占 1 个端口 短连接且不复用 → 快速耗尽
缓解 连接池/keepalive(根治);tcp_tw_reuse(仅出向)

服务端监听连接不消耗临时端口(同一个监听端口承载多连接,靠四元组区分,B01 §3)。


4. 心跳与扇出的成本

4.1 心跳 QPS

心跳 QPS = 连接数 / 心跳间隔
连接数 间隔 30 s 间隔 10 s
1 万 333 QPS 1,000 QPS
10 万 3,333 QPS 10,000 QPS
100 万 33,333 QPS 100,000 QPS

这条算式决定心跳间隔:B08 §3.1 说"间隔 < LB 空闲超时/2",这里补上"还要受自身 QPS 能力约束"。

4.2 扇出(fan-out)

模型 一条消息的处理量 适用
点对点 1 次写 私聊
小群(≤100) 100 次写 群聊
广播(10 万) 10 万次写 → 必须异步/批量 公告、行情

判据:单条消息的写次数 × 消息速率 就是服务端吞吐需求。

广播 1 万/秒 × 10 万订阅者 = 10 亿次写/秒   ← 不可行

解法:不是"逐连接写",而是分层扇出——应用 → 网关层(按连接分片)→ 连接层;或用消息队列做背压(E-S02-025)。


5. 水平扩展:三个必须解决的问题

问题 现象 方案
连接归属 用户 A 连在节点 1,消息却在节点 3 产生 路由表(userId → nodeId)或用发布订阅广播
广播风暴 每节点都收到全量消息 按 topic 分片订阅
重连均衡 节点重启后连接全压到剩余节点 抖动 + 连接数上限 + 灰度重启

5.1 典型拓扑

        ┌────────────┐
客户端 →│ 接入层 LB  │→ 连接节点(无状态化:会话存 Redis)
        └────────────┘        │
                              ▼
                        消息总线(Kafka/Redis PubSub)
                              │
                     业务服务(产生消息)

关键设计:连接节点尽量无状态(会话/订阅关系放外部存储),否则扩容要先迁移连接。


6. 观测项(上线必看)

指标 采集 阈值
当前连接数 应用计数器 接近容量 70% 告警
连接建立速率 每秒 accept 突增 → 重连风暴
心跳超时率 判死连接数/分钟 突增 → 网络或客户端异常
每连接内存 RSS / 连接数 上升 → 泄漏或缓冲未限
消息积压 发送队列长度 持续增长 → 下游慢
TIME_WAIT 数 ss -tan state time-wait | wc -l 很高 → 主动关闭过多

7. 五个误区

误区 真相
"连接数上限就看 ulimit" 内存与业务缓冲往往是先到的墙
"一条消息广播出去就行" 扇出成本 = 订阅者数 × 消息速率
"连接节点可以有状态,反正有会话" 有状态会让扩容/重启变复杂
"心跳越短越健康" 心跳 QPS 与连接数成正比
"TIME_WAIT 无所谓" 服务端主动踢连接时会堆积(实测 2000 连接 → 2006 TIME_WAIT)

8. 自检

  1. 单机上限的三个约束是什么?哪个最先到?
  2. 10 万连接需要多少内存?给出你的估算式与假设。
  3. 广播 1 万/秒给 10 万订阅者,为什么"逐连接写"不可行?解法是什么?
  4. 为什么连接节点要尽量无状态?
  5. 心跳间隔 30 s、10 万连接,心跳 QPS 是多少?服务器扛得住吗?

9. 下一篇

S02-A01 · 实时通信架构选型

工程细节讲完,进入架构决策:自建 WebSocket、买推送服务、还是"消息队列 + 客户端拉取"?下一篇给三方案对比、容量估算与迁移回滚。


本篇属于系列《S02 · 计算机与网络基础》。扩展点 E-S02-024/025。2000 连接实测(1.9 s 建立、9.9 KB/连接、2006 TIME_WAIT)为 2026-11-05。

S02-J03 · 实时通信工程:连接数、心跳、消息可靠性与水平扩展
https://www.clxhxhhr.top/posts/5194/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。