从vivo 大规模特征存储实践中学点经验
原創?黃偉鋒?
本文旨在介紹 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 服務,但背后我們做了大量的可靠性保障工作。
?
?
?
二、方案選型
?
在方案選型上,我們遵循一些基本原則:
-
源自開源,按需定制。
-
內部開源,集思廣益。
-
語言主流,架構主流。
-
可靠至上,高可維護。
?
先簡單介紹一下我們早期方案調研的一些優缺點分析:
?
?
說實話,調研的都是優秀的開源項目,但光靠官方代碼和設計文檔,沒有深入的實踐經驗,我們是很難斷定一個開源產品是真正適合我們的,適當的賽馬可以更好校準方案選型,同時也一定程度反映出我們較強的執行力。
?
總的來說我們是要在現有需求、潛在需求、易用性、架構先進性、性能、可維護性等各個方面中找到一個最優平衡,經過一段時間的理論調研和實踐以后,最終我們選擇了 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. 系統架構
?
?
在 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 成為最終一致的狀態,算法原型如下:
?
?
對 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 大规模特征存储实践中学点经验的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: idea配置tomcat里你不知道的小秘
- 下一篇: 调一调Hive on Spark参数,毫