9 pmemkv: 持久内存KV库
持久内存编程不容易。前面几章我们描述了使用持久内存的应用必须负责操作的原子性和数据结构的一致性。PMDK库,像libpmemobj,在设计时时刻考虑着灵活性和简单性。通常,这是一对儿矛盾的需求,为了一个会牺牲另一个。事实上大多数场景,API的灵活性会增加复杂性。
在当前云计算生态系统中,对数据有着不可预料的需求。消费者期望web services提供可预测的低延迟的可靠的数据。持久内存字节寻址能力和大容量特性很好的契合了云环境。今天,带有高级智能的大量设备通过各式各样的网络联接起来,商家和消费者都正在寻找不断增加吸引力的特性:可以更快、在任何地方访问他们的数据。终端设备上存储容量会越来越少,这对于使用云的消费者很好。到2020年,IDC预测,相比较消费者的终端设备,会有更多内容存储到公有云上。见图(Figure 9-1)。
云生态的模块化和服务模式的多样化定义了我们熟知的编程和应用开发。我们称之为云原生计算,其结果是产生了很多流行的大量高级语言、框架和抽象层。Figure 9-2展示了GitHub上基于pull requests开发合作模式的15种最流行的语言。
在云环境下,平台通常是虚拟化的,应用被重度抽象不能直接获得低层硬件的细节。问题是如何使得持久内存编程更容易,在给定物理设备云原生环境仅从本地到指定Server?答案之一是kv存储。设计用于带有直接API的存储、检索、管理关联数组的数据存储典型范例能够非常容易地利用持久内存的优势。这就是为什么创建pmemkv的原因。
9.1 pmemkv 架构
市场上有许多可用的kv数据库。他们有不同的特性和licenses要求,并且他们的API面向不同的场景。然而,他们的核心API仍然相同。他们都提供类似put、get、remove、exists、open,以及close方法。我们出版这本书时,最流行的kv数据库是Redis。开源版本见:https://redis.io/,企业版本见:https://redislabs.com。
DB-Engines (https://db-engines.com) 显示出Redis比其他竞品有明显的更高分数。
Pmemkv作为一个独立的工程被创建,不仅是PMDK系列库对云原生支持的补充,而且提供一个为持久内存构建的KV API。为pmemkv开发者的主要目标之一是为开源社区创建友好的环境,开发有PMDK帮助的新引擎并集成到其他编程语言。Pmemkv使用与PMDK相同的BSD 3-Clause permissive license。pmemkv原生API是C和C++。支持其他编程语言联编,如:JavaScript、Java和Ruby。其他语言也可以很容易的增加支持。
Pmemkv API与大多数kv库类似。几个存储引擎灵活性和功能性都可用。每个引擎有不同的性能并且用于解决不同的问题。因此,每个引擎提供的功能也有所不同。特性描述如下:
• 持久性: 持久引擎保障修改不丢失并且断电安全,而易失性的只保留内容在应用生命周期;
• 并发性: 并发引擎保障一些方法(如:get()/put()/remove() )是线程安全的;
• Keys排序: 排序引擎提供范围查询,像get_above()。
Pmemkv与其他kv的不同是它直接访问数据。这意味着从持久内存读取数据不需要copy到内存DRAM。这已经在第1章中提到,并在Figure 9-5再次展示。
直接访问数据将大大提高应用的速度。当程序只关注存储在数据库中的数据时更为显著。传统的方法,需要copy整个数据到某buffer,然后返回给应用。使用pmemkv,我们提供给应用一个直接可访问的指针,应用只在需要时读取。
为了使API适应各种引擎类型,产生了灵活的pmemkv_config配置结构。它存储引擎配置选项并且允许你调整它的行为。每个引擎都有文档描述其支持配置参数。Pmemkv库以引擎插件化和扩展性方式来设计的,以支持开发者自己的需求。开发者可以自由修改存在的引擎或者贡献一个新的引擎。https://github.com/pmem/pmemkv/blob/master/CONTRIBUTING.md#engines
清单Listing 9-1,展示了使用C API来进行pmemkv_config结构的基本设置。所有设置代码包装成了一个自定义的函数 config_setup(),它会在下一节phonebook例子中用到。你可以看到在pmemkv 如何处理错误—所有方法,除了pmemkv_close() pmemkv_errormsg(),都会返回一个状态。我们使用pmemkv_errormsg()函数来获得错误信息。返回值的完整列表参见pmemkv手册页。
Listing 9-1. pmemkv_config.h – An example of the pmemkv_config structure using the C API
1 #include <cstdio>
2 #include <cassert>
3 #include <libpmemkv.h>
4
5 pmemkv_config* config_setup(const char* path, const uint64_t fcreate, const uint64_t size) {
6 pmemkv_config *cfg = pmemkv_config_new();
7 assert(cfg != nullptr);
8
9 if (pmemkv_config_put_string(cfg, "path", path) != PMEMKV_STATUS_OK) {
10 fprintf(stderr, "%s", pmemkv_errormsg());
11 return NULL;
12 }
13
14 if (pmemkv_config_put_uint64(cfg, "force_create", fcreate) != PMEMKV_STATUS_OK) {
15 fprintf(stderr, "%s", pmemkv_errormsg());
16 return NULL;
17 }
18
19 if (pmemkv_config_put_uint64(cfg, "size", size) != PMEMKV_STATUS_OK) {
20 fprintf(stderr, "%s", pmemkv_errormsg());
21 return NULL;
22 }
23
24 return cfg;
25 }
- 第5行: 我们定义自己函数用于准备配置并设置将要使用的引擎的所有需要的参数;
- 第6行: 创建C config 类的实例,如果失败返回空指针nullptr;
- 第9-22行: 所有参数一个接一个放到配置中,每个参数的放入都使用该类参数专用的函数,每个都检查是否存储成功,没有错误发生时返回PMEMKV_STATUS_OK ,表示成功。
9.2 电话薄例子
清单Listing 9-2使用pmemkv C++ API展示了一个简单的电话薄例子。Pmemkv的主要意图之一是提供一个类似其他KV库的API。这会使它非常易懂、易用。我们重用来自清单Listing 9-1的config_setup()函数。
Listing 9-2. A simple phonebook example using the pmemkv C++ API
37 #include <iostream>
38 #include <cassert>
39 #include <libpmemkv.hpp>
40 #include <string>
41 #include "pmemkv_config.h"
42
43 using namespace pmem::kv;
44
45 auto PATH = "/daxfs/kvfile";
46 const uint64_t FORCE_CREATE = 1;
47 const uint64_t SIZE = 1024 ∗ 1024 ∗ 1024; // 1 Gig
48
49 int main() {
50 // Prepare config for pmemkv database
51 pmemkv_config ∗cfg = config_setup(PATH, FORCE_CREATE, SIZE);
52 assert(cfg != nullptr);
53
54 // Create a key-value store using the "cmap" engine.
55 db kv;
56
57 if (kv.open("cmap", config(cfg)) != status::OK) {
58 std::cerr << db::errormsg() << std::endl;
59 return 1;
60 }
61
62 // Add 2 entries with name and phone number
63 if (kv.put("John", "123-456-789") != status::OK) {
64 std::cerr << db::errormsg() << std::endl;
65 return 1;
66 }
67 if (kv.put("Kate", "987-654-321") != status::OK) {
68 std::cerr << db::errormsg() << std::endl;
69 return 1;
70 }
71
72 // Count elements
73 size_t cnt;
74 if (kv.count_all(cnt) != status::OK) {
75 std::cerr << db::errormsg() << std::endl;
76 return 1;
77 }
78 assert(cnt == 2);
79
80 // Read key back
81 std::string number;
82 if (kv.get("John", &number) != status::OK) {
83 std::cerr << db::errormsg() << std::endl;
84 return 1;
85 }
86 assert(number == "123-456-789");
87
88 // Iterate through the phonebook
89 if (kv.get_all([](string_view name, string_view number) {
90 std::cout << "name: " << name.data() <<
91 ", number: " << number.data() << std::endl;
92 return 0;
93 }) != status::OK) {
94 std::cerr << db::errormsg() << std::endl;
95 return 1;
96 }
97
98 // Remove one record
99 if (kv.remove("John") != status::OK) {
100 std::cerr << db::errormsg() << std::endl;
101 return 1;
102 }
103
104 // Look for removed record
105 assert(kv.exists("John") == status::NOT_FOUND);
106
107 // Try to use one of methods of ordered engines
108 assert(kv.get_above("John", [](string_view key, string_view value) {
109 std::cout << "This callback should never be called" << std::endl;
110 return 1;
111 }) == status::NOT_SUPPORTED);
112
113 // Close database (optional)
114 kv.close();
115
116 return 0;
117 }
- 第51行: 通过调用config_ setup() 设置pmemkv_config 结构;
- 第55行: 创建pmem::kv::db 的易失性对象实例(也就是内存中),提供管理持久数据库的接口;
- 第57行: 使用配置参数打开后台为cmap引擎的KV数据库。Cmap引擎是持久并发hash map 引擎,用libpmemobj-cpp 实现的。在第13章有对cmap引擎内部算法和数据结构的更多描述;
- 第58行: pmem::kv::db 提供static errormsg() 方法扩展了错误消息。此例中,我们使用errormsg() 作为处理错误消息的一部分;
- 第63 和67行: put()函数插入一个k-v对到数据库。该函数所有引擎都要确保实现。此例中,我们插入两对k-v到数据库并与status::OK比较返回状态是否OK。这是推荐的方法来判断函数是否成功;
- 第74行: count_all()函数只有一个参数size_t,该函数返回存储在数据库中的元素数量到参数变量cnt中;
- 第82行: get() 函数返回key为“John” 的值。该值copy到用户提供的number变量中。成功返回status::OK,失败返回错误。该函数被所有引擎实现;
- 第86行: 此例中 ,key “John” 的值期望是 “123-456-789”,如果不是抛出断言错误;
- 第89行: 此例中使用的get_all() 函数让应用直接、只读访问数据。Key和value 变量都引用到存储在持久内存中的数据。此例中,我们简单的打印名字和访问到的每一对的数量;
- 第99行:通过调用remove()从数据库移除 “John”和他的电话号码,该函数被所有引擎实现;
- 第105行: 移除 “John, 123-456-789”后,确认是否还存在在数据库中。exists() 检测所给key是否存在。如果存在,返回 status::OK,否则返回status::NOT_FOUND ;
- 第108行: 不是每个引擎都提供所有可用API方法的实现。此例中,我们使用cmap引擎,就是一个无序引擎。这就是为什么cmap不支持get_above() 函数,以及get_below(), get_between(), count_above(), count_below(), count_between()等,调用这些函数会返回status::NOT_SUPPORTED;
- 第114行: 最后,我们调用close() 方法关闭数据库。调用此方法是可选择的,也就说可以不调用,因为kv分配到栈上,并且所有需要的析构方法将自动被调用,就和存放在栈上的其他变量一样。
9.3 持久内存上云
我们将使用JavaScript语言联编重写电话薄例子。pmemkv 可用的几种语言联编有: JavaScript,、Java、Ruby和 Python。然而不是所有的相同功能都等价于原生C 和C++的对应部分。清单Listing 9-3展示了电话薄使用JavaScript语言联编写的应用。
Listing 9-3. A simple phonebook example written using the JavaScript bindings for pmemkv v0.8
1 const Database = require('./lib/all');
2
3 function assert(condition) {
4 if (!condition) throw new Error('Assert failed');
5 }
6
7 console.log('Create a key-value store using the "cmap" engine');
8 const db = new Database('cmap', '{"path":"/daxfs/ kvfile","size":1073741824, "force_create":1}');
9
10 console.log('Add 2 entries with name and phone number');
11 db.put('John', '123-456-789');
12 db.put('Kate', '987-654-321');
13
14 console.log('Count elements');
15 assert(db.count_all == 2);
16
17 console.log('Read key back');
18 assert(db.get('John') === '123-456-789');
19
20 console.log('Iterate through the phonebook');
21 db.get_all((k, v) => console.log(` name: ${k}, number: ${v}`));
22
23 console.log('Remove one record');
24 db.remove('John');
25
26 console.log('Lookup of removed record');
27 assert(!db.exists('John'));
28
29 console.log('Stopping engine');
30 db.stop();
高层pmemkv语言联编的目的是使持久内存编程更容易并且为云软件开发者提供便捷的工具。
9.4 本章小结
在本章中,我们已经展示了一个熟悉的KV数据库对于更广泛的云软件开发人员受众来说是如何使用持久内存并直接访问数据的简单方法。模块化设计、灵活的引擎API以及与许多最流行的云编程语言的集成使得pmemkv成为云本地软件开发人员的直观选择。作为一个开源的轻量级库,它可以很容易地集成到现有的应用程序中,以便立即开始利用持久内存。
一些pmemkv存储引擎使用第8章描述的libpmemobj-cpp实现。这些引擎实现为开发者理解如何在应用中使用PMDK(及其相关库)提供了真实世界的例子。