写在前面
不管什么工具,会使用永远只是第一步,第二步是当其出现某些问题时,拥有排查和修复问题的能力,而我们在使用Redis的过程中,变慢
就是其中一个比较棘手的问题,因此本文就一起来看下,当遇到该类问题时应该如何排查,以求能够在工作中帮助到你,当然也更加是帮助我自己,下面我们就开始吧!
1:Redis真的变慢了吗?
这部分我们一起看下如何评判Redis是否变慢
,比如说某命令的执行时间是1ms,就一定是变慢了吗?不一定,因为如果是对配置很差的机器,这可能是其最好的表现了
,但是对于每秒能够处理10万请求
的机器,这肯定就是慢了,但,这都是很主观的判断,并没有一个对比的指标,因此我们就需要这样的一个指标,在这里,这个指标就是基线性能
,所谓基线性能就是在服务器没有任何压力,没有任何干扰的情况下的基本性能。其实,从Redis2.8.7版本开始,Redis在redis-cli命令提供了--intrinsic-latency {测试秒数}
,可以用来测试Redis基线性能,如下:
d:\program_files\Redis-x64-2.8.2402>.\redis-cli --intrinsic-latency 120
Max latency so far: 1 microseconds.
Max latency so far: 4 microseconds.
Max latency so far: 5 microseconds.
Max latency so far: 16 microseconds.
Max latency so far: 33 microseconds.
Max latency so far: 35 microseconds.
Max latency so far: 41 microseconds.
Max latency so far: 77 microseconds.
Max latency so far: 93 microseconds.
Max latency so far: 106 microseconds.
Max latency so far: 118 microseconds.
Max latency so far: 486 microseconds.
Max latency so far: 4048 microseconds.
3268568472 total runs (avg latency: 0.0367 microseconds / 367.13 nanoseconds per run).
Worst run took 110260x longer than the average latency.
可以看到基线性能的数据延迟是0.0367微妙
,如果是在实际运行过程中,这个时间变为0.0367微妙*2=0.0634微妙
左右则肯定就是变慢了,现在我们确定Redis确实是变慢了,剩下的就是定位变慢的原因了。
2:变慢了该怎么办?
首先可能是我们的使用方式有问题,其次是操作系统的配置问题,最后还有可能是文件系统的问题,我们也从这三个方便展开说明。
2.1:使用方式
主要有两种情况,第一种是使用了时间复杂度较高的命令,即存在慢查询的情况,第二种是设置了大量的key在相同的时间过期。
2.1.1:慢查询
我们可以通过配置慢查询日志的方式来排查慢查询问题,处理方式如下:
首先需要配置慢日志,有两种配置方式,第一种是通过设置redis.conf,如下:
# You can configure the slow log with two parameters: one tells Redis
# what is the execution time, in microseconds, to exceed in order for the
# command to get logged, and the other parameter is the length of the
# slow log. When a new command is logged the oldest one is removed from the
# queue of logged commands.
# The following time is expressed in microseconds, so 1000000 is equivalent
# to one second. Note that a negative number disables the slow log, while
# a value of zero forces the logging of every command.
slowlog-log-slower-than 10000
# There is no limit to this length. Just be aware that it will consume memory.
# You can reclaim memory used by the slow log with SLOWLOG RESET.
slowlog-max-len 128
如上使用了slowlog-log-slower-than
指令(directive)和slowlog-max-len
指令(directive)。这种是永久方案,正式环境还是建议进行如此适当的配置,第二种方案是使用config set
动态修改,如下配置:
127.0.0.1:6379> CONFIG SEt slowlog-log-slower-than 1 # 为了方便测试,将慢查询时长设置为1微妙,正常不会设置这么短,
OK
127.0.0.1:6379> CONFIG SET slowlog-max-len 128 # 内存中的慢日志队列最多记录128个元素,超过的将会删除老的
OK
如下执行一些操作:
127.0.0.1:6379> set name jack
OK
127.0.0.1:6379> get name
"jack"
127.0.0.1:6379> slowlog get 2
1) 1) (integer) 10
2) (integer) 1668663759
3) (integer) 3
4) 1) "get"
2) "name"
2) 1) (integer) 9
2) (integer) 1668663757
3) (integer) 6
4) 1) "set"
2) "name"
3) "jack"
然后通过slowlog get {个数} ,返回指定个数的慢查询日志,返回的顺序是执行命令的时间倒序,如下:
127.0.0.1:6379> slowlog get 2
1) 1) (integer) 10
2) (integer) 1668663759
3) (integer) 3
4) 1) "get"
2) "name"
2) 1) (integer) 9
2) (integer) 1668663757
3) (integer) 6
4) 1) "set"
2) "name"
3) "jack"
返回结果的每项含义如下:
1):唯一性(unique)的日志标识符。日志的唯一id只有在Redis服务器重启的时候才会重置,这样可以避免对日志的重复处理。
2):被记录命令的执行时间点,以UNIX时间戳格式表示
3):查询执行时间,单位为微秒
4):执行的命令,以数组的形式排列
也可以通过slowlog len 命令查询当前慢日志条目(entry)的个数,如下:
127.0.0.1:6379> slowlog len
(integer) 4
想要清空(cleared)慢日志队列的话,可以通过slowlog reset
命令(command)。
当慢日志条目个数超过了slowlog-max-len配置的队列最大长度后,将会删除老的条目,生产环境中不建议将该值设置的过大,因为会占用内存空间,比较建议的做法是,如果是需要保留慢日志的话,可以定期的将慢日志条目同步到三方组件,如MySQL中,进行持久化存储,直接使用其日志ID作为主键ID也是个不错的选择。
2.1.2:过期key删除
想要搞清楚过期key删除为什么会导致查询变慢,需要先搞清楚过期key删除方式,是这样子的,Redis会定期,默认是100毫秒,检测过期需要删除的key,采样ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
个数的key,并将其中过期的key全部删除,如果采样的key,超过四分之一的key都过期,则会重复这个采样删除的过程,直到过期key个数低于四分之一为止,需要注意的是,这个删除过期key的过程是阻塞的(Redis4之后使用了异步线程进行优化),因此如果是同一时刻有大量的key过期的话,可能就会造成主线程的长时间阻塞,对于这个问题就需要排查程序是否有大量的key通过expire设置了相同的过期时间,这种问题的解决方法也比较简单,对于同一批key,在设置expire的时候,通过生成一定范围内的随机数,使key的过期时间分散在一定范围内。
2.2:文件系统
和文件系统相关的其实就是AOF 了,在了解AOF如何影响主线程的操作性能之前,我们需要看下AOF的工作方式,AOF将日志写到磁盘上是通过2个文件系统的调用来完成,一个是write,另一个是fsync,其中write用来将日志写到内核缓冲区,fsync用来将内核缓存区的内容写到磁盘,fsync因为要向磁盘写数据所以速度较慢。AOF可以设置的值有no
,everysec
,always
,和write,fsync这两个系统调用的关系如下:
当我们将appendfsync设置为always时,因为每一个数据写入的命令,都要同步的等fsync执行完成,所以此时会阻塞主线程,进而影响Redis的性能,导致变慢。那么当我们设置为everysec时是不是就不会导致这个问题了呢?也是会的,因为主线程在第一次启动了一个子线程执行fsync后,还会监测其执行进度,如果是在第二次需要执行fsync时发现上次的子线程fsync还没有执行完毕的话,依然会阻塞主线程,导致Redis变慢,那么什么时候会出现这种情况呢,当磁盘IO压力很大的时候就可能出现了,在不考虑其他应用对IO资源占用的情况下,Redis本身也会导致IO压力变大,其中AOF重写 就是主要元凶,AOF重写用来减小AOF文件的大小,当数据较多时,其对IO的压力会比较大,对于这种情况,我们一般有如下的几种解决方案:
1:业务允许的话,将appendfsync改为no
2:使用Redis集群,减少单个Redis实例的数据量,从而减少AOF重写对IO的压力
3:更换IO性能更好的固态硬盘,但代价也更大,其速度是普通机械硬盘的10倍以上
4:设置no-appendfsync-on-rewrite yes,代表在进行AOF重写时不要调用fsync,但这种方式如果发生了异常宕机,会导致数据丢失
以上方案要根据具体的业务场景进行分析,选择一种最适合自己的,个人感觉其中1,4
可能是会被用到比较多。因为2,3
不管是带来的工作量,还是成本都还是比较高的。
2.3:操作系统
和操作系统相关的因素是内存,其中可能让Redis变慢的主要是2方面的原因,第一个是swap,第二个是内存大页,我们分别来看下。
2.3.1:swap
swap是操作系统在内存紧张时执行的一种机制,即将部分不常用的内存空间对应的数据写到磁盘,然后将该内存分配给程序使用,当需要用到该部分数据时再将数据从磁盘上读取出来,这里涉及到了磁盘的IO,所以速度会比较慢,这也是swap可能导致Redis变慢的原因。
如何判断是否发生了swap呢,首先需要先确定Redis实例的进程号,如下:
[root@localhost 1300]# ./redis-cli info | grep process_id
1366
然后通过如下命令查看是否发生了swap:
[root@localhost 1366]# cd /proc/1366
[root@localhost 1366]# cat smaps | egrep '^(Swap|Size)'
Size: 584 kB
Swap: 0 kB
Size: 4 kB
Swap: 4 kB
Size: 4 kB
Swap: 0 kB
Size: 462044 kB
Swap: 462008 kB
Size: 21392 kB
Swap: 0 kB
其中的size代表的就是申请的内存块大小(可以看到是申请了多个内存块的,而不是一大块)
,紧跟着的swap就是该块内存发生了swap的大小,当大于0时就是代表发生了内存交换,当和size相等时就说明全部发生了内存交换swap。当出现了超过MB级别的内存交换swap时就有可能会导致Redis变慢了,说明Redis内存紧张了,针对这种情况有如下的方案可供参考:
1:如果是集群环境,则增加新节点,降低单节点的内存占用量
2:增加机器内存
2.3.2:大内存页
内存页是操作系统分配内存时的最小单位,一般是4kb,Linux 内核从 2.6.38版本开始支持2M的大内存页的分配。
理论上大内存页应该是更好了才对,因为会减少内存分配的时长,但是我们需要考虑RBD 的场景,在执行RDB时,为了保证RDB生成快照的数据数据一致性(为同一时刻的数据)
,对于更新操作,Redis会使用cow机制,对对应的内存页拷贝一份,因此对于大内存页,即使是修改的只是几个字节的数据,也需要拷贝2M的数据,而这个过程是会阻塞主线程的,所以也会导致Redis变慢的问题,对于这个问题处理也比较简单,只需要关闭大内存页机制就可以了,如下可以查看是否打开了大内存页:
[root@localhost 1300]# cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never
可以看到其中包含always
关键字,就说明打开了大内存页机制,可以通过如下命令关闭大内存页:
[root@localhost 1300]# echo never > /sys/kernel/mm/transparent_hugepage/enabled
[root@localhost 1300]# cat /sys/kernel/mm/transparent_hugepage/enabled
never
3:总结
如下是遇到Redis变慢问题时的checklist:
1:获取Redis实例在当前环境下的基线性能。
2:是否用了慢查询命令?如果是的话,就使用其他命令替代慢查询命令,或者把聚合计算命令放在客户端做。
3:是否对过期key设置了相同的过期时间?对于批量删除的key,可以在每个key的过期时间上加一个随机数,避免同时删除。
4:是否存在bigkey?对于bigkey的删除操作,如果你的Redis是4.0及以上的版本,可以直接利用异步线程机制减少主线程阻塞;如果是Redis4.0以前的版本,可以使用SCAN命令迭代删除;对于bigkey的集合查询和聚合操作,可以使用SCAN命令在客户端完成。
5:RedisAOF配置级别是什么?业务层面是否的确需要这一可靠性级别?如果我们需要高性能,同时也允许数据丢失,可以将配置项no-appendfsync-on-rewrite设置为yes,避免AOF重写和fsync竞争磁盘IO资源,导致Redis延迟增加。当然,如果既需要高性能又需要高可靠性,最好使用高速固态盘作为AOF日志的写入盘。
6:Redis实例的内存使用是否过大?发生swap了吗?如果是的话,就增加机器内存,或者是使用Redis集群,分摊单机Redis的键值对数量和内存压力。同时,要避免出现Redis和其他内存需求大的应用共享机器的情况。
7:在Redis实例的运行环境中,是否启用了透明大页机制?如果是的话,直接关闭内存大页机制就行了。
8:是否运行了Redis主从集群?如果是的话,把主库实例的数据量大小控制在2~4GB,以免主从复制时,从库因加载大的RDB文件而阻塞。
9:是否使用了多核CPU或NUMA架构的机器运行Redis实例?使用多核CPU时,可以给Redis实例绑定物理核;使用NUMA架构时,注意把Redis实例和网络中断处理程序运行在同一个CPUSocket上。
写在后面
参考文章列表:
Redis慢日志 。
redis之AOF和RDB持久化 。
揭秘:SSD固态价格大跳水后,为何还是无法取代机械硬盘? 。