推ter 架構如何支持上億用戶
談到設計推ter, 我們首先要問一個本質問題: 設計推ter的基本方法論是什么?
其實是我們計算機設計最基本的方法: 分治法(Divide and Conquer)。
什么是分治法呢?就是把問題不斷的拆解,拆解到你可以解決為止,它的藝術在于,從哪個維度來拆解非常考驗我們能力。
如果要求一周開發出推ter,你會怎么做?
你的架構是什么樣的呢?
相信你一定不會給出復雜的架構。前端是各種各樣的業務邏輯,后端是MySQL數據庫,這樣就夠了。因為這已經解決了當時的問題,滿足了一周開發出來的要求。
但隨著推ter的成長,我們會遇到各種各樣新的挑戰:
-
MySQL難擴展。
為什么難擴展?因為它把同樣的一個數據分成各種關系存在里面,每次取的時候,都要通過Join來進行復雜的操作,這個Join當數據被切分的時候存在更多的服務器上,會變得越來越復雜,所以很難擴展。
-
小變化也要全部部署。
任何變化都需要部署到所有機器上,因為服務不斷升級,就變成了每天不斷部署,變成了daily deployment。所以每次部署的時候耽誤時間很長。
-
性能差。
因為所有服務都要部署在一起,造成了它的內存占用率大,而且部分核心模塊還因為最初當時的單線程設計成為了各種瓶頸。
-
架構混亂。
因為所有模塊在一起很混亂。
那要怎么破解這些問題?
答案是將問題拆解開來。
第一刀將存儲切開。
我們看后臺,可以拆成存Tweets,存User,存Timeline,存Social Graph不同內容,Timeline可以拿Redis這樣的數據庫來保存,而對于其它的數據比如Gizzard其實是一個分布式的MySQL數據庫。并不是所有數據都不適合用MySQL分布,我們可以把這些適合的用MySQL來shutting一下,而不適合的用別的數據庫來存,就是一個存儲切開的方法。
第二刀是將路由、展示、邏輯切開。
我們可以看到又多出了一層,邏輯層。Tweets,User,Timeline它們分別對應后邊的各種數據存儲。前端會有外部訪問的接口,API訪問接口,并且它還保存了以前數據的整個架構,用來進行一些小規模的使用。最前面需要有一個Routing,來將不同的請求分布到不同的API上。所以之后推ter就變成了上百上千個小小的數據模塊,包括它的服務模塊,他們通過之間的相互調用來完成具體的請求。
如果Lady Gaga發了個推文,會發生什么呢?
首先她發出一個推文,會到達最上面一層的外部模塊,負責把推文寫出來;接著到了API模塊,負責接收這個推文;再往后是Fanout,Fanout將推文的ID推薦給所有訂閱這個用戶的信息收件箱里,收件箱就是Timeline,比如有400萬人訂閱了Lady Gaga,就會有400萬的收件箱收到這個消息;緊接著由于我們Timeline要發到收件箱里,收件箱必然是一個最復雜的操作,為了優化它的性能,就把它們用分布式的方式存儲在Redis里面,Redis是偏向內存的數據庫,所以能夠很快的存儲這些信息;但Redis里存的只是ID,所以當一個用戶具體要她推文的時候,實際上要通過Timeline服務去找到這個用戶對應的是哪個Redis服務器上存了推文的 Timeline list,接著找到所有ID,然后這個Timeline service把ID的具體內容通過另外的數據庫裝載進來,最終得到結果反饋給用戶。這些保證了我們推整個數據的時候速度最快,能夠達到每秒30萬次的性能。
雖然有了這樣的過程,如何支持搜索呢?
很簡單的想法是當我們寫入API,它Fanout到每個用戶的Timeline list的時候,我們可以拿另外的Ingester把這個推文放到里面,Ingesert最后把它放到Earlybird,就是所謂的倒排索引中。比如有個推文發過來“我喜歡太陽”,就把我,喜歡,太陽,拆成三個詞,把這些單詞,進行倒排索引,存到Search index里面。這時候實際會用到很多很多Earlybird,這樣就能建立很多倒排索引,能夠并行的去做。當用戶一個請求過來之后,比如搜索“早上吃飯”,就會把morning和eat作為兩個關鍵詞發到我的Earlybird集群里,得到結果后Blender會把它組合到一塊,并反饋結果。當然很多用戶可以并行的向Blender發送請求,從而得到最終結果,這就是我們的搜索服務。
如何通知用戶新消息到達呢?
第一個方法是另外再開一個Write API,在有新東西發生以后,我把它放到一個Push的服務里,那所有用戶只要都連到這個后臺里,HTTP PUSH,就會通知他有新消息產生了。同理對Mobile ,會有Mobile Push,當然大家對不同的信息有不同的對接方法,大家可以去仔細考慮下怎么能做到這個樣子。
如何搭建這樣的服務呢?
最基本的是開源項目推ter-Server。
-
配置服務,IP之類;
-
管理服務,哪些down了、控制、啟動等;
-
日志服務,運行怎么樣,以后出問題找誰等;
-
生命周期服務,什么時候啟,什么時候關,什么時候控制;
-
監控服務,到底有沒有出錯,出錯以后怎么辦,互相報警等
這些東西合到一起,就構成了這樣服務的基本架構。
各個服務之間如何交流?
這就是傳統的RPC(遠程的進程調用),大家能夠通訊的不僅是數據,而且可以通訊命令或請求之類的。這時需要開源項目Finagle。
-
能夠提供服務發現,因為有很多服務,所以要找誰發送這個請求呢;
-
負載均衡,可能有十個人提供服務,先放到誰那兒?
-
重試,如果失敗了怎么辦,是否需要重試?
-
基本的線程池和鏈接池,大家可以復用,不用每次去創建,浪費資源了;
-
統計信息的收集;
-
分布式調試。
如何調用一個服務?
因為從A調用B,大家之間各種遠程,寫代碼上要怎么做呢?可以使用函數來調用(Service as a Function),背后實際上是Function programming的思想。
可以看下這個基本例子:trait Service[Req,Rep]extends(Req=>Future[Rep])
簡單理解為:我有一個請求想得到一個Response,就是Request到Response。這里面實際上可以先面向未來實現,之后當它執行的時候,就會得到相對應的結果。
多個服務的調用如何整合在一起?
我們看一看它要如何運行?比如我們有一個請求是得到一個用戶的所有Timeline數據。它其實有很多步,第一步是得到User ID;第二步得到User timeline list里的那些消息ID;下一步是針對每一個消息,得到它的每一個具體內容,比如“我早上吃飯”這些內容,這些內容里面可能還有圖片,還需要得到圖片的數據。這是一個很復雜的過程,但這個箭頭表達了它們最基本的執行順序。
最佳的執行路徑是什么?
首先是得到ID,再得到Timeline以后可以并行地讀取每個tweet,可能會有的快有的慢,然后又得到一些它的具體信息,比如說圖片之類,所以這就是最優策略。
下面我們來看一看代碼:
第一行是得到用戶ID,用一個面向未來的方程得到一個用戶ID。
第二行根據ID拿FlatMap,FlatMap其實是后面針對每一個ID執行的函數,這個函數來得到這個ID用戶的Timeline list。所以這也是我們Function programming,或者如果用Spark也會經常碰見的函數。
得到每一個ID以后,實際上要針對每個ID執行,所以需要Map。Map每一個ID是做什么的,得到這個Tweet的具體信息。代碼第二行就是根據ID得到他的具體內容。
當然如果這個東西有圖片,那需要得到圖片。
中間一步還需要把這些東西并行起來,大家都去單獨得到Tweet,并且把它集合放到一塊兒去。整個過程我們就得到了最終的代碼。
這其實就是 第三刀:切開“做什么”和“怎么做”。 在這個代碼過程中,所謂面向函數的編程,只是寫了想做什么事兒,但具體怎么做,比如從哪個機器找、連接服務器、從誰那兒抓取,怎么并行等都沒有寫,實際上這些東西都被封裝到了一個底層的庫里面。所以現在推ter就可以有兩個團隊,一個團隊負責寫代碼,“做什么”;另一個團隊負責寫底層是怎么實現的。這樣就能實現并行開發,這也是Function programming的一個好處。
一個服務器的架構長什么樣呢?
上方是你的服務,下方就是你的服務器,包括一些集群的管理、內存、Java的虛擬機、操作系統和硬件。
有很多服務如何整合?
結果整合到一起就是很多層,底層都是一樣的,但上層跑了不同的服務:HTTP、聚集器、時間timeline服務等。我們所有東西加在一起,系統就會跑上千個不同的小服務器,而且之間會有各種各樣的備份。
那如何來統計出現的服務器情況呢?
比如會統計平均延遲,或統計一些信息來驗證服務器是不是出問題了,實際上,在這種大規模里面,一個比較好的方法就是不要拿平均值統計,因為特殊情況會拉低所有情況。舉個例子,假設北京有一個人,他年收入是100萬,另外99個人每個人年收入都是1元。那平均下來每個人收入都是1萬塊錢,但實際上大多數人是很窮困的。所以不能拿平均數來算,尤其是在服務器的情況下。所以大家一般都會取中位數(median),百分之90的點,百分之99的點和百分之999的點。我們可以看到,下圖是一個服務發生故障的前后。在服務故障發生前,百分之99點的平均延遲是100毫秒,但故障發生后變成400毫秒,發現這個問題以后就會去抓。但如果用平均值,很可能就發現不了這個問題。
這樣還有一個好處,當我們做到上面整合的架構以后,每一個負責寫代碼的團隊,只負責處理跟它相關的上下層就行,而不用做那么多交互,所以每個人的東西都非常簡單,這也是所謂微服務的一個好處。
如何監控一個服務呢?
實際上針對這個服務在整個棧里面用的時間,會有一個圖表達出來。
如何監控一個請求呢?
實際上,基于一個請求,會有一個整個運行的最佳策略,這就需要運行的整個過程的圖。這就是我們之前講的,得到一個用戶所有Timeline內容信息的鋸齒圖,Zipkin,這也是一個開源項目。
如何監控系統的運行情況呢?
因為這個時候失敗已經成為常事了。舉個例子,假設每秒有30萬的請求,有99.99%的成功率,那每秒一定會有30個失敗的,所以不能說每次失敗怎么樣,統計失敗率會比單獨的失敗更重要,而且寫代碼的時候要為這個失敗來進行自動重試。大家不用糾結每次的失敗,只要大部分過去就行了。
最后做下總結
-
學好分治法,走遍天下都不怕。
-
函數式設計切分做什么和怎么做。做什么是由函數式設計來寫,而怎么做由底層的語言和編譯器來優化。
-
面向錯誤讓我們使用了統計域監控。99.9%的點出什么問題了,這樣就叫做基于統計域的監控方式。
參考資料:《Real-Time Systems at 推ter》
來自:http://mp.weixin.qq.com/s/c9mFDmHRLbuKbqS6aGUdiQ