【編者按】在之前我們有分享過HighScalability創始人Tod Hoff總結的WhatsApp早期架構,其中包括了大量的Erlang優化來支撐單服務器200萬并發連接,以及如何支撐所有類型的手機并提供一個完美的用戶體驗。然而時過境遷,兩年后WhatsApp又是如何支撐10倍于之前的流量,以及應用的飛速擴展,這里我們一起看Tod帶來的總結。以下為譯文:
兩年內的飛躍
天價應用當下的規模顯然不能與兩年前同日而語,這里總結了一些WhatsApp兩年內發生的主要變化:
1. 從任何維度上都可以看到WhatsApp的巨變,但是工程師的數量卻一直未變。當下,WhatsApp有更多的主機、更多的數據中心、更多的內存、更多的用戶以及更多的擴展性問題,然而最引以為豪的卻是那支10人工程團隊——每個工程師平均負責4000萬個用戶。當然,這也是云時代的勝利:工程師只負責軟件的開發,網絡、硬件及數據中心運維全部假手于人。
2. 在之前,面對負載的激增,他們必須讓單服務器支撐盡可能多的連接數,但是現在他們已經步出了那個時代。當然,基于總體成本的控制,他們仍然需要控制主機的數量并讓SMP主機更效率的運行。
3. 瞬時的好處。鑒于現在的架構已經囊括多媒體、圖片、文本、音頻,無需保存這些大體積格式的信息讓系統大大的簡化,架構的重心被放在吞吐量、緩存以及分片等。
4. Erlang的世界。即使他們打造的仍然是一個分布式系統,遇見的問題也大同小異,但是從始至終都在說Erlang確實值得稱道。
5. Mnesia,這個Erlang數據庫似乎已成為他們問題的主要來源。因此,不得不懷疑一味的緊抓Erlang會不會比較盲目,是否有其他更好的替代方案。
6. 如此規模下問題之多你可以想象。海量連接數的保持、隊列因優先級操作變得太長、計時器、不同負載下的代碼表現問題、高負載下高優先級消息得不到處理、一個操作被另一個操作意外打斷、故障導致的資源問題以及不同用戶平臺的兼容性等,巨型架構的打造絕非一朝一夕。
7. Rick的發現和處理問題能力讓人贊嘆,也可以說是吃驚。
Rick的分享總是非常精彩,他樂于分享許多細節,其中有許多只能在生產環境出現。下面是他 最新分享總結:
統計
月4.65億用戶
平均每日接收190億消息,發送400億消息
6億張圖片,2億條語音,1億段視頻
峰值期間1.47億的并發連接數——電話連接到系統
峰值期間每秒23萬次登陸操作——手機上線及下線
峰值期間每秒32.4萬信息流入,71.2萬的流出
約10個工程師致力于Erlang,他們肩負了開發與運維
節日的峰值
平安夜流出達146 Gb/s,相當多的帶寬用于服務手機
平安夜視頻下載達3.6億次
新年夜圖片下載約20億(46 k/s)
新年夜有張圖片下載了3200萬次
堆棧
Erlang R16B01(打了自己的補丁)
FreeBSD 9.2
Mnesia(數據庫)
Yaws
使用了SoftLayer云服務和實體服務器
硬件
大約550個服務器+備份
150個左右的Chat服務器(每個服務器處理大約100萬的手機、峰值期間1.5億的連接)
250個左右的多媒體信息服務器
2x2690v2 Ivy Bridge 10-core(總計40的超線程技術)
數據庫節點擁有512GB的內存
標準計算節點搭載64GB內存
SSD主要用于可靠性,存儲資源不足時還用于存儲視頻
Dual-link GigE x2(公共的面向用戶,私有的用于后端系統)
Erlang系統使用的核心超過1.1萬個
系統概況
獨愛Erlang
非常棒的語言,適合小工程團隊。
非常棒的SMP可擴展性。可以運行高配的主機,并且有益于減少節點。運維復雜性只與節點數有關,而不是核心數。
可以飛快的更新代碼。
擴展性就像掃雷,但是他們總可以在問題爆發之前發現并解決。世界級事件相當于做系統的壓力測試,特別是足球賽,會帶來非常高的峰值。服務器故障(通常是內存)、網絡故障以及差的軟件的推送都考驗著系統。
傳統的架構
手機客戶端連接到MMS(多媒體)
Chat連接到瞬態離線存儲,用戶之間的消息傳輸通過后端系統控制。
Chat連接到數據庫,比如Account、Profile、Push、Group等。
發送到手機的消息
文本消息
通知:群組消息,個人簡介照片改變等
狀態消息:輸入狀態、離開狀態、在線或離線情況等
多媒體數據庫
內存Mnesia數據庫使用大約2TB的RAM,跨16個分片存儲180億條記錄。
只存儲正在發布的消息和多媒體,但是在多媒體發布時,會將信息儲存在數據庫中。
當下單服務器只運行100萬的并發連接,而在兩年前這個數字是200萬,因為現在服務器要做的事情更多了:
隨著用戶量的增多,WhatsApp期望每個服務器上預留更多的空間以應對峰值。
許多過去不是運行在這個服務器上的功能現在被移到上面,因此服務器更忙了。
解耦
隔離瓶頸,讓之不會存在整個系統中
緊耦合會導致相繼故障
前端系統和后端系統首先要分離
隔離一切,讓組件間不會存在影響。
正在解決問題時,保持盡可能多的吞吐量。
異步處理以最小化吞吐量延時
當延時不可預知及在不同點存在時,異步可以盡可能的保證吞吐量。
解耦可以讓系統運行盡可能的快。
避免HOL阻塞
線頭阻塞是首位處理會餓死隊列中其他項目。
分離讀和寫隊列。特別是在表格上執行事務,寫入方面的延時不會影響讀取隊列,通常情況下讀的速度會很快,因此任何阻塞都會影響讀性能。
分離節點內部隊列。如果節點或者網絡連接的節點出現問題,它可能會阻塞應用程序中其他任務。因此,發往不同節點的消息會分配不同的進程(Erlang中的輕量級并發),因此只有當消息發送給問題節點時才會做備份,這將允許消息自由的傳輸,問題被隔離開來,給Mnesia打補丁以保證async_dirty級響應時間。App發送消息后就會被解耦,因此當一個節點發生故障時,不會導致負載問題。
在不確定延時場景下使用FIFO模型。
Meta Custering
本節出現在講話的第29分鐘,不幸但是,信息量不大。
需要一種方法來控制單集群體積,并允許他跨很長距離。
建立wandist,基于gen_tcp的分布式傳輸,由許多需要相互通信的節點組成。
1個基于pg2的透明路由層,建立一個單跳路由調度系統。
舉個例子:兩個數據中心的兩個主集群,位于兩個不同數據中心的兩個多媒體集群,以及兩個數據中心間一個共享的全局集群,他們之間都使用wandist進行連接。
例子
使用async_dirty來避免Mnesia事務耦合,大部分情況下不會使用事務。
只在從數據庫中恢復時才使用call,其他情況下都使用cast來維持異步模型。在Erlang,消息隊列會因等待handle_call響應而造成阻塞,handle_cast不會造成阻塞是因為它不關注結果。
Call使用超時而不是監視,減少遠端進程競爭以及分發時傳輸的數據。
如果只是想追求最好的交付能力,為cast使用nosuspend。這樣會阻止節點受到下游問題影響——不管是節點失敗還是網絡問題(在這些情況下,發送數據緩沖池會備份到發送節點上),進程發送的開始指令會被調度系統掛起,從而造成了相繼故障——大家都在等待,卻沒有操作正在被處理。
使用大的發送緩沖器,從而降低收來自網絡和下游系統的影響。
并行
任務分配
需要在1.1萬個核心上分配任務
始于單線程的gen_server,然后建立了一個gen_factory負責多節點之間的任務傳遞。
在負載達到了一定程度,調度過程本身就變成了瓶頸,不僅僅是執行時間問題。
因此建立一個gen_industry,位于gen_factory之上,從而并行的攝入所有輸入,并且有能力立刻給工作節點分配。
工作節點的尋址類似數據庫通過key查找,因此這里存在不確定延時,比如IO,所以為了避免線頭阻塞,這里使用了一個FIFO模型。
分割服務
在2到32間進行分割,大部分服務都被分割成32個。
pg2 addressing,分布式進程組,用于集群上的分片尋址。
節點進行主從設置,用于容災。
限制訪問單ets或者mnesia進程的數量到8,這會讓鎖爭用處于控制當中。
Mnesia
因為沒有使用事務去保證一致性,他們使用一個進程對一個節點上的記錄進行連續訪問。哈希到一個分片,會映射到1個mnesia fragment,最后會被調度到1個factory,隨后是節點。因此,對每個單記錄的訪問都會被轉換成一個獨立的Erlang進程。
每個mnesia fragment都只能在1個節點上的應用程序等級進行讀取,這樣復制流只需要在一處進行。
一旦節點間存在復制流,分片的更新速度上就會存在瓶頸。他們給OTP打補丁以實現多個事務管理器,用以實現async_dirty,從而記錄可以并行的進行修改,這將產生更多的吞吐量。
打補丁允許Mnesia庫直接被分割到多個庫上,這就意味著它可以寫多個驅動,這么做可以直接提升磁盤的吞吐量。這里存在的問題是Mnesia達到峰值,通過多個磁盤來分攤IO,而為了進一步提升可擴展性及性能,甚至會加入SSD。
將Mnesia“island”縮減到2個,每個“island”都是一個Mnesia集群。因此在表格被分割成32份時,將會有16個“island”支撐一個表格。從而,他們可以進行更好的schema operation,因為只需要修改兩個節點。在同時打開1或2個節點時,可以減少加載時間。
通過設置警報快速處理Mnesia中的網絡分片,讓它們繼續保持運行,然后手動的調節將它們整合。
優化
在峰值情況下,離線存儲系統曾是1個非常大的瓶頸,無法更快的將消息推送到系統。
每條消息都被用戶快速的讀取,60秒內完成50%。
添加一個回寫緩存,這樣消息就可以在寫入文件系統之前被交付,緩存命中率達98%。
如果IO系統因為負載而阻塞,緩存會對消息交付起到額外的緩沖作用,直到IO系統恢復。
給BEAM(Erlang VM打補丁)以實現異步文件IO來避免線頭阻塞問題,在所有異步工作線程上輪訓文件系統端口請求,在大型mailbox和緩慢磁盤的情況下可以緩解寫入。
讓大型的mailbox遠離緩存,有些用戶加入了大量的組,每小時收入數千消息。他們會影響整個緩存,并讓處理變慢。將它從緩存中驅除。需要注意的是,不成比例的大型用戶處理是每個系統都存在的問題,其中包括Twitter。
使用大量的fragments降低mnesia表格的訪問速度
賬戶表格被分割成512份打入“island”,這就意味著用戶和這512個分片間存在一個稀疏映射,大部分的fragments都是空的和空閑的。
主機數量翻倍,但是吞吐量降低。記錄訪問變慢的原因是當目標為7時,哈希鏈的大小超過了2K。
這里存在的一個問題是哈希模式會導致建立大量的空bucket,有些甚至會非常長。雙線的變化解決了這個問題,并將性能從4提升到1。
補丁
計時器輪上的競爭,當1個主機的連接數達到幾百萬,同時每個鏈接上的手機發生變化時就會建立或重置計時器,從而導致了每秒數十萬的計時器。計時器輪上的鎖則是競爭的主要來源,解決方法就是建立多個計時器輪。
mnesia_tm是個非常大的選擇循環,因此雖然負載未滿,也可能會造成事務的積壓,打補丁以收取事務流并且保存以作稍后處理。
添加多個mnesia_tm async_dirty發送者
存在許多的跨集群操作,因此mnesia最好從附近的節點加載。
給異步文件IO加入循環調度。
使用ets哈希開防止w/ phash2的同時發生。
優化ets main/name table來應對規模
不要隊列mnesia dump,因為隊列中存在太多的dumps時,schema ops將不可行。
2月22日的停機
即使做了如此多的努力,停機仍然不可避免,而且發生在了最不應該發生的時候,在被Facebook收購后宕機了210分鐘。
負載的變化導致了問題的發生,此次宕機歸結于后端系統的路由問題。
路由器造成了一片局域網的癱瘓,造成了集群中大量節點的斷開和重連。同時,在節點重連之后,集群出現了前所未有的不穩定狀態。
最終,他們不得不停機修復,這種情況在幾年內都未出現過。
在檢查中,他們發現了一個過度耦合的子系統。在斷開和重連時,他們發現pg2在做n^3的消息,消息隊列在數秒鐘內從0飆升到了400萬,為此他們推出了1個補丁。
特性發布
無法模擬如此規模的流量,特別是高峰期間,比如新年的鐘聲。因此,他們只能緩慢的發布特性,先在小流量下發布,然后迅速迭代直到良好運行,接著再向其他的集群推廣。
上線是一個滾動的更新過程。冗余一切,如果他們想做一個BEAM更新,在安裝后他們需要一個個的重啟集群中的節點然后更新。也存在熱補丁的情況,但是很罕見,通常的升級都非常麻煩。