赞
踩
我们知道,Redis是一个key-value数据库,它的数据是运行在内存中的
其读写效率比将数据存储到磁盘上的数据库要快很多
虽然性能强大,但是如果我们不了解Redis的内存回收策略,就有可能导致Redis消耗内存过高甚至导致内存溢出,严重影响系统性能
通过 top 的输出发现:
光看系统资源整体使用情况不能精确的定位到问题
我们继续观察 top 输出,这次我们将重点放到了各个进程的资源使用情况
发现:
由 top 的输出我们不难发现,这台服务器上的 redis实例消耗了大量的内存,而且cpu使用率很高,应该是有应用往 redis 上进行大量的读写操作
这台服务器上的 Redis 是运行在 docker 上的,我们进入到 redis 容器里面
docker exec -it redis /bin/bash
我们还可以使用如下命令来查看 redis 容器的一些详细信息
docker inspect redis
然后找到 Redis 的配置文件路径,检查一下是不是配置出了问题
与对部署相同服务的服务器redis配置文件对比了一下,发现配置并没有什么问题
不是配置问题,那到底是什么原因导致Redis占用了这么多内存?这些使用的内存Redis不会回收的吗?
在回答这些问题时,我们先来了解一下Redis的内存回收策略以及键过期机制
Redis是基于内存的数据库,常被用作缓存,以此来提高系统的响应速率与性能
Redis内存消耗
Redis进程的内存消耗主要包括:自身内存 + 对象内存 + 缓冲内存 + 内存碎片
一般来讲,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使用的内存回收策略 |
当该值 > 1时,说明有部分内存并没有用于数据存储,而是被内存碎片所消耗,如果该值很大,说明碎片率严重
当该值 < 1时,一般出现在操作系统把Redis swap 到硬盘导致,出现这种情况要格外关注,由于硬盘速度远远慢于内存,Redis 性能会变得很差,甚至僵死
建议要设置和内存一样大小的交换区,如果没有交换区,一旦 Redis 突然需要的内存大于当前操作系统可用内存时,Redis 会因为内存溢出而被内核的 OOM Killer 直接杀死
Redis 使用 maxmemory 参数限制最大可用内存。限制内存的目的主要有
maxmemory 限制的是 Redis 实际使用的内存量,也就是 used_memory 统计项对应的内存。实际消耗的内存可能会比 maxmemory 设置的大,要小心因为这部内存导致 OOM。所以,如果你有 10GB 的内存,最好将 maxmemory 设置为 8 或者 9G
volatile-lru:
#在设置了过期时间的所有键中,选取最近最少使用的数据删除。
volatile-lfu:
#在设置了过期时间的所有键中,选取最近最不常用,也就是一定时期内被访问次数最少的数据删除
volatile-random:
#筛选出设置了过期时间的键值对,随机删除。
volatile-ttl:
#筛选出设置了过期时间的键值对,越早过期的越先被删除。
allkeys-lru:
#在所有键中,选取最近最少使用的数据删除
allkeys-lfu:
#在所有键中,选取最近最不常用,也就是一定时期内被访问次数最少的数据删除
allkeys-random:
#采用随机淘汰策略删除所有的键值对,这个策略不常用。
noeviction:
#不淘汰任何键值对,当内存满时,如果进行读操作,例如get命令,它将正常工作,而做写操作,它将返回错误,也就是说,当Redis采用这个策略内存达到最大的时候,它就只能读不能写了
出了上面提到的内存回收机制可以有效解决消耗内存过高的原因之外,Redis还有一个过期机制,可以给key设置一个过期时间,一旦超过过期时间,这个key就会被被删除,内存被回收
PS:上面是内存不足的「淘汰策略」,这一种是过期键的删除策略,两者是不同的,不要搞混了
#如果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
当中,其选择的是策略 2
和策略 3
的综合使用。不过 Redis
的定期删除只会扫描设置了过期时间的键,因为设置了过期时间的键 Redis
会单独存储,所以不会出现扫描所有键的情况
同一时间大量Key过期会有什么影响?
Redis 是单线程的,收割的时间也会占用线程的处理时间,如果收割的太过于繁忙,以至于忙不过来?会不会导致线上读写指令出现卡顿?
Redi将每个设置了过期时间的Key放入到一个独立的字典中,会定时遍历这个字典来删除,默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略
为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms
#如果Redis 实例中所有的 key (几十万个)在同一时间过期会怎样?
Redis会持续扫描过期字典(循环),知道过期字典中的过期key变得稀疏,才会降低扫描次数
内存管理器需要频繁回收内存页,此时会产生一定的CPU消耗,必然会导致线上读写请求出现明显卡顿的现象
当客户端请求到来时(服务器如果正好进入过期扫描状态),请求将会至少等待25ms才会进行处理,入锅客户端将超时时间设置的比较短(10ms),那么就会出现大量的连接因为超时而关闭,业务端就会出现很多异常,而且这时你还无法从Redis的slowlog中看到慢查询记录
从库不会进行过期扫描,从库对过期的处理是被动的
当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的处理
AOF对过期Key的处理
了解了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
之后我们等待一段时间再看,发现不再告警了,使用内存也降下去了
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。