我们每天的生活都离不开微信。微信基本上已经成了在中国生活的一部分。

但是,微信有些方面的功能总是看起来令人觉得有些奇怪。例如:

  • 微信几乎是第一款需要手机扫码才能登录PC端的
  • 微信群和微信聊天记录在切换设备后会完全无法同步
  • 你无法得知自己是否被一个微信好友拉黑了
  • 你不能同时在多个移动设备上同时登录微信

从产品的角度来看,这些设定看起来彷佛是在给用户制造障碍,也成了大家普遍对微信的槽点。那么,微信的团队为什么要这么设计,而为什么又不改良它的体验呢?我想或许问题并不只是产品和需求分析这么简单,而是还需要进一步探寻架构上的本质。

在我生活在澳大利亚期间,我发现一件很有趣的事情:在澳大利亚,我使用微信给同样在澳大利亚的好友发送微信消息,他也可以几乎瞬间收到。假想微信在国内建立单点的服务器,那么这种性能一定不足以支撑澳大利亚内的通信。数据从澳洲抵达中国再从中国推送到澳洲至少需要500ms。而表现来看,仿佛只花了30ms就完成了一条微信消息的传递。

那么这意味着这条消息很可能根本就没有去中国,而是只在澳大利亚当地就完成了收发和传递。微信在澳大利亚是单独部署有计算节点的。而这么来看,微信的服务器,应该是遍布全球各地,并没有一个中心化的服务器。微信是个分布式应用。

如果微信是分布式应用,对上面几个现象就不难解释了。但是我这么猜测,必须还得建立一套完整的可行性验证,并且能够满足现有微信的所有用例,我的这个猜想才算没有错误。而为了满足完整的分布式通讯,微信至少要设计三个算法:

  • 服务器选择算法
  • 服务器端切换算法
  • 用户发现算法

这三个算法,第一个服务器选择算法,可以让一台微信移动端的App定位到最合适的微信服务器。服务器端切换算法可以实现在原有服务器不再可用时,平滑的移动到新服务器上并且继续保持消息。用户发现算法则可以搜索出目标用户所在的服务器。在这三个算法建立完善的基础上,用户可以直接定位到自己需要连接的目标微信服务器,并且目标微信服务器也知道你要将消息发送的目标用户所在的服务器,然后在两台服务器之间直接点对点通信即可。

服务器选择算法

微信的服务器为了扩展方便,他们可能会在全球各地建立大量微信节点,保证任何一个地理位置的人,都有一台离他不算太远的微信服务器可连。但是,即使硬件上保证了这一点,那目标节点的用户怎么才能得知这台离他不远的服务器的IP等信息,并且知道这台服务器最合适呢?服务器选择算法中,首先就得包含一个服务器的注册与发现。而在没有中心节点的情况下,要想维持一份数据,普遍的做法是采用gossip协议。

gossip协议是一种基于流行病传播方式构建的信息交换协议。它的原理是想象将我们需要传递的信息就是一种病毒,而这种的病毒R0 > 1,从而它让每个个体在被感染后都平均能够再传播超过一次。那么随着时间变化,所有个体都会感染这种病毒。这种协议的优势在于去中心化,是一种分布式协议,能够在一个集群中解决传播的状态一致性问题。换句话说,如果微信有一个“可以使用的微信服务器列表”,那么每新增一台服务器,这台服务器就可以向集群传播自己的信息,而这种信息很快就会“感染”所有的微信服务器,保证最终的数据一致性。最终,每一个微信服务器都能得知完整的可用服务器列表。这向技术,也普遍应用在了DNS记录的传播。

例如微信使用gossip协议维持了一份服务器清单,那么对于设备来说,只需要查询任意一台服务器就可以得到服务器列表。根据设备IP它不难得知自己的地理位置是在哪个国家哪个省,从而筛选出省内的服务器列表。再对这些服务器进行测速,或者搜索这些服务器中连接数最小的那个,就可以确定这台微信App需要连接的目标微信服务器了。

服务端切换算法

服务器选择后,我们还需要考虑目标服务器本身可能出现故障的情况,或者微信可能会撤走一台服务器。如果微信真的撤走了一台服务器,需要立即重新进行服务器选择。此时App端不再能够连接到撤走的那一台,而连接到另一台可用的好服务器上。

当然除了这两种情况,我曾经经历过这件事:我在北京乘坐飞机前往新加坡。在飞机上我手机全程关机。当我抵达新加坡后,我连接新加坡的LTE后,立即启动微信,发现有一大群人给我发了微信消息。看起来是个很平常的事情,但是仔细思考:假如我在新加坡启动微信后,微信发现自己连接原有北京的微信服务器已经延迟实在是太高,触发了服务器选择算法。服务器重选后,我很快确定了要连接到新加坡的服务器。但是微信的消息并不会广播满整个分布式集群,新加坡的微信服务器实际上并无法得知我的未读消息。所以必须需要一个服务器端切换算法来实现将我在北京服务器上的未读消息同步到新加坡服务器上。

例如我在飞机上执行了恢复出厂设置,我的手机并不知道我之前连接的是北京服务器的话,这个服务器切换算法,必须让新加坡服务器自主的得知:该用户在切换服务器之前连接的是哪一台,再向原有的老服务器尝试拉取消息历史就好了。

分布式系统,让全计算集群能够得到一个数据的最终一致性的算法最典型的不就是gossip嘛。我们完全可以让“用户处在哪一台服务器”这个key value靠gossip传播满全集群。新加坡服务器很可能在我降落之前就已经知道了我是北京人了。当我连接它时,他直接去向北京服务器拉取我的未读消息历史,推送给我的手机就完事儿了。

用户发现算法

那么还剩最后一个问题:我在航班中,亲朋好友给我发送微信消息的时候,他们是如何得知我在哪台服务器上呢?自然这个问题也不难回答了。我的每个亲朋好友都和我一样,都是同样的微信终端,需要靠服务器选择算法连接到距离他们较近的服务器,例如上海服务器。而用户-服务器这个key value已经广播满了微信集群,在我的飞机降落前,他们始终会先将消息发给上海服,上海服发现当地并没有安度因,安度因注册在了北京,然后立即将消息转交给北京服。此时上海服完全可以不保存这条消息。对于他来说他只是个中转传递的节点。北京服只需要暂存这个消息,当推送给我的手机App之后,它也不需要保存这条消息了。

当我航班一落地,我的微信App先触发服务器选择算法,选择新加坡。新加坡当地的微信服发现一位注册在北京的用户连接了新加坡服,立即触发服务器切换算法,把我的消息历史同步。此时北京服可以删除这些消息历史了。

当然,使用gossip协议来广播用户所在位置只是我的一种猜想。如果微信所有服务器形成的是一个图状的数据结构,也而完全可以利用类似路由的算法去搜索用户,相关的算法就是dijkastra算法,可以求出图中的最短路径,再按照路径发送消息。不过我猜测为了设计简洁,微信服务器与服务器之间的连接更可能是点对点。发现用户的话,又可以靠图的广度优先遍历算法。只不过,具体微信使用的是哪一个,我们在阅读源码前,觉得gossip可能性更大一些,因为它可以解释后续更多关于微信的现象。但可以肯定的是,单条消息是不会广播满整个网络的,而是只存在于需要收发的两台服务器之间,其它服务器是完全不可知的。

用户登录

微信的功能不止是发消息。它还有登录注册的流程。微信的用户账户控制我们猜测,更大可能并非是分布式设计。即使是gossip也只能保证最终一致性。而用户的基本资料、密码,属于对实时一致性要求更高,又不应该分布式存储的机密数据。将其安置在一台中央化的数据中心中更为合适。大家需要登录注册时,就直连这个中央的认证网关就好了。它的压力不会太大,毕竟不会有人每天没事儿了不停切号玩。

群聊

群聊也需要解释,但是它并不难。但是在解释群聊前,我们可以先类比一下微信的通信过程和Email的过程。其实两者高度相似:Email的SMTP协议也没有中央节点。而是靠Email地址的后半部分进行服务器选择,收件人的后半部分来进行用户搜索的。Email几乎不会有服务器切换的过程。所以,一旦确定了收件人发件人所在服务器,就直接两台服务器之间直连把Email发送过去就完事儿了。

而我们其实经常群发Email,收到群发的Email后也喜欢Reply All。而这种用法,实际上就和微信的群聊高度相似了:在群中聊天,实际上就是在不停Reply All一封Email,需求自然就解决了。

那退群怎么实现呢?自然也不难。我在被人Reply All后,如果不想继续参与这个讨论,我会Reply All:”请将我从收件人列表中移除!“。大家如果都足够聪明,就会以后Reply All时不再发给我,自然我就收不到了,实现了退群的用例。

文件传输

文件传输就更不难解释了。直接类比Email的附件功能就妥妥的。只不过微信还是会额外在文件存储几天后删除文件,从而保证服务器本身的空间不会占用太大。

为什么微信要这样设计?

这样设计有诸多好处。首先我们从运维上来看:真正做到了哪里不快加哪里。假想最近突然涌现出大批非洲用户,而非洲并没有微信的节点,这些用户在进行服务器选择的时候,可能都选择到了南非的节点上,距离他们都较远。为了加速非洲中北部的微信使用体验,微信团队其实可以直接在非洲中北部架设一台微信服务器,安装完成后什么都不需要做。服务器本身就会将自己存在的这个事实gossip出去。非洲中北部的用户很快就会在下一次触发服务器选择算法时切换到中北部的服务器,从而大幅度降低延迟。

另外,这样设计对于单台服务器的生命,其实也可以大幅度弱化。如果单一几台服务器故障,对于微信来说根本不是问题。这些服务器的用户很快就会重新进行服务器选择,而选择到其它可用的微信服务器上去。用户甚至可能都无法感知服务器的故障。总体来说,分布式的应用设计,让微信就像是运营商的基站一样,哪里信号差,在哪里加一个。哪里的坏了,并不影响上网,只是他们可能暂时性变卡而已。

15亿人每天都发大量的微信消息,这些消息让任何一个单点式的计算节点都极难承担。而在中国,假如微信在全国每个城市都建立了节点,那么这个超大的流量就会分散到每个城市里去。任何一个节点压力都不高。微信就可以非常容易的实现横向扩展,还不需要负载均衡。这种运维体验简直好到不要再好。微信真正做到了靠架构设计本身,而不是靠大量的钱和计算资源,就承载了极高的用户并发。

微信也是个国际化的应用。有相当多的外国人在使用微信。他们在沟通时,不应当使用单点的数据中心,来让数据经过中国。微信的设计更像是通讯的基础设施,能够让外国人直接在当地把信息完成交换。而如果不同地区法律不同,微信也可以完全只修改这个地区的服务器即可。例如某国需要将聊天记录进行备案和审查,那么微信可以只在这个国家部署有审查功能的服务器,而不需要调整整体架构,对App来说也是无感知的。

类比QQ

而类比QQ:QQ是有完整的多设备同时登录、聊天记录跨设备同步、QQ群的保持和搜索等这些功能的。它更大的可能是一个集中式的部署,靠强大的负载均衡和运维能力支撑这一个数据中心正常。如果单点服务挂了,全球的QQ业务就挂了。在澳大利亚,我试过聊QQ,那种体验是相当的卡,一条消息真的要转半天。所以,腾讯的技术团队应该也是遇到了相同的问题,但是直接在QQ的基础上改又实在是太过于费劲,干脆重新从0开发一个全新的干净的、分布式的、全球化、高并发的IM应用,作为通信的基础设施吧。

面对这样的设计架构,我也不得不称赞微信的团队设计能力真的是相当的高明,能够靠分布式的网络,分散压力,让消息直接通过最近的路径传递,让服务器的生命变得都不重要,哪里不快加哪里。实在是难得的优秀的系统设计!

解释疑惑

目前为止,开头我提出的几个难题,我想应该已经不难解答了。

  • 微信几乎是第一款需要手机扫码才能登录PC端的
  • 微信群和微信聊天记录在切换设备后会完全无法同步
  • 你无法得知自己是否被一个微信好友拉黑了
  • 你不能同时在多个移动设备上同时登录微信

为什么需要手机扫码才能登录PC端?这是因为如果你是通过输入密码登录的PC端,那么重新运行服务器选择算法后,一旦选择的和你的手机不是同一台服务器,那么你的手机就再也收不到消息了。靠手机扫码并非是PM为了难为用户,而是需要先确定你的手机连接的是哪台微信服务器,确定之后,再让PC也连接同一台服务器,从而保证当有消息时,手机和PC都能正常的收发和同步。当然,这也可以解释第四个问题:为什么不能同时在多个设备上登录微信,也是为了保证在gossip协议工作时,你不会同时注册于两台不同的微信服务器上。

而且,我们也能发现:PC刚刚登录后,它的消息历史实际上是靠手机同步到PC中的。因为服务器上根本没有存储你的聊天历史。

这也可以解释第二个问题:为什么换了设备后聊天记录会全没?因为服务器只是一个中转、传达、暂存功能。微信的聊天记录完全保存在本地,在你更换设备后自然本地没有这些数据,就全丢光了。群聊也是。群聊我们类比了Email的群发,那么在Email都没有同步过来的情况下,自然无法确定你加了几个群了。

我还有最后一个漏洞没有讨论:微信的好友列表是怎么维持的?由于换了设备,好友列表也能正常同步,所以好友列表应该并不是只存本地,而是存在于服务器上的。但是当你被微信好友拉黑时,实际上并没有发出一条消息,所以你的本地的消息历史中,仍然存在有和拉黑了你的好友的消息记录,就像Email发送者即使Block了你的Email,你仍然能够看到你们的聊天历史一样。自然这种设计,就导致你无法得知这件事了。只有在你尝试发送消息时发送失败了,才能证明可能他已经不再想和你说话了。

致产品经理

说了这么多,越是研究一个东西的本质,越发发现它实际上比想象的复杂得多。然而,我还是认识太多产品经理,喜欢无脑模仿微信,喜欢无脑做手机扫码,也遇到太多PM天天吐槽微信各种槽点,想干掉微信。“不就是发个消息嘛,有那么费劲吗?” 答案是:当然有。而且直到目前上面这个微信的协议的猜想中,我相信也有大量的疏漏以及和真实情况的偏差。有些功能,并不是程序员脑残,而是这种数据结构本身根本就支撑不了。但是有太多PM意识不到这件事。他们的一行需求,可能会让整个体系重构。