技术与生活 性能之重 持久内存编程指南—乱译连载 (10.持久内存的易失性使用)

持久内存编程指南—乱译连载 (10.持久内存的易失性使用)

本章展示了持久性内存的大容量是如何用来保存易失性应用程序数据的。应用程序可以选择从DRAM或持久内存或两者中分配和访问数据。memkind是一个非常灵活和易于使用的库,其语义类似于开发人员经常使用的libc的 malloc/free API。libvmemcache是一个可嵌入的轻量级内存缓存解决方案,它允许应用程序以可伸缩的方式有效地使用持久内存的大容量。libvmemcache是GitHub上的一个开源项目,位于https://github.com/pmem/vmemcache。

10  持久内存的易失性使用

10.1    简介

本章讨论需要大量易失性内存的应用程序,如何利用大容量持久内存作为动态随机存取内存(DRAM)的补充。

使用大型数据集的应用程序,如内存数据库、缓存系统和科学模拟,通常受到系统中可用的易失性内存容量或加载完整数据集所需的DRAM成本的限制。持久内存提供了一个高容量的内存层来解决这些内存不足的应用程序问题。

在存储器存储层次结构(如第1章所述)中,数据被存储在各层中,频繁访问的数据被放置在DRAM中以进行低延迟访问,较少访问的数据被放置在更大容量、更高延迟的存储设备中。这种解决方案的例子包括在Flash上的Redis(https://redislabs.com/redis-enterprise/ technology/redis-on-flash/)和Memcached的Extstore(https://memcached.org/ blog/extstore-cloud/)。

对于不需要持久性的内存密集型应用程序,使用更大容量的持久性内存作为易失性内存提供了新的机会和解决方案。

使用持久内存作为易失性内存解决方案是有利的,当应用:

  1. 控制DRAM和系统内其他存储层之间的数据排布;
  2. 不需要持久化数据;
  3. 可以使用持久性内存的本机延迟,它比DRAM慢,但比(NVMe)固态驱动器(SSD)快。

10.2  背景

应用程序管理不同类型的数据结构,如用户数据、键值存储、元数据和工作缓冲区。设计使用内存和存储的分层解决方案可以提高应用程序性能,例如,在DRAM中放置频繁访问且需要低延迟访问的对象;在持久内存上存储需要分配更大尺寸的对象,而这些对象在持久内存上不具有延迟敏感度。传统的存储设备则被用来提供持久性。

10.2.1 内存分配

如第1章至第3章所述,持久性内存通过持久性内存感知文件系统(提供对应用程序的直接访问)上的内存映射文件向应用程序公开。由于malloc() 和free() 不能同时在内存和内存映射文件上进行操作,因此需要一个接口为多种内存类型提供malloc() 和free() 语义。这个接口被实现为memkind库(http://memkind.github.io/memkind/)。

10.2.2 memkind如何工作

Memkind库是建立在jemalloc之上的用户可扩展堆管理器,它支持在多种内存之间对堆进行分区。在引入高带宽存储器(HBM)时,Memkind被用来支持不同类型的存储器。此处引入了一种PMEM类型来支持持久内存。

不同“类型”内存由施加到虚拟地址范围的操作系统内存策略定义。Memkind支持的内存特性不包括用户扩展,如:对非一致内存访问(NUMA)的控制和页面大小。图10-1显示了libmemkind组件和硬件支持的轮廓。

图10-1 memkind组件和硬件支撑的总体轮廓

Memkind库充当一个包装器,将内存分配请求从应用程序重定向到管理堆的分配器。在发布时,只支持jemalloc分配器。未来的版本可能会引入并支持多个分配器。Memkind为jemalloc提供不同类型的内存:静态类型是自动创建的,而动态类型是由应用程序使用Memkind_create_kind() 创建的。

10.2.3 支持的内存“种类”

动态PMEM类型最适合通过启用DAX的文件系统与内存可寻址持久存储一起使用,该文件系统支持不通过系统页缓存分页的加载/存储操作。对于PMEM类型,memkind库支持内存映射文件上的传统malloc/free-like接口。当应用程序使用PMEM调用memkind_create_kind() 时,临时文件(tmpfile(3))将在已mounted的DAX文件系统上创建,并将内存映射到应用程序的虚拟地址空间中。当程序终止时,这个临时文件会被自动删除,给人一种波动的感觉。

图10-2显示了来自两个内存源的内存映射:DRAM(MEMKIND_DEFAULT)和persistent memory(PMEM_KIND)。对于来自DRAM的分配,应用程序可以调用memkind_malloc(),并将kind参数设置为MEMKIND_DEFAULT,而不是使用common malloc()。MEMKIND_DEFAULT是一种静态类型,使用操作系统的默认页大小进行分配。请参阅memkind文档以获得大页和巨页支持。

图10-2 应用程序使用不同“种类”的内存

在将libmemkind与DRAM和持久内存一起使用时,需要理解的要点是:

  1. 应用程序可以使用两个内存池,一个来自DRAM,另一个来自持久内存;
  2. 可以同时访问两个内存池,通过将kind type设置为PMEM_KIND以使用持久性内存,设置成MEMKIND_ DEFAULT以使用DRAM;
  3. jemalloc是用于管理各种内存的单一内存分配器;
  4. memkind库是jemalloc的包装器,它为不同类型的内存分配提供了统一的API;
  5. PMEM_KIND内存分配由在持久性内存感知文件系统上创建的临时文件(tmpfile(3))提供。当应用程序退出时,文件将被销毁。分配不是持久的;
  6. 对持久内存使用libmemkind需要对应用程序进行简单的修改。

10.3  Memkind API

与持久内存编程相关的memkind API函数如清单10-1所示,并在本节中进行了描述。完整的memkind API可在memkind手册页中找到,见(http://memkind.github.io/memkind/man_pages/memkind.html)。

Listing 10-1. Persistent memory-related memkind API functions
KIND CREATION MANAGEMENT: 
int memkind_create_pmem(const char *dir, size_t max_size, memkind_t *kind);
int memkind_create_pmem_with_config(struct memkind_config *cfg, memkind_t *kind); memkind_t memkind_detect_kind(void *ptr);
int memkind_destroy_kind(memkind_t kind);

KIND HEAP MANAGEMENT: 
void *memkind_malloc(memkind_t kind, size_t size); 
void *memkind_calloc(memkind_t kind, size_t num, size_t size); 
void *memkind_realloc(memkind_t kind, void *ptr, size_t size); 
void memkind_free(memkind_t kind, void *ptr);
size_t memkind_malloc_usable_size(memkind_t kind, void *ptr); 
memkind_t memkind_detect_kind(void *ptr);

KIND CONFIGURATION MANAGEMENT: 
struct memkind_config *memkind_config_new(); 
void memkind_config_delete(struct memkind_config *cfg); 
void memkind_config_set_path(struct memkind_config *cfg, const char  *pmem_dir); 
void memkind_config_set_size(struct memkind_config *cfg, size_t pmem_size); 
void memkind_config_set_memory_usage_policy(struct memkind_config *cfg, memkind_mem_usage_policy policy);

10.3.1 类型管理API

memkind库支持一个插件体系结构来合并新的内存类型,称为动态类型。Memkind库提供了创建和管理动态类型堆的API。

10.3.2 类型创建

使用memkind_create_pmem()函数从文件源创建pmem类型的内存。此文件在指定目录(PMEM_DIR)中创建为tmpfile(3),并且未链接,因此文件名不在该目录下列出。程序终止时将自动删除临时文件。

根据应用程序要求,使用memkind_create_pmem()创建固定或动态堆大小。此外,可以创建和提供配置,而不是将配置选项传递给*_create_*函数。

10.3.3 创建固定大小的堆

需要固定内存量的应用程序可以为memkind_create_pmem()的PMEM_MAX_SIZE参数指定一个非零值,如下所示。这定义了要为指定类型的内存创建的内存池的大小。PMEM_MAX_SIZE的值应小于PMEM_DIR中指定的文件系统的可用容量,以避免ENOMEM或ENOSPC错误。内部数据结构struct memkind由库内部填充,并由内存管理函数使用。

int memkind_create_pmem(PMEM_DIR, PMEM_MAX_SIZE, &pmem_kind)

memkind_create_pmem() 的参数:

  • PMEM_DIR 是要创建的临时文件所在的目录;
  • PMEM_MAX_SIZE 单位是字节,传递给jemalloc的内存区域的大小;
  • &pmem_kind memkind 数据结构的地址。

如果成功,memkind_create_pmem()返回零。失败时,返回一个错误号,使用memkind_error_message() 可以转换为错误消息字符串。清单10-2显示了如何在/daxfs文件系统上创建32MiB PMEM类型。此列表中包含了memkind_fatal()的定义,用于打印memkind错误消息并退出。本章中的其余示例假设此例程的定义如下所示。

Listing 10-2. Creating a 32MiB PMEM kind
void memkind_fatal(int err) {
    char error_message[MEMKIND_ERROR_MESSAGE_SIZE];
memkind_error_message(err, error_message,        MEMKIND_ERROR_MESSAGE_SIZE);    
fprintf(stderr, "%s\n", error_message);
exit(1); 
}
/* ... in main() ... */
#define PMEM_MAX_SIZE (1024 * 1024 * 32)
struct memkind *pmem_kind; 
int err;
// Create PMEM memory pool with specific size 
err = memkind_create_pmem("/daxfs",PMEM_MAX_SIZE, &pmem_kind);
if (err) {    
memkind_fatal(err); 
}

还可以使用函数memkind_ucreate_pmem_with_config()创建具有特定配置的堆。此函数使用memkind_config,其中包含可选参数,如大小、文件路径和内存使用策略。清单10-3展示了如何使用memkind_config_new() 构建一个test_cfg,然后将该配置传递给memkind_create_pmem_with_config() 来创建一个PMEM类型。我们使用清单10-2示例中相同的路径和大小参数进行比较。

Listing 10-3. Creating PMEM kind with configuration
struct memkind_config *test_cfg = memkind_config_new();
memkind_config_set_path(test_cfg, "/daxfs"); 
memkind_config_set_size(test_cfg, 1024 * 1024 * 32);
memkind_config_set_memory_usage_policy(test_cfg, MEMKIND_MEM_USAGE_POLICY_ CONSERVATIVE);
// create a PMEM partition with specific configuration 
err = memkind_create_pmem_with_config(test_cfg, &pmem_kind); 
if (err) {
    memkind_fatal(err); 
}

10.3.4 创建可变大小的堆

当PMEM_MAX_SIZE设置为0时,如下所示,只要临时文件可以增长,就可以满足分配。最大堆大小增长受PMEM_DIR参数下装载的文件系统容量的限制。

memkind_create_pmem(PMEM_DIR, 0, &pmem_kind)

memkind_create_pmem() 参数:

  • PMEM_DIR 是要创建的临时文件所在的目录;
  • PMEM_MAX_SIZE 设置为0;
  • &pmem_kind memkind 数据结构的地址。

如果PMEM kind创建成功,memkind_create_pmem()将返回零。失败时,可以使用memkind_error_message()将memkind_create_pmem()返回的错误号转换为错误消息字符串,如清单10-2中的memkind_fatal()例程所示。清单10-4展示了如何创建具有可变大小的PMEM类型。

Listing 10-4. Creating a PMEM kind with variable size
struct memkind *pmem_kind; 
int err; 
err = memkind_create_pmem("/daxfs",0,&pmem_kind);
 if (err) {
    memkind_fatal(err); 
}

10.3.5 检测内存类型

Memkind既支持该类型的自动检测,也提供函数进行检测,支持检测与指针引用的内存相关联的类型。

10.3.6 自动类型检测

使用libmemkind时,支持自动检测内存类型以简化代码更改。因此,memkind库将自动检索从中进行分配的内存池的类型,因此可以在不指定类型的情况下调用表10-1中列出的堆管理函数。

memkind库从分配器元数据内部跟踪给定对象的类型。然而,为了获得这些信息,一些操作可能需要获取一个锁来防止来自其他线程的访问,这可能会对多线程环境中的性能产生负面影响。

10.3.7 内存类型检测

Memkind还提供Memkind_detect_kind()函数,如下所示,用于查询并返回传递给函数的指针所引用的内存类型。如果输入指针参数为空,则函数返回空。传递到memkind_detect_kind()的输入指针参数必须已由先前对memkind_malloc()、memkind_calloc()、memkind_realloc()或memkind_posix_umemalign()的调用返回。

memkind_t memkind_detect_kind(void *ptr)

与自动检测方法类似,此函数具有非零性能开销。清单10-5显示了如何检测是哪种类型。

Listing 10-5. pmem_detect_kind.c – how to automatically detect the ‘kind’ type
73  err = memkind_create_pmem(path, 0, &pmem_kind);    
74  if (err) {    
75      memkind_fatal(err);    
76  }    
77
78  /* do some allocations... */    
79  buf0 = memkind_malloc(pmem_kind, 1000);    
80  buf1 = memkind_malloc(MEMKIND_DEFAULT, 1000);    
81    
82  /* look up the kind of an allocation */    
83  if (memkind_detect_kind(buf0) == MEMKIND_DEFAULT) {    
84      printf("buf0 is DRAM\n");    
85  } else {    
86      printf("buf0 is pmem\n");    
87  }

10.3.8 销毁类型对象

使用下面显示的memkind_destroy_kind()函数删除以前使用memkind_create_pmem()或memkind_create_pmem_with_config()函数创建的kind对象。

int memkind_destroy_kind(memkind_t kind);

使用清单10-5中相同的pmem_detect_kind.c代码,清单10-6显示了在程序退出之前如何销毁该类型。

Listing 10-6. Destroying a kind object
89     err = memkind_destroy_kind(pmem_kind);    
90     if (err) {    
91         memkind_fatal(err);    
92     }

当memkind_create_pmem()或memkind_create_pmem_with_uconfig()返回的类型被成功销毁时,为该类型对象分配的所有内存都会被释放。

10.3.9 堆管理API

本节中描述的堆管理函数有一个以ISO C标准API为模型的接口,带有一个额外的“kind”参数来指定用于分配的内存类型。

10.3.10 分配内存

memkind库提供memkind_malloc()、memkind_calloc()和memkind_realloc()函数来分配内存,定义如下:

void *memkind_malloc(memkind_t kind, size_t size); 
void *memkind_calloc(memkind_t kind, size_t num, size_t size); 
void *memkind_realloc(memkind_t kind, void *ptr, size_t size);

memkind_malloc() 分配指定类型的未初始化内存的size字节。分配的空间被适当地对齐(在可能的指针强制之后)以存储任何对象类型。如果size为0,那么memkind_malloc()返回NULL。

memkind_calloc() 为num个对象分配空间,每个对象的长度为size字节。结果与使用num*size参数调用memkind_malloc()相同。例外情况是分配的内存显式初始化为0字节。如果num或size为0,则memkind_calloc()返回NULL。

memkind_realloc() 将ptr引用的先前分配内存的大小更改为指定类型的size大小。内存的内容保持不变,取新size和旧size中的较小者【如果新的较小,则截断内容】。如果新size更大,则新分配内存部分的内容是未定义的。如果成功,ptr引用的内存将被释放,并返回指向新分配内存的指针。

清单10-7中的代码示例展示了如何使用memkind_malloc()从DRAM和持久内存(pmem_kind)分配内存。我们建议使用单个库来简化代码,而不是使用公共的C库malloc()来实现DRAM,使用memkind_malloc()来实现持久内存。

Listing 10-7. An example of allocating memory from both DRAM and persistent memory
/* * Allocates 100 bytes using appropriate "kind" * of volatile memory */
// Create a PMEM memory pool with a specific size  
err = memkind_create_pmem(path, PMEM_MAX_SIZE, &pmem_kind);  
if (err) {
      memkind_fatal(err);  
}  
char *pstring = memkind_malloc(pmem_kind, 100);  
char *dstring = memkind_malloc(MEMKIND_DEFAULT, 100);

10.3.11 释放分配的内存

为了避免内存泄漏,可以使用memkind_free()函数释放分配的内存,定义如下:

void memkind_free(memkind_t kind, void *ptr);

memkind_free() 使ptr引用的已分配内存可用于将来的分配。此指针必须由以前对memkind_malloc()、memkind_calloc()、memkind_realloc()或memkind_posix_umemalign()的调用返回。否则,如果先前调用了memkind_free(kind,ptr),则会发生未定义的行为。如果ptr为空,则不执行任何操作。如果在调用memkind_free()的上下文中类型未知,可以将NULL作为指定给memkind_free()的类型,但这需要对正确的类型进行内部查找。应该始终指定正确的类型,因为查找类型可能会导致严重的性能损失。

清单10-8显示了使用memkind_free()的四个示例。前两个指定类型,后两个使用NULL自动检测种类。

Listing 10-8. Examples of memkind_free() usage
/* Free the memory by specifying the kind */ 
memkind_free(MEMKIND_DEFAULT, dstring); 
memkind_free(PMEM_KIND, pstring);
/* Free the memory using automatic kind detection */ 
memkind_free(NULL, dstring); 
memkind_free(NULL, pstring);

10.3.12 类型配置管理

还可以使用函数memkind_ucreate_pmem_with_config()创建使用指定配置的堆。此函数要求使用可选参数(如大小、文件路径和内存使用策略)完整的memkind_config结构。

10.3.13 内存使用策略

在jemalloc中,名为dirty_decay_ms的运行时选项确定它将未使用的内存返回操作系统的速度。较短的残留时间可以更快地清除未使用的内存页,但清除会消耗CPU周期。在使用此参数之前,应仔细考虑此操作所需的内存和CPU周期之间的权衡。memkind库支持与此功能相关的两个策略:

  1. MEMKIND_MEM_USAGE_POLICY_DEFAULT;
  2. MEMKIND_MEM_USAGE_POLICY_CONSERVATIVE。

对于分配给PMEM类型的场所,使用MEMKIND_MEM_ USAGE_POLICY_DEFAULT策略的dirty_decay_ms的最小值和最大值为0ms到10000ms。设置MEMKIND_MEM_USAGE_POLICY_CONSERVATIVE更短的残留时间可以更快地清除未使用的内存,减少内存使用。定义内存使用策略,使用memkind_config_set_memory_usage_policy(),如下所示:

void memkind_config_set_memory_usage_policy (struct memkind_config *cfg, memkind_mem_usage_policy policy );

MEMKIND_MEM_USAGE_POLICY_DEFAULT 默认的内存使用策略

• MEMKIND_MEM_USAGE_POLICY_CONSERVATIVE 允许改变dirty_decay_ms 参数

清单10-9显示如何自定义配置中使用memkind_config_set_memory_usage_policy()。

Listing 10-9. An example of a custom configuration and memory policy use
73  struct memkind_config *test_cfg =    
74      memkind_config_new();    
75  if (test_cfg == NULL) {    
76      fprintf(stderr,    
77          "memkind_config_new: out of memory\n");    
78      exit(1);    
79  }    
80    
81  memkind_config_set_path(test_cfg, path);    
82  memkind_config_set_size(test_cfg, PMEM_MAX_SIZE);    
83  memkind_config_set_memory_usage_policy(test_cfg,    
84      MEMKIND_MEM_USAGE_POLICY_CONSERVATIVE);    
85    
86  // Create PMEM partition with the configuration    
87  err = memkind_create_pmem_with_config(test_cfg,    
88      &pmem_kind);    
89  if (err) {    
90      memkind_fatal(err);    
91  } 

10.3.14 其他memkind代码示例

memkind源代码树包含许多其他代码示例,可在GitHub上找到:https://github.com/memkind/memkind/tree/master/examples。

10.4  PMEM Kind 的C++分配器

一个新的pmem::allocator类模板被创建来支持持久内存的分配,这遵循了C++ 11分配器的要求。该分配器可以与C++兼容的数据结构一起使用,如:

  • Standard Template Library (STL) ;
  • Intel® Threading Building Blocks (Intel® TBB) 库。

pmem::allocator类模板使用前面描述的memkind_create_pmem()函数。这个分配器是有状态的,没有默认的构造函数。

10.4.1 pmem::allocator 的方法

pmem::allocator(const char *dir, size_t max_size); 
pmem::allocator(const std::string& dir, size_t max_size) ; 
template <typename U> pmem::allocator<T>::allocator(const pmem::allocator<U>&); 
template <typename U> pmem::allocator(allocator<U>&& other); 
pmem::allocator<T>::~allocator(); T* pmem::allocator<T>::allocate(std::size_t n) const; 
void pmem::allocator<T>::deallocate(T* p, std::size_t n) const ; 
template <class U, class... Args> void pmem::allocator<T>::construct(U* p, Args... args) const;
void pmem::allocator<T>::destroy(T* p) const;

有关pmem::allocator类模板的更多信息,请参阅pmem分配器(3)手册页。

10.4.2 嵌套容器

多层容器(如列表、元组、映射、字符串等的vector)在处理嵌套对象时带来了挑战。假设您需要创建字符串vector并将其存储在持久内存中。这项任务面临的挑战及其解决方案包括:

  1. 挑战:std::string不能用于此目的,因为它是std::basic_string的别名。pmem:allocator需要一个使用pmem:allocator新的别名。

方案:使用pmem::allocator分配器创建时,使用typedef定义一个std::basic_string的别名为pmem_string;

  • 挑战:如何确保最外层的vector使用pmem::allocator的正确实例正确地构造嵌套的pmem_string。

方案:从C++ 11和以后,std::scoped_allocator_ adaptor适配器类模板可以与多层次容器一起使用。此适配器的目的是正确初始化嵌套容器中的有状态分配器,例如当嵌套容器的所有级别都必须放在同一内存段中时。

10.5  C++的例子

本节介绍了几个完整的代码示例,演示了使用C和C++的libmemkind的使用。

10.5.1 使用pmem::allocator

如前所述,可以将pmem::allocator与任何类似STL的数据结构一起使用。清单10-10中的代码示例包含了pmem_allocator.h头文件以使用pmem::allocator。

Listing 10-10. pmem_allocator.cpp: using pmem::allocator with std:vector
37  #include <pmem_allocator.h>    
38  #include <vector>    
39  #include <cassert>    
40    
41  int main(int argc, char *argv[]) {    
42      const size_t pmem_max_size = 64 * 1024 * 1024; //64 MB    
43      const std::string pmem_dir("/daxfs");    
44    
45      // Create allocator object    
46      libmemkind::pmem::allocator<int>    
47          alc(pmem_dir, pmem_max_size);    
48
49      // Create std::vector with our allocator.    
50      std::vector<int,    
51          libmemkind::pmem::allocator<int>> v(alc);    
52    
53      for (int i = 0; i < 100; ++i)    
54          v.push_back(i);    
55    
56      for (int i = 0; i < 100; ++i)    
57          assert(v[i] == i);
  • Line 43: 定义了一个64MiB的持久内存池;
  • Lines 46-47: 创建一个pmem::allocator<int>类型的分配器对象alc;
  • Line 50: 我们创建std::vector<int, pmem::allocator<int> >类型的vector对象v,并将第47行对象的alc作为参数传入。pmem::allocator是有状态的,没有默认构造函数。这需要将分配器对象传递给vector构造函数;否则,如果调用std::vector<int, pmem::allocator<int> >的默认构造函数,则会发生编译错误,因为vector构造函数将尝试调用pmem::allocator的默认构造函数,而pmem::allocator尚不存在。

10.5.2 创建字符串vector

清单10-11展示了如何创建驻留在持久内存中的字符串vector。我们使用pmem::allocator分配器将pmem_string定义为std::basic_string的typedef。在本例中,std::scoped_allocator_adaptor允许vector将pmem::allocator实例传播到存储在向量对象中的所有pmem_string对象。

Listing 10-11. vector_of_strings.cpp: creating a vector of strings
37  #include <pmem_allocator.h>    
38  #include <vector>    
39  #include <string>    
40  #include <scoped_allocator>    
41  #include <cassert>
42  #include <iostream>    
43    
44  typedef libmemkind::pmem::allocator<char> str_alloc_type;    
45    
46   typedef std::basic_string<char, std::char_traits<char>,  str_alloc_type> pmem_string;    
47    
48  typedef libmemkind::pmem::allocator<pmem_string> vec_alloc_type;    
49    
50   typedef std::vector<pmem_string, std::scoped_allocator_adaptor <vec_alloc_type> > vector_type;    
51    
52  int main(int argc, char *argv[]) {    
53      const size_t pmem_max_size = 64 * 1024 * 1024; //64 MB    
54      const std::string pmem_dir("/daxfs");    
55    
56      // Create allocator object    
57      vec_alloc_type alc(pmem_dir, pmem_max_size);    
58      // Create std::vector with our allocator.    
59      vector_type v(alc);    
60    
61      v.emplace_back("Foo");    
62      v.emplace_back("Bar");    
63    
64      for (auto str : v) {    
65              std::cout << str << std::endl;    
66      }
  • 第46行:将pmem_string定义为std::basic_string的typedef;
  • 第48行:使用pmem_string类型定义pmem::allocator分配器;
  • 第50行:使用std::scoped_allocator_adaptor允许vector将pmem::allocator实例传播到存储在vector对象中的所有pmem_string对象。

10.6  使用持久内存扩展易失性内存

持久内存被内核视为一个设备。在典型的用例中,使用-o dax选项创建并装载一个持久性内存感知文件系统,文件被内存映射到进程的虚拟地址空间中,以便让应用程序可以直接加载/存储对持久性内存区域的访问。

Linux内核v5.1增加了一个新特性,使得持久性内存可以作为易失性内存得到更广泛的使用。这是通过将持久内存设备绑定到内核来完成的,内核将其作为DRAM的扩展来管理。由于持久内存具有与DRAM不同的特性,因此该设备提供的内存在其对应的socket上作为单独的NUMA节点可见。

要使用MEMKIND_DAX_KMEM类型,需要使用设备DAX提供pmem,该设备将pmem公开为具有/dev/DAX*类似名称的设备。如果您有一个现有的dax设备,并且要将设备型号类型迁移到使用DEV_DAX_KMEM,请使用下面的命令:

$ sudo daxctl migrate-device-model

要使用第一个可用region(NUMA节点)上的所有可用容量创建新的dax设备,请使用:

$ sudo ndctl create-namespace --mode=devdax --map=mem

要创建指定region和容量的新dax设备,请使用:

$ sudo ndctl create-namespace --mode=devdax --map=mem --region=region0 --size=32g

要显示namespaces列表,请使用:

$ ndctl list

如果已在其他模式(如默认fsdax)中创建namespace,则可以使用以下方法重新配置设备,其中namespace0.0是要重新配置的现有命名空间:

$ sudo ndctl create-namespace --mode=devdax --map=mem --force -e namespace0.0

有关创建新namespace的详细信息请阅读https://docs.pmem.io/ndctl-users-guide/managing-namespaces#creating-namespaces

必须转换DAX设备才能使用system-ram模式。可以使用以下命令将dax设备转换为适合与系统内存一起使用的NUMA节点:

$ sudo daxctl reconfigure-device dax2.0 --mode=system-ram

这会将设备从使用设备dax驱动程序迁移到dax pmem驱动程序。下面显示了一个示例输出,其中dax1.0配置为默认devdax类型,dax2.0为system-ram:

$ daxctl list
    [
      {
        "chardev":"dax1.0",
        "size":263182090240,
        "target_node":3,
        "mode":"devdax"
      },
      {
        "chardev":"dax2.0",
        "size":263182090240,
        "target_node":4,
        "mode":"system-ram"
      }
    ]

现在可以使用numactl -H来显示硬件NUMA配置。以下示例输出是从一个2-socket系统收集的,显示节点4是一个新的system-ram支持的NUMA节点,该节点是从持久内存创建的:

$ numactl -H
    available: 3 nodes (0-1,4)
    node 0 cpus:  0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
 23 24 25 26 27 56 57 58 59 60 61 62 63 64 65 66 67 68 69 
70 71 72 73 74 75 76 77 78 79 80 81 82 83
    node 0 size: 192112 MB
    node 0 free: 185575 MB

node 1 cpus:  28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 
47 48 49 50 51 52 53 54 55 84 85 86 87 88 89 90 91 92 93 
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 
110 111    
node 1 size: 193522 MB    
node 1 free: 193107 MB    
node 4 cpus:    node 4 size: 250880 MB    
node 4 free: 250879 MB    
node distances:
    node   0   1   4
      0:  10  21  17      
1:  21  10  28      
4:  17  28  10

要使NUMA节点联机并让内核管理新内存,请使用:

$ sudo daxctl online-memory dax0.1 dax0.1: 5 sections already online dax0.1: 0 new sections onlined onlined memory for 1 device

此时,内核将使用新的容量进行正常操作。新内存显示在工具中,如下面所示的lsmem示例,其中我们在0x000000338000000-0x00000035ffffffff地址范围中看到附加的10GiB system-ram:

$ lsmem
 RANGE                            SIZE   STATE REMOVABLE   BLOCK 0x0000000000000000-0x000000007fffffff    2G online        no        0 0x0000000100000000-0x000000277fffffff  154G online        yes      2-78 0x0000002780000000-0x000000297fffffff    8G online        no      79-82 0x0000002980000000-0x0000002effffffff   22G online         yes     83-93 0x0000002f00000000-0x0000002fffffffff    4G online          no     94-95 0x0000003380000000-0x00000035ffffffff   10G online         yes    103-107 0x000001aa80000000-0x000001d0ffffffff  154G online         yes    853-929 0x000001d100000000-0x000001d37fffffff   10G online        no     930-934 0x000001d380000000-0x000001d8ffffffff   22G online        yes     935-945 0x000001d900000000-0x000001d9ffffffff    4G online        no      946-947

Memory block size:         2G 
Total online memory:     390G 
Total offline memory:      0B

为了从使用持久内存创建的NUMA节点以编程方式分配内存,在使用system-ram DAX设备的libmemkind中添加了一种新的静态类型MEMKIND_DAX_KMEM。

使用MEMKIND_DAX_KMEM作为memkind_malloc()的第一个参数,如下所示,您可以在单个应用程序中使用来自不同NUMA节点的持久内存。持久内存仍然物理地连接到CPU socket插槽,因此应用程序应注意确保CPU affinity相关性以获得最佳性能。

memkind_malloc(MEMKIND_DAX_KMEM, size_t size)

图10-3显示了一个创建了两个静态kind对象的应用程序:MEMKIND_ DEFAULT和MEMKIND_DAX_KMEM。

图10-3 应用程序—从不同类型的内存创建了两种对象

前面描述的PMEM_KIND与MEMKIND_DAX_ KMEM的区别在于,MEMKIND_DAX_KMEM是静态类型,使用带有MAP_PRIVATE标志的mmap(),而动态PMEM_KIND是使用memkind_create_ pmem()创建的,并在DAXenabled文件系统上内存映射文件时使用MAP_SHARED标志。

使用fork(2)系统调用创建的子进程从父进程继承MAP_PRIVATE映射。当父进程修改内存页时,内核将触发写时拷贝机制,为子进程创建未修改的拷贝。这些页与原始页分配在同一个NUMA节点上。

10.7  libvmemcache: 大容量持久内存的高效易失性KV缓存

一些现有的内存数据库(IMDB)依赖于手工动态内存分配(malloc、jemalloc、tcmalloc),在长时间运行时,这些数据库可能会出现外部和内部内存碎片,导致大量内存无法分配。内部和外部碎片简要解释如下:

  1. 当分配的内存超过所需数量,并且未使用的内存包含在分配的区域中时,会发生内部碎片。例如,如果请求的分配大小为200字节,而分配的是256字节的块;
  2. 当动态分配可变内存大小时,会发生外部碎片,导致无法分配连续的内存块,尽管请求的内存块在系统中仍然可用。当大容量的持久性存储器被用作易失性存储器时,这个问题更加明显。运行时间长的应用程序需要解决这个问题,特别是当分配的大小有很大的变化时。应用程序和运行时环境以不同的方式处理此问题,例如:
  3. Java and .NET 使用压实垃圾收集;
  4. Redis and Apache Ignite* 使用碎片整理算法;
  5. Memcached 使用slab分配器。

以上每个分配器机制都有优缺点。垃圾收集和碎片整理算法要求在堆上进行处理,以释放未使用的分配或移动数据以创建连续空间。Slab分配器通常在初始化时定义一组固定大小的bucket,而不知道应用程序将需要多少个bucket。如果slab分配器耗尽了某个bucket大小,它将从较大的bucket中进行分配,这将减少可用空间量。这些机制可能会阻止应用程序的处理并降低其性能。

10.7.1 libvmemcache 概览

libvmemcache是一个可嵌入的轻量级内存缓存解决方案,其核心是K-V存储。它旨在充分利用大容量内存,例如持久内存,以可伸缩的方式高效地使用内存映射。它经过优化,可通过支持load/store操作的DAXenabled文件系统与内存可寻址持久存储一起使用。libvmemcache具有以下独特的特性:

  • 基于扩展的内存分配器避免了影响大多数内存数据库的碎片问题,它允许缓存为大多数工作负载实现非常高的空间利用率;
  • 缓冲LRU(最近使用最少)结合了传统的LRU双链表和无阻塞环形缓冲区,在现代多核cpu上提供高扩展性;
  • 独特的索引critnib数据结构提供了高性能,同时也非常节省空间。

libvmemcache的缓存被调整为在相对较大的值下以最优方式工作。虽然最小可支持256字节,但大于1K字节性能最好。libvmemcache对分配有更多的控制,因为它使用基于区段的方法(类似于文件系统区段的方法)实现自定义内存分配方案。因此,libvmemcache可以串联并获得可观的空间效率。另外,因为它是一个缓存,所以它可以在最坏的情况下收回数据以分配新条目。Libvmemcache总是准确的分配与其释放内存完全相同的内存,少了元数据开销。对于基于公共内存分配器(如memkind)的缓存,情况并非如此。libvmemcache设计用于处理terabyte-sized的内存工作负载,具有非常高的空间利用率。

libvmemcache通过在启用DAX的文件系统上自动创建临时文件并将其映射到应用程序的虚拟地址空间来工作。当程序终止时,临时文件会被删除,并产生波动性。图10-4显示了应用程序使用传统的malloc()从DRAM分配内存,并使用libvmemcache将驻留在启用DAX的文件系统上的临时文件从持久内存映射到内存。

图10-4 应用程序使用启用DAX文件系统的libvmemcache内存映射临时文件

尽管libmemkind支持不同类型的内存和内存消耗策略,但底层的分配器是jemalloc,它使用动态内存分配。表10-2比较了libvmemcache和libmemkind的实现细节。

10.7.2 libvmemcache 设计

libvmemcache有两个主要设计方面:

  1. 用于改进/解决碎片问题的分配器设计;
  2. 一种可扩展且高效的LRU策略。

10.7.3 基于区段的分配器

libvmemcache可以解决在处理terabyte-sized大小的内存工作负载时的碎片问题,并提供高空间利用率。图10-5显示了一个创建许多小对象的工作负载示例,随着时间的推移,分配器由于碎片而停止。

图10-5 工作负载样例—创建许多小对象,分配器因碎片而无法分配

libvmemcache使用基于extent的分配器,其中extent是为存储数据库中的数据而分配的连续块集。扩展通常与文件系统支持的大块(扇区、页等)一起使用,但当使用支持较小块大小的持久内存(cache line)时,此类限制不适用。图10-6显示,如果一个连续的空闲块不可用于分配对象,则使用多个非连续块来满足分配请求。非连续分配显示为对应用程序来说看到的是一次单一分配。

图10-6 使用非连续空闲块满足更大的分配请求

10.7.4 可扩展替换策略

LRU缓存传统上实现为双链表。当从这个列表中检索到一个项时,它会从列表的中间移到前面,所以不会被逐出。在多线程环境中,多个线程可能与前端元素争用,所有线程都试图将要检索的元素移到前端。因此,在移动要检索的元素之前,前元素总是被锁定(与其他锁一起),这会导致锁争用。这种方法不可扩展,效率低下。基于缓冲区的LRU策略创建了一个可伸缩且高效的替换策略。在LRU链表的前面放置一个非阻塞的环形缓冲区,以跟踪正在检索的元素。当检索到元素时,它将被添加到此缓冲区中,并且只有当缓冲区已满(或元素被逐出)时,链接列表才会被锁定,并且缓冲区中的元素将被处理并移动到列表的前面。这种方法保留了LRU策略,并提供了一种可伸缩的LRU机制,对性能的影响最小。图10-7显示了基于环形缓冲区的LRU算法设计。

图10-7 基于环形缓存的LRU设计

10.7.5 使用libvmemcache

表10-3列出了libvmemcache提供的基本功能。有关完整列表,请参见libvmemcache手册页(https://pmem.io/vmemcache/manpages/master/vmemcache.3.html)。

为了演示如何使用libvmemcache,清单10-12展示了如何使用默认值创建vmemcache实例。此示例在启用DAX的文件系统上使用临时文件,并显示如何在key “meow”的缓存未命中后注册回调。

Listing 10-12. vmemcache.c: An example program using libvmemcache
37  #include <libvmemcache.h>    
38  #include <stdio.h>    
39  #include <stdlib.h>    
40  #include <string.h>    
41    
42  #define STR_AND_LEN(x) (x), strlen(x)    
43    
44  VMEMcache *cache;    
45    
46  void on_miss(VMEMcache *cache, const void *key,    
47      size_t key_size, void *arg)    
48  {    
49      vmemcache_put(cache, STR_AND_LEN("meow"),    
50           STR_AND_LEN("Cthulhu fthagn"));    
51  }    
52    
53  void get(const char *key)    
54  {    
55      char buf[128];    
56      ssize_t len = vmemcache_get(cache,    
57      STR_AND_LEN(key), buf, sizeof(buf), 0, NULL);    
58      if (len >= 0)    
59          printf("%.*s\n", (int)len, buf);    
60      else    
61          printf("(key not found: %s)\n", key);    
62  }    
63    
64  int main()    
65  { 
66      cache = vmemcache_new();    
67      if (vmemcache_add(cache, "/daxfs")) {    
68          fprintf(stderr, "error: vmemcache_add: %s\n",    
69                  vmemcache_errormsg());    
70              exit(1);    
71      }    
72    
73      // Query a non-existent key    
74      get("meow");    
75    
76      // Insert then query    
77      vmemcache_put(cache, STR_AND_LEN("bark"),    
78          STR_AND_LEN("Lorem ipsum"));    
79      get("bark");    
80    
81      // Install an on-miss handler    
82      vmemcache_callback_on_miss(cache, on_miss, 0);    
83      get("meow");   
84    
85      vmemcache_delete(cache);
  • 第66行:创建一个vmemcache的新实例,其中包含eviction_policy和extent_size的默认值;
  • 第67行:调用vmemcache_add()函数将缓存与给定路径关联;
  • 第74行:调用get()函数查询已存在的key。此函数调用vmemcache_get()函数,并检查函数的成功/失败;
  • 第77行:调用vmemcache_put()插入新key;
  • 第82行:添加一个on-miss回调处理程序,将key “meow”插入缓存;
  • 第83行:使用get()函数检索key “meow”;
  • 第85行:删除vmemcache实例。

10.8  本章小结

本章展示了持久性内存的大容量是如何用来保存易失性应用程序数据的。应用程序可以选择从DRAM或持久内存或两者中分配和访问数据。memkind是一个非常灵活和易于使用的库,其语义类似于开发人员经常使用的libc的 malloc/free API。libvmemcache是一个可嵌入的轻量级内存缓存解决方案,它允许应用程序以可伸缩的方式有效地使用持久内存的大容量。libvmemcache是GitHub上的一个开源项目,位于https://github.com/pmem/vmemcache。

作者: charlie_chen

编程是一生最爱: >> 架构与设计; >> 软件工程; >> 项目管理; >> 产品研发。
联系我们

联系我们

022-XXXXXXXX

在线咨询: QQ交谈

邮箱: 1549889473@qq.com

欢迎交流。
关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部