《從無到有:微信后臺系統的演進之路》要點:
本文介紹了從無到有:微信后臺系統的演進之路,希望對您有用。如果有疑問,可以聯系我們。
2011.1.21 微信正式發布.這一天距離微信項目啟動日約為2個月.就在這2個月里,微信從無到有,大家可能會好奇這期間微信后臺做的最重要的事情是什么?
我想應該是以下三件事:
微信起初定位是一個通訊工具,作為通訊工具最核心的功能是收發消息.微信團隊源于廣硏團隊,消息模型跟郵箱的郵件模型也很有淵源,都是存儲轉發.
?微信消息模型
圖1展示了這一消息模型,消息被發出后,會先在后臺臨時存儲;為使接收者能更快接收到消息,會推送消息通知給接收者;最后客戶端主動到服務器收取消息.
由于用戶的帳戶、聯系人和消息等數據都在服務器存儲,如何將數據同步到客戶端就成了很關鍵的問題.為簡化協議,我們決定通過一個統一的數據同步協議來同步用戶所有的基礎數據.
最初的方案是客戶端記錄一個本地數據的快照(Snapshot),需要同步數據時,將Snapshot帶到服務器,服務器通過計算Snapshot與服務器數據的差異,將差異數據發給客戶端,客戶端再保存差異數據完成同步.不過這個方案有兩個問題:一是Snapshot會隨著客戶端數據的增多變得越來越大,同步時流量開銷大;二是客戶端每次同步都要計算Snapshot,會帶來額外的性能開銷和實現復雜度.
幾經討論后,方案改為由服務計算Snapshot,在客戶端同步數據時跟隨數據一起下發給客戶端,客戶端無需理解Snapshot,只需存儲起來,在下次數據同步數據時帶上即可.同時,Snapshot被設計得非常精簡,是若干個Key-Value的組合,Key代表數據的類型,Value代表給到客戶端的數據的最新版本號.Key有三個,分別代表:帳戶數據、聯系人和消息.這個同步協議的一個額外好處是客戶端同步完數據后,不需要額外的ACK協議來確認數據收取成功,同樣可以保證不會丟數據:只要客戶端拿最新的Snapshot到服務器做數據同步,服務器即可確認上次數據已經成功同步完成,可以執行后續操作,例如清除暫存在服務的消息等等.
此后,精簡方案、減少流量開銷、盡量由服務器完成較復雜的業務邏輯、降低客戶端實現的復雜度就作為重要的指導原則,持續影響著后續的微信設計開發.記得有個比較經典的案例是:我們在微信1.2版實現了群聊功能,但為了保證新舊版客戶端間的群聊體驗,我們通過服務器適配,讓1.0版客戶端也能參與群聊.
微信后臺使用三層架構:接入層、邏輯層和存儲層.
微信后臺主要使用C++.后臺服務使用Svrkit框架搭建,服務之間通過同步RPC進行通訊.
?Svrkit 框架
Svrkit是另一個廣硏后臺就已經存在的高性能RPC框架,當時尚未廣泛使用,但在微信后臺卻大放異彩.作為微信后臺基礎設施中最重要的一部分,Svrkit這幾年一直不斷在進化.我們使用Svrkit構建了數以千計的服務模塊,提供數萬個服務接口,每天RPC調用次數達幾十萬億次.
這三件事影響深遠,乃至于5年后的今天,我們仍繼續沿用最初的架構和協議,甚至還可以支持當初1.0版的微信客戶端.
這里有一個經驗教訓——運營支撐系統真的很重要.第一個版本的微信后臺是倉促完成的,當時只是完成了基礎業務功能,并沒有配套的業務數據統計等等.我們在開放注冊后,一時間竟沒有業務監控頁面和數據曲線可以看,注冊用戶數是臨時從數據庫統計的,在線數是從日志里提取出來的,這些數據通過每個小時運行一次的腳本(這個腳本也是當天臨時加的)統計出來,然后自動發郵件到郵件組.還有其他各種業務數據也通過郵件進行發布,可以說郵件是微信初期最重要的數據門戶.
2011.1.21 當天最高并發在線數是 491,而今天這個數字是4億.
在微信發布后的4個多月里,我們經歷了發布后火爆注冊的驚喜,也經歷了隨后一直不溫不火的困惑.
這一時期,微信做了很多旨在增加用戶好友量,讓用戶聊得起來的功能.打通騰訊微博私信、群聊、工作郵箱、QQ/郵箱好友推薦等等.對于后臺而言,比較重要的變化就是這些功能催生了對異步隊列的需求.例如,微博私信需要跟外部門對接,不同系統間的處理耗時和速度不一樣,可以通過隊列進行緩沖;群聊是耗時操作,消息發到群后,可以通過異步隊列來異步完成消息的擴散寫等等.
異步隊列在群聊中的應用.微信的群聊是寫擴散的,也就是說發到群里的一條消息會給群里的每個人都存一份(消息索引).
為什么不是讀擴散呢?有兩個原因:
異步隊列作為后臺數據交互的一種重要模式,成為了同步RPC服務調用之外的有力補充,在微信后臺被大量使用
微信的飛速發展是從2.0版開始的,這個版本發布了語音聊天功能.之后微信用戶量急速增長,2011.5用戶量破100萬、2011.7 用戶量破1000萬、2012.3 注冊用戶數突破1億.伴隨著喜人成績而來的,還有一堆幸福的煩惱.
微信發布時功能很簡單,主要功能就是發消息.不過在發語音之后的幾個版本里迅速推出了手機通訊錄、QQ離線消息、查看附近的人、搖一搖、漂流瓶和朋友圈等等功能.有個廣為流傳的關于朋友圈開發的傳奇——朋友圈歷經4個月,前后做了30多個版本迭代才最終成型.其實還有一個鮮為人知的故事——那時候因為人員比較短缺,朋友圈后臺長時間只有1位開發人員.
用戶多了,功能也多了,后臺模塊數和機器量在不斷翻番,緊跟著的還有各種故障.
幫助我們順利度過這個階段的,是以下幾個舉措:
雖然各種需求撲面而來,但我們每個實現方案都是一絲不茍完成的.實現需求最大的困難不是設計出一個方案并實現出來,而是需要在若干個可能的方案中,甄選出最簡單實用的那個.
這中間往往需要經過幾輪思考——討論——推翻的迭代過程,謀定而后動有不少好處,一方面可以避免做出華而不實的過度設計,提升效率;另一方面,通過詳盡的討論出來的看似簡單的方案,細節考究,往往是可靠性最好的方案.
邏輯層的業務邏輯服務最早只有一個服務模塊(我們稱之為mmweb),囊括了所有提供給客戶端訪問的API,甚至還有一個完整的微信官網.這個模塊架構類似Apache,由一個CGI容器(CGIHost)和若干CGI組成(每個CGI即為一個API),不同之處在于每個CGI都是一個動態庫so,由CGIHost動態加載.
在mmweb的CGI數量相對較少的時候,這個模塊的架構完全能滿足要求,但當功能迭代加快,CGI量不斷增多之后,開始出現問題:
1) 每個CGI都是動態庫,在某些CGI的共用邏輯的接口定義發生變化時,不同時期更新上線的CGI可能使用了不同版本的邏輯接口定義,會導致在運行時出現詭異結果或者進程crash,而且非常難以定位;
2) 所有CGI放在一起,每次大版本發布上線,從測試到灰度再到全面部署完畢,都是一個很漫長的過程,幾乎所有后臺開發人員都會被同時卡在這個環節,非常影響效率;
3) 新增的不太重要的CGI有時穩定性不好,某些異常分支下會crash,導致CGIHost進程無法服務,發消息這些重要CGI受影響沒法運行.
于是我們開始嘗試使用一種新的CGI架構——Logicsvr.
除了API服務外,其他后臺服務模塊也遵循“大系統小做”這一實踐準則,微信后臺服務模塊數從微信發布時的約10個模塊,迅速上漲到數百個模塊.
這一時期,后臺故障很多.比故障更麻煩的是,因為監控的缺失,經常有些故障我們沒法第一時間發現,造成故障影響面被放大.
監控的缺失一方面是因為在快速迭代過程中,重視功能開發,輕視了業務監控的重要性,有故障一直是兵來將擋水來土掩;另一方面是基礎設施對業務邏輯監控的支持度較弱.基礎設施提供了機器資源監控和Svrkit服務運行狀態的監控.這個是每臺機器、每個服務標配的,無需額外開發,但是業務邏輯的監控就要麻煩得多了.當時的業務邏輯監控是通過業務邏輯統計功能來做的,實現一個監控需要4步:
1) 申請日志上報資源;
2) 在業務邏輯中加入日志上報點,日志會被每臺機器上的agent收集并上傳到統計中心;
3) 開發統計代碼;
4) 實現統計監控頁面.
可以想象,這種費時費力的模式會反過來降低開發人員對加入業務監控的積極性.于是有一天,我們去公司內的標桿——即通后臺(QQ后臺)取經了,發現解決方案出乎意料地簡單且強大:
3.1) 故障報告
之前每次故障后,是由QA牽頭出一份故障報告,著重點是對故障影響的評估和故障定級.新的做法是每個故障不分大小,開發人員需要徹底復盤故障過程,然后商定解決方案,補充出一份詳細的技術報告.這份報告側重于:如何避免同類型故障再次發生、提高故障主動發現能力、縮短故障響應和處理過程.
3.2) 基于 ID-Value 的業務無關的監控告警體系
基于 ID-Value 的監控告警體系
微信后臺每個存儲服務都有自己獨立的存儲模塊,是相互獨立的.每個存儲服務都有一個業務訪問模塊和一個底層存儲模塊組成.業務訪問層隔離業務邏輯層和底層存儲,提供基于RPC的數據訪問接口;底層存儲有兩類:SDB和MySQL.
SDB適用于以用戶UIN(uint32_t)為Key的數據存儲,比方說消息索引和聯系人.優點是性能高,在可靠性上,提供基于異步流水同步的Master-Slave模式,Master故障時,Slave可以提供讀數據服務,無法寫入新數據.
由于微信賬號為字母+數字組合,無法直接作為SDB的Key,所以微信帳號數據并非使用SDB,而是用MySQL存儲的.MySQL也使用基于異步流水復制的Master-Slave模式.
第1版的帳號存儲服務使用Master-Slave各1臺.Master提供讀寫功能,Slave不提供服務,僅用于備份.當Master有故障時,人工切讀服務到Slave,無法提供寫服務.為提升訪問效率,我們還在業務訪問模塊中加入了memcached提供Cache服務,減少對底層存儲訪問.
第2版的帳號存儲服務還是Master-Slave各1臺,區別是Slave可以提供讀服務,但有可能讀到臟數據,因此對一致性要求高的業務邏輯,例如注冊和登錄邏輯只允許訪問Master.當Master有故障時,同樣只能提供讀服務,無法提供寫服務.
第3版的帳號存儲服務采用1個Master和多個Slave,解決了讀服務的水平擴展能力.
第4版的帳號服務底層存儲采用多個Master-Slave組,每組由1個Master和多個Slave組成,解決了寫服務能力不足時的水平擴展能力.
最后還有個未解決的問題:單個Master-Slave分組中,Master還是單點,無法提供實時的寫容災,也就意味著無法消除單點故障.另外Master-Slave的流水同步延時對讀服務有很大影響,流水出現較大延時會導致業務故障.于是我們尋求一個可以提供高性能、具備讀寫水平擴展、沒有單點故障、可同時具備讀寫容災能力、能提供強一致性保證的底層存儲解決方案,最終KVSvr應運而生.
KVSvr使用基于Quorum的分布式數據強一致性算法,提供Key-Value/Key-Table模型的存儲服務.傳統Quorum算法的性能不高,KVSvr創造性地將數據的版本和數據本身做了區分,將Quorum算法應用到數據的版本的協商,再通過基于流水同步的異步數據復制提供了數據強一致性保證和極高的數據寫入性能,另外KVSvr天然具備數據的Cache能力,可以提供高效的讀取性能.
KVSvr一舉解決了我們當時迫切需要的無單點故障的容災能力.除了第5版的帳號服務外,很快所有SDB底層存儲模塊和大部分MySQL底層存儲模塊都切換到KVSvr.隨著業務的發展,KVSvr也不斷在進化著,還配合業務需要衍生出了各種定制版本.現在的KVSvr仍然作為核心存儲,發揮著舉足輕重的作用.
這種需求越來越多,我們就開始做一個媒體平臺,這個平臺后來從微信后臺分出,演變成了微信公眾平臺,獨立發展壯大,開始了微信的平臺化之路.除微信公眾平臺外,微信后臺的外圍還陸續出現了微信支付平臺、硬件平臺等等一系列平臺.
微信平臺
微信走出國門的嘗試開始于3.0版本.從這個版本開始,微信逐步支持繁體、英文等多種語言文字.不過,真正標志性的事情是第一個海外數據中心的投入使用.
海外數據中心的定位是一個自治的系統,也就是說具備完整的功能,能夠不依賴于國內數據中心獨立運作.
1) 多數據中心架構
多數據中心架構
系統自治對于無狀態的接入層和邏輯層來說很簡單,所有服務模塊在海外數據中心部署一套就行了.
但是存儲層就有很大麻煩了——我們需要確保國內數據中心和海外數據中心能獨立運作,但不是兩套隔離的系統各自部署,各玩各的,而是一套業務功能可以完全互通的系統.因此我們的任務是需要保證兩個數據中心的數據一致性,另外Master-Master架構是個必選項,也即兩個數據中心都需要可寫.
2) Master-Master 存儲架構
多數據中心的數據Master-Master架構
3) 數據中心間的數據一致性
這個Master-Master架構可以在不同數據中心間實現數據最終一致性.如何保證業務邏輯在這種數據弱一致性保證下不會出現問題?
這個問題可以被分解為2個子問題:
用戶訪問自己的數據
用戶可以滿世界跑,那是否允許用戶就近接入數據中心就對業務處理流程有很大影響.如果允許就近接入,同時還要保證數據一致性不影響業務,就意味著要么用戶數據的Master需要可以動態的改變;要么需要對所有業務邏輯進行仔細梳理,嚴格區分本數據中心和跨數據中心用戶的請求,將請求路由到正確的數據中心處理.
考慮到上述問題會帶來很高昂的實現和維護的復雜度,我們限制了每個用戶只能接入其歸屬數據中心進行操作.如果用戶發生漫游,其漫游到的數據中心會自動引導用戶重新連回歸屬數據中心.
這樣用戶訪問自己數據的一致性問題就迎刃而解了,因為所有操作被限制在歸屬數據中心內,其數據是有強一致性保證的.此外,還有額外的好處:用戶自己的數據(如:消息和聯系人等)不需要在數據中心間同步,這就大大降低了對數據同步的帶寬需求.
用戶訪問其他用戶的數據
由于不同數據中心之間業務需要互通,用戶會使用到其他數據中心用戶創建的數據.例如,參與其他數據中心用戶創建的群聊,查看其他數據中心用戶的朋友圈等.
仔細分析后可以發現,大部分場景下對數據一致性要求其實并不高.用戶稍遲些才見到自己被加入某個其他數據中心用戶建的群、稍遲些才見到某個好友的朋友圈動態更新其實并不會帶來什么問題.在這些場景下,業務邏輯直接訪問本數據中心的數據.
當然,還是有些場景對數據一致性要求很高.比方說給自己設置微信號,而微信號是需要在整個微信帳號體系里保證唯一的.我們提供了全局唯一的微信號申請服務來解決這一問題,所有數據中心通過這個服務申請微信號.這種需要特殊處置的場景極少,不會帶來太大問題.
4) 可靠的數據同步
海外數據中心建設周期長,投入大,微信只在香港和加拿大有兩個海外數據中心.但世界那么大,即便是這兩個數據中心,也還是沒法輻射全球,讓各個角落的用戶都能享受到暢快的服務體驗.
通過在海外實際對比測試發現,微信客戶端在發消息等一些主要使用場景與主要競品有不小的差距.為此,我們跟公司的架構平臺部、網絡平臺部和國際業務部等兄弟部門一起合作,圍繞海外數據中心,在世界各地精心選址建設了數十個POP點(包括信令加速點和圖片CDN網絡).另外,通過對移動網絡的深入分析和研究,我們還對微信的通訊協議做了大幅優化.微信最終在對比測試中趕上并超過了主要的競品.
2013.7.22 微信發生了有史以來最大規模的故障,消息收發和朋友圈等服務出現長達5個小時的故障,故障期間消息量跌了一半.故障的起因是上海數據中心一個園區的主光纖被挖斷,近2千臺服務器不可用,引發整個上海數據中心(當時國內只有這一個數據中心)的服務癱瘓.
故障時,我們曾嘗試把接入到故障園區的用戶切走,但收效甚微.雖然數百個在線模塊都做了容災和冗余設計,單個服務模塊看起來沒有單點故障問題;但整體上看,無數個服務實例散布在數據中心各個機房的8千多臺服務器內,各服務RPC調用復雜,呈網狀結構,再加上缺乏系統級的規劃和容災驗證,最終導致故障無法主動恢復.在此之前,我們知道單個服務出現單機故障不影響系統,但沒人知道2千臺服務器同時不可用時,整個系統會出現什么不可控的狀況.
其實在這個故障發生之前3個月,我們已經在著手解決這個問題.當時上海數據中心內網交換機異常,導致微信出現一個出乎意料的故障,在13分鐘的時間里,微信消息收發幾乎完全不可用.在對故障進行分析時,我們發現一個消息系統里一個核心模塊三個互備的服務實例都部署在同一機房.該機房的交換機故障導致這個服務整體不可用,進而消息跌零.這個服務模塊是最早期(那個時候微信后臺規模小,大部分后臺服務都部署在一個數據園區里)的核心模塊,服務基于3機冗余設計,年復一年可靠地運行著,以至于大家都完全忽視了這個問題.
為解決類似問題,三園區容災應運而生,目標是將上海數據中心的服務均勻部署到3個物理上隔離的數據園區,在任意單一園區整體故障時,微信仍能提供無損服務.
1) 同時服務
傳統的數據中心級災備方案是“兩地三中心”,即同城有兩個互備的數據中心,異地再建設一個災備中心,這三個數據中心平時很可能只有一個在提供在線服務,故障時再將業務流量切換到其他數據中心.這里的主要問題是災備數據中心無實際業務流量,在主數據中心故障時未必能正常切換到災備中心,并且在平時大量的備份資源不提供服務,也會造成大量的資源浪費.
三園區容災的核心是三個數據園區同時提供服務,因此即便某個園區整體故障,那另外兩個園區的業務流量也只會各增加50%.反過來說,只需讓每個園區的服務器資源跑在容量上限的2/3,保留1/3的容量即可提供無損的容災能力,而傳統“兩地三中心”則有多得多的服務器資源被閑置.此外,在平時三個園區同時對外服務,因此我們在故障時,需要解決的問題是“怎樣把業務流量切到其他數據園區?”,而不是“能不能把業務流量切到其他數據園區?”,前者顯然是更容易解決的一個問題.
2) 數據強一致
三園區容災的關鍵是存儲模塊需要把數據均勻分布在3個數據園區,同一份數據要在不同園區有2個以上的一致的副本,這樣才能保證任意單一園區出災后,可以不中斷地提供無損服務.由于后臺大部分存儲模塊都使用KVSvr,這樣解決方案也相對簡單高效——將KVSvr的每1組機器都均勻部署在3個園區里.
3) 故障時自動切換
4) 容災效果檢驗
我們在一番勒緊褲腰帶節省機器資源、消滅低負載機器后,所有機器的負載都上來了,服務過載變得經常發生了.解決這一問題的有力武器是Svrkit框架里的具有QoS保障的FastReject機制,可以快速拒絕掉超過服務自身處理能力的請求,即使在過載時,也能穩定地提供有效輸出.
近年,互聯網安全事件時有發生,各種拖庫層出不窮.為保護用戶的隱私數據,我們建設了一套數據保護系統——全程票據系統.其核心方案是,用戶登錄后,后臺會下發一個票據給客戶端,客戶端每次請求帶上票據,請求在后臺服務的整個處理鏈條中,所有對核心數據服務的訪問,都會被校驗票據是否合法,非法請求會被拒絕,從而保障用戶隱私數據只能用戶通過自己的客戶端發起操作來訪問.
基于Quorum算法的KVSvr已經實現了強一致性、高可用且高性能的Key-Value/Key-Table存儲.最近,微信后臺又誕生了基于Paxos算法的另一套存儲系統,首先落地的是PhxSQL,一個支持完整MySQL功能,又同時具備強一致性、高可用和高性能的SQL存儲.
文章來自微信公眾號:互聯網架構師
轉載請注明本頁網址:
http://www.snjht.com/jiaocheng/3749.html