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建索引也是要额外耗费存储的】!两张表存储建二狗设计如下:

  • 表1:Thread Table
    • row_key = thread_id
    • column_key = null
    • value = 其他的基本信息
  • 表2:ParticipantHashCode Table
    • row_key = participant_hash_code
    • column_key = null
    • value = thread_id

    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找个副手帮忙登记在线的用户

  • 增加一个Channel Service(频道服务)
    • 为每个聊天的Thread增加一个Channel信息
    • 对于较大群,在线用户先需要订阅到对应的 Channel 上
    • 用户上线时,Web Server (message service) 找到用户所属的频道(群),并通知 Channel Service 完成订阅
      • Channel就知道哪些频道里有哪些用户还活着
      • 用户如果断线了,Push Service 会知道用户掉线了,通知 Channel Service 从所属的频道里移除
  • Message Service 收到用户发的信息之后
    • 找到对应的channel
    • 把发消息的请求发送给 Channel Service,而不是具体的client
    • 原来发500条消息变成发1条消息
  • Channel Service 找到当前在线的用户
    • 然后发给 Push Service 把消息 Push 出去
        然后数据的流转流程就变成这样了:

      

       这里为啥不直接用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   聊天系统介绍

posted @ 2022-08-26 11:36  第七子007  阅读(1555)  评论(0编辑  收藏  举报