高并发弹幕系统架构设计

高并发弹幕系统架构设计

Tags
WebSocket
IM设计
集群
读写分离
隔离设计
分层设计
锁设计
CPU调度
订阅发布模式
池化设计
DOM算法
CreatedTime
Aug 24, 2022 05:03 AM
Slug
2021-07-12-danmu
UpdatedTime
Last updated August 24, 2022

技术复杂度

对于一个直播间(头部主播):
  • 在线人数100w
  • 弹幕频率 1000条/秒
那么服务端推送频率就是:100w * 1000条/秒 = 10亿条/秒
如果是N个直播间,那么推送频率是:10N亿条/秒

方案对比:拉 vs 推

(客户端)拉模式
notion image
(服务端)推模式:基于websocket协议,可以看Web端通信-websocket协议(已整理)了解ws协议。
notion image

语言技术选型

NodeJS:
  • 单线程模型。需要遍历非常多的用户集合,性能有限,不适合做推送。
C/C++:
  • tcp通讯、ws协议实现成本高,需要diy
Go:
  • 多线程,推送性能高
  • 基于协程模型,能实现高并发
  • websocket是标准库,无需要其他社区库

技术难点和解决方案

内核瓶颈与优化

notion image
优化方案:
  • 服务端将一秒内的n条消息,合成一条消息推送
    • 实现思路:弄个消息队列,每隔1秒来扫描它,然后将消息队列中积累的数据依次发送给客户端
  • 合并后的效果,每秒的推送次数只等于在线连接数

锁瓶颈与优化

notion image
优化方案(GoLang下,可以利用多线程模型提高遍历性能):
  • 大锁拆小锁,不上全局锁:将用户连接打散到多个用户集合中,每个集合有自己的锁
  • 多线程并发推送多个用户集合,提高遍历性能,避免锁竞争
  • 读写锁取代互斥锁:多个推送任务都可以同时获取某个用户集合的读锁,然后遍历它们进行推送

CPU瓶颈

notion image
优化方案:
  • json编码前置:不用每次推送都推送json格式,在推送前,将json进行编码,之后的推送用编码后的数据,避免百万次重复编码
  • 消息合并前置:N条消息合并成一条消息后,大包数据只需要编码一次

分布式架构

单机架构图:
notion image
单机瓶颈:
notion image
 
分布式架构图(这图画的是真丑):
notion image
gateway网关集群:就是前面实现的服务,他们负责将ws数据推到对应的连接上。
logic逻辑集群:用来接收自家客户端业务发来的ws数据,然后将他们广播给网关集群。
业务方和连接:业务方连接连的是gateway服务,用来接收数据;但是发送ws数据走的logic服务。
总体思想:接收消息和推送消息分为2个集群,不放在一个服务中。

弹幕系统前端设计

核心点

  • 使用等待队列来存储弹幕消息,定时消费
  • 使用Pool池化技术来避免多余dom节点浪费
  • 对于过多的消息,直接丢弃

代码

interface IDomContainer { dom: HTMLElement // DOM 结构 text: string // 弹幕信息 } class DM { private waitQueue: string[]; private queueSize: number; private interval: number; private domPool: IDomContainer[]; // domPool private poolSize: number; constructor() { this.waitQueue = []; this.queueSize = 10000; // 防止内存被撑爆 this.interval = 100; } add(text: string) { if (this.waitQueue.length >= this.queueSize) { return; } this.waitQueue.push(text); } time() { setInterval(() => { if (!this.waitQueue.length) { return; } const consumeList = this.waitQueue.splice(0, 10); consumeList.forEach(item => this.render(item)); }, this.interval); } render(text: string) { if (this.domPool.length >= this.poolSize) { return; } if (!this.domPool.length) { const c = { dom: document.createElement('div'), text, }; this.domPool.push(c); } const c = this.domPool.pop() as IDomContainer; this.drawDanMuEle(c); this.recover(c); } drawDanMuEle(container: IDomContainer) { // 向dom节点,绘制弹幕内容(text) const { dom, text } = container; // 省略其他处理,比如dom节点的屏幕动态 } recover(container: IDomContainer) { container.text = ''; this.domPool.push(container); } }

参考