之前介绍过微信红包系统的架构(参考:微信红包后台系统设计、微信红包设计方案)。
微信作为国内在线用户最多的国民应用,其架构设计中的应对高流量、低延迟的一些技巧,在其他公司或团队解决高并发问题时一定有一些参考作用。今天介绍下微信视频号聊天室的高并发消息收发解决方案。
视频号在直播时相当于把所有的关注者拉到一个群里面,因为同一时间一个用户只能关注一个聊天室,所以他的技术方案和之前介绍的群聊天还是有些差别的。
之前介绍过钉钉的群聊天方案。参考:钉钉架构设计
群功能消息特点:一个群少于500人,群成员之间有关系,群成员流动性低,对离线消息较为关注。
聊天室消息特点:数万人参与,成员之间没有关系,聊天室成员流动性较高,不关注离线消息。
基于以上两个特点,群消息适合写扩散机制,而聊天室适合读扩散机制。
聊天室可以看做是一个基于房间的临时消息信道,主要功能包括消息收发、在线状态统计等。
微信聊天室形态最早出现在2017年,当时主要用于电竞直播间,支持了高性能、高实时、高可扩展的消息收发。
聊天室消息的一大特点就是要做到高实时,基于上面架构图,思考下如何实现用户消息的实时同步呢?
可以采用长轮询方案。
用户读请求进来之后,在RecvSrv进行消息轮询。当发现新消息之后会给接入层发送重新获取的请求,这样增量的消息就被读取到客户端了。
这里为什么没有考虑websocket呢?
首先ws的推模式有可能丢消息,这样端上还需要一个拉模式兜底。同时推模式需要维护一个精准的在线列表,成本变大。这种长轮询对于客户端来说其实是个短连接,客户端实现起来更简单。
因为聊天室更适合采用消息的读扩散模型,这样会给读盘造成压力,所以需要有个cache解决这个问题。
SendSvr接收到用户消息时,写入消息列表,然后向RecvSvr集群发送通知。当RecvSvr接收到新消息通知后,异步线程拉取消息。
当RecvSvr收到某个聊天室的写消息请求时,触发该聊天室的异步轮询,为了避免通知失效导致无法更新消息,需要有个兜底,比如1s内触发一次轮询拉取。同时需要考虑机器异常重启时的数据自动恢复能力。
读写队列过程中可能存在并发的情况,可以采用COW思想。当有写更新,后续的读到最新写的队列读。当再写队列时,比对下读队列,看看是否需要从新读补全到主数据上。
以上是聊天室1.0的架构是否可以支持千万级同时在线的直播呢?
还存在一些问题,比如消息信道不能保证所有消息都下发。一个房间内用户的状态信息聚合到同一台statSvr,存在单点瓶颈,并发变更时导致部分房间在线数跳变。缺少历史在线人数统计。
那2.0架构如何解决以上问题的呢?
一些信令丢失的原因是因为,在RecvSvr的cache中只保留了最近2000条消息,有些消息在客户端还没来得及收到,就被cache淘汰了。
所以目前看起来,写扩展和简单的读扩散都不可行。
还有一种方法是拆分,比如消息分级,缓存在两个cache里面。但也会有问题,比如recevSvr需要同时读两个消息表,这样会消耗recvSvr对cache的两倍访问。
在1.0架构上,每个消息发送到服务端之后,recvSvr其实是都可以感知到notify的,所以每次拉取的消息数量都不会很多,这一过程不会丢消息。
为充分利用cache,同时保证消息的下发可以这样设计。
在消息表上对重要消息打标。在recvSvr拉到消息后,将消息分为普通消息和重要消息。这样在消息收取时,先收取重要消息,再收取普通消息,实现了重要消息的优先下发。
聊天室在线状态统计实现,参考了微信设备在线的设计。
每个直播房间对应一个sect。按照房间id,选择一台机器作为master,该房间的请求读写此机器。为解决单点问题,将master下这个房间的数据同步到其他sect下的机器上,这样即使master挂了,仍然可以通过其他机器进行读写。
微信团队还是用机器维度的分布式方案,简单点用分布式缓存就行了。
记录聊天室活跃用户,可以在服务端记录两个值:用户id、活跃时间。
通过定期心跳,更新用户在线状态。
之前提到过,一个直播间有上千万人,以1000w为例,如果10s心跳一次,请求就是6000w/min,考虑到缓存穿透、并发性能,需要从磁盘回拉数据,可能导致拉齐到一些需要延迟删除的用户状态信息,以至于获取到的在线用户数远大于实际用户数。
怎么解呢?
可以考虑拆key+读写分离+异步聚合落盘方案解决。
每台机器负责统计部分在线数据。每台机器内按照uid做哈希,打散到多个shard。每个shard对应一个key。
在数据聚合查询时,每台机器拉取自己所有的key,最终组合出一个完整的在线列表。
这样通过拆机器、hash等方式将原有6000w/min的请求打散了,降低了并发问题。在聚合查询时,每个机器负责自己的查询聚合,降低了穿透的问题和数据不一致的问题。
大小直播间的流量差异很大的,所以需要考虑大小直播间不要相互影响。
大直播间需要更多的机器保证体验,小直播间用小规模机器即可。
这里微信团队考虑了微信支付对于大小商户的隔离做法,做了流量隔离。
对于可预测的大直播间加白,直接走vip;
其他直播间走普通;
大直播间在线人数多,需要拆在线列表key;
在longpolling的机制下,直播间一直有消息的话,100w的在线每分钟至少会产生6kw/min的请求,而1500w更是高达9亿/min。
logicsvr是cpu密集型的服务,按30w/min的性能来算,至少需要3000台。所以这个地方必须要有一些柔性措施把控请求量,寻找一个体验和成本的平衡点。
这个措施一定不能通过logicsvr拒绝请求来实现,原因是longpolling机制下,客户端接收到回包以后是会马上发起一次新请求的。logicsvr拒绝越快,请求量就会越大,越容易造成滚雪球。
上图可以看到,正常情况下,当recvSvr没有收到新消息时,可以让请求在proxy层hold住,等待连接超时或是notify。
所以可以通过在proxy的柔性控制,让请求或者回包在proxy hold一段时间,来降低请求频率。
根据不同的在线数,设置不同的收取间隔。在客户端上下文里面记录下上次收取的时间。成功收取消息后,让请求在proxy层hold一段时间。
微信这么多年在微信支付、微信红包、小游戏、在线聊天产品形态下积累了业界最顶尖的最佳实践,所以业界老有人说微信技术差,人家能随意支持千万级、亿级流量,技术水平肯定是业界最强的。可能少的是在业界的一些曝光吧。