system desing 系统设计(十二): IM聊天软件功能设计
1、说到聊天IM,大家第一个想起的肯定是微信了!这种国民级别的APP,肯定人手一个账号,其界面的功能看着也简单:
登陆微信后首先有个list,列举了用户当前所有的会话;本想用session表示,但这个单词已经在http/https的cookie里面被使用了,为了防止混淆,这里用thread替代单个会话,所以大家看到的界面就是thread List!点击某个会话就进入了具体的聊天界面,用户可以在这里看到message。如果是群聊,还能看到message的发送者,整体的功能是不是很简单了?
聊天IM最重要的就是用户的数据了(这不是废话么),应该没人反对吧?A给B发的消息,B如果没收到,或者收到了错误的数据,两个用户都要骂娘!错误或丢失的消息,轻则影响用户体验,重则造成用户重大损失(比如JC蜀黍侦办案件时可能会查看当事人的聊天记录等),所以设计IM系统的第一步就是设计数据库表的结构!这里能用的数据库表设计如下:
(1)Thread Table:当然是列举了所有会话!注意,这里包括所有用户的所有会话。以微信为例,假如有30亿注册用户,平均每个用户有100个会话,那么这个表就有3000亿行!
- id:就是会话id啦,全局唯一!
- last_message: 这个会话最后收到的消息,是需要显实在app上的
- create_at: 会话的创建时间
- participant_user_ids: 会话的参与者。如果是两个人点对点聊天,就是另一个userId;如果是群聊,就需要把所有的user_id都列举了!可以做成【uid1,uid2,uid3...】的形式
- participant_user_hashcode: 对上面的participant_user_ids求hash,便于快速定位群聊消息的thread_id!
(2)UserThreadTable:
- user_id: 这个表存放的是特定用户的thread信息了
- 其他都是该user自己的属性
- 为什么要和ThreadTable分开了?松耦合可以防止数据冗余、臃肿,也利于后期表结构更改!当然确定也明显:需要join,增加耗时!
(3)Message Table:
- thread_id: 哪个会话的message?
- user_id: 哪个用户发的?
- content和create_at最重要了,懂得都懂!
数据表的设计定了,数据该用什么存了? 以2019年其公开课提供的10.8亿月活【现在肯定不止这个数了】、日发送450亿条message推算:
-
Average QPS = 45B / 86400 = 520k,每秒发送52w条message
- Peak QPS = 520k * 3 = 1.5m,峰值按照3倍计算,每秒发送150w条message
- 假设每条message占用50byte空间,每天45billion的message大约需要2T的空间!
这么大的数据量,这么高的并发量,是用sql,还是nosql了?
(4)先来分析一下MessageTable,有以下两个特点:
- 读多,写多,一条message就像一条log日志
- 修改少
- 需要用timestamp对消息的收发顺序排序
综合对比,还是用nosql数据库吧!以Cassandra为例,存储结构如下:
-
row_key = thread_id
-
column_key = created_at 因为要按照时间倒序
-
value = 其他信息,比如表的其他字段
可以直接用thread_id做index,用creat_at来sort message!sharding直接用thread_id就好!
(5)UserThread Table:这个表和user相关的,存放每个user自己private的数据,特点如下:
- 更新比较频繁
- 到了后期新增数据量会减缓
- 需要用timestamp对消息的收发顺序排序
综合考虑还是用nosql数据库,存储结构如下:
-
row_key = user_id
-
column_key = updated_at 按照更新时间sort
- value = 其他信息,比如表的其他字段
- 用user_id来sharding
(6)最后看看ThreadTable,特点如下:
- last_message会频繁更新
- 后期新增行的频率会基本稳定
这么来看,貌似用sql和Nosql都还行!如果用sql数据库,需要同时index by:
- thread_id:用于查询某个对话的信息
- participant_user_hashcode: 用户查询某两个或多个用户【就是群聊啦】之间是否已经有会话
如果用Nosql数据库了?如果同时支持按照 thread_id 和participant_hash_code 进行查询,我们需要两张表:因为nosql不支持join,所以需要根据participant_hash_code查thread_id【像不像ES的invert index?】,然后根据thread_id查last_message、avarta、create_at、particitant_user_ids等其他字段;相比于sql数据库的B+索引,速度快很多【sql数据库能做的,nosql数据库也能做,大不了就多建几张表嘛!其实sql建索引也是要额外耗费存储的】!两张表存储建二狗设计如下:
2、表是设计好了,怎么让user顺利地读写这些表了?
(1)先来看看最简单粗暴的方式:A send message to B
- A send message to B,先把消息发到web server
- web server查询是否有 A 和 B 的会话记录(Thread),如果没有则创建对应的 Thread,再根据thread id把message等写入database
-
B 每隔 10s 访问一次服务器获取最新的信息(Poll)【轮询收消息】
这么做的缺点也很明显:B无法实时real-time收到A的消息,所以这个架构其实适用于邮箱mail等实时性要求不是那么高的场景!
(2)上述架构无法做到real-time的根本原因就是没有任何服务主动通知notify B有消息到了,B只能不停的poll web server,所以这里需要改进一下:用户一旦发送消息,需要立即主动push到接受消息的用户!这次换一下,让B给A发消息,架构设计如下:
- A如果上线,先找web server提供push server的地址
- A给push server发消息,告诉push server自己已经login,相当于在push server register了
- B给A发消息后,还是先经过web server,再由web server转到push server,最后由push server主动发送给A,完成整个通信
把push server和web server分开,是为了松耦合!push server要给大量的client发送message,肯定是要做负载均衡的,还是用consistency hash算法最好,如下:
3、(1)上面是P2P的message发送机制,群聊咋整了?群聊和P2P聊天相比,不就是接受消息的人多了么?群内一旦有人发送消息,我通过push server直接把message主动push到群内所有人不就O了么?理论上讲,这么做可以,但是效率不高:一个群少则几个人,多则几百人,最大的问题是:群里的人并不随时都在线的,对于不在线的人也push消息,没必要啊!缺陷如下:
- 浪费带宽和服务器计算资源
- 用户不在线的时候push,导致其无法接受消息;当用户登陆后,是不是要重新push message了?
所以对于群聊,只给在线的user push message就行了!那么又该怎么区分用户是否在线了?参考上面push server思路,给message server找个副手帮忙登记在线的用户:
这里为啥不直接用push service记录用户是否在线了?为啥还要单独搞出个channel?还是那个原因:松耦合!利用微服务的思路,把功能拆细,每个service只负责少量原子功能,可以降低后期的运维成本!
(2)关于用户是否online,还有另一种办法:client主动给服务端发消息上报,就是heartbeat机制!比如每隔3-5s给server发个消息,告诉server自己still online!Facebook Messenger就是这么干的!这就是抓到的heatbeat包!
参考:
1、https://cloud.tencent.com/developer/article/1445567 从0到10亿,微信后台架构设计
2、https://mp.weixin.qq.com/s/fMF_FjcdLiXc_JVmf4fl0w 微信后台系统演进之路
3、https://xie.infoq.cn/article/19e95a78e2f5389588debfb1c 设计亿级别消息IM
4、https://juejin.cn/post/6997299938912649230 揭秘微信朋友圈这种信息推流背后的系统设计
5、https://www.163.com/dy/article/GF9KBB7I0511X1MK.html 企业微信IM设计
6、https://www.bilibili.com/video/BV1Za411Y7rz?p=76&vd_source=241a5bcb1c13e6828e519dd1f78f35b2 聊天系统介绍