适用读者:要把长连接系统做到"十万级"并对容量负责的人。
前置: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. 自检
- 单机上限的三个约束是什么?哪个最先到?
- 10 万连接需要多少内存?给出你的估算式与假设。
- 广播 1 万/秒给 10 万订阅者,为什么"逐连接写"不可行?解法是什么?
- 为什么连接节点要尽量无状态?
- 心跳间隔 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。