基于持久内存的 单机上亿(128B)QPS -- 持久化 k/v 存储引擎
文章目錄
- 性能數(shù)據(jù)
- 設(shè)計背景
- 設(shè)計架構(gòu)
- Hash 索引結(jié)構(gòu) 及 PMEM空間管理形態(tài)
- 基本API 及 實現(xiàn)
- API
- 初始化流程
- 寫流程
- 讀流程
- 刪除流程
- PMEM Allocator設(shè)計
- 主要組件
- 空間分配流程
- 空間釋放
- 圖數(shù)據(jù)庫 on KVDK 性能
性能數(shù)據(jù)
這個kv 存儲引擎是持久化的存儲引擎,存儲介質(zhì)是PMEM,也就是intel 的傲騰系列持久化內(nèi)存。
先來看一組這個存儲引擎的性能數(shù)據(jù):
以下測試均為單 numa下的性能數(shù)據(jù),整個單機需要乘2
readrandom read:
read ops 73660400, write ops 0writerandom write:
read ops 0, write ops 48483400read50% write 50%:
read ops 62263100, write ops 4447500
可以看到在128B 下的讀 整個單機能到1.4億,寫也接近1億,讀寫混合場景 的讀仍然能保持一億 qps的情況下寫能接近千萬;最重要的是這個性能是極為穩(wěn)定的(波峰波谷抖動5%以內(nèi)),意味著長尾是可控的。
這個數(shù)據(jù)是在 4 * 128G pmem 上跑的, 硬件本身的性能量級大概是 寫帶寬能到 7.5G/s,讀帶寬在512B下能到11.4G/s,在4K 下能到20.4G/s。
為了更直觀得對比性能,將最為通用的rocksdb 引擎 用最優(yōu)的參數(shù)放在pmem上直接跑(128B disable wal):
- 寫性能 上限也就400w/s,且跑一段時間之后一定會write-stall(后臺compaction消耗了太多的帶寬)
- 讀性能 上限也就300w/s
感興趣的同學(xué)可以私信要參數(shù)。
綜合來看 在PMEM 存儲介質(zhì)上的持久化引擎 使用rocksdb 是完全無法發(fā)揮硬件本身的性能,這個kv引擎單純性能數(shù)據(jù)來看,同樣提供持久化存儲能力的情況下,不論是讀還是寫 都超過rocksdb 峰值吞吐10x 以上,且長尾優(yōu)于rocksdb 接近2個量級。
rocksdb on pmem的 瓶頸主要是在:
- rocksdb 的運行需要走內(nèi)核協(xié)議棧(xfs-dax/ext4-dax),這一部分開銷相比于直接通過 pmdk走 libpmem 驅(qū)動 消耗了更多的cpu。
- rocksdb 為了維護(hù)本身的全序能力而引入的后臺線程 compaction 在數(shù)據(jù)流極速增加的情況下消耗了太多的cpu以及io 資源。
項目地址:https://github.com/pmem/kvdk
設(shè)計背景
隨著高性能存儲硬件的極速發(fā)展,傳統(tǒng)的存儲架構(gòu)逐漸無法發(fā)揮新硬件的性能,從而降低了整體的TCO 收益。
對于PMEM這樣的接近內(nèi)存性能的持久化存儲,其必然有著相比于普通nvme-ssd 來說更高的成本,所以如何在成本上升的情況下保持較高的TCO,那傳統(tǒng)引擎的架構(gòu)則需要在PMEM上重新調(diào)整。
同時,對于各個存儲項目來說 選擇 rocksdb 作為自己的 單機k/v 存儲引擎 也是無奈之舉:
- rocksdb 擁有完善且活躍的社區(qū)來持續(xù)推動
- rocksdb 的易用性和可擴展性 能夠支持 調(diào)整不同的workload
但是對于當(dāng)前高速發(fā)展的一些存儲方向來說,rocksdb的部分功能確有一些冗余:
- 非全序需求的存儲場景:像是圖數(shù)據(jù)庫 以及 支持redis協(xié)議的持久化分布式存儲中的hset/hmset等主流命令 其實都不需要全序能力。
- 為sata-ssd設(shè)計的 lsm-tree 存儲引擎 將隨機寫變成順序?qū)?來提升寫性能,這個場景在PMEM 上顯然不存在(底層沒有block粒度的GC,且讀寫之間全雙工,互不影響),順序?qū)懞碗S機寫性能對PMEM來說基本一樣。
所以,我們和Intel 一起共建了 on PMEM 的工業(yè)級持久化 hash 結(jié)構(gòu)的 kv引擎,來提升應(yīng)用在PMEM 上的TCO收益。
設(shè)計架構(gòu)
Hash 索引結(jié)構(gòu) 及 PMEM空間管理形態(tài)
雖然PMEM 能夠支持64B 粒度的存取,但是如果想要hash 索引持久化(防止內(nèi)存放不下的情況)則最后上層用戶的一次讀寫必然會出現(xiàn)對磁盤的多次更新,這對性能來說是一個非常大的損失。
所以設(shè)計上,主體架構(gòu)還是讓整個Hash 索引放在內(nèi)存中,內(nèi)存會保存實際的value on pmem 的偏移。因為pmem 是插在DIMM 插槽上,距離cpu 足夠近,所以從DRAM 讀取value 所在的偏移之后 cpu 再去PMEM 上加載實際的數(shù)據(jù),延時和訪存是一個量級。
同時,索引如果全放在內(nèi)存,如果需要rehash,那整個索引的性能和復(fù)雜度都會較高,所以為了設(shè)計架構(gòu)的簡潔,在初始化的時候直接分配好索引對應(yīng)的內(nèi)存結(jié)構(gòu),實際寫入生成新的索引時再具體分配對應(yīng)的存儲空間(當(dāng)然,也支持直接分配好對應(yīng)的索引空間,這樣寫入的時候性能會更好一些)。
先看看整體的設(shè)計架構(gòu),我們再來描述一下設(shè)計細(xì)節(jié):
DRAM中:
- 預(yù)先分配好的Hash索引部分,按照slot粒度進(jìn)行劃分。每個slot 中包含一個或者多個bucket,整個內(nèi)存中會創(chuàng)建2^27 個bucket。默認(rèn)每個slot 中放置一個bucket,也就是會有2^27 個slot。具體每個slot 最多存放的bucket 數(shù)量是可以配置,bucket數(shù)量足夠多,完全隨機場景下每個k/v 會被均勻打散到不同的bucket下。
同時,每個slot中會放置一個spinklock 來減少當(dāng)前slot內(nèi)部bucket 更新的競爭代價。后續(xù)也可以按照slot粒度來配置一些 cache和bloom filter 來加速讀。 - 每一個bucket 則保存實際存儲 索引到pmem 數(shù)據(jù) 的HashEntry。包括按照key的prefix_len 生成的hash值,當(dāng)前k/v的類型(新的數(shù)據(jù)類型 還是 刪除類型 以及 支持prefix range的skiplit類型),在pmem上的存儲偏移,和當(dāng)前k/v的狀態(tài)信息。每一個hashEntry 定長的,總共16B。
每個bucket 的 HashEntry 會持續(xù)追加到bucket 后面,默認(rèn)一個bucket 最多放置 8個HashEnry,也就是一個bucket大小是128B,也是cacheline 對齊的。
同時,除了Hash 索引部分會放在內(nèi)存中,也會創(chuàng)建一些管理pmem 存儲空間的 Allocator數(shù)據(jù)結(jié)構(gòu),用來管理pmem 空間的分配和釋放,這個待會會細(xì)說。
每個Allocator 會對應(yīng)一個寫線程,初始化Engine 的時候會默認(rèn)啟動48個后臺寫線程(性能最好的一個數(shù)量),每個寫線程專門負(fù)責(zé)請求的寫入處理。
PMEM中:
- pmem 的空間組織是按照block粒度進(jìn)行劃分的,每一個block大小是64B,一個kv可能會分配多個block,默認(rèn)key大小上限是64K,value大小上限是64M。
- block 中包含16B的k/v元數(shù)據(jù),包括8B 的DataHeader ,用來存放當(dāng)前k/v 的checksum和總大小;還有一個 8B 的DataMeta,存放當(dāng)前kv的 timestamp, type 以及 k_size 和 value_size,再之后就是 key和value的實際數(shù)據(jù)了。
基本API 及 實現(xiàn)
API
KVDK 支持 基本的k/v接口 以及 構(gòu)造prefix-range 友好的 skiplist hash接口(索引部分中的bucket 從之前的數(shù)組 變更為跳表),并且會持久化最后一層跳表節(jié)點,設(shè)計上會更復(fù)雜一些,以上架構(gòu)圖中并沒有體現(xiàn)。
基本API如下:
#define FOREACH_ENUM(GEN) \GEN(Ok) \GEN(NotFound) \GEN(MemoryOverflow) \GEN(PmemOverflow) \GEN(NotSupported) \GEN(MapError) \GEN(BatchOverflow)\GEN(TooManyWriteThreads) \GEN(InvalidDataSize) \GEN(IOError) \GEN(InvalidConfiguration) \GEN(Abort)
#define GENERATE_ENUM(ENUM) ENUM,
#define GENERATE_STRING(STRING) #STRING,typedef enum {FOREACH_ENUM(GENERATE_ENUM)
} KVDKStatus;class Engine {
public:// Open a new KVDK instance or restore a existing KVDK instance with the// specified "name". The "name" indicates the dir path that persist the// instance.//// Stores a pointer to the instance in *engine_ptr on success, write logs// during runtime to log_file if it's not null.//// To close the instance, just delete *engine_ptr.static Status Open(const std::string &name, Engine **engine_ptr,const Configs &configs, FILE *log_file = stdout);// Insert a STRING-type KV to set "key" to hold "value", return Ok on// successful persistence, return non-Ok on any error.virtual Status Set(const pmem::obj::string_view key,const pmem::obj::string_view value) = 0;virtual Status BatchWrite(const WriteBatch &write_batch) = 0;// Search the STRING-type KV of "key" and store the corresponding value to// *value on success. If the "key" does not exist, return NotFound.virtual Status Get(const pmem::obj::string_view key, std::string *value) = 0;// Remove STRING-type KV of "key".// Return Ok on success or the "key" did not exist, return non-Ok on any// error.virtual Status Delete(const pmem::obj::string_view key) = 0;...
}
初始化流程
主要是Open 流程:
-
如果配置了 devdax模式,則會做一些devdax的檢查;如果不是,則檢查傳入的pmem 路徑是否是一個合規(guī)的pmem設(shè)備。
devdax 模式 是一種pmem 的namespace模式,這個模式下的pmem namespace空間是一個字符設(shè)備形態(tài),可以直接通過pmdk來訪問而不需要像 fsdax這樣構(gòu)造一個文件系統(tǒng)來進(jìn)行訪問。
相關(guān)介紹可以參考:https://github.com/pmem/kvdk/pull/93 -
在指定的fsdax 設(shè)備路徑 利用 pmem_map_file 映射一個大小的pmem空間 或者 devdax 模式下直接映射 字符設(shè)備形態(tài)的pmem空間。
-
持久化一些 option 配置 以及 創(chuàng)建writebatch 需要的目錄
-
初始化 Pmem Allocator 、管理寫線程的 ThreadManager、以及 內(nèi)存HashTable
-
嘗試 Recovery 已有的 Pmem數(shù)據(jù):
a. 處理之前WriteBatch 未完成的請求。
b. 多線程 以 segment 為粒度 遍歷PMEM 上的 data entry.
c. 根據(jù)checksum 校驗數(shù)據(jù)是否完整
d. 根據(jù)讀到的DataEntry類型來決定如何構(gòu)造內(nèi)存索引(默認(rèn)我們會使用stringDataRecord,還有 SortedDataRecord 和 DlistDataRecord類型)。假如是stringDataRecord,則會先搜索hashtable 查看是否已經(jīng)存在相同的key,并根據(jù)timestamp 判斷哪一個data entry 是最新版本。
e. 將完整的新版本 DataEntry 插入到 HashTable中,將不完整的舊的DataEntry 占用的空間放入到 FreeList中。
f. 重復(fù)以 c-e 步驟,知道各個線程完成自己的 segmemt 數(shù)據(jù)重放 -
啟動后臺定期合并 freelist 空閑塊的線程,來提升pmem空間的利用率
寫流程
主要是Set API的實現(xiàn)。
- 初始化一個寫線程,上限是48個。
MaybeInitWriteThread();。 - 計算當(dāng)前key 的64 bits hash值,按照hash值獲取到該key 所屬的bucket 以及 slot.
KeyHashHint GetHint(const pmem::obj::string_view &key) {KeyHashHint hint;hint.key_hash_value = hash_str(key.data(), key.size());hint.bucket = get_bucket_num(hint.key_hash_value);hint.slot = get_slot_num(hint.bucket);hint.spin = &slots_[hint.slot].spin;return hint;} - 對當(dāng)前slot 進(jìn)行加鎖,保證PMEM 的寫入 以及 對應(yīng)bucket 的更新是原子的。因為bucket 以及 slot數(shù)量足夠多,隨機場景下的鎖沖突概率較低。
- 從該key 對應(yīng)的bucket中搜索是否有已存在的的 hash entry,找到了則標(biāo)記當(dāng)前HashEntry的status為update,并返回HashEntry的地址。沒有找到則標(biāo)記status為initial,返回bucket中最后一個 hashentry的結(jié)束地址 作為當(dāng)前hashEntry的起始地址。
- 向pmem 中寫入 DataEntry。先嘗試從 Segment 末尾追加寫入,如果空間不足,則嘗試用best-fit 算法從freelist中分配空間。如果freelist 中還沒有可用的空閑空間,則直接向PMEM 申請一塊新的segment。寫入數(shù)據(jù)。
- 更新hash Entry。按照第二步查找的結(jié)果進(jìn)行更新,如果是update 標(biāo)識之前已經(jīng)有這個k/v了, 則直接更新value的offset即可。否則,追加到所屬bucket最后一個hashEntry之后。
- 回收Pmem空間。如果索引的 status 是 update,表示舊的pmem空間已經(jīng)不可用了,需要被回收。會將當(dāng)前空間放入到free list中。
寫入過程 帶spinlock 查 hash索引 以及 在 pmem上的 更新,完全隨機場景下spinlock的 沖突極小 且 因為是Hash索引的更新, 接近 O(1) 的時間 以及 訪問延時和內(nèi)存一個量級的落盤 延時 以及 CPU的消耗都會被降到最低(NUMA 架構(gòu)下需要綁定numa才行,不然跨核訪問的延時還是比較高的,這對內(nèi)存來說也是一樣的)。
讀流程
主要是 Get API的實現(xiàn)。
- 計算key 的64 bits hash值,并根據(jù) hash后綴得到所屬的bucket和slot.
- 從 bucket 中查找該key 的 HashEntry,找到,則拿著offset 從 pmem上讀取對應(yīng)的 DataEntry;否則返回 NotFound。
- Double check 讀到的數(shù)據(jù)是否正確( entry-type, checksum的校驗)。
需要注意的是Get 操作是無需對 slot加鎖的,因為讀 bucket內(nèi)部的 HashEntry時 不論是讀 HashHeader 還是 offset 都是8B字節(jié),對內(nèi)存來說都是可以原子訪問的,這也是將 HashEntry 設(shè)計為 8B HashHeader 以及 8B offset 的原因。
刪除流程
刪除操作和 LSM-tree 類似,也是寫入一個 delete record。
需要注意的是這個Delete record的處理 和普通的 data record的處理有一些差異。因為Delete record 的空間被回收,但是PMEM上還存在更老版本的data entry,那么在recovery 也就是open 的過程中會 讀到這個更老的版本,這樣就存在數(shù)據(jù)不一致的情況。
所以,針對Delete record 的數(shù)據(jù)不會立即回收,而是保留其 delete record 以及 內(nèi)存中的 HashEntry,當(dāng)再次有該key 的更新時會直接將 delete record的 空間加入到free list中。否則,為了防止 recovery時數(shù)據(jù)不一致的情況,當(dāng)且僅當(dāng)該key 的所有老版本 data entry 均被復(fù)用之后才能復(fù)用 delete record 及其 HashEntry占用的空間。
PMEM Allocator設(shè)計
前面頻繁提到 freelist 以及 pmem alloctor 的空間管理,這一部分時除了 Hash 以及 Sorted 索引 之外 內(nèi)存中最重要的一個組件了,其分配空間的高效性 以及 空間利用率 直接關(guān)系到這個存儲引擎的性能 以及 易用性。
貼一張已有多的設(shè)計架構(gòu):
這里的設(shè)計架構(gòu)和 tcmalloc 比較接近TCMalloc 實現(xiàn)原理,能稍微簡單一些,當(dāng)然,也有一些功能還不夠全面(內(nèi)部stats展示什么的)。
總體上就是:
- 維護(hù)了 thread-cache 和 thread-freelist來緩存一部分存儲空間,因為更靠近cpu,所以空間的分配和釋放都是非常方便的。
- 還有一個共享的內(nèi)存池,功能類似于tcmalloc 的 transfercache 以及 central-freelist,用來為thread-cache 提供空間分配 以及 空閑free-list 的合并,來提升pmem空間的利用率。
- 同樣,為了保證空閑空間的合并效率以及大空間的分配效率,不論是在thread-cache 還是在 共享的內(nèi)存中都做了按 size區(qū)分,類似tcmalloc 的size-class,這樣同一range 大小的空間管理都在一套數(shù)據(jù)結(jié)構(gòu)之中,對性能和空間利用率都比較友好。
主要組件
- PMEM Space: 從PMEM map出的一塊空間,分為若干segment,每個segment又分成若干blocks,block是allocator的最小分配單元
- Thread caches:為前臺線程cache一些可用空間,避免線程競爭。包括一段segment和一個free list。Free list管理被釋放的空閑空間,是一個鏈表的數(shù)組,每個鏈表結(jié)點是一個指向空閑空間的指針,鏈表的數(shù)組下標(biāo)表示結(jié)點指針指向的空閑空間的大小,即包含多少個block。
- Pool:為了均衡各thread cache的資源,由一個后臺線程周期地將thread cache中的free list以及segment移動到后臺的pool中,pool中的資源由所有前臺線程共享。后臺線程還會周期性地將pool中的相鄰碎片空間合并為大塊空間“Merged Space”。
空間分配流程
- 線程查看cached PMem space是否有足夠空間,若無,則嘗試從pool中fetch一塊merged space作為新的cache,然后從cached PMem space尾部分配空間
- 若pool中無可用Merged space,則嘗試從free list中分配空間。首先查看cache中的free list,若無可用空間,則從pool中拿取另一段free list。
- 若從free list分配空間仍然失敗,則從PMEM Space中fetch一段新的segment
空間釋放
就是將將free的空間指針加入 thread-cache中 對應(yīng)大小的free list鏈表
圖數(shù)據(jù)庫 on KVDK 性能
KVDK 模擬了簡單的graph workload 圖 workload on kvdk 性能 PR。
圖數(shù)據(jù)庫 在現(xiàn)有的互聯(lián)網(wǎng)生態(tài)下還是有較大的發(fā)展前景,無數(shù)的個人終端(手機/PC/Paid)都會作為互聯(lián)網(wǎng)中的一個節(jié)點,各個APP/互聯(lián)網(wǎng)應(yīng)用 希望能夠利用這一些節(jié)點 以及他們的行為作足夠深入的研究和分析,來生產(chǎn)一些利于他們 也 利于自己的流量或者產(chǎn)品。這個過程就頻繁得探索不同節(jié)點之間的關(guān)系,在以億為頂點的單位 以 萬億 為頂點屬性的單位 構(gòu)成的超大規(guī)模網(wǎng)絡(luò)中,利用傳統(tǒng)關(guān)系型數(shù)據(jù)庫來做數(shù)據(jù)分析,探索頂點之間的關(guān)系類型,產(chǎn)生的頻繁的join 操作性能必然不會很好。
所以,nebula, dGraph, TigerGraph,UDB 等這樣的 圖數(shù)據(jù)庫 才會出現(xiàn),超大規(guī)模的社交網(wǎng)絡(luò)數(shù)據(jù) 對任何企業(yè)都是財富,而利用這一些財富快速創(chuàng)造出更多的財富 才是 互聯(lián)網(wǎng)企業(yè)在當(dāng)今內(nèi)卷的社會 立足的根本方法。
圖數(shù)據(jù)庫因為其本身存儲的就是 關(guān)系類型的數(shù)據(jù),不過不是按照表形態(tài),而是點邊形態(tài)。
所以,在KVDK 下模擬了圖數(shù)據(jù)庫多的基本形態(tài),來展示KVDK 在圖存儲場景下的極致性能(沒有LDBC 標(biāo)準(zhǔn),畢竟不是專業(yè)的生產(chǎn)圖數(shù)據(jù)庫)。
包括以下基本特性:
-
基本的點和邊的構(gòu)造過程(圖數(shù)據(jù)的加載)。
圖數(shù)據(jù)庫的存儲形態(tài)是 將無數(shù)的點+邊 構(gòu)成的圖網(wǎng)絡(luò)轉(zhuǎn)化為能被存儲引擎識別到的k/v 形態(tài)。
這里是在快手場景的圖存儲編碼。
a. 對于一個頂點來說,將一個頂點編碼為k/v 的過程 是 key: 頂點的id, value : 頂點的屬性。比如:快手/抖音 的一個用戶作為頂點,會為這個用戶生成一個唯一標(biāo)識,同時這個用戶的個人信息/喜好等等 都會作為info中的一種存儲下來。
b. 邊 包含兩個頂點,這個時候社交網(wǎng)絡(luò) 或者 電商推薦系統(tǒng) 應(yīng)該都是有向圖,則會將該邊 編碼為兩個k/v。其中一個存儲 key: src --> dst的關(guān)系,另一個kv 存儲 key: dst <-- src的關(guān)系。 也就是 key : src_vertex(整個頂點編碼) 或者 dst_vertex; value : 上圖中的edge 編碼 之后的數(shù)據(jù)。
c. 邊列表 和邊的存儲一樣,只不過value 就是 邊列表,一個頂點有多個出邊,則這一些邊都會編碼為一個value,作為這個頂點的value。 -
基本的圖算法
這一些 圖算法 是需要跑在 點邊構(gòu)成的圖網(wǎng)絡(luò)中的。包括基本的 TopN(擁有關(guān)注人數(shù)最多的前十個大V),廣度優(yōu)先遍歷 N 度好友關(guān)系等。這一些算法的性能 直接關(guān)系到從圖數(shù)據(jù)庫中提煉 用戶特征的性能,更直接一些的話就是 能分析出當(dāng)前APP 用戶的喜好甚至 未來可能的喜好(深度學(xué)習(xí)),從而直接創(chuàng)造收益。 -
支持不同引擎 的性能對比(memory/rocksdb/leveldb/kvdk)
因為圖場景下沒有辦法用qps 直接對比性能,所以測試的過程就是使用不同的存儲引擎 跑在相同的硬件環(huán)境下(相同的CPU,內(nèi)存,PMEM) 用相同的 workload ,看總共的運行時間,當(dāng)然每一種workload 會跑多輪,結(jié)果取平均時間。
主要對比持久化存儲性能,rocksdb和kvdk。因為leveldb 很多優(yōu)化沒有rocksdb細(xì)致,這里就沒有進(jìn)行對比了。
最終的測試結(jié)果很清晰:
- 構(gòu)造圖數(shù)據(jù)的場景(頻繁的寫入和讀取),kvdk 的性能優(yōu)于rocksdb 接近20被
- 廣度優(yōu)先 ,查找4度好友關(guān)系(查找A 關(guān)注的好友算第一度,查找A關(guān)注的好友關(guān)注的好友 算第二度。。。),大量的從存儲引擎上的讀,kvdk 也優(yōu)于rocksdb 8倍。
- TopN 其實涉及大量的CPU計算,先從存儲引擎讀數(shù)據(jù),再利用 容量有限的最小堆 進(jìn)行計算。這里kvdk 性能優(yōu)于rocksdb 8倍。
更詳細(xì)的測試方式可以在這個PR中嘗試: https://github.com/pmem/kvdk/pull/118。
顯然,在新型存儲介質(zhì)下,KVDK 的性能相比于 rocksdb 擁有顯著的優(yōu)勢。
當(dāng)然,想要在 通用存儲引擎的道路上走的更遠(yuǎn),還需要更多的功能才能作為一個生產(chǎn)級別的存儲引擎 – 和rocksdb 功能對標(biāo)(單機事務(wù)能力,備份,測試/運維系統(tǒng))。
本文在 Hash 索引部分的介紹僅僅介紹了基本的Hash結(jié)構(gòu),因為 Hash 結(jié)構(gòu)對 scan 性能并不友好,所以kvdk 還提供了 高性能的prefix-scan 的能力,索引部分的bucket 變更成為了跳表,并且會持久化最后一層跳表結(jié)構(gòu),高層指針仍然保存在DRAM 中。
歡迎感興趣的同學(xué)試用 討論 https://github.com/pmem/kvdk。
總結(jié)
以上是生活随笔為你收集整理的基于持久内存的 单机上亿(128B)QPS -- 持久化 k/v 存储引擎的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: “绿冻杨枝折”上一句是什么
- 下一篇: Rocksdb的事务(二):完整事务体系