vivo 大規模特徵存儲實踐

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

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

1、需求分析

AI 技術在 vivo 內部應用愈來愈普遍,其中特徵數據扮演着相當重要的角色,用於離線訓練、在線預估等場景,咱們須要設計一個系統解決各類特徵數據可靠高效存儲的問題。github

1. 特徵數據特色

(1)Value 大redis

特徵數據通常包含很是多的字段,致使最終存到 KV 上的 Value 特別大,哪怕是壓縮過的。算法

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

特徵場景要存的數據量很大,內存型的 KV(好比 Redis Cluster)是很難知足需求的,並且很是昂貴。無論離線場景仍是在線場景,併發請求量大,Value 又不小,吞吐天然就大了。 mongodb

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

大部分特徵場景要求讀寫延時很是低,並且持續平穩,少抖動。緩存

(4)不須要範圍查詢性能優化

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

(5)定時灌海量數據

不少特徵數據剛被算出來的時候,是存在一些面向 OLAP 的存儲產品上,並且按期算一次,但願有一個工具能把這些特徵數據及時同步到在線 KV 上。

(6)易用

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

2. 潛在需求

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

    咱們的目標是星辰大海,毫不僅限於知足特徵場景。

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

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

  • 可維護性強

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

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

3. 存儲系統的冰山

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

vivo 大規模特徵存儲實踐

2、方案選型

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

  • 源自開源,按需定製。

  • 內部開源,集思廣益。

  • 語言主流,架構主流。

  • 可靠至上,高可維護。

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

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

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

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

3、Nebula 簡介

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

1. 存儲計算分離

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

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

2. 強一致,架構主流

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

3. 可伸縮

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

4.易維護

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

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

4、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 的方式,也是咱們須要的,但面向圖場景,後續有沒有進一步的規劃,須要持續關注社區動態。

5、特徵存儲平臺介紹

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 集羣。

6、將來展望

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 存儲產品,是否是能夠複用起來,減輕運維和研發維護的負擔呢?

7、最後

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

vivo 大規模特徵存儲實踐

相關文章
相關標籤/搜索