微信後臺基於時間序的海量資料冷熱分級架構設計實踐

IT技術精選文摘發表於2018-05-15

1、寫在前面


微信的後臺資料儲存隨著微信產品特性的演進,經歷了數次的架構改造,才形成如今成熟的大規模分散式儲存系統,有條不紊的管理著由數千臺異構機型組成的機器叢集,得以支撐每天千萬億級的訪問、鍵值以及 PB 級的資料。

作為以手機為平臺的移動社交應用,微信內大部分業務生成的資料是有共性可言的:資料鍵值帶有時間戳資訊,並且單使用者資料隨著時間在不斷的生成。我們將這類資料稱為基於時間序的資料。比如朋友圈中的發表,或者移動支付的賬單流水等業務生成的資料都滿足這樣的特徵。基於時間序的資料都天然帶有冷熱分明屬性――這是由手機的物理特性決定的,它的尺寸有限的螢幕所展示的資料只能分屏,通過手指的滑動,平滑而又連續的沿時間軸依次訪問――通常是由最新生成的資料,慢慢回溯到較早前的資料。同時朋友圈等業務都是資訊讀擴散的應用場景,這就意味著它們生成的後臺資料具有讀多寫少的鮮明特徵。

在微信的實際應用場景中,這類資料的主要特點包括:資料量大、訪問量大、重要程度高等。

通過堆機器來橫向擴充套件儲存自然可以應對如上的各種挑戰,然而在成本預算緊張的前提下,機器數目是有限的。在這種情況下,基於時間序的海量資料的冷熱分級架構便應運而生。該架構正是為了應對後臺日益膨脹的這類資料,本著充分利用機器資源,發揮各種硬體介質特長的原則,結合資料的冷熱分明、讀多寫少的訪問特徵而開發和設計出來的。它基於資料分層的理念,根據不同時間段的資料在訪問熱度和資料量上的差異,定製不同的服務策略,在縱向上擴充套件儲存的邊界。橫向擴充套件儲存是易於理解的,通過向原叢集中增加相同型別的機器――其中必然涉及到一輪歷史資料的遷移――最終新舊機器負載均衡,彼此之間並無差異的對外提供服務。在這種方案下,資料橫向流動,系統一視同仁的對待,顯然並無因地制宜思想的容身之所。

通過這樣的一種縱向分層、單獨擴充套件的思路,即為我們系統提供了極大的靈活性,解決了節日期間儲存層面臨的記憶體瓶頸,以從長遠的角度為我們緩解了成本壓力,解決了儲存層面臨的磁碟容量瓶頸。

當然一套成功的大型分散式系統僅有這些是不夠的,還必須包括資料多副本複製策略以及分割槽演算法等,也要有能應對複雜的現網運營環境的能力。我們結合各層的服務特點,制訂了相對應的資料強一致演算法,如記憶體層通過版本號控制來保證與儲存層的完全一致,儲存層通過 Paxos Group 實現多副本容災,而機械盤層則通過序列寫來保證。我們同時也實現了自己的去中心化的資料路由演算法,確保了資料和流量的均勻分佈,並且保證這種特性在橫向擴充套件後依然成立。

通過如上工作的努力,環環相扣,我們的基於時間序的海量資料的冷熱分層架構成功的應對了 PB 級資料、千億級訪問以及萬億級鍵值帶來的挑戰。

2、系統設計


1資料模型

而特性 b) 則保證了本架構的必要性、實用性。如果資料規模有限,以使用者的賬戶資訊舉例,它就像我們日常生活中的戶口本,它只有一份,對單使用者而言不會新增。則我們通常用固定的機器叢集儲存就可以,並且鮮有變更。而我們要處理的是使用者的日記本、或者記賬簿,它們每天都在不斷生成新資料。

我們以現網某個叢集的例項情況舉例,說明下此類業務資料有如下的特點:

  • 1. 資料量大:PB 級資料,萬億級鍵值,並且在源源不斷的生成中,然而新生成的資料相較於歷史存量資料佔比小。下圖展示了該叢集資料在各時間段的一個佔比情況;

2. 訪問量大:峰值可達每分鐘數十億次訪問,尤其是在節日期間,使用者高漲的熱情更可以轉化成平日三至五倍的訪問量。同時具有冷熱分明、讀多寫少 (讀寫比例甚至可達 100:1) 的訪問特徵,比如節日期間倍增的訪問通常是對節日期間生成的新增資料的訪問。下圖展示了該叢集訪問在各時間段的一個佔比情況;


  • 3. 資料安全性要求高:這類資料通常是使用者感知敏感資料,一旦丟失,轉化成的使用者投訴率高。


2系統架構


系統由三個層次組成,如圖所求,分別是記憶體層、儲存層(熱資料儲存層)以及機械磁碟層(冷資料儲存層)。

從時間軸上看,它們服務的資料由熱至冷,如下圖所示:

從客戶端的角度看,三層都是並列的,客戶端都會直接的與某層中的某臺機器發生通訊。具體的區別點在於,記憶體層和機械磁碟層對客戶端而言是隻讀的。所有的寫都是由客戶端直接寫向儲存層。我們將去中心化的配置分發到客戶端機器上,配置的型別包括記憶體層路由、儲存層路由以及其它後設資料,客戶端根據配置中的時間分隔點以及流量比例,來決定將當前的讀請求分發到記憶體層還是儲存層的具體機器上。配置支援快速分發和動態載入,可以在秒級實現更新。

下面我們再詳細的分析各層的設計策略。

3記憶體層

我們通過版本號來實現了這一目的。我們為快取中的每一份資料都維持了一份版本號,儲存層中相應的也有一份。只有當快取中的版本號與儲存層的版本號達到一致時,才會認為快取中的資料是有效的。所以,客戶端每次對記憶體層的讀請求,都會由快取層相應的產生一次讀請求發到儲存層。在一次 RPC 請求中完成有效性的識別以及過期資料的更新。

從直覺上看,採用這種方案的強一致快取並沒有降低儲存層的訪問壓力。因為客戶端對快取層的請求,與快取層對儲存層的請求是 1:1 的。然而這個方案點的關鍵在於,我們成功的疏解了儲存層的記憶體瓶頸。將儲存層快取資料的功能,轉移到快取層的記憶體上。我們現在對儲存層的要求就是能夠儘量的快取更多的版本號,提供高效的版本號訪問能力就可以了。從這種意義上來看,這個強一致性快取就是儲存層記憶體的延伸。因此,我們將它稱為記憶體層。它的優勢在於可動態的調整流量比例,並且可以在訪問高峰期快速的擴容。後面的章節我們也描述瞭如何通過工程手段優化版本號互動帶來的資源消耗。

我們在記憶體層中設計了簡單、輕量的支援變長資料的快取結構。每臺機器包含數十條 LRU 鏈,每條鏈都是一個共享記憶體形式的一維陣列。所有的資料都追加寫在陣列的最新位置,到尾部後就從頭開始迴圈。自然,這樣的結構需要一個索引來記錄資料的位置。這種方式固然浪費一些記憶體空間,但避免了記憶體的動態分配。

4儲存層

因此我們採用了 lsm-tree 演算法來實現這一需求。該演算法和 B+ 樹一樣是種建立索引的技術。不同的是它基於多元件 (C0\C1 等元件),通過延遲提交和歸併排序的方式,將 B+ 樹的隨機 IO 轉變成了記憶體操作和順序 IO。在我們的訪問模型下,所有的寫都是熱點資料,只會提交到 C0 元件。然後在適當的時機,同 C1 元件中的資料進行多路歸併排序。通過該演算法,我們可以同時實現資料分層和資料有序的目的。

Leveldb 是 Google 公司開源的儲存引擎庫,它正是基於 lsm-tree 演算法的思想開發出來的。因此,我們複用了它成熟的資料結構元件,如日誌格式、資料檔案格式、記憶體表格式等。然而它其中的一些執行時策略,卻會給我們的現網運營帶來麻煩。比如說執行時不受限的 compact 策略、資料檔案索引的懶載入等,會觸發不可控的讀盤,造成服務的抖動;同時大量的動態記憶體分配也會對機器的記憶體使用帶來一定不可控的因素。因此,我們拋棄了這些執行時行為,定義了自己的管理策略,使系統變得更加可控。同時,我們利用不同資料的訪問差異,對冷、熱資料的儲存進行了各自的定製,按照時間段定義按塊壓縮的粒度、索引的粒度等,行之有效的調和了 CPU、記憶體、磁碟容量、磁碟 IO 等系統資源之間的轉換關係。

冷資料的連結和冷叢集的路由表,都是記錄在儲存層中而對前端不可見的。具體的設計思想,我們在下節中詳述。

5機械硬碟層


機械硬碟容量雖大,但是 IO 效能低下,故障率高。一種常見的思路是冷資料儲存層獨立與熱資料儲存層,而對客戶端直接可見――客戶端持有一份冷資料儲存層的路由,並且獨自路由――這無疑是種簡單、易於理解的方案,但是在我們的應用場景中面臨著兩個問題:無法精確防空以及加劇機械硬碟層的 IO 緊張。

定義 TB 訪問量為每 TB 資料的每秒的訪問次數。在我們的應用場景中,每 TB 歷史資料的實際訪問量設為 N,則機械硬碟的服務能力僅為 N 的一半。如果冷資料儲存層獨立,則它需要自己維持所有的資料索引,而記憶體容量不足以支援數十 T 資料的索引,只能將索引落盤,則每次對資料的讀取都要帶來兩次隨機讀盤。因此,我們將冷資料索引以及冷資料儲存層的路由表,都放到了熱資料儲存層中,而對前端不可見。

為了容災,我們必須為每份資料儲存多份副本。如果採用雙副本方案,則系統需要冗餘 50% 的訪問能力,以應對另外一份副本失效的情況,在 io 瓶頸的前提下,這種方案是不可取的。因此我們採用了三副本方案,只要冗餘三分之一的能力。每份副本分佈在不同的園區,可以實現園區級的容災。

由於機械盤容量大、計算能力差,我們採用 NO RAID 的方式組織了盤組。為了更好的實現單盤失效導致資料丟失的故障的災後恢復,我們實現了同組三臺機器在盤級別資料的完全相同。為了達到盤級別的負載均衡,我們通過預計算路由、硬編碼的方式,實現了 (資料 ->機器 ->盤 ->檔案) 的單調對映,由資料的鍵值可以直接定位到盤的索引以及檔案的編號。

作為機械硬碟層的補充,一個冷資料下沉的模組是必須的,它作為冷資料儲存層的唯一 Writer,我們通過兩階段提交的方式確保了下沉過程的透明性,通過控制流程發起時機保證資源使用不影響現網服務,通過預佔位、序列寫入的方式,確保了資料在冷資料儲存層檔案級別的完全一致。

3、資料強一致性保證


業務要求系統必須保證在資料的多份副本之間保持強一致性。――這是一個歷久彌新的挑戰。我們將分記憶體層、儲存層、機械硬碟層分別來考慮資料的強一致性維持。

1強一致快取


正如前文描述,記憶體層作為一種強一致性分散式快取,它完全是向儲存層對齊的,自身無法判別資料有效性,本身多副本之間也沒有互動的必要。它對前端而言是隻讀的,所有的寫請求並不通過它,它只能算是儲存層中資料的一個檢視。所以它對前端資料有效性的承諾完全是依賴於儲存層的正確性的。

2Paxos Group

Paxos Group 因為採用了無主模型,組內所有機器在任一時刻都處於相同的地位。Paxos 演算法本質是個多副本同步寫演算法,當且僅當系統中的多數派都接受相同值後,才會返回寫成功。因此任意單一節點的失效,都不會出現系統的不可用。

強一致性寫協議的主要問題來源於 Paxos 演算法本身,因為要確保資料被系統內的多數派接受,需要進行多階段的互動。我們採用如下的方法,解決了 paxos 演算法寫過程中出現的問題:基於 fast accept 協議優化了寫演算法,降低了寫盤量以及協議訊息傳送、接收次數,最終實現了寫耗時和失敗的降低;基於隨機避讓、限制單次 Paxos 寫觸發 Prepare 的次數等方法,解決了 Paxos 中的活鎖問題。

強一致性讀協議本身和 Paxos 演算法並沒有太大的關係,只要確認多副本之間的多數派,即可獲取到最新的資料。我們通過廣播的方式獲取到叢集中多數機器(包含自身)的 paxos log 的狀態,然後判斷本機資料的有效性。

為了防止多節點同時失效,我們將資料的多副本分佈在不同園區的機器上。園區是同一個城市不同資料中心的概念。如此,我們的結構足以應對單資料中心完全隔離級別的災難。

3序列寫入


因為對客戶端透明,冷資料下沉流程作為機械硬碟層的唯一寫者,則該層的資料一致性是易於實現的。我們通過三副本序列寫入、全部提交才算成功的方式來實現了多副本之間的資料一致性。

作為補充,冷資料叢集為資料塊增加了 CRC 校驗和一致性恢復佇列,當單機資料不可用 (丟失或者損壞) 時,首先客戶端會跳轉到其它備份中讀 (三機同時對外提供讀服務),一致性恢復佇列會非同步的從其它備份資料塊中恢復本機資料。

因為採用了 No Raid 方式組織的盤組,並且同組機器間盤級別資料檔案一致,在單盤故障引發資料丟失時,只要從其它機器相同序盤中傳輸資料檔案即可。

4、資料分割槽


1靜態對映表


資料分割槽的主要目的是為了確保同層機器間的負載均衡,並且當機器規模發生變化後,在最終仍然可以達到負載均衡的一種狀態。

經典的一致性雜湊演算法的初衷是為了健壯分散式快取,基於執行時動態的計算雜湊值和虛擬節點來進行定址。資料儲存與分散式快取的不同在於,儲存必須保證資料對映的單調性,而快取則無此要求,所以經典的一致性雜湊通常會使用機器 IP 等作為引數來進行雜湊,這樣造成的結果一方面是資料的落點時而發生改變,一方面是負載通常不均衡。因此我們改造了此演算法。

2組內均衡


組是資料分割槽的獨立單元,是虛擬節點對應的實體單位。組之間是互相獨立的。每組由多臺物理機器組成,這是 Paxos Group 生效的基本單位。一份資料包括的多份副本分別散落在組內的各臺機器上。為了在組內機器上保證負載均衡,我們同樣設計了一套演算法,規定了資料副本之間的訪問優先順序,前端會依優先順序逐一的請求資料,只要成功獲取,即中斷這個過程。然後我們再將副本按優先順序均勻的散落在組內機器上,如此即可實現組內負載的均衡。

3資料遷移


靜態對映表是非常靈活的,在不達到組數上限的情況下,可以任意的增加一組或者多組機器。當然這個過程中一些資料的路由對映發生了改變,則就涉及到了歷史資料的挪騰。為了在挪騰的過程中不影響服務,保證資料依然可讀可寫,我們開發出了對前端透明的,基於遷移標誌位,通過資料雙寫和非同步挪資料的方式實現的安全的、可回退的資料遷移流程。

4最小不變塊


儲存層和機械硬碟層通過冷資料連結耦合在了一起。因為兩層使用了相同的對映表,那麼當儲存層因擴容而發生遷移時,那麼冷資料連結無疑也要重新定址,進行一輪重新定位。如果我們以單鍵值為粒度記錄冷資料連結和進行冷資料下沉,那麼在萬億鍵值的語境下,效率無疑是低下。因此我們設計了最小不變塊的演算法,通過兩階段雜湊,使用中間的雜湊桶聚集資料,將資料鍵值和冷資料儲存層的機器路由隔離開來。通過該演算法,我們可以實現:批量的轉存冷資料、熱資料儲存層批量的以塊 (block) 為單位記錄冷資料連結、當熱資料儲存層發生擴容時,塊 (block) 內的資料不因擴容被打散掉,而可以整體的遷移到新目標機上。

5、工程實現


糟糕的工程實現可以毀掉一個完美的系統設計,因此,如何在工程實現的過程中,通過技術的手段,提升系統的表現,同樣值得重視。

1高效快取


記憶體層的設計嚴重依賴儲存層資料版本號的高效獲取,那自然是版本號請求全落在記憶體中就可以了。因此,針對這種情況我們為定長的版本號設計了一套極簡的、輕量的、行之有效的快取――記憶體容量不足以支撐版本號全快取。


它的資料結構只是一個二維陣列,一維用來構建 hash 鏈,一維用來實現 LRU 鏈。每次讀或者寫都需要通過陣列內資料的挪動,來進行更新。如此一來,我們就通過千萬級數目的 LRU 鏈群,實現了快取整體的 LRU 淘汰。它具有定長,可共享記憶體搭載,程式重啟不丟失、記憶體使用率高等優點。


2批量操作


對系統伺服器而言,前端訪問過來的某個請求,其對應的邏輯操作都是序列的,我們自然可以梳理這個序列流程中的 CPU 消耗點進行優化。然而當主要的瓶頸被逐漸的消滅掉後,CPU 消耗點變得分散,優化效果就變得微乎其微了。因此,我們只能尋找其它突破點。

3請求合併


既然單機的邏輯操作效能已經得到了極大的提升,那麼前後端的網路互動階段,包括接入層的打包解包、協議處理等環節,成為了資源的主要消耗點。參考批量操作的經驗,我們同樣使用批量化的技術來優化效能――即將後臺訪問過來的單條請求 (Get) 在記憶體層聚合成一次批量請求 (Batch Get)。

4路由收斂


因為每個資料都是根據鍵值單獨進行路由的,如果要進行請求合併,我們就必須確保同一個批量請求內的資料,都會定址到相同的 Paxos Group 上。因此,我們必須在記憶體層將落到同一臺儲存機器上的 Get 請求聚合起來。我們首先在記憶體層和儲存層採用了相同的路由演算法,然後將記憶體層的組數同儲存層的組數進行對齊,來完成了這一目標。

6、相關工作


在設計的階段,我們充分的調研了業界的各類方案,大到系統的整體架構,小到具體的技術點。各種方案自有應用場景、各有千秋,不能單純以好壞區別,我們同樣基於自己的業務場景,謹慎的選擇合適的方案,或者棄而不用。在此儘量敘述。

處理 SNS 類業務生成的資料,業界有多種的冷熱分離架構可以參考。我們以 Facebook 的 Cold Storage 系統舉例而言,它也是基於冷熱分層的想法,設計出了服務它們照片業務資料的儲存方案。不同的是它採用了軟硬體結合的方法,一方面定製專門的伺服器(包括硬碟、電源等)和資料中心,一方面降低冷資料的備份數、增加糾刪碼等手段。

同樣,業界有諸多關於如何實現資料一致性的方案。包括我們微信自研的 Quorum 協議,它是一種 NWR 協議,採用非同步同步的方式實現資料多副本。即然是非同步同步,那在多副本達到最終一致,必然存在一個時間差,那麼在單機出現離線的情況下,就會有一定概率導致資料的不可用。而我們追求的是在單點故障下,所有的資料都保證強可用性。

因此,我們採用了無主的去中心化的 Paxos Group 實現了這一目標,其中非租約是 PaxosStore 架構的一個創新亮點。在故障時通常系統是抖動的,會有時斷時續的狀況,常見的租約做法在這種場景下容易出現反覆切換主機而導致長期不可用,而 PaxosStore 的非租約結構能夠輕鬆應對,始終提供良好的服務。PaxosStore 核心程式碼正在整理開源當中,預計四季度會正式釋出,同時該專案的底層框架也基於我們已開源的協程庫 github.com/libco。

公眾號推薦:


相關文章