Intro
一開始是被邀回答這個問題, 如果好設(shè)計微信, 需要學(xué)哪些技術(shù)? 我覺得時間比空口羅列技術(shù)關(guān)鍵詞要稍微有用一點, 于是花了1:45小時寫了這篇設(shè)計. 從一個小的突破口, 從最基礎(chǔ)的需求出發(fā)來設(shè)計一下微信聊天的功能.
開一開腦洞的同時, 沒想到還讓我琢磨出了幾種微信現(xiàn)有的問題/限制: 無法云端備份聊天記錄, 微信群不能超過500人等等. 我認為是最初設(shè)計系統(tǒng)的時候有一些無法scale的缺陷, 那么導(dǎo)致了現(xiàn)在要花很大的人力和金錢去重改, 所以還沒有被當做第一要務(wù)吧!
讓我們開始, 一個大前提:
Client side message可以簡單地通過P2P來實現(xiàn), 比如使用socket.io. 但是我們這里考慮的是造一個微信, 就要將可能考慮到的全流程都涉及到. 這里假設(shè)我們的message不是通過client-client P2P實現(xiàn)的, 而是通過客戶端-> server -> 客戶端實現(xiàn)的, 那么就可以用sendMessage這個例子來介紹一下系統(tǒng)設(shè)計.
Scoping
- 需要做微信的什么功能? [我們假設(shè)就是要實現(xiàn)chat這個功能]
- 這個功能中間有多少個小的模塊需要考慮? [發(fā)送信息, 存儲信息, 讀取信息]
- 這些小功能如果用最簡單的方式, 大概需要哪些技術(shù)模塊實現(xiàn)? [User Interface, Web Server, Backend Service, Data Storage, Notification model]
- 假設(shè)有多少用戶會用? [假設(shè)每天有1K用戶, 1 million 用戶, 1 billion 用戶時不同的情況]
- Deployment: 這個產(chǎn)品如何到用戶手里? [根據(jù)個人經(jīng)驗, 假如說是web端好了]
- Next step: determine database based on call pattern, scaling, caching ...
Workflow
design的第一步, 都是要以最簡單明了的方式, 把需要的功能實現(xiàn)了: 先考慮,就2個人需要chat, 看是能怎么做?
根據(jù)上面的回答的那些問題, 把每一個環(huán)節(jié)寫下來.
想象一下, 你是userA, 你的女朋友是userB. 不要問為什么你是userA而女朋友是userB, 按照管理, 程序員絕大比例是單身男 , 這里讓你有一次女朋友吧!
Workflow1
你發(fā)送信息
=> Request傳到了WebServer
=> Request 傳到BackendService
=> 信息存儲在Database, 同時發(fā)送notification
=> 女朋友 的手機端不斷地在poll notification, 并且收到notification
=> 取決于這個notification里面是否包括chat的內(nèi)容, 女朋友可能再向 WebServer,Service, Database request信息的具體內(nèi)容
如果女朋友心情好, 選擇回復(fù), 那么重復(fù)以上動作
Workflow2
女朋友心血來潮, 看你手機記錄, 在app里面向上找chat history, 滑動一頁
=> 這個request傳到 WebServer
=> 找到相應(yīng)的Backend Service
=> 根據(jù)時間或者其他什么分頁方式, 從Database讀取上一頁的chat history
=> Backend Service
=> Web Server
=> 獲取的信息傳送回到女朋友這里, 看到你半夜找朋友吃雞的記錄
如果女朋友心情不好, 那么你就呵呵了.
Design Details
User Interface
UI固然非常重要, 但是在設(shè)計初期, 不必要全身心掉入UI的設(shè)計和選擇中, 基本上需要考慮的一些點, 記下來就可以. 比如:
選擇Angular做前端的controller, view.
選擇Bootstrap來潤色UI element
用Angular本身的testing framework來做testing
差不多到此為止, 下面去關(guān)注跟重要的部分.
注意: 在client端, 可能本地會運行一個小的server, 不斷地poll notifications:
這里可以用到一個AWS SQS的技術(shù), 不斷地對某一個queue讀取, 看有沒有發(fā)給自己的notification.
Web Server
我們要做一個chat的工具, 所以可以預(yù)料到:
同一個server上因為大量的user會經(jīng)過大量的I/O
server上面最重要的不過是把信息來回傳遞, 并不需要做很多業(yè)務(wù)信息的處理
基于這兩點, 我們可以暫且選用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, 而這些最好都是在墻內(nèi)的(不和真正的外界user接觸, 也不會expose給外界).
上面提到的web server會把每個request都傳到service來, 這中間會通過一道道防火墻和security check, 確保安全.
在service里面, 我們會有API的mapping, 比如:
put sendMessage(userA, userB, message): send message from A to B.
- 把數(shù)據(jù)存儲到database
- send SNS/SQS notification, 然后user會被notify
get getMessage(userA, timestamp, pageNum): based on timestamp and page num, read historical messages
- 從database里面根據(jù)request的信息, 讀取之前存儲的message
deleteMessage(messageId): remove a message from database
- 從database里面根據(jù)messageId, 刪除信息
Data Storage
我們選怎么樣的data storage呢? 有傳統(tǒng)的Sql database, 也有流行的non-sql database.
這里其實兩種都可以. 我們姑且將這個table命名為 MessageTable 我們在database里面很可能是用message id來存儲單個信息entry:
- messageId: string
- message: string
- sender: string
- receiver: string
- timestamp: date
寫入database好像比較簡單.
那么我們要支持哪些種讀取呢? 比如:
女朋友讀取你和她之間前10分鐘的數(shù)據(jù): 需要 index on sender, receiver, timestamp
根據(jù)messageId 刪除entire message entry: 因為messageId是primaryKey, 直接用它刪就好了.
其他一些微信里面可能有的功能:
找到所有提到'吵架'的message: index on message
MessageTable 主要需要的一些功能就是以上, 但每個API的使用頻率可能不同, 排列一下:
寫入: 對應(yīng)sendMessage, 相對是最多的
讀取: 對應(yīng)getMessage, 比write應(yīng)該少點, 你的女朋友不會一直不斷地翻記錄, 手會累, 多數(shù)還是發(fā)信息.
刪除(其實也是write): 對應(yīng)deleteMessage, 相對少一點
搜索: 可能是index on message, 相對少一點
根據(jù)不同的call pattern, 我們在設(shè)計service的時候, 可能就會有輕重緩急的不同來分布這些API traffic. 比如: writeAPI被用的最多最多, 那么我們可能給這個service多一些box.
Notification Model
上面提到了我們可以用AWS SNS/SQS的方式來實現(xiàn)notification.
這里可以解釋幾點:
- notification model的最終原理, 其實都是有個server在一個端口不斷地polling(), 也就是說我們的客戶端在不斷地問郵局: 有我的信件嘛, 有我的信件嘛, 永不停止.
- 并不一定要用AWS的服務(wù), 其他的也可以實現(xiàn), 這里說SQS方便解釋.
注意: 為什么要用queue呢?
- 后到的message, 后處理; 先到的message, 應(yīng)該先到用戶那里
- 得到了notification了以后, 需要把這個message從queue里面刪除掉, 也是queue的原理
Deployment
這里不只是說你的APP怎么到用戶那里呢: app store, 或者網(wǎng)頁access; 這里更多是說, 如果的有更新, 那么怎么到用戶那里?
我們會用到一個pipeline的概念: 每一個stage都應(yīng)該有不同的testing, 打個比方, 吃飯要吃: 涼菜, 熱菜, 湯.
涼菜: 在test environment里面, 這里鏈接的都是test domain的web server, service, 和內(nèi)部的測試用戶.
熱菜: 這里是跟production environment 一樣了, 所有的dependency也都是在production, 然后你去測試你的APP.
湯: 這是最后的階段,也是public accessible 的那個stage:在這個環(huán)境里面的APP, 用戶就可以用到了.
你需要借助一些已有的host/deployment工具來推送和測試你的代碼.
比較簡單常用的一個服務(wù)器網(wǎng)站叫做Heroku, 是SalesForce下的一個服務(wù); 當然AWS也有一些列的host/server服務(wù), 也可以使用.
More and More
到這一步, 好像全部做完了嘛! 你和女朋友終于可以在你寫的微信上面聊天了!
問題1
你開心地邀請了你的朋友一起加入, 那么問題來了:
雖然你是一個程序員, 但是你的女朋友是交際花, 突然一夜之間來了1000個朋友加入了你的微信服務(wù)器, 你開始感受到延遲; 第二天晚上, 突然有了1 million個用戶加入, 你的服務(wù)器瞬間爆炸, 宕機了. 你該怎么辦?
第一個手段無非是: 再買幾個個box 來handle requests, 同時擴大你的database read/write capacity.
這樣scaling好像能夠減輕一點壓力, 但是很快又不行了, 當?shù)诙€million, 第三個million朋友來的時候, 你發(fā)現(xiàn)這些人又不給錢, 所以你買不起服務(wù)器了, 女朋友要難過傷心了!!!
這時候怎么辦?
前面提到了, 每種不同的操作, 有自己的重要新, 比如sendMessage()就非常非常多用和重要, 而read historical message就沒所謂; 而同時, notifiction也是非常非常核心.
回到design的初期, 我們可以選擇分流, 開兩個service:
WriteMessageService: 往上面買100個服務(wù)器
ReadMessageService: 只買50個服務(wù)器, 夠用就好了
過去你可能總共需要200個服務(wù)器, 因為所有的traffic混在一起, 加大了每個服務(wù)器的平均負荷. 而現(xiàn)在減少成了總共150個服務(wù)器, 省下了資金, 也可以繼續(xù)維持你的微信運營, 女朋友又對你笑了, 很高興很幸福啊!
問題2
不久之后, 你突然發(fā)現(xiàn), 你當初只用了1個database instance, 但是現(xiàn)在你有了10 million, 一個database的讀/寫完全沒有辦法支持, 也就是說, 很多read/write message都在跟database交互的時候出錯沒有了. 一半以上的用戶感受到了大幅度延遲和發(fā)送失誤, 產(chǎn)生不滿, 你的女朋友的手機也無法發(fā)送了, 感到非常氣憤. 這時候怎么辦?
再加上5個database instance吧, 讓來往的traffic去不同的database讀寫好不好?
這里有兩種情況可以考慮:
- 將5個database變成各自的replication. 這樣讀起來可能方便了: 用load balancer 把request分配去不同的database讀; 但是這里有個問題: 你寫的時候怎么辦!? 每次要同時寫到5個地方, 速度不一定一樣, 而且復(fù)制也可能在network里失敗斷掉, 那么用戶每次讀寫就不consistent. 對于我們這個注重讀寫的APP, 這樣的分布不行; 如果是寫的快慢和consistency不重要, 但是讀的需求很大, 才可能用這個模式.
- 另一個方法: 將5個database分成5分, 每一個database承載一部分的用戶, 而且永遠承載這些用戶. 這里可以用用戶的名字做個hash, 最后hash的結(jié)果來判斷存去哪個database. 當然啦, 每次在選擇database的時候, 可能要多一個判斷, 根據(jù)用戶的id, 去不同的database存取.
問題3
這里還引出了又一個問題: 我們的message是不是應(yīng)該跟著用戶走? 也就是說, 我們需要把所有跟某個用戶相關(guān)的message, 全部復(fù)制一遍. 那么實際上微信這么做么?
過去在用QQ的時候, 有個漫游設(shè)置, 現(xiàn)在分析開來, 也就是根據(jù)某個用戶個人的需求, 將他所有的message 漫游, 根據(jù)他的messageID 跟著人, 存到同一個database里面.
而微信貌似沒有做這樣的操作: 所有的message好像都是在local, 如果換手機, 并且不轉(zhuǎn)移message, 那么message就全部丟失了.
我可以理解微信不做漫游message: 因為那么多億人, 沒一個人, 就存一個他的version of chat history, 這樣可能太過費勁了. 當然, 并不是說解決不了, 但可能并沒有巨大的需求, 所以沒有去實現(xiàn), 可以理解.
雖然微信可能沒有在云端做這個getMessage()的服務(wù), 而是在本地讀手機, 但并不是說我們上面的設(shè)計都白費了. 我確定, 微信可能會是暫時存儲一定量的信息, 比如:
'最近/尚未簽收'的信息: 換手機, 上一條微信在第一個手機上還沒有打開看的, 在第二個手機上依然受到了新信息.
又或者說, 你1000個朋友同時每個人給你發(fā)了100條短信; 假設(shè)你的手機是10年前的諾基亞, 只有32MB的容量, 那么100k個短信會讓你手機爆掉吧;如果沒有, 那么這些信息可能存在某個臨時數(shù)據(jù)庫, 而不在你的手機上.
(wait, 難道微信不存在數(shù)據(jù)庫而是直接強行塞到你手機里? .... 好危險哈哈哈...不可能的啦)
你的手機應(yīng)該就是不斷地去poll()message, 然后給你發(fā)個message count, 而message本身, 還要重新去read. 這里有幾個可能的步驟:
- queue里面的message嚴格要求簽收, 如果不簽收, 不會刪除
- 在一定時間里面 (1 week) 在云端存取未讀信息, 比如說某個地方有個24 hours cache
一旦過期, 這些信息就被自動刪除. 而在期限內(nèi)讀, 就可以順利拿到, 并且存一個local copy.
這樣想, 是不是我們看的一些視頻或者照片, 過了很久之后, 就打不開了, 說過期了呀? 我猜就是這個原因.
再重申一下, 為什么會需要過期:
- 內(nèi)容太大, 并不是非常多人一直去read history
- 根據(jù)我們粗略的設(shè)計, database分布的時候, 這些數(shù)據(jù)要跟著user存儲的地方, 被完全復(fù)制一遍, 不合理 (當然啦, 這個naive的設(shè)計導(dǎo)致了這個結(jié)果, 其實是有很多辦法拆分和優(yōu)化的, 可以有效率的實現(xiàn))
這里提到了Cache或者是臨時database.
Cache是自然而然的過期, 刪除.
如果支持TTL的database, 也是可以將數(shù)據(jù)自動過期刪除的. TTL: time to live
其他問題:
還有很多其他問題可以考慮:
- calculate 具體的read/write, API volume來決定box的數(shù)量
- 根據(jù)跟具體的requirement來細化database數(shù)據(jù)的分布和access pattern
- 如何handle traffic monitoring, 采取什么樣的action 等等
結(jié)束語
做一個粗略design就是這么high. 寫完這些, 大概耗時1個小時45分鐘.
這個design能不能用呢? 我覺得實現(xiàn)你和女朋友的單方面溝通, 是綽綽有余的, 但是思考的過程中已經(jīng)發(fā)現(xiàn)了非常多的漏洞和可以用actual use case填補的地方. 真的要給1million個朋友用, 估計夠嗆: 我們巧妙地忽略了UX的設(shè)計, 和PM的斗爭, 無窮無盡的Testing等等等等. 先寫到這!