聊天应用每个人都用过,但一千个人心中就有一千个聊天应用。这篇博客就来探讨一下聊天系统的设计。如何设计一个聊天系统,首先需要搞清楚,要设计一个什么样的聊天系统。
理解问题并确定设计的边界
市面上常见的聊天系统有QQ、微信、飞书等,有的面向小学生,也有的专注于办公场景,还有的专攻游戏。
:我们需要支持哪些主要功能?
:需要支持单聊、群聊。为了简单起见,可以先只支持文本消息。群聊人数上限为200。
:应用的规模如何?
:DAU为1亿。
:聊天记录需要永久保存吗?
:forever!
首先搞清楚我们的需求,我们需要设计这样一个聊天系统,它类似简化版的QQ:
- 支持单聊与群聊,群聊人数上限500。
- 支持展示在线状态。
- 支持多设备登录。
- 支持通知推送。
- 目标日活1亿。
尝试封底估算
- 写操作:假设每个用户平均每天发10条消息,QPS = 1亿 * 10 / (24 * 60 * 60) ≈ 12w
- 读操作:一般聊天系统读写操作比例是1:1
- 存储:一天产生1亿 * 10 = 10亿条消息,假设每条消息50B,一年的存储为 10亿 * 50B * 365 ≈ 16TB
高层级的设计
了解了需求,接下来需要考虑聊天应用的基本操作有哪些:
- 接收来自发送端的消息。
- 消息转发给接收端。
- 接收端离线时,在服务器暂存消息,直到接收端上线时再进行转发。
大概像这个样子:
对于大部分C/S应用来说,HTTP协议时久经考验的选择。HTTP1.1也支持长链接,减少了TCP握手的开销。对于聊天服务器的发送端来说自然也是没有什么问题,但考虑到接收端需要实时接收来自服务端的消息,这个场景下就有心无力了。WebSocket时服务器向客户端发送异步更新的常用的解决方案。其由客户端主动发起握手,双端共同维护全双工的链接,用来做消息信道;对于其它的功能,比如注册、登录等仍然使用HTTP也是OK的。
组件划分
聊天系统大体上来看,可以分为上图这三个组件:
- 无状态服务:提供登录、注册、个人信息的查询修改等功能。其中服务发现用于给客户端提供可用的聊天服务器列表。
- 有状态服务:聊天服务维持了跟多个客户端的会话,所以是有状态的。
- 其它组件:推送通知也是一个很重要且较为复杂的系统,可以另行考虑设计。
鉴于我们系统庞大的用户量,单机服务的设计肯定是不可取的,否则会被人笑话。
所以需要将我们的设计进行调整,支持分布式部署。
改进后的设计
改进后的设计如上:
- 聊天server负责消息的收发。
- 在线状态server管理用户的在线/离线状态。
- API server负责用户登录、个人信息的查询等功能。
- 通知server负责可靠推送。
- 底层存储采用键值存储。
关于聊天消息是用关系型数据库如MySQL来存储,还是用键值数据库如Apache Cassandra来存储?这取决于聊天系统中的数据读/写模型。
存储的选择
一般而言,在聊天系统中,典型的有两种类型的数据:
- 基本数据,如用户信息、个人设置、好友列表等。
- 聊天数据,即消息。
尤其以聊天数据的量最为庞大。大部分情况下用户查看的都是近期的聊天记录,太早的鲜有人问津。用户常用一些功能可能涉及消息的随机访问,如调整到回复消息处、搜索指定内容的消息。考虑到键值存储一般较关系型数据库而言更容易横向扩展、数据访问延时低的特点,我们用键值存储系统作为聊天系统的底层存储是个不错的选择。另外,一些知名的聊天系统也采用键值存储,如Facebook Messenger。
数据模型
消息是在键值存储中的样子长什么样?
单聊
msg_id(bigint)作为主键,其必须是唯一的,并且有序,可以考虑使用SnowFlake算法生成。数据列有:from(bigint), to(bigint), content(text), create_time(timestamp)。
群聊
group_id(bigint), msg_id(bigint)作为复合主键,数据列有:from(big_int), content(text), create_time(timestamp)。
进一步细化设计
服务发现
服务发现的主要功能是根据地理位置、服务器性能、网络状况等因素选择合适的聊天server给客户端,使其就近择优接入。
ZooKeeper是一个开源的服务发现解决方案,通过注册可用的聊天服务器,可以基于一定的规则去选择一个合适的server给客户端。
如上,client登录聊天系统后,负载均衡器将请求分配到API server。API server校验登录信息后,通过ZooKeeper提供的服务发现找到对该client来说最优的聊天server,并返回给client。随后client与这个最优聊天server建立WebSocket链接,开始通信。
消息流
对于单聊而言,client A发送一条消息给B,聊天server1将消息发到消息队列,并存储到键值存储中。如果client B在线,则消息直接被转发到server 2,否则通知到推送通知服务器。
对于群聊场景,接收端涉及到多个用户,所以可以考虑使用多个消息队列。来自client A的消息,被复制到每个群成员的消息队列中,即收件箱。这样接收端只需要查看自己的收件箱,就知道有没有新消息了,简化了消息的处理。另外,把消息复制到每个接收者的收件箱有一定的开销,但因为群成员数量并不多,这个开销也是可以接受的。知名的聊天应用有微信就是使用了这样的机制。
但如果群成员数量进一步增大,如QQ频道,每个频道有上万人,就不适合这种方式了,此种场景需要将消息的扩散尽量延后。
维护在线状态
用户登录、退出登录、或者网络状态变化,可能涉及在线状态的改变。对于网络状态变化场景,考虑到移动应用网络经常不稳定,切换多的场景,可以设定一定的心跳机制,长时间断链的情况下,由server端主动将其改为离线状态。
在线状态变更,另外也涉及到一些组件的关注,比如通知服务、客户端好友列表。可以考虑通过主动push推送来同步状态的变更。对于群列表,涉及到的状态推送可能扩散的比较多,可能影响性能,可以考虑用户主动拉取的时候获取对应状态,而不是主动推送。
总结
到此,我们初步的设计了一个支持单聊和群聊的聊天系统,使用Https作为client与API server的通信协议,基于WebSocket实现消息的通信。服务端包含了多个组件:API服务、在线状态服务、聊天服务、推送通知服务以及键值存储。
但还是有许多没考虑到的地方,比如扩展性的考虑如支持富媒体消息,常见的聊天应用都是支持的;如安全方面的考虑如消息的端到端加密,国外的电报就是如此设计的;一些异常处理,如消息发送失败的重试机制等。