Redis 性能影响 - 内存碎片和缓冲区

  • 一. 内存碎片带来的性能影响
  • 1.1 内存碎片的形成
  • 1.2 清理内存碎片
  • 1.3 总结
  • 二. 内存缓冲区溢出问题
  • 2.1 客户端通信中的缓冲区
  • 2.1.1 输入缓冲区溢出和避免
  • 2.1.2 输出缓冲区溢出和避免
  • 2.2 主从集群中的缓冲区
  • 2.2.1 复制缓冲区溢出和避免
  • 2.2.2 复制积压缓冲区溢出和避免
  • 2.3 总结


一. 内存碎片带来的性能影响

首先,我们需要明确并且知道一点:Redis中删除某个数据之后,实际上是将Redis 释放的内存空间会由内存分配器管理,并不会立即返回给操作系统。因此在一定的时间操作系统仍然记录着这个删除的key占用的内存。

问题:Redis释放的内存不一定是连续的,因此这种不连续的内存空间就有可能无法拿来保存数据。那么就会变相的减少Redis能够实际保存的数据量

换句话说:应用程序申请的是一块连续的内存地址,但是实际上机器提供的却是内存碎片。

1.1 内存碎片的形成

内存碎片形成的原因主要有2点:

  • 内因:操作系统的内存分配机制。
  • 外因:Redis的负载特征造成。

首先来说下内因:内存分配器会按照固定大小来分配内存,而不是按需分配。例如Linux下默认是4KB,开启内存大页机制后就变成2MB

Redis中使用jemalloc分配器来分配内存。它会按照一系列固定大小的内存来进行分配。例如当Redis中需要申请一个20B大小的空间来保存数据,那么jemalloc分配器就会分配32B

  • 倘若此时应用还要写入5B大小的数据,那么无需申请额外的空间。
  • 倘若此时应用还要写入20B大小的数据,那么必须在申请额外的空间了,此时就会有产生内存碎片的风险(之前分配的32B中,10B就是内存碎片了)

如图:

redis内存碎片率 redis 内存碎片_redis


紧接着就是外因部分了:

首先我们一个Redis实例,里面有着不同大小的键值对,那么根据内存分配器的分配机制来看。就有可能分配着不同大小的连续内存空间。

另一方面,我们对键值对也有可能有着不同的操作,增删改查。那么看下这个图:

redis内存碎片率 redis 内存碎片_Redis_02


上图中,白色部分的就是内存碎片,可以看出大小不一的键值对以及修改删除操作导致产生了内存碎片。

1.2 清理内存碎片

清理内存碎片之前,首先应该做的就是判断是否有内存碎片:

info memory

结果如下:

redis内存碎片率 redis 内存碎片_redis_03


请看我红色框框圈起来的地方:

mem_fragmentation_ratio:3.22

mem_fragmentation_ratio代表Redis实例当前的内存碎片率。其计算公式为:

mem_fragmentation_ratio = used_memory_rss / used_memory
  • used_memory_rss:操作系统实际分配Redis的物理内存空间。
  • used_memoryRedis为了保存数据而实际申请的空间。

针对mem_fragmentation_ratio,有两个参考:

  • mem_fragmentation_ratio ∈ (1, 1.5]:属于合理范围内,暂时可以放放。
  • mem_fragmentation_ratio ∈ (1.5, +∞)表明内存碎片率超过了50%,需要采取措施降低内存碎片率。

那么如何清理内存碎片呢(一般不会重启实例,因为生产上往往不允许这种神操作出现),在Redis4.0-RC3 版本以后,Redis提供了内置的内存碎片清理机制。

# 开启自动内存碎片清理功能
config set activedefrag yes

开启自动清理机制之后,需要同时满足两个条件才可以触发执行:

  • active-defrag-ignore-bytes 100mb:表示内存碎片的字节数达到 100MB 时,开始清理。
  • active-defrag-threshold-lower 10:表示内存碎片空间占操作系统分配给 Redis 的总空间比例达到 10% 时,开始清理。

redis内存碎片率 redis 内存碎片_Redis_04

除此之外,值得注意的是,虽然Redis提供了这样的自动内存清理机制,能够带来清理内存碎片的好处,但是与此同时的必定有着其对应的牺牲,也就是性能影响问题:

  • 碎片清理是有代价的:操作系统需要把多份数据拷贝到新位置,把原有空间释放出来,这会带来时间开销
  • Redis 是单线程,在数据拷贝时,Redis 进入阻塞状态,从而导致 Redis 无法及时处理请求,性能就会降低。

因此在开启自动清理内存碎片机制的情况下,需要合理考虑得失。Redis就提供了另外的两个参数:控制清理操作占用的 CPU 时间比例的上下限。
active-defrag-cycle-min 25: 表示自动清理过程所用 CPU 时间的比例不低于 25%,保证清理能正常开展。
active-defrag-cycle-max 75:表示自动清理过程所用 CPU 时间的比例不高于 75%一旦超过,就停止清理。

上述4个相关的配置如下(我的机器):

redis内存碎片率 redis 内存碎片_java_05

问题来了,如果mem_fragmentation_ratio的值小于1,代表什么?

前面我们知道,着个指标的计算用大白话来说就是 总物理内存 / 实际数据所需内存。小于1,代表总物理内存不够数据存储了,那不就开启swap机制了吗!

  1. Redis没有足够的物理内存可以使用,这会导致Redis一部分内存数据会被换到Swap中。
  2. 那么之后当Redis访问Swap中的数据时,延迟会变大,性能下降。

那可不行,那如果你的Redis实例中,这个指标小于1,赶紧加大内存,或者考虑搞成Redis集群!

1.3 总结

到这里为止,文章中个人认为最重要的几个点是:

  1. 通过info memory命令查看内存的使用情况。
  2. mem_fragmentation_ratio,如果超过了1.5,建议可以考虑进行内存碎片的清理了。
  3. 内存碎片的清理可以开启自动清理内存碎片机制,是主线程执行的会发生阻塞。需要合理配置对应的参数,保证Redis的高性能。
  4. mem_fragmentation_ratio如果值小于1,说明物理内存不够真实数据的保存了,此时机器应该开启了swap机制,会导致Redis性能的严重下降。应该考虑增加机器的内存配置了,或者搞集群。

二. 内存缓冲区溢出问题

背景:客户端和服务端之间,有一个输入和输出缓冲区。主要用来避免请求或者数据丢失。在Redis中主要有两个应用场景:

  • 就是在客户端和服务器端之间进行通信时:用来暂存客户端发送的命令数据,或者是服务器端返回给客户端的数据结果。
  • 主从节点间进行数据同步时:用来暂存主节点接收的写命令和数据。

服务器会和每个连接的客户端都设置一个输入输出缓冲区,大概图如下:

redis内存碎片率 redis 内存碎片_java_06

2.1 客户端通信中的缓冲区

缓冲区的功能我们说过:主要就是用一块内存空间来暂时存放命令数据。

那么什么是缓冲区溢出?

倘若如果往里面写入数据的速度持续地大于从里面读取数据的速度,就会导致缓冲区需要越来越多的内存来暂存数据。当缓冲区占用的内存超出了设定的上限阈值时,就会出现缓冲区溢出。

2.1.1 输入缓冲区溢出和避免

导致输入缓冲区溢出的情况主要分为两种:

  • 写入bigkey
  • 服务器端处理请求的速度太慢。比如Redis主线程出现了阻塞。

那么如何避免溢出呢?可以使用以下命令来查看服务端和客户端之间的输入缓冲区的使用情况:

client list

结果如下:

id=7392 addr=127.0.0.1:54854 laddr=127.0.0.1:6379 fd=9 name= age=5007 idle=0 flags=N db=0 sub=0 psub=0 
multi=-1 qbuf=26 qbuf-free=40928 argv-mem=10 obl=0 oll=0 omem=0 
tot-mem=61466 events=r cmd=client user=default redir=-1

图示:

redis内存碎片率 redis 内存碎片_java_07


其中比较重要的几个参数:

  • addr:不同客户端的IP和端口号。
  • qbuf:表示输入缓冲区已经使用的大小。单位字节。
  • cmd:客户端最新执行的命令。
  • qbuf-free:输入缓冲区剩余的可用空间大小。单位字节。

那么倘若 qbuf-free值太小,就需要引起注意,因为此时一旦写入大量命令,就容易引起缓冲区的溢出。

对于输入缓冲区。我们可以从两个方面去考虑避免溢出:

  • 输入缓冲区规定上限是1GB。因此其无法增大。该方案行不通。
  • 从命令和数据发送方考虑:避免客户端写入 bigkey,以及避免 Redis 主线程阻塞。

2.1.2 输出缓冲区溢出和避免

对于输出缓冲区,其主要分为两个部分:

  • 固定缓冲空间16KB,用于暂存OK响应和出错信息。例如:
  • redis内存碎片率 redis 内存碎片_缓存_08

  • 动态缓冲空间:用来暂存可以变的响应结果。例如:
  • redis内存碎片率 redis 内存碎片_缓存_09

输出缓冲区发生溢出主要有三种情况:

  • 服务器端返回bigkey的大量数据。
  • 执行了monitor命令。
  • 缓冲区大小设置不合理。

对于bigkey,无论是输入还是输出缓冲区,都会有很大的影响,而且Redis本身的性能也受其影响,bigkey相关内存的开辟和释放,都需要很大的资源去消耗。无论从各个方面来考虑,bigkey的存在都是不建议有的。

然后是monitor命令,我们来看下他是干啥的:

  1. 首先准备两个会话,会话1 输入monitor命令,进入监控。
  2. redis内存碎片率 redis 内存碎片_redis内存碎片率_10

  3. 另外一个 会话2 可以随便执行几个命令,如图:
  4. redis内存碎片率 redis 内存碎片_java_11

  5. 此时 会话1 的监控页面:
  6. redis内存碎片率 redis 内存碎片_缓存_12

  7. MONITOR 的输出结果会持续占用输出缓冲区,最后的结果就是发生溢出。 因此这个命令不要再生产上使用。

其次就是关于输出缓冲区大小的设置,这点和输入缓冲区并不相同,输入缓冲区的大小不可设置。而输出缓冲区可以。通过client-output-buffer-limit来配置。它主要有四个参数:

  • 客户端类型。
  • 缓冲区的大小。
  • 缓冲区持续写入的数据量限制。
  • 缓冲区持续写入的时间限制。

如果在持续写入的时间限制内写入的数据量超过了规定的量或者超出输出缓冲区规定的大小,则会关闭客户端连接。此时最好根据客户端的性质来具体配置:

倘若该客户端是主要进行读写命令交互的普通客户端:

# 主要进行读写命令交互的普通客户端 normal表示普通客户端,0代表不限制
client-output-buffer-limit normal 0 0 0

倘若该客户端是订阅客户端,即订阅了 Redis 频道的订阅客户端。

# pubsub代表订阅客户端
client-output-buffer-limit pubsub 8mb 2mb 60

上述配置代表:

  1. 实际占用缓冲区的大小 > 8MB ,服务端就会关闭与该客户端的连接。
  2. 60s内如果持续对输出缓冲区写入超过2MB的数据,同样关闭与客户端之间的连接。

2.2 主从集群中的缓冲区

主从集群之间主要的操作还是在于数据的复制,而数据的复制分为两种:

  • 全量复制。
  • 增量复制。

2.2.1 复制缓冲区溢出和避免

这一块主要发生在全量复制这个阶段,主库在向从库传输RDB文件的同时,会将客户端发送的写命令请求保存到复制缓冲区中。等待文件传输完毕,在发送给从库去执行。如图:

redis内存碎片率 redis 内存碎片_java_13

因此,倘若RDB传输过程比较久,同时传输期间主库又接收到大量的写命令,从而导致复制缓冲区中的命令越来越多,最后导致溢出。

解决:

  1. 控制主节点保存的数据大小。避免全量同步执行速度太慢。
  2. 使用 client-output-buffer-limit 命令,设置复制缓冲区大小。

我们可以看下复制缓冲区相关的配置:

config get client-output-buffer-limit

结果如下:

redis内存碎片率 redis 内存碎片_redis内存碎片率_14


还记得 2.1.2 小节中对于输出缓冲区的设置吗?我们可以发现用的是同一个命令,只不过有以下区别:

  • pubsub:就是订阅客户端。
  • normal:常规的客户观(进行简单的命令交互)。
  • slave该配置项就是针对复制缓冲区的。

那么设置的时候我们可以这么来:

# 设置的时候请加上slave,因为一共有三种,这里需要指定的是复制缓冲区的配置
# 复制缓冲区上限为512mb,超过了就断开连接
# 2分钟内写入的数据超过64mb,断开连接
config set client-output-buffer-limit slave 512mb 64mb 120

2.2.2 复制积压缓冲区溢出和避免

复制积压缓冲区(即repl_backlog_buffer,在Redis - Redis主从数据一致性和哨兵机制中有提到)主要作用于增量复制

  1. 主节点在把接收到的写命令同步给从节点时,同时会把这些写命令写入复制积压缓冲区。
  2. 一旦从节点发生宕机,当重启后,从节点就会从复制积压缓冲区中,读取断连期间主节点接收到的写命令,进而进行增量同步。

如图:

redis内存碎片率 redis 内存碎片_redis内存碎片率_15

因为在相关文章中讲到过了,这里就截个图:

redis内存碎片率 redis 内存碎片_Redis_16


因此可以适当调大一点这个值。

2.3 总结

文章提到了4个缓冲区。

  • 输入缓冲区:保存客户端发送的命令。
  • 输出缓冲区:保存服务端返回的数据。
  • 复制缓冲区:用于全量复制时,保存新写入的命令。
  • 复制积压缓冲区:用于增量复制时,保存新写入的命令

从文章的内容来看:

  1. 服务端和每个客户端之间都存在对应的几种缓冲区。
  2. 当发生了缓冲区溢出,服务端会断开与对应客户端之间的连接。
  3. 此时会导致朱从节点同步失败,或者程序无法读写Redis

缓冲区溢出,可以从三个方面来分别解决:

命令数据发送太快。

  1. 与普通客户端:避免bigkey
  2. 复制缓冲区:避免大型RDB文件的产生。

命令数据处理太慢:减少Redis主线程上的阻塞操作。可以有针对的利用异步机制。


缓冲区太小了:

  1. 使用 client-output-buffer-limit 进行相关配置,主要配置输出缓冲区和复制缓冲区。
  2. repl_backlog_buffer进行相关配置,增大复制积压缓冲区。
  3. 输入缓冲区大小无法修改。
  4. 避免在生产上使用monitor监控命令。