Redis核心技术与实战【学习笔记】 - 12.Redis删除数据后,为什么内存占用率还是很高?

前言

在使用 Redis 是,经常会遇到一个问题:明明做了数据删除,数据量不大,但是 使用 top 命令查看时,发现 Redis 还是占用了很多内存。

这是因为,当删除数据后,Redis 释放的内存空间会由内存分配器管理,并不会立即返回操作系统,所以,操作系统操作系统仍然会记录这给 Redis 分配了大量内存。

这往往会伴随一个潜在的风险点: Redis 释放的内存空间可能并不是联系的,那么,这些不连续的内存空间可能处于一种闲置的状态。这会导致一个问题:虽然有空闲时间,Redis 却无法用来保存数据。

所以,本章就聊聊 Redis 的内存空间存储效率问题,为什么数据明明已经删除了,但内存却闲置着没有用,以及相应的解决办法。


1. 内存碎片

通常,内存空间闲置是因为操作系统发生了严重的内存碎片。

那么,什么是内存碎片?

为方便理解,举个高铁的车厢座位的例子。假设一节车厢的座位共有 60 个,现在已经卖了 57 张票,有 3 个朋友要一起乘坐高铁出行,刚好需要三张连载一起的票。但是,在选座位时,发现已经买不到连续的座位了。于是,你们只好换了一趟车。这样一来,这 3 个朋友就要改变出行时间,而且这趟车就空置了三个座位。

其实,这趟车的空座和朋友们的人数是匹配的,只是这些空座位是分散的,如下所示:

我们把这些分散的空座位叫作"车厢座位碎片"。基于上面的例子,操作系统的内存碎片就很容易理解了。虽然,操作系统的剩余内存空间总量足够,但是应用申请的是一块连续地址空间的 N 字节,但是剩余内存中,没有大小为 N 字节的连续空间了,那么,这些剩余的空间就是内存碎片。

2.Redis的内存碎片是如何形成的?

其实,内存碎片的形成有内因和外因两个层面的原因。

简单来说,内因是操作系统的内存分配机制,外因是 Redis 的负载特征。

2.1 内因:内存分配器的分配策略

内存分配器的分配策略就决定了操作系统无法做到"按需分配"。这是因为,内存分配器一般是按固定大小来分配内存,而不是完全按照应用程序申请的内存空间大小来进行分配。

Redis 可以使用 libc、jemalloc、tcmalloc 多种内存分配器来分配内存,默认使用 jemelloc。下面我们就解释下 jemalloc 的分配策略和问题,其他分配器也存在类似的问题。

jemalloc 的分配策略是按照一系列固定的大小分配内存空间,例如 8 字节、16 字节、32 字节、...、2KB、4KB、8KB 等等。当程序申请的内存最近接某个固定值时,jemalloc 会给它分配相应大小的空间。

这样的分配方式本身是为了减少分配的次数。例如,Redis 申请一个 20 字节的空间报错数据,jemalloc 就会分配 32 字节,此时,如果应用还要写入 10 字节数据,Redis 就不用再向操作系统申请空间了,这就避免了一次分配的操作。

但是,如果 Redis 每次向分配器申请的内存空间大小不一样,这种分配方式就会有形成碎片的风险,而这正好来源于 Redis 的外因了。

2.2 外因:键值对大小不一样和删改操作

Redis 通常作为共有的缓存系统或键值数据库对外提供服务,所以,不同业务应用的数据都可能保存在 Redis 中,这就会带来不同大小的键值对。这样一来,Redis 申请内存空间分配时,本身就会有大小不一的空间需求。这是第一个外因。

上面刚刚讲过,内存分配器只能按固定大小分配内存,所以分配的空间一般都会比申请的空间大,不会完全一致,这本身就会造成一定的内存碎片,降低内存空间存储效率。

比如说,应用 A 保存 6 字节数据,jemalloc 按分配策略会分配 8 字节。如果应用 A 不再保存新数据,那么,这里多出来的 2 字节空间就是内存碎片了。

第二个外因是,这些键值对会被修改和删除,这会导致空间的扩容和释放。具体来说,一方面,如果修改后的键值对变大或变小了,就需要占用额外的空间或者释放不用的空间。另一方面,删除的键值对就不再需要内存空间了,此时,就会把空间释放出来,形成空闲空间。

  • 一开始,应用 A、B、C、D 分别保存了 3、1、2、4 字节的数据,并占据了相应的内存空间。
  • 然后,应用 D 删除了 1 字节,这 1 字节的内存空间就空出来了。
  • 紧接着,应用 A 修改了数据,从 3 字节变为 4 字节。为了保持 A 数据空间的连续性,操作系统就把 B 的数据拷贝到别的空间,比如拷贝到 D 刚刚是否的空间中。
  • 此时,应用 C 和 D 也分别删除了 2 字节和 1 字节的数据,整个内存空间上就分别出现了 2 字节和 1 字节的空闲碎片。
  • 如果 E 想要一个 3 字节的连续空间,显然是不能满足的。因为虽然空间总量足够,但是确实碎片空间,并不是连续的。

好了,我们知道了造成内存碎片的内因和外因,其中内存分配器的策略是内因,而 Redis 的负载属于外因,包括了大小不一的键值对和键值对修改删除带来的内存空间变化。

大量的内存碎片会造成 Redis 的内存实际利用率变低,接下来,我们就要来解决这个问题了。不过在解决这个问题之前,还要判断 Redis 在运行过程中是否存在内存碎片。

3.如何判断是否有内存碎片?

为了能让用户监测到实时内存使用情况,Redis 自身提供了 INFO 命令,可以用来查询内存使用的详细信息,命令如下:

bash 复制代码
127.0.0.1:6379> INFO memory
# Memory
used_memory:1073741736
used_memory_human:1024.00M
used_memory_rss:1997159792
used_memory_rss_human:1.86G
...
mem_fragmentation_ratio:1.86

这里有这么一个参数 mem_fragmentation_ratio 的指标,它表示 Redis 当前的内存碎片率。那么,这个内存碎片率是怎么计算的?

bash 复制代码
mem_fragmentation_ratio = used_memory_rss / used_memory
  • used_memory_rss:操作系统实际分配给 Redis 的物理内存空间,里面就包含了内存碎片。
  • used_memory:是 Redis 为保持数据实际申请使用的空间地址。

例如,Redis 申请使用了 100 字节(used_memory),操作系统实际分配了 128 字节(used_memory_rss ),此时 mem_fragmentation_ratio 就是 1.28。

知道了这个指标,我们该如何使用?

  • mem_fragmentation_ratio 大于 1 但小于 1.5:这种情况是合理的。这是因为刚才介绍的因素是难以避免的。毕竟,内因的分配器是一定要使用的,分配策略是通用的,不会轻易修改;而外因由 Redis 负载决定,也无法限制。所以,存在内存碎片也是正常的。
  • mem_fragmentation_ratio 大于 1.5:这种表明内存碎片率已经超过了 50%。一般情况下,这个时候,我们就需要采取一些措施来降低内存碎片了。

4.如何清理内存碎片?

Redis 发送内存碎片后,一个"简单粗暴"的方法就是重启 Redis 实例。当然,这并不是一个"优雅"的方法,毕竟,重启 Redis 会带来两个后果:

  • 如果 Redis 中的数据没有持久化,那么数据就会丢失
  • 即使 Reids 持久化了,还需要通过 AOF 或 RDB 进行恢复,恢复时长取决于 AOF 或 RDB 的大小,如果只有一个 Redis 实例,恢复阶段无法提供服务。

幸运的是,从 Redis 4.0.3 版本以后,Redis 自身提供了一种内存碎片自动清理的方法,我们先来看下这个方法的基本原理。

操作系统:碎片清理的基本原理

内存碎片清理,简单来说,就是"搬家让位,合并空间"。比如说,刚刚的高铁车厢选座的例子,你和小家伙不想耽误时间,所以直接买了作为不在一起的三张票。但是,上车后,你和小伙伴通过和别人调换作为,又坐在一起了。

例如,在碎片清理前,这段 10 字节的空间中分别有 1 个 2 字节和 1 个 1 字节的空闲空间,只是这两个空间并不连续。操作系统在清理碎片时,会先把应用 D 的数据拷贝到 2 字节的空闲空间中,并释放 D 原来占用的空间。然后,再把 B 的数据拷贝到 D 原来的空间中。这样一来,这段 10 字节空间的最后三个字节就是一块连续的空间了。到这里,碎片清理结束。

不过,需要注意,碎片清理是有代价的,操作系统需要把多分数据拷贝到新位置,把原有空间释放出来,这会带来时间开销。因为 Redis 是单线程,在数据拷贝时,Redis 只能等着,这就导致 Redis 无法及时处理请求,性能就会降低。而且,有时候,数据拷贝还需要注意顺序,就像刚刚说的清理内存碎片的例子,操作系统需要先拷贝 D,并释放 D 的空间后,才能拷贝 B。这种对顺序性的要求,会进一步增加 Redis 的等待时间,导致性能下降。

有什么办法可以尽量缓解这个问题吗? 这就要提到,Redis 专门为自动内存碎片清理机制设置的参数了。我们可以通过设置参数,来控制内存碎片清理的开始和结束时机,以及占用的 CPU 比例,从而减少碎片清理对 Redis 本身请求处理的性能影响。

Redis:内存碎片清理

首先,Redis 需要启用自用内存碎片清理,可以把 activedefrag 配置项设置为 yes,命令如下:

bash 复制代码
127.0.0.1:6379> config set activedefrag yes
OK

这个命令只是启动了自动清理功能,但是具体什么时候清理,会受到下面这两个参数的控制。这两个参数分别设置了触发内存清理的条件,如果同时满足这两个条件,就开始清理。在清理过程中,只要有一个条件不满足了,就停止自动清理。

  • active-defrag-ignore-bytes 100mb:表示内存碎片的字节数达到 100 MB 时,开始清理;
  • active-defrag-threshold-lower 10:表示内存碎片空间占操作系统分配给 Redis 的总空间的比例达到 10% 时,开始清理。
bash 复制代码
127.0.0.1:6379> config set active-defrag-ignore-bytes 100mb
OK
127.0.0.1:6379> config set active-defrag-threshold-lower 10
OK

为了尽可能减少碎片清理对 Redis 正常请求处理的影响,自动内存碎片清理功能在执行时,还会监控清理操作占用的 CPU 时间,而且还设置了两个参数,分别用于控制清理操作占用的 CPU 时间比例的上、下限,既保证清理工作能正常进行,又避免降低 Redis 性能。这两个参数具体如下:

  • active-defrag-cycle-min 25:表示自动清理过程所用 CPU 的时间比例不低于 25%,保证清理能正常清理。
  • active-defrag-cycle-max 75:表示自动清理过程所用 CPU 的时间比例不高于 75%,一旦超过,就停止清理,从而避免在清理时,大量的内存拷贝阻塞 Redis,导致响应延迟高。
bash 复制代码
127.0.0.1:6379> config set active-defrag-cycle-min 25
OK
127.0.0.1:6379> config set active-defrag-cycle-max 75
OK

自动内存碎片清理机制在控制碎片清理启停的时机上,既考虑了碎片的空间占比、对 Redis 内存使用效率的影响,还考虑了清理机制本身的 CPU 时间占比、对 Redis 性能的影响。而且,清理机制还提供了 4 个参数,让我们可以根据实际应用中的数据量需求和性能,灵活使用。

相关推荐
fanTuanye30 分钟前
redis 缓存穿透,缓存击穿,缓存雪崩
java·redis·缓存
星星点点洲8 小时前
【Redis】谈谈Redis的设计
数据库·redis·缓存
Lion Long12 小时前
CodeBuddy 中国版 Cursor 实战:Redis+MySQL双引擎驱动〈王者荣耀〉战区排行榜
数据库·redis·mysql·缓存·腾讯云·codebuddy首席试玩官·codebuddy
柯南二号20 小时前
MacOS 用brew 安装、配置、启动Redis
redis
星星点点洲1 天前
【Redis】RedLock实现原理
redis·缓存
我来整一篇1 天前
用Redis的List实现消息队列
数据库·redis·list
加什么瓦1 天前
Redis——数据结构
数据库·redis·缓存
lybugproducer1 天前
浅谈 Redis 数据类型
java·数据库·redis·后端·链表·缓存
青山是哪个青山1 天前
Redis 常见数据类型
数据库·redis·bootstrap