Intro
一开始是被邀回答这个问题, 如果好设计微信, 需要学哪些技术? 我觉得时间比空口罗列技术关键词要稍微有用一点, 于是花了1:45小时写了这篇设计. 从一个小的突破口, 从最基础的需求出发来设计一下微信聊天的功能.
开一开脑洞的同时, 没想到还让我琢磨出了几种微信现有的问题/限制: 无法云端备份聊天记录, 微信群不能超过500人等等. 我认为是最初设计系统的时候有一些无法scale的缺陷, 那么导致了现在要花很大的人力和金钱去重改, 所以还没有被当做第一要务吧!
让我们开始, 一个大前提:
Client side message可以简单地通过P2P来实现, 比如使用socket.io. 但是我们这里考虑的是造一个微信, 就要将可能考虑到的全流程都涉及到. 这里假设我们的message不是通过client-client P2P实现的, 而是通过客户端-> server -> 客户端实现的, 那么就可以用sendMessage这个例子来介绍一下系统设计.
Scoping
- 需要做微信的什么功能? [我们假设就是要实现chat这个功能]
- 这个功能中间有多少个小的模块需要考虑? [发送信息, 存储信息, 读取信息]
- 这些小功能如果用最简单的方式, 大概需要哪些技术模块实现? [User Interface, Web Server, Backend Service, Data Storage, Notification model]
- 假设有多少用户会用? [假设每天有1K用户, 1 million 用户, 1 billion 用户时不同的情况]
- Deployment: 这个产品如何到用户手里? [根据个人经验, 假如说是web端好了]
- Next step: determine database based on call pattern, scaling, caching ...
Workflow
design的第一步, 都是要以最简单明了的方式, 把需要的功能实现了: 先考虑,就2个人需要chat, 看是能怎么做?
根据上面的回答的那些问题, 把每一个环节写下来.
想象一下, 你是userA, 你的女朋友是userB. 不要问为什么你是userA而女朋友是userB, 按照管理, 程序员绝大比例是单身男 , 这里让你有一次女朋友吧!
Workflow1
你发送信息
=> Request传到了WebServer
=> Request 传到BackendService
=> 信息存储在Database, 同时发送notification
=> 女朋友 的手机端不断地在poll notification, 并且收到notification
=> 取决于这个notification里面是否包括chat的内容, 女朋友可能再向 WebServer,Service, Database request信息的具体内容
如果女朋友心情好, 选择回复, 那么重复以上动作
Workflow2
女朋友心血来潮, 看你手机记录, 在app里面向上找chat history, 滑动一页
=> 这个request传到 WebServer
=> 找到相应的Backend Service
=> 根据时间或者其他什么分页方式, 从Database读取上一页的chat history
=> Backend Service
=> Web Server
=> 获取的信息传送回到女朋友这里, 看到你半夜找朋友吃鸡的记录
如果女朋友心情不好, 那么你就呵呵了.
Design Details
User Interface
UI固然非常重要, 但是在设计初期, 不必要全身心掉入UI的设计和选择中, 基本上需要考虑的一些点, 记下来就可以. 比如:
选择Angular做前端的controller, view.
选择Bootstrap来润色UI element
用Angular本身的testing framework来做testing
差不多到此为止, 下面去关注跟重要的部分.
注意: 在client端, 可能本地会运行一个小的server, 不断地poll notifications:
这里可以用到一个AWS SQS的技术, 不断地对某一个queue读取, 看有没有发给自己的notification.
Web Server
我们要做一个chat的工具, 所以可以预料到:
同一个server上因为大量的user会经过大量的I/O
server上面最重要的不过是把信息来回传递, 并不需要做很多业务信息的处理
基于这两点, 我们可以暂且选用nodeJS: node的长处在于非常快的I/O 可以快速handle非常多的request.
另外的好处: node和前端都是些javascript, 在做起来的时候不用switch context太多
这一步可以稍微涉及一下API:
put sendMessage(userA, userB, message): send message from A to B
get getMessage(userA, timestamp, pageNum): based on timestamp and page num, read historical messages
deleteMessage(messageId): remove a message from database
Backend Service
Service的选择也可以有很多, 但为了方便理解, 我们这里也选用nodejs.
需要一个backend service有security的因素. 在这一步, 你的service真的在和database交流, 而这时候会用到很多access credential, 而这些最好都是在墙内的(不和真正的外界user接触, 也不会expose给外界).
上面提到的web server会把每个request都传到service来, 这中间会通过一道道防火墙和security check, 确保安全.
在service里面, 我们会有API的mapping, 比如:
put sendMessage(userA, userB, message): send message from A to B.
- 把数据存储到database
- send SNS/SQS notification, 然后user会被notify
get getMessage(userA, timestamp, pageNum): based on timestamp and page num, read historical messages
- 从database里面根据request的信息, 读取之前存储的message
deleteMessage(messageId): remove a message from database
- 从database里面根据messageId, 删除信息
Data Storage
我们选怎么样的data storage呢? 有传统的Sql database, 也有流行的non-sql database.
这里其实两种都可以. 我们姑且将这个table命名为 MessageTable 我们在database里面很可能是用message id来存储单个信息entry:
- messageId: string
- message: string
- sender: string
- receiver: string
- timestamp: date
写入database好像比较简单.
那么我们要支持哪些种读取呢? 比如:
女朋友读取你和她之间前10分钟的数据: 需要 index on sender, receiver, timestamp
根据messageId 删除entire message entry: 因为messageId是primaryKey, 直接用它删就好了.
其他一些微信里面可能有的功能:
找到所有提到'吵架'的message: index on message
MessageTable 主要需要的一些功能就是以上, 但每个API的使用频率可能不同, 排列一下:
写入: 对应sendMessage, 相对是最多的
读取: 对应getMessage, 比write应该少点, 你的女朋友不会一直不断地翻记录, 手会累, 多数还是发信息.
删除(其实也是write): 对应deleteMessage, 相对少一点
搜索: 可能是index on message, 相对少一点
根据不同的call pattern, 我们在设计service的时候, 可能就会有轻重缓急的不同来分布这些API traffic. 比如: writeAPI被用的最多最多, 那么我们可能给这个service多一些box.
Notification Model
上面提到了我们可以用AWS SNS/SQS的方式来实现notification.
这里可以解释几点:
- notification model的最终原理, 其实都是有个server在一个端口不断地polling(), 也就是说我们的客户端在不断地问邮局: 有我的信件嘛, 有我的信件嘛, 永不停止.
- 并不一定要用AWS的服务, 其他的也可以实现, 这里说SQS方便解释.
注意: 为什么要用queue呢?
- 后到的message, 后处理; 先到的message, 应该先到用户那里
- 得到了notification了以后, 需要把这个message从queue里面删除掉, 也是queue的原理
Deployment
这里不只是说你的APP怎么到用户那里呢: app store, 或者网页access; 这里更多是说, 如果的有更新, 那么怎么到用户那里?
我们会用到一个pipeline的概念: 每一个stage都应该有不同的testing, 打个比方, 吃饭要吃: 凉菜, 热菜, 汤.
凉菜: 在test environment里面, 这里链接的都是test domain的web server, service, 和内部的测试用户.
热菜: 这里是跟production environment 一样了, 所有的dependency也都是在production, 然后你去测试你的APP.
汤: 这是最后的阶段,也是public accessible 的那个stage:在这个环境里面的APP, 用户就可以用到了.
你需要借助一些已有的host/deployment工具来推送和测试你的代码.
比较简单常用的一个服务器网站叫做Heroku, 是SalesForce下的一个服务; 当然AWS也有一些列的host/server服务, 也可以使用.
More and More
到这一步, 好像全部做完了嘛! 你和女朋友终于可以在你写的微信上面聊天了!
问题1
你开心地邀请了你的朋友一起加入, 那么问题来了:
虽然你是一个程序员, 但是你的女朋友是交际花, 突然一夜之间来了1000个朋友加入了你的微信服务器, 你开始感受到延迟; 第二天晚上, 突然有了1 million个用户加入, 你的服务器瞬间爆炸, 宕机了. 你该怎么办?
第一个手段无非是: 再买几个个box 来handle requests, 同时扩大你的database read/write capacity.
这样scaling好像能够减轻一点压力, 但是很快又不行了, 当第二个million, 第三个million朋友来的时候, 你发现这些人又不给钱, 所以你买不起服务器了, 女朋友要难过伤心了!!!
这时候怎么办?
前面提到了, 每种不同的操作, 有自己的重要新, 比如sendMessage()就非常非常多用和重要, 而read historical message就没所谓; 而同时, notifiction也是非常非常核心.
回到design的初期, 我们可以选择分流, 开两个service:
WriteMessageService: 往上面买100个服务器
ReadMessageService: 只买50个服务器, 够用就好了
过去你可能总共需要200个服务器, 因为所有的traffic混在一起, 加大了每个服务器的平均负荷. 而现在减少成了总共150个服务器, 省下了资金, 也可以继续维持你的微信运营, 女朋友又对你笑了, 很高兴很幸福啊!
问题2
不久之后, 你突然发现, 你当初只用了1个database instance, 但是现在你有了10 million, 一个database的读/写完全没有办法支持, 也就是说, 很多read/write message都在跟database交互的时候出错没有了. 一半以上的用户感受到了大幅度延迟和发送失误, 产生不满, 你的女朋友的手机也无法发送了, 感到非常气愤. 这时候怎么办?
再加上5个database instance吧, 让来往的traffic去不同的database读写好不好?
这里有两种情况可以考虑:
- 将5个database变成各自的replication. 这样读起来可能方便了: 用load balancer 把request分配去不同的database读; 但是这里有个问题: 你写的时候怎么办!? 每次要同时写到5个地方, 速度不一定一样, 而且复制也可能在network里失败断掉, 那么用户每次读写就不consistent. 对于我们这个注重读写的APP, 这样的分布不行; 如果是写的快慢和consistency不重要, 但是读的需求很大, 才可能用这个模式.
- 另一个方法: 将5个database分成5分, 每一个database承载一部分的用户, 而且永远承载这些用户. 这里可以用用户的名字做个hash, 最后hash的结果来判断存去哪个database. 当然啦, 每次在选择database的时候, 可能要多一个判断, 根据用户的id, 去不同的database存取.
问题3
这里还引出了又一个问题: 我们的message是不是应该跟着用户走? 也就是说, 我们需要把所有跟某个用户相关的message, 全部复制一遍. 那么实际上微信这么做么?
过去在用QQ的时候, 有个漫游设置, 现在分析开来, 也就是根据某个用户个人的需求, 将他所有的message 漫游, 根据他的messageID 跟着人, 存到同一个database里面.
而微信貌似没有做这样的操作: 所有的message好像都是在local, 如果换手机, 并且不转移message, 那么message就全部丢失了.
我可以理解微信不做漫游message: 因为那么多亿人, 没一个人, 就存一个他的version of chat history, 这样可能太过费劲了. 当然, 并不是说解决不了, 但可能并没有巨大的需求, 所以没有去实现, 可以理解.
虽然微信可能没有在云端做这个getMessage()的服务, 而是在本地读手机, 但并不是说我们上面的设计都白费了. 我确定, 微信可能会是暂时存储一定量的信息, 比如:
'最近/尚未签收'的信息: 换手机, 上一条微信在第一个手机上还没有打开看的, 在第二个手机上依然受到了新信息.
又或者说, 你1000个朋友同时每个人给你发了100条短信; 假设你的手机是10年前的诺基亚, 只有32MB的容量, 那么100k个短信会让你手机爆掉吧;如果没有, 那么这些信息可能存在某个临时数据库, 而不在你的手机上.
(wait, 难道微信不存在数据库而是直接强行塞到你手机里? .... 好危险哈哈哈...不可能的啦)
你的手机应该就是不断地去poll()message, 然后给你发个message count, 而message本身, 还要重新去read. 这里有几个可能的步骤:
- queue里面的message严格要求签收, 如果不签收, 不会删除
- 在一定时间里面 (1 week) 在云端存取未读信息, 比如说某个地方有个24 hours cache
一旦过期, 这些信息就被自动删除. 而在期限内读, 就可以顺利拿到, 并且存一个local copy.
这样想, 是不是我们看的一些视频或者照片, 过了很久之后, 就打不开了, 说过期了呀? 我猜就是这个原因.
再重申一下, 为什么会需要过期:
- 内容太大, 并不是非常多人一直去read history
- 根据我们粗略的设计, database分布的时候, 这些数据要跟着user存储的地方, 被完全复制一遍, 不合理 (当然啦, 这个naive的设计导致了这个结果, 其实是有很多办法拆分和优化的, 可以有效率的实现)
这里提到了Cache或者是临时database.
Cache是自然而然的过期, 删除.
如果支持TTL的database, 也是可以将数据自动过期删除的. TTL: time to live
其他问题:
还有很多其他问题可以考虑:
- calculate 具体的read/write, API volume来决定box的数量
- 根据跟具体的requirement来细化database数据的分布和access pattern
- 如何handle traffic monitoring, 采取什么样的action 等等
结束语
做一个粗略design就是这么high. 写完这些, 大概耗时1个小时45分钟.
这个design能不能用呢? 我觉得实现你和女朋友的单方面沟通, 是绰绰有余的, 但是思考的过程中已经发现了非常多的漏洞和可以用actual use case填补的地方. 真的要给1million个朋友用, 估计够呛: 我们巧妙地忽略了UX的设计, 和PM的斗争, 无穷无尽的Testing等等等等. 先写到这!