本文旨在介紹 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. 支撐時序數據庫
這個領域 Influxdb 目前比較領先,但開源版本不支持分布式,只依賴一種為時序數據設計的單機存儲引擎(TSM),實用價值非常有限。
我們的 KV 產品提供了現成的分布式復制能力、標準化的平臺能力、高可用保障措施,我們希望能盡可能復用起來。
結合起來,是不是可以考慮把 TSM 跟分布式復制能力做一個整合,外加對時序場景友好的 Sharding 策略,構建一個高可用的分布式時序存儲引擎,替換掉開源 InfluxDB 的單機存儲層。
12. 支撐對象存儲的元數據存儲
元數據存儲對“對象存儲”來說至關重要,既然我們已經提供了一個強大的 KV 存儲產品,是不是可以復用起來,減輕運維和研發維護的負擔呢?
編輯:hfy
-
數據存儲
+關注
關注
5文章
970瀏覽量
50894 -
存儲系統
+關注
關注
2文章
409瀏覽量
40852 -
vivo
+關注
關注
12文章
3303瀏覽量
63267
發布評論請先 登錄
相關推薦
評論