Skip to content

支持持久内存 & 动态阈值控制数据分布

Patryk Kaminski edited this page Apr 14, 2022 · 2 revisions

1. 持久内存的介绍

根据IDC的预测,未来改变世界的十大科技里面前三名分别是量子计算,5G通信和持久内存。现在,5G时代已经到来,各种新的应用将层出不穷,根据IDC预测未来数年数据将以每年27%的速度增长。

5G带来数据爆炸

同时我们现在有大量的数据没有充分挖掘,只是保存在相对冷的硬盘和SSD中。数据如同石油,需要将冷的数据挖掘出来变成真正产生流量的热的数据,这样热的数据将呈现爆发式增长,到2025年热数据大概会占到整个数据的30%。

2025年热数据

应对数据爆炸的方法,可以: 第一,通过扩展机器的数量,横向扩展,cost增加,IT成本,机架成本巨大。 第二,可以通过增加单机密度,需要CPU核心数增长,同时内存需求同比例,甚至高比例增长来满足实时性的需求。 通过观察服务器CPU的核数增长的速度,可以看出CPU核心的增长越来越快,通过增加单机密度来提升性能已经成为各大互联网厂商的一个共识。而持久内存可以做到大容量,持久化的特点可以很好的满足对于数据实时处理的需求。现今的DRAM内存的容量增长趋缓,一般服务器使用单根小于64GB的内存。而处理器的核心数越来越多,处理能力越来越强,较小的内存容量无法充分使用处理器的能力,造成资源浪费。持久内存给Redis带来新价值,单根容量大、性价比高,可以通过运行更多的Redis实例或者更大容量的实例来充分释放单机处理器的能力。通过持久内存来扩展内存,大大降低业务的总成本(Total Cost of Ownership ,TCO)。

2. 持久内存的工作方式

持久内存主要有两种工作的方式, 内存模式和应用直接访问模式。

  1. 其中内存模式将系统中的DRAM当成持久内存的缓存,应用不需要做任何改动。对于redis来说,内存模式也是一个可选择的方式。但是这一种情况下,DRAM的空间在系统中将不可见,对于系统来讲就不能充分使用持久内存。
  2. 应用直接访问方式,在这种方式下,持久内存和DRAM的空间同时可以由系统来管理。所以需要应用来自己管理数据的分布。如果要使用持久特性,需要使用Intel持久内存编程套件PMDK。持久内存的应用直接访问方式往往是通过对pmem-aware文件系统,进行mmap来进行直接访问,这种情况下将没有系统调用,没有上下文切换,中断。所以内核中对于匿名页的Copy-on-write也不支持。这在redis中使用fork来进行background Save,是必须要有的功能,这样就必须在redis应用中自己去实现copy-on-write的功能。 持久内存工作方式
  3. KMEMDAX方式, 在linux kernel>5.1以后,AD模式可以将管理的持久内存空间交给MMU管理,这样从系统中可以将持久内存作为一个和DRAM相类似的匿名空间,可以将持久内存看着是一个较慢,较大的内存节点。在持久内存和DRAM同时存在的系统中间,尽量将redis的索引数据放到DRAM中,而将用户数据放到持久内存。使用动态阈值管理数据分布,既可以保证较小的索引数据尽量放到内存中,又可以保证DRAM和持久内存符合一定的容量比列。对于一些生命周期较短并频繁访问的数据,我们会通过优化将其放置到DRAM中。 KMEMDAX

3. 动态阈值控制数据的分布

Redis是一个广泛用于互联网企业的NoSQL内存数据库,其中各家互联网企业都积极投身Redis的研究和定制化,以满足各家互联网企业的需求,并解决在业务中的痛点。Antirez的Redis社区维护了Redis核心代码,无法满足中国互联网客户快速迭代、快速解决业务中痛点的需求。memKeyDB基于Antirez的核心代码,依赖互联网客户积极贡献代码,满足中国客户的需求。 如何将数据有效的按照一定的比列来分布到持久内存和内存中,是一个设计的难点。现在memKeyDB基于KMEMDAX方式,实现了动态阈值控制数据的分布,大于或等于阈值的数据都会放到持久内存中,而小于阈值的数据将会放到内存中。

  1. 如果用户数据较小,那么threshold也会较小,更多的小数据会被放到持久内存中。因为数据都较小,此时系统压力小,所以性能不会有大的影响。
  2. 如果用户数据较大,那么相应的threshold也会较大,较大的数据会放到持久内存中,而较小的数据会放到内存中,此时的性能也可以满足实时的需要。
dynamicthreshold

动态阈值控制数据分布的优点:

  1. 算法简单,简单的反馈式操作,不断地调整数据分布的位置,代码改动小。
  2. Redis的场景中性能受限于网络的带宽而不是在内存和持久内存的性能。即使绝大多数数据放在持久内存中,性能仍然可以达到DRAM 90%以上。
  3. 此时DRAM和持久内存都对系统可见,所以系统可以充分使用DRAM和持久内存的资源,可以进一步降低TCO。
  4. 在一个系统中可以对不同的Redis server设置不同的DRAM和持久内存的比例,能够对客户进行必要的分层和QoS的管理。

动态阈值机制监控DRAM和持久内存相关的分配器统计信息,定期调整内部动态阈值。这将平衡每个介质的利用率,使其接近为应用程序配置的目标值。该算法检查当前DRAM和持久内存使用比例的速率和趋势,进一步调整阈值以加速达到所需比例。 除了用户键值数据外,应用程序还使用了许多内部结构,它们可能分配后被很快释放,例如当一个新的客户端连接到Redis服务器时, “client” 结构中的一些buffer数据用完后就会很快的释放。出于性能考虑,总是把它们放在DRAM上是值得的。 有些结构是描述用户数据的元数据。与键和值相比,它们通常非常小,但存储在持久内存上时可能会影响性能。引入了以下相关的代码优化:

  1. 将Redis对象结构(robj)始终存储在DRAM中(嵌入的字符串除外)。
  2. 默认情况下,将主Redis哈希表存储在DRAM中。由于哈希表的大小对于数据集主要包含小对象的场景会难以保证所能达到的目标比例。因此有一个配置选项允许将主哈希表的位置更改为持久内存
  3. Antrez的OS Redis使用jemalloc为应用程序分配内存。在memKeyDB中,它被替换为基于jemalloc的memkind分配器。它通过引入内存类型的概念来管理来自DRAM和持久内存的分配。malloc用于从两种媒体分配。为了简化代码,使用free时可以无需指定内存类型(使用NULL类型) ,分配器本身可以识别指针使用的内存并正确释放它, 而不需要程序跟踪分配的来源。
  4. 对于优化为总是从DRAM分配的结构,将此内存类型信息传递给memkind的free函数。这消除了分配器识别释放空间类型,加快了free执行速度。 memkind是一个通用的分配器,但对于像Redis这样的应用程序,通过在 “configure” 部分传递特定的参数进行优化:
  5. arenas的数量:对于持久内存类型,memkind通常为每个core创建4个arenas。对于单线程应用程序,可以限制为1个arena。这样当通过遍历所有arena来收集分配器统计信息时,会加快速度。
  6. -Lg_quantum:继承Redis中jemalloc config的值。创建一个addition分配类,Redis经常使用这个类。这有助于提高内存利用率。

4. 配置KMEMDAX

  1. KMEMDAX这个特性是在kernel 5.1以后支持的,也可以将支持KMEMDAX的patch移植到之前的kernel版本,这个需要额外的支持。 我们倾向于客户可以直接使用较新的kernel以能够快速支持kMEMDAX的这个特性。 下面是检查系统是否支持kmemdax,如果不支持可以: 升级kernel或者将kmemdax的kernel patch打入现有的kernel版本
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ cat /boot/config-5.2.0-1.el7.elrepo.x86_64 |grep KMEM
CONFIG_DEVKMEM is not set
CONFIG_DEV_DAX_KMEM=m
CONFIG_HAVE_DEBUG_KMEMLEAK=y
CONFIG_DEBUG_KMEMLEAK is not set
  1. 安装ipmctl,ndctl, daxctl, memKeyDB依赖KMemDax特性,需要daxctl>65,可以先安装daxctl 67,ndctl 67
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ wget https://mirrors.aliyun.com/centos/8-stream/BaseOS/x86_64/os/Packages/daxctl-67-2.el8.x86_64.rpm
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ wget https://mirrors.aliyun.com/centos/8-stream/BaseOS/x86_64/os/Packages/daxctl-libs-67-2.el8.x86_64.rpm
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ wget https://mirrors.aliyun.com/centos/8-stream/AppStream/x86_64/os/Packages/daxctl-devel-67-2.el8.x86_64.rpm
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ wget https://mirrors.aliyun.com/centos/8-stream/BaseOS/x86_64/os/Packages/ndctl-libs-67-2.el8.x86_64.rpm
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ wget https://mirrors.aliyun.com/centos/8-stream/BaseOS/x86_64/os/Packages/ndctl-67-2.el8.x86_64.rpm
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ wget https://mirrors.aliyun.com/centos/8-stream/BaseOS/x86_64/os/Packages/json-c-0.13.1-0.2.el8.x86_64.rpm

[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ ls
daxctl-67-2.el8.x86_64.rpm        daxctl-libs-67-2.el8.x86_64.rpm   ndctl-67-2.el8.x86_64.rpm
daxctl-devel-67-2.el8.x86_64.rpm  json-c-0.13.1-0.2.el8.x86_64.rpm  ndctl-libs-67-2.el8.x86_64.rpm

[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ sudo rpm -ivh json-c-0.13.1-0.2.el8.x86_64.rpm
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ sudo rpm -ivh daxctl-libs-67-2.el8.x86_64.rpm
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ sudo rpm -ivh daxctl-67-2.el8.x86_64.rpm
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ sudo rpm -ivh daxctl-devel-67-2.el8.x86_64.rpm
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ sudo rpm -ivh ndctl-libs-67-2.el8.x86_64.rpm
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ sudo rpm -ivh ndctl-67-2.el8.x86_64.rpm

[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ sudo yum install ndctl
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile
Package matching ndctl-65-5.1.al7.x86_64 already installed. Checking for update.
Nothing to do[ali-cloud@iZbp16jovvkf36ujwt2396Z root]$ sudo yum install ipmctl
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ sudo yum install ipmctl
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile
Resolving Dependencies
--> Running transaction check
---> Package ipmctl.x86_64 0:02.00.00.3764-1.el7 will be installed
--> Processing Dependency: libipmctl(x86-64) = 02.00.00.3764-1.el7 for package: ipmctl-02.00.00.3764-1.el7.x86_64
--> Processing Dependency: libipmctl.so.4()(64bit) for package: ipmctl-02.00.00.3764-1.el7.x86_64
--> Running transaction check
---> Package libipmctl.x86_64 0:02.00.00.3764-1.el7 will be installed
--> Finished Dependency Resolution
 
Dependencies Resolved
 
===================================================================================================================================================
 Package                           Arch                           Version                                       Repository                    Size
===================================================================================================================================================
Installing:
 ipmctl                            x86_64                         02.00.00.3764-1.el7                           epel                          81 k
Installing for dependencies:
 libipmctl                         x86_64                         02.00.00.3764-1.el7                           epel                         474 k
=======
Transaction Summary
===================================================================================================================================================
Install  1 Package (+1 Dependent package)
 
Total download size: 555 k
Installed size: 2.5 M
Is this ok [y/d/N]:
  1. 重新配置namespace, 检查namespace,我们必须是要支持devdax,关于如何配置devdax,请参考 https://pmem.io/ndctl/ndctl-create-namespace.html
[root@iZbp16jovvkf36ujwt2396Z ~]# ndctl list
[
  {
"dev":"namespace1.0",
"mode":"devdax",
"map":"mem",
"size":541163782144,
"uuid":"ca146a53-f1bc-417f-a961-6b898f413070",
"chardev":"dax1.0",
"align":2097152
  },
  {
"dev":"namespace0.0",
"mode":"devdax",
"map":"mem",
"size":541163782144,
"uuid":"d9aeb1a7-8c30-400d-bf0a-9c8701026f06",
"chardev":"dax0.0",
"align":2097152
  }
]
  1. 如果你看不到任何的namespace,需要根据下图来安装和创建持久内存namespace provisiondevdax

  2. 检查一下KMEMDAX module是不是在系统中

[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ lsmod|grep "dax"

device_dax       20480 0

dax_pmem        16384 0

dax_pmem_core     16384 1 dax_pmem 

如果你看到的 dax_pmem_core 不是dax_pmem,可以通过下面的命令进行转换:

[root@localhost ~]# daxctl migrate-device-model 

安排modprobe禁用dax_pmem_compat(如果存在),并部署dax_pmem模块以转换为/sys/bus/dax模型。v5.1之前的内核版本可能不支持/sys/bus/dax,在这种情况下,在内核更新之前,这个命令的结果是一个nop。从/sys/class/dax更改为/sys/bus/dax的动机是允许设备dax实例的替代驱动程序,特别是dax_kmem驱动程序。然后重启系统,让配置生效. 参考 https://pmem.io/ndctl/daxctl-migrate-device-model.html

  1. 将devdax切换成为system memory;每次重启后都需要将dax0.0/dax1.0配置成为system memory
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ sudo daxctl reconfigure-device dax0.0 --mode=system-ram
dax0.0:
  WARNING: detected a race while onlining memory
  Some memory may not be in the expected zone. It is
  recommended to disable any other onlining mechanisms,
  and retry. If onlining is to be left to other agents,
  use the --no-online option to suppress this warning
dax0.0: all memory sections (503) already online
[
  {
"chardev":"dax0.0",
"size":541163782144,
"target_node":2,
"mode":"system-ram",
"movable":false
  }
]
reconfigured 1 device
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ sudo daxctl reconfigure-device dax1.0 --mode=system-ram
dax1.0:
  WARNING: detected a race while onlining memory
  Some memory may not be in the expected zone. It is
  recommended to disable any other onlining mechanisms,
  and retry. If onlining is to be left to other agents,
  use the --no-online option to suppress this warning
dax1.0: all memory sections (503) already online
[
  {
"chardev":"dax1.0",
"size":541163782144,
"target_node":3,
"mode":"system-ram",
"movable":false
  }
]
reconfigured 1 device
  1. 检查system memory,如果memory看到是这样的情况,那么表示kmemdax已经创建成功
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ numactl -H
available: 4 nodes (0-3)
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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
node 0 size: 95046 MB
node 0 free: 85099 MB
node 1 cpus: 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
node 1 size: 96758 MB
node 1 free: 88201 MB
node 2 cpus:
node 2 size: 515072 MB
node 2 free: 515038 MB
node 3 cpus:
node 3 size: 515072 MB
node 3 free: 515041 MB
node distances:
node   0   1   2   3
  0:  10  21  17  28
  1:  21  10  28  17
  2:  17  28  10  28
  3:  28  17  28  10

5. 下载memKeyDB,编译安装

  1. 下载memKeyDB的版本,编译安装 (请确认编译环境的正确性,推荐编译器gcc版本>4.9),下面是redis6.0.5支持版本。
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ cd ~
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ wget https://github.com/memKeyDB/memKeyDB/releases/tag/6.0-pmem-support
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ tar -xvf 6.0-pmem-support
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ wget https://github.com/memkind/memkind/archive/v1.10.1-rc2.tar.gz
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ tar -xvf v1.10.1-rc2.tar.gz
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ mv memkind-1.10.1-rc2/ 6.0-pmem-support/deps/memkind
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ cd 6.0-pmem-support
[ali-cloud@iZbp16jovvkf36ujwt2396Z 6.0-pmem-support]$
[ali-cloud@iZbp16jovvkf36ujwt2396Z 6.0-pmem-support]$ make distclean;make -j 50
  1. 下面是redis3.2.12 支持版本
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ cd ~
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ wget https://github.com/memKeyDB/memKeyDB/releases/tag/3.2-pmem-support
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ tar -xvf 3.2-pmem-support
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ wget https://github.com/memkind/memkind/archive/v1.10.1-rc2.tar.gz
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ tar -xvf v1.10.1-rc2.tar.gz
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ mv memkind-1.10.1-rc2/ 3.2-pmem-support/deps/memkind
[ali-cloud@iZbp16jovvkf36ujwt2396Z ~]$ cd 3.2-pmem-support
[ali-cloud@iZbp16jovvkf36ujwt2396Z 3.2-pmem-support]$
[ali-cloud@iZbp16jovvkf36ujwt2396Z 3.2-pmem-support]$ make distclean;make -j 50

6. memKeyDB测试

  1. 测试memKeyDB DRAM和持久内存1:8
[ali-cloud@iZbp16jovvkf36ujwt2396Z memKeyDB]$ sudo numactl -C 0 /usr/local/bin/redis-server ./redis.conf --memory-alloc-policy ratio --dram-pmem-ratio 1 8
[root@iZbp16jovvkf36ujwt2396Z ~]# numactl -C 26 /usr/local/bin/redis-benchmark -t "lpush" -r 10000000 -n 4000000 -c 16 -d 64                    
====== LPUSH ======
4000000 requests completed in 36.78 seconds
16 parallel clients
64 bytes payload
keep alive: 1

100.00% <= 1 milliseconds
100.00% <= 2 milliseconds
100.00% <= 2 milliseconds
108766.59 requests per second

如果你的键值都比较小,难以达到所配置的比列,可以使用下面的option "--hashtable-on-dram no" 来保证目标比列的达成。
[ali-cloud@iZbp16jovvkf36ujwt2396Z memKeyDB]$ sudo numactl -C 0 /usr/local/bin/redis-server ./redis.conf --memory-alloc-policy ratio --dram-pmem-ratio 1 8 --dynamic-threshold-min 10 --hashtable-on-dram no
[root@iZbp16jovvkf36ujwt2396Z ~]# numactl -C 26 /usr/local/bin/redis-benchmark -t "lpush" -r 10000000 -n 4000000 -c 16 -d 64
====== LPUSH ======
  4000000 requests completed in 37.17 seconds
  16 parallel clients
  64 bytes payload
  keep alive: 1

100.00% <= 1 milliseconds
100.00% <= 1 milliseconds
107628.14 requests per second
  1. 测试memKeyDB, 所有数据都放在DRAM中 (只是为了对比测试)
[ali-cloud@iZbp16jovvkf36ujwt2396Z memKeyDB]$ sudo numactl -C 0 /usr/local/bin/redis-server ./redis.conf --memory-alloc-policy only-dram
[root@iZbp16jovvkf36ujwt2396Z ~]# numactl -C 26 /usr/local/bin/redis-benchmark -t "lpush" -r 10000000 -n 4000000 -c 16 -d 64
====== LPUSH ======
  4000000 requests completed in 36.81 seconds
  16 parallel clients
  64 bytes payload
  keep alive: 1

100.00% <= 1 milliseconds
100.00% <= 1 milliseconds
108666.12 requests per second
  1. 测试脚本,测试脚本可以根据需求和硬件配置进行修改
#服务器端配置 Server configuration
rm *.rdb -rf
port=6379
for i in `seq 0 103`
do
numactl -C $i /usr/local/bin/redis-server /home/ali-cloud/memKeyDB/redis.conf --memory-alloc-policy ratio --dram-pmem-ratio 1 8 --dynamic-threshold-min 10 --hashtable-on-dram no --port $port  --daemonize yes --logfile /tmp/redis_${port}.log --protected-mode no --bind 0.0.0.0 >/dev/null 2>&1 &
port=$((port+1))
done

## 客户端配置client configuration

[root@iZbp16jovvkf36ujwt2397Z ~]# yum install redis
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile
Package redis-3.2.12-2.el7.x86_64 already installed and latest version
Nothing to do

killall redis-benchmark
pmem_dram=ratio8
server_ip=172.16.79.125
port=6379

for i in `seq 0 103`
do
numactl -C $i /usr/bin/redis-benchmark -h ${server_ip}  -p ${port} -t "lpush" -r 10000000 -n 4000000 -c 16 -d 64 >/dev/null 2>&1 &
port=$((port+1))
done

## wait al the 103 benchmark finished. 

while [ $(ps -ef | grep -c redis-benchmark) -gt 1 ];do
echo -e "\e[36mWaiting $(($(ps -ef | grep -c redis-benchmark)-1)) benchmark finish... \e[0m"
sleep 5
done

cases=(get set sadd)

for wl in ${cases[@]}
do

mkdir $wl
port=6379
for i in `seq 0 103`
do
numactl -C $i /usr/bin/redis-benchmark -h ${server_ip}  -p ${port} -t $wl -r 10000000 -n 4000000 -c 16 -d 32 >> ${wl}/${pmem_dram}_${port}.redis &
port=$((port+1))
done

## wait al the 103 benchmark finished. 

while [ $(ps -ef | grep -c redis-benchmark) -gt 1 ];do
echo -e "\e[36mWaiting $(($(ps -ef | grep -c redis-benchmark)-1)) benchmark finish... \e[0m"
sleep 5
done
done

4. 设计目标

在这个设计中,我们期望在25G~40G网络的情况下,整体的性能在各种数据类型上, 吞吐可以达到 >90%, 延时不超过120%。

5. 设计实现

在redis的config中增加对于DRAM和持久内存的比例的设定。