面临的问题
初期业务主要的场景是直播间的群聊消息以及一小部分的单聊消息。由于是教育场景,所以业务在划分聊天室的时候是以班级为单位进行划分的,假设每个聊天室的人数为500人。
问题一:用户的维护
直播场景的群聊与微信等常见的群聊在用户维护上有很大区别。微信的群用户关系相对比较固定,用户进群退群是相对低频操作,用户集合相对固定。而直播间里的用户进出是非常频繁的,而且直播间是有时效性的。实际进出直播间峰值QPS
不会超过1万,使用Redis
可以解决聊天室用户列表存储及过期清理问题。
问题二:消息转发
当一个500人的聊天室所有用户同时发送消息时,消息的转发QPS
为500*500=2.5w。从直播用户端视角考虑:
实时性:如果消息服务做消峰处理,峰值消息的堆积会造成消息延时增大,而有些信令消息具有时效性,太大延迟会影响用户的体验及互动实时性。
用户体验:端展示各类用户聊天和信令消息一般一屏不会超过10-20条; 如果每秒超过20条消息下发会出现持续刷屏的现象; 大量的消息也会给端上带来持续的高负荷。
因此我们为消息定义了不同的优先级。高优先级消息优先转发处理并且保证不丢弃; 低优先级消息进行一定丢弃策略后再进行转发。
问题三:历史消息
业务上需要生成回放视频,需要获取历史信令、互动聊天等消息。要求能够快速写入历史消息以保证消息转发的时效性。
消息的保存主要包含写扩散和读扩散两大类。我们采用读扩散的方式,读扩散可以减少存储空间,也可以减少消息保存的时间。考虑到回放的优先级不高,所以在存储组件的选择上我们选择了Pika
。Pika
是接口与Redis
类似可以减少学习、开发成本。同时由于它是采用追加的方式,所以写性能可以与Redis
媲美。
问题四:消息顺序
信令消息顺序的要求,需要保证同一个人发送消息的顺序,以及需要保证同一个聊天室内的用户收到消息顺序都是相同的。
解决消息顺序可以使用Kafka
之类的队列来保证,但是用Kafka
有一定的延迟。为了降低延迟我们采用一致性哈希的策略来处理消息的转发,稍后会详细介绍。
设计目标
打造稳定、高效的消息通讯服务端。
- 提供高可靠、高稳定、高性能的长连接服务;
- 支撑百万长连接同时在线;
- 支持多集群快速部暑,扩容;
稳定性和扩展性
稳定性和扩展性在整体架构是非常重要的部分,消息服务主要的策略是动态配置
和策略下发
,可以有效的实现多机房的动态调度,故障的自动转移,同时也具备了横向扩展的能力。
- 配置:每个端在连接服务端之前,会获取
SDK
的常用配置信息,比如重试策略、超时时间等核心参数和业务的调度地址 - 调度:根据不同的业务类型和用户信息,请求调度地址获取到当前服务的连接地址
- 连接:向目标服务集群发起连接
- 故障转移:如果服务中途发生问题导致断线,会再次发起调度,后端集群会重新计算可用的集群,将用户调度到可用集群节点
- 断线的可能有多种多样,常见的有以下方面:
- 网络不稳定,导致网络丢包,触发心跳超时
- 客户端,服务端发生异常,主动关闭连接
- 在配置和调度接口高可用的层面,会采用多地址轮询,多次重试,
CDN
静态资源兜底等策略来依次保障
大家应该会疑问,为什么不把调度地址在第一步的配置服务中一起返回,要拆分为两步进行?这里其实是考虑消息服务支持多业务的并发,配置服务在初始化SDK
的时候只获取一次,获取 SDK
的核心控制参数。后续不同的业务可以在不同时机动态获取各自的调度地址,可以有效的提升业务的隔离性和稳定性,做到业务之间互不影响。
安全性
在安全性和抗攻击性方面采取了一系列的措施,对消息内容的加密和账户的安全认证,我们目前采用的方式有:
- 为每个
App
业务方下发AppKey
,在调度过程中会校验签名,以免不安全的流量拿到调度地址,或者至少需要突破App
的防御才能拿到 - 调度拿到的接入地址中会有合法的
Token
,服务端Auth
模块会验证Token
的时效性以及合法性,以及用户的账户体系,以免不法分子投机,增加破解成本 - 可动态开启内容加密功能,会获取用户独立的对称加密
AES
密匙,进行内容加密传输 - 数据传输使用
TLS
/SSL
隧道加密 - 利用安全的网络环境,比如
IDC
的机房防护,云厂商的智能防护等
连接的稳定性
在真实复杂的网络环境中,可能有极少部分用户网络稳定性方面比较差,存在丢包,导致频繁断线,或者彻底无法建立连接。我们一方面会收集客户端的网络状态日志,对用户进行诊断;另一方面还针对这最后一公里的问题进行网络加速。
使用云厂商的 TCP
加速,Websocket
加速,解决部分边缘用户的连接问题
自建边缘节点,可以更加精细化的控制
海外网络加速,对接多家云服务厂商
可靠性和一致性
作为消息服务的核心保障,分别会在服务端和客户端的架构中重点介绍,主要保障策略为:
- 消息的严格确认机制,保证客户端发送到服务端的消息,只要确认后,就一定会落盘,从而保障上行消息不丢,之后下行消息可以多次重试
- 客户端失败重传,排序,超时等策略,主要借鉴
TCP
滑动窗口的思想,可以将已确认顺序的消息返回业务层,乱序的消息要根据策略超时等待 - 服务端的防止重复提交和重发策略
- 全局唯一的消息
ID
和局部有序的序列号,来保障唯一性和顺序性
服务端消息ID的设计
消息 ID
的设计对消息的可靠一致性有着非常重要的意义,一方面要保证任何一条消息可追溯查询,保证消息的唯一性;另一方面需要保证消息的顺序性。
消息服务生成唯一 ID
的算法借鉴 Twitter
雪花算法 Snowflake
,做了一些切合服务的微调。
局部有序的序列ID
在有序序列 ID
上,我们采用一些创新的设计,为每个会话,例如一组单聊,一组群聊都会生成独立的序列号。互相没有影响,可以大幅提升序号的发号性能。序号的主要的目的是为了保障消息的局部顺序性。那么服务端在实现可靠性和一致性上做了哪些工作呢?首先我们对消息结构做了重新设计,新的消息结构如下图:
新的消息结构是类似于单向链表,与传统的单向链表的区别在于链表是逆序的,每条消息都有一个前序消息的 SeqId
、本条消息的 SeqId
、全局消息 Id
(消息追踪使用)。消息的 SeqId
都是针对于聊天对象的,例如聊天室 A
和聊天室 B
的消息 SeqId
是相互独立的。
这里采用一个前序 ID
的主要考虑是可以利用单调递增的序列来保障消息的可靠一致性,注意这里并没有要求连续单调递增。比如图中我们的序列号为:101
,102
,104
,107
。我们可以采用一些支持海量高性能的序列号生成服务来支持,采用预先分配号码段的策略来提升性能。有兴趣的话大家可以网上查询一些比较成熟的方案。当然,消息体量不大的基础上可以简单粗暴的使用 Redis
生成连续递增的序号。
消息可靠性一致性保障
消息的可靠性一致性主要体现在以下几个方面:
- 端上收到的消息不丢,不重,不乱序
有了前面介绍的SeqId
,客户端就可以对消息进行排序、空洞判断等逻辑处理,具体流程如下图:
- 同一房间内用户看到的消息顺序一致
- 用户发送的顺序和用户接收到的顺序一致
后面两个特性,服务端主要是通过将同一发送者的消息 Hash
到同一线程中进行处理,同一个房间内的消息,也 Hash
到同一个线程内处理。
客户端如果消息发送失败了怎么保证消息可靠呢?
为了保证发送重传并且消息不重复我们做了两件事:
-
首先同样我们也引入一个序列号,这个序列号是用户级别的,一个用户在线期间只会维护一个序列号,这个序列号是单调递增的。当客户端发送失败时会尝试一定次数的重发,如果
N
次都重发失败会回调通知业务层。客户端重发的时候序列号是保持不变的,因为序列号是单调递增的,所以服务端就可以通过判断序列是否重复来区别是否是重发的消息。 -
其次服务端会缓存一定时间的消息发送结果,当判断消息为重传时会查询上一次发送结果,直接把结果返回给客户端。整体流程如下图:
用户遍布全国各地,每个用户的网络情况都不一样,而且质量千差万别,所以对于 TCP
长连接掉线的情况是再正常不过。因为在掉线到重新连接上这段时间内服务端是无法正常将消息转发该用户的,我们通过下图所示的流程来保障用户重连后能收到丢失的消息。