长链接方案的设计
# 12.长链接方案的设计
本篇定位:长连接是即时通讯、实时推送、在线协作的"血管"——HTTP 短连接撑不起 IM、直播弹幕、股票行情。本文从一个推送系统崩塌的故事讲起,回答三个核心问题——为什么需要长连接?业界怎么实现海量长连接?心跳和断线重连怎么做对?
# 目录介绍
# 01.一次推送的崩塌
# 1.1 千万设备掉线
某 IoT 平台连接着 800 万台智能设备,2023 年春节凌晨 0 点准时给所有设备推送"新年祝福"。0:00:00,推送服务开始批量下发。0:00:03,长连接接入层 CPU 飙到 95%。0:00:10,40% 的设备开始断连重连。0:00:30,接入层雪崩,800 万设备全部掉线。
| 时间点 | 现象 |
|---|---|
| 0:00:00 | 触发新年推送 |
| 0:00:03 | 接入层 CPU 95% |
| 0:00:10 | 设备开始断连重连 |
| 0:00:30 | 接入层全线崩溃 |
| 0:01:00 | 800 万设备同时重连,TCP 握手洪水 |
| 0:15:00 | 紧急扩容 + 限流,逐步恢复 |
| 0:45:00 | 完全恢复,但黄金时段已过 |
# 1.2 故障扩散链路
flowchart TD
A[凌晨 0 点群发推送] --> B[接入层 CPU 飙升]
B --> C[心跳响应延迟]
C --> D[设备判定服务端不可用]
D --> E[设备同时断连]
E --> F[设备同时重连<br/>800万 TCP 握手]
F --> G[接入层连接耗尽]
G --> H[新连接进不来<br/>已连接被踢]
H --> I[全网雪崩]
style E fill:#ffebee
style F fill:#ffebee
style I fill:#ffebee
2
3
4
5
6
7
8
9
10
11
12
13
真正的根因不是推送量大,而是这 5 个细节:
- 没有错峰下发——所有设备同时收到推送
- 重连没有退避——掉线后立刻重连
- 重连没有抖动——所有设备同时重连
- 接入层没有过载保护——超过容量也接收
- 客户端心跳过频——闲时也 30 秒一次心跳
# 1.3 反思长连接设计
事后这个团队总结了三个最深刻的教训:
- 长连接 = 状态长期持有 = 故障传染范围极大
- 心跳和重连必须做"错峰" + "退避",否则一掉就雪崩
- 接入层必须有过载保护,宁可拒绝新连接也不能让已连接挂掉
# 02.要解决的核心矛盾
# 2.1 短连接的瓶颈
HTTP 短连接每次请求都要 TCP 三次握手 + TLS 握手,一次完整握手约 200-500ms。在 IM 场景下:
graph LR
subgraph "❌ 短连接轮询"
A[客户端] -->|每 5 秒发请求| Server[服务端]
Server -->|无新消息也要回| A
end
subgraph "✅ 长连接推送"
B[客户端] <-->|长期持有连接| Server2[服务端]
Server2 -.有消息直接推.-> B
end
style A fill:#ffebee
style B fill:#e8f5e8
2
3
4
5
6
7
8
9
10
11
12
13
短连接的 4 个致命问题:
- 实时性差(必须轮询)
- 服务器压力大(无效请求 90%+)
- 流量浪费(每次都重新握手)
- 不省电(手机轮询耗电严重)
# 2.2 实时性与省电
移动端长连接的天敌是电池。心跳越频繁电池越耗,但太疏远又会被运营商 NAT 网关回收:
| 心跳间隔 | 消息延迟 | 电池影响 | NAT 回收风险 |
|---|---|---|---|
| 30 秒 | 极低 | 严重耗电 | 几乎无 |
| 3 分钟 | 中 | 轻 | 低 |
| 5 分钟 | 中 | 极轻 | 中(运营商常见 5min 回收) |
| 10 分钟 | 较高 | 几乎无 | 高 |
实战折中:自适应心跳——前台 3 分钟、后台 5 分钟、网络稳定时延长、不稳定时缩短。
# 2.3 海量与稳定
单机能承载多少长连接?理论上 65535 个端口够用,但实际受内存和文件句柄限制:
| 指标 | 单机典型值 |
|---|---|
| 文件句柄上限 | 100w(调内核参数) |
| 每连接内存 | 8KB-32KB |
| 100w 连接内存 | 8GB-32GB |
| 单机实际承载 | 50w-100w(业务复杂度决定) |
海量场景必须分布式接入层,单点会有性能瓶颈和单点故障。
# 2.4 长连接的本质
长连接 = 用"状态长期持有"换"实时推送 + 低延迟"
它的核心追求是 让消息"主动到达"客户端,而不是客户端"主动来取"。
# 03.业界主流方案
# 03.1 协议层选型
| 协议 | 定位 | 典型场景 |
|---|---|---|
| TCP 私有协议 | 自定义协议、性能极致 | 大型 IM(微信、钉钉) |
| WebSocket | 浏览器标准、HTTP 升级 | Web IM、实时仪表盘 |
| MQTT | 物联网标准、低带宽 | IoT 设备 |
| gRPC Streaming | RPC 双向流 | 微服务长连接 |
| Server-Sent Events | HTTP 单向推送 | 通知、订单状态 |
| HTTP Long Polling | HTTP 模拟长连接 | 兼容老浏览器 |
| QUIC | 基于 UDP、低延迟 | 弱网 + 移动场景 |
flowchart LR
A[HTTP Long Polling<br/>兼容性最好] --> B[Server-Sent Events<br/>单向推送]
B --> C[WebSocket<br/>浏览器双向]
C --> D[TCP 私有协议<br/>极致性能]
D --> E[QUIC<br/>下一代标准]
A1[1990s] -.- A
B1[2010s] -.- B
C1[2011] -.- C
D1[一直存在] -.- D
E1[2020s] -.- E
style A fill:#e3f2fd
style C fill:#e8f5e8
style D fill:#fff3e0
style E fill:#f3e5f5
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 03.2 横向对比矩阵
| 维度 | TCP 私有 | WebSocket | MQTT | gRPC Stream | QUIC |
|---|---|---|---|---|---|
| 传输层 | TCP | TCP | TCP | TCP/HTTP2 | UDP |
| 协议复杂度 | 自己定 | 中 | 低 | 中 | 高 |
| 浏览器支持 | ❌ | ✅ | 弱 | 弱 | 部分 |
| 跨语言 | 自己实现 | ✅ | ✅ | ✅ | 上升中 |
| 省流量 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 弱网表现 | 取决于实现 | 一般 | 好 | 一般 | 极好 |
| 典型代表 | 微信 mmtls | 大多数 Web IM | 特斯拉车机 | 内部微服务 | YouTube |
# 03.3 典型架构案例
案例:某 IM 应用的长连接架构
graph TB
subgraph "客户端"
Mobile[手机 App]
Web[Web 浏览器]
Desktop[桌面客户端]
end
subgraph "接入层 长连接网关"
LB[四层 LB / DNS]
Gate1[接入网关 1]
Gate2[接入网关 2]
GateN[接入网关 N]
end
subgraph "路由层"
Router[路由服务]
Redis[(Redis 路由表<br/>uid → 网关地址)]
end
subgraph "业务层"
IM[IM 业务服务]
Push[推送服务]
DB[(消息存储)]
end
Mobile -->|TCP 私有| LB
Web -->|WebSocket| LB
Desktop -->|TCP 私有| LB
LB --> Gate1 & Gate2 & GateN
Gate1 & Gate2 & GateN <--> Router
Router <--> Redis
Router --> IM
Router --> Push
IM --> DB
style Gate1 fill:#fff3e0
style Router fill:#e8f5e8
style Redis fill:#f3e5f5
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
核心设计:
- 接入层与业务层解耦:接入层只管连接维护和路由,业务逻辑在后端
- 路由表用 Redis:
uid → 当前所在的接入网关地址 - 业务层主动推送:业务层根据路由表找到目标网关,再由网关推送给客户端
# 04.设计核心原则
# 04.1 心跳保活原则
心跳的两个目标:
- 保活:防止 NAT / 防火墙回收闲置连接
- 探测:发现死连接(半打开状态)
sequenceDiagram
participant C as 客户端
participant S as 服务端
Note over C,S: 正常心跳
C->>S: PING
S-->>C: PONG
Note over C,S: 服务端死了(半打开)
C->>S: 业务请求
Note right of C: 没人响应<br/>但 TCP 不知道
Note over C,S: 心跳暴露问题
C->>S: PING
Note right of C: 等待 N 秒超时
C->>C: 判定连接死了 → 重连
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
自适应心跳算法(参考微信):
class AdaptiveHeartbeat {
private var interval = 4 * 60 * 1000L // 初始 4 分钟
private val MIN = 30 * 1000L // 最小 30 秒
private val MAX = 10 * 60 * 1000L // 最大 10 分钟
fun onSuccess() {
// 心跳成功 → 适度延长
interval = (interval * 1.2).toLong().coerceAtMost(MAX)
}
fun onFail() {
// 心跳失败 → 立刻缩短
interval = (interval * 0.5).toLong().coerceAtLeast(MIN)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 04.2 断线重连原则
铁律:重连必须有退避 + 抖动,不然就是开篇的 800 万雪崩。
graph LR
A[第1次失败] -->|立即重连 0s| B[第2次失败]
B -->|2s + 随机| C[第3次失败]
C -->|4s + 随机| D[第4次失败]
D -->|8s + 随机| E[第5次失败]
E -->|16s + 随机| F[最大 60s + 随机]
style A fill:#fff3e0
2
3
4
5
6
7
8
指数退避 + 随机抖动:
fun nextDelay(retryCount: Int): Long {
val base = minOf(2.0.pow(retryCount).toLong() * 1000, 60_000L)
val jitter = Random.nextLong(0, base / 2) // 加 0-50% 抖动
return base + jitter
}
2
3
4
5
为什么要抖动:所有客户端同时间被踢下线后,如果固定退避,所有客户端会在同一时刻一起重连——又一次雪崩。抖动让重连请求在时间轴上摊开。
# 04.3 消息可靠原则
长连接的消息可能在以下时机丢失:
flowchart TD
A[业务发送消息] --> B{服务端 → 客户端}
B -->|网络抖动| C[消息丢失]
B -->|客户端崩溃| C
B -->|半打开连接| C
Solution[可靠投递三件套] --> S1[消息编号]
Solution --> S2[ACK 确认]
Solution --> S3[超时重传]
style C fill:#ffebee
style Solution fill:#e8f5e8
2
3
4
5
6
7
8
9
10
11
12
经典 ACK 机制:
| 流程 | 动作 |
|---|---|
| 1. 服务端发送 | 带消息 ID msg_001 |
| 2. 客户端收到 | 立即回复 ACK msg_001 |
| 3. 服务端收到 ACK | 标记为已送达 |
| 4. 超时未收到 ACK | 重传 |
| 5. 客户端去重 | 收到重复 ID 直接丢弃 |
# 04.4 横向扩展原则
单机有上限,必须能水平扩展:
graph TB
subgraph "❌ 单接入点"
Client1[100w 客户端] --> Single[单接入服务<br/>承载上限]
Single -.过载崩溃.-> Down[全网瘫痪]
end
subgraph "✅ 接入集群"
Client2[100w 客户端] --> LB2[LB 分流]
LB2 --> G1[网关1<br/>10w]
LB2 --> G2[网关2<br/>10w]
LB2 --> G3[网关N<br/>10w]
end
style Down fill:#ffebee
style G1 fill:#e8f5e8
style G2 fill:#e8f5e8
style G3 fill:#e8f5e8
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
关键能力:
- 接入层无状态(连接信息在 Redis)
- 单台挂了 LB 自动剔除
- 容量不够即时加机器
# 05.方案落地实战
# 05.1 整体架构
flowchart TB
subgraph "客户端 SDK"
SDK[长连接 SDK]
SDK --> HB[心跳模块]
SDK --> RC[重连模块]
SDK --> Q[发送队列]
SDK --> ACK[ACK 管理]
end
subgraph "接入层"
DNS[智能 DNS<br/>就近接入]
Gateway[接入网关集群]
Gateway --> ConnMgr[连接管理]
Gateway --> Codec[协议编解码]
Gateway --> AuthM[鉴权]
end
subgraph "路由层"
Route[路由服务]
RouteCache[(Redis<br/>uid→网关)]
end
subgraph "业务层"
Biz[业务服务]
MsgStore[(消息存储)]
OfflineStore[(离线消息)]
end
SDK -->|建连+鉴权| DNS --> Gateway
Gateway <--> Route
Route <--> RouteCache
Route <--> Biz
Biz <--> MsgStore
Biz <--> OfflineStore
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 05.2 心跳机制设计
完整心跳设计要素:
mindmap
root((心跳设计))
频率自适应
前台 3-5 分钟
后台 5-10 分钟
根据 NAT 探测调整
协议轻量
心跳包 < 10 字节
不带任何业务数据
超时处理
响应超时 N 倍重连
连续 3 次失败重连
错峰策略
初始延迟随机
避免万人同时心跳
2
3
4
5
6
7
8
9
10
11
12
13
14
15
反例:所有客户端固定 60s 心跳 → 接入层 QPS = 客户端数 / 60,100 万客户端 = 1.6w QPS 心跳,浪费严重。
正例:3-5 分钟 + 随机化 → 同样客户端 QPS 仅 3000。
# 05.3 重连退避策略
完整重连流程:
flowchart TD
Start([连接断开]) --> CheckNet{网络可用?}
CheckNet -->|否| WaitNet[监听网络变化]
WaitNet --> CheckNet
CheckNet -->|是| Backoff[计算退避时间]
Backoff --> AddJitter[加随机抖动]
AddJitter --> Wait[等待]
Wait --> Connect[尝试连接]
Connect -->|失败| RetryCount{重试次数++}
RetryCount -->|< 上限| Backoff
RetryCount -->|>= 上限| Reset[等待用户操作或<br/>重置后从头开始]
Connect -->|成功| Success[重置重试计数<br/>同步离线消息]
style Backoff fill:#fff3e0
style AddJitter fill:#fff3e0
style Success fill:#e8f5e8
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 05.4 消息可靠投递
端到端可靠投递:
sequenceDiagram
participant U1 as 发送方
participant G1 as 接入网关A
participant Biz as 业务层
participant Store as 消息存储
participant G2 as 接入网关B
participant U2 as 接收方
U1->>G1: 发送消息 msg_001
G1->>Biz: 转发
Biz->>Store: 持久化
Store-->>Biz: 写入成功
Biz-->>G1: ACK_S
G1-->>U1: 服务端 ACK
Biz->>G2: 推送给在线接收方
G2->>U2: 推送 msg_001
U2-->>G2: 客户端 ACK
G2-->>Biz: 标记已读
Biz->>Store: 更新已读状态
Note over U2: 如果离线
Note over Store: 留在离线消息<br/>下次上线推送
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
两段 ACK 的意义:
- 服务端 ACK:保证消息不丢(已落库)
- 客户端 ACK:保证消息已被收到
# 05.5 海量连接管理
单机优化要点:
| 维度 | 优化项 | 效果 |
|---|---|---|
| OS 内核 | ulimit -n 1000000 文件句柄 | 突破 65535 限制 |
| TCP 参数 | tcp_keepalive_time tcp_max_syn_backlog | 提升握手能力 |
| 网络模型 | epoll(Linux)/ kqueue(BSD) | 单进程百万连接 |
| 内存优化 | 连接对象池化 | 降低 GC 压力 |
| 零拷贝 | sendfile / splice | 减少内存拷贝 |
| 协议精简 | 二进制 + Varint | 流量减半 |
典型技术栈:
- Java 系:Netty(事实标准)
- Go 系:原生 net + goroutine
- C++ 系:libevent / boost.asio
- 专业级:自研(如微信 mars、支付宝 mPaaS)
# 06.关键问题解决
# 06.1 连接路由问题
问题:业务层有消息要推给 user_123,但 user_123 连接在 1000 个接入网关里的哪一个?
sequenceDiagram
participant Client as 客户端
participant Gateway as 接入网关3
participant Redis as Redis 路由表
participant Biz as 业务服务
Note over Client,Gateway: 上线时注册路由
Client->>Gateway: 建连+鉴权
Gateway->>Redis: SET uid:123 → gateway-3
Note over Biz,Client: 业务推送时查路由
Biz->>Redis: GET uid:123
Redis-->>Biz: gateway-3
Biz->>Gateway: 推送消息
Gateway->>Client: 转发到客户端
Note over Client,Redis: 下线时清理路由
Client--xGateway: 断开
Gateway->>Redis: DEL uid:123
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 06.2 多端在线问题
典型 IM 多端场景:用户在手机 + Web + 桌面同时登录。
| 维度 | 多端策略 |
|---|---|
| 路由表 | uid → [gateway-1, gateway-3, gateway-7] 多对多 |
| 消息广播 | 所有在线端都推送 |
| 状态同步 | 一端已读,其他端同步已读 |
| 互斥端类型 | 例如同种手机型号互踢 |
# 06.3 弱网兼容问题
移动端弱网的典型表现:
- 信号切换(4G ↔ Wi-Fi)→ 连接频繁断开
- 高延迟(300ms+)→ 心跳超时误判
- 高丢包(10%+)→ 消息丢失
应对策略:
mindmap
root((弱网应对))
协议层
QUIC 替代 TCP
多路复用 1 条连接传多消息
0-RTT 重连
应用层
消息分片
ACK 重传
去重
心跳层
自适应间隔
失败快速重连
数据层
消息压缩
二进制协议
增量同步
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 07.常见陷阱与反例
# 07.1 心跳过频反例
反例:某 IM 客户端为了"实时性"设了 30 秒心跳。
问题:
- 用户电池续航减少 20%
- 接入层心跳 QPS 占了 60%
- 流量浪费严重
正确:3-5 分钟自适应心跳。只有当心跳失败时才需要更频繁。
# 07.2 雪崩重连反例
反例:开篇的 800 万设备同时重连。
正确:指数退避 + 随机抖动。
# 07.3 内存泄漏反例
反例:接入网关用 Java 写的,每来一个连接 new 一个 ConnectionContext,断连后没及时释放——24 小时后 OOM。
正确:
- 使用对象池
- 断连立即清理路由 + 释放对象
- 监控连接数 vs 对象数(应基本相等)
mindmap
root((三大反例))
心跳过频
电池消耗
流量浪费
接入层压力
雪崩重连
固定退避
没有抖动
重连风暴
内存泄漏
对象未释放
路由未清理
OOM 崩溃
2
3
4
5
6
7
8
9
10
11
12
13
14
# 08.演进路线
# 08.1 V1 单机长连接
特征:业务起步、连接数 < 10w。
做法:
- 单台服务直接用 Netty
- 简单的连接管理
- 心跳 + 重连基础能力
# 08.2 V2 接入层集群
特征:连接数 10w-1000w。
做法:
- 接入网关集群化(多实例)
- 路由表用 Redis
- 业务层与接入层分离
- 完整的心跳 / 重连 / ACK
# 08.3 V3 全球多接入点
特征:千万级 + 全球用户。
做法:
- 全球多机房部署接入网关
- 智能 DNS 就近接入
- 跨机房消息路由
- 协议升级(QUIC / 私有协议)
- 完整的容灾切换
flowchart LR
V1[V1 单机<br/>< 10w] --> V2[V2 集群<br/>10w-1000w]
V2 --> V3[V3 全球多接入<br/>千万级]
style V1 fill:#e3f2fd
style V2 fill:#e8f5e8
style V3 fill:#fff3e0
2
3
4
5
6
7
# 09.总结与决策
# 09.1 上线检查表
新增长连接服务上线前对照:
- [ ] 协议选型完成(TCP/WebSocket/MQTT/QUIC)
- [ ] 心跳间隔合理(推荐 3-5 分钟自适应)
- [ ] 心跳有错峰随机化
- [ ] 重连有指数退避 + 抖动
- [ ] 消息有序号 + ACK + 重传
- [ ] 客户端有去重逻辑
- [ ] 接入层水平扩展能力就绪
- [ ] 路由表设计完成(Redis / etcd)
- [ ] 接入层有过载保护(连接数限流)
- [ ] 单机 OS 参数已优化(文件句柄 / TCP)
- [ ] 监控就绪(连接数、心跳成功率、消息延迟、断连率)
- [ ] 多端在线策略明确
- [ ] 弱网兼容方案就位
- [ ] 容灾切换演练完成
# 09.2 选型决策树
flowchart TD
Start([我需要长连接吗?]) --> Q1{消息实时性要求?}
Q1 -->|秒级以上可接受| Polling[HTTP 轮询<br/>简单够用]
Q1 -->|毫秒级| Q2{客户端类型?}
Q2 -->|浏览器为主| Q3{是否需要双向?}
Q3 -->|是| WS[WebSocket]
Q3 -->|否服务端推| SSE[Server-Sent Events]
Q2 -->|手机原生 App| Q4{资源 / 性能要求?}
Q4 -->|极致| TCP[TCP 私有协议<br/>+ 自研 SDK]
Q4 -->|标准| WS2[WebSocket]
Q2 -->|IoT 设备| MQTT[MQTT]
Q2 -->|微服务间| gRPC[gRPC Streaming]
Q2 -->|弱网为主 移动| QUIC[QUIC]
style Polling fill:#e3f2fd
style WS fill:#e8f5e8
style TCP fill:#fff3e0
style MQTT fill:#f3e5f5
style QUIC fill:#ffebee
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
最后一句话:长连接是分布式系统里"最容易雪崩的组件"——开篇 800 万设备掉线只是因为 5 个细节同时做错。
好的长连接 = 心跳错峰、重连退避、消息可靠、连接可控。