直播弹幕 / IM 长连接
前面的场景都是"客人来了就走"(一次请求一次响应),这里的客人却进门就不走了——千万人同时挂着连接等你推消息。一条弹幕发出去要复制百万份,这就是"扇出"的恐怖之处:连接数吃内存,扇出量吃带宽。
场景描述与业务特征
直播弹幕、IM 聊天、在线协作文档的光标同步、股票行情推送……它们的共同点是:服务器需要主动把消息推给客户端,而不是等客户端来问。这就必须维持一条"随时能说话"的长连接(通常是 WebSocket 或自研 TCP 协议)。
类比:普通 HTTP 请求像打客服电话——问完就挂,下次再拨;长连接像拉了个不解散的微信群——你不说话也占着一个席位,而且群主(服务器)随时可能 @全体成员。千万人的"群",光是维持席位就是巨大开销,一次 @全体 更是灾难级流量。
🔌 连接即成本
每条空闲连接都占用内存(几KB~几十KB)+ 文件描述符 + 心跳流量。1000 万连接哪怕什么都不干,也是一笔巨大的固定开销。
📢 写扩散爆炸
一条弹幕 × 100 万观众 = 100 万次推送。消息量不是按"发送数"算,而是按"发送数 × 房间人数"算——乘法一开,什么都爆炸。
🎭 容忍度分级
弹幕丢几条没人发现,但私信/礼物消息一条都不能丢。同一条连接上跑着"可丢"和"不可丢"两种货,得分开对待。
整体运作流程
核心思路是把系统拆成三层:接入层(只管养连接的"前台")、逻辑层(管业务的"办公室")、路由层(记住"谁在哪台机器上"的"总机")。发一条弹幕的旅程如下:
关键设计:接入网关只做一件事——养连接。它不懂业务,只负责收发字节和心跳,所以单机能扛几十万连接,且业务发版不用断开用户连接。逻辑层无状态随便重启,扇出压力则通过"消息总线广播 → 各网关本地循环推送"分摊——1 条消息进总线,N 台网关各自推给自己名下的观众。
核心瓶颈与数据一致性挑战
🧠 瓶颈一:连接数吃内存
每条 TCP 连接内核缓冲区 + 应用层会话对象,轻松吃掉 20~50KB。100 万连接 ≈ 几十 GB 内存,还没算 TLS 握手的 CPU。传统"一连接一线程"模型在 1 万连接就跪了,必须用 epoll/IO 多路复用。
📡 瓶颈二:扇出量吃带宽
百万人房间,每秒 1000 条弹幕 × 100 字节 × 100 万人 = 100GB/s 下行带宽——物理上不可能全推。扇出不是"能不能优化"的问题,而是"必须砍掉多少"的问题。
💓 瓶颈三:心跳风暴
为了知道连接还活着,客户端定期发心跳。1000 万连接 × 30 秒心跳 = 33 万 QPS 的"空转"流量。心跳间隔还不能太长——运营商 NAT 会悄悄掐掉几分钟不说话的连接。
🔀 瓶颈四:消息有序与不丢
私信要求"不丢、不重、有序",但消息跨网关、跨总线传输,天然乱序。需要会话级序号(seq)+ 客户端拉补:推送只是"提醒",客户端发现 seq 跳号就主动拉齐——推拉结合又出现了。
优化手段、获益与代价
🔌 手段一:接入层与逻辑层分离 架构分层
网关只养连接不懂业务(收字节、发字节、心跳保活),业务全部放到无状态逻辑层。网关用 epoll + 多 Reactor 模型,单机稳稳扛住几十万连接。
📦 手段二:消息合并批推(攒一批再发) 扇出削峰
弹幕不逐条推送,而是网关侧每 100ms 攒一批,打包压缩成一个包推下去。100 条弹幕从 100 次系统调用变成 1 次。
✂️ 手段三:弹幕抽样降级(超过阈值就"限流不限看") 降级策略
人眼每秒最多看清 10~20 条弹幕。百万人房间每秒几千条弹幕,全推是浪费——服务端按房间热度抽样,只推一部分"代表弹幕",自己发的那条永远给自己回显。
🔢 手段四:seq 序号 + 推拉结合(重要消息不丢) 可靠投递
私信/礼物走"信箱模型":先落库并分配递增 seq,推送只是"有新信了"的门铃。客户端对比本地 seq,发现跳号就主动拉取补齐。
💓 手段五:智能心跳 + 重连退避 连接治理
心跳间隔自适应探测(从 30 秒逐步拉长,摸到当前网络 NAT 超时的边界);断线重连带随机退避 + 网关摘流时下发"迁移指令"让客户端分批换机。
多种解决方案对比与选型建议
| 推送通道方案 | 实时性 | 连接成本 | 开发/运维成本 | 适用场景 |
|---|---|---|---|---|
| 短轮询 客户端定时来问"有新消息吗" | 差 · 秒级起步 | 零(无长连接) | 极低 | 消息极少的场景(订单状态页),或长连接的兜底降级 |
| 长轮询 来问了先不答,有消息才响应 | 中 · 亚秒级 | 中(挂起请求也占资源) | 低 | 兼容性要求极高的老旧环境、Web 端兜底 |
| SSE HTTP 单向推送流 | 好 | 中 | 低 · 就是 HTTP | 只需下行的场景:行情推送、AI 流式输出、新闻直播 |
| WebSocket 标准双向长连接 | 好 · 毫秒级 | 中 | 中 · 需网关体系 | 绝大多数弹幕/IM/协作场景的默认选择 |
| 自研 TCP/QUIC 协议 私有二进制协议 | 最好 · 可控到底 | 最低(协议头几字节) | 极高 · 全家桶自己造 | 亿级 DAU 的头部 IM(微信/QQ 级别),弱网优化到极致 |
| 你的规模 | 推荐套餐 | 说明 |
|---|---|---|
| 万级在线 | 开源方案直接上:Socket.IO / Centrifugo,单集群搞定 | 别自研,重点把 seq 补拉和重连退避做对 |
| 百万级在线 | WebSocket 自建网关三层架构 + Kafka 广播 + 弹幕抽样 | 本页正文的标准形态,接入层逻辑层必须分家 |
| 千万级在线 | 自研协议 + 单机百万连接调优 + 边缘节点就近接入 | 内核参数、内存池、零拷贝全都要抠,团队得有网络专家 |
推演检验:换个条件还成立吗?
背下架构图不算学会,能判断"条件变了方案还成不成立"才算。先自己推,再展开参考。
推演 1:如果业务从"一个 100 万人大直播间"变成"1 万个 100 人小群"(总人数相同),扇出架构的压力一样吗?
💡 参考推演
总扇出量相同,但压力形态完全变了:大直播间是单点热流量,靠"房间维度批推合并 + 抽样降级"扛住;万个小群里批推合并几乎失效(每群消息频率低),瓶颈转移到会话路由表——每条消息都要查"这个群的成员在哪些网关上"。同一套组件,热点分布一变,优化重心就从"带宽"变成"路由查询"——架构评估不能只看总量,要看分布。
推演 2:如果产品要求"打赏消息绝不能丢",而弹幕高峰期正在抽样降级,两者冲突吗?
💡 参考推演
不冲突,前提是做了消息分级:普通弹幕走"可丢通道"(抽样、合并、降级都允许),打赏/礼物走"可靠通道"(seq 编号 + ack 确认 + 断线补拉)。同一条 WebSocket 长连接上,不同消息可以有不同 SLA——把"一条链路一个标准"拆成"一类消息一个标准",是长连接系统最重要的设计思想。否则要么为弹幕过度建设,要么把打赏也抽样丢了。
推演 3:把心跳间隔从 30 秒改成 5 秒,断线发现更快了,这是纯赚吗?
💡 参考推演
不是。代价至少三重:心跳包总量 ×6,百万连接下网关 CPU 和带宽显著上升;移动端每 5 秒唤醒一次网络,耗电量飙升(系统会惩罚频繁唤醒的 App);弱网下 5 秒超时误判暴增,反而制造更多不必要的重连。正确答案是智能心跳:WiFi 稳定时拉长到几分钟,弱网时才缩短——检测速度和资源消耗是一对永恒的博弈,固定值永远是次优解。