微信使用的越来越广泛,我们来聊一聊如何设计微信后台架构。我们从三个方面入手:
-
how to communicate? (网络层)
-
how to store data? (数据层)
-
how to build API? (逻辑层)
1. how to communicate
1) how to communicate between two users?
微信最基本的功能是聊天,本质是两个用户的沟通。如何连接互联网上的两个用户?
有两个用户user 1和user 2希望互相通信,user 1如何知道user 2在哪儿呢?传统的TCP连接需要IP地址。但在网络user 1如何知道user 2的地址呢?我们可以引入server,做为两个用户约会时的见面地点。试想最开始的时候,client和client之间可以直接连接,而不需要其他媒介。但是随着client的增加,我们需要一个共享数据的中转站,而server能够帮助client来共享数据和资源。比如DNS server帮client识别各种URL对应的IP地址,communications server能够帮助user或者设备终端进行通信。
(详见wikipedia: https://en.wikipedia.org/wiki/Server_(computing))
我们一定要用server吗?不一定,我们可以用P2P模式。P2P (peer-to-peer) 模式把任务和负载分给一个个peer(网络上的节点,也是p2p网络形成的基本单位,每一个peer都有独特的peer ID)。不同于client-server模式,P2P中的每个peer不仅是资源的提供者,也是资源的消耗者。换言之,每个节点既是client,又是server。而在client-server的集中式的管理模式中,资源的消费和提供者往往是分开的。
有两种群消息分发模式:一种是pull,一种是push。Push模式在收到一个用户发出的消息后,会将消息直接扩散到每个群用户的消息列表中。Pull模式在收到一个用户发出的消息后,会将消息保存在频道的列表中,因此每个群用户需要主动进入这个消息列表中拉去自己需要的消息。
微信使用的是第二种方式,它的缺点是当群用户数量大的时候,开销较大,幸运的是微信群是有人数上限的。
2. how to store data?
1) what data need to be saved?
微信有三种核心数据:accounts/contacts/messages。每一种都可以使用单独的方式保存。
那么对应每种数据,使用SQL存储还是NoSQL存储?实际上,绝大部分数据既能使用SQL,也能使用NoSQL。对于account信息,可以选择MySQL,因为较为稳定;对于contacts和messages,可以使用NoSQL存储。
2) why do we need memory?
Disk容量大,持久保存,但是因为读写涉及到磁盘的机械运动而速度较慢。因此我们引入memory来进行补足,它缓存了我们需要频繁访问的数据信息,因此当我们进行较复杂的join类操作时可以直接从memory中取数据,性能能够提升10倍以上。现在的工业界,1TB的数据基本都可以在memory中解决。
3) is one data center enough?
一个数据中心是不够的,原因不在于数据量大,而在于多数据中心的容灾(例如挖断光缆),和数据的就近访问。
一个架构是有三个数据中心,每个中心都有全数据,承接2/3的负载。当一个数据中心不能正常工作了,它的负载会转移到另两个数据中心。
4) how to monitor the number of registered/online users?
注册用户可以直接在MySQL中count。在线用户可以通过log,上线+1,下线-1。这里需要注意的是用户超时退出的情况
5) how to connect?
如果一个用户知道了另一个用户的信息,他们是如何建立连接的呢?现实中我们可以发邮箱,打电话在网络中,发短信对应短连接(short connection),打电话对应长连接(persistent connection)。前者在每次联系的往返都要建立一次连接,而后者可以持续一个较长时间的连接。
“长连接: 指在一个TCP连接上可以连续发送多个数据包,在TCP连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接; 一般需要自己做在线维持。 短连接: 指通信双方有数据交互时,就建立一个TCP连接,数据发送完成后,则断开此TCP连接。”
通常的短连接操作步骤是:
连接→数据传输→关闭连接;
而长连接通常就是:
连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接;
(参考:http://blog.chinaunix.net/uid-26000296-id-3758651.html)
6) how to save traffic?
长连接可以节省建立链接的流量,除此之外我们还能通过批量发送和数据压缩来节省流量。
批量发送是指将一堆信息一起打包传输。这个在消息特别频繁的时候较为有用。
P2P模式也能够省服务器流量。因为P2P可以在client之间传递信息。随着我们的带宽环境越来越好,P2P省的流量越来越不显著,而它不稳定的缺点却越发明显,所以我们可以感觉到P2P并没有之前那么火。
推拉结合的模式(pull-push)也能省流量。当A给B发送消息,B可能无需立刻读取,比如他正在开会。那么只需提醒B有新消息,而这些消息不用立刻发给B。在实际的微信中,头像右上方常常有个小红圈显示有多少个新消息,这些新消息并没有存在client端,只有当用户打开微信读取的时候,消息才实际传输过来。此过程中B就是在“拉取”信息;因此我们推的只是消息数量,而之后再拉去消息。这种推拉模式下,数据很容易被批量打包。
7) what is sync between users and servers?
client和server需要同步什么信息?微信最基本的是account,message和contact。如何保持数据的同步呢?比如微信有手机版和网页版,用户手机里新添加了一个联系人,他希望网页版也能看到这个联系人信息。
解决这个问题,需要针对数据标记timestamp,从而能够得知本地数据是否过期。但是不同的设备的timestamp也许是不准确的(设备本身的时间也许不准)。因此可以使用逻辑时钟。逻辑时钟相当于版本号,v1、v2、v3……。
3. how to build API?
把整个架构理解为一座冰山,API是浮在水面上的一角,系统通过它与用户沟通。
1) how to handle a user's request?
我们要思考,clientr会有哪些请求呢?例如:
(1)send message
(2)edit account
(3)read message
(4) Red packet
(5)log in/out
(6)moments
可以分类为聊天相关、账号相关、feed流相关。
Feed火于rss年代,google reader通过读一个网站的rss信息,获得该页面是否更新,rss里产生的消息叫做feed。
2) how to write less code?
作为程序员,我们希望能够用简约的代码实现丰富的功能,这使我们想到代码的模块化复用。也与OOD的思想相通。
API可以分层:业务层API(用户能访问到的层,比如修改备注等),基础API(提供更抽象的功能)。基础API有两类:基础逻辑API,Data API。Data API为了能更好的处理data,比如有memory cache等等。在微信里,基础逻辑API和Data API是并行的。如何考虑两个层是并行还是串行的呢?从业务API来看,如果它既能够访问逻辑API,又能访问Data API,那么这两层是并行的,反之可以是串行的。究竟是并行好还是串行好,这里并没有一个最好的方法,只有最适合的方法。
回到冰山的例子。API是上面显露的部分,水中的部分是一个个logic module。在传统的CGI模式中,有一个container包含所有ogic module。当用户通过API调用数据时,container决定哪个logic module处理这个请求。
这个模式有很多缺点,包括单点故障,连带效应,耦合等等。比如说一旦container出现问题,整个系统将不能正常工作,或者某个模块出现故障,整个container也可能崩溃。
解决这些问题的基本想法是拆解,降低它们的耦合度。micro-service就是基于这个思想,降低模块之间的关联度一个故障并不影响其他模块。每个模块之间可以通信。并我们可以有一个master掌管这些模块,也可以有很多个master。有的负责聊天相关的功能,有的负责图片相关的功能。这和数据库里的shard思想也有些神似。
3) how to log
对于程序运行时的log统计,是个辛苦的工作,尤其是在分布式的环境中。
微信搭建了一套分布式的log框架,在每个机器有一个进程统计数据,比如error count。机器里的其他进程都能通过简单的API往这个进程写数据,比如error count++。这样只需要一行代码,就实现了log的统计。这些log进程再定期往一个master里传递数据即可。从而大大简化了分布式系统的debug工作。
由于时间关系,我们无法设计所有技术细节,只能提供一个大体的思路。但正如上一次talk show里所说,技术是由需求和瓶颈驱动的。在设计系统时,我们要考虑到用户的需求是什么,如何满足这些需求,有什么更好的解法从而一步步找到适合我们的答案。