Zeppelin:一個分散式KV存儲平台之概述

過去的一年多的時間中,大部分的工作都圍繞著Zeppelin這個項目展開,經歷了Zeppelin的從無到有,再到逐步完善穩定。見證了Zeppelin的成長的同時,Zeppelin也見證了我的積累進步。對我而言,Zeppelin就像是孩提時代一同長大的朋友,在無數次的遊戲和談話中,交換對未知世界的感知,碰撞對未來的憧憬,然後刻畫出更好的彼此。這篇博客中就向大家介紹下我的這位老朋友。Zeppelin是一個高性能,高可用的分散式Key-Value存儲平台,以高性能、大集群為目標,說平台是因為Zeppelin不是終點而是起點,在Zeppelin的基礎上,不僅能夠提供KV的訪問,還可以通過簡單的一層轉換滿足更複雜的協議需求。本文就將從背景,技術細節,回顧和未來計劃幾個方面來進行介紹。

背景

Zeppelin的故事首先從我們之前的一個項目Pika說起,Pika是一個完全兼容Redis協議的單機存儲,用多線程及LSM的方式,在降低Redis內存成本的同時基本保持了其高性能的特點。 正是由於Pika項目在公司內外的普及,讓我們認識到有大量需要高性能的存儲需求,同時隨著Pika項目的推進,以及業務的發展,這種曾經被我們定義為緩存的需求正向著更大容量和更高性能發展,因此一個大容量高性能的分散式Pika勢在必行。

同時,維護Ceph的經驗給我們強化了一個認識,那就是從一個原子的用戶介面出發可以很方便的構建出各種複雜的上層需求和用戶介面,正如Ceph從一個高一致的對象存儲平台Rados出發構建了對象存儲、塊存儲和文件存儲。Zeppelin作為一個高性能的KV存儲平台,可以向上構建高性能S3,Table Store,Redis協議等,可以看出並沒有一個合適的開源實現能夠同時滿足我們的需求。

最後,之前的項目Pika、QConf、Bada等給我們積累了不少的經驗和豐富穩定的基礎庫,包括網路庫Pink,輔助庫Slash,引擎庫Nemo,一致性庫Floyd,再加上我們對Rocksdb的積累。這時我們離需要的高性能KV存儲平台其實已經並不遙遠。再加上陳宗志同學的蜜汁不屑,Zeppelin就開始了自己的征程。從2016年7月正式立項,到半年後2017年3月0.3.1版本開始接入業務,再到現在1.2.3版本,Zeppelin已經逐步完善穩定,並接入包括搜索,代碼發布,信息流,靜床在內的眾多業務的近二十個集群。

通過上面的背景介紹,可以看出在設計之初,我們就對Zeppelin有如下幾個主要期許:

  • 高性能:Zeppelin和Pika的立命之本,因此無論語言選擇,副本方式,引擎選擇還是其他結構設計都不能以犧牲性能作為代價。
  • 大集群:因此需要有更好的可擴展性和必要的業務隔離及配額;
  • 作為支撐平台,向上支撐更豐富的協議;

Zeppelin的整個設計和實現都圍繞這三個目標努力。這裡將從API、數據分布、元信息管理、一致性、副本策略、數據存儲、故障檢測幾個方面來分別介紹其技術細節。

API

為了讓讀者對Zeppelin有個整體印象,先介紹下其提供的介面:

  • 基本的KV存儲相關介面:Set、Get、Delete;
  • 支持TTL;
  • HashTag及針對同一HashTag的Batch操作,Batch保證原子,這一支持主要是為了支撐上層更豐富的協議。

數據分布

最為一個分散式存儲,首要需要解決的就是數據分布的問題。另一篇博客淺談分散式存儲系統數據分布方法中介紹了可能的數據分布方案,Zeppelin選擇了比較靈活的分片的方式,如下圖所示:

用邏輯概念Table區分業務,並將Table的整個Key Space劃分為相同大小的分片(Partition),每個分片的多副本分別存儲在不同的存儲節點(Node Server)上,因而,每個Node Server都會承載多個Partition的不同副本。Partition個數在Table創建時確定,更多的Partition數會帶來更好的數據均衡效果,提供擴展到更大集群的可能,但也會帶來元信息膨脹的壓力。實現上,Partition又是數據備份、數據遷移、數據同步的最小單位,因此更多的Partition可能帶來更多的資源壓力。Zeppelin的設計實現上也會盡量降低這種影響。

可以看出,分片的方式將數據分布問題拆分為兩層隱射:從Key到Partition的映射可以簡單的用Hash實現。而Partition副本到存儲節點的映射相對比較複雜,需要考慮穩定性、均衡性、節點異構及故障域隔離(更多討論見淺談分散式存儲系統數據分布方法)。關於這一層映射,Zeppelin的實現參考了CRUSH對副本故障域的層級維護方式,但擯棄了CRUSH對降低元信息量稍顯偏執的追求。

在進行創建Table、擴容、縮容等集群變化的操作時,用戶需要提供整個:

  • 集群分層部署的拓撲信息(包含節點的機架、機器等部署信息);
  • 存儲節點權重;
  • 各個故障層級的分布規則;

Zeppelin根據這些信息及當前的數據分布直接計算出完整的目標數據分布,這個過程會盡量保證數據均衡及需要的副本故障域。下圖舉例展示了,副本在機架(cabinet)級別隔離的規則及分布方式。更詳細的介紹見Decentralized Placement of Replicated Data

元信息管理

上面確定了分片的數據分布方式,可以看出,包括各個分片副本的分布情況在內的元信息需要在整個集群間共享,並且在變化時及時擴散,這就涉及到了元信息管理的問題,通常有兩種方式:

  • 有中心的元信息管理:由中心節點來負責整個集群元信息的檢測、更新和維護,這種方式的優點是設計簡潔清晰,容易實現,且元信息傳播總量相對較小並且及時。最大的缺點就是中心節點的單點故障。以BigTable和Ceph為代表。
  • 對等的元信息管理:將集群元信息的處理負擔分散到集群的所有節點上去,節點間地位一致。元信息變動時需要採用Gossip等協議來傳播,限制了集群規模。而無單點故障和較好的水平擴展能力是它的主要優點。Dynamo和Redis Cluster採用的是這種方式。

考慮到對大集群目標的需求,Zeppelin採用了有中心節點的元信息管理方式。其整體結構如下圖所示:

可以看出Zeppelin有三個主要的角色,元信息節點Meta Server、存儲節點Node Server及Client。Meta負責元信息的維護、Node的存活檢測及元信息分發;Node負責實際的數據存儲;Client的首次訪問需要先從Meta獲得當前集群的完整數據分布信息,對每個用戶請求計算正確的Node位置,並發起直接請求。

為了減輕上面提到的中心節點的單點問題。我們採取了如下策略:

  • Meta Server以集群的方式提供服務,之間以一致性演算法來保證數據正確。
  • 良好的Meta設計:包括一致性數據的延遲提交;通過Lease讓Follower分擔讀請求;粗粒度的分散式鎖實現;合理的持久化及臨時數據劃分等。更詳細的介紹見:Zeppelin不是飛艇之元信息節點
  • 智能Client:Client承擔更多的責任,比如緩存元信息;維護到Node Server的鏈接;計算數據分布的初始及變化。
  • Node Server分擔更多責任:如元信息更新由存儲節點發起;通過MOVE,WAIT等信息,實現元信息變化時的客戶端請求重定向,減輕Meta壓力。更詳細的介紹見:Zeppelin不是飛艇之存儲節點

通過上面幾個方面的策略設計,盡量的降低對中心節點的依賴。即使Meta集群整個異常時,已有的客戶端請求依然能正常進行。

一致性

上面已經提到,中心元信息Meta節點以集群的方式進行服務。這就需要一致性演算法來保證:

即使發生網路分區或節點異常,整個集群依然能夠像單機一樣提供一致的服務,即下一次的成功操作可以看到之前的所有成功操作按順序完成。

Zeppelin中採用了我們的一致性庫Floyd來完成這一目標,Floyd是Raft的C++實現。更多內容可以參考:Raft和它的三個子問題。

利用一致性協議,Meta集群需要完成Node節點的存活檢測、元信息更新及元信息擴散等任務。這裡需要注意的是,由於一致性演算法的性能相對較低,我們需要控制寫入一致性庫的數據,只寫入重要、不易恢復且修改頻度較低的數據。

副本策略

為了容錯,通常採用數據三副本的方式,又由於對高性能的定位,我們選擇了Master,Slave的副本策略。每個Partition包含至少三個副本,其中一個為Master,其餘為Slave。所有的用戶請求由Master副本負責,讀寫分離的場景允許Slave也提供讀服務。Master處理的寫請求會在修改DB後寫Binlog,並非同步的將Binlog同步給Slave。

上圖所示的是Master,Slave之間建立主從關係的過程,右邊為Slave。當元信息變化時,Node從Meta拉取最新的元信息,發現自己是某個Partition新的Slave時,將TrySync任務通過Buffer交給TrySync Moudle;TrySync Moudle向Master的Command Module發起Trysync;Master生成Binlog Send任務到Send Task Pool;Binlog Send Module向Slave發送Binlog,完成數據非同步複製。更詳細內容見:Zeppelin不是飛艇之存儲節點。未來也考慮支持Quorum及EC的副本方式來滿足不同的使用場景。

數據存儲

Node Server最終需要完成數據的存儲及查詢等操作。Zeppelin目前採用了Rocksdb作為存儲引擎,每個Partition副本都會佔有獨立的Rocksdb實例。採用LSM方案也是為了對高性能的追求,相對於B+Tree,LSM通過將隨機寫轉換為順序寫大幅提升了寫性能,同時,通過內存緩存保證了相對不錯的讀性能。庖丁解LevelDB之概覽中以LevelDB為例介紹了LSM的設計和實現。

然而,在數據Value較大的場景下,LSM寫放大問題嚴重。為了高性能,Zeppelin大多採用SSD盤,SSD的隨機寫和順序寫之間的差距並不像機械盤那麼大,同時SSD又有擦除壽命的問題,因此LSM通過多次重複寫換來的高性能優勢不太划算。而Zeppelin需要對上層不同協議的支撐,又不可避免的會出現大Value,LSM upon SSD針對這方面做了更多的討論,包括這種改進在內的其他針對不同場景的存儲引擎及可插拔的設計也是Zeppelin未來的發展方向。

故障檢測

一個好的故障檢測的機制應該能做到如下幾點:

  • 及時:節點發生異常如宕機或網路中斷時,集群可以在可接受的時間範圍內感知;
  • 適當的壓力:包括對節點的壓力,和對網路的壓力;
  • 容忍網路抖動
  • 擴散機制:節點存活狀態改變導致的元信息變化需要通過某種機制擴散到整個集群;

Zeppelin 中的故障可能發生在元信息節點集群或存儲節點集群,元信息節點集群的故障檢測依賴下層的Floyd的Raft實現,並且在上層通過Jeopardy階段來容忍抖動。更詳細內容見:Zeppelin不是飛艇之元信息節點。

而存儲節點的故障檢測由元信息節點負責, 感知到異常後,元信息節點集群修改元信息、更新元信息版本號,並通過心跳通知所有存儲節點,存儲節點發現元信息變化後,主動拉去最新元信息並作出相應改變。

最後,Zeppelin還提供了豐富的運維、監控數據,以及相關工具。方便通過Prometheus等工具監控展示。

回顧及未來發展

通過本文對Zeppelin設計的介紹,可以看出Zeppelin並不是一個適用於任何場景的萬能葯,它一直圍繞自己的高性能、易擴展、支持上層協議的目標,也就犧牲了對一致性的滿足,因此Zeppelin並不適合對數據一致性要求高的需求場景,同時也不能支持像資料庫、文件系統、塊存儲等對一致性要求很高的上層協議。

目前Zeppelin已經完成了包括擴容縮容,中心節點成員變化在內的大部分作為分散式存儲的基本需求。下一步會依然圍繞我們的設計初心,同時針對目前的一些問題進行進一步的迭代,包括:

  • 可選的存儲引擎:目前的Rocksdb存儲引擎有自己的應用場景限制,比如在大value情況下顯著的讀寫放大,空間放大,以及讀場景對緩存的過分依賴。而支持更多的上層協議就需要面對更多的數據和業務場景,因此可選的存儲引擎就成為一個急迫的發展方向,包括WiscKey,BitCast在內的其他存儲引擎也會成為Zeppelin的選項。
  • 跨機房同步:目前的Zeppelin集群分機房部署,而越來越多的業務出現對跨機房同步的需要。
  • 更豐富的語言介面:目前已經支持C++,Go,Python及Php。
  • 更精確地運維控制:比如對不同副本DB的Compaction時機的控制,數據恢復時更動態的流量控制,暴露更多的內部狀態數據等。
  • 上層協議的支持和完善:目前已經支持了簡單的TableStore及高性能S3,同時支持上層協議也需要更好的對協議元數據的管理方式,目前Batch操作原子性需要通過HashTag限制到一個分片。

相關

Zeppelin

Floyd

Raft

淺談分散式存儲系統數據分布方法

Decentralized Placement of Replicated Data

Zeppelin不是飛艇之元信息節點

Zeppelin不是飛艇之存儲節點

Raft和它的三個子問題

庖丁解LevelDB之概覽

LSM upon SSD

推薦閱讀:

淺顯易懂地解讀Paxos演算法
分散式系統理論基礎 - 時間、時鐘和事件順序
Scaling Memcache in Facebook 筆記(二)
分散式系統理論進階 - Raft、Zab

TAG:分布式存储 | 分布式系统 |