微信后台架构浅析--读写扩散技术

首先我写这篇博客所要探讨的问题是什么,自己查询资料得出的结论或者理解记录下来,产生了这篇博客

问题背景:

              我相信现在几乎我们每个人都在使用微信,那么你知道微信平台每天的信息量有多大吗?2017年微信官方在知名论坛上公布了其今年9月平均日登陆用户达9.02亿,同比增长17%。你在这9亿人里吗?每天有如此多的用户使用微信,你能想象到每天发送多少微信消息呢?380亿!是不是很吃惊,微信朋友圈消息,语音、视频消息较往年都有较大增长,微信每天要处理这么多的消息,那他是怎么保证消息传递的可靠性并且有效的降低服务器或者客户端的压力呢?下面就跟我一起来了解并且揭开解决这一问题的方法之一的读写扩散技术。

初期:

           要知道微信早期定位是一个通讯工具,并没有象现在这样分支很多,作为一个通讯工具,跟qq一样,最重要的就是收发消息,并且保证消息的可靠性,这里有必要说一下我想说的一种可靠性不足的场景:假设A,B客户端通过服务器S通信,如果A    发送了消息,但是B却不在线,这时候微信是如何保证消息可以在B上线的时候把消息推送给B的,或者说B是如何获取“离线消息”的呢?

这里我找到了微信早期的消息模型:存储转发(下图)。

微信后台架构浅析--读写扩散技术_第1张图片

所谓存储转发就好像邮箱的处理方式一样,当客户端A发送消息给B,B这时候并不在线,,那么消息会临时存在服务器,当然了消息不能一直在服务器持久化存储,因为服务器的内存资源毕竟有限,当时的解决方法就是服务器暂存了消息以后,会尽快发送消息通知给接收端,从而使接收端更快的将消息接收,客户端收到通知以后主动到服务器收取消息,然后服务器会释放这块消息存储。

第二阶段:

             因为用户的账户、联系人和消息都在服务器存储,如何将数据同步到客户端就成了很大的问题,于是有了下面的方案,做一个同步协议来同步这些基础数据。

  方案一:

客户端记录一个本地数据的快照(Snapshot),需要同步消息的时候,客户端将这个Snapshot带到服务器,服务器通过计算Snapshot和服务器数据的差异,将差异数据发送到客户端,客户端再保存差异数据完成同步。这种方案存在两个问题:

1.随着微信客户的越来越多,需要同步的数据就越来绝大,增大了流量开销

2.客户端每次同步都要计算Snapshot,会带来额外的性能开销和实现复杂度。

那这个问题怎么解决呢?

方案二:

解放客户端, 让服务器来计算Snapshot,然后跟消息一起发送给客户端,这时候客户端不需要知道这个Snapshot是什么,他只需要把这个Snapshot存储起来,在下一次数据同步的时候发送给服务器,这样服务器通过对比客户端的Snapshot和之前服务器保存的Snapshot进行对比,如果客户端发送过来的Snapshot是最新的,则服务器可以判断已经完成了数据同步,采用这个方案的另一个好处是,客户端不用发送ack到服务器来通知消息收到,因为服务器通过对比快照就能判断消息发送并且客户端接收成功,同步完以后服务器可以大胆的清楚暂存的消息。

 如今微信的功能越来越丰富了。其中很重要的就是群聊和朋友圈,同样是之前的问题,群聊和朋友圈又是如何解决的呢?

在说这个问题之前,我们先来了解一下读写扩散的概念:

inbox: 收件箱,你收到的消息,即你所关注的人发布的消息。
outbox: 发件箱,你发布的消息。

写扩散(Push)

该方式为每个用户维护一个订阅列表,记录该用户订阅的消息索引(一般为消息ID、类型、发表时间等一些元数据)。每当用户发布消息时,都会去更新其follower的订阅列表。
优点:读很轻。初始化时仅需要读取自己的inbox即可。
缺点:写很重。每发布一个消息,会导致大量的写操作。
注:一般来说,用户发布消息,并不会更新所有followers的订阅列表,仅更新在线followers即可。

读扩散(Pull)

该方式为每个用户维护一个发送列表,记录该用户所有发表过的消息索引。
优点:写很轻,节省空间。用户每发布一条消息,仅需更新自己的outbox。
缺点:读操作很重,计算量大。假设你收听了1k用户,则初始化时,需要从1k个用户的outbox拉取消息,然后计算获得最新的n条消息。

混合模式(Push+Pull)

该方式既为读写扩散的结合,根据用户followers的数量来决定是读扩散还是写扩散。例如followers大于1k的,则使用读扩散,否则使用写扩散。

从目前现在网上的一些资料来看,Twitter是写扩散,腾讯微薄是读扩散,新浪微薄则是二者结合。

好,下面我们就结合群聊和朋友圈的场景来看一下读写扩散是怎么使用的。

这里要知道微信每个人的好友有上限,群聊的人数也有上限,所以这就限制了微信的设计复杂度没有新浪微博那样复杂,因为群聊是个耗时的操作,所以可以采用异步队列写扩散的方式来实现消息的发送。当A用户在群里发送了一条消息(在outbox)以后,会在群中其他所有人的inbox中添加一条这条消息的索引,即使你并没有在线,当离线用户上线的时候,只需要检查自己的inbox列表就能收到“离线消息”。换句话说就是:消息扩散写到每个人的消息存储(消息收件箱)后,接收者到后台同步数据时,只需要检查自己收件箱即可,同步逻辑跟单聊消息是一致的,这样可以统一数据同步流程,实现起来也会很轻量。至于为什么不采用读扩散,一是因为一个群聊的人数相对较少,利用写扩散更加合适,二是如果采用读扩散,微信群聊每秒钟都可能有消息产生,那么用户会执行大量的读操作,这个过程并不是完全可靠的,很可能发生数据丢失。

下面再看一个微信朋友圈的例子(转载自:例子):

比如有两个用户小王和Mary。小王和Mary各自有各自的相册,可能在同一台服务器上,也可能在不同的服务器上。现在小王上传了一张图片到自己的朋友圈。上传图片不经过微信后台服务器,而是直接上传到最近的腾讯CDN节点,所以非常快。图片上传到该CDN后,小王的微信客户端会通知微信的朋友圈CDN:这里有一个新的发布(比如叫K2),这个发布的图片URL是什么,谁能看到这些图片,等等此类的元数据,来把这个发布写到发布的表里。

在发布的表写完之后,会把这个K2的发布索引到小王的相册表里。所以相册表其实是很小的,里面只有索引指针。相册表写好了之后,会触发一个批处理的动作。这个动作就是去跟小王的每个好友说,小王有一个新的发布,请把这个发布插入到每个好友的时间线里面去。

然后比如说现在Mary上朋友圈了,而Mary是小王的一个好友。Mary拉自己的时间线的时候,时间线会告诉到有一个新的发布K2,然后Mary的微信客户端就会去根据K2的元数据去获取图片在CDN上的URL,把图片拉到本地。

在这个过程中,发布是很重的,因为一方面要写一个自己的数据副本,然后还要把这个副本的指针插到所有好友的时间线里面去。如果一个用户有几百个好友的话,这个过程会比较慢一些。这是一个单数据副本写扩散的过程。但是相对应的,读取就很简单了,每一个用户只需要读取自己的时间线表,就这一个动作就行,而不需要去遍历所有好友的相册表。

为什么选择这样一个写扩散的模型?因为读是有很多失败的。一个用户如果要去读两百个好友的相册表,极端情况下可能要去两百个服务器上去问,这个失败的可能性是很大的。但是写失败了就没关系,因为写是可以等待的,写失败了就重新去拷贝,直到插入成功为止。所以这样一个模型可以很大的减少服务的开销。

虽然我不知道微信后台具体采用的什么技术或者工具来实现的读写扩散,但是我觉得这可以通过Redis的list和set两个数据解耦来实现,比如微信朋友圈都知道的功能,当你发布一个朋友圈的时候,只有和你拥有共同好友的人才能看到评论,这里就可以使用redis的set的求交集、并集、差集等操作来找到你的而共同好友进而决定向哪些用户发送消息索引。





            



  

    

你可能感兴趣的:(微信后台架构浅析--读写扩散技术)