现象描述
在项目中,每次开机重启的时候,有一个Java应用都会报错:
Caused by: org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisLoadingException: LOADING Redis is loading the dataset in memory at
... 42 common frames omitted
经调查,发现该应用在启动的过程中,会从mysql中读取数据,然后写入到redis中去。
解决之路
抛出异常:LOADING Redis is loading the dataset in memory
经过百度,大致了解到,出现上述原因,主要是三个原因:
原因1:
redis中dump.rdb文件到达3G时,所有redis的操作都会抛出此异常。
解决方法:redis.conf中 maxmemory 调大,同时开启转换功能
maxmemory 3GB
maxmemory-policy allkeys-lru
原因2:另外一个原因是当Redis重启后,需要将持久化数据重新写入也会报这个错,等预热之后就正常了。
原因3:服务器内存不够
1. 初步判断
实际调查过程中,发现原因2可以解释目前的这种现象。redis刚开始重启的时候,能够连接上,但是无法往redis中写数据;
等待大概三四十秒之后,再次启动Java应用,则可以正常写入。因此,是在redis从硬盘中读取数据,往内存中写数据的这个阶段,花费时间比较长。
2. 问题定位
通过命令info memory查看redis占用的内存大小,信息如下:
> INFO MEMORY
# Memory
used_memory:12567192
used_memory_human:11.99M
used_memory_rss:19259392
used_memory_rss_human:18.37M
used_memory_peak:13259520
used_memory_peak_human:12.65M
used_memory_peak_perc:94.78%
used_memory_overhead:4126808
used_memory_startup:803312
used_memory_dataset:8440384
used_memory_dataset_perc:71.75%
.....
通过配置used_memory_human可知,redis中占用的内存仅 12M。理论上是不可能花费这么长的时间的。通过重启redis后,立马使用redis客户端往redis中写数据,发现依旧会报错“LOADING Redis is loading the dataset in memory ”。这说明,在Redis启动后,可连接之后,有一段时间,是不能往Redis中写入数据的。
3. 突然想到
在解决问题的过程中,突然想到redis.yml(项目中的redis是由docker启动)中对redis的数据配置了一个文件夹。进入该文件夹,发现了一个文件:appendonly.aof。查看改文件的大小,有3.4G左右大。因此判断,是由于该文件导致的redis启动缓慢。
4. 原理探索
4.1 redis 持久化的两种方式
aof是啥?通过查询,知道aof是redis持久化的一种方式。用于在复杂系统中,防止Redis宕机后数据丢失,对Redis进行持久化的方法。
redis持久化方式,一共有两种:一种是aof,一种是rdb.
- aof: Append Only File :追加方式持久化。通过记录每一次对数据的记录,追加到.aof文件中。当redis重启时,通过读取.aof文件,来实现对数据的恢复。
- rdb: Redis DataBase 将当前数据库的快照保存到磁盘上,并以.rdb文件格式存到磁盘中。当redis重启时,通过阅读.rdb文件来实现对数据的恢复。
截止目前为止,已经找到现场的原因:本项目的redis,采用的是aof文件方式来进行持久化的。现在的问题时aof文件过大,导致的Redis启动过慢。那应该是系统运行时间过长,导致.aof文件不断累加,导致最终累计成了3.4G。
- 有没有一种配置,来控制aof的大小?类似日志中的滚动删除?只保存近30天的日志?
没有。redis的持久化,就是为了保证redis中的数据不丢失。如果有这种配置,会造成redis的持久化没有意义。
4.2 aof与重写机制
这一部分, 是部门以为有经验的同事何总提醒我的。这就是知识面的问题了。通过查询资料发现,AOF持久化方案中,提供了一种重写机制,用户对aof文件进行压缩。
随着运行时间的增长,执行的命令越来越多,会导致AOF文件越来越大,当AOF文件过大时,redis会执行重写机制来压缩AOF文件。这个压缩和上面提到的RDB文件的算法压缩不同,重写机制主要是将文件中无效的命令去除。
重写的场景举例:
- 同一个key的值,只保留最后一次写入;
- 已删除或者已过期数据相关命令会被去除(这样就避免了,aof文件过大而实际内存数据小的问题(如频繁修改数据时,命令很多,实际数据很少)
重写的触发方式:
- 手动执行 bgrewriteaof 触发AOF重写
- 在redis.conf文件中配置重写的条件,如:
auto-aof-rewrite-min-size 64MB // 当文件小于64M时不进行重写
auto-aof-rewrite-min-percenrage 100 // 当文件比上次重写后的文件大100%时进行重写
重写的过程
如上图所示,当AOF重写被触发之后,发生如下重写步骤:
- 从主进程中fork出子进程,并拿到fork时的AOF文件数据写到一个临时AOF文件中,即上图中的“AOF重写缓存”文件。
- 在重写过程中,redis收到的命令会同时写到AOF缓冲区和重写缓冲区中,这样保证重写不丢失重写过程中的命令
- 重写完成后通知主进程,主进程会将AOF缓冲区中的数据追加到子进程生成的文件中
- redis会原子的将旧文件替换为新文件,并开始将数据写入到新的aof文件上
关于重写机制需要注意以下几点:
- 执行重写时如果发现有进程正在执行重写,那么直接返回。如果有进程正在执行BGSAVE,那么会等BGSAVE执行完成后再执行重写。
- Redis执行重写时会fork一个进程进行,其开销和RDB一样
- 重写过程不影响原有的AOF过程,write,save操作都不影响。
- 重写过程中有几种时刻会阻塞主进程: 在fork子进程时,将重写缓冲区的数据写到磁盘上时,使用新AOF文件替换旧文件时。
5. 根据原理解决问题
5.1 在配置文件中设置触发重写的条件
从原理可知,触发aof重写机制有两种方法,一种是手动执行重写命令,另一种是通过在redis.conf文件中来配置。为了今后运维的简单,采用了第二种方法,通过在redis.conf文件中配置如下信息:
auto-aof-rewrite-min-size 64MB // 当文件小于64M时不进行重写
auto-aof-rewrite-min-percenrage 100 // 当文件比上次重写后的文件大100%时进行重写
只有当上述两个条件均满足是,才会触发重写。
5.2 反推现像的产生原因
通过阅读配置文件,发现系统中已经配置了此重写条件。但是当前系统的业务数据在redis中,也只有14MB左右。为何配置类重写机制,但是.aof文件会还是会有3.4G?为何aof重写机制没有触发。通过查阅资料,得知:auto-aof-rewrite-percentage 这个配置产生的条件为:
# 表示当前aof文件大小超过上一次aof文件大小的百分之多少的时候会进行重写。如果之前没有重写过,以启动时aof文件大小为准
auto-aof-rewrite-percentage 100
只可能是由一下两个原因共同造成的:
- 原来系统遗留的历史数据。aof文件是从别的系统中拷贝过来的历史数据。
- 在重写配置文件生效前,aof文件已经很大了。
修改重写配置之前,aof文件的大小假设为2G。配置重写机制之后,由于系统之前没有重写过,重写机制会议重启时的 auto-aof-rewrite-percentage 会以2G作为重写的机制。也就是只有到目前的3.4G累加到4G之后,才会触发重写。
解决方案
因此,针对这种现场,解决方法有两种:
方案(1)手动执行重写
bgrewriteaof
方案(2)直接删除.aof文件
让其按照配置的文件大小来执行重写。
这种方式适合系统中没有需要持久化的数据,aof文件均是脏数据的场景。总的来说,还是推荐方案1.