高并发弹幕系统架构设计

技术复杂度

对于一个直播间(头部主播):

  • 在线人数100w
  • 弹幕频率 1000条/秒

那么服务端推送频率就是:100w * 1000条/秒 = 10亿条/秒

如果是N个直播间,那么推送频率是:10N亿条/秒

方案对比:拉 vs 推

(客户端)拉模式

Untitled.png

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

Untitled.png

语言技术选型

NodeJS:

  • 单线程模型。需要遍历非常多的用户集合,性能有限,不适合做推送。

C/C++:

  • tcp通讯、ws协议实现成本高,需要diy

Go:

  • 多线程,推送性能高
  • 基于协程模型,能实现高并发
  • websocket是标准库,无需要其他社区库

技术难点和解决方案

内核瓶颈与优化

Untitled.png

优化方案:

  • 服务端将一秒内的n条消息,合成一条消息推送

    实现思路:弄个消息队列,每隔1秒来扫描它,然后将消息队列中积累的数据依次发送给客户端

  • 合并后的效果,每秒的推送次数只等于在线连接数

锁瓶颈与优化

Untitled.png

优化方案(GoLang下,可以利用多线程模型提高遍历性能):

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

CPU瓶颈

Untitled.png

优化方案:

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

分布式架构

单机架构图:

Untitled.png

单机瓶颈:

Untitled.png

分布式架构图(这图画的是真丑):

Untitled.png

gateway网关集群:就是前面实现的服务,他们负责将ws数据推到对应的连接上。

logic逻辑集群:用来接收自家客户端业务发来的ws数据,然后将他们广播给网关集群。

业务方和连接:业务方连接连的是gateway服务,用来接收数据;但是发送ws数据走的logic服务。

总体思想:接收消息和推送消息分为2个集群,不放在一个服务中。

弹幕系统前端设计

核心点

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

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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);
}
}

参考

bookmark

bookmark

bookmark