在有贊早期的時候,當時只有 MySQL 作存儲,codis 作緩存,隨着業務發展,某些業務數據用 MySQL 不太合適, 而 codis 因爲當緩存用, 並不適合作存儲系統, 所以, 急需一款高性能的 NoSQL 產品作補充。考慮到當時運維和開發人員都很是少, 咱們須要一個能快速投入使用, 又不須要太多維護工做的開源產品。 當時對比了幾個開源產品, 最終選擇了 aerospike 做爲咱們的 KV 存儲方案。 事實證實, aerospike 做爲一個成熟的商業化的開源產品承載了一個很是好的過渡時期 在不多量的開發和運維工做支持下, 一直穩定運行沒有什麼故障, 期間知足了不少的業務需求, 也所以能抽出時間投入更多精力解決其餘的中間件問題。node
然而隨着有讚的快速發展, 單純的 aerospike 集羣慢慢開始沒法知足愈來愈多樣的業務需求。 雖然性能和穩定性依然很優秀, 可是因爲其索引必須加載到內存, 對於愈來愈多的海量數據, 存儲成本會居高不下。 更多的業務需求也決定了咱們未來須要更多的數據類型來支持業務的發展。 爲了充分利用已有的 aerospike 集羣, 並考慮到當時的開源產品並沒有法知足咱們全部的業務需求, 所以咱們須要構建一個能知足有贊將來多年的 KV 存儲服務。git
在設計這樣一個能知足將來多年發展的底層 KV 服務, 咱們須要考慮如下幾個方面:github
基於以上幾點, 咱們作了以下的架構設計:redis
爲了整合和方便之後的擴展, 咱們使用 proxy 屏蔽了具體的後端細節, 而且使用普遍使用的 redis 協議做爲咱們對上層業務的接口, 一方面充分利用了開源的 redis 客戶端產品減小了開發工做量, 一方面減小了業務的接入學習成本, 一方面也能對已經使用的 aerospike 集羣和 codis 集羣作比較平滑的整合減小業務遷移工做量。 在此架構下, 咱們將來也能經過在 proxy 層面作一些協議轉換工做就能很方便的利用將來的技術成果, 經過對接更多優秀的開源產品來進一步擴展咱們的 KV 服務能力。算法
有了此架構後, 咱們就能夠在不改動現有 aerospike 集羣的基礎上, 來完善咱們目前的KV服務短板, 所以咱們基於幾個成熟的開源產品自研了 ZanKV 這個分佈式 KV 存儲。 自研 ZanKV 有以下特色:後端
自研 ZanKV 的總體架構圖以下所示:緩存
整個集羣由 placedriver + 數據節點 datanode + etcd + rsync 組成。 各個節點的角色以下:網絡
下面咱們來一一講述具體的內部實現細節。數據結構
首先, 咱們須要一個單機的高性能高可靠的 KV 存儲引擎做爲基石來保障後面的全部工做的展開, 同時咱們可能還須要考慮可擴展性, 以便將來引入更好的底層存儲引擎。 在這一方面, 咱們選擇了 RocksDB 做爲起點, 考慮到它的接口和易用性, 並且是 FB 通過多年的時間打造的一個已經比較穩定的開源產品, 它同時也是衆多開源產品的共同選擇, 基本上不會有什麼問題, 也能及時響應開源社區的需求。架構
RocksDB 僅僅提供了簡單的 Get,Set,Delete 幾個有限的接口, 爲了知足 redis 協議裏面豐富的數據結構, 咱們須要在 KV 基礎上封裝更加複雜的數據結構, 所以咱們在 RocksDB 上層構建了一個數據映射層來知足咱們的需求, 數據映射也是參考了幾個優秀的開源產品(pika, ledis, tikv 等)。
完成單機存儲後, 爲了保證數據的可靠性, 咱們經過 raft 一致性協議來可靠的將數據複製到多臺機器上, 確保多臺機器副本數據的一致性。 選擇 raft 也是由於 etcd 已經使用 Golang 實現了一個比較完整且成熟的 raft library 供你們使用。可是 etcd 自己並不能支持海量數據的存儲, 所以爲了能無限擴展存儲能力, 咱們在 etcd raft 基礎上引入了 raft group 分區概念, 使得咱們可以經過不斷增長 raft 分區的方法來實現同時並行處理多個 raft 複製的能力。
最後, 咱們經過 redis 協議來完成對外服務, 能夠看到, 經過以上幾個分層 ZanKV DataNode 節點就能提供豐富的數據存儲服務能力了, 分層結構以下圖所示:
爲了支持海量數據, 單一分區的 raft 集羣是沒法知足無限擴展的目標的, 所以咱們須要支持數據分區來完成 scale out。 業界經常使用的分區算法能夠分爲兩類: hash 分區和 range 分區, 兩種分區算法各有本身的適用場景, range 分區優點是能夠全局有序, 可是須要實現動態的 merge 和 split 算法, 實現複雜, 而且某些場景容易出現寫熱點。 hash 分區的優點是實現簡單, 讀寫數據通常會比較均衡分散, 缺點是分區數通常在初始化時設定爲固定值, 增減分區數須要遷移大量數據, 並且很難知足全局有序的查詢。 綜合考慮到開發成本和某些數據結構的順序需求, 咱們目前採起前綴 hash 分區算法, 這樣能夠保證前綴相同的數據全局有序知足一部分業務需求的同時, 減小了開發成本保證系統能儘快上線。
另外, 考慮到有贊從此的業務會愈來愈多, 將來須要能方便的隔離不一樣業務, 也方便不斷的加入新的特性同時能平滑升級, 咱們引入了 namespace 的概念。 namespace 能夠動態的添加到集羣, 而且 namespace 之間的配置和數據徹底隔離, 包括副本數, 分區數, 分區策略等配置均可以不一樣。 而且 namespace 能夠支持指定一些節點放置策略, 保證 namespace 和某些特性的節點綁定(目前多機房方案經過機架感知方式實現副本至少分佈在一個以上機房)。 有了 namespace, 咱們就能夠把一些核心的業務和非核心的業務隔離到不一樣的 namespace 裏面, 也能夠將不兼容的新特性加到新的 namespace 給新業務用, 而不會影響老的業務, 從而實現平滑升級。
能夠看到, 一個大的集羣會有不少 namespace, 每一個 namespace 又有不少分區數, 每一個分區又須要多個副本, 這麼多數據, 必須得有一個節點從全局的視角去優化調度整個集羣的數據來保證集羣的穩定和數據節點的負載均衡。 placedriver 節點須要負責指定的數據分區的節點分佈,還會在某個數據節點異常時, 自動從新分配數據分佈。 這裏咱們使用分離的無狀態 PD 節點來實現, 這樣帶來的好處是能夠獨立升級方便運維, 也能夠橫向擴展支持大量的元數據查詢服務, 全部的元數據存儲在 etcd 集羣上。 多個 placedriver 經過 etcd 選舉來產生一個 master 進行數據節點的分配和遷移任務。 每一個 placedriver 節點會 watch 集羣的節點變化來感知整個集羣的數據節點變化。
目前數據分區算法是經過 hash 分片實現的, 對於 hash 分區來講, 全部的 key 會均衡的映射到設定的初始分區數上, 通常來講分區數都會是 DataNode 機器節點數的幾倍, 方便將來擴容。 所以 PD 須要選擇一個算法將分區分配給對應的 DataNode, 有些系統可能會使用一致性 hash 的方式去把分區按照環形排列分攤到節點上, 可是一致性 hash 會致使數據節點變化時負載不均衡, 也不夠靈活。 在 ZanKV 裏咱們選擇維護映射表的方式來創建分區和節點的關係, 映射表會根據必定的算法並配合靈活的策略生成。
從上圖來看, 整個讀寫流程: 客戶端進行讀寫訪問時, 對主 key 作 hash 獲得一個整數值, 而後對分區總數取模, 獲得一個分區 id, 再根據分區 id, 查找分區 id 和數據節點映射表, 獲得對應數據節點, 接着客戶端將命令發送給這個數據節點, 數據節點收到命令後, 根據分區算法作驗證, 並在數據節點內部發送給本地擁有指定分區 id 的數據分區的 leader 來處理, 若是本地沒有對應的分區 id 的 leader, 寫操做會在 raft 內部轉發到 leader 節點, 讀操做會直接返回錯誤(可能在作 leader 切換)。 客戶端會根據錯誤信息決定是否須要刷新本地 leader 信息緩存再進行重試。
能夠看到讀寫壓力都在分區的 leader 上面, 所以咱們須要儘量的確保每一個節點上擁有均衡數量的分區 leader, 同時還要儘量減小增減節點時發生的數據遷移。 在數據節點發生變化時, 須要動態的修改分區到數據節點的映射表, 動態調整映射表的過程就是數據平衡的過程。 數據節點變化時會觸發 etcd 的 watch 事件, placedriver 會實時監測數據節點變化, 來判斷是否須要作數據平衡。 爲了不影響線上服務, 能夠設置數據平衡的容許時間區間。 爲了不頻繁發生數據遷移, 節點發生變化後, 會根據緊急狀況, 判斷數據平衡的必要性, 特別是在數據節點升級過程當中, 能夠避免沒必要要的數據遷移。 考慮如下幾種狀況:
穩定集羣節點數默認只會增長, 每次發現新的數據節點, 就自動增長, 節點異常不會自動減小。 若是穩定集羣節點數須要減小, 則須要調用縮容API進行設置, 這樣能夠避免網絡分區時沒必要要的數據遷移。 當集羣正常節點數小於等於穩定節點數一半時, 自動數據遷移將不會發生, 除非人工介入。
數據過時做爲 redis 的功能特性之一,也是 ZanKV 須要重點考慮和設計支持的。與 redis 做爲內存存儲不一樣,ZanKV 做爲強一致性的持久化存儲,面臨着須要處理大量過時的落盤數據的場景,在總體設計上,存在着諸多的權衡和考慮。
首先,ZanKV 並不支持毫秒級的數據過時(對應 redis 的 pexpire 命令),這是由於在實際的業務場景中不多存在毫秒級數據過時的需求,且在實際的生產網絡環境中網絡請求的 RTT 也在毫秒級別,精確至毫秒級的過時對系統壓力過大且實際意義並不高。
在秒級數據過時上, ZanKV 支持了兩種數據過時策略,分別用以不一樣的業務場景。用戶能夠根據本身的需求,針對不一樣的 namespace 配置不一樣的過時策略。下面將詳細闡述兩種不一樣過時策略的設計和權衡。
最初設計數據過時功能時,預期的設計目標爲:保持數據一致性的狀況下徹底兼容 redis 數據過時的語義。一致性數據過時,就是爲了知足該設計目標所作的設計方案。
正如上文中提到的,ZanKV 目前是使用 rocksdb 做爲存儲引擎的落盤存儲系統,不管是何種過時策略或者實現,都須要將數據的過時信息經過必定方式的編碼落盤到存儲中。在一致性過時的策略下,數據的過時信息編碼方式以下:
如上圖所示,在存在過時時間的狀況下,任何一個 key 都須要額外存儲兩個信息:
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 請求處理中刪除該時間戳以前的全部過時數據。圖示以下:
該策略能有效的減小大量數據過時狀況下的 raft 請求,下降網絡流量和 raft 請求處理壓力。有興趣的讀者能夠在 ZanKV 的開源項目上幫助咱們進行相應的探索和實現。
另一個缺點是任何數據的刪除和寫入,須要同步操做表1和表2的數據,寫放大明顯。所以,該方案僅適用於過時的數據量不大的狀況,對大量數據過時的場景性能不夠好。因此,結合實際的業務使用場景,又設計了非一致性本地刪除的數據過時策略。
該策略的出發點在於,絕大多數的業務僅僅關注數據保留的時長,如業務要求相關的數據保留 3 個月或者半年,而並不關注具體的數據清理時間,也不會在寫入以後屢次調整和修改數據的過時時間。在這種業務場景的考慮下,設計了非一致性本地刪除的數據過時策略。
與一致性數據過時不一樣的是,在該策略下,再也不存儲表1的數據,而僅僅保留表2的數據,以下圖所示:
同時,數據過時刪除再也不經過 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 參數的調優和操做系統的參數調優, 大部分調優都是參考官方的文檔, 這裏重點說明如下幾個參數:
level0_file_num_compaction_trigger * write_buffer_size * min_write_buffer_number_tomerge = max_bytes_for_level_base
來減小寫放大。rocksdb::ReadOptions::iterate_upper_bound
參數來提早結束迭代, 詳細能夠參考這篇文章: https://www。cockroachlabs。com/blog/adventures-performance-debugging/# echo never > /sys/kernel/mm/redhat_transparent_hugepage/enabled # echo never > /sys/kernel/mm/redhat_transparent_hugepage/defrag
雖然 ZanKV 目前已經在有贊內部使用了一段時間, 可是仍然有不少須要完善和改進的地方, 目前還有如下幾個規劃的功能正在設計和開發:
主要是在 HASH 這種數據類型時實現以下相似功能, 方便業務經過其餘 field 字段查詢數據
IDX。FROM test_hash_table WHERE 「age>24 AND age<31"
目前 etcd 的 raft 實現會把沒有 snapshot 的 raft log 保存在 memory table 裏面, 在 ZanKV 這種多 raft group 模式下會佔用太多內存, 須要優化使得大部分 raft log 保存在磁盤, 內存只須要保留最近少許的 log 用於 follower 和 leader 之間的交互。 選擇 raft log 磁盤存儲須要避免雙層 WAL 下降寫入性能。
二級索引只能知足簡單的單 field 查詢, 若是須要高效的使用多個字段同時過濾, 來知足更豐富的多維查詢能力, 則須要引入多索引過濾。 此功能能夠知足一大類不須要全文搜索以及精確排序需求的數據搜索場景。 業界已經有支持 range 查詢的壓縮位圖來實現的開源產品, 在索引過濾這種特殊場景下, 性能會比倒排高出很多。
主要是利用 raft learner 的特色, 實時的把 raft log 導出到其餘系統。 進一步作針對性的場景, 好比轉換成列存作 OLAP 場景等。
以上特性都有巨大的開發工做量, 目前人力有限, 歡迎有志之士加入咱們或者參與咱們的開源項目, 但願能充分利用開源社區的力量使得咱們的產品快速迭代, 提供更穩定, 更豐富的功能。
限於篇幅, 以上只能大概講述 ZanKV 幾個重要的技術思路, 還有不少實現細節沒法一一講述清晰, 項目已經開源: https://github.com/youzan/Zan... , 歡迎你們經過閱讀源碼來進一步瞭解細節, 並貢獻源碼來共同構建一個更好的開源產品, 也敬請期待後繼更佳豐富的功能特性實現細節介紹。