Redis消耗内存过高

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: Redis是一种内存数据库,读写效率要比将数据保存到磁盘上的数据库要快很多如果我们不了解Redis的内存回收策略,就可能导致系统内存过高甚至内存溢出,严重影响系统性能

1.案例现象

发现生产环境上的一台服务器出现内存使用率达到阈值的告警

登上机器先看一下系统整体使用的情况

top

通过 top 的输出发现:

  • 系统平均负载没有异常
  • 系统cpu使用率没有异常
  • 系统已使用的物理内存(used)数值特别高,达到了总物理内存的80%以上
  • 而且buffer/cache的数值也不小,这说明有应用产生了大量的读写缓存

光看系统资源整体使用情况不能精确的定位到问题

我们继续观察 top 输出,这次我们将重点放到了各个进程的资源使用情况

发现:

  • redis进程占用了最多的内存,达到了20G
  • redis进程的使用率也达到了90%以上

由 top 的输出我们不难发现,这台服务器上的 redis实例消耗了大量的内存,而且cpu使用率很高,应该是有应用往 redis 上进行大量的读写操作

2.定位问题

既然知道了是Redis消耗了大量的内存,我们首先查看一下redis的配置,看看配置层面有没有出现问题

cat /etc/redis.conf

与对部署相同服务的服务器redis配置文件比了一下,发现配置并没有什么问题

现在问题来了

  1. 是什么导致redis使用这么多内存
  2. 这些内存不会回收的吗

在回答这些问题时,我们先来了解一下Redis的内存回收策略

Redis内存回收淘汰策略

Redis是基于内存的数据库,常被用作缓存,以此来提高系统的响应速率与性能

Redis内存消耗

Redis进程的内存消耗主要包括:自身内存 + 对象内存 + 缓冲内存 + 内存碎片
image-20220919141527899.png

  • 自身内存

一般来讲,Redis空进程自身内存消耗非常少,通常 usedmemoryrss 在 3MB 左右时,used_memory 一般在 800KB 左右,一个空的 Redis 进程消耗内存可以忽略不计

  • 对象内存

Redis内存占用最大的一块,存储着用户所有的数据

对象内存消耗可以简单理解为这两个对象的内存消耗之和(还有类似过期之类的信息)

在使用 Redis 时很容易忽略键对内存消耗的影响,应当避免使用过长的键以及给键设置一个过期时间

  • 缓冲内存

主要包括客户端缓冲,复制积压缓冲和AOF缓冲

客户端缓冲指的是所有接入到 Redis 服务器 TCP 连接的输入输出缓冲

复制积压缓冲区是Redis 在 2.8 版本后提供的一个可重用的固定大小缓冲区,用于实现部分复制功能。根据 repl-backlog-size 参数控制,默认 1MB。对于复制积压缓冲区整个主节点只有一个,所有的从节点共享此缓冲区。因此可以设置较大的缓冲区空间,比如说 100MB,可以有效避免全量复制

AOF 重写缓冲区:这部分空间用于在 Redis AOF 重写期间保存最近的写入命令。AOF 重写缓冲区的大小用户无法控制,取决于 AOF 重写时间和写入命令量,不过一般都很小

  • 内存碎片

Redis 默认的内存分配器采用 jemalloc,可选的分配器还有:glibc、tcmalloc

内存分配器为了更好地管理和重复利用内存,分配内存策略一般采用固定范围的内存块进行分配

Redis 正常碎片率一般在 1.03 左右

以下场景容易出现高内存碎片问题:

Redis内部有自己的内存管理器,为了提高内存使用的效率,来对内存的申请和释放进行管理。
Redis中的值删除的时候,并没有把内存直接释放,交还给操作系统,而是交给了Redis内部有内存管理器。
这就使得如果大量的key在短时间内过期被删除,这些内存不会释放给操作系统,而是交给内部内存管理器,导致redis实际占用的内存与申请的内存相差过大,就会导致大量的内存碎片
  • 子进程内存消耗

子进程内存消耗主要指执行 AOF 重写 或者进行 RDB 保存时 Redis 创建的子进程内存消耗

Redis 内存相关的指标

我们可以在redis客户端上通过info memory 命令可以获得 Redis 内存相关的指标

属性名 属性说明
used_memory Redis分配器分配的内存总量,即内部存储的所有数据内存占用量
used_memory_human 以可读的格式返回used_memory
used_memory_rss 从操作系统的角度显示Redis进程占用的物理内存总量
used_memory_peak 内存使用的最大值,即used_memory的峰值
used_memory_peak_human 以可读的格式返回used_memory_peak
used_memory_lua Lua引擎所消耗的内存大小
mem_fragmentation_ratio 内存碎片率,一般在1.03左右
mem_allocator 在编译期redis使用的内存分配器
maxmemory Redis能够使用的最大内存上限,0表示没有限制 字节为单位
maxmemory_policy Redis使用的内存回收策略
  • mem_fragmentation_ratio

当该值 > 1时,说明有部分内存并没有用于数据存储,而是被内存碎片所消耗,如果该值很大,说明碎片率严重

当该值 < 1时,一般出现在操作系统把Redis swap 到硬盘导致,出现这种情况要格外关注,由于硬盘速度远远慢于内存,Redis 性能会变得很差,甚至僵死

建议要设置和内存一样大小的交换区,如果没有交换区,一旦 Redis 突然需要的内存大于当前操作系统可用内存时,Redis 会因为内存溢出而被内核的 OOM Killer 直接杀死

  • maxmemory

Redis 使用 maxmemory 参数限制最大可用内存。限制内存的目的主要有

  1. 用于缓存场景,当超出内存上限 maxmemory 时使用 LRU 等回收策略释放空间
  2. 防止所用的内存超过服务器物理内存,导致 OOM 后进程被系统杀死

maxmemory 限制的是 Redis 实际使用的内存量,也就是 used_memory 统计项对应的内存。实际消耗的内存可能会比 maxmemory 设置的大,要小心因为这部内存导致 OOM。所以,如果你有 10GB 的内存,最好将 maxmemory 设置为 8 或者 9G

  • maxmemory_policy

image-20220919153309057.png
Redis默认采用noeviction策略

volatile-lru:
#在设置了过期时间的所有键中,选取最近最少使用的数据删除。

volatile-lfu:
#在设置了过期时间的所有键中,选取最近最不常用,也就是一定时期内被访问次数最少的数据删除

volatile-random:
#筛选出设置了过期时间的键值对,随机删除。

volatile-ttl:
#筛选出设置了过期时间的键值对,越早过期的越先被删除。

allkeys-lru:
#在所有键中,选取最近最少使用的数据删除

allkeys-lfu:
#在所有键中,选取最近最不常用,也就是一定时期内被访问次数最少的数据删除

allkeys-random:
#采用随机淘汰策略删除所有的键值对,这个策略不常用。

noeviction:
#不淘汰任何键值对,当内存满时,如果进行读操作,例如get命令,它将正常工作,而做写操作,它将返回错误,也就是说,当Redis采用这个策略内存达到最大的时候,它就只能读不能写了

Redis键过期机制

出了上面提到的内存回收机制可以有效解决消耗内存过高的原因之外,Redis还有一个过期机制,可以给key设置一个过期时间,一旦超过过期时间,这个key就会被被删除,内存被回收

PS:上面是内存不足的「淘汰策略」,这一种是过期键的删除策略,两者是不同的,不要搞混了

  • 查看key的过期时间
#如果key存在过期时间,返回剩余生存时间(秒);如果key是永久的,返回-1;如果key不存在或者已过期,返回-2
#TTL单位是秒,PTTL单位是毫秒
127.0.0.1:6379> TTL KEY
127.0.0.1:6379> PTTL KEY
  • 设置过期时间
#设置一个key在当前时间"seconds"(秒)之后过期。返回1代表设置成功,返回0代表key不存在或者无法设置过期时间
#EXPIRE单位是秒,PEXPIRE单位是毫秒
EXPIRE key seconds

127.0.0.1:6379> EXPIRE name 60
(integer) 1
#设置一个key在"timestamp"(时间戳(秒))之后过期。返回1代表设置成功,返回0代表key不存在或者无法设置过期时间
#EXPIREAT单位是秒,PEXPIREAT单位是毫秒
EXPIREAT key "timestamp"

127.0.0.1:6379> EXPIREAT name 1586941008
(integer) 1
#SETEX在逻辑上等价于SET和EXPIRE合并的操作,区别之处在于SETEX是一条命令,而命令的执行是原子性的,所以不会出现并发问题
SETEX key "seconds" "value"

127.0.0.1:6379> SETEX name 100 jack
OK

Redis key过期处理

Redis key过期处理的方式有三种

  • 惰性删除

    不管键有没有过期都不主动删除,等到每次去获取键时再判断是否过期,如果过期就删除该键,否则返回键对应的值。这种策略对内存不够友好,可能会浪费很多内存

    缺点:若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露(无用的数据占用了大量的内存)

  • 定时删除

    在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除

    缺点:定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重,因为每个定时器都会占用一定的 CPU 资源

  • 定期删除

    系统每隔一段时间就定期扫描一次,发现过期的键就进行删除

    以下两种方式可以触发定期删除

    • 配置redis.conf 的hz选项,默认为10 (即1秒执行10次,100ms一次,值越大说明刷新频率越快,最Redis性能损耗也越大)
    • 配置内存回收策略,当Redis消耗内存达到最大内存使用限制,就会自行对应的策略,来对过期key进行删除

Redis 当中,其选择的是策略 2 和策略 3 的综合使用。不过 Redis 的定期删除只会扫描设置了过期时间的键,因为设置了过期时间的键 Redis 会单独存储,所以不会出现扫描所有键的情况

同一时间大量Key过期会有什么影响?

Redis 是单线程的,收割的时间也会占用线程的处理时间,如果收割的太过于繁忙,以至于忙不过来?会不会导致线上读写指令出现卡顿?

Redi将每个设置了过期时间的Key放入到一个独立的字典中,会定时遍历这个字典来删除,默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略

  1. 从过期字典中随机 20 个 key;
  2. 删除这 20 个 key 中已经过期的 key;
  3. 如果过期的 key 比率超过 1/4,那就重复步骤 1

为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms

#如果Redis 实例中所有的 key (几十万个)在同一时间过期会怎样?
Redis会持续扫描过期字典(循环),知道过期字典中的过期key变得稀疏,才会降低扫描次数
内存管理器需要频繁回收内存页,此时会产生一定的CPU消耗,必然会导致线上读写请求出现明显卡顿的现象

当客户端请求到来时(服务器如果正好进入过期扫描状态),请求将会至少等待25ms才会进行处理,入锅客户端将超时时间设置的比较短(10ms),那么就会出现大量的连接因为超时而关闭,业务端就会出现很多异常,而且这时你还无法从Redis的slowlog中看到慢查询记录

slave的过期策略

从库不会进行过期扫描,从库对过期的处理是被动的

当 master 采用定期或惰性删除过期键时,会同步一个del操作到 slave,这样从库也可以删除过期 key,但是 salve 从不会自己处理过期 key,只会应用 master 同步过来的del操作

也就是说即使键slave已经过期了,slave也不会自己处理过期后如果主库不同步DEL操作过来,那么从库并不会采用主动或惰性的方式去清理过期键

这样就会造成一个问题:

slave是提供读服务的,如果客户端在slave上读取了一个过期的key,而且master没有及时地处理,那么客户端仍能读取到

这个问题在Redis3.2以下会存在,但之后Redis进行了优化:如果客户端在slave读取到了过期的key,再发起读请求的时候,Redis会判断这个key是否过期,如果过期则返回nil

但是slave依旧不会对过期key进行任何处理,而是等待maser同步del操作

RDB对过期Key的处理

  • 持久化数据到RDB文件

    • 持久化之前会检查key是否过期,过期的key不进入RDB文件
  • 从RDB文件恢复数据

    • 数据载入数据库之前,会对key进行过期检查,如果过期则不导入数据库(主库)
    • 如果RDB文件里有过期的键,那还是会载入,但是主从在数据同步时(全量复制),slave的数据会被清空(丢弃原先所有数据),所以不影响

AOF对过期Key的处理

  • 持久化数据到 AOF 文件

    • 如果某个 key 过期,还没有被删除,该 key 是不会进入 aof 文件的,因为没有发生修改命令
    • 当 key 过期被删除后,就会向 aof 文件追加一条 del 命令(在将来的以 aof 文件恢复数据的时候该过期的键就会被删掉)
  • AOF重写

    • 重写时,会先判断 key 是否过期,已过期的 key不会重写到 aof 文 件

3.解决问题

了解了 Redis 的内存回收机制以及过期机制之后,我们分别来看一下

我们首先看一下任意 key 的过期时间是多少

#从当前数据库中随机返回一个 key
127.0.0.1:6379> RANDOMKEY 

127.0.0.1:6379> TTL key
(integer) 12032145

我们发现,key 的过期时间设置成了一千多万秒!这个过期时间也太长了吧

我们再看下 Redis 的内存回收策略

127.0.0.1:6379> info memory
maxmemory:0
maxmemory_policy:"noeviction"

可以看到,我们并没有设置内存最大限制,而且内存回收策略是 noeviction,即不淘汰任何键值对

到这一步就开始明朗起来了:

由于 key 的过期时间设置的太长,没有设置最大可用内存限制而且内存回收策略是 noeviction

就会使得原先的数据还没过期,又有新的数据写进来,导致消耗内存越来越多,而系统又无法进行回收

解决方法

  • 重新给键设置过期时间

这个不太现实,生产环境中有大量的 key,不可能说一个一个的重新设置

而且我们使用的是 docker 中的 redis,已经打包成一个容器,修改的话要花大量的精力和时间

  • 修改配置文件(推荐使用)

设置最大内存使用限制以及更改回收机制

我们修改redis的配置文件

vim /etc/redis.conf
maxmemory:10G
maxmemory_policy:"volatile-lru"

我们设置了最大内存使用限制为10G,一旦redis占用内存超过10GB,就会触发内存回收机制 volatile-lru——即在设置了过期时间的 key 里,删除最近最少使用的key

之后我们等待一段时间再看,发现不再告警了,使用内存也降下去了

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
28天前
|
NoSQL 算法 Redis
redis内存淘汰策略
Redis支持8种内存淘汰策略,包括noeviction、volatile-ttl、allkeys-random、volatile-random、allkeys-lru、volatile-lru、allkeys-lfu和volatile-lfu。这些策略分别针对所有键或仅设置TTL的键,采用随机、LRU(最近最久未使用)或LFU(最少频率使用)等算法进行淘汰。
41 5
|
2月前
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
|
3月前
|
存储 缓存 NoSQL
Redis Quicklist 竟让内存占用狂降50%?
【10月更文挑战第11天】
58 2
|
3月前
|
JavaScript 前端开发 安全
如何避免闭包带来的内存消耗呢
【10月更文挑战第12天】如何避免闭包带来的内存消耗呢
43 0
|
4月前
|
缓存 NoSQL 算法
14)Redis 在内存用完时会怎么办?如何处理已过期的数据?
14)Redis 在内存用完时会怎么办?如何处理已过期的数据?
84 0
|
4月前
|
存储 缓存 NoSQL
Redis 过期删除策略与内存淘汰策略的区别及常用命令解析
Redis 过期删除策略与内存淘汰策略的区别及常用命令解析
85 0
|
2月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
338 1
|
28天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
2月前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80
|
2月前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
25 3