从无到有:微信后台系统的演进之路

从无到有

2011.1.21 微信正式发布。这一天距离微信项目启动日约为2个月。2个月里,微信后台最重要的事情:

  1. 确定了微信的消息模型

微信的消息模型是 存储转发 。消息被发出后,会先在后台 临时存储 ;为使接收者能更快接收到消息,会推送消息通知给接收者;最后客户端主动到服务器收取消息。

  1. 制定了数据同步协议

由于用户的帐户、联系人和消息等数据都在服务器存储,如何将数据同步到客户端就成了很关键的问题。为简化协议,我们决定通过一个 统一的数据同步协议 来同步用户所有的基础数据。

几经讨论后,方案改为由服务计算Snapshot,在客户端同步数据时跟随数据一起下发给客户端,客户端无需理解Snapshot,只需存储起来,在下次数据同步数据时带上即可。同时,Snapshot被设计得非常精简,是若干个Key-Value的组合,Key代表数据的类型,Value代表给到客户端的数据的最新版本号。Key有三个,分别代表:帐户数据、联系人和消息。这个同步协议的一个额外好处是客户端同步完数据后,不需要额外的ACK协议来确认数据收取成功,同样可以保证不会丢数据:只要客户端拿最新的Snapshot到服务器做数据同步,服务器即可确认上次数据已经成功同步完成,可以执行后续操作,例如清除暂存在服务的消息等等。

此后,精简方案、减少流量开销、尽量由服务器完成较复杂的业务逻辑、降低客户端实现的复杂度就作为重要的指导原则,持续影响着后续的微信设计开发。记得有个比较经典的案例是:我们在微信1.2版实现了群聊功能,但为了保证新旧版客户端间的群聊体验,我们通过服务器适配,让1.0版客户端也能参与群聊。

  1. 定型了后台架构

比较特别的是,微信后台每一种不同类型的数据都使用 单独的数据访问服务和数据存储服务 ,例如帐户、消息和联系人等等都是独立的。

微信后台主要使用C++。后台服务使用Svrkit框架搭建,服务之间通过同步RPC进行通讯。Svrkit是另一个广??????台就已经存在的高性能RPC框架。

这三件事影响深远,乃至于5年后的今天,我们仍继续沿用最初的架构和协议,甚至还可以支持当初1.0版的微信客户端。

这里有一个经验教训-- 运营支撑系统 真的很重要。第一个版本的微信后台是仓促完成的,当时只是完成了基础业务功能,并没有配套的业务数据统计等等。

小步慢跑

在微信发布后的4个多月里,我们经历了发布后火爆注册的惊喜,也经历了随后一直不温不火的困惑。

这一时期,微信做了很多旨在增加用户好友量,让用户聊得起来的功能。打通腾讯微博私信、群聊、工作邮箱、QQ/邮箱好友推荐等等。对于后台而言,比较重要的变化就是 这些功能催生了对异步队列的需求 。例如,微博私信需要跟外部门对接, 不同系统间的处理耗时和速度不一样,可以通过队列进行缓冲 ;群聊是 耗时操作,消息发到群后,可以通过异步队列来异步完成消息的扩散写 等等。

群聊写扩散

微信的群聊是写扩散的,也就是说发到群里的一条消息会给群里的每个人都存一份(消息索引)。为什么不是读扩散呢?有两个原因:

  1. 群的人数不多,群人数上限是10(后来逐步加到20、40、100,目前是500), 扩散的成本 不是太大,不像微博,有成千上万的粉丝,发一条微博后,每粉丝都存一份的话,一个是 效率太低 ,另一个 存储量 也会大很多;
  2. 消息扩散写到每个人的消息存储(消息收件箱)后,接收者到后台同步数据时,只需要检查自己收件箱即可,同步逻辑跟单聊消息是一致的,这样可以 统一数据同步流程 ,实现起来也会很轻量。

快速成长

微信的飞速发展是从2.0版开始的,这个版本发布了语音聊天功能。之后微信用户量急速增长,2011.5用户量破100万、2011.7 用户量破1000万、2012.3 注册用户数突破1亿。伴随着喜人成绩而来的,还有一堆幸福的烦恼。

  • 业务快速迭代的压力 在发语音之后的几个版本里迅速推出了手机通讯录、QQ离线消息、查看附近的人、摇一摇、漂流瓶和朋友圈等等功能。

有个广为流传的关于朋友圈开发的传奇——朋友圈历经4个月,前后做了30多个版本迭代才最终成型。其实还有一个鲜为人知的故事——那时候因为人员比较短缺,朋友圈后台长时间只有1位开发人员。 * 后台稳定性的要求

用户多了,功能也多了,后台模块数和机器量在不断翻番,紧跟着的还有各种故障。

帮助我们顺利度过这个阶段的,是以下几个举措:

  1. 极简设计

虽然各种需求扑面而来,但我们每个实现方案都是一丝不苟完成的。实现需求最大的困难不是设计出一个方案并实现出来,而是需要在若干个可能的方案中,甄选出最简单实用的那个。 这中间往往需要经过几轮思考——讨论——推翻的迭代过程,谋定而后动有不少好处,一方面可以避免做出华而不实的过度设计,提升效率;另一方面,通过详尽的讨论出来的看似简单的方案,细节考究,往往是可靠性最好的方案。

  1. 大系统小做

逻辑层的业务逻辑服务最早只有一个服务模块(我们称之为mmweb),囊括了所有提供给客户端访问的API,甚至还有一个完整的微信官网。这个模块架构类似Apache,由一个CGI容器(CGIHost)和若干CGI组成( 每个CGI即为一个API ),不同之处在于 每个CGI都是一个动态库so,由CGIHost动态加载

作者 张文瑞 发布于 2016年1月20日 | 6 讨论 分享到:微博微信FacebookTwitter有道云笔记邮件分享稍后阅读我的阅读清单 从无到有

2011.1.21 微信正式发布。这一天距离微信项目启动日约为2个月。就在这2个月里,微信从无到有,大家可能会好奇这期间微信后台做的最重要的事情是什么?

我想应该是以下三件事:

  1. 确定了微信的消息模型

微信起初定位是一个通讯工具,作为通讯工具最核心的功能是收发消息。微信团队源于广硏团队,消息模型跟邮箱的邮件模型也很有渊源,都是存储转发。

图 1 微信消息模型

图1展示了这一消息模型,消息被发出后,会先在后台临时存储;为使接收者能更快接收到消息,会推送消息通知给接收者;最后客户端主动到服务器收取消息。

  1. 制定了数据同步协议

由于用户的帐户、联系人和消息等数据都在服务器存储,如何将数据同步到客户端就成了很关键的问题。为简化协议,我们决定通过一个统一的数据同步协议来同步用户所有的基础数据。

最初的方案是客户端记录一个本地数据的快照(Snapshot),需要同步数据时,将Snapshot带到服务器,服务器通过计算Snapshot与服务器数据的差异,将差异数据发给客户端,客户端再保存差异数据完成同步。不过这个方案有两个问题:一是Snapshot会随着客户端数据的增多变得越来越大,同步时流量开销大;二是客户端每次同步都要计算Snapshot,会带来额外的性能开销和实现复杂度。

几经讨论后,方案改为由服务计算Snapshot,在客户端同步数据时跟随数据一起下发给客户端,客户端无需理解Snapshot,只需存储起来,在下次数据同步数据时带上即可。同时,Snapshot被设计得非常精简,是若干个Key-Value的组合,Key代表数据的类型,Value代表给到客户端的数据的最新版本号。Key有三个,分别代表:帐户数据、联系人和消息。这个同步协议的一个额外好处是客户端同步完数据后,不需要额外的ACK协议来确认数据收取成功,同样可以保证不会丢数据:只要客户端拿最新的Snapshot到服务器做数据同步,服务器即可确认上次数据已经成功同步完成,可以执行后续操作,例如清除暂存在服务的消息等等。

此后,精简方案、减少流量开销、尽量由服务器完成较复杂的业务逻辑、降低客户端实现的复杂度就作为重要的指导原则,持续影响着后续的微信设计开发。记得有个比较经典的案例是:我们在微信1.2版实现了群聊功能,但为了保证新旧版客户端间的群聊体验,我们通过服务器适配,让1.0版客户端也能参与群聊。

  1. 定型了后台架构

图 2 微信后台系统架构

微信后台使用三层架构:接入层、逻辑层和存储层。

接入层提供接入服务,包括长连接入服务和短连接入服务。长连接入服务同时支持客户端主动发起请求和服务器主动发起推送;短连接入服务则只支持客户端主动发起请求。 逻辑层包括业务逻辑服务和基础逻辑服务。业务逻辑服务封装了业务逻辑,是后台提供给微信客户端调用的API。基础逻辑服务则抽象了更底层和通用的业务逻辑,提供给业务逻辑服务访问。 存储层包括数据访问服务和数据存储服务。数据存储服务通过MySQL和SDB(广硏早期后台中广泛使用的Key-Table数据存储系统)等底层存储系统来持久化用户数据。数据访问服务适配并路由数据访问请求到不同的底层数据存储服务,面向逻辑层提供结构化的数据服务。比较特别的是,微信后台每一种不同类型的数据都使用单独的数据访问服务和数据存储服务,例如帐户、消息和联系人等等都是独立的。 微信后台主要使用C++。后台服务使用Svrkit框架搭建,服务之间通过同步RPC进行通讯。

图 3 Svrkit 框架

Svrkit是另一个广硏后台就已经存在的高性能RPC框架,当时尚未广泛使用,但在微信后台却大放异彩。作为微信后台基础设施中最重要的一部分,Svrkit这几年一直不断在进化。我们使用Svrkit构建了数以千计的服务模块,提供数万个服务接口,每天RPC调用次数达几十万亿次。

这三件事影响深远,乃至于5年后的今天,我们仍继续沿用最初的架构和协议,甚至还可以支持当初1.0版的微信客户端。

这里有一个经验教训——运营支撑系统真的很重要。第一个版本的微信后台是仓促完成的,当时只是完成了基础业务功能,并没有配套的业务数据统计等等。我们在开放注册后,一时间竟没有业务监控页面和数据曲线可以看,注册用户数是临时从数据库统计的,在线数是从日志里提取出来的,这些数据通过每个小时运行一次的脚本(这个脚本也是当天临时加的)统计出来,然后自动发邮件到邮件组。还有其他各种业务数据也通过邮件进行发布,可以说邮件是微信初期最重要的数据门户。

2011.1.21 当天最高并发在线数是 491,而今天这个数字是4亿。

小步慢跑

在微信发布后的4个多月里,我们经历了发布后火爆注册的惊喜,也经历了随后一直不温不火的困惑。

这一时期,微信做了很多旨在增加用户好友量,让用户聊得起来的功能。打通腾讯微博私信、群聊、工作邮箱、QQ/邮箱好友推荐等等。对于后台而言,比较重要的变化就是这些功能催生了对异步队列的需求。例如,微博私信需要跟外部门对接,不同系统间的处理耗时和速度不一样,可以通过队列进行缓冲;群聊是耗时操作,消息发到群后,可以通过异步队列来异步完成消息的扩散写等等。

图 4 单聊和群聊消息发送过程

图4是异步队列在群聊中的应用。微信的群聊是写扩散的,也就是说发到群里的一条消息会给群里的每个人都存一份(消息索引)。为什么不是读扩散呢?有两个原因:

群的人数不多,群人数上限是10(后来逐步加到20、40、100,目前是500),扩散的成本不是太大,不像微博,有成千上万的粉丝,发一条微博后,每粉丝都存一份的话,一个是效率太低,另一个存储量也会大很多; 消息扩散写到每个人的消息存储(消息收件箱)后,接收者到后台同步数据时,只需要检查自己收件箱即可,同步逻辑跟单聊消息是一致的,这样可以统一数据同步流程,实现起来也会很轻量。 异步队列作为后台数据交互的一种重要模式,成为了同步RPC服务调用之外的有力补充,在微信后台被大量使用。

快速成长

微信的飞速发展是从2.0版开始的,这个版本发布了语音聊天功能。之后微信用户量急速增长,2011.5用户量破100万、2011.7 用户量破1000万、2012.3 注册用户数突破1亿。

伴随着喜人成绩而来的,还有一堆幸福的烦恼。

业务快速迭代的压力 微信发布时功能很简单,主要功能就是发消息。不过在发语音之后的几个版本里迅速推出了手机通讯录、QQ离线消息、查看附近的人、摇一摇、漂流瓶和朋友圈等等功能。

有个广为流传的关于朋友圈开发的传奇——朋友圈历经4个月,前后做了30多个版本迭代才最终成型。其实还有一个鲜为人知的故事——那时候因为人员比较短缺,朋友圈后台长时间只有1位开发人员。

后台稳定性的要求 用户多了,功能也多了,后台模块数和机器量在不断翻番,紧跟着的还有各种故障。

帮助我们顺利度过这个阶段的,是以下几个举措:

  1. 极简设计

虽然各种需求扑面而来,但我们每个实现方案都是一丝不苟完成的。实现需求最大的困难不是设计出一个方案并实现出来,而是需要在若干个可能的方案中,甄选出最简单实用的那个。

这中间往往需要经过几轮思考——讨论——推翻的迭代过程,谋定而后动有不少好处,一方面可以避免做出华而不实的过度设计,提升效率;另一方面,通过详尽的讨论出来的看似简单的方案,细节考究,往往是可靠性最好的方案。

  1. 大系统小做

逻辑层的业务逻辑服务最早只有一个服务模块(我们称之为mmweb),囊括了所有提供给客户端访问的API,甚至还有一个完整的微信官网。这个模块架构类似Apache,由一个CGI容器(CGIHost)和若干CGI组成(每个CGI即为一个API),不同之处在于每个CGI都是一个动态库so,由CGIHost动态加载。

在mmweb的CGI数量相对较少的时候,这个模块的架构完全能满足要求,但当功能迭代加快,CGI量不断增多之后,开始出现问题:

  1. 每个CGI都是动态库,在某些CGI的共用逻辑的接口定义发生变化时, 不同时期更新上线的CGI可能使用了不同版本的逻辑接口定义 ,会导致在运行时出现诡异结果或者进程crash,而且非常难以定位;
  2. 所有CGI放在一起,每次大版本发布上线,从 测试到灰度再到全面部署完毕,都是一个很漫长的过程 ,几乎所有后台开发人员都会被同时卡在这个环节,非常影响效率;
  3. 新增的不太重要的CGI有时稳定性不好,某些异常分支下会 crash ,导致 CGIHost进程无法服务 ,发消息这些重要CGI受影响没法运行。

于是我们开始尝试使用一种新的CGI架构——Logicsvr。将Svrkit框架和CGI逻辑通过静态编译生成 可直接使用HTTP访问的Logicsvr 。我们将mmweb模块 拆分为8个不同服务模块 。拆分原则是: 实现不同业务功能 的CGI被拆到不同Logicsvr, 同一功能但是重要程度不一样 的也进行拆分。例如,作为核心功能的消息收发逻辑,就被拆为3个服务模块:消息同步、发文本和语音消息、发图片和视频消息。

每个Logicsvr都是一个独立的二进制程序,可以 分开部署 、独立上线。

  1. 业务监控

这一时期,后台故障很多。比故障更麻烦的是,因为 监控的缺失 ,经常有些故障我们没法第一时间发现,造成故障影响面被放大。

我们去公司内的标杆——即通后台(QQ后台)取经了,发现解决方案出乎意料地简单且强大:

  1. 故障报告

开发人员需要彻底复盘故障过程,然后商定解决方案,补充出一份详细的技术报告。这份报告侧重于:如何避免同类型故障再次发生、提高故障主动发现能力、缩短故障响应和处理过程。

  1. 基于 ID-Value 的 业务无关的监控告警体系

监控体系实现思路非常简单,提供了2个API,允许业务代码 在共享内存中对某个监控ID进行设置Value或累加Value的功能 。 每台机器上的Agent会定时将所有ID-Value上报到 监控中心 ,监控中心对数据汇总入库后就可以通过统一的监控页面输出监控曲线,并 通过预先配置的监控规则 产生报警。 QUESTION: 对曲线的监控规则是什么?

新开发的功能也会预先加入相关监控项,以便 在少量灰度阶段就能直接通过监控曲线了解业务是否符合预期

  1. KVSvr

微信后台每个存储服务都有自己独立的存储模块,是相互独立的。 每个存储服务都有 一个业务访问模块和一个底层存储模块 组成。 业务访问层隔离业务逻辑层和底层存储,提供基于RPC的数据访问接口 ;底层存储有两类:SDB和MySQL。 QUESTION: 没太懂。

因此对一致性要求高的业务逻辑,例如注册和登录逻辑只允许访问Master。当Master有故障时,同样只能提供读服务,无法提供写服务。

最后还有个未解决的问题:单个Master-Slave分组中, Master还是单点,无法提供实时的写容灾,也就意味着无法消除单点故障 。另外Master-Slave的流水同步延时对读服务有很大影响,流水出现较大延时会导致业务故障。于是我们寻求一个可以提供高性能、具备读写水平扩展、没有单点故障、可同时具备读写容灾能力、能提供强一致性保证的底层存储解决方案,最终KVSvr应运而生。

KVSvr使用基于Quorum的分布式数据强一致性算法,提供 Key-Value/Key-Table 模型的存储服务。传统Quorum算法的性能不高,KVSvr创造性地 将数据的版本和数据本身做了区分 ,将Quorum算法应用到数据的版本的协商,再通过基于流水同步的异步数据复制提供了数据强一致性保证和极高的数据写入性能,另外KVSvr天然具备数据的Cache能力,可以提供高效的读取性能。

平台化

2011.8 深圳举行大运会。微信推出“微信深圳大运志愿者服务中心”服务号,微信用户可以搜索“szdy”将这个服务号加为好友,获取大会相关的资讯。 当时后台对“szdy”做了特殊处理,用户搜索时,会随机返回“szdy01”,“szdy02”,…,“szdy10”这10个微信号中的1个,每个微信号背后都有一个志愿者在服务。

2011.9 “微成都”落户微信平台,微信用户可以搜索“wechengdu”加好友,成都市民还可以在“附近的人”看到这个号,我们在后台给这个帐号做了一些特殊逻辑,可以支持后台 自动回复用户发的消息

这种需求越来越多,我们就开始做一个媒体平台,这个平台后来从微信后台分出,演变成了微信公众平台,独立发展壮大,开始了微信的平台化之路。除微信公众平台外,微信后台的外围还陆续出现了微信支付平台、硬件平台等等一系列平台。

QUESTION: 好奇是否可以做到多个客服服务一个微信公众号?

走出国门

  1. 防雪崩

最担心的是雪崩:某个服务因为某些原因出现过载,导致请求处理时间被大大拉长。于是服务吞吐量下降,大量请求积压在服务的请求队列太长时间了,导致访问这个服务的上游服务出现超时。更倒霉的是上游服务还经常会重试,然后这个过载的服务仅有的一点处理能力都在做无用功(即处理完毕返回结果时,调用端都已超时放弃),终于这个过载的服务彻底雪崩了。最糟糕的情况是上游服务每个请求都耗时那么久,雪崩顺着RPC调用链一级级往上传播,最终单个服务模块的过载会引发大批服务模块的雪崩。

我们在一番勒紧裤腰带节省机器资源、消灭低负载机器后,所有机器的负载都上来了,服务过载变得经常发生了。 解决这一问题的有力武器是Svrkit框架里的具有QoS保障的FastReject机制,可以快速拒绝掉超过服务自身处理能力的请求,即使在过载时,也能稳定地提供有效输出。

  1. 安全加固

近年,互联网安全事件时有发生,各种拖库层出不穷。为保护用户的隐私数据,我们建设了一套数据保护系统——全程票据系统。其核心方案是, 用户登录后,后台会下发一个票据给客户端,客户端每次请求带上票据,请求在后台服务的整个处理链条中,所有对核心数据服务的访问,都会被校验票据是否合法,非法请求会被拒绝,从而保障用户隐私数据只能用户通过自己的客户端发起操作来访问。

QUESTION: 票据和 token 的区别是?核心数据的定义,是需要登录才能访问/提交的数据,还是任意读写数据?校验是否合法,是怎么校验的?