攜程 Redis On Rocks 實踐,節省 2/3 Redis成本
作者簡介:patpatbear,攜程軟體技術專家,負責攜程快取核心的維護,熱愛開源,專注於高效能、分散式NoSQL系統的建設和應用。
一、背景
redis使用記憶體作為儲存介質,具有良好的效能和低延遲,但其記憶體容量通常成為瓶頸,且記憶體價格較高,導致redis使用成本較高。
隨著SSD磁碟效能的不斷提高,NVMe SSD的隨機讀寫延遲也僅有幾十微秒,與redis的固有延遲(100~200us)相當,用SSD作為儲存介質也可以達到較低的延遲,同時節省成本。
因此我們研發了ROR(Redis-On-Rocks)產品,透過對redis核心增強以支援資料冷熱交換,使用磁碟擴充套件快取容量,可節省約2/3成本,而效能也能滿足大多數業務需求。
二、ROR簡介
ROR核心思路很簡單:在redis codebase基礎上擴充套件冷熱交換功能,實現redis資料冷熱多級儲存,降低快取的綜合使用成本。
ROR將資料分為冷熱兩部分:
-
熱資料沿用redis引擎,使用記憶體儲存,資料結構和原生redis完全一致
-
冷資料選用RocksDB引擎,使用磁碟儲存,以subkey為粒度儲存在RocksDB中
ROR負責冷熱資料的交換:
-
換入(從RocksDB到redis):當客戶端訪問冷資料,則將RocksDB中的資料換入到redis中,ROR把命令依賴的資料換入到redis,後續命令執行與原生redis一致。
-
換出(從redis到RocksDB):當記憶體用量超過maxmemory之後,則將熱資料換出到RocksDB中,ROR冷熱交換演算法採用了redis原生的LFU演算法,原本被redis evict的資料將被交換到記憶體中。
由於ROR繼承了redis的資料結構和命令實現,只負責冷熱資料交換,因此可以相容幾乎所有的redis命令,可快速跟進redis官方新特性。
三、與RoF對比
從長遠發展考慮,redis是事實上的快取標準,快取核心基於社群開源redis更便於跟進社群redis演進,因此ROR選擇了基於redis基礎上擴充套件冷熱交換能力。
RedisLabs的商業產品Redis-on-Flash(RoF)與ROR設計思路類似,但是調研之後,我們發現RoF在成本、通用性、效能等方面並不能滿足我們的需求。
3.1 成本
RoF把value儲存在磁碟、key保留在記憶體主表,可以方便地相容dbsize、scan、randomkey等命令,但佔用的記憶體量會隨著dbsize線性上升。
冷資料key儲存在記憶體主表(hashtable),每個key的輔助指標、robj等平均佔用約50B,生產String型別平均value大小為512B。從成本的角度看,按照key佔記憶體10%,value佔90%計算
-
換出80%value,可減少72% (80%*90%) 記憶體
-
換出80%冷key,可繼續減少29% (10%*80%/(100%-72%))記憶體
因此ROR並不把冷資料的key儲存在記憶體,而是儲存到RocksDB中單獨的meta column family。
考慮到meta column family訪問比較頻繁,且只儲存type、expire之類的少量後設資料,因此用少量記憶體(block cache)可以快取多數冷key。
經驗證,分配256MB block cache後,把冷資料的key儲存到RocksDB並不會降低整體QPS,但會增加IO執行緒的CPU消耗,由於redis宿主機cpu利用率只有10%,用cpu換記憶體是可以接受的。
3.2 通用性
為了避免重複快取,RoF禁用了RocksDB層的table cache和檔案系統層的page cache。這意味著訪問冷key時必須進行IO操作,因此冷key和熱key的訪問延遲會有較大區別。
為了提高通用性,ROR合理利用RocksDB層的table cache和作業系統層的page cache,儘可能利用未被佔用的記憶體,減少訪問冷key和熱key之間的延遲差距。
實際上,無論是DBA還是業務方,都很難準確預測快取叢集是否存在明顯的冷熱特徵。ROR適用於通用場景,能夠大大減少溝通成本和業務方關於延遲的擔憂。
在redis遷移至ROR時,我們並不評估應用程式是否具有冷熱特徵,只要業務QPS在redis的一半以下,對P99延遲不是非常敏感,就可以將其遷移到ROR。
3.3 效能
RoF按key粒度儲存,key與RocksDB key一一對應;而ROR按subkey粒度儲存,subkey都與RocksDB key一一對應。
對於HSET、HGET等聚合型別命令,RoF需要換入換出整個key,而ROR只讀寫必要的subkey,因此讀寫放大遠低於RoF,QPS和延遲也優於RoF。
以下為ROR、RoF在大壓力(100執行緒不限QPS)和普通壓力(1000執行緒10000QPS),讀寫純冷資料的QPS和延遲。可以看出:
-
大壓力情況下,ROR HGET、HSET命令QPS約為RoF的2~3倍
-
普通壓力情況下,ROR延遲約300~500us,遠低於RoF 14~120ms 延遲
測試說明:
-
資料:hash:5,000,000 (key count) * 2KB (per key,5個field,每個filed 400B)
-
配置:ROR的maxmemory設定為200MB;RoF有最小記憶體限制,設定為2G
-
場景:a)100thd:壓力測試,100客戶端併發,不限速測試;b)1wqps:模擬常規訪問,1000客戶端,限速1W QPS測試
對於超大的聚合key,RoF將整個key載入到記憶體中,會有明顯的延遲尖刺(可達秒級);而ROR只將必要的subkey換入記憶體,則不會有明顯的延遲尖刺。
多數使用redis的業務對延遲比較敏感,不能接受過大延遲尖刺。
測試說明:
-
hash:共有1,000,000個元素,每個元素128B
-
list:共有1,000,000個元素,每個元素128B
四、實現方案
4.1 冷熱交換
以下是客戶端訪問到冷key時ROR的處理過程。其中藍色模組與原生redis相同,橙色模組為ROR新增的冷熱交換功能。
總體上ROR先冷熱交換(swap),再執行命令處理流程。
冷熱交換(swap)過程主要分為以下步驟:
1)語法分析:分析客戶端命令涉及哪些key和subkey。比如,可以分析出MGET k1 k2 k3依賴於k1,k2,k3;而HMGET h1 f1 f2 f3,依賴於 h1.{ f1, f2, f3 }。
2)加鎖:根據語法分析出的結果,對命令所依賴的key加鎖。值得注意的是,這裡的鎖並不是pthread_mutex之類的執行緒鎖,而是ROR專案實現的一種單執行緒鎖,本質上是一個等待佇列,詳細介紹參考後續併發控制章節。
3)提交SWAP任務:拿到鎖之後,提交swap任務到IO執行緒組執行RocksDB讀寫。
4)執行RocksDB讀寫:IO執行緒組執行RocksDB讀操作。
5)合併資料:將RocksDB讀取的資料合併到redis中。
經過swap過程之後,冷資料已經換入到redis,後續執行命令與原生redis一致。
4.2 併發控制
redis架構上為單主執行緒,而RocksDB提供的是阻塞模式的API,直接使用redis主執行緒呼叫RocksDB將極大降低redis的效能。為了提高IO吞吐,ROR使用了額外的IO執行緒組執行RocksDB讀寫。由於增加了IO執行緒組,對於同一key的讀寫不再是單執行緒,如果不加控制,那麼資料將變得錯亂。
為了控制併發,ROR設計實現了一種單執行緒可重入鎖來保證同一時間對同一key只有一個客戶端在進行IO交換。這裡的鎖並不是pthread_mutex這種系統執行緒鎖,其本質是一個等待佇列:當key被鎖定後,嘗試獲取該鎖的客戶端必須等待前序客戶端釋放鎖之後才能獲取到key的鎖。
如下圖所示,C1、C2、C3三個客戶端先後執行了MGET命令,其中Key1、Key2、Key3均為冷資料。
C1依賴Key1、Key2,由於這2個key未被鎖定且為冷,因此C1獲取到Key1、Key2的鎖,並觸發了Key1、Key2換入;
C2依賴Key2、Key3,由於Key2被C1鎖定,因此C2等待C1執行結束才能獲取key2鎖;Key3未被鎖定且為冷,因此C2獲取到了Key2的鎖,並觸發了Key3換入;
C3依賴Key1、key3,由於Key1、Key3分別被C1、C2鎖定,因此C3等待C1、C2執行結束後才能獲取Key1、Key3鎖。
因此最終換入Key1、Key2、Key3換入後,客戶端執行順序為C1=>C2=>C3。
以上是一個簡單的示例,ROR為了實現FLUSHDB/BGSAVE之類涉及整個keyspace的命令併發控制需求,等待佇列包含KEY、DB、SVR三種粒度的鎖,大粒度的鎖需等待細粒度鎖釋放後才能獲得。此外為了確保MULTI/EXEC事務不產生死鎖,允許同一個事務重複鎖定同一key(亦即可重入)。
如下圖所示,C1、C2兩個客戶端先後發起2個事務。
C1依賴Key1(2次),由於C1在同一事務中依賴Key1(2次)且為冷,因此C1獲得Key1鎖並觸發換入;
C2依賴Key2(2次)、DB0、SVR鎖,由於C2在同一事務中依賴Key2(2次)且為冷,因此C2獲得Key2鎖並觸發換入;注意由於C2依賴DB0鎖,DB0鎖範圍大約Key1、Key2,因此只有C1釋放Key1之後才能獲得DB0鎖。
假設Key1先於Key2被換入,Key1換入後,C1事務得到執行並釋放Key1鎖。
當Key2換入後,C2獲得DB0鎖以及SVR鎖(獲得所有鎖),C2事務得到執行。
4.3 冷資料儲存
與業界多數方案一樣,ROR的冷資料儲存採用了RocksDB引擎,設計上參考了kvrocks、pika等專案,主要有3個要點:
-
key儲存到RocksDB
-
subkey與RocksDB KV對應(i.e. 按subkey儲存)
-
lazy刪除聚合型別key
key儲存到RocksDB
ROR為了做到記憶體消耗與dbsize無關,記憶體中並不會儲存冷key,key型別、expire、version等資訊會儲存到RocksDB的metaCF中。這樣設計主要是考慮每個key需要額外消耗約50B,如果dbsize為1億則需要額外消耗約5GB記憶體。對dbsize大、value小的叢集來講,額外消耗的記憶體過多,冷熱分離的價效比則不高。
因此ROR和RoF不同,不會把冷key儲存在記憶體中,少量與key相關(scan、randomkey、dbsize)命令,則進行適配性改造。
subkey與RocksDB KV對應
RocksDB的資料型別只有KV,與redis支援hash、set、zset等聚合型別key不能一一對應,因此需要構造redis聚合型別key與RocksDB KV型別之間的對應關係。
最直接的方案是將redis的聚合型別key直接序列化單個為RocksDB KV,但這種方案的缺點非常明顯,即HGET hash subkey只依賴單個subkey的命令,也需要將整個聚合型別key換入到記憶體,這會造成嚴重的讀寫放大。
因此ROR將聚合型別的subkey儲存為RocksDB KV,換入聚合型別資料冷key只需要換入必要的subkey。
lazy刪除聚合型別key
對於聚合型別key而言,每個subkey對應RocksDB KV,ROR刪除聚合key需要刪除掉所有的subkey,直接從RocksDB中迭代刪除複雜度為O(N),會造成延遲尖刺。
參考pika、kvrocks的設計,聚合型別key都有版本號,ROR刪除聚合key時,只刪掉metaCF的後設資料,而其他subkey則在RocksDB compaction中透過compaction filter逐漸過濾刪除。
hash/set/zset編碼
以下是hash/set型別的編碼格式:
每個hash/set在metaCF有1個RocksDB KV,記錄了型別、超時時間、版本以及subkey數量。
每個hash/set在defaultCF有N個RocksDB KV,每個subkey對應一個。由於每個subkey都記錄了對應的version,因此刪除聚合key只需要把metaCF的KV刪掉即完成lazy刪除。
zset型別的編碼格式類似,只多了scoreCF記錄zset的score排序。
list編碼
由於與hash/set/zset的操作差別較大,list資料模型設計上也有所差別。設計上,ROR記憶體中的list仍復用redis資料結構,且list可能只有部分subkey在記憶體中。
模型上list的設計如下:
-
list為任意段rockslist(冷)和memlist(熱)的組合
-
list元素要麼在memlist、要麼在rockslist,memlist沒有交集
-
分段資訊儲存在listObjectMeta.segments中,segments的每個元素表示一段,記錄了每段的型別以及長度。
rockslist也按subkey粒度儲存在RocksDB中。
4.4 cuckoo filter減少IO
前面提到ROR為了做到記憶體用量與dbsize無關,key元資訊不儲存在記憶體中,因此如果客戶端訪問的key不是熱資料,則必須查詢RocksDB才能確認key是否存在:對於key存在的情況,讀RocksDB並換入冷資料是必要的;但如果key不存在,則讀RocksDB是非必要的。特別是當業務keyspace miss率高的情況(比如重複讀不存在的key),存在大量的非必要IO情況,降低了整體效能。
對於過濾不存在key問題,用bloom filter能以8~10 bit per key的記憶體取得很好的過濾效果,但由於bloom filter不支援刪除,而ROR的keyspace始終處於動態變化中,因此bloom filter功能上無法滿足需求。
經過調研之後,我們發現cuckoo filter可以很好地滿足我們的需求,支援刪除並且記憶體消耗量僅需8 bit per key即可滿足ROR過濾準確度需求。
由於無法預測準確到key數量,ROR實現cuckoo filter時採用了多個容量指數增長的cuckoo filter組成的cascading cuckoo filter。
經過測試我們發現,對於keyspace miss場景,cuckoo filter可以將ROR的QPS從5W提升到6W左右,吞吐提升約20%;對於keyspace hit場景則無明顯影響。
4.5 相容redis複製
ROR的複製協議完全相容redis原生複製,全量複製採用RDB格式,增量複製使用RESP協議。由於完全相容redis原生複製協議,ROR可以直接對接xpipe,具備DR能力。
流式全量複製
ROR與Redis全量複製主要流程相同:master fork出child程式,由child程式打RDB。ROR由於有冷熱兩類資料,因此生成RDB的與原生Redis有區別:
-
熱資料生成RDB方案不變
-
冷資料先獲取RocksDB CHECKPOINT,然後SCAN冷資料轉換為RDB格式
冷資料(RocksDB部分)生成RDB的一種方案是將冷key臨時載入記憶體,複用redis的序列化方法構造RDB,但這種方案載入全部冷key會消耗大量CPU,當遇到redis宿主機當機重啟,大量redis全量同步爭用CPU將導致全量同步時間過長。
出於效能考慮,ROR構造RDB並不載入冷key,而是採用了流式構造RDB的方案:使用一個IO執行緒迭代RocksDB全量資料,並將迭代的資料流式append到RDB中。需要注意的是,流式構造RDB依賴於ROR在儲存設計上將同一個聚合型別key的subkey儲存在RocksDB相鄰位置。
實現層面,流式構造RDB方案避免了把key載入到記憶體並跳過redis層重新編碼,直接將RockDB資料流式填充到rdb,全量複製速度315MB/s,可以達到redis複製效能(390MB/s)的80%左右。
併發增量複製
redis增量複製過程中,master透過單個複製客戶端推送複製流到slave。由於複製客戶端只有1個(冷熱交換最大併發為1)如果ROR slave直接用複製客戶端交換資料,會出現slave複製無法跟上master寫入。
為了提高複製交換效能,ROR將從複製客戶端將收到的命令分發到多個worker客戶端,併發執行交換。
如果worker客戶端在交換結束後直接呼叫命令,那麼slave上命令執行的順序可能與master不同,造成主從資料不一致。
ROR採用的方案下,worker客戶端交換結束後並不立即執行命令,而是等到前序命令全部執行完之後在執行。由於slave執行增量複製命令與master向下傳播的複製流的命令順序一致,可以確保主從資料一致。
如上圖所示,①、②、④在併發執行IO操作,雖然②、④可能在①之前完成資料交換,但一定會等到①完成IO後再執行命令。
ROR增量複製併發改造後,slave處理複製命令速度從幾千QPS提升到大於master的最大寫入速度(5~10W QPS左右,與冷熱資料佔比相關)。
五、生產實踐
從經驗來看,多數redis叢集QPS較低但記憶體用量較大,redis宿主機通常因為達到記憶體上限觸發擴容,但CPU資源則比較空閒,比如攜程內redis宿主機平均CPU使用率約15%,但平均記憶體使用率達到50%。
ROR採用磁碟增加了快取容量,能容納更多的資料量,但RocksDB引擎的compaction和壓縮會消耗更多的CPU資源,因此ROR可以認為是用空閒的CPU換記憶體的成本解決方案。
成本方面,經驗資料顯示1個ROR例項可容納3個redis例項的資料,因此redis遷ROR能節省2/3的成本。
目前在ROR在生產部署了幾萬個例項。由於海外公有云記憶體單價高,已基本全部部署為ROR,每年可以節省成本上千萬元。
效能方面,從吞吐量考慮,攜程內部redis叢集高QPS佔比較低,遠低於ROR的QPS上限(參考上文效能資料)。
從延遲考慮,ROR設計上合理利用快取,按subkey粒度儲存,且硬體上nvme SSD延遲只有幾十微秒,因此與Redis相比延遲並沒有特別明顯的上升。
以下為一個典型redis叢集遷移ROR後延遲對比,其中80%為冷資料、20%為熱資料,遷移前後客戶端訪問延遲從200us變為220us左右。
六、專案開源與未來計劃
6.1 專案開源
目前ROR(Redis-On-Rocks)已開源,採用與Redis一致的BSD協議。
6.2 未來計劃
1)提升單例項QPS
部分業務場景(比如大資料相關業務)不但資料量大,而且QPS也比較高,這些叢集可能出現ROR主執行緒100%情況。針對這些場景,我們考慮從軟硬體2個層面最佳化,軟體層面考慮減少冷熱交換損耗、自動化pipeline減少網路CPU消耗;硬體層面使用更高主頻的CPU提升上限。
2)完善資料結構支援
部分使用頻次較少的資料結構待最佳化,比如:bitmap目前按照string處理,讀寫放大比較大,待最佳化效能;stream目前尚未支援,使用記憶體儲存,待支援。
3)減少全量同步
國內與海外的頻寬比較小,如果出現全量同步則海外業務受影響時間會比較久。隨著隨著海外部署量上升,這個問題的影響性逐步增大,後續ROR考慮提供可用性與一致性的選項,允許少量資料不一致的情況下增量同步。
來自 “ 攜程技術 ”, 原文作者:patpatbear;原文連結:https://mp.weixin.qq.com/s/iSeLZy9wlez07Rv4wT44og,如有侵權,請聯絡管理員刪除。
相關文章
- 節省數億IT成本,B站FinOps實踐
- redis入門指南(四)—— redis如何節省空間Redis
- 節省數億IT成本,B站FinOps最佳化實踐
- Redis核心原理與實踐--Redis啟動過程原始碼分析Redis原始碼
- Redis使用與實踐Redis
- redis實踐及思考Redis
- 記十億級Es資料遷移mongodb成本節省及效能優化實踐MongoDB優化
- Redis 實戰 —— 02. Redis 簡單實踐 - 文章投票Redis
- “無線射頻識別”可節省1/3物流成本
- Spring Data Redis 最佳實踐!SpringRedis
- Redis 實戰 —— 03. Redis 簡單實踐 - Web應用RedisWeb
- 壓力測試redis redis-benchmark 優化實踐Redis優化
- 節省 58% IT 成本,呼叫函式計算超過 30 億次,石墨文件的 Serverless 實踐函式Server
- 伺服器租用能節省哪些成本伺服器
- Redis原理及實踐之GeoHashRedis
- Redis Cluster深入與實踐(續)Redis
- Redis主從同步配置實踐Redis主從同步
- 開源實踐 | 攜程在OceanBase的探索與實踐
- 開源實踐 | 攜程在 OceanBase 的探索與實踐
- Java Redis系列2 (redis的安裝與使用+redis持久化的實現))JavaRedis持久化
- 淺談Redis的隱性成本Redis
- Redis威脅流量監聽實踐Redis
- Redis叢集環境搭建實踐Redis
- Redis在秒殺功能的實踐Redis
- Redis的底層實現---字串章節Redis字串
- 攜程小程式內嵌webview實踐指南WebView
- 攜程MySQL遷移OceanBase最佳實踐|分享MySql
- IATA:過去50年航空實際單位成本節省4倍
- 攜程DBA負責人俞榕剛:OceanBase在攜程的落地和實踐
- 資料成本:雲端儲存成本高嗎如何節省資料儲存成本
- Redis 中的原子操作(3)-使用Redis實現分散式鎖Redis分散式
- Python–Redis實戰:第三章:Redis命令:第七節:其他命令PythonRedis
- Python–Redis實戰:第三章:Redis命令:第五節:有序集合PythonRedis
- 千億級資料遷移mongodb成本節省及效能優化實踐(附效能對比質疑解答)MongoDB優化
- Redis Pipelining 底層原理分析及實踐Redis
- Redis叢集slot遷移改造實踐Redis
- kubernetes生產實踐之redis-clusterRedis
- ScaleFlux CSD 2000 在攜程的應用實踐UX