redis的内存

Redis Memory

Posted by zwtisme on September 12, 2018

介绍redis的内存相关优化。

内存消耗

1.内存使用统计

通过如下命令可获取redis实例的内存使用情况

127.0.0.1:6379> info memory
属性说明
used_memory:12597672 redis的当前内存
used_memory_human:12.01M
used_memory_rss:17149952 系统显示的redis进程内存
used_memory_rss_human:16.36M
used_memory_peak:14492352 redis的最大内存
used_memory_peak_human:13.82M
used_memory_peak_perc:86.93%
total_system_memory:4018454528 系统内存
total_system_memory_human:3.74G
used_memory_lua:37888 lua引擎内存
used_memory_lua_human:37.00K
maxmemory:1073741824 最大可用内存
maxmemory_human:1.00G
maxmemory_policy:noeviction
mem_fragmentation_ratio:1.36 内存碎片率
used_memory_rss/used_memory
mem_allocator:jemalloc-4.0.3 内存分配器

需要特别注意内存碎片率:

  • mem_fragmentation_ratio>1:说明存在内存碎片,如果比值很大则碎片率较严重,可重启实例解决
  • mem_fragmentation_ratio<1:说明redis内存已超过系统内存,产生了磁盘交换,会导致redis的性能大幅下降

2.内存使用划分

redis的内存使用主要包括:自身内存,对象内存,缓冲内存,内存碎片

自身内存(used_memory)

一个空的redis,used_memory为800k左右,used_memory_rss在2M左右,也就是说redis自身占用的内存很小

对象内存(used_memory)

用于存储所有的数据,包括key与value,是占用内存最大的

缓冲内存(used_memory)

缓存内存包括:客户端缓冲、复制积压缓冲区、AOF缓冲区

内存碎片(used_memory_rss-used_memory)

redis默认使用的是jemalloc内存分配器,在64位系统下,会按照如下规则进行分配内存:

  • 小:[8byte],[16byte,32byte,48byte,…,128byte],[192byte,256byte,…,512byte],[768byte,1024byte,…,3840byte]
  • 大:[4KB,8KB,12KB,…,4072KB]
  • 巨大:[4MB,8MB,12MB,…]

当为了存储5KB的对象时,会采用8KB的块存储,剩下的3KB就会变成碎片,而不能分配给其他对象使用。

下面的情况可能会出现较多的内存碎片:

  • 频繁的更新操作,对已存在的键做append,setrange等更新操作
  • 大量的键在过期后删除,释放的空间不能充分利用

可通过如下方式尝试解决:

  • 数据对齐:在条件允许的情况做数据对齐,比如尽量采用数字类型或固定长度的字符串
  • 安全重启:如果redis做了高可用,可重启碎片率较高的节点,注意保证服务的可用性

3.子进程内存消耗

子进程内存消耗主要是执行AOF/RDB重写时Redis创建的子进程内存消耗,理论上需要1倍的父进程内存来完成重写,但是因为Linux的写时复制技术(copy-on-write),子进程并不需要消耗1倍的父进程内存,但最好还是预留出一些内存(父进程内存的[0%-100%])用于子进程的重写操作,防止内存溢出。

如下设置可用于避免一些内存问题:

1.允许内核可以分配所有的物理内存,防止Redis进程执行fork时因系统剩余内存不足而失败

[root@DEV-HROEx64 mm]# sysctl vm.overcommit_memory=1
[root@DEV-HROEx64 redis]# vim /etc/sysctl.conf

2.关闭Linux的THP(Transparent Huge Pages)机制,此机制会导致copy-on-write期间复制内存页的单位从4KB变为2MB,如果父进程有大量写命令,会加重内存拷贝量,从而造成过度内存消耗

[root@DEV-HROEx64 mm]# echo never > /sys/kernel/mm/redhat_transparent_hugepage/enabled
[root@DEV-HROEx64 mm]# vim /sys/kernel/mm/redhat_transparent_hugepage

内存管理

1.服务器内存

一台单纯的redis服务器,内存使用主要包括如下几个部分:

  • linux系统本身(1G)
  • 其它一些服务进程(1G)
  • redis主进程,可能有多个实例(剩余内存/实例个数+0.5)
  • redis子进程(子进程执行过程中写入命令量,可粗略为单实例的50%)

上述数值只作为参考使用,具体配置需要结合实际情况。

Tips:如果单机部署多个实例,且实例都有持久化需求,需要避免多实例在同一时间进行持久化操作,防止内存溢出

2.内存上限

设置maxmemory的作用:

  • 当缓存使用的内存超过maxmemory时,触发内存回收策略释放空间
  • 防止内存超过物理内存,而产生磁盘交换,大幅降低redis的性能

根据1中的计算,可以得出可设置的maxmemory。

Tips:因为内存碎片的原因,此处的maxmemory=used_memory,而非used_memory_rss,所以要注意这部分是否会导致内存溢出

3.动态调整内存上限

使用如下命令,可用来为正在运行中的redis进程动态修改最大内存,以满足需求

127.0.0.1:6379> CONFIG set maxmemory 1G

4.内存回收策略

1.过期键删除
  • 惰性删除:当客户端读取带超时属性的键时,如果键已过期则进行删除并返回空
  • 定时任务删除:定时任务,默认每秒运行10次(config hz),回收过期键算法如下

image

慢模式与快模式的内部执行逻辑相同,只是执行超时时间不同,慢模式为25ms超时,快模式为1ms且2秒内只能运行一次。

2.内存溢出控制策略
  • noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息(error)OOM command not allowed when used memory,此时Redis只响应读操作
  • allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止
  • allkeys-random:随机删除所有键,直到腾出足够空间为止
  • volatile-lru:根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略
  • volatile-random:随机删除过期键,直到腾出足够空间为止
  • volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略

redis每次执行命令时都会尝试回收内存的操作,如果redis一直运行在(used_memory>maxmemory)且回收策略非(noeviction)的情况时,会频繁触发回收内存的操作,从而影响性能。

Tips:为了避免频繁回收内存产生的开销,redis最好工作在maxmemory>used_memory状态下

内存优化

1.redisObject对象

redis存储的数据都是使用redisObject对象进行封装的,包含如下属性:

type unsigned 4 0.5B 对象的数据类型 string、hash、list、set、zset
encoding unsigned 4 0.5B 对象的编码类型 对象在内部使用哪种数据结构
lru unsigned LRU_BITS(24) 3B LRU计时时钟 对象最后一次被访问的时间
refcount int 4B 引用计数器 对象被引用的次数,当为0时,可以安全回收当前对象空间
*ptr void(64位系统) 8B 数据指针 整数:直接存储数据
其它:指向数据的指针

Tips:当存储的值为字符串类型且长度<=44字节时,内部编码会使用embstr类型,字符串sds和redisObject一起分配,从而只要一次内存操作即可,在高并发常见如果能控制字符串长度可以提高性能

2.缩减键值对象

key

redis的key是使用string存储,需要占用存储空间,所以设计键时,在满足业务区分的情况下,尽量越短越好

value
  • 精简业务信息,去除不必要存储的数据
  • 将字符串进行压缩后存储

Tips:选用压缩工具时,需要综合考虑压缩速度和计算开销成本

3.共享对象池

redis内部维护了[0-9999]的整数对象池,当创建整数值对象或list、hash、set、zset的内部元素为整数值时可以直接使用对象池的整数对象,从而节约内存。

Tips:当设置maxmemory并启用LRU相关淘汰策略如:volatile-lru,allkeys-lru时,Redis禁止使用共享对象池;对于ziplist编码的值对象,也是不能使用共享对象池,因为ziplist使用压缩且内存连续,对象判断成本过高

为何只有整数对象池:

  • 复用几率大
  • 比较算法的时间复杂度为o(1),字符串为o(n),其他复杂数据结构如hash,list等需要o(n*n)

4.字符串优化

redis中所有键都是字符串类型,值对象除了整数都使用字符串存储。

字符串结构

redis的字符串结构为简单动态字符串(simple dynamic string),结构如下:

  • len[int]:已用字节长度
  • free[int]:未用字节长度
  • buf[][char]:字节数组

sds字符串结构有如下特点:

  • o(1)时间复杂度获取:字符串长度,已用长度,未用长度
  • 保存字节数组,支持安全的二进制数据存储
  • 内部实现空间预分配机制,降低内存再分配的频率
  • 惰性删除机制,字符串缩减后的空间不释放,作为预分配空间保留
预分配机制

当对字符串进行append,setrange等修改操作时,会触发预分配机制,避免不断的重分配内存和字节数据复制,但同时会造成内存的浪费。空间预分配规则如下:

  • 第一次创建len属性等于数据实际大小,free等于0,不做预分配
  • 修改后如果已有free空间不够且数据小于1M,每次预分配一倍容量。如原有len=60byte,free=0,再追加60byte,预分配120byte,总占用空间:60byte+60byte+120byte+1byte
  • 修改后如果已有free空间不够且数据大于1MB,每次预分配1MB数据。如原有len=10MB,free=0,当再追加100byte,预分配1MB,总占用空间:10MB+100byte+1MB+1byte

Tips:减少字符串的部分修改操作(append,setrange等),直接使用set来修改整个字符串,降低预分配带来的内存浪费与内存碎片率

字符串重构

不一定把每份数据作为字符串整体存储,像json这样的数据可以使用hash结构,使用二级结构存储也能帮我们节省内存。同时可以使用hmget、hmset命令支持字段的部分读取修改,而不用每次整体存取

5.编码优化

redis中类型与编码的对应关系如下:

类型 编码 数据结构 条件
string int 整数编码 8个字节的长整型
embstr 优化内存分配的字符串编码 小于等于44个字节的字符串
raw 动态字符串编码 大于44个字节的字符串
hash ziplist 压缩列表编码 field个数<=hash-max-ziplist-entries(默认512)
value最大空间(字节)<=hash-max-ziplist-value配置(默认64)
hashtable 散列表编码 不满足ziplist条件
list quicklist 快速列表编码
ziplist组成的双向链表
list-max-ziplist-size(正数):节点ziplist最多包含的entry个数
list-max-ziplist-size(负数[-5 -1]):节点ziplist的字节大小
list-compress-depth:压缩深度(默认0,不压缩)
set intset 整数集合编码 元素都是整数
集合长度<=set-max-intset-entries(默认512)
hashtable 散列表编码 不满足intset条件
zset ziplist 压缩列表编码 field个数<=zset-max-ziplist-entries(默认128)
value最大空间(字节)<=zset-max-ziplist-value配置(默认64)
skiplist 跳跃表编码 不满足ziplist条件时

在了解了内部编码后,就可以通过调节条件参数来让redis使用对应的编码,从而减少内存,不过调整也不是随意的,需要慎重考虑。

Tips:编码类型的转换是写入redis数据时自动进行的,且转换只能从小内存编码往大内存编码进行。

6.控制键的数量

当redis中存在大量的键时,也会消耗内存,可以根据业务情况,利用redis中hash等数据结构,进行二级存储,降低外层键数量,从而节省内存。

在进行此类优化时,需要注意如下情况:

  • hash编码必须调整为使用ziplist,ziplist长度控制在1000内,存储的对象需要为小对象,预估键规模设计hash的分组规模
  • hash重构后键不能使用超时(expire)与淘汰(lru)机制自动删除,需要手动维护

参考资料

redis开发与运维

linux写时复制

lru算法

redisObject

c语言指针占几个字节

quicklist

redis info信息