設(shè)計(jì)微信的聊天系統(tǒng)

微信 chat design

Intro

一開始是被邀回答這個(gè)問(wèn)題, 如果好設(shè)計(jì)微信, 需要學(xué)哪些技術(shù)? 我覺得時(shí)間比空口羅列技術(shù)關(guān)鍵詞要稍微有用一點(diǎn), 于是花了1:45小時(shí)寫了這篇設(shè)計(jì). 從一個(gè)小的突破口, 從最基礎(chǔ)的需求出發(fā)來(lái)設(shè)計(jì)一下微信聊天的功能.

開一開腦洞的同時(shí), 沒想到還讓我琢磨出了幾種微信現(xiàn)有的問(wèn)題/限制: 無(wú)法云端備份聊天記錄, 微信群不能超過(guò)500人等等. 我認(rèn)為是最初設(shè)計(jì)系統(tǒng)的時(shí)候有一些無(wú)法scale的缺陷, 那么導(dǎo)致了現(xiàn)在要花很大的人力和金錢去重改, 所以還沒有被當(dāng)做第一要?jiǎng)?wù)吧!

讓我們開始, 一個(gè)大前提:

Client side message可以簡(jiǎn)單地通過(guò)P2P來(lái)實(shí)現(xiàn), 比如使用socket.io. 但是我們這里考慮的是造一個(gè)微信, 就要將可能考慮到的全流程都涉及到. 這里假設(shè)我們的message不是通過(guò)client-client P2P實(shí)現(xiàn)的, 而是通過(guò)客戶端-> server -> 客戶端實(shí)現(xiàn)的, 那么就可以用sendMessage這個(gè)例子來(lái)介紹一下系統(tǒng)設(shè)計(jì).

Scoping

  • 需要做微信的什么功能? [我們假設(shè)就是要實(shí)現(xiàn)chat這個(gè)功能]
  • 這個(gè)功能中間有多少個(gè)小的模塊需要考慮? [發(fā)送信息, 存儲(chǔ)信息, 讀取信息]
  • 這些小功能如果用最簡(jiǎn)單的方式, 大概需要哪些技術(shù)模塊實(shí)現(xiàn)? [User Interface, Web Server, Backend Service, Data Storage, Notification model]
  • 假設(shè)有多少用戶會(huì)用? [假設(shè)每天有1K用戶, 1 million 用戶, 1 billion 用戶時(shí)不同的情況]
  • Deployment: 這個(gè)產(chǎn)品如何到用戶手里? [根據(jù)個(gè)人經(jīng)驗(yàn), 假如說(shuō)是web端好了]
  • Next step: determine database based on call pattern, scaling, caching ...

Workflow

design的第一步, 都是要以最簡(jiǎn)單明了的方式, 把需要的功能實(shí)現(xiàn)了: 先考慮,就2個(gè)人需要chat, 看是能怎么做?

根據(jù)上面的回答的那些問(wèn)題, 把每一個(gè)環(huán)節(jié)寫下來(lái).

想象一下, 你是userA, 你的女朋友是userB. 不要問(wèn)為什么你是userA而女朋友是userB, 按照管理, 程序員絕大比例是單身男 , 這里讓你有一次女朋友吧!

Workflow1

你發(fā)送信息

=> Request傳到了WebServer

=> Request 傳到BackendService

=> 信息存儲(chǔ)在Database, 同時(shí)發(fā)送notification

=> 女朋友 的手機(jī)端不斷地在poll notification, 并且收到notification

=> 取決于這個(gè)notification里面是否包括chat的內(nèi)容, 女朋友可能再向 WebServer,Service, Database request信息的具體內(nèi)容

如果女朋友心情好, 選擇回復(fù), 那么重復(fù)以上動(dòng)作

Workflow2

女朋友心血來(lái)潮, 看你手機(jī)記錄, 在app里面向上找chat history, 滑動(dòng)一頁(yè)

=> 這個(gè)request傳到 WebServer

=> 找到相應(yīng)的Backend Service

=> 根據(jù)時(shí)間或者其他什么分頁(yè)方式, 從Database讀取上一頁(yè)的chat history

=> Backend Service

=> Web Server

=> 獲取的信息傳送回到女朋友這里, 看到你半夜找朋友吃雞的記錄

如果女朋友心情不好, 那么你就呵呵了.

Design Details

User Interface

UI固然非常重要, 但是在設(shè)計(jì)初期, 不必要全身心掉入U(xiǎn)I的設(shè)計(jì)和選擇中, 基本上需要考慮的一些點(diǎn), 記下來(lái)就可以. 比如:

選擇Angular做前端的controller, view.

選擇Bootstrap來(lái)潤(rùn)色UI element

用Angular本身的testing framework來(lái)做testing

差不多到此為止, 下面去關(guān)注跟重要的部分.

注意: 在client端, 可能本地會(huì)運(yùn)行一個(gè)小的server, 不斷地poll notifications:

這里可以用到一個(gè)AWS SQS的技術(shù), 不斷地對(duì)某一個(gè)queue讀取, 看有沒有發(fā)給自己的notification.

Web Server

我們要做一個(gè)chat的工具, 所以可以預(yù)料到:

同一個(gè)server上因?yàn)榇罅康膗ser會(huì)經(jīng)過(guò)大量的I/O

server上面最重要的不過(guò)是把信息來(lái)回傳遞, 并不需要做很多業(yè)務(wù)信息的處理

基于這兩點(diǎn), 我們可以暫且選用nodeJS: node的長(zhǎng)處在于非常快的I/O 可以快速handle非常多的request.

另外的好處: node和前端都是些javascript, 在做起來(lái)的時(shí)候不用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.

需要一個(gè)backend service有security的因素. 在這一步, 你的service真的在和database交流, 而這時(shí)候會(huì)用到很多access credential, 而這些最好都是在墻內(nèi)的(不和真正的外界user接觸, 也不會(huì)expose給外界).

上面提到的web server會(huì)把每個(gè)request都傳到service來(lái), 這中間會(huì)通過(guò)一道道防火墻和security check, 確保安全.

在service里面, 我們會(huì)有API的mapping, 比如:

put sendMessage(userA, userB, message): send message from A to B.

  • 把數(shù)據(jù)存儲(chǔ)到database
  • send SNS/SQS notification, 然后user會(huì)被notify

get getMessage(userA, timestamp, pageNum): based on timestamp and page num, read historical messages

  • 從database里面根據(jù)request的信息, 讀取之前存儲(chǔ)的message

deleteMessage(messageId): remove a message from database

  • 從database里面根據(jù)messageId, 刪除信息

Data Storage

我們選怎么樣的data storage呢? 有傳統(tǒng)的Sql database, 也有流行的non-sql database.

這里其實(shí)兩種都可以. 我們姑且將這個(gè)table命名為 MessageTable 我們?cè)赿atabase里面很可能是用message id來(lái)存儲(chǔ)單個(gè)信息entry:

  • messageId: string
  • message: string
  • sender: string
  • receiver: string
  • timestamp: date

寫入database好像比較簡(jiǎn)單.

那么我們要支持哪些種讀取呢? 比如:

女朋友讀取你和她之間前10分鐘的數(shù)據(jù): 需要 index on sender, receiver, timestamp

根據(jù)messageId 刪除entire message entry: 因?yàn)閙essageId是primaryKey, 直接用它刪就好了.

其他一些微信里面可能有的功能:

找到所有提到'吵架'的message: index on message

MessageTable 主要需要的一些功能就是以上, 但每個(gè)API的使用頻率可能不同, 排列一下:

寫入: 對(duì)應(yīng)sendMessage, 相對(duì)是最多的

讀取: 對(duì)應(yīng)getMessage, 比write應(yīng)該少點(diǎn), 你的女朋友不會(huì)一直不斷地翻記錄, 手會(huì)累, 多數(shù)還是發(fā)信息.

刪除(其實(shí)也是write): 對(duì)應(yīng)deleteMessage, 相對(duì)少一點(diǎn)

搜索: 可能是index on message, 相對(duì)少一點(diǎn)

根據(jù)不同的call pattern, 我們?cè)谠O(shè)計(jì)service的時(shí)候, 可能就會(huì)有輕重緩急的不同來(lái)分布這些API traffic. 比如: writeAPI被用的最多最多, 那么我們可能給這個(gè)service多一些box.

Notification Model

上面提到了我們可以用AWS SNS/SQS的方式來(lái)實(shí)現(xiàn)notification.

這里可以解釋幾點(diǎn):

  • notification model的最終原理, 其實(shí)都是有個(gè)server在一個(gè)端口不斷地polling(), 也就是說(shuō)我們的客戶端在不斷地問(wèn)郵局: 有我的信件嘛, 有我的信件嘛, 永不停止.
  • 并不一定要用AWS的服務(wù), 其他的也可以實(shí)現(xiàn), 這里說(shuō)SQS方便解釋.

注意: 為什么要用queue呢?

  • 后到的message, 后處理; 先到的message, 應(yīng)該先到用戶那里
  • 得到了notification了以后, 需要把這個(gè)message從queue里面刪除掉, 也是queue的原理

Deployment

這里不只是說(shuō)你的APP怎么到用戶那里呢: app store, 或者網(wǎng)頁(yè)access; 這里更多是說(shuō), 如果的有更新, 那么怎么到用戶那里?

我們會(huì)用到一個(gè)pipeline的概念: 每一個(gè)stage都應(yīng)該有不同的testing, 打個(gè)比方, 吃飯要吃: 涼菜, 熱菜, 湯.

涼菜: 在test environment里面, 這里鏈接的都是test domain的web server, service, 和內(nèi)部的測(cè)試用戶.

熱菜: 這里是跟production environment 一樣了, 所有的dependency也都是在production, 然后你去測(cè)試你的APP.

湯: 這是最后的階段,也是public accessible 的那個(gè)stage:在這個(gè)環(huán)境里面的APP, 用戶就可以用到了.

你需要借助一些已有的host/deployment工具來(lái)推送和測(cè)試你的代碼.

比較簡(jiǎn)單常用的一個(gè)服務(wù)器網(wǎng)站叫做Heroku, 是SalesForce下的一個(gè)服務(wù); 當(dāng)然AWS也有一些列的host/server服務(wù), 也可以使用.

More and More

到這一步, 好像全部做完了嘛! 你和女朋友終于可以在你寫的微信上面聊天了!

問(wèn)題1

你開心地邀請(qǐng)了你的朋友一起加入, 那么問(wèn)題來(lái)了:

雖然你是一個(gè)程序員, 但是你的女朋友是交際花, 突然一夜之間來(lái)了1000個(gè)朋友加入了你的微信服務(wù)器, 你開始感受到延遲; 第二天晚上, 突然有了1 million個(gè)用戶加入, 你的服務(wù)器瞬間爆炸, 宕機(jī)了. 你該怎么辦?

第一個(gè)手段無(wú)非是: 再買幾個(gè)個(gè)box 來(lái)handle requests, 同時(shí)擴(kuò)大你的database read/write capacity.

這樣scaling好像能夠減輕一點(diǎn)壓力, 但是很快又不行了, 當(dāng)?shù)诙€(gè)million, 第三個(gè)million朋友來(lái)的時(shí)候, 你發(fā)現(xiàn)這些人又不給錢, 所以你買不起服務(wù)器了, 女朋友要難過(guò)傷心了!!!

這時(shí)候怎么辦?

前面提到了, 每種不同的操作, 有自己的重要新, 比如sendMessage()就非常非常多用和重要, 而read historical message就沒所謂; 而同時(shí), notifiction也是非常非常核心.

回到design的初期, 我們可以選擇分流, 開兩個(gè)service:

WriteMessageService: 往上面買100個(gè)服務(wù)器

ReadMessageService: 只買50個(gè)服務(wù)器, 夠用就好了

過(guò)去你可能總共需要200個(gè)服務(wù)器, 因?yàn)樗械膖raffic混在一起, 加大了每個(gè)服務(wù)器的平均負(fù)荷. 而現(xiàn)在減少成了總共150個(gè)服務(wù)器, 省下了資金, 也可以繼續(xù)維持你的微信運(yùn)營(yíng), 女朋友又對(duì)你笑了, 很高興很幸福啊!

問(wèn)題2

不久之后, 你突然發(fā)現(xiàn), 你當(dāng)初只用了1個(gè)database instance, 但是現(xiàn)在你有了10 million, 一個(gè)database的讀/寫完全沒有辦法支持, 也就是說(shuō), 很多read/write message都在跟database交互的時(shí)候出錯(cuò)沒有了. 一半以上的用戶感受到了大幅度延遲和發(fā)送失誤, 產(chǎn)生不滿, 你的女朋友的手機(jī)也無(wú)法發(fā)送了, 感到非常氣憤. 這時(shí)候怎么辦?

再加上5個(gè)database instance吧, 讓來(lái)往的traffic去不同的database讀寫好不好?

這里有兩種情況可以考慮:

  • 將5個(gè)database變成各自的replication. 這樣讀起來(lái)可能方便了: 用load balancer 把request分配去不同的database讀; 但是這里有個(gè)問(wèn)題: 你寫的時(shí)候怎么辦!? 每次要同時(shí)寫到5個(gè)地方, 速度不一定一樣, 而且復(fù)制也可能在network里失敗斷掉, 那么用戶每次讀寫就不consistent. 對(duì)于我們這個(gè)注重讀寫的APP, 這樣的分布不行; 如果是寫的快慢和consistency不重要, 但是讀的需求很大, 才可能用這個(gè)模式.
  • 另一個(gè)方法: 將5個(gè)database分成5分, 每一個(gè)database承載一部分的用戶, 而且永遠(yuǎn)承載這些用戶. 這里可以用用戶的名字做個(gè)hash, 最后hash的結(jié)果來(lái)判斷存去哪個(gè)database. 當(dāng)然啦, 每次在選擇database的時(shí)候, 可能要多一個(gè)判斷, 根據(jù)用戶的id, 去不同的database存取.

問(wèn)題3

這里還引出了又一個(gè)問(wèn)題: 我們的message是不是應(yīng)該跟著用戶走? 也就是說(shuō), 我們需要把所有跟某個(gè)用戶相關(guān)的message, 全部復(fù)制一遍. 那么實(shí)際上微信這么做么?

過(guò)去在用QQ的時(shí)候, 有個(gè)漫游設(shè)置, 現(xiàn)在分析開來(lái), 也就是根據(jù)某個(gè)用戶個(gè)人的需求, 將他所有的message 漫游, 根據(jù)他的messageID 跟著人, 存到同一個(gè)database里面.

而微信貌似沒有做這樣的操作: 所有的message好像都是在local, 如果換手機(jī), 并且不轉(zhuǎn)移message, 那么message就全部丟失了.

我可以理解微信不做漫游message: 因?yàn)槟敲炊鄡|人, 沒一個(gè)人, 就存一個(gè)他的version of chat history, 這樣可能太過(guò)費(fèi)勁了. 當(dāng)然, 并不是說(shuō)解決不了, 但可能并沒有巨大的需求, 所以沒有去實(shí)現(xiàn), 可以理解.

雖然微信可能沒有在云端做這個(gè)getMessage()的服務(wù), 而是在本地讀手機(jī), 但并不是說(shuō)我們上面的設(shè)計(jì)都白費(fèi)了. 我確定, 微信可能會(huì)是暫時(shí)存儲(chǔ)一定量的信息, 比如:

'最近/尚未簽收'的信息: 換手機(jī), 上一條微信在第一個(gè)手機(jī)上還沒有打開看的, 在第二個(gè)手機(jī)上依然受到了新信息.

又或者說(shuō), 你1000個(gè)朋友同時(shí)每個(gè)人給你發(fā)了100條短信; 假設(shè)你的手機(jī)是10年前的諾基亞, 只有32MB的容量, 那么100k個(gè)短信會(huì)讓你手機(jī)爆掉吧;如果沒有, 那么這些信息可能存在某個(gè)臨時(shí)數(shù)據(jù)庫(kù), 而不在你的手機(jī)上.

(wait, 難道微信不存在數(shù)據(jù)庫(kù)而是直接強(qiáng)行塞到你手機(jī)里? .... 好危險(xiǎn)哈哈哈...不可能的啦)

你的手機(jī)應(yīng)該就是不斷地去poll()message, 然后給你發(fā)個(gè)message count, 而message本身, 還要重新去read. 這里有幾個(gè)可能的步驟:

  • queue里面的message嚴(yán)格要求簽收, 如果不簽收, 不會(huì)刪除
  • 在一定時(shí)間里面 (1 week) 在云端存取未讀信息, 比如說(shuō)某個(gè)地方有個(gè)24 hours cache
    一旦過(guò)期, 這些信息就被自動(dòng)刪除. 而在期限內(nèi)讀, 就可以順利拿到, 并且存一個(gè)local copy.

這樣想, 是不是我們看的一些視頻或者照片, 過(guò)了很久之后, 就打不開了, 說(shuō)過(guò)期了呀? 我猜就是這個(gè)原因.

再重申一下, 為什么會(huì)需要過(guò)期:

  • 內(nèi)容太大, 并不是非常多人一直去read history
  • 根據(jù)我們粗略的設(shè)計(jì), database分布的時(shí)候, 這些數(shù)據(jù)要跟著user存儲(chǔ)的地方, 被完全復(fù)制一遍, 不合理 (當(dāng)然啦, 這個(gè)naive的設(shè)計(jì)導(dǎo)致了這個(gè)結(jié)果, 其實(shí)是有很多辦法拆分和優(yōu)化的, 可以有效率的實(shí)現(xiàn))

這里提到了Cache或者是臨時(shí)database.

Cache是自然而然的過(guò)期, 刪除.

如果支持TTL的database, 也是可以將數(shù)據(jù)自動(dòng)過(guò)期刪除的. TTL: time to live

其他問(wèn)題:

還有很多其他問(wèn)題可以考慮:

  • calculate 具體的read/write, API volume來(lái)決定box的數(shù)量
  • 根據(jù)跟具體的requirement來(lái)細(xì)化database數(shù)據(jù)的分布和access pattern
  • 如何handle traffic monitoring, 采取什么樣的action 等等

結(jié)束語(yǔ)

做一個(gè)粗略design就是這么high. 寫完這些, 大概耗時(shí)1個(gè)小時(shí)45分鐘.

這個(gè)design能不能用呢? 我覺得實(shí)現(xiàn)你和女朋友的單方面溝通, 是綽綽有余的, 但是思考的過(guò)程中已經(jīng)發(fā)現(xiàn)了非常多的漏洞和可以用actual use case填補(bǔ)的地方. 真的要給1million個(gè)朋友用, 估計(jì)夠嗆: 我們巧妙地忽略了UX的設(shè)計(jì), 和PM的斗爭(zhēng), 無(wú)窮無(wú)盡的Testing等等等等. 先寫到這!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容