HTTP 请求断就断了,重发一次没事。
但 WebSocket / WebRTC / 长轮ling 不一样:
- 一个连接挂几小时,断线=会话丢
- 跨境 RTT 高,心跳设计错,NAT 设备直接把连接掐了
- 边缘节点重启,用户瞬间掉线,重连风暴打爆源站
- 多区域部署,同房间消息顺序错乱、重复投递
很多团队把香港机当“转发入口”,其实它更适合当:
长连接会话的收敛与状态同步中枢。
用户连边缘,边缘连香港;
香港管连接路由、会话状态、跨区消息总线、优雅摘流。
让“断线”变成用户无感的重协商,而不是全量重连。
本文给一套生产向实时长连接架构,偏 IM/推送/协同/行情/出海 SaaS。
一、先定性:长连接不是“HTTP 加个 Upgrade”
无状态服务:LB 轮询、节点挂了换一个。
有状态长连接:连接本身是唯一资源。
核心矛盾:
text
客户端 ——跨洋长连接—— 边缘 边缘重启 / 路由切换 / 光纤抖
只要“连接”断,客户端状态、房间订阅、游标、ACK 全没。
所以架构目标不是“不断”,是:
断得让用户无感:秒级重协商、状态不丢、消息不重不漏。
二、整体架构:香港当“会话中枢”不扛裸流量
text
[客户端](大陆/东南亚/日韩/欧美) │ WebSocket / WebTransport / QUIC ▼ 区域边缘(港/新/日/美西) ├─ 终结 TCP/TLS,保持本地连接 ├─ 本地订阅表(room -> 边缘连接集合) ├─ 断线检测 + 优雅 draining ▼ 内部控制/数据双通道(mTLS) 香港会话中枢(轻量集群) ├─ 全局连接路由表(uid -> 当前边缘节点) ├─ 跨区消息总线(Redis Cluster Pub/Sub / NATS JetStream) ├─ 会话状态存储(Redis:room/ack/游标/灰度标签) ├─ 重连调度(告诉客户端“去连哪个边缘”) ▼ 业务源站(只处理逻辑,不维持百万 socket)
原则:
- 边缘只管“连接存活”,不管全局状态
- 香港管“用户在哪、房间状态是什么、消息往哪投”
- 源站不直连客户端,避免重连风暴穿透到业务层
三、连接分层:把“网络连”和“业务会话”拆开
1. 两层 ID
text
transport_id : 边缘节点本地的 socket 标识(断线即废) session_id : 用户业务会话(重连不变,带 token 恢复)
客户端重连时带:
json
{ "session_id":"s_1024", "last_ack":88001, "region":"hk" }
香港中枢校验后:
- 恢复房间订阅
- 补发 last_ack 之后的消息
- 新 transport 绑定到老 session
用户感知:掉线 800ms,回来还在原房间,消息没重复。
2. 心跳与 NAT 穿透
跨境长连接最怕中间设备超时:
- 大陆→香港:NAT 表 30–60s 回收
- 移动网络:30s 无包就断
策略:
text
应用层心跳:25s 一次(带 jitter) TCP keepalive:开,但只作兜底 WebSocket ping/pong:服务端主动发 移动端后台:用 APNs/FCM 透传唤醒,不靠长连死撑
内核侧:
bash
net.ipv4.tcp_keepalive_time = 60 net.ipv4.tcp_keepalive_intvl = 10 net.ipv4.tcp_keepalive_probes = 3
四、跨区消息总线:同房间不跨洋广播
错误做法:
- 每个边缘都回源查房间成员 → 跨洋 RT 高、源站崩
- 消息写 MySQL 再拉 → 延迟炸
正确做法:边缘本地有订阅表,香港总线做跨区转发。
text
用户A 在 港边缘 用户B 在 美西边缘 同房间 chat:888
流程:
- A 发消息 → 港边缘写本地 ACK
- 港边缘 publish 到香港总线
room:888 - 香港总线按路由表投:美西边缘有订阅 → 推过去
- 美西边缘本地投递给 B,B ACK 回总线
- 总线记录全局 max_ack,去重
Redis 实现骨架:
bash
# 发布 XADD room:888:msg * seq 88002 uid A payload "<json>" # 边缘订阅 XREADGROUP edge:usw STREAMS room:888:msg >
要点:
- 用 Stream + 消费组,避免重复投递
- seq 单调自增,客户端做幂等去重
- 离线消息按 last_ack 补发,不全量重推
五、优雅摘流:边缘发布/重启不掉线
1. 摘流三阶段
text
1) 标记 draining:不再接新连接(DNS/Anycast 权重调 0) 2) 存量连接:发 control 帧“请重连到 hk-b” 3) 等客户端自然断开(30s 窗口) 4) 进程退出
客户端逻辑:
js
ws.on('control:redirect', (e)=>{ setTimeout(()=> connect(e.backup_edge), 200 + rand(300)); });
不是“断线重连”,是“被礼貌请走”。
2. Anycast + 权重双保险
- BGP Anycast:光纤断,3s 内路由收敛
- GSLB 权重:发布期把 5% 流量先摘,观察错误率
- 边缘健康探测:应用层心跳,不是只 ping 端口
六、连接质量观测:别只看“在线数”
关键指标:
指标 |
含义 |
建连耗时 p95 |
DNS+TLS+WS 握手全链路 |
断线率/小时 |
真实用户掉线频率 |
重连成功 p95 |
用户无感关键 |
消息端到端延迟 |
发→收 p95/p99 |
重复投递率 |
seq 幂等是否生效 |
边缘 draining 时长 |
发布是否平滑 |
跨区总线积压 |
区域同步是否跟不上 |
香港机跑对照拨测:
bash
# 从多区域探建连质量 for edge in hk sg jp usw; do curl -w "$edge ws_handshake=%{time_appconnect}\n" \ --http1.1 -o /dev/null -s https://$edge.example.com/healthz done
多节点测速/拨测视角可交叉验证:真实用户在不同运营商下建连是否稳定、跨洋段丢包是否影响长连。
七、弱网与移动端:QUIC/WebTransport 兜底
传统 WS over TCP:
- 丢包就阻塞
- 切换网络(WiFi→4G)源端口变,连接必断
升级路径:
text
优先:WebTransport(HTTP/3 + QUIC) 兜底:WebSocket 更老:长轮ing(带 backoff)
QUIC 优势:
- 0-RTT 重连(session 恢复)
- 切换网络不断流
- 多路复用无队头阻塞
香港边缘起 QUIC:
nginx
listen 443 quic; http3 on; add_header Alt-Svc 'h3=":443"; ma=86400';
八、安全与合规
- 长连接鉴权:session_id + 短期 ticket,不塞 JWT 明文
- 消息体字段级加密(敏感聊天/交易)
- 香港机不存业务主数据,只存连接路由+房间索引
- 审计:谁订阅了哪个房间、重连来源 IP 段
- 限流:单 uid 连接数、单房间广播扇出上限
- 防重连风暴:指数退避 + 抖动 + 边缘排队
九、最小落地清单
- [ ] 边缘终结 TLS,本地维护 transport 表
- [ ] 香港中枢存 session 路由 + 房间状态(Redis Cluster)
- [ ] 消息总线用 Stream/PubSub,带 seq 去重
- [ ] 心跳 25s+jitter,TCP keepalive 开
- [ ] 优雅 draining:控制帧重定向,不硬踢
- [ ] Anycast+GSLB 双权重摘流
- [ ] 客户端 session 恢复:last_ack 补发
- [ ] QUIC/WebTransport 兜底,TCP 弱网不炸
- [ ] 指标:建连 p95 / 断线率 / 重连 p95 / 重复投递率
- [ ] 多节点拨测交叉验证区域建连质量
十、什么别用长连接层干
- 把业务状态放边缘内存(重启即丢)
- 心跳 5s 一次(移动网耗电+被 NAT 误杀)
- 消息不幂等(重连重复弹消息)
- 发布直接杀进程(全量重连风暴)
- 源站直连百万 socket(业务线程被 IO 拖死)
- 只监控“在线数”不监控“端到端延迟”
十一、总结
香港服务器在实时架构里最被低估的价值,不是“延迟低”,而是:
让有状态连接,在跨境网络里变得“可迁移”。
HTTP 可以随便换节点,
但 WebSocket 不行——
用户连的是“那一条线”,线断了,会话就不能装作没发生。
所以把香港当中枢:
- 边缘只保连接
- 香港保“用户在哪、房间是什么、消息到哪”
- 断线不是故障,是重协商
- 发布不是杀进程,是礼貌请走
- 消息不靠“再发一次”,靠 seq + ack + 补发
出海 IM、行情推送、协同白板、直播弹幕,
稳不稳不看单节点并发多高,
看凌晨两点光纤抖的那 30 秒:
用户没掉线,
弹幕没重复,
房间没串号,
重连 800ms 回来,
谁都没发现后端刚发过一版。
一台香港小机,
不处理一条业务消息逻辑,
却让百万长连接,
在亚太到欧美之间,
像一根永远没断过的线。