Rocksdb 与 TitanDb 原理分析 及 性能对比测试
文章目錄
- 前言
- Rocksdb的compaction機制
- compaction作用
- compaction分類
- level style compaction(rocksdb 默認進行的compaction策略)
- level 的文件存儲結構
- compaction過程
- compaction中的level target size
- universal style compaction
- fifo style compaction
- Titan相比于rocksdb的核心優化
- key-value 存儲優化
- key-value 區分邏輯
- 版本控制
- GC垃圾回收機制
- 編譯和安裝
- Rocksdb
- TitanDb
- 測試
- 使用rocksdb提供的接口編寫測試代碼
- 編譯鏈接rocksdb和titandb 各自動態庫,生成二進制文件
- 測試
- 參考資料
前言
本文主要針對非關系型數據庫rocksdb以及在rocksdb基礎上所做的一些優化的titandb做一個整體的介紹。
從他們編譯,安裝,測試以及titanDb相比于rocksdb優化的方面進行一個整體歸納總結。
Rocksdb的compaction機制
compaction作用
compaction作為rocksdb使用LSM tree管理核心key-value數據的一種合并策略,主要是為了保證LSM tree的c0,c1…cn tree不過于冗余,影響讀性能。
關于LSM tree的介紹可以參考論文《The Log-Structured Merge-Tree (LSM-Tree)》
compaction分類
- level style compaction 按層級進行合并
- universal style compaction
- fifo style compaction
- none compaction
level style compaction(rocksdb 默認進行的compaction策略)
核心是 將數據分成互不重疊的一系列固定大小(例如 2 MB)的SSTable文件,再將其分層(level)管理。
level 的文件存儲結構
- 數據通過內存寫到作為buffer的L0中,再持久化到底層的L1-Ln
- 當需要確認key的位置時,通過二分查找確認key所處的文件,再去具體文件中查找。
該過程會遍歷整個level集合
- 每一層level都有target size,compaction的目的是為了限制每一層數據,且每一層target的增加都是成倍增加
compaction過程
- 當level0的數據到達時,則會觸發compaction(
level0_file_num_compaction_trigger表示L0的SST文件個數達到了觸發參數)到L1。操作過程會將L0中的file pick出來,以防重疊
- 當持續從L0 compaction到L1時,L1可能會達到target size。此時會從L1中挑選至少1個file,并與L2中的file進行合并,重新寫入L2中
- 當L2也達到了 target size,則和之前一樣進行file的合并,寫入到L3
- 并發compaction,通過
max_background_compactions控制可以進行并發compaction的個數
- L0 – L1 在并發compaction的層中,該問題可能會是compaction的性能瓶頸。
優化方式:通過設置max_subcompactions參數,來使用多線程方式分割 file range 進行并發compaction
compaction中的level target size
level_compaction_dynamic_level_bytes 來控制level target size的是固定還是可以動態進行調整的
-
level_compaction_dynamic_level_bytes = false則表示當rocksdb開始加載時,每一層的大小都一定固定了,且運行過程中不會發生變化。
eg: rocksdb初始化參數如下max_bytes_for_level_base = 268435456 level_compaction_dynamic_level_bytes = false max_bytes_for_level_multiplier = 10 num_levels = 3則最終的rocksdb每一層的大小為
以上配置下的各個層級targe size 為:
L1: 268435456
L2: 2684354560
L3: 26843545600 -
level_compaction_dynamic_level_bytes = true則表示rocksdb每一層的level大小并不是固定的,而是可以動態進行變化的。
當前模式下每一層的level size的動態調整同樣是基于QOS的思想,我們通過以上compaction的實現可以發現IO一定是通過level0 flush到底層的L1-LN,那么如果IO密集型業務下短時間內L0增長的sst文件急劇增加且遠大于L1的大小,這個時候如果L1-LN的大小還是固定的,那么IO肯定就阻塞在了從L0-L1之間。
此時可以通過該參數讓系統內部自動進行每一層的target size的調整:eg: 原來的L1 - L3: 1G,10G,100G
此時L0短時間內的SST文件及占用容量達到了3G,超過了L1 的1G,那么進行動態調整后(修改Level的相關配置參數)的L1-L3的大小變成了:3G,18G,108G
universal style compaction
核心實現同樣是通過 讀放大和空間放大 來降低寫放大,universal comapction
- 每當某個尺寸的SSTable數量達到既定個數時,合并成一個大的SSTable
- 通過讀放大和空間放大(合并時的臨時數據結構占用空間較大)來最小化寫放大
這種方式的讀放大和空間放大較為嚴重,雖然寫放大在一定程度上得到了緩解(同一個sst文件并不會產生多次寫入),但這并不是一個可以代替level方式的策略。
fifo style compaction
fifo style compaction當文件過時時,優先壓縮最老的文件,有點像cahce中數據的過濾。
所有的SST文件 都先存儲在L0中,當L0中的文件總大小超過了CompactionOptionsFIFO::max_table_files_size,則會選擇最老的文件進行刪除,所以它的key-value的數據從來不會復寫,也就是寫放大系數一直都是1。
當然這種方式有一個非常嚴重的問題是:核心key-value數據的刪除不會告知給用戶!!
這可嚴重影響系統 的可靠性啊。
Titan相比于rocksdb的核心優化
通過對Rocksdb的compaction過程的分析,我們發現rocksdb默認使用的level 策略存在寫放大的情況,且在正常IO的時候compaction是在后臺進行的,這個時候顯然compaction和上層IO產生競爭,從而影響整體的rocksdb寫性能。
此時Titan借助論文wisckey提出的key-value分離存儲的思想進行了一系列相關的優化。
具體的優化方面如下幾個方面:
key-value 存儲優化
如下圖為titan中寫請求闡述的數據存放方式
在LSM管理的SST文件之下多了一種blob結構,用來存儲value數據,具體的blobfile結構如下
其中:
- blob record 有序得保存了key-value的鍵值對,且key是從sst中進行的一份拷貝,用來和保存和value的映射,方便后續進行過時key的垃圾回收, 所以這里的存儲存在一些寫放大。不過,key本身數據量比較小,并不會有太激烈的IO資源競爭。
- meta block主要是為了可擴展性,來存儲了當前blobfile的一些屬性信息。
- meta index則主要是記錄metablock的索引信息,方便快速查找meta block。
key-value 區分邏輯
傳統的rocksdb在進行創建sorted string table(SST)文件的時候也是通過工廠函數模式進行table文件的實例化,這里titan也是借用該設計模式進行 key-value的分離實現,邏輯實現如下:
- 當value-size >= min_blob_size的時候,將整個key-value存放在blobfile之中,同時創建一個key-index的結構,存放在SST中,用來索引該key-value
- 反之,將整個key-value 存放在SST文件中
版本控制
因為多了一種數據結構,那么在分布式存儲系統中,即要保證在上層應用并發add或者delete的時候仍然能夠對外提供一致性的訪問服務。此時,blob file也要加入到系統的版本控制中,這里主要用的是Multi-version Concurrency Control (MVCC)多版本控制的思想。
通過頭尾相對的雙向鏈表進行版本文件的管理,同時設置一個versionSet的數據結構來管理所有的文件,同時增加一個current指針,一直指向最新的version版本。通過這種版本控制的方式,達到不需要對文件加鎖,即可在并發add/delete的時候對外提供一致性服務。
GC垃圾回收機制
titan的GC機制主要有兩個功能進行協助:
- BlobFileSizeCollector
這是一種屬性收集器,用來從對應的SST文件中SST屬性信息,最后收集到的屬性集叫做BlobFileSizeProperties。
收集前的表格和收集后的BlobFileSizeProperties表格如下:
對于SST的表格內容(大括號里面的內容):第一列表示blob file ID ,第二列表示blob record在blob file中的偏移地址,第三列表示blob record的大小。
對于BlobFileSizeProperties表格內容如下:第一列表示blob fileID,第二列表示數據的大小。 - EventListener
在compaction的時候,rocksdb本身會丟掉一部分舊數據來釋放空間。同樣,在titandb中,compaction之后,titan中的部分blobfile存儲的數據可能已經過時。此時,titan通過監控compaction的事件來觸發自身的GC機制。
這里GC做的內容是,通過對比compaction輸入,輸出前后的BlobFileSizeProperties表格內容變化的情況,來決定哪一些數據可以進行丟棄。過程如下:
可以發現輸入輸出前后:
blobfile 1: 從size-1536變為了size:512,那么表示blobfile1可以丟棄1024大小的內容
blobfile 2: 從size-1024 變成了沒有輸出,那么表示blobfile2可以完全丟棄
blobfile 3: 同blobfile2,也可以完全丟棄
對于每一個有效的blobfile,titan會在內存中維護一個有效的變量來記錄當前blob file可丟棄的大小。當進行compaction的時候,變量會隨著相應的blobfile的變化進行累加(以上event listener)。當GC開始的時候,會從累計的blobfile可丟棄的變量中挑選一個最大的blobfile最為可丟棄的候選人。
為了降低寫放大,titan盡可能減少GC所造成的IO,使用可控的配置來控制GC的寫放大,當blobfile挑選出來作為可丟棄的候選者的時候需要滿足一定的大小才能夠真正被丟棄。
這里使用的GC實現算法入下:
- 從blobfile中隨機選取一部分數據A,起其size 為a
- 遍歷A中所有的key, 使用d 來累加所有過時的key所代表的blob record(我們上面說過blobfile的結構,blob reocord代表的是key 對應的data的大小)
- 計算比例 r = d / a,如果r >= discardable_ratio,即認為當前過時的key已經超過了一定的比例,那么就在對應的blobfile上進行GC,否則不進行GC。
綜上為titanDb在rocksdb基礎之上所做的一些優化以及優化產生的周邊開發。顯然,key-value的分離存儲和管理是核心優化點,解決了rocksdb 原生compaction帶來的大量的value寫放大問題。
且通過后續對應的測試,也確實發現了big value場景下,寫性能確實有2-3倍的優化提升。
后續將深入titan的核心實現,詳細分析研究titan的實現邏輯,從而應用到現有的業務上。
編譯和安裝
Rocksdb
基本變異過程可以參考官方給的安裝步驟,不過坑比較多,可以將一下步驟和官方給的步驟結合起來看。
- 確認系統編譯器版本
gcc -v,建議版本在gcc 4.8.5以上,否則變異出來最新的代碼會缺少一些glibc的屬性信息。
這里如果發現系統gcc版本確實比較低,可以下載gcc較高版本編譯安裝一下,我這里選擇的是gcc 5.3,詳細可以參考gcc編譯安裝這里需要注意:
- 如果個人不想安裝的gcc在系統默認的目錄下,且gcc版本變更為可配置的,可以在gcc源碼生成makefile的那一步進行安裝的路徑指定(否則系統默認會安裝到/usr/local下):
./configure --prefix=/xxxx - 當GCC 編譯安裝在指定的目錄之后可以可以通過系統變量加載glibc庫和我們編譯好的gcc版本
修改當前用戶的.bashrc文件(root用戶的在/etc/.bashrc,個人用戶直接編輯~/.bashrc)
增加如下內容:
export CC=/xxx/gcc-5.3/bin/gcc#makefile中的gcc的路徑
export CXX=/xxx/gcc-5.3/bin/g++#makefile中的g++路徑
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/xxx/gcc-5.3/lib64#指定glibc的庫,使用當前版本編譯器的庫
- 如果個人不想安裝的gcc在系統默認的目錄下,且gcc版本變更為可配置的,可以在gcc源碼生成makefile的那一步進行安裝的路徑指定(否則系統默認會安裝到/usr/local下):
gflags安裝,gflags是谷歌提供的一些第三方庫,這里rocksdb部分功能需要使用到。
該步驟的安裝可以參考:gflags安裝a. git clone https://github.com/gflags/gflags.git b. cd gflags c. mkdir build && cd build#以下DCMAKE_INSTALL_PREFIX 之后的路徑為自己想要安裝的路徑,如果有root權限且可以安裝到系統目錄下,那么可以不用指定prefix選項 d. cmake .. -DCMAKE_INSTALL_PREFIX=/xxx -DCMAKE_BUILD_TYPE=Release e. make && make install #增加gflags的include 和 lib庫的路徑到系統庫下面,如上面未指定路徑,則系統默認安裝在 #/usr/local/gflags f. 編輯當前用戶下的bashrc,添加如下內容: export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/xxx/gcc-5.3/lib64:/xxx/gflags/lib export LIBRARY_PATH=$LIBRARY_PATH:/xxx/gflags/include- 安裝
snappy
sudo yum install snappy snappy-devel - 安裝
zlib
yum install zlib zlib-devel - 安裝
bzip2
yum install bzip2 bzip2-devel - 安裝
lz4
yum install lz4-devel - 安裝
zstardard
以上已經將wget https://github.com/facebook/zstd/archive/v1.1.3.tar.gz mv v1.1.3.tar.gz zstd-1.1.3.tar.gz tar zxvf zstd-1.1.3.tar.gz cd zstd-1.1.3 make && sudo make installrocksdb以及titandb所需要的一些系統庫安裝完成,接下進行rocksdb的安裝 - rocksdb源碼下載:
git clone https://github.com/facebook/rocksdb.git
或者直接在以上github鏈接中下載對應的源碼包即可
安裝,可以參考官方給的安裝過程rocksdb官網安裝過程
此時成功之后,可以看到expample下生成了官方所給出的一些rocksdb的測試樣例。cd rocksdb make static_lib #編譯rocksdb的靜態庫 cd example && make all
TitanDb
以上rocksdb的一些插件都安裝完成且成功之后,記錄下gflags是安裝路徑,接下來進行TitanDbb的安裝。Titan的官方安裝過程如下titan安裝
#titanDb屬于rocksdb 6.4版本基礎上的一個插件,所以編譯時仍然需要rocksdb對應代碼的支持
#下載rocksdb對應版本的代碼
git clone https://github.com/tikv/rocksdb pingcap_rocksdbgit clone https://github.com/tikv/titan.git
cd titan && mkdir build && cd build#編譯時需制定我們剛才下載好的titan使用的rocksdb版本代碼,在pingcap_rocksdb目錄下
#(不能使用最新的rocksdb代碼,否則部分文件會缺失
#同時還要指定我們編譯好的glfags 安裝的路徑
cmake .. -DROCKSDB_DIR=../pingcap_rocksdb -DCMAKE_PREFIX_PATH=/xxx/gflags -DCMAKE_BUILD_TYPE=Releasemake -j #編譯
測試
使用rocksdb提供的接口編寫測試代碼
目標:測試rocksdb的寫性能。
需求子功能如下:
- 可以指定寫請求的輸入
- key的選擇范圍 可以輸入(測試熱點讀的性能),默認(2^64)范圍內,key的生成在指定的范圍內是隨機的
- value的size可以輸入,可以測試不同大小的value場景下寫的性能
- compaction線程數目可以指定
編寫測試代碼如下 rocksdb_titan_test.h:
#include <ctime>
#include <cstdio>
#include <cstdlib>
#include <cassert>#include <sys/time.h>
#include <unistd.h>
#include <signal.h>#include <iostream>
#include <string>
#include <vector>#include "rocksdb/db.h"using namespace std;static long db_count = 3; //生成三個db數據庫,使用三個字進程分別向三個db數據庫壓測數據
static long test_count;
static long key_range;static long compaction_num = 32; //指定compaction的線程數,默認是32個
static long value_size = 100; //指定value_size的大小,默認是100KBstatic long db_no = -1;static long parse_long(const char *s)
{char *end_ptr = nullptr;long v = strtol(s, &end_ptr, 10);assert(*s != '\0' && *end_ptr == '\0');return v;
}static double now()
{struct timeval t;gettimeofday(&t, NULL);return t.tv_sec + t.tv_usec / 1e6;
}static string long_to_str(long n)
{char s[30];sprintf(s, "%ld", n);return string(s);
}/*初始化各個參數 以及 生成子進程來負責分別向各自的數據庫進行壓測*/
static void init(int argc, char *argv[])
{assert(argc == 5);test_count = parse_long(argv[1]);key_range = parse_long(argv[2]);value_size = parse_long(argv[3]);compaction_num = parse_long(argv[4]);/*如果key輸入為0,那么key的范圍為2^62次方,即完全的隨機key寫 */if (key_range == 0){key_range = 1L << 62;}assert(db_count > 0 && db_count <= 20 && test_count > 0 && key_range > 0 && value_size > 0 && compaction_num >= 0);for (long i = 0; i < db_count; ++ i){pid_t pid = fork();assert(pid >= 0);if (pid == 0){//childsignal(SIGHUP, SIG_IGN); //接收到兩個終止信號,則子進程終止運行db_no = i;break;}}if (db_no < 0){//parentsleep(1);exit(0);}srand((long)(now() * 1e6) % 100000000);
}/*在給定的key的范圍內,隨機生成key*/
static string rand_key()
{char s[30];unsigned long long n = 1;for (int i = 0; i < 4; ++ i){n *= (unsigned long long)rand();}sprintf(s, "%llu", n % (unsigned long long)key_range);string k(s);return k;
}/*使用模版函數來實例化Rocksdb和titanDb*/
template <class DB, class OPT>
static void do_test(const string &db_name)
{OPT options;options.create_if_missing = true;options.stats_dump_period_sec = 30;Options.use_fsync=true; //初始化db時使用SYNC寫,否則數據會通過文件系統寫入到pagecache中就返回了。if(compaction_num == 0) {options.compaction_style = kCompactionStyleNone;// 如設置的compaction線程數為0,則需制定None參數來禁止compaction} else {Options.max_background_compactions = compaction_num; //否則設置具體線程數}string db_full_name = db_name + "_" + long_to_str(db_no); //組合各個子db數據庫的名稱printf("%s: db_count=%ld, test_count=%ld, key_range=%ld\n", db_full_name.c_str(), db_count, test_count, key_range);/*rocksdb自身的open函數,打開./db/rocksdb_1 或者 ./db/titan_1作為db目錄*/DB *db;rocksdb::Status status = DB::Open(options, string("./db/") + db_full_name, &db);if (!status.ok()){cerr << "open db failed: " << status.ToString() << endl;exit(1);}const size_t long_value_len = 5 * 1024 * 1024;string long_value(long_value_len, 'x');assert(long_value.size() == long_value_len);for (size_t i = 0; i < long_value_len; ++ i){long_value[i] = (unsigned char)(rand() % 255 + 1);}double ts = now();const size_t value_slice_len = value_size * 1024; //每一個value的分片大小for (long i = 1; i <= test_count; ++ i){rocksdb::Slice rand_value(long_value.data() + rand() % (long_value_len - value_slice_len), value_slice_len);/*Put請求*/rocksdb::Status s = db->Put(rocksdb::WriteOptions(), rand_key(), rand_value);if (!s.ok()){cerr << "Put failed: " << s.ToString() << endl;exit(1);}/*每寫10000 key-value到數據庫,計算耗時和寫入性能*/if (i % 10000 == 0){double tm = now() - ts;printf("%s: time=%.2f, count=%ld, speed=%.2f\n", db_full_name.c_str(), tm, i, i / tm); fflush(stdout);}}printf("\n");sleep(30); //等待最后的stat dump輸出delete db;
}
rocksdb_test.cpp
#include "rocksdb_titan_test.h"int main(int argc, char *argv[])
{init(argc, argv);do_test<rocksdb::DB, rocksdb::Options>("rocksdb");
}
titan_test.cpp
#include "titan/db.h"
#include "rocksdb_titan_test.h"int main(int argc, char *argv[])
{init(argc, argv);do_test<rocksdb::titandb::TitanDB, rocksdb::titandb::TitanOptions>("titan");
}
編譯鏈接rocksdb和titandb 各自動態庫,生成二進制文件
將以上三個文件放在同一目錄下db_code,且在當前目錄下編寫用于編譯鏈接庫的Makefile
PS:makefile中的一些路徑(g++/gflags)需要和之前編譯rocksdb/titandb時的路徑一致
#高版本g++所在路徑
CC = /xxx/gcc-5.3/bin/gcc#指定編譯時所需要的庫
CFLAGS = -std=gnu++11 -Wall -O2 -fno-strict-aliasing -fPIC -pthread -rdynamic#指定rocksdb和titandb 運行時需要依賴的庫
LDFLAGS = -lz -lbz2 -ldl -lrt -llz4 -lsnappyROCKSDB_DIR = /home/xxx/rocksdb_vs_titan/rocksdb
ROCKSDB_INCLUDE_FLAGS = -I$(ROCKSDB_DIR)/include
ROCKSDB_LIB = $(ROCKSDB_DIR)/librocksdb.arocksdb:$(CC) $(CFLAGS) $(R_INCLUDE_FLAGS) -o rocksdb_test rocksdb_test.cpp $(R_LIB) $(LDFLAGS)TITANR_DIR = /home/xxx/rocksdb_vs_titan/rocksdb_vs_titan/pingcap_rocksdb
TITAN_DIR = /home/xxx/rocksdb_vs_titan/rocksdb_vs_titan/titan
TITAN_INCLUDE_FLAGS = -I$(TITAN_DIR)/include -I$(TITANR_DIR)/include -I$(TITANR_DIR)
TITAN_LIB = $(TITAN_DIR)/build/libtitan.a $(TITAN_DIR)/build/rocksdb/librocksdb.atitan:$(CC) $(CFLAGS) $(T_INCLUDE_FLAGS) -o titan_test titan_test.cpp $(T_LIB) $(LDFLAGS)all: rocksdb titan
編譯:
在當前目錄下db_code執行:
make rocksdb_test 來生成rocksdb的測試二進制文件 rocksdb_test
在當前目錄下db_code執行:
make titan_test 生成titandb的測試二進制文件 titan_test
或者執行make all生成兩個測試的二進制文件
測試
- 尋找一塊磁盤(建議ssd),格式化成文件系統
mkfs.xfs /dev/sdd - 掛載進入到文件系統
mount /dev/sdd /db_test && cd /db_test - 拷貝我們編譯好的兩個二進制文件到當前目錄,創建db文件夾
- 運行方式如下:
./rocksdb_test 500000 0 8 32
測試五十萬value大小為8KB隨機key寫性能,后臺設置compaction線程個數為32
輸出如下:
Thu May 14 23:03:14 2020
rocksdb_1: db_count=3, test_count=500000, key_range=4611686018427387904
Thu May 14 23:03:14 2020
rocksdb_2: db_count=3, test_count=500000, key_range=4611686018427387904
Thu May 14 23:03:14 2020
rocksdb_0: db_count=3, test_count=500000, key_range=4611686018427387904
Thu May 14 23:03:14 2020rocksdb_3: time=275.50, count=30000, speed=108.89
Thu May 14 23:03:14 2020rocksdb_2: time=277.97, count=30000, speed=107.92
Thu May 14 23:03:14 2020rocksdb_0: time=280.40, count=30000, speed=106.99
Thu May 14 23:03:14 2020rocksdb_3: time=282.53, count=30000, speed=106.18
Thu May 14 23:03:14 2020rocksdb_2: time=358.02, count=40000, speed=111.73
同時在當前目錄的db目錄下可以看到對應的rocksdb數據庫相關文件已經生成
#查看當前目錄下的db目錄
ls db/
rocksdb_0 rocksdb_1 rocksdb_2#查看rocksdb_0目錄可以看到已經生成的sst和manifest文件
ls rocksdb_0
001303.sst
001304.log
001305.sst
CURRENT
IDENTITY
LOCK
LOG
MANIFEST-000009
OPTIONS-000005
可以通過LOG文件,分析rocksdb的寫過程 + compaction過程以及產生的寫放大的詳細情況。
通過以上方式分別在不同value size 和compaction線程數的情況下對rocksdb和titan Db的性能進行測試。目前僅僅測試了寫,發現titanDb的隨機key以及big value情況下的寫性能優于rocksdb 2-3倍。
具體原因可以參考以上titanDB相對于rocksdb的核心優化點。
參考資料
wisckey論文:https://www.usenix.org/system/files/conference/fast16/fast16-papers-lu.pdf
titanDb設計: https://pingcap.com/blog/titan-storage-engine-design-and-implementation/
titanDb代碼: https://github.com/tikv/titan
rocksdb設計: https://github.com/facebook/rocksdb/wiki
rocksdb代碼: https://github.com/facebook/rocksdb
總結
以上是生活随笔為你收集整理的Rocksdb 与 TitanDb 原理分析 及 性能对比测试的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 阴阳师回合外造成伤害是什么意思?
- 下一篇: 其实直升机是不是很脆弱的?为啥电影中的直