设计微信的聊天系统

微信 chat design

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等等等等. 先写到这!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,793评论 25 707
  • 第二次服靶向药的时间:3月12日,植树节。距今天已经三周了。升血小板的药停了,上次住院抽血检查的血小板计数大约是2...
    栀言片语阅读 264评论 1 2
  • 柳摇因风起,水浊源浪故。 心若遇风浪,清静归何处?
    鼓楼和鱼pandada阅读 149评论 0 0
  • 很多人说女人谈恋爱的时候智商为零,不其然,恰恰相反恋爱中的女人智商情商双飙。 很多看似很傻很天真的话那也是在心理想...
    赖七钱阅读 242评论 1 1