在這篇文章中砚蓬,我們將會(huì)講述我們?cè)赗ockset使如何使用RocksDB和對(duì)RocksDB進(jìn)行調(diào)優(yōu)從而達(dá)到更好的性能的。我們認(rèn)為讀者對(duì)基于LSM tree構(gòu)建的存儲(chǔ)引擎盆色,例如RocksDB 如何工作的已經(jīng)很熟悉了灰蛙。
在Rockset,我們想要我們的用戶能夠以次秒級(jí)的寫延遲速度將數(shù)據(jù)導(dǎo)入到Rockset里面隔躲,并能夠在10毫秒的速度進(jìn)行查詢摩梧,因此,我們需要一個(gè)既可以支持快速在線寫入又可以支持快速讀取的存儲(chǔ)引擎宣旱。RocksDB就是這樣一種高性能的存儲(chǔ)引擎仅父。RocksDB被很多大公司使用,例如Facebook浑吟、Linkedin笙纤、Uber等。還有些項(xiàng)目例如MongoRocks组力、Rocksandra省容、MyRocks等等都是使用RocksDB作為存儲(chǔ)引擎,并且已經(jīng)成功的減少了空間放大和寫延遲的問題燎字。RocksDB的KV模型很適合用來實(shí)現(xiàn)混合索引腥椒。混合索引是輸入一個(gè)文檔候衍,在RocksDB存三份數(shù)據(jù)笼蛛,行式數(shù)據(jù)、列式數(shù)據(jù)蛉鹿、和搜索索引滨砍。因此我們決定使用RocksDB作為我們的存儲(chǔ)引擎。我們團(tuán)隊(duì)在RocksDB上有很豐富的經(jīng)驗(yàn)妖异,我們的CTO-Dhruba Borthakur在facebook開發(fā)了RocksDB惋戏。對(duì)于每一個(gè)數(shù)據(jù)的文檔,產(chǎn)生一系列的KV對(duì)随闺,然后把他們寫入到RocksDB數(shù)據(jù)庫(kù)里面日川。
讓我快速描述一下RocksDB存儲(chǔ)節(jié)點(diǎn)在整個(gè)系統(tǒng)架構(gòu)中的位置。
當(dāng)用戶創(chuàng)建一個(gè)collection矩乐,系統(tǒng)內(nèi)部會(huì)創(chuàng)建N個(gè)分片龄句,每個(gè)分片會(huì)進(jìn)行K次復(fù)制(通常k=2)回论,以實(shí)現(xiàn)高讀取可用性,每一個(gè)分片副本被分配給葉結(jié)點(diǎn)分歇。每一個(gè)葉節(jié)點(diǎn)被分配了許多集合的許多分片副本傀蓉。在生產(chǎn)環(huán)境中,每個(gè)節(jié)點(diǎn)分配的大概100個(gè)分片副本职抡。葉節(jié)點(diǎn)為分配給他們的每個(gè)分片副本創(chuàng)建一個(gè)RocksDB實(shí)例葬燎。對(duì)于每個(gè)分片副本,葉結(jié)點(diǎn)不斷從DistributedLogStore提取更新數(shù)據(jù)缚甩,并將更新數(shù)據(jù)應(yīng)用在RocksDB實(shí)例中谱净,當(dāng)收到查詢請(qǐng)求的時(shí)候,給葉節(jié)點(diǎn)分配了查詢計(jì)劃片段擅威。更多細(xì)節(jié)請(qǐng)參考Aggregator Leaf Tailer 或者 Rockset White Paper.
為了實(shí)現(xiàn)在每個(gè)葉子節(jié)點(diǎn)在不斷更新的同時(shí)保持高效的查詢壕探,我們花費(fèi)了大量時(shí)間對(duì)RocksDB進(jìn)行調(diào)優(yōu),下面郊丛,我們將會(huì)描述如何對(duì)RocksDB進(jìn)行調(diào)用李请。
RocksDB-Cloud
RocksDB 是一個(gè)嵌入式的KV存儲(chǔ)。一個(gè)RocksDB實(shí)例的數(shù)據(jù)不會(huì)復(fù)制到另外一臺(tái)機(jī)器上厉熟。當(dāng)機(jī)器宕機(jī)导盅,RocksDB不能恢復(fù)。為了實(shí)現(xiàn)持久性揍瑟,我們開發(fā)了RocksDB-cloud這個(gè)項(xiàng)目白翻,RocksDB-cloud把所有RocksDB實(shí)例的數(shù)據(jù)和元數(shù)據(jù)傳到S3上。所有葉節(jié)點(diǎn)的SST文件都復(fù)制到S3里面月培,當(dāng)一個(gè)葉子節(jié)點(diǎn)宕機(jī)的時(shí)候嘁字,這臺(tái)節(jié)點(diǎn)的s3數(shù)據(jù)將會(huì)被分配給所有分片復(fù)制節(jié)點(diǎn)中的一個(gè)節(jié)點(diǎn)恩急。對(duì)于每一個(gè)新的分片復(fù)制杉畜,葉子節(jié)點(diǎn)將會(huì)讀取相對(duì)應(yīng)的失敗葉子節(jié)點(diǎn)的s3 Bucket里的RocksDB文件。
Disable WAL
RocksDB將會(huì)把所有的更新寫入一個(gè)write ahead Log 里和活躍的memtable里面衷恭。這個(gè)write ahead log 是當(dāng)進(jìn)程重啟的時(shí)候此叠,用來恢復(fù)memtables 里面的數(shù)據(jù)。在Rockset随珠,所有的對(duì)于collections的更新都會(huì)首先被寫入到DistributedLogStore灭袁。DistributedLogStore 本身的作用和write ahead log 的作用是一樣的,而且窗看,我們也不需要保證查詢之間的數(shù)據(jù)一致性茸歧。因此先丟失memtables的數(shù)據(jù)墨礁,然后在重新啟動(dòng)的時(shí)候履怯,從DistributedLogStore 里面重新獲取也是可以隘截。所以,禁用RocksDB的write ahead log疙咸,意味著所有RocksDB寫操作都會(huì)在內(nèi)存中進(jìn)行。
Writer rate Limit
正如上面所講的宙拉,葉節(jié)點(diǎn)主要復(fù)制更新和數(shù)據(jù)查詢例朱,相比于查詢,我們可以容忍更高的寫延遲只锭,我們盡可能多的用一小部分可用計(jì)算容量來處理寫請(qǐng)求著恩,用大部分計(jì)算用量來處理讀請(qǐng)求。我們限制葉結(jié)點(diǎn)上的RocksDB實(shí)例每秒寫入的數(shù)據(jù)量蜻展。我們也限制寫線程的數(shù)量喉誊。這有助于最小化RocksDB寫入對(duì)查詢延遲的影響。此外纵顾,通過這種方式限制寫操作裹驰,我們永遠(yuǎn)不會(huì)以LSM樹不平衡或觸發(fā)RocksDB內(nèi)置的不可預(yù)測(cè)的back-pressure或stall機(jī)制而結(jié)束。注意片挂,這兩個(gè)特性在RocksDB中都不可用幻林,但是我們?cè)赗ocksDB上實(shí)現(xiàn)了它們。RocksDB支持速率限制音念,它可以限制存儲(chǔ)設(shè)備的寫入速率沪饺,但是我們需要一種可以限制從應(yīng)用程序?qū)懭隦ocksDB實(shí)例的機(jī)制。
Sorted Write Batch
如果單個(gè)update 可以通過WriteBatch實(shí)現(xiàn)批處理更處理闷愤,更進(jìn)一步整葡,如果WriteBatch 中連續(xù)的keys 是按順序進(jìn)行,那么RocksDB可以實(shí)現(xiàn)更高的寫入吞吐量讥脐。我們可以充分利用這兩個(gè)優(yōu)勢(shì)遭居,我們首先將持續(xù)傳入的更新批量處理為100KB大小的微型批次,并對(duì)其進(jìn)行排序旬渠,然后將其寫入RocksDB中俱萍。
Dynamic Level Target Sizes
在具有分層壓縮策略的LSM樹中,直到超過當(dāng)前l(fā)evel的目標(biāo)大小的時(shí)候告丢,才會(huì)使用下一個(gè)level的文件壓縮策略枪蘑。每個(gè)level的目標(biāo)大小是根據(jù)level 1 的目標(biāo)大小 和 level 系數(shù)(通常為10)確定的。當(dāng)last level達(dá)到RocksDB的博客所說的目標(biāo)大小的時(shí)候岖免,將會(huì)導(dǎo)致一個(gè)比預(yù)期的更高的空間放大系數(shù)岳颇。為了緩解這個(gè)現(xiàn)象,RocksDB可以根據(jù)上一個(gè)level的目標(biāo)大小動(dòng)態(tài)的設(shè)置每一個(gè)level的目標(biāo)大小颅湘。無論RocksDB中存儲(chǔ)的數(shù)據(jù)量為多少话侧,我們都將通過使用此特性來實(shí)現(xiàn)RocksDB預(yù)期的1.111的空間放大系數(shù)。這個(gè)特性可以通過設(shè)置dvancedColumnFamilyOptions::level_compaction_dynamic_level_bytes
為true
開啟闯参。
Shared Block Cache
如上所訴瞻鹏,葉結(jié)點(diǎn)分配了許多集合的分片副本术羔,每個(gè)分片副本都有一個(gè)RocksDB實(shí)例,我們沒有為每個(gè)RocksDB實(shí)例分配單獨(dú)的塊緩存乙漓,而是為一個(gè)葉結(jié)點(diǎn)上的所有RocksDB實(shí)例分配了一個(gè)全局的塊緩存级历。通過將所有分片副本中未使用的塊踢出葉子內(nèi)存,有助于提升內(nèi)存的利用率叭披。我們?yōu)閴K緩存分配葉子容器大約25%的可用內(nèi)存寥殖。即使有多余的可用內(nèi)存,我們也不增加塊緩存大小涩蜘。這是因?yàn)槲覀兿M僮飨到y(tǒng)的頁(yè)緩存可以使用這部分內(nèi)存嚼贡。頁(yè)緩存緩存所有的壓縮塊,而塊緩存緩存未壓縮塊同诫。因此頁(yè)緩存可以更密集的緩存那些不經(jīng)常用的文件塊粤策。正如Optimizing Space Amplification in RocksDB 這篇論文所講的那樣,F(xiàn)aceBook 部署了三個(gè)RocksDB實(shí)例误窖,頁(yè)緩存使文件系統(tǒng)的讀取減少了52%叮盘。頁(yè)緩存由計(jì)算機(jī)上的所有容器共享,因此頁(yè)緩存為計(jì)算機(jī)上運(yùn)行的所有頁(yè)容器提供服務(wù)霹俺。
No Compression For L0 & L1
根據(jù)RocksDB的設(shè)計(jì)原則柔吼,與其他level相比,LSM 樹中的L0 和 L1級(jí)別包含的數(shù)據(jù)非常少丙唧。因此愈魏,在L0和L1級(jí)別上壓縮數(shù)據(jù)沒有什么效果。但是只要不在這些級(jí)別上進(jìn)行壓縮想际,就可以節(jié)省一些CPU培漏。從L0到L1的每次壓縮都需要訪問所有L1文件。同樣胡本,range scan 不使用布隆過濾器牌柄,而是查找L0中所有文件。如果L0和L1的數(shù)據(jù)在讀取的時(shí)候進(jìn)行解壓縮打瘪,在寫入的時(shí)候進(jìn)行壓縮友鼻,那么這兩個(gè)頻繁的CPU密集型操作將使用CPU傻昙。這就是RocksDB小組不推薦壓縮L0和L1中的數(shù)據(jù)闺骚,而推薦使用LZ4壓縮其他級(jí)別數(shù)據(jù)的原因。
Bloom Filters ON key Prefixes
正如在converged indexing 中描述的那樣妆档,我們將以三種不同的方式和三鐘不同的key ranges 將每個(gè)文檔的每一列存到RocksDB中僻爽。對(duì)于查詢,我們對(duì)每個(gè)key ranges的讀取方法不同贾惦,具體來說胸梆,我們不會(huì)使用一個(gè)具體的key來在這些key ranges里面查找key敦捧。我們通常使用較小的共享key 前綴來查找key。因此碰镜,通過設(shè)置BlockBasedTableOptions::whole_key_filtering
為false兢卵,整個(gè)keys就不會(huì)用來填充,進(jìn)而導(dǎo)致為每個(gè)sst文件創(chuàng)建布隆過濾器绪颖。我們也可以設(shè)置ColumnFamilyOptions::prefix_extractor
秽荤,這樣只會(huì)為有用的key的前綴,創(chuàng)建布隆過濾器柠横。
Iterator Freepool
當(dāng)處理查詢的時(shí)候窃款,從RocksDB讀取數(shù)據(jù),我們需要?jiǎng)?chuàng)建一個(gè)或者多個(gè) rocksdb::Iterators牍氛。對(duì)于查詢來說晨继,需要執(zhí)行range scan 或者 檢索多個(gè)字段,因此需要?jiǎng)?chuàng)建多個(gè)iterators搬俊。但是創(chuàng)建這些迭代器是非常昂貴的和浪費(fèi)資源的紊扬。我們可以使用這些迭代器的空閑池,并嘗試在一次查詢中重復(fù)使用用迭代器唉擂。 但是我們不能在多次查詢中重復(fù)使用迭代器珠月。因?yàn)槊總€(gè)迭代器都引用特定的RocksDB快照。對(duì)于一次查詢來說楔敌,我們使用相同的RocksDB快照啤挎。
最后,這里有一些我們?cè)O(shè)置的RocksDB具體配置文件卵凑。
Options.max_background_flushes: 2
Options.max_background_compactions: 8
Options.avoid_flush_during_shutdown: 1
Options.compaction_readahead_size: 16384
ColumnFamilyOptions.comparator: leveldb.BytewiseComparator
ColumnFamilyOptions.table_factory: BlockBasedTable
BlockBasedTableOptions.checksum: kxxHash
BlockBasedTableOptions.block_size: 16384
BlockBasedTableOptions.filter_policy: rocksdb.BuiltinBloomFilter
BlockBasedTableOptions.whole_key_filtering: 0
BlockBasedTableOptions.format_version: 4
LRUCacheOptionsOptions.capacity : 8589934592
ColumnFamilyOptions.write_buffer_size: 134217728
ColumnFamilyOptions.compression[0]: NoCompression
ColumnFamilyOptions.compression[1]: NoCompression
ColumnFamilyOptions.compression[2]: LZ4
ColumnFamilyOptions.prefix_extractor: CustomPrefixExtractor
ColumnFamilyOptions.compression_opts.max_dict_bytes: 32768</pre>