微服務的理念與騰訊一直倡導的「大系統小作」有不少相通之處,本文將分享微信後臺架構的服務發現、通訊機制、集羣管理等基礎能力與其上層服務劃分原則、代碼管理規則等。前端
背景介紹
首先,咱們須要敏捷開發。過去幾年,微信都是很敏捷地在開發一些業務。因此咱們的底層架構須要支撐業務的快速發展,會有一些特殊的需求。程序員
另外,目前整個微信團隊已經有一千多人了,開發人員也有好幾百。整個微信底層框架是統一的,微信後臺有千級模塊的系統。好比說某某服務,有上千個微服務在跑,而集羣機器數有幾萬臺,那麼在這樣的規模下,咱們會有怎麼樣的挑戰呢?web
咱們一直在說「大系統小作」,聯想一下,微服務與騰訊的理念有哪些相同與不一樣的地方呢?經過對比,最終發現仍是有許多相通的地方。因此我挑出來說講咱們的實踐。編程
看過過去幾個會議的內容,可能你們會偏向於講整一個大的框架,好比整個雲的架構。可是我這邊主要講的是幾個特殊的點。後端
概覽詳情
開始看一下咱們的結構。全球都有分佈,主要有上海、深圳、香港、加拿大幾個數據中心。緩存
其中上海服務國內北方的用戶,深圳負責南方用戶,加拿大服務北美、南美和歐洲,香港服務東南亞、中東和非洲地區。服務器
而後來看看咱們的架構:微信
- 最上邊是咱們的產品;
- 而後有一個號稱幾億在線的長鏈接和短鏈接的服務;
- 中間有一個邏輯層,後臺框架講的主要是邏輯層日後這塊,包括咱們的 RPC、服務分組、過載保護與數據存儲;
- 底層有個 PaxosStore 存儲平臺。
整套就是這麼個體系。微服務很容易去構建,可是規模變大後有哪些問題,須要哪些能力?這裏挑出三個點來說一下:網絡
1、敏捷
但願你的服務很快實現,不太多去考慮。像咱們早期互聯網業務,甚至包括 QQ 等,咱們很注重架構師的一個能力,他須要把握不少的東西。他設置每一個服務的時候,要先算好不少資源,算好容災怎麼作。容災這個問題直接影響業務怎麼去實現的,因此有可能你要作一個具體邏輯的時候要考慮不少問題,好比接入服務、數據同步、容災等等每一個點都要考慮清楚,因此節奏會慢。
2、容錯
當你的機器到了數萬臺,那天天都有大量機器會有故障。再細一點,能夠說是每個盤的故障更頻繁一點。
3、高併發
基礎架構
接下來看看咱們的基礎架構。
整個微服務的架構上,咱們一般分紅這些部分:
- 服務佈局
- 服務之間怎麼作一些遠程調用
- 容錯(主要講一下過載保護)
- 部署管理
服務佈局
分兩層,一個是城市間。城市之間的數據是相對獨立的,除了少數帳號全球同步,大部分業務都但願作成電子郵件式的服務,各自有自身的環境在跑,之間使用相似於電子郵件的通訊。因此咱們選擇讓每一個城市自治,它們之間有一個 200-400ms 的慢速網絡,國內會快點,30ms。
而城市內部,就是每一個園區是一套獨立的系統,能夠互相爲對方提供備份。要求獨立的電源與網絡接入。
城市內部會有整套的劃分,終端 --> 接入層 --> 邏輯層 --> 存儲層 都是徹底獨立的一套系統。
遠程調用
看到不少框架,居然是沒有協程的,這很詫異。早年咱們 QQ 郵箱、微信、圖像壓縮、反垃圾都是一個 web 服務,只有存儲層會獨立到後面去,甚至用 web 直連 MySQL。由於它早期比較小,後來變大以後就用微服務架構。
每一個東西都變成一個小的服務,他們是跨機的。你能夠想象一下,天天咱們不少人買早餐的時候,掏出手機作一個微信支付,這一個動做在後臺會引發上百次的調用。這有一個複雜的鏈路。在 2014 年以前,咱們微信就是沒有作異步的,都是同步的,在這麼多調用裏,A 服務調用 B,那要先等它返回,這樣就佔住了一條進程或者線程。因此其實 13 年的時候,咱們發生了大大小小的故障,很大一部分緣由就在這裏。
而後 13 年末的時候,這個問題太嚴重了,嚴重到,好比發消息的時候,你去拿一個頭像之類的,它只要抖動,就可能引起整一條調用鏈的問題,而且由於過程保護的不完善,它會把整個消息發送的曲線掉下去,這是咱們很痛苦的時間。
而後當時咱們就去考慮這些方案,13 年的時候抽出 3 我的從新作了一個完整的庫 libco。(兩千行),實現時間輪盤與事件處理鏈、經常使用網絡編程模式、同步原語等。它分爲三大塊,事件驅動、網絡 HOOK 和協程機制。
早期是多進程爲主,當年切多線程的時候,也遇到一大波修改,後來線程裏有了一個線程變量就好多了。若是沒有這個東西,你可能要把許多變量改爲參數再一層一層傳遞下去。有了線程變量就好多了。如今咱們的協程變量也是這個意義,效果就像寫一個宏同樣。
另外一個是,咱們支持 CGI,早期庫在 CGI 上遇到問題,因此沒有推廣。由於一個標準 CGI 服務是基於一些古老的接口的,像 getENV、setENV,就是說你的 coreString 是經過 ENV 來獲得的,那麼這個咱們也把它給 HOOK 掉了,它會根據你的協程去分派。
最難的一個是 gethostbyname 方法,我發現不少人就連在異步編程裏,處理 hostbyname 也多是用了一套獨立線程去作,或者你很辛苦地把整個代碼摳出來從新寫一遍,這個確定是有不少問題的。因此咱們 libco 就把這個 gethostbyname 給完整地支持了。
最後若是你還不爽,說通常業務邏輯能夠這麼幹,那我還有不少後臺代碼怎麼辦呢?不少有經驗的老的程序員可能要拿着他們那一堆很複雜的異步編程的代碼來質疑咱們,他們不認爲他們的代碼已經徹底能夠被協程所取代了。
他們有以下兩個質疑:
- 質疑性能:協程有不少切換,會不會帶來更大開銷?
- 你可能處理幾萬併發就好,消耗個 1G 內存就行,可是咱們這裏是處理千萬併發哦,這麼大的規模,我不信任你這個東西。
這樣咱們實際上是面臨了一個問題,由於一些老代碼,越是高級的人寫的,它的技術棧越深,稍微改動一點代碼,就出 BUG 了。
可是你用協程的話,不少變量就天然在一個連續的內存裏了,至關於一個小的內存池,就好比 if……else……這個你沒有必要去 new 一個東西保存狀態的,直接放在棧裏就好了,因此它的性能更好了。
第二個是,它要求很高的併發。因爲協程要一個棧,咱們通常開 128k,若是你對這個代碼掌控得比較好,可能開 16k,就算是這樣,你要開 1 萬個協程,仍是要 100 多 M 的內存。因此咱們後來就在這基礎上作了一個能夠支持千萬鏈接的協程模式。
Libco 是一個底層庫,讓你很方便開發,可是大部分開發人員不是直接面對 libco 的,咱們花了一年時間把整個微信後臺絕大部分邏輯服務、存儲服務改爲基於 libco,整個配置就直接經過配一臺機器上的併發數配 10 倍甚至 20、30 倍,這樣子就一會兒把整個問題解決了。
過載保護
併發數上去後容易引起另外一個問題,早期的時候,後端服務性能高,邏輯服務性能相對弱,很容易被 hold,不可能給後端發起不少鏈接,不具備「攻擊性」,但修改完成後,整個前端變得很強,那可能對後端產生很大的影響。這個時候就要來考慮一下過載保護了。
通常會提到幾個點。
1.輕重分離:
就是一個服務裏邊不要又有重的操做,又有輕的,這樣過載的時候,大量的請求都被某些小請求攔截掉了,資源被佔滿了。
2.隊列:
過載保護通常是說系統內部服務在作過去的事情,作無用功。它們可能待在某個隊列裏邊,好比服務時間要求 100ms,但它們老是在作 1s 之前的任務,因此整個系統會崩潰。因此老的架構師會注重說配好每個服務的隊列長度,估算好。可是在繁忙的開發中,是很難去控制的。
3.組合命令式:
後端服務並非只有一個,上邊這個圖中的例子,想要調用不少服務,而後 AB 都過載,它們每個其實都只是過載一點,經過率可達到 80%,可是前端須要這兩個服務的組合服務,那麼這裏就可能只能達到 60% 的經過率。而後後邊若是是更多的服務,那麼每一個服務的一點點過載,到了前端就是很嚴重的問題。怎麼解決呢?
這本書在 十二、13 年的時候很火,裏邊提到了兩個對咱們有用的點。
- 一個是「但願系統是分佈式的,去中心化」,指系統過載保護依賴每個節點自身的狀況去作,而不是下達一個統一的中心指令。
- 二是「但願整個控制是基於反饋的」,它舉了一些例子,像抽水馬桶,像過去鍊鋼鐵的參數很難配,可是隻要有一個反饋機制就好解決了。
因而咱們構建了一套看起來有點複雜的過載保護系統。
整個系統基於反饋,而後它把整個拒絕的信息全程傳遞了。看到最右邊,有幾個典型的服務,從一個 CGI 調用一個後臺服務,再調用另外一個後臺服務,它會在 CGI 層面就把它的重要程度往下傳。回到剛纔那個前端調用 A、B 服務的例子,使用這樣的一種重要程度傳遞,就能夠直接拒絕那些相同用戶的 20% 的請求,這樣就解決了這個問題。
怎麼配隊列?這個只是反映了生產者和服務者處理能力的差別,觀察這個差別,就能夠獲得一個好的拒絕的數。你不須要去配它多長,只須要去看一個請求在隊列裏待的平均時間是否能夠接受,是一個上漲趨勢仍是一個降低趨勢。這樣咱們就能夠決定要不要去拒絕。那這樣幾乎是全自動的。你只要配得相對大一點就好了,能夠抗一些抖動。在接入以前就評估它,在過去一段時間內平均隊列耗時多長,若是超過預支,咱們就往下調。這樣就把整個系統的過載能力提高了不少。
這是一個具體的作法,咱們會考慮兩個維度,一個是後臺服務,可能服務不少不一樣的前端,它可能來源於一個支付的請求,通過層層調用,到達後臺;或者是一個發消息的服務;它也多是一個不重要的小服務,若是這個帳戶服務過載的時候,那麼咱們能夠根據這個表來自動地優先去拒絕一些不那麼重要的服務請求,使得咱們核心服務能力能夠更好地提供。這樣整個系統就能夠作到很好的過載保護。
數據存儲
上邊提到一個數據層,那咱們是怎麼去作數據的呢?
在過去不少年裏,咱們多是儘量去事務化、不追求強一致,通常是採用主備同步的方法。但咱們的目標仍是強一致的存儲。
強一致是說,寫一個數據以後,服務器的返回成功不會由於單機故障而丟失。早年咱們用的是本身設計的協議,嚴格來證實的話,沒有 Paxos 這麼嚴謹,因此咱們在過去一年多的時間內,從新作了一個 Paxos 存儲。
它是一個同步複製的數據存儲,支持各個園區之間的數據一致性,而且是能夠多組多寫的,就是說任何一個園區接入,它均可以進行數據的強制讀寫。另外它並不僅是 key-value 模式,它支持 key-value、list、表。在微信這邊不多會說徹底依賴 key-value 的,由於不少業務都是有列表、表格等的請求,因此不少年前就開始用表格的存儲。
Paxos 可用性很高,因此咱們就敢作單表有億行的設計,這樣像公衆號粉絲等須要很大的,幾千萬甚至幾億行的記錄,就不用考慮本身去分表。而且這個存儲可使用類 SQL 的語句去作,它是徹底保證事務的。
它仍是插件化系統,不只支持 LSM,還支持其它存儲引擎。
而後它低成本,後臺 CPU 有 E3-1230V3,也有 E5-2670 型號的,內存,CPU 與 ssd 之間有一些能力用不上,因此咱們系統是能夠靈活組合不少不一樣存儲介質的。
這個系統是跑在同城的,也就是上海內部、深圳內部、加拿大內部和香港內部。它們之間的延遲相對較低,幾毫秒的級別。這是一個非租約的,沒有 leader,不存在切換的不可用期,隨時均可以切換任何一個園區。負載均衡這一塊咱們沿用 kb64 架構,6 臺機爲一組。由於園區故障少,平時單機時,分攤 25% 的流量,總體比較穩定。6 臺爲一組時,整個做爲一個 set,有不少 set 之間的適用一致性要去作,會有一個很細粒度的伸縮性,好比它能夠 100 組擴展到 101 組。
爲何用這麼重的方式呢?由於但願應用是 簡單快速 的,不用假設一個數據寫完以後還可能被回退掉,這樣只會有不少額外的開銷,會有不少問題。好比公衆號,他們有不少素材庫之類的很重要的存儲,若是數據忽然丟了,或者說回退了,沒有了,那用戶投訴是會很嚴重的。微信帳號這邊也是這樣,若是一個帳戶註冊了,可是這個數據回退了,那也是很嚴重的問題。
另外一個緣由是 可用性。在一個傳統的主備系統裏面,當主機掛掉,面臨切不切備機的抉擇,而後你會層層請示,說明目前的同步情況,甚至你不知道當前的同步情況,通過不少流程來請示是否切換備機。
而另外,它也不是一個高成本的方案。
爲何不用 Raft 呢?Raft 的開源頗有價值,它把互聯網後臺的數據一致性能力提高了不少,就算是一個很小的團隊,它也能直接用 Raft 得到一個強一致能力,而這可能就已經超過了許多互聯網後臺的強一致能力,由於不少後臺都是用了很古老的架構,好比長期用到主機架構。
Raft 與 Paxos 的區別是什麼呢?其實 Raft 和 Paxos 不是一個層面的概念,這個圖就是典型的經過一個 log 變動 db 的架構,經過三條 log 一致性作到數據持久強一致性。那 Paxos 在哪裏?在一個 log 的某一個 entry 那邊,三個點構成一個常量。
那 Raft 是什麼呢?它是整一個二維的東西,就是說,基於一個 Paxos 強一致協議作的一條 log,它整個就是一個 Raft。因此咱們能夠認爲 Raft 實際上是 Paxos(log)的一種選擇。若是你容許綠色部分不存在,那它就不是 Raft。由於 Raft 的設計是你本身作的,它與 Paxos 不要緊。
整個 PaxosStore 架構如圖:
它包含了不少層,包括緩存和匯聚層、同步複製的組件等。
這一套方案是在線上用了好幾千臺的,是一個非租約的方案。存儲引擎能夠自由定製。若是想用大表,那能夠用 leveldb。若是想用更強的 LSM,也能夠選擇。而後咱們也有不少 Bitcask 的模型,更適合於內存的 key-value。
因爲有幾萬臺機,因此變很重要,咱們也基於 BT 作了一套存儲方案。它會以園區爲根據地,一般一個變動,會以 BT 協議發送到每一個園區裏,而後園區內部把同機架機器分紅一個分組,而後分組內再互傳。就我瞭解,Facebook 和 Twitter、Ebay 都是這樣作的。