场景七 · 直播弹幕 / IM 长连接
高并发场景 · Scenario 07

直播弹幕 / IM 长连接

前面的场景都是"客人来了就走"(一次请求一次响应),这里的客人却进门就不走了——千万人同时挂着连接等你推消息。一条弹幕发出去要复制百万份,这就是"扇出"的恐怖之处:连接数吃内存,扇出量吃带宽

海量长连接消息扇出C10M 方向推拉结合
千万级单平台同时在线长连接
100万+/秒头部直播间弹幕扇出峰值
<1秒弹幕端到端延迟要求
100万单机连接数天花板(C10M方向)
📑 本页目录
01

场景描述与业务特征

直播弹幕、IM 聊天、在线协作文档的光标同步、股票行情推送……它们的共同点是:服务器需要主动把消息推给客户端,而不是等客户端来问。这就必须维持一条"随时能说话"的长连接(通常是 WebSocket 或自研 TCP 协议)。

类比:普通 HTTP 请求像打客服电话——问完就挂,下次再拨;长连接像拉了个不解散的微信群——你不说话也占着一个席位,而且群主(服务器)随时可能 @全体成员。千万人的"群",光是维持席位就是巨大开销,一次 @全体 更是灾难级流量。

🔌 连接即成本

每条空闲连接都占用内存(几KB~几十KB)+ 文件描述符 + 心跳流量。1000 万连接哪怕什么都不干,也是一笔巨大的固定开销。

📢 写扩散爆炸

一条弹幕 × 100 万观众 = 100 万次推送。消息量不是按"发送数"算,而是按"发送数 × 房间人数"算——乘法一开,什么都爆炸。

🎭 容忍度分级

弹幕丢几条没人发现,但私信/礼物消息一条都不能丢。同一条连接上跑着"可丢"和"不可丢"两种货,得分开对待。

02

整体运作流程

核心思路是把系统拆成三层:接入层(只管养连接的"前台")、逻辑层(管业务的"办公室")、路由层(记住"谁在哪台机器上"的"总机")。发一条弹幕的旅程如下:

📱 观众 发弹幕 / 收弹幕 🔌 接入网关 A 持有 100万 长连接 🔌 接入网关 B…N 水平扩容,只养连接 🧠 逻辑层 鉴权/风控/敏感词 无状态,随便扩 📮 消息总线 按房间号分区 广播给各网关 📇 会话路由 uid → 在哪台网关 ① 上行发送 ② 审核 ③ 入总线 ④ 广播到持有该房间连接的网关 ⑤ 扇出下发
弹幕链路:上行只有 1 条,下行被网关层扇出成百万条——所以"养连接"和"算业务"必须分家

关键设计:接入网关只做一件事——养连接。它不懂业务,只负责收发字节和心跳,所以单机能扛几十万连接,且业务发版不用断开用户连接。逻辑层无状态随便重启,扇出压力则通过"消息总线广播 → 各网关本地循环推送"分摊——1 条消息进总线,N 台网关各自推给自己名下的观众

03

核心瓶颈与数据一致性挑战

🧠 瓶颈一:连接数吃内存

每条 TCP 连接内核缓冲区 + 应用层会话对象,轻松吃掉 20~50KB。100 万连接 ≈ 几十 GB 内存,还没算 TLS 握手的 CPU。传统"一连接一线程"模型在 1 万连接就跪了,必须用 epoll/IO 多路复用。

📡 瓶颈二:扇出量吃带宽

百万人房间,每秒 1000 条弹幕 × 100 字节 × 100 万人 = 100GB/s 下行带宽——物理上不可能全推。扇出不是"能不能优化"的问题,而是"必须砍掉多少"的问题。

💓 瓶颈三:心跳风暴

为了知道连接还活着,客户端定期发心跳。1000 万连接 × 30 秒心跳 = 33 万 QPS 的"空转"流量。心跳间隔还不能太长——运营商 NAT 会悄悄掐掉几分钟不说话的连接。

🔀 瓶颈四:消息有序与不丢

私信要求"不丢、不重、有序",但消息跨网关、跨总线传输,天然乱序。需要会话级序号(seq)+ 客户端拉补:推送只是"提醒",客户端发现 seq 跳号就主动拉齐——推拉结合又出现了。

🚨
事故现场还原:某顶流明星官宣,直播间瞬间涌入 500 万人。接入网关内存告警 → 运维重启了一台网关 → 该网关上 80 万连接同时断开、同时重连 → 重连请求打爆了鉴权服务 → 鉴权挂了导致其他网关的重连也失败 → 全平台雪崩。这就是长连接系统最经典的"重连风暴":连接断开不可怕,可怕的是它们约好了一起回来。正确姿势:重连必须带随机退避(1~30秒随机延迟),网关下线要"优雅摘流"——先停新连接,存量连接慢慢迁移。
04

优化手段、获益与代价

🔌 手段一:接入层与逻辑层分离 架构分层

网关只养连接不懂业务(收字节、发字节、心跳保活),业务全部放到无状态逻辑层。网关用 epoll + 多 Reactor 模型,单机稳稳扛住几十万连接。

✅ 获益:业务天天发版,连接一年不断;网关和逻辑层可以按各自瓶颈独立扩容(网关缺内存,逻辑层缺 CPU)。
⚠️ 代价:多了一跳网络转发和一套"会话路由"服务要维护;网关协议一旦定了就很难改,得预留好版本字段。

📦 手段二:消息合并批推(攒一批再发) 扇出削峰

弹幕不逐条推送,而是网关侧每 100ms 攒一批,打包压缩成一个包推下去。100 条弹幕从 100 次系统调用变成 1 次。

✅ 获益:推送次数降 2 个数量级,包头开销和系统调用大幅减少;配合压缩,带宽再省 60%+。
⚠️ 代价:弹幕延迟增加最多 100ms(观众无感知,但行情推送类场景不能这么玩);批量包丢了就是丢一批。

✂️ 手段三:弹幕抽样降级(超过阈值就"限流不限看") 降级策略

人眼每秒最多看清 10~20 条弹幕。百万人房间每秒几千条弹幕,全推是浪费——服务端按房间热度抽样,只推一部分"代表弹幕",自己发的那条永远给自己回显。

✅ 获益:扇出量与房间人数解耦(再热的房间每人每秒也只收 20 条),带宽成本从"不可能"变成"可控"。
⚠️ 代价:你发的弹幕别人可能看不到(但你自己看得到,体验上无感);抽样策略要防"只推大哥的弹幕"引发公平性争议。

🔢 手段四:seq 序号 + 推拉结合(重要消息不丢) 可靠投递

私信/礼物走"信箱模型":先落库并分配递增 seq,推送只是"有新信了"的门铃。客户端对比本地 seq,发现跳号就主动拉取补齐。

✅ 获益:推送丢了也没关系(下次心跳或拉取会补上),实现"不丢不重有序";断线重连后一次拉齐,体验丝滑。
⚠️ 代价:每个会话要维护 seq 状态和信箱存储,成本比"推完就忘"的弹幕高一个量级——所以只给重要消息用。

💓 手段五:智能心跳 + 重连退避 连接治理

心跳间隔自适应探测(从 30 秒逐步拉长,摸到当前网络 NAT 超时的边界);断线重连带随机退避 + 网关摘流时下发"迁移指令"让客户端分批换机。

✅ 获益:心跳流量省 50%+(移动端更省电);彻底避免"重连风暴"这个长连接系统的头号杀手。
⚠️ 代价:自适应探测逻辑复杂,不同运营商 NAT 行为千奇百怪,需要大量线上数据调参。
🧭
不可能三角站位:本场景把宝押在性能(扇出吞吐)和可用性上,对弹幕类消息大方地放弃一致性(抽样丢弃、乱序无所谓);但对私信/礼物这类"要紧话",用 seq + 信箱换回最终一致。同一条连接上跑两套投递语义,是这个场景最独特的设计智慧。降级路线:带宽吃紧 → 加大抽样比例 → 关闭弹幕特效/头像 → 只保留礼物和系统消息 → 最后连在线人数都改成"10万+"模糊显示。
05

多种解决方案对比与选型建议

推送通道方案实时性连接成本开发/运维成本适用场景
短轮询
客户端定时来问"有新消息吗"
差 · 秒级起步零(无长连接)极低消息极少的场景(订单状态页),或长连接的兜底降级
长轮询
来问了先不答,有消息才响应
中 · 亚秒级中(挂起请求也占资源)兼容性要求极高的老旧环境、Web 端兜底
SSE
HTTP 单向推送流
低 · 就是 HTTP只需下行的场景:行情推送、AI 流式输出、新闻直播
WebSocket
标准双向长连接
好 · 毫秒级中 · 需网关体系绝大多数弹幕/IM/协作场景的默认选择
自研 TCP/QUIC 协议
私有二进制协议
最好 · 可控到底最低(协议头几字节)极高 · 全家桶自己造亿级 DAU 的头部 IM(微信/QQ 级别),弱网优化到极致
你的规模推荐套餐说明
万级在线开源方案直接上:Socket.IO / Centrifugo,单集群搞定别自研,重点把 seq 补拉和重连退避做对
百万级在线WebSocket 自建网关三层架构 + Kafka 广播 + 弹幕抽样本页正文的标准形态,接入层逻辑层必须分家
千万级在线自研协议 + 单机百万连接调优 + 边缘节点就近接入内核参数、内存池、零拷贝全都要抠,团队得有网络专家
🎯
选型口诀:"只下行用 SSE,双向默认 WebSocket,亿级才配自研协议。"再记住两条保命原则:① 消息分级——弹幕可丢可抽样,私信必须 seq 信箱保序;② 永远为"重连风暴"做预案——随机退避 + 优雅摘流,缺一个都可能全平台雪崩。
06

推演检验:换个条件还成立吗?

背下架构图不算学会,能判断"条件变了方案还成不成立"才算。先自己推,再展开参考。

推演 1:如果业务从"一个 100 万人大直播间"变成"1 万个 100 人小群"(总人数相同),扇出架构的压力一样吗?

💡 参考推演

总扇出量相同,但压力形态完全变了:大直播间是单点热流量,靠"房间维度批推合并 + 抽样降级"扛住;万个小群里批推合并几乎失效(每群消息频率低),瓶颈转移到会话路由表——每条消息都要查"这个群的成员在哪些网关上"。同一套组件,热点分布一变,优化重心就从"带宽"变成"路由查询"——架构评估不能只看总量,要看分布。

推演 2:如果产品要求"打赏消息绝不能丢",而弹幕高峰期正在抽样降级,两者冲突吗?

💡 参考推演

不冲突,前提是做了消息分级:普通弹幕走"可丢通道"(抽样、合并、降级都允许),打赏/礼物走"可靠通道"(seq 编号 + ack 确认 + 断线补拉)。同一条 WebSocket 长连接上,不同消息可以有不同 SLA——把"一条链路一个标准"拆成"一类消息一个标准",是长连接系统最重要的设计思想。否则要么为弹幕过度建设,要么把打赏也抽样丢了。

推演 3:把心跳间隔从 30 秒改成 5 秒,断线发现更快了,这是纯赚吗?

💡 参考推演

不是。代价至少三重:心跳包总量 ×6,百万连接下网关 CPU 和带宽显著上升;移动端每 5 秒唤醒一次网络,耗电量飙升(系统会惩罚频繁唤醒的 App);弱网下 5 秒超时误判暴增,反而制造更多不必要的重连。正确答案是智能心跳:WiFi 稳定时拉长到几分钟,弱网时才缩短——检测速度和资源消耗是一对永恒的博弈,固定值永远是次优解。