使用開源技術構建有贊分布式 KV 存儲服務

背景

在有贊早期的時候,當時只有 MySQL 做存儲,codis 做緩存,隨著業務發展,某些業務數據用 MySQL 不太合適, 而 codis 由於當緩存用, 並不適合做存儲系統, 因此, 急需一款高性能的 NoSQL 產品做補充。考慮到當時運維和開發人員都非常少, 我們需要一個能快速投入使用, 又不需要太多維護工作的開源產品。 當時對比了幾個開源產品, 最終選擇了 aerospike 作為我們的 KV 存儲方案。 事實證明, aerospike 作為一個成熟的商業化的開源產品承載了一個非常好的過渡時期 在很少量的開發和運維工作支持下, 一直穩定運行沒有什麼故障, 期間滿足了很多的業務需求, 也因此能抽出時間投入更多精力解決其他的中間件問題。

然而隨著有讚的快速發展, 單純的 aerospike 集群慢慢開始無法滿足越來越多樣的業務需求。 雖然性能和穩定性依然很優秀, 但是由於其索引必須加載到內存, 對於越來越多的海量數據, 存儲成本會居高不下。 更多的業務需求也決定了我們將來需要更多的數據類型來支持業務的發展。 為了充分利用已有的 aerospike 集群, 並考慮到當時的開源產品並無法滿足我們所有的業務需求, 因此我們需要構建一個能滿足有贊未來多年的 KV 存儲服務。

設計與架構

在設計這樣一個能滿足未來多年發展的底層 KV 服務, 我們需要考慮以下幾個方面:

  • 需要儘量使用有大廠背書並且活躍的開源產品, 避免過多的工作量和太長的週期
  • 避免完全依賴和耦合一個開源產品, 使得無法適應未來某個開源產品的不可控變化, 以及無法享受將來的技術迭代更新和升級
  • 避免使用過於複雜的技術棧, 增加後期運維成本
  • 由於業務需要, 我們需要有能力做方便的擴展和定製
  • 未來的業務需求發展多樣, 單一產品無法滿足所有的需求, 可能需要整合多個開源產品來滿足複雜多樣的需求
  • 允許 KV 服務後端的技術變化的同時, 對業務接口應該儘量穩定, 後繼升級不應該帶來過多的遷移成本。

基於以上幾點, 我們做了如下的架構設計:

使用開源技術構建有贊分佈式 KV 存儲服務

為了整合和方便以後的擴展, 我們使用 proxy 屏蔽了具體的後端細節, 並且使用廣泛使用的 redis 協議作為我們對上層業務的接口, 一方面充分利用了開源的 redis 客戶端產品減少了開發工作量, 一方面減少了業務的接入學習成本, 一方面也能對已經使用的 aerospike 集群和 codis 集群做比較平滑的整合減少業務遷移工作量。 在此架構下, 我們未來也能通過在 proxy 層面做一些協議轉換工作就能很方便的利用未來的技術成果, 通過對接更多優秀的開源產品來進一步擴展我們的 KV 服務能力。

有了此架構後, 我們就可以在不改動現有 aerospike 集群的基礎上, 來完善我們目前的KV服務短板, 因此我們基於幾個成熟的開源產品自研了 ZanKV 這個分佈式 KV 存儲。 自研 ZanKV 有如下特點:

  • 使用Go語言開發, 利用其高效的開發效率, 也能減少後期維護難度, 方便後期定製。
  • 使用大廠且成熟活躍的開源組件 etcd raft,RocksDB 等構建, 減少開發工作量
  • CP 系統和現有 aerospike 的 AP 系統結合滿足不同的需求
  • 提供更豐富的數據結構
  • 支持更大的容量, 和 aerospike 結合在不損失性能需求的前提下大大減少存儲成本

自研 ZanKV 的整體架構圖如下所示:

使用開源技術構建有贊分佈式 KV 存儲服務

整個集群由 placedriver + 數據節點 datanode + etcd + rsync 組成。 各個節點的角色如下:

  • PD node: 負責數據分佈和數據均衡, 協調集群裡面所有的 zankv node 節點, 將元數據寫入 etcd
  • datanode: 負責存儲具體的數據
  • etcd: 負責存儲元數據, 元數據包括數據分佈映射表以及其他用於協調的元數據
  • rsync: 用於傳輸 snapshot 備份文件

下面我們來一一講述具體的內部實現細節。

實現內幕

DataNode 數據節點

首先, 我們需要一個單機的高性能高可靠的 KV 存儲引擎作為基石來保障後面的所有工作的展開, 同時我們可能還需要考慮可擴展性, 以便未來引入更好的底層存儲引擎。 在這一方面, 我們選擇了 RocksDB 作為起點, 考慮到它的接口和易用性, 而且是 FB 經過多年的時間打造的一個已經比較穩定的開源產品, 它同時也是眾多開源產品的共同選擇, 基本上不會有什麼問題, 也能及時響應開源社區的需求。

RocksDB 僅僅提供了簡單的 Get,Set,Delete 幾個有限的接口, 為了滿足 redis 協議裡面豐富的數據結構, 我們需要在 KV 基礎上封裝更加複雜的數據結構, 因此我們在 RocksDB 上層構建了一個數據映射層來滿足我們的需求, 數據映射也是參考了幾個優秀的開源產品(pika, ledis, tikv 等)。

完成單機存儲後, 為了保證數據的可靠性, 我們通過 raft 一致性協議來可靠的將數據複製到多臺機器上, 確保多臺機器副本數據的一致性。 選擇 raft 也是因為 etcd 已經使用Go語言實現了一個比較完整且成熟的 raft library 供大家使用。但是 etcd 本身並不能支持海量數據的存儲, 因此為了能無限擴展存儲能力, 我們在 etcd raft 基礎上引入了 raft group 分區概念, 使得我們能夠通過不斷增加 raft 分區的方法來實現同時並行處理多個 raft 複製的能力。

最後, 我們通過 redis 協議來完成對外服務, 可以看到, 通過以上幾個分層 ZanKV DataNode 節點就能提供豐富的數據存儲服務能力了, 分層結構如下圖所示:

使用開源技術構建有贊分佈式 KV 存儲服務

Namespace 與分區

為了支持海量數據, 單一分區的 raft 集群是無法滿足無限擴展的目標的, 因此我們需要支持數據分區來完成 scale out。 業界常用的分區算法可以分為兩類: hash 分區和 range 分區, 兩種分區算法各有自己的適用場景, range 分區優勢是可以全局有序, 但是需要實現動態的 merge 和 split 算法, 實現複雜, 並且某些場景容易出現寫熱點。 hash 分區的優勢是實現簡單, 讀寫數據一般會比較均衡分散, 缺點是分區數一般在初始化時設定為固定值, 增減分區數需要遷移大量數據, 而且很難滿足全局有序的查詢。 綜合考慮到開發成本和某些數據結構的順序需求, 我們目前採取前綴 hash 分區算法, 這樣可以保證前綴相同的數據全局有序滿足一部分業務需求的同時, 減少了開發成本保證系統能儘快上線。

另外, 考慮到有贊今後的業務會越來越多, 未來需要能方便的隔離不同業務, 也方便不斷的加入新的特性同時能平滑升級, 我們引入了 namespace 的概念。 namespace 可以動態的添加到集群, 並且 namespace 之間的配置和數據完全隔離, 包括副本數, 分區數, 分區策略等配置都可以不同。 並且 namespace 可以支持指定一些節點放置策略, 保證 namespace 和某些特性的節點綁定(目前多機房方案通過機架感知方式實現副本至少分佈在一個以上機房)。 有了 namespace, 我們就可以把一些核心的業務和非核心的業務隔離到不同的 namespace 裡面, 也可以將不兼容的新特性加到新的 namespace 給新業務用, 而不會影響老的業務, 從而實現平滑升級。

PlaceDriver Node 全局管理節點

可以看到, 一個大的集群會有很多 namespace, 每個 namespace 又有很多分區數, 每個分區又需要多個副本, 這麼多數據, 必須得有一個節點從全局的視角去優化調度整個集群的數據來保證集群的穩定和數據節點的負載均衡。 placedriver 節點需要負責指定的數據分區的節點分佈,還會在某個數據節點異常時, 自動重新分配數據分佈。 這裡我們使用分離的無狀態 PD 節點來實現, 這樣帶來的好處是可以獨立升級方便運維, 也可以橫向擴展支持大量的元數據查詢服務, 所有的元數據存儲在 etcd 集群上。 多個 placedriver 通過 etcd 選舉來產生一個 master 進行數據節點的分配和遷移任務。 每個 placedriver 節點會 watch 集群的節點變化來感知整個集群的數據節點變化。

目前數據分區算法是通過 hash 分片實現的, 對於 hash 分區來說, 所有的 key 會均衡的映射到設定的初始分區數上, 一般來說分區數都會是 DataNode 機器節點數的幾倍, 方便未來擴容。 因此 PD 需要選擇一個算法將分區分配給對應的 DataNode, 有些系統可能會使用一致性 hash 的方式去把分區按照環形排列分攤到節點上, 但是一致性 hash 會導致數據節點變化時負載不均衡, 也不夠靈活。 在 ZanKV 裡我們選擇維護映射表的方式來建立分區和節點的關係, 映射表會根據一定的算法並配合靈活的策略生成。

使用開源技術構建有贊分佈式 KV 存儲服務

從上圖來看, 整個讀寫流程: 客戶端進行讀寫訪問時, 對主 key 做 hash 得到一個整數值, 然後對分區總數取模, 得到一個分區 id, 再根據分區 id, 查找分區 id 和數據節點映射表, 得到對應數據節點, 接著客戶端將命令發送給這個數據節點, 數據節點收到命令後, 根據分區算法做驗證, 並在數據節點內部發送給本地擁有指定分區 id 的數據分區的 leader 來處理, 如果本地沒有對應的分區 id 的 leader, 寫操作會在 raft 內部轉發到 leader 節點, 讀操作會直接返回錯誤(可能在做 leader 切換)。 客戶端會根據錯誤信息決定是否需要刷新本地 leader 信息緩存再進行重試。

可以看到讀寫壓力都在分區的 leader 上面, 因此我們需要儘可能的確保每個節點上擁有均衡數量的分區 leader, 同時還要儘可能減少增減節點時發生的數據遷移。 在數據節點發生變化時, 需要動態的修改分區到數據節點的映射表, 動態調整映射表的過程就是數據平衡的過程。 數據節點變化時會觸發 etcd 的 watch 事件, placedriver 會實時監測數據節點變化, 來判斷是否需要做數據平衡。 為了避免影響線上服務, 可以設置數據平衡的允許時間區間。 為了避免頻繁發生數據遷移, 節點發生變化後, 會根據緊急情況, 判斷數據平衡的必要性, 特別是在數據節點升級過程中, 可以避免不必要的數據遷移。 考慮以下幾種情況:

  • 新增節點: 平衡優先級最低, 僅在允許的時間區間並且沒有異常節點時嘗試遷移數據到新節點
  • 少於半數節點異常: 等待一段時間後, 才會嘗試將異常節點的副本數據遷移到其他節點, 避免節點短暫異常時遷移數據。
  • 集群超過半數節點異常: 很可能發生了網絡分區, 此時不會進行自動遷移, 如果確認不是網絡分區, 可以手動強制調整集群穩定節點數觸發遷移。
  • 可用於分配的節點數不足: 假如副本數配置是 3, 但是可用節點少於 3 個, 則不會發生數據遷移

穩定集群節點數默認只會增加, 每次發現新的數據節點, 就自動增加, 節點異常不會自動減少。 如果穩定集群節點數需要減少, 則需要調用縮容API進行設置, 這樣可以避免網絡分區時不必要的數據遷移。 當集群正常節點數小於等於穩定節點數一半時, 自動數據遷移將不會發生, 除非人工介入。

數據過期的實現

數據過期作為 redis 的功能特性之一,也是 ZanKV 需要重點考慮和設計支持的。與 redis 作為內存存儲不同,ZanKV 作為強一致性的持久化存儲,面臨著需要處理大量過期的落盤數據的場景,在整體設計上,存在著諸多的權衡和考慮。

首先,ZanKV 並不支持毫秒級的數據過期(對應 redis 的 pexpire 命令),這是因為在實際的業務場景中很少存在毫秒級數據過期的需求,且在實際的生產網絡環境中網絡請求的 RTT 也在毫秒級別,精確至毫秒級的過期對系統壓力過大且實際意義並不高。

在秒級數據過期上, ZanKV 支持了兩種數據過期策略,分別用以不同的業務場景。用戶可以根據自己的需求,針對不同的 namespace 配置不同的過期策略。下面將詳細闡述兩種不同過期策略的設計和權衡。

一致性數據過期

最初設計數據過期功能時,預期的設計目標為:保持數據一致性的情況下完全兼容 redis 數據過期的語義。一致性數據過期,就是為了滿足該設計目標所做的設計方案。

正如上文中提到的,ZanKV 目前是使用 rocksdb 作為存儲引擎的落盤存儲系統,無論是何種過期策略或者實現,都需要將數據的過期信息通過一定方式的編碼落盤到存儲中。在一致性過期的策略下,數據的過期信息編碼方式如下:

使用開源技術構建有贊分佈式 KV 存儲服務

如上圖所示,在存在過期時間的情況下,任何一個 key 都需要額外存儲兩個信息:

  • key 對應的數據過期時間。我們稱之為表1
  • 使用過期時間的 unix 時間戳為前綴編碼的 key 表。我們稱之為表2

rocksdb 使用 LSM 作為底層數據存儲結構,掃描按照過期時間順序存儲的表2速度是比較快的。在上述數據存儲結構的基礎上,ZanKV 通過如下方式實現一致性數據過期: 在每個 raft group 中,由 leader 進行過期數據掃描(即掃描表2),每次掃描出至當前時間點需要過期的數據信息, 通過 raft 協議發起刪除請求,在刪除請求處理過程中將存儲的數據和過期元數據信息(表1和表2的數據)一併刪除。在一致性過期的策略下,所有的數據操作都通過 raft 協議進行,保證了數據的一致性。同時,所有 redis 過期的命令都得到了很好的支持,用戶可以方便的獲取和修改 key 的生存時間(分別對應 redis 的 TTL 和 expire 命令),或者對 key 進行持久化(對應 redis 的 persist 指令)。但是,該方案存在以下兩個明顯的缺陷:

在大量數據過期的情況下,leader 節點會產生大量的 raft 協議的數據刪除請求,造成集群網絡壓力。同時,數據過期刪除操作在 raft 協議中處理,會阻塞寫入請求,降低集群的吞吐量,造成寫入性能抖動。

目前,我們正在計劃針對這個缺陷進行優化。具體思路是在過期數據掃描由 raft group 的 leader 在後臺進行,掃描後僅通過 raft 協議同步需要過期至的時間戳,各個集群節點在 raft 請求處理中刪除該時間戳之前的所有過期數據。圖示如下:

使用開源技術構建有贊分佈式 KV 存儲服務

該策略能有效的減少大量數據過期情況下的 raft 請求,降低網絡流量和 raft 請求處理壓力。有興趣的讀者可以在 ZanKV 的開源項目上幫助我們進行相應的探索和實現。

另外一個缺點是任何數據的刪除和寫入,需要同步操作表1和表2的數據,寫放大明顯。因此,該方案僅適用於過期的數據量不大的情況,對大量數據過期的場景性能不夠好。所以,結合實際的業務使用場景,又設計了非一致性本地刪除的數據過期策略。

非一致性本地刪除

該策略的出發點在於,絕大多數的業務僅僅關注數據保留的時長,如業務要求相關的數據保留 3 個月或者半年,而並不關注具體的數據清理時間,也不會在寫入之後多次調整和修改數據的過期時間。在這種業務場景的考慮下,設計了非一致性本地刪除的數據過期策略。

與一致性數據過期不同的是,在該策略下,不再存儲表1的數據,而僅僅保留表2的數據,如下圖所示:

使用開源技術構建有贊分佈式 KV 存儲服務

同時,數據過期刪除不再通過 raft 協議發起,而是集群中各個節點每隔 5 分鐘掃描一次表2中的數據,並對過期的數據直接進行本地刪除。

因為沒有表2的數據,所以在該策略下,用戶無法通過 ttl 指令獲取到 key 對應的過期時間,也無法在設置過期時間後重新設置或者刪除 key 的過期時間。但是,這也有效的減少了寫放大,提高了寫入性能。

同時,因為刪除操作都由本地後臺進行,消除了同步數據過期帶來的集群寫入性能抖動和集群網絡流量壓力。但是,這也犧牲了部分數據一致性。與此同時,每隔 5 分鐘進行一次的掃描也無法保證數據刪除的實時性。

總而言之,非一致性本地刪除是一種權衡後的數據過期策略,適用於絕大多數的業務需求,提高了集群的穩定和吞吐量,但是犧牲了一部分的數據一致性,同時也造成部分指令的語義與 redis 不一致。

用戶可以根據自己的需求和業務場景,在不同的 namespace 中配置不同的數據過期策略。

前綴定期清理

雖然非一致性刪除通過優化, 已經大幅減少了服務端壓力, 但是對於數據量特別大的特殊場景, 我們還可以進一步減少服務端壓力。 此類業務場景一般是數據都有時間特性, 因此 key 本身會有時間戳信息 (比如日誌監控這種數據), 這種情況下, 我們提供了前綴清理的接口, 可以一次性批量刪除指定時間段的數據, 進一步避免服務端掃描過期數據逐個刪除的壓力。

跨機房方案

ZanKV 目前支持兩種跨機房部署模式,分別適用於不同的場景。

單個跨多機房集群模式

此模式, 部署一個大集群, 並且都是同城機房, 延遲較小, 一般是 3 機房模式。 部署此模式, 需要保證每個副本都在不同機房均勻分佈, 從而可以容忍單機房宕機後, 不影響數據的讀寫服務, 並且保證數據的一致性。

部署時, 需要在配置文件中指定當前機房的信息, 用於數據分佈時感知機房信息。不同機房的數據節點, 使用不同機房信息, 這樣 placedriver 進行副本配置時, 會保證每個分區的幾個副本都均勻分佈在不同的機房中。

跨機房的集群, 通過 raft 來完成各個機房副本的同步, 發生單機房故障時, 由於另外 2 個機房擁有超過一半的副本, 因此 raft 的讀寫操作可以不受影響, 且數據保證一致。 等待故障機房恢復後, raft 自動完成故障期間的數據同步, 使得故障機房數據在恢復後能保持同步。此模式在故障發生和恢復時都無需任何人工介入, 在多機房情況下保證單機房故障的可用性的同時,數據一致性也得到保證。 此方式由於有跨機房同步, 延遲會有少量影響。

多個機房內集群間同步模式

如果是異地機房, 或者機房網絡延遲較高, 使用跨機房單集群部署方式, 可能會帶來較高的同步延遲, 使得讀寫的延遲都大大增加。 為了優化延遲問題, 可以使用異地機房集群間同步模式。 由於異地機房是後臺異步同步的, 異地機房不影響本地機房的延遲, 但同時引入了數據同步滯後的問題, 在故障時可能會發生數據不一致的情況。

此模式的部署方式稍微複雜一些, 基本原理是通過在異地機房增加一個 raft learner 節點異步的拉取 raft log 然後重放到異地機房集群。 由於每個分區都是一個獨立的 raft group, 因此分區內是串行回放, 各個分區間是並行回放 raft log。 異地同步機房默認是隻讀的, 如果主機房發生故障需要切換時, 可能發生部分數據未同步, 需要在故障恢復後根據 raft log 進行人工修復。 此方式缺點是運維麻煩, 且故障時需要修數據, 好處是減少了正常情況下的讀寫延遲。

性能調優經驗

ZanKV 在初期線上運行時, 積累了一些調優經驗, 主要是 RocksDB 參數的調優和操作系統的參數調優, 大部分調優都是參考官方的文檔, 這裡重點說明以下幾個參數:

  • block cache: 由於 block cache 裡面都是解壓後的 block, 和 os 自帶文件 cache 功能有所區別, 因此需要平衡兩者之間的比例(一些壓測經驗建議10%~30%之間)。 另外分區數很多, 因此需要配置不同 RocksDB 實例共享來避免過多的內存佔用。
  • write buffer: 這個無法在多個 rocksdb 實例之間共享, 因此需要避免太多, 同時又不能因為太小而發送寫入 stall。 另外需要和其他幾個參數配合保證: level0_file_num_compaction_trigger*write_buffer_size*min_write_buffer_number_tomerge=max_bytes_for_level_base 來減少寫放大。
  • 後臺 IO 限速: 這個主要是使用 rocksdb 自帶的後臺 IO 限速來避免後臺 compaction 帶來的讀寫毛刺。
  • 迭代器優化: 這個主要是避免 rocksdb 的標記刪除特性影響數據迭代性能, 在迭代器上使用 rocksdb::ReadOptions::iterate_upper_bound參數來提前結束迭代, 詳細可以參考這篇文章: https://www.cockroachlabs.com/blog/adventures-performance-debugging/
  • 禁用透明大頁 THP: 操作系統的透明大頁功能在存儲系統這種訪問模式下, 基本都是建議關閉的, 不然讀寫毛刺現象會比較嚴重。
  1. # echo never > /sys/kernel/mm/redhat_transparent_hugepage/enabled
  2. # echo never > /sys/kernel/mm/redhat_transparent_hugepage/defrag

Roadmap

雖然 ZanKV 目前已經在有贊內部使用了一段時間, 但是仍然有很多需要完善和改進的地方, 目前還有以下幾個規劃的功能正在設計和開發:

二級索引

主要是在 HASH 這種數據類型時實現如下類似功能, 方便業務通過其他 field 字段查詢數據

  1. IDX.FROM test_hash_table WHERE “age>
  2. 24
  3. AND age<
  4. 31
  5. "

優化 raft log

目前 etcd 的 raft 實現會把沒有 snapshot 的 raft log 保存在 memory table 裡面, 在 ZanKV 這種多 raft group 模式下會佔用太多內存, 需要優化使得大部分 raft log 保存在磁盤, 內存只需要保留最近少量的 log 用於 follower 和 leader 之間的交互。 選擇 raft log 磁盤存儲需要避免雙層 WAL 降低寫入性能。

多索引過濾

二級索引只能滿足簡單的單 field 查詢, 如果需要高效的使用多個字段同時過濾, 來滿足更豐富的多維查詢能力, 則需要引入多索引過濾。 此功能可以滿足一大類不需要全文搜索以及精確排序需求的數據搜索場景。 業界已經有支持 range 查詢的壓縮位圖來實現的開源產品, 在索引過濾這種特殊場景下, 性能會比倒排高出不少。

數據實時導出和 OLAP 優化

主要是利用 raft learner 的特點, 實時的把 raft log 導出到其他系統。 進一步做針對性的場景, 比如轉換成列存做 OLAP 場景等。

以上特性都有巨大的開發工作量, 目前人力有限, 歡迎有志之士加入我們或者參與我們的開源項目, 希望能充分利用開源社區的力量使得我們的產品快速迭代, 提供更穩定, 更豐富的功能。

那如何學習才能快速入門並精通呢?

當真正開始學習的時候難免不知道從哪入手,導致效率低下影響繼續學習的信心。

但最重要的是不知道哪些技術需要重點掌握,學習時頻繁踩坑,最終浪費大量時間,所以有一套實用的視頻課程用來跟著學習是非常有必要的。

為了讓學習變得輕鬆、高效,今天給大家免費分享一套阿里架構師傳授的一套教學資源。幫助大家在成為架構師的道路上披荊斬棘。

這套視頻課程詳細講解了(Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構)等這些成為架構師必備的內容!

而且還把框架需要用到的各種程序進行了打包,根據基礎視頻可以讓你輕鬆搭建分佈式框架環境,像在企業生產環境一樣進行學習和實踐。

使用開源技術構建有贊分佈式 KV 存儲服務

後臺私信回覆“ 架構 ” (不是評論區) 就可以馬上免費獲得這套價值一萬八的內部教材!


分享到:


相關文章: