vivo 大規模特徵存儲實踐

本文首發於 vivo互聯網技術 微信公衆號 
鏈接:https://mp.weixin.qq.com/s/u1LrIBtY6wNVE9lzvKXWjA
作者:黃偉鋒

本文旨在介紹 vivo 內部的特徵存儲實踐、演進以及未來展望,拋磚引玉,吸引更多優秀的想法。

一、需求分析

AI 技術在 vivo 內部應用越來越廣泛,其中特徵數據扮演着至關重要的角色,用於離線訓練、在線預估等場景,我們需要設計一個系統解決各種特徵數據可靠高效存儲的問題。

1. 特徵數據特點

(1)Value 大

特徵數據一般包含非常多的字段,導致最終存到 KV 上的 Value 特別大,哪怕是壓縮過的。

(2)存儲數據量大、併發高、吞吐大

特徵場景要存的數據量很大,內存型的 KV(比如 Redis Cluster)是很難滿足需求的,而且非常昂貴。不管離線場景還是在線場景,併發請求量大,Value 又不小,吞吐自然就大了。 

(3)讀寫性能要求高,延時低

大部分特徵場景要求讀寫延時非常低,而且持續平穩,少抖動。

(4)不需要範圍查詢

大部分場景都是單點隨機讀寫。

(5)定時灌海量數據

很多特徵數據剛被算出來的時候,是存在一些面向 OLAP 的存儲產品上,而且定期算一次,希望有一個工具能把這些特徵數據及時同步到在線 KV 上。

(6)易用

業務在接入這個存儲系統時,最好沒有太大的理解成本。

2. 潛在需求

  • 擴展爲通用磁盤 KV,支撐各個場景的大容量存儲需求

    我們的目標是星辰大海,絕不僅限於滿足特徵場景。

  • 支撐其他 Nosql/Newsql 數據庫,資源複用

    從業務需求出發,後續我們會有各種各樣 Nosql 數據庫的需求,如圖數據庫、時序數據庫、對象存儲等等,如果每個產品之間都是完全隔離,沒有任何資源(代碼、平臺能力等等)複用,維護成本是巨大的。

  • 可維護性強

    首先實現語言不能太小衆,否則人才招聘上會比較困難,而且最好能跟我們的技術棧發展方向匹配。

    架構設計上不能依賴太多第三方服務組件,降低運維的複雜性。

3. 存儲系統的冰山

綜合以上需求,最終我們決定兼容 Redis 協議,用戶看到的只是一個類似單機版的 Redis 服務,但背後我們做了大量的可靠性保障工作。

vivo 大規模特徵存儲實踐

二、方案選型

在方案選型上,我們遵循一些基本原則:

  • 源自開源,按需定製。

  • 內部開源,集思廣益。

  • 語言主流,架構主流。

  • 可靠至上,高可維護。

先簡單介紹一下我們早期方案調研的一些優缺點分析:

vivo 大規模特徵存儲實踐vivo 大規模特徵存儲實踐

說實話,調研的都是優秀的開源項目,但光靠官方代碼和設計文檔,沒有深入的實踐經驗,我們是很難斷定一個開源產品是真正適合我們的,適當的賽馬可以更好校準方案選型,同時也一定程度反映出我們較強的執行力。

總的來說我們是要在現有需求、潛在需求、易用性、架構先進性、性能、可維護性等各個方面中找到一個最優平衡,經過一段時間的理論調研和實踐以後,最終我們選擇了 Nebula。

三、Nebula 簡介

Nebula Graph 是一個高性能、高可用、高可靠、數據強一致的、開源的分佈式圖數據庫

1. 存儲計算分離

Nebula 採用存儲計算分離的設計,有狀態的存儲服務 和 無狀態的計算服務 是分層的,使得存儲層可以集中精力提升數據可靠性,只暴露簡單的 KV 接口,計算層可以聚焦在用戶直接需要的計算邏輯上,而且大大提升運維部署的靈活性。

不過作爲圖數據庫,爲了提升性能,Nebula 把一部分圖計算邏輯下沉到存儲層,這也是靈活性與性能之間的一個比較現實的權衡。

2. 強一致,架構主流

Nebula 的強一致使用 Raft,是目前實現多副本一致性的主流方法,而且這個 Raft 實現已經初步通過了 Jepsen 線性一致性測試,作爲一個剛起步不久的開源項目,對增加用戶的信心很有幫助。

3. 可伸縮

Nebula 的橫向擴展能力得益於其 Hash-based 的 Multi-raft 實現,同時自帶一個用於負載均衡的調度器(Balancer),架構和實現都比較簡潔(至少目前還是),上手成本低。

4.易維護

Nebula 內核使用 C++ 實現,跟我們基礎架構的技術棧發展方向比較匹配。經過評估,Nebula 一些基本的平臺能力(如監控接口、部署模式)比較簡單易用,跟我們自身平臺能很好對接。

代碼實現做了較好抽象,可以靈活支持多種存儲引擎,爲我們後來針對特徵場景的性能優化奠定了很好的基礎。

四、Nebula Raft 簡介

上文提到 Nebula 是依賴 Raft 保證強一致的,這裏簡單介紹一下 Nebula Raft 的特點:

1. 選主 與 任期

一個 Raft Group 的生命週期是由一個又一個連續的任期組成的,每個任期開始會選出一個 Leader,其他成員爲 Follower,一個任期內只有一個 Leader,如果任期內 Leader 不可用,會馬上進入下一個任期,選新的 Leader。這種 Strong Leader 機制使得 Raft 的工程實現難度遠低於它的祖師爺 - Paxos。

2. 日誌複製、壓縮

標準的 Raft 實現中,每個從客戶端來的寫請求都會轉換成 “操作日誌” 寫到 wal 文件中,Leader 在把操作日式更新到自己狀態機後,會主動向所有 Follower 異步複製日誌,直到超過半數的 Follower 應答後,才返回客戶端寫入成功。

實際運行中,wal 的文件會越來越大,如果沒有一個合理的 wal 日誌回收機制,wal 文件將很快佔滿整個磁盤,這個回收機制就是日誌壓縮(Log Compaction)。Nebula 的 Log Compaction 實現比較簡潔,用戶只需要配置一個 wal_ttl 參數,即可在不破壞集羣正確性的前提下,把 wal 文件的空間佔用控制在一個穩定的範圍。

Nebula 實現了 Raft batch 和 pipeline 機制,支持 Leader 到 Follower 的批量和亂序日誌提交,在高併發場景下,能有效提升集羣整體吞吐能力。

3. 成員變更

跟典型的 Raft 實現類似,這裏着重提一下 Nebula Raft 的 Snapshot 機制。

當一個 Raft Group 增加成員時,新成員節點需要從當前的 Leader 中獲取所有的日誌並重放到自身的狀態機中,這是一個不容小覷的資源開銷,對 Leader 造成較大的壓力。爲此一般的 Raft 會提供一個 Snapshot 機制,以此解決節點擴容的性能問題,以及節點故障恢復的時效問題。

Snapshot,即 Leader 把自身狀態機打成一個“鏡像”單獨保存,在 Nebula Raft 實現中,“鏡像”就是 Rocksdb 實例(即狀態機本身),新成員加入時,Leader 會調用 Rocksdb 的 Iterator 掃描整個實例,過程中把讀到的值分批發送給新成員,最終完成整個 Snapshot 的拷貝過程。

4. Multi-raft 實現

如果一個集羣只有一個 Raft Group,很難通過加機器實現橫向擴展,適用場景非常有限,自然想到的方法就是把集羣的數據拆分出多個不同的 Raft Group,這裏就引入了 2 個新問題:(1)數據如何分片(2)分片如何均勻分佈到集羣中。

實現 Multi-raft 是一個有挑戰且很有意思的事情,業界有 2 種主流的實現方式,一種是 Hash-based 的,一種是 Region-based,各有利弊,大部分情況下,前者比較簡單有效,Nebula 目前採用 Hash-based 的方式,也是我們需要的,但面向圖場景,後續有沒有進一步的規劃,需要持續關注社區動態。

五、特徵存儲平臺介紹

1. 系統架構

vivo 大規模特徵存儲實踐vivo 大規模特徵存儲實踐

在 Nebula 原有架構基礎上,增加了一些組件,包括 Redis Proxy、Rediscluster Proxy 以及平臺化相關的組件。

Meta 實例是存整個集羣的元信息,包括數據分片路由規則,space 信息等等,其本身也是一個 Raft Group。

Storage 實例是實際存數據的節點,假設一個集羣多個分片對應 m 個 Raft Group,每個 Raft Group 對應 n 個副本,Nebula 就是把 m * n 個副本均勻分佈到這多個 Storage 實例中,併力求每個實例中的 Leader 數也相近。

Graph 實例是圖 API 的服務提供者以及整個集羣的 Console,無狀態。

Redis 實例兼容了 Redis 協議,實現了部分 Redis 原生的數據結構,無狀態。

Rediscluster 實例兼容了 Redis Cluster 協議,無狀態。

2. 性能優化

(1)集羣調優

實際接入生產業務時,往往需要針對不同場景調整參數,這個工作在在早期佔用了大量的時間,但確實也爲我們積累寶貴的經驗。

(2)WiscKey

前文提到的大部分特徵場景的 Value 都比較大,單純依賴 Rocksdb 會導致嚴重的寫放大,原因在於頻繁觸發 Compaction 邏輯,而且每次 Compaction 的時候都會把 Key 和 Value 從磁盤掃出來,在 Value 大的場景下,這個開銷非常可怕。爲此學術界提出過一些解決方案,其中 WiscKey 以實用性而廣受認可,工業界也落地了其開源實現(Titandb)。

Titandb 詳細原理可參考其 官方文檔,簡單來說,就是改造 Rocksdb,兼容對外接口,保留 LSM-tree,新增 BlobFile 存儲,Key Value 分離存儲,Key 存  LSM-tree,Value 存 BlobFile,依賴 SSD 磁盤隨機讀寫性能,犧牲範圍查詢性能,減少大 Value 場景下的寫放大。

得益於 Nebula 支持多存儲引擎的設計,Titandb 很輕鬆就集成到 Nebula Storage,在實際生產中,的確在性能上給我們帶來不錯的收益。

3. TTL 機制

不管是 Rocksdb, 還是 Titandb,都兼容了 Compaction Filter 接口,即在 Compaction 的時候會調用這個 Filter 來判斷是否需要過濾掉具體的數據。我們在實際寫入 Storage 的 Value 中種入了 TTL,在 Compaction Filter 的時候,掃描每個 Value,提取出 TTL 判斷 Value 是否過期了,如果是,則刪除掉對應 Key-Value 對。

然而,實踐中我們發現,Titandb 在 Compaction 的時候,如果 Value 很大被分離到 BlobFile 後,Filter 是讀不到具體 Value 的(只有留在 LSM-tree 裏的小 Value 才能被讀到)。這就對我們 TTL 機製造成很大的不利,導致過期的數據沒有辦法回收。爲此,我們做了一點特殊處理,當大 Value 被分離到 BlobFile 後,LSM-tree 裏會存 Key-Index 對,Index 就是 Value 在 BlobFile 中的位置,我們嘗試把 TTL 種到 Index 中,使得 Filter 時能解析出 TTL,從而實現所有過期數據的物理刪除。

4. 易用

易用性是一個數據庫走向成熟的標誌,是一個很大的課題。

從不同用戶的視角出發,會引申出不同的需求集合,用戶角色可以包括 運維 dba、業務研發工程師、運維工程師等等,最終我們希望在各個視角都能超出預期,實現真正高易用的存儲產品。這裏簡單介紹我們在易用性上的一些實踐:

(1)兼容 redis 協議

我們改造了美圖開源的 KVrocks(一個基於 Rocksdb 的兼容 redis 協議的單機磁盤 KV 產品),依賴 Nebula C++ 版本的 Storage Client,把底層依賴 Rocksdb 的邏輯替換成 Nebula Storage KV 接口的讀寫邏輯,從而實現一個無狀態的 redis 協議兼容層(Proxy),同時我們根據實際需要額外實現了一些命令。當然,我們只是針對特徵場景實現了一些 redis 命令,要在分佈式 KV 基礎上兼容所有 redis 的指令,需要考慮分佈式事務,這裏我先賣個關子,敬請期待。

(2)支持從 Hive 批量導入數據到 KV

對特徵場景來說,這個功能也是易用性的一種體現,Nebula 目前針對圖結構的數據已經實現了從 Hive 導數據,稍加改造就能兼容 KV 格式。

(3)平臺化運維

前期我們在公共配置中心上維護了所有線上集羣的元信息,並落地了一些簡單的作業,如一鍵部署集羣、一鍵卸載集羣、定時監控上報、定時命令正確性檢查、定時實例健康檢測、定時集羣負載監控等等,能滿足日常運維的基本需求。同時,vivo 內部在建設一個功能完善的 DBaaS 平臺,已經實際支撐了不少 DB 產品的平臺化運維,包括 redis、mysql、elasticsearch、mongodb 等等,大大提升業務的數據管理效率,所以,最終特徵存儲是要跟平臺全面結合、共同演進,不斷實現產品易用性和健壯性的突破。

5. 災備

(1)定期冷備

Nebula 本身提供了冷備機制,我們只需要設計好個性化的定時備份策略,即可較好滿足業務需求,這裏不詳細描述,感興趣可以看看 Nebula 的 集羣快照機制

(2)實時熱備

熱備落地一共分兩期:

第一期:**比較簡單,只考慮增量備份,且容忍有損。**

目前 KV 主要服務特徵場景(或緩存場景),對數據可靠性要求不是特別高,而且數據在存儲中駐留的時間不會很長,很快就會被 TTL 清理掉。爲此熱備方案中暫不支持存量數據的備份。

至於增量備份,就是在 Proxy 層把 “寫請求” 再異步寫一次到備集羣,主集羣還是繼續執行同步寫,只要 Proxy cpu 資源足夠,不會影響主集羣本身的讀寫性能。這裏會存在數據丟失的風險,比如 Proxy 異步沒寫完,進程突然掛了,這時備集羣是會丟一點數據的,但正如之前提到,大部分特徵場景(或緩存場景)對這種程度的數據丟失是可容忍。

第二期: 既保證增量備份,也要保證存量備份。

Nebula Raft 引入了 Learner,它也是 Raft Group 中的一個副本,但既不參與選主,也不影響多數派提交,它只是默默的接收來自 Leader 的日誌複製請求。跟其他 Follower 一樣,Learner 一旦掛了,Leader 會不斷重試複製日誌給 Learner,直到 Learner 重啓恢復。

有了這個機制,要實現存量備份就變的簡單了,我們可以實現一個災備組件,僞裝成 Learner,掛到 Raft Group 中,這時 Raft 的成員變更機制會保證 Leader 中的存量數據和增量數據都能以日誌的形式同步給災備組件,同時組件另一側依賴 Nebula Storage Client 把源日誌數據轉換成寫請求應用到災備集羣。

6. 跨機房雙活

雙活也是分兩期落地:

第一期:不考慮衝突處理,不保證集羣間的最終一致。

這個版本的實現同樣簡單,可以理解是 2 個集羣互爲災備,對有同城雙活、故障轉移需求,對最終一致性要求不高的業務還是很有幫助的。

第二期:引入 CRDT 處理衝突,實現最終一致。

這個版本對可靠性的要求比較高,複用災備二期的能力,在 Learner 中獲取集羣的寫請求日誌。

一般雙活情況下,兩個 KV 集羣會分佈在不同機房,單元化的業務服務會各自讀寫本機房 KV 的數據,兩個不同機房的 KV 相互同步變更。假如兩個 KV 更新了同一個 Key,並同步給對方,這時應該怎麼處理衝突呢?

最簡單直接的方案就是最 “晚” 寫的數據更新到兩個 KV,保證最終一致,這裏的 “晚” 不是指絕對意義上的先來後到,而是根據寫操作發生的時間戳,同一個 Key 兩個機房的寫操作都能取到各自的時間戳,但機房之間時鐘不一定同步,這就可能導致實際先發生的操作 時間戳可能更大,但我們的目標是實現最終一致,不是跟時鐘同步機制較勁,所以問題不大。針對這個思路,知名最終一致性方案 CRDT 已經給出了相應的標準實現。

KV 實際存的數據只有 String 類型,對應於 CRDT 裏的 Register 數據結構,其中一種實現就是 Op-based LWW(Last-Write-Wins) Register,顧名思義,就是最 “晚” 寫的 Value 成爲最終一致的狀態,算法原型如下:

vivo 大規模特徵存儲實踐vivo 大規模特徵存儲實踐

對 CRDT 感興趣的可以看看網上的其他資料,這裏不詳細描述。

慶幸的是,vivo 內部已經在 Redis Cluster 上實現了 CRDT Register ,並提供了保障數據跨機房可靠傳輸的組件,使得新 KV 存儲可以站在巨人的肩膀上。需要注意的是,KV 線上大量 mset 的寫請求,而 CRDT Register 只支持單個 Set 的請求衝突處理,所以在雙活組件 Learner 中,從 Leader 收到的 Batch Write 請求需要拆解成一個一個的 Set 命令,然後再同步給 Peer 集羣。

六、未來展望

1. 擴展成通用 KV 存儲

我們立項特徵存儲的時候,就目標要做成通用 KV 存儲,成爲更多數據庫的強力底座。但要做成一個通用 KV 存儲,還需要很多工作要落實,包括可靠性、平臺能力、低成本方面的提升。慶幸業界已經有很多優秀的實踐,給我們提供很大的參考價值。

2. 持續完善平臺能力

最簡單的,參考 vivo 內部以及各大互聯網公司 redis 平臺化管理實踐,新 KV 的平臺能力建設還有非常多的事情要做,而且後續還會跟智能化 DB 運維結合在一起,想象空間更大。

3. 持續完善正確性校驗機制

數據可靠性和正確性是一個數據庫產品的安身立命之本,需要持續完善相應的校驗機制。

現階段我們還沒法承諾金融級的數據可靠性,我們會持續往這個方向努力,目前滿足一些特徵場景和緩存場景還是可行的。

我們已經在逐漸引入一些開源的 chaos 工具,希望能持續深入挖掘出系統的潛在問題,爲用戶提供更可靠的數據存儲服務。

4. 強化調度能力

分佈式數據庫核心是圍繞存儲、計算、調度 3 個話題展開的,可見調度的重要性,負載均衡就是其中一個環節,目前 Hash-based 的分片規則,後續能否改成 Region-based 的分片規則?能否跟 k8s 結合構建雲原生的 KV 存儲產品?能否讓數據分佈調整變得更智能、更自動化 …… 我們拭目以待。

5. 冷熱數據分離

本質還是成本和性能的權衡,對一些規模特別大的集羣,可能 90% 的數據是很少被訪問的,這些數據哪怕存到閃存,也是一種資源的浪費。一方面我們希望被頻繁訪問的數據能得到更好的讀寫性能,另一方面我們希望能最大限度的節省成本。

一個比較直接的方法,就是把熱數據存到內存和閃存上,一些冰封的冷數據則存到一些更便宜的介質(比如機械磁盤),這就需要系統自身具備判斷能力,能持續動態區分出哪些屬於熱數據,哪些屬於冷數據。

6. 支持更多類型的存儲引擎

目前已經支持了 Rocksdb 和 Titandb,後續會考慮引入更多類型的存儲引擎,比如純內存的,或者基於 AEP 等新閃存硬件產品的存儲引擎。

7.支持遠端 HDFS 冷備

對於在線場景,數據備份還是很重要的,當前 Nebula 已經支持本地集羣級的快照備份,但機器掛了,還是會存在大量數據丟失的風險,我們會考慮把數據冷備到遠端,比如 HDFS。是不是隻要把 HDFS 掛載成本地目錄,集羣把快照 dump 到指定目錄就可以了呢?我們會做進一步的思考和設計。

8. SPDK 磁盤讀寫

實際測試告訴我們,同樣是依賴 nvme 磁盤,單機上使用 SPDK 比不使用 SPDK 吞吐提升接近 1 倍。SPDK 這種 Bypass Kernel 的方案已經是大勢所趨,對磁盤 io 容易成爲瓶頸的場景,使用 SPDK 能有效提升資源利用率。

9. KV SSD

鑑於 SPDK Bypass Kernel 的優勢,業界提出了一種新的解決方案(KV SSD)。

Rocksdb 基於 LSM-tree 實現,Compaction 機制會帶來嚴重的寫放大,而 KV SSD 提供了原生的 KV接口,兼容 Rocksdb API,可以將新的數據記錄直接寫入到 SSD 中,不需要再進行反覆的 Compaction 操作,從而將 Rocksdb 的寫放大減小到 1,是一個非常值得嘗試的新技術。

10. 支撐圖數據庫

我們的 KV 產品之所以訂製 Nebula,其中一個重要原因是爲圖數據庫做準備的,目前已經在嘗試接入一些有圖需求的業務,以後希望能跟開源社區合作,共建領先的圖數據庫能力。

11. 支撐時序數據庫

在 5G 和 物聯網時代,時序數據庫起着非常重要的作用。

這個領域 Influxdb 目前比較領先,但開源版本不支持分佈式,只依賴一種爲時序數據設計的單機存儲引擎(TSM),實用價值非常有限。

我們的 KV 產品提供了現成的分佈式複製能力、標準化的平臺能力、高可用保障措施,我們希望能儘可能複用起來。

結合起來,是不是可以考慮把 TSM 跟分佈式複製能力做一個整合,外加對時序場景友好的 Sharding 策略,構建一個高可用的分佈式時序存儲引擎,替換掉開源 InfluxDB 的單機存儲層。

12.  支撐對象存儲的元數據存儲

元數據存儲對“對象存儲”來說至關重要,既然我們已經提供了一個強大的 KV 存儲產品,是不是可以複用起來,減輕運維和研發維護的負擔呢?

七、最後

實踐過程中我們需要不斷協調資源、收集需求、迭代產品,力求接入更多場景,收集更多需求,更好打磨我們的產品,儘早進入良性循環,一句話總結下心得體會:

vivo 大規模特徵存儲實踐

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章