作者:林楓 & 張根興來源:雲加社區背景:兩個十億級的挑戰
PaxosStore 是微信内廣泛應用的強一緻性的分布式存儲系統,它廣泛支撐了微信的在線應用,峰值過億TPS,運行在數千台服務器上,在線服務場景下性能強悍。但在軟件開發中沒有銀彈,在面對離線産出、在線隻讀的數據場景,PaxosStore 面臨了兩個新的十億挑戰:
10億 / 秒 的挑戰:
· "看一看"團隊需要一個存儲系統來存放CTR過程需要用到的模型,實現存儲和計算分離,使得推薦模型的大小不會受限于單機内存。
· 每次對文章的排序打分,ctrsvr 會從這個存儲系統中拉取成千上萬個特征,這些特征需要是相同版本的,PaxosStore 的 BatchGet 不保證相同版本。
· 業務方預估,這個存儲系統需要支持10億/秒的QPS,PaxosStore 的副本數是固定的,無法增加隻讀副本。
· 這個存儲系統需要有版本管理和模型管理的功能,支持曆史版本回退。
10億 / 小時 的挑戰:
· 微信内部不少團隊反饋,他們需要把10億級(也就是微信用戶的數量級)信息,每天定期寫到 PaxosStore 中,但 PaxosStore 的寫入速度無法滿足要求,有時候甚至一天都寫不完,寫太快還會影響現網的其他業務。
2. 數據路由
· 考慮擴縮容,FeatureKV 會把一個版本的數據切分為 N 份, N 現在是 2400,通過哈希 HashFun(key) % N 來決定 key 屬于那份文件。
· KVSvr 加載哪些文件是由一緻性哈希決定的,角色相同的 KVSvr 會加載相同一批在擴縮容的時候,數據騰挪的單位是文件。
· 由于這個一緻性哈希隻有 2400 個節點,當 2400 不能被 sect 内機器數量整除時,會出現比較明顯的負載不均衡的情況。所以 FeatureKV 的 sect 内機器數得能夠整除2400。還好 2400 是一個幸運數,它 30 以内的因數包括 1,2,3,4,5,6,8,10,12,15,16,20,24,25,30 ,已經可以滿足大部分場景了。
· 上圖是 N=6 時候的例子,Part_00[0-5] 表示 6 份數據文件。從 RoleNum=2 擴容成 RoleNum=3 的時候,隻需要對 Part_003 和 Part_005 這兩份文件進行騰挪,Part_005 從 Role_0遷出至 Role_2,Part_003 從 Role_1 遷出至 Role_2。
· 由于現網所用的 N=2400 ,節點數較少,為了減少每次路由的耗時,我們枚舉了 RoleNum<100 && 2400%RoleNum==0 的所有情況,打了一個一緻性哈希表。
3. 系統擴展性
· FeatureKV 的 FKV_WFS 上存有當前可用版本的所有數據,所以擴容導緻的文件騰挪,隻需要新角色的機器從 FKV_WFS 拉取相應編号的文件,舊角色機器的丢棄相應編号的文件即可。
· 當 BatchSize 足夠大的時候,一次 BatchGet 的 rpc 數量等價于 Role 數量,這些 rpc 都是并行的。當 Role 數量較大時,這些 rpc 出現最少一個長尾請求的概率就越高,而 BatchGet 的耗時是取決于最慢一個 rpc 的。上圖展示了單次 rpc 是長尾請求的概率是 0.01% 的情況下,不同 Role 數量情況下的 BatchGet 長尾概率,通過公式 1 - (0.999^N) 計算。
· 增加 Sect(讀性能擴容):
· 每個 Sect 都有全量的數據,增加一個 Sect 意味着增加一個隻讀副本,可以達到讀性能擴容的效果。
· 由于一個 BatchGet 隻需要發往一個 Sect ,RPC 數量是收斂的,不會因為底下的 KVSvr 有 200 台而發起 200 次 RPC。這種設計可以降低 BatchGet 操作的平均耗時,減少長尾請求出現的概率。
· 增加 Role(存儲容量 讀性能擴容):
· 假設每台機的存儲能力是相等的,增加 Role 的數量便可以增加存儲容量。
· 由于整個模塊的機器都多了,所以讀性能也會增加,整個模塊在讀吞吐量上的擴容效果等價于增加 Sect。
· 但當 Role 數量較大時,一次 BatchGet 涉及的機器會變多,出現長尾請求概率會增大,所以一般建議 Role 的數量不要超過30。
· 增加 DataSvr(寫性能擴容):
· DataSvr 是一個無狀态服務,可以做到分鐘級的擴容速度。
· 底下的寫任務是分布式的跑,一次寫會切分為多個并行的 job,增加 DataSvr 的實例數,可以增加整個模塊的寫性能。
· 數據遷移都是以文件為級别,沒有複雜的遷移邏輯,不考慮灰度流程的話,可以在小時級完成,考慮灰度流程一般是一天内。
4. 系統容災
· KVSvr 側:
· 每個 Sect 的機器是部署在同一個園區的,隻需要部署 2 個 Sect 就可以容忍一個園區的機器故障。
· 具體案例:2019年3月23号,上海南彙園區光纜被挖斷,某個 featurekv 有 1/3 的機器在上面,故障期間服務穩定。
· 故障期間部分 RPC 超時,導緻長尾請求增加。但是換機重試之後大部分請求都成功了,最終失敗出現次數很低。後續全局屏蔽了南彙園區的機器之後,長尾請求和最終失敗完全消失。
· DataSvr/WFS 側:
· 即便這兩部分整個挂掉, FeatureKV 的 KVSvr 還是可以提供隻讀服務,對于大部分 定時批量寫、在線隻讀 的場景,這樣已經足夠了。
· 具體案例:2019年6月3号,某個分布式文件系統集群故障,不可用9小時。某個 featurekv 的 USER_FS 和 FKV_WFS 都是這個集群。故障期間業務方的輸出産出流程也停止了,沒有産生寫任務。整個故障期間,featurekv 的讀服務穩定。
十億每秒的挑戰 在線讀服務的具體設計
1. KVSvr 讀性能優化
為了提高 KVSvr 的性能,我們采取了下面一些優化手段:
· 高性能哈希表:針對部分數據量較少、讀請求很高的數據,FeatureKV 可以用 MemTable 這一個全内存的表結構來提供服務。Memtable 底層實現是一個我們自己實現的隻讀哈希表,在 16 線程并發訪問的時候可以達到 2800w 的 QPS,已經超過了 rpc 框架的性能,不會成為整個系統瓶頸。
· libco aio:針對部分數據量較大、讀請求要求較低的數據,FeatureKV 可以用 BlkTable 或 IdxTable 這兩種表結構來提供服務,這兩表結構會把數據存放在 SSD 中。而 SSD 的讀性能需要通過多路并發訪問才能完全發揮。在線服務不可能開太多的線程,操作系統的調度是有開銷的。這裡我們利用了 libco 中對 linux aio 的封裝,實現了協程級的多路并發讀盤,經過壓測在 value_size 是 100Byte 的情況下,TS80A 上 4 塊 SSD 盤可以達到 150w /s 的QPS。
· 數據包序列化:在 perf 調優的過程中,我們發現 batch_size 較大的情況下(ctrfeaturekv 的平均 batch_size 是 4k ),rpc 數據包的序列化時耗時會較大,所以這裡我們自己做了一層序列化/反序列化,rpc 層的參數是一段二進制 buffer。
· 數據壓縮:不同業務對數據壓縮的需求是不一樣的,在存儲模型的場景,value 會是一段浮點數/浮點數數組,表示一些非 0. 特征。這時候如果用 snappy 這類明文壓縮算法,效果就不太好了,壓縮比不高而且浪費 cpu。針對這類場景,我們引入了半精度浮點數(由 kimmyzhang 的 sage 庫提供)來做傳輸階段的數據壓縮,降低帶寬成本。
2. 分布式事務 BatchGet 的實現
· 需求背景:更新分為全量更新和增量更新兩種,一次更新包括多條數據,每次更新都會讓版本号遞增,BatchGet 也會返回 多條數據。業務方希望這些更新都是事務的,BatchGet 的時候如果一個更新沒有全部執行完,那就返回上一個版本的數據,不能返回半新半舊的數據。
· RoleNum=1 的情況:
· 數據沒有分片,都落在同一台機器上,我們調研後發現有這麼兩種做法:
· MVCC: 多版本并發控制,具體實現就是 LevelDB 這樣的存儲引擎,保存多版本的數據,可以通過 snapshot 控制數據的生命周期,以及訪問指定版本的數據。這種方案的數據結構需要同時支持讀寫操作,後台也得有線程通過清理過期的數據,要支持全量更新也是比較複雜。
· COW: 寫時複制,具體的實現就是雙 Buffer 切換,具體到FeatureKV的場景,增量更新還需要把上一個版本的數據拷貝一份,再加上增量的數據。這種方案的好處是可以設計一個生成後隻讀的數據結構,隻讀的數據結構可以有更高的性能,缺點是需要雙倍的空間開銷。
·
·
· 為了保證在線服務的性能,我們采用了 COW 的方式,設計了 第一部分 中提到了隻讀哈希表,來做到單機的事務 BatchGet。
· RoleNum>1 的情況:
· 數據分布在不同機器,而不同機器完成數據加載的時間點不一樣,從分布式的角度去看,可能沒有一個統一的版本。
· 一個直觀的想法,就是保存最近N份版本,然後選出每個 Role 都有的、最新的一份版本。
· N 的取值會影響存儲資源(内存、磁盤)的開銷,最少是2。為了達到這個目的,我們在 DataSvr 側加入了這麼兩個限制:
· 單個表的更新是串行的。
· 寫任務開始結束之前,加多一步版本對齊的邏輯,即等待所有的 kvsvr 都加載完最新的版本。
· 這樣我們就可以在隻保留最近 2 個版本的情況下,保證分布式上擁有一個統一的版本。在 COW 的場景下,隻要把另外一個 Buffer 的數據延期删除(直到下次更新才删),就可以了保留最近 2 個版本了,内存開銷也不會變大。
擁有全局統一的版本之後,事務 BatchGet 應該怎麼實現呢?
· 先發一輪 rpc 詢問各 role 的版本情況?這樣做會讓QPS翻倍,并且下一時刻那台機可能就發生數據更新了。
· 數據更新、版本變動其實是很低頻的,大部分時刻都是返回最新一個版本就行了,并且可以在回包的時候帶上 B-Version (即另外一個 Buffer 的版本),讓 client 端在出現版本不一緻的時候,可以選出一個全局統一的版本 SyncVersion,再對不是 SyncVersion 的數據進行重試。
· 在數據更新的時候,數據不一緻的持續時間可能是分鐘級的,這種做法會帶來一波波的重試請求,影響系統的穩定性。所以我們還做了一個優化就是緩存下這個 SyncVersion ,每次 BatchGet 的時候,如果有 SyncVersion 緩存,則直接拉取 SyncVersion 這個版本的數據。
3. 版本回退
· 每個表的元數據中有一個回退版本字段,默認是0表示不處于回退狀态,當這個字段非0,則表示回退至某個版本。
· 先考慮如何實現版本回退:
· 考慮簡單的情況,一個表每次都是全量更新。那麼每次讓都是讓 KVSvr 從 FKV_WFS 拉取指定版本的數據到本地,走正常的全量更新流程就好了。
· 然後,需要考慮增量的情況。如果一個表每次更新都是增量更新,那麼回退某個版本 Vi,就需要把 V1 到 Vi 這一段都拉到 KVSvr 本地,進行更新重放,類似于數據庫的 binlog,當累計了成千上萬的增量版本之後,這是不可能完成的事。
· 我們需要有一個異步的 worker,來把一段連續的增量,以及其前面的全量版本,合并為一個新的全量版本,類似 checkpoint 的概念,這樣就可以保證一次回退不會涉及太多的增量版本。這個異步的 worker 的實現在 DataSvr 中。
· 更進一步,這裡有一個優化就是如果回退的版本在本地雙 Buffer 中,那麼隻是簡單的切換一下雙 Buffer 的指針就好,可以做到秒級回退效果。實際上很多回退操作都是回退到最後一個正常版本,很可能是上一個版本,在本地的雙 Buffer 中。
· 處于回退狀态的表禁止寫入數據,防止再次寫入錯誤的數據。
· 再考慮如何解除回退:
· 解除回退就是讓某個表,以回退版本的數據繼續提供服務,并且以回退版本的數據為基礎執行後續的增量更新。
· 直接解除回退狀态,現網會先更新為回退前的版本,如果還有流量的話則會讀到回退前的異常數據,這裡存在一個時間窗口。
· 數據的版本号要保證連續遞增,這一點在數據更新的流程中會依賴,所以不能簡單粗暴的删除最後一段數據。
· 為了避免這個問題,我們借用了COW的思想,先複制一遍。具體的實現就是把當前回退的版本,寫出一個全量的版本,作為最新的數據版本。
· 這一步需要點時間,但在回退的場景下,我們對解除回退的耗時要求并不高。隻要回退夠快,解除回退是安全的,就可以了。
十億每小時的挑戰 離線寫流程的具體設計
1. 背景
· DataSvr 主要的工作是把數據從 USER_FS 寫入 FKV_WFS,在寫入過程需要做路由切分、數據格式重建等工作,是一個流式處理的過程。
· FeatureKV 中目前有三種表結構,不同的表結構在寫流程中有不一樣的處理邏輯:
· MemTable: 數據全内存,索引是無序的哈希結構,容量受限于内存,離線寫邏輯簡單。
· IdxTable: 索引全内存,索引是有序的數組,Key量受限于内存,離線寫邏輯較為簡單,需要寫多一份索引。
· BlkTable: 塊索引全内存,索引是有序的數據,記錄着磁盤中一個 4KB 數據塊的 begin_key 和 end_key,容量沒限制,離線寫流程複雜,需要對數據文件進行排序。
2. 單機的 DataSvr
· 一開始,我們隻有 MemTable,數據都是全内存的。MemTable 的數據最大也就 200 GB,這個數據量并不大,單機處理可以節省分布式協同、結果合并等步驟的開銷,所以我們有了上面的架構:
· 一次寫任務隻由一個 DataSvr 執行。
· Parser 每次處理一個輸入文件,解析出 Key-Value 數據,計算路由并把數據投遞到對應的 Que。
· 一個 Sender 負責處理一個 Que 的數據,底下會對應多份 FKV_FS 的文件。FKV_FS 上的一個文件隻能由一個 Sender 寫入。
· 總的設計思想是,讓可以并行跑的流程都并行起來,榨幹硬件資源。
· 具體的實現,加入了很多批量化的優化,比如對FS的IO都是帶 buffer 的,隊列數據的入隊/出隊都是 batch 的等,盡量提高整個系統的吞吐能力。
· 最終,在台 24 核機器上的寫入速度可以達到 100MB/s,寫入 100GB 的數據隻需要 20 分鐘左右。
3. 分布式的 DataSvr
· 再往後,FeatureKV 需要處理十億級Key量、TB級的數據寫入,因此我們加入了 IdxTable 和 BlkTable 這兩種表結構,這對于寫流程的挑戰有以下兩點:
· 生成的數據需要有序,隻有有序的數據才能做到範圍索引的效果,讓單機的key量不受内存限制。
· TB 級的寫速度,100MB/s 是不夠用的,寫入 1TB 需要接近 3 小時的時間,并且這裡是不可擴展的,即便有很多很多機器,也是 3 小時,這裡需要變得可以擴展。
· 先考慮數據排序的問題:
· 我們得先把數據切片跑完,才能把一個 Part 的數據都拿出來,對數據進行排序,前面的數據切片類似于 MapReduce 的 Map,後續的排序就是 Reduce,Reduce 中存在着較大的計算資源開銷,需要做成分布式的。
· Map 階段複用上述的單機 DataSvr 邏輯,數據切分後會得到一份臨時的全量結果,然後實現一個分布式的 Reduce 邏輯,每個 Reduce 的輸入是一份無序的數據,輸出一份有序的數據及其索引。
· 這種做法有一次全量寫和一次全量讀的額外開銷。
· 具體的流程如下圖所示,DATASVR SORTING 階段由多台 DataSvr 參與,每個淺藍色的方框表示一個 DataSvr 實例。
· 再考慮大數據量情況下的擴展性:
· 參考上圖,現在 DataSvr 的排序階段其實已經是分布式的了,唯一一個單點的、無法擴容的是數據切片階段。
· 實現分布式的數據切片,有兩種做法:
i 一是每個 DataSvr 處理部分輸入的 User_Part 文件,每個 DataSvr 都會輸出 2400 個切片後的文件,那麼當一次分布式切片有 K 個 DataSvr 實例參與,就會生成 2400 * K 個切片後的文件,後續需要把相同編号的文件合并,或者直接作為排序階段的輸入。
ii 二是每個 DataSvr 負責生成部分編号的 FKV 文件,每次都讀入全量的用戶輸入,批處理生成一批編号的 FKV 文件。
· 第一種做法如果是處理 MemTable 或者 IdxTable,就需要後接一個 Merging 過程,來把 TMP_i_0, TMP_i_1, TMP_i_2 ... 合并為一個 FKV_i。而處理 BlkTable 的時候,由于其後續是有一個 Sorting 的邏輯的,隻需要把 Sorting 的邏輯改為接受多個文件的輸入即可。故這種做法的壞處是在數據量較少的時候,MemTable 或者 IdxTable 采用分布式數據切片可能會更慢,Merging 階段的耗時會比分布式切片減少的耗時更多;
· 第二種做法生成的直接就是 2400 個文件,沒有後續 Merging 流程。但它會帶來讀放大的問題,假設數據被切分成為 T 批,就會有 T-1 次額外的全量讀開銷。在數據量大的情況下,批數會越多,因為排序的數據需要全部都進内存,隻能切得更小;
· 在小數據場景,單機的數據分片已經足夠了,所以我們選用了第一種方案。
· 是否分布式切分,是一個可選項,在數據量較小的情況下,可以不走這條路徑,回到單機 DataSvr 的處理流程。
· 最終,我們得到了一個可以線性擴展的離線處理流程,面對10億、1TB數據的數據:
· 在實現 BlkTable 之前,這是一個不可能完成的任務。
· 在實現分布式數據切片之前,這份數據需要 120min 才能完成寫入。
· 現在,我們隻需要 71min 便可以完成這份數據的寫入。
· 上面這一套流程,其實很像 MapReduce,是多個 Map, Reduce 過程拼接在一起的結果。我們自己實現了一遍,主要是基于性能上的考慮,可以把系統優化到極緻。
現網運營狀況
· FeatureKV 在現在已經部署了 10 個模塊,共 270 台機,業務涉及看一看,搜一搜,微信廣告,小程序,微信支付,數據中心用戶畫像,附近的生活,好物圈等各類數業務,解決了離線生成的數據應用于在線服務的痛點問題,支撐着各類數據驅動業務的發展。
· 最大的一個模型存儲模塊有210台機:
· 11億特征/s: 日均峰值 BatchGet 次數是29w/s,平均 BatchSize 是 3900,模塊壓測時達到過 30億特征/s。
· 15ms: 96.3% 的 BatchGet 請求在 15ms 内完成,99.6% 的 BatchGet 請求在 30ms 内完成。
· 99.999999%:99.999999% 的事務 BatchGet 執行成功。
· 微信廣告基于 FeatureKV 實現個性化拉取 個性化廣告位置,推薦策略能夠及時更新。相比于舊的方案,拉取量和收入都取得了較大的增長,拉取 21.8%,收入 14.3%。
· 微信支付在面對面發券以及支付風控中都有用 FeatureKV,存儲了多份十億級的特征,之前一天無法更新完的數據可以在數小時内完成更新。
總結
一開始,這類定時批量寫、在線隻讀的需求不太普遍,一般業務會用 PaxosStore 或者文件分發來解決。
但随着越來越多的應用/需求都與數據有關,這些數據需要定期大規模輸入到在線服務當中,并需要很強的版本管理能力,比如用戶畫像、機器學習的模型(DNN、LR、FM)、規則字典,甚至正排/倒排索引等,因此我們開發了 FeatureKV 來解決這類痛點問題,并取得了良好的效果。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!