2372 字
约 7 分钟
7
S02-B08 · 实时通信的稳定性:断线、重连、心跳与幂等

适用读者:长连接上线后收到"消息没收到""重复收到""莫名断开"投诉的人。
前置:S02-B07(四种实时通信)。
实测环境:Ubuntu 24.04.1 / Linux 6.8;内核参数为 2026-11-05 实测读取。
覆盖旧文:A-639(WebSocket 断线怎么办,160 点)。
覆盖扩展:E-S02-015(keepalive vs 心跳)、E-S02-016(幂等与去重)。
上一篇:S02-B07 · WebSocket、SSE、长轮询:四种实时通信怎么选
下一篇:S02-B09 · HTTP 与 RPC。


1. 一句话结论

长连接的"稳定性"不是"不断",而是"断了能发现、能重连、能补数据、且不重复处理"。 四件事缺一个,用户就会投诉。

能力 缺了会怎样
能发现断(心跳) 连接已经死了,服务端还在往里写(写进黑洞)
能重连(退避 + 抖动) 一次网络抖动 → 全量客户端同时重连 → 打垮服务端
能补数据(序号/游标) 断线期间的消息永久丢失
能去重(幂等) 重连后重发 → 重复扣款、重复发消息

2. 断线有四种,处理方式不同

类型 表现 谁先发现
网络抖动 短暂丢包/切换网络 通常 TCP 自己恢复(不一定要重连)
对端崩溃 进程死了,没有 FIN 只能靠心跳/keepalive
中间设备超时 NAT/负载均衡清理空闲连接 空闲一段时间后突然断(应用层心跳可避免)
主动关闭 收到 FIN/RST 立刻知道(可以优雅重连)

最危险的是第 2、3 类:没有 FIN,应用层以为连接还在,写出去的数据全进黑洞。


3. TCP keepalive 为什么不够用(实测内核默认值)

$ cat /proc/sys/net/ipv4/tcp_keepalive_time
7200          # 空闲 7200 秒(2 小时)后才发第一个探测
$ cat /proc/sys/net/ipv4/tcp_keepalive_intvl
75            # 之后每 75 秒一次
$ cat /proc/sys/net/ipv4/tcp_keepalive_probes
9             # 9 次无响应才判定断开

算一下:最坏情况下 2 小时 + 9×75 秒 ≈ 2 小时 11 分钟 才判定连接已死。

项 TCP keepalive 应用层心跳
默认探测间隔 7200 s 由你定(常见 20–60 s)
能穿透应用层代理吗 ❌(纯 TCP 层) ✅(走业务协议)
能否携带业务状态 ❌ ✅(可带 load、seq)
对 NAT 保活的帮助 有,但太慢 ✅(NAT 老化通常 5–30 分钟)
可观测性 差(只能看到连接断) 好(能记录 RTT、丢心跳次数)

结论:应用层心跳是必需的,keepalive 只能作为兜底。 而且要把 tcp_keepalive_time 调小(如 60–300 秒),否则崩溃的连接会一直占着资源。

3.1 心跳间隔怎么定(不是拍脑袋)

约束 取值建议
短于 NAT/负载均衡的空闲超时 一般 LB 是 60 s,心跳取 20–30 s
短于"用户可接受的发现延迟" 移动端弱网下 30–60 s 可接受
不要太频繁 每条连接每分钟一次心跳 × 10 万连接 = 1666 QPS,要算进容量

公式:心跳间隔 < min(NAT 空闲超时, LB 空闲超时) / 2,再乘以连接数估算额外 QPS。

判据:心跳不是"越短越稳"——它本身是负载。10 万连接 × 30 s 间隔 = 3,333 次/秒的服务端压力。

3.2 超时判定与重连触发

发送 ping(带时间戳) → 收到 pong
  若连续 2–3 次未收到 → 判定连接死亡 → 主动 close → 重连

为什么是"2–3 次":一次丢包太常见(弱网),一次就断会造成"频繁重连";太多次则发现太慢。3 次 × 30 s = 90 s 是常见的折中。


4. 重连:指数退避 + 抖动(必须)

第 1 次:立即(或 0.5 s)
第 2 次:1 s
第 3 次:2 s
第 4 次:4 s
……上限:30 s

为什么要"抖动"(jitter):如果服务端重启,10 万客户端会在同一秒全部重连——这叫重连风暴,足以把刚恢复的服务再次打挂。

import random
delay = min(cap, base * (2 ** attempt))
delay = delay * (0.5 + random.random() * 0.5)    # 加 50% 抖动
策略 无抖动 有抖动
服务端重启后 1 秒内连接数 几乎全部 均匀分散
恢复时间 可能二次雪崩 平稳恢复

额外两条实践:

  • 重连要带"断点":Last-Event-ID(SSE)或业务游标(自定义协议),否则重连后丢消息。
  • 重连成功后要做"状态对齐":客户端可能需要拉一次全量快照,而不只是接增量。

5. 消息可靠性:不丢、不重、有序(E-S02-016)

5.1 不丢:序号 + 确认 + 补发

服务端 ── seq=101 ──► 客户端
服务端 ── seq=102 ──► (丢了)
客户端 ── ack=101 ──► 服务端   ← 服务端知道 102 没到,重发

服务端必须保留"每连接的发送缓冲"(最近 N 条),才能补发。没有缓冲 = 断了就丢。

5.2 不重:幂等键

方案 说明 适用
客户端生成消息 ID 服务端按 ID 去重(Redis SETNX / 唯一索引) 客户端→服务端
服务端序号 + 客户端去重 客户端记录"已处理到的最大 seq" 服务端→客户端
业务幂等键 订单号、支付流水号 关键交易

判据:凡是"重发"链路上的操作,都必须幂等。 至少一次(at-least-once)+ 去重 = 实际上的恰好一次。

5.3 有序:单连接内天然有序,跨连接不保证

场景 顺序保证
同一条 TCP 连接 ✅ 有序(B05 的字节流)
重连后 ❌ 需序号排序
多设备/多连接 ❌ 需服务端统一排序(或用业务时间戳 + 客户端合并)

6. 一个可落地的参数表

参数 建议值 依据
应用心跳间隔 20–30 s < LB/NAT 空闲超时的一半
心跳超时判定 连续 3 次未响应 容忍单次丢包
服务端 tcp_keepalive_time 60–300 s 兜底清理死连接
重连初始延迟 0.5–1 s 快速恢复
重连退避上限 30 s 避免无限拉长
重连抖动 50% 防重连风暴
服务端发送缓冲 最近 100–1000 条/连接 与内存预算权衡
客户端去重窗口 ≥ 缓冲条数 覆盖补发范围

内存估算示例:10 万连接 × 每条缓冲 100 条 × 每条 200 B ≈ 2 GB。这条算式在 J03(容量)会展开。


7. 排障清单

现象 可能原因 验证
消息"发了但没收到" 连接已死但未发现(无心跳) 服务端写失败日志、连接存活时长
重连后消息重复 缺幂等/去重 查业务日志里的重复 ID
断线后消息丢失 无序号/无缓冲/无 Last-Event-ID 对比 seq 缺口
服务重启后大面积超时 重连无抖动(风暴) 观察重连 QPS 曲线
连接数持续增长不下降 死连接未清理 ss -s 的 estab 数、keepalive 配置

8. 五个误区

误区 真相
"开了 TCP keepalive 就不用应用心跳" 默认 2 小时才探测,且穿不透应用层代理
"心跳越频繁越可靠" 心跳本身是负载,10 万连接下很可观
"重连就是重试" 无退避无抖动 = 重连风暴
"at-least-once 就够了" 必须配去重,否则重复扣款
"断线重连后接着推增量就行" 可能已经丢了中间状态,需要对齐快照

9. 自检

  1. 为什么"对端崩溃"这种断线只能靠心跳发现?
  2. 从内核默认值算出 TCP keepalive 最坏判定时间是多少?
  3. 心跳间隔的取值依据有哪三条约束?
  4. 重连抖动解决的具体问题是什么?
  5. "至少一次 + 去重"为什么等价于"恰好一次"(工程意义上)?

10. 下一篇

S02-B09 · HTTP 与 RPC:为什么 RPC 更快,又为什么仍要 HTTP

长连接与推送讲完,回到"调用"本身:RPC 凭什么比 HTTP 快(协议开销?序列化?连接复用?),以及为什么对外接口仍然普遍用 HTTP。下一篇给可测量的对比维度与选型判据。


本篇属于系列《S02 · 计算机与网络基础》。旧文来源:A-639;扩展点 E-S02-015/016。内核 keepalive 参数为 2026-11-05 实测。

S02-B08 · 实时通信的稳定性:断线、重连、心跳与幂等
https://www.clxhxhhr.top/posts/5189/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。