pmemkv
在云环境中,平台通常是虚拟化的,且应用程序高度抽象化,以避免对底层硬件细节作出显式假定。问题是:如果物理设备仅与特定服务器保持本地连接,那么如何在云原生环境中简化持久内存编程呢?
其中一个解决方法是键值(KV)存储。这种数据存储方式旨在用简单易用的 API 存储、检索和管理相关数据。市面上有许多键值数据存储解决方案。它们的特性各不相同,其 API 也是针对不同的用例而设计,但它们的核心 API 保持不变,它们全都可以提供 put、get、remove、exists、open、close 等方法。KV引擎的底层数据结构决定了 key 的组织方式,或 value 的索引方式,这些数据结构将直接影响到 KV 引擎在不同工作负载下的性能,如点查询( point query) 较多时,hash-based index 会比较合适,而范围查询( range search) 更适合采用 b+tree, skiplist 等数据结构。
如图 4所示(165行),pmemkv实现基于持久内存的本地 KV 引擎,此时如何管理 value 将成为影响性能的关键。为此pmemkv采取了以下设计:可插拔的引擎,有些引擎是pmemkv实现的,有的是从外部的项目移植过来的。持久化的引擎基于libpemobj/libpmemobj++实现;非持久化的引擎是基于memkind,而并发是基于intel TBB。pmemkv的核心是基于c++实现的,其不涉及持久内存。pmemkv 的原生 API 是 C 和 C++。还提供其他编程语言绑定,如 JavaScript、Java, 和 Ruby,也可以轻松添加其他语言。pmemkv基于各种数据结构,已经实现了非常多的kv引擎,我们非常希望大家可以贡献自己的引擎,或者使用我们的pmemkv作为你kv引擎的参考设计。 现有的引擎有这么几种类型和分类:
1.persistent 和 volatile (持久和易失),持久化引擎可以快速恢复数据,volatile性能会更好。
2.排序或者不排序, 排序可以时候range scan而不排序适合点查。
3.并发或者单线程。
从支持的数据结构来看,我们有concurrent hashmap/ sorted hashmap/ b+树/concurrent skiplist/radix等等。你可以直接使用pmemkv,也可以获得灵感来做为您的kv引擎的参考。 我们非常希望收到客户的使用反馈帮助我们进一步去支持更多的数据结构,更多的kv设计。
pmemkv支持的引擎有很多,我们选择cmap来将持久内存作为SSD持久缓存的,其中key就是req_id,而value就是一个4K的page。我们可以观察单线程下,使用cmap的性能。示例 4 pmemkv实现持久化页缓存使用的核心接口如下:
1.pmemkv_config *cfg = pmemkv_config_new();
2.pmemkv_config_put_string(cfg, “path”, path)
3.pmemkv_config_put_uint64(cfg, “force_create”, fcreate)
4.pmemkv_config_put_uint64(cfg, “size”, size)
5.pmemkv_open(“cmap”, cfg, &db);
6.pmemkv_put(db, str, strlen(str), (const char *)content, PAGE_SIZE);
7.pmemkv_get(db, str, strlen(str),pmemkv_get_value, &val);
示例 4 pmemkv实现持久化页缓存
#include <iostream>
#include <chrono>
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <libpmemkv.h>
#include <string>
#include <cassert>
#define PAGE_SIZE 4096
#define PMEM_SIZE 100*1024*1024*1024UL
pmemkv_db *db=NULL;
pmemkv_config* config_setup(const char* path, const uint64_t fcreate, const uint64_t size)
{
pmemkv_config *cfg = pmemkv_config_new();
assert(cfg != nullptr);
if (pmemkv_config_put_string(cfg, "path", path) != PMEMKV_STATUS_OK) {
fprintf(stderr, "%s", pmemkv_errormsg());
return NULL;
}
if (pmemkv_config_put_uint64(cfg, "force_create", fcreate) != PMEMKV_STATUS_OK) {
fprintf(stderr, "%s", pmemkv_errormsg());
return NULL;
}
if (pmemkv_config_put_uint64(cfg, "size", size) != PMEMKV_STATUS_OK) {
fprintf(stderr, "%s", pmemkv_errormsg());
return NULL;
}
return cfg;
}
//初始化,传入的是文件的名称,文件名称也可以定义在头文件中间。
int cbs_init(const char * filename)
{
int s;
pmemkv_config *cfg=NULL;
//这个地方关于force_create的行为不是很符合客户的使用习惯,我们将重新定义create_if_missing 这个option, 详细的请参考https://github.com/pmem/pmemkv/issues/576
if(access(filename,F_OK)==0) {
cfg = config_setup(filename, 0,PMEM_SIZE);
} else {
cfg = config_setup(filename, 1,PMEM_SIZE);
}
assert(cfg != NULL);
//cmap open with the cfg.
s = pmemkv_open("cmap", cfg, &db);
assert(s == PMEMKV_STATUS_OK);
assert(db != NULL);
return 0;
}
//写入或者更新一个页,输入req_id表示更新到什么地方,content表示更新的内容
int write_req(uint64_t req_id, unsigned char * content) {
int s;
char str[20];
//itoa(req_id,string, 10);
sprintf(str,"%ld",req_id);
s = pmemkv_put(db, str, strlen(str), (const char *)content, PAGE_SIZE);
assert(s==PMEMKV_STATUS_OK);
return 0;
}
size_t get_key_count()
{
size_t cnt;
int s;
s=pmemkv_count_all(db,&cnt);
assert(s == PMEMKV_STATUS_OK);
return cnt;
}
void pmemkv_get_value(const char *value, size_t valuebytes, void *val) {
//printf("0x%x\n",value[0]);
//printf("%ld\n",valuebytes);
//*(unsigned char **) val=(unsigned char *)value;
//call back function mainly for atomic? that means out of the call back function, you might not get the real value content or might changed by other thread?
memcpy((unsigned char *)val,value,valuebytes);
return;
}
unsigned char * read_req(uint64_t req_id,unsigned char * val ) {
int s;
char str[20];
sprintf(str,"%ld",req_id);
//val is a heap with 4096.
s = pmemkv_get(db, str, strlen(str),pmemkv_get_value, val);
assert(s == PMEMKV_STATUS_OK);
return val;
}
void delete_page(uint64_t req_id) {
return;
}
#define WRITE_COUNT 100000
#define OVERWRITE_COUNT 10000
int main()
{
// calculate the time
unsigned char * page_content=(unsigned char *)malloc(4096);
uint64_t i=0;
auto start=std::chrono::steady_clock::now();
auto stop=std::chrono::steady_clock::now();
std::chrono::duration<double> diff=stop-start;
unsigned char * read_content;
memset(page_content,0xab,4096);
start=std::chrono::steady_clock::now();
cbs_init("/mnt/pmem0/cbs_pmemkv");
stop=std::chrono::steady_clock::now();
diff=stop-start;
std::cout<<"cbs_init time"<<diff.count()<<std::endl;
size_t cnt=get_key_count();
std::cout<<"kv count"<<cnt<<std::endl;
start = std::chrono::steady_clock::now();
for(i=0;i<WRITE_COUNT;i++) {
write_req(i,page_content);
}
stop=std::chrono::steady_clock::now();
diff=stop-start;
std::cout<<"write_req time"<<diff.count()/WRITE_COUNT<<std::endl;
memset(page_content,0xcd,4096);
start = std::chrono::steady_clock::now();
for(i=0;i<OVERWRITE_COUNT;i++) {
write_req(i,page_content);
}
stop=std::chrono::steady_clock::now();
diff=stop-start;
std::cout<<"overwrite write_req update take time "<< diff.count()/OVERWRITE_COUNT<<std::endl;
start = std::chrono::steady_clock::now();
for(i=0;i<OVERWRITE_COUNT;i++) {
read_content=read_req(i,page_content);
}
stop=std::chrono::steady_clock::now();
diff=stop-start;
std::cout<<"overwrite read_req take time "<<diff.count()/OVERWRITE_COUNT<<std::endl;
printf("the page should fill with paten 0xcd, 0x%x\n", page_content[0]);
start = std::chrono::steady_clock::now();
for(i=OVERWRITE_COUNT;i<WRITE_COUNT;i++) {
read_content=read_req(i,page_content);
}
stop=std::chrono::steady_clock::now();
diff=stop-start;
std::cout<<"overwrite->write count read_req take time "<<diff.count()/(WRITE_COUNT-OVERWRITE_COUNT)<<std::endl;
printf("the page should fill with patern 0xab, 0x%x\n", page_content[0]);
free(page_content);
//start = std::chrono::steady_clock::now();
//for(i=0;i<WRITE_COUNT;i++) {
// delete_page(i);
//}
//stop=std::chrono::steady_clock::now();
//diff=stop-start;
//std::cout<<"delete write count take time "<<diff.count()/WRITE_COUNT<<std::endl;
return 0;
}
编译”g++ cbs_req_pmemkv.cpp -o cbs_req_pmemkv -lpmemkv -O2”,然后使用“taskset -c 2 ./cbs_req_pmemkv运行这段代码,我们看到第一次写4KB页大概消耗45us,第二次大概消耗10us,恢复的过程大概消耗0.07s;可以使用“pmempool info -O /mnt/pmem0/cbs_pmemkv|more”来观察对象空间的大小。
➜ ~ taskset -c 2 ./cbs_req_pmemkv
cbs_init time0.104486
kv count0
write_req time 4.53356e-05
overwrite write_req update take time 1.2625e-05
overwrite read_req take time 2.17473e-06
the page should fill with paten 0xcd, 0xcd
overwrite->write count read_req take time 3.36632e-06
the page should fill with patern 0xab, 0xab
➜ ~ taskset -c 2 ./cbs_req_pmemkv
cbs_init time0.0789432
kv count100000
write_req time 9.77862e-06
overwrite write_req update take time 9.67322e-06
overwrite read_req take time 2.1298e-06
the page should fill with paten 0xcd, 0xcd
overwrite->write count read_req take time 2.09206e-06
the page should fill with patern 0xab, 0xab
Call For Action:
1.https://github.com/pmem/pmemkv/tree/master/examples
2.https://pmem.io/pmemkv/
3.自己开发一个简单的KV引擎集成到pmemkv这个框架中。
Tips:
1.pmemkv仅关注pmem的键值存储接口。应用程序级别的性能主要取决于pmemkv的算法。有很多优化机会。
2.pmemkv当前仅使用PMEM,DRAM没有完全使用,这可能会导致性能不够好。正在进行一些优化工作。
3.借助libpmemobj支持,在pmemkv上的恢复性能非常好。某些引擎基于libpmemobj,因此它具有与libpmemobj相同的局限性。
关注英特尔边缘计算社区,表示您确认您已年满 18 岁,并同意向英特尔分享个人信息,以便通过电子邮件和电话随时了解最新英特尔技术和行业趋势。您可以随时取消订阅。英特尔网站和通信内容遵守我们的隐私声明和使用条款。