八) redis 3中的内存碎片问题以及如何解决

部分参考下列链接
https://my.oschina.net/u/3387961/blog/1844450
https://zhuanlan.zhihu.com/p/38079073

1. redis内存统计
通过info (可以单独加上memory仅保留memory相关结果)
在这里插入图片描述
可以得到几个指标,我们最关注这四个:

  • used_memory: 从redis角度出发看到的内存消耗(包括redis真实内存消耗+swap分区消耗)
  • used_memory_rss :从操作系统层面看到的总共为redis分配了多少内存
  • mem_fragmentation_ratio: 内存碎片化率,它是used_memory_rss/used_memory,也就是OS分配的总内存除以redis使用的内存(含swap)。
    这个比值即可以大于1,也可以小于1。大于1是因为内存碎片问题,导致redis对内存使用效率偏低,从而OS分配的内存比较高;小于1则是因为内存不足时分配了swap分区,这是从磁盘上抽象出来的,性能比较差,也会影响redis性能。
    正常来说,理想的比值是1.03,或者是1~1.5
  • mem_allocator:redis内存分配器,默认是jemalloc,它在减少内存碎片方面做的相对比较好。
    在64位OS中,它将内存空间分为小、大、巨大三个范围,每个范围内又划分了多个小的内存块单位。当redis存储数据时,会选择大小最合适的内存块进行存储。
    在这里插入图片描述
    例如,要存储大小130bytes的对象,jemalloc会给他分片一个160字节的内存单元中。

2. redis内存模型
redis和所有应用一样,其内存分配都是要由OS来管理。用户增加K-V对象,redis就会向OS申请内存划分到应用;反之,用户移除K-V时,redis会向OS返回内存。但是这里的返回并不是及时的,是由redis自己的内存分配器进行控制的(也就是说例如一个redis实例有5G数据,移除了2G数据后,其占用内存扔会保持在5G左右)。

这样做的目的是redis希望自己控制内存向VM(虚拟内存,磁盘的某个特定分区或文件swap文件)的进度,因为释放内存交换到VM是个相对耗时的操作,所以redis希望自己控制进程,从而提高效率。这样这个不用的内存,可以由redis决定来分配给后续的其他新的K-V对象。

但是前面提到,jemalloc会给每个对象分配一个偏大的最接近的内存单元。那么当redis的对象变化剧烈,并且每次修改后的value的大小和之前的value差异很大的时候,就不能使用原有value的空间,而是要分配一个新的空间,而原有的空间也不会马上释放。因此,就有会一部分额外的空间被占用了,产生了内存碎片,这一部分不会体现在used_memory中,但是会体现在used_memory_rss中,故就会体现出内存碎片率大于1。

解决方案有2个:
1)重启redis服务,重启后redis会从备份文件中读取数据,在内存中进行重排,此时会为每个数据重新选择合适的内存单元,从而减少内存碎片;
2)redis 4之后提供了相关的指令来手动回收内存碎片(包括一个自动回收内存碎片的命令),这个配合监控(监控内存碎片率)使用效果更佳。

自动回收内存碎片
$ redis-cli -p 6383 config set activedefrag yes

https://blog.csdn.net/varyall/article/details/81510915
手工回收
$ redis-cli -p 6383 memory purge

3 redis 数据存储的细节
Redis数据存储的细节,涉及到内存分配器(如jemalloc)、简单动态字符串(SDS)、5种对象类型以及内部编码、redisObject。以执行一条命令set hello world为例:

1)综述
结合前面提到的redis存储结构,是在一个字典dict中维护了两个哈希表ht(一个正常用,一个扩容时辅助使用),每个哈希表中类似于HashMap,是一个数组链表,链表中每个节点都是一个dictEntry。
在这里插入图片描述
上图中最后面的节点dictEntry它的存储结构详细如下
在这里插入图片描述
1)dictEntry对应实际的K-V键值对,里面存储了指向key和value的指针,next则指向下一个dictEntry。
2)key:该例中就是“hello”,注意key并不是以字符串来存储的,而是存储在SDS中;
3)value:value既不是以字符串存储,也不是SDS,而是存储在一个redisObject中。实际上,不管value是那种类型(比如常见的5种基础类型),都是通过redisObject来存储的。在redisObject中,通过type来制定类型,ptr则指向实际对象的地址。不过对于本例中的value是一个字符串world,它经过redisObject做了包装,但是本质还是通过了SDS来最终存储。RedisObject中还会制定对象的编码方式(每个类型都有2种以上的编码方式)
4)jemalloc:上述的几种对象都是通过这种内存分配器来进行内存分配的,实际上是分配了一个比它大的最接近的一个设定值。

2)redisObject
Redis对象有5种类型;无论是哪种类型,Redis都不会直接存储,而是通过redisObject对象进行存储。
redisObject对象非常重要,Redis对象的类型、内部编码、内存回收、共享对象等功能,都需要redisObject支持,下面将通过redisObject的结构来说明它是如何起作用的。
redisObject的定义如下(不同版本的Redis可能稍稍有所不同):

扫描二维码关注公众号,回复: 5321717 查看本文章
typedef struct redisObject {
  unsigned type:4;
  unsigned encoding:4;
  unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
int refcount;
void *ptr;
} robj;

Type就是常见的5种类型(字符串、hash、list双向链表、set集合、sortSet带score的有序集合)

通过encoding属性,Redis可以根据不同的使用场景来为对象设置不同的编码,大大提高了Redis的灵活性和效率。以列表对象为例,有压缩列表和双端链表两种编码方式;如果列表中的元素较少,Redis倾向于使用压缩列表进行存储,因为压缩列表占用内存更少,而且比双端链表可以更快载入;当列表对象元素较多时,压缩列表就会转化为更适合存储大量元素的双端链表。可以根据不同的应用场景切换内部编码,提高效率。而且接口与实现分离,当需要增加或改变内部编码时,用户使用不受影响。

refcount记录的是该对象被引用的次数,类型为整型。refcount的作用,主要在于对象的引用计数和内存回收。当创建新对象时,refcount初始化为1;当有新程序使用该对象时,refcount加1;当对象不再被一个新程序使用时,refcount减1;当refcount变为0时,对象占用的内存会被释放。
注意每个redisObject都维护了一个refCount值,是引用计数,通过该值可以确定是否需要回收该redisObject对象。

lru记录的是对象最后一次被命令程序访问的时间
ptr指针指向具体的数据,如前面的例子中,set hello world,ptr指向包含字符串world的SDS。

猜你喜欢

转载自blog.csdn.net/xiaohesdu/article/details/87906380