Redis-排查Redis为什么变慢了

Redis为什么变慢了?常见延迟问题定位与分析

Redis作为内存数据库,拥有非常高的性能,单个实例的QPS能够达到10W左右。但我们在使用 Redis 时,经常时不时会出现访问延迟很大的情况,如果你不知道 Redis 的内部实现原理,在排查问题时就会一头雾水。

Redis出现访问延迟变大,都与我们的使用不当或运维不合理导致的。

以下这篇文章我们就来分析一下 Redis 在使用过程中,经常会遇到的延迟问题以及如何定位和分析。

链路追踪

链路追踪就是在服务访问外部依赖的出入口,记录下每次请求外部依赖的响应延时。

如果发现确实是操作 Redis 的这条链路耗时变长了,那么此时需要把焦点关注在业务服务到 Redis 这条链路上。

从服务到 Redis 这条链路变慢的原因可能有网络(网络不好,数据包在传输时存在延迟、丢包等),或者是 Redis本身的问题,吸引进一步排查问题的所在。

如果是服务器之间网络存在问题,这时候就要找网络运维同事,让其协助解决网络问题。

基准性能

简单来讲,基准性能就是指 Redis 在一台负载正常的机器上,其最大的响应延迟和平均响应延迟分别是怎样的?

如果对比曾经上线压测过的正常情况下Redis响应时长,确实是执行命令耗时变长了,那再进行下面的分析操作:

  • 查看Redis所在服务器的网络使用情况,排查是否为网络负载达到瓶颈
  • 查看应用服务器与Redis服务器之间网络响应时长

使用redis-cli -h 172.0.0.1 -p 6379 --intrinsic-latency 60,查看Redis在60秒内的最大网络响应延迟(单位:微秒)

在这里插入图片描述

这里是说在60秒内的最大响应时长是84微秒(0.084毫秒)

还可以使用redis-cli -h 172.0.0.1 -p 6973 --latency-history -i 1查看redis在一段时间内最大、最小、平均访问延迟。
在这里插入图片描述
这里是每1秒钟输出一次采样的redis平均操作耗时,平均耗时这个指标分布在0.08ms和0.13ms之间

如果观察到,这个实例的运行延迟是正常 Redis 基准性能的 2 倍以上,即可认为这个 Redis 实例确实变慢了

接下来我们一步步来分析可能导致 Redis 变慢的原因是什么。

使用了复杂度过高的命令

如果在使用Redis时,发现访问延迟突然增大,如何进行排查?

这种情况可以通过增加 Redis 慢日志统计来看

首先设置 Redis的 慢日志阈值,只有超过阈值的命令才会被记录,这里的单位是微秒,例如设置慢日志的阈值为 5 毫秒,同时设置只保留最近 500 条慢日志记录:

//命令执行超过 5 毫秒记录慢日志
CONFIG SET slowlog-log-slower-than 5000
//只保留最近 500 条慢日志
CONFIG SET slowlog-max-len 500

记录慢日志之后,可以使用 slowlog get 5 来查看 top 5 的操作慢的命令:

127.0.0.1:6379> SLOWLOG get 5
1) 1) (integer) 32693       # 慢日志ID
   2) (integer) 1593763337  # 执行时间
   3) (integer) 5299        # 执行耗时(微秒)
   4) 1) "LRANGE"           # 具体执行的命令和参数
      2) "user_list_2000"
      3) "0"
      4) "-1"
2) 1) (integer) 32692
   2) (integer) 1593763337
   3) (integer) 5044
   4) 1) "GET"
      2) "book_price_1000"
...

通常来说命令执行慢有两种原因:

  • 使用了O(n)以上的复杂度很高的命令,例如 sort、sunion 等,这种原因通常表现为 Redis 的 CPU 使用率很高,查询数据量不一定很大
  • 单次操作返回的数据量太大,导致 Redis 在数据组装和返回传输上面耗时

由于开源版本6.0之前的 Redis 是单线程的,上面两种原因会导致其他命令阻塞,表现为整体 Redis 操作命令都很耗时。

针对这种情况如何解决呢?

  • 尽量不使用 O(N) 以上复杂度过高的命令,对于数据的聚合操作,放在客户端做
  • 执行 O(N) 命令,保证 N 尽量的小(推荐 N <= 300),每次获取尽量少的数据,让 Redis 可以及时处理返回

操作、存储大的bigkey

如果代码内部都是的一些简单操作命令,例如set、get、hset、hget等,但是慢日志里面仍然能看到这些命令,那就要检查是否操作了大key,这种类型的 key 我们一般称之为 bigkey。因为大 key 在内存分配的时候会比较耗时,释放内存也更耗时,这是导致 Redis 响应慢的原因。

Redis 提供了扫描 bigkey 的命令,我们可以使用redis-cli -h 172.0.01 -p 6379 --bigkeys -i 0.01查看,一个实例中 bigkey 的分布情况:

Sampled 829675 keys in the keyspace!
Total key length in bytes is 10059825 (avg len 12.13)

Biggest string found 'key:291880' has 10 bytes
Biggest   list found 'mylist:004' has 40 items
Biggest    set found 'myset:2386' has 38 members
Biggest   hash found 'myhash:3574' has 37 fields
Biggest   zset found 'myzset:2704' has 42 members

36313 strings with 363130 bytes (04.38% of keys, avg size 10.00)
787393 lists with 896540 items (94.90% of keys, avg size 1.14)
1994 sets with 40052 members (00.24% of keys, avg size 20.09)
1990 hashs with 39632 fields (00.24% of keys, avg size 19.92)
1985 zsets with 39750 members (00.24% of keys, avg size 20.03)

这个命令的原理其实是在 Redis 内部执行了 scan 命令,遍历 Redis 里面所有的key,分别执行 strlen、llen、hlen、scard、zcard 命令,来获取 String 类型的长度、容器类型(List、Hash、Set、ZSet)的元素个数。

当执行这个命令时,需要注意一下几点问题:

  • 进行 bigkey 扫描时,会导致 Redis 的 OPS 陡增,生产环境一定要控制好执行的频率, -i参数后面就是执行的间隔时间,单位是秒
  • 而对于容器类型的key,只能扫描出元素最多的key,但元素最多的key不一定占用内存最多,这一点需要我们注意下。不过使用这个命令一般我们是可以对整个实例中key的分布情况有比较清晰的了解。

针对 bigkey 导致延迟的问题。我们可以这么做:

  • 尽量避免写入 bigkey
  • 针对大key的问题,Redis官方在4.0版本推出了lazy-free的机制,用于异步释放大key的内存,降低对Redis性能的影响。即使这样,我们也不建议使用大key,大key在集群的迁移过程中,也会影响到迁移的性能

大量数据集中过期

我们是否在操作 Redis 的过程中,每次都在一个时间点就会发生一波延迟。这时候我们需要排查一下是否存在大量的 key 在同一个时间过期呢。

为什么集中过期就会导致 Redis 延迟变大?

集中过期导致 Redis 响应慢的原因是跟 Redis 的过期策略有关,主要有两种:

  • 主动过期:Redis 内部维护了一个定时任务,默认是间隔 100毫秒 就会从全局的过期哈希表中随机取出 20 个 key进行删除,如果过期哈希表里面的 key 的数量占比超过了总 key 数量的 25%,则会重复该删除操作,直到过期 key 数量占比下降到 25% 以下或者删除操作超过了 25 毫秒才退出循环
  • 被动过期是当访问某个 key 的时候会判断他是否过期,如果过期了,则直接删除

Redis 设置集中过期的命令是 expireat 和 pexpireat,排查集中过期的时候可以搜索这些关键字。

解决这种集中过期有两种方案:

  • 集中过期 key 增加一个随机过期时间,把集中过期的时间打散,比如(redis.expireat(key, expire_time + random(1000)))降低 Redis 清理过期 key 的压力
  • 可以开启 lazy-free 机制(lazyfree-lazy-expire yes),当删除过期 key 时,把释放内存的操作放到后台线程中执行,避免阻塞主线程

Redis实例内存使用达到了上限

如果 Redis 实例设置了内存上限 maxmemory,内存使用也达到了 maxmemory,这也可能导致 Redis 响应慢。

Redis 作为缓存使用时,通常会给这个实例设置一个内存上限 maxmemory,然后设置一个数据淘汰策略。

当实例的内存达到了 maxmemory 后,会发现之后每次写入新数据,操作延迟变大了,这是为什么?

Redis 数据达到上限了,每次写入新数据之前,Redis 需要先从实例中踢出一部分数据,让整个实例的内存维持在 maxmemory 之下,然后才能把新数据放进内存中,这个踢出同样是耗时的操作,淘汰数的逻辑跟删除过期key的逻辑一样,所以淘汰会增加 Redis 响应时间。

淘汰策略:

  • allkeys-lru:不管 key 是否设置了过期,淘汰最近最少访问的 key
  • volatile-lru:只淘汰最近最少访问、并设置了过期时间的 key
  • allkeys-random:不管 key 是否设置了过期,随机淘汰 key
  • volatile-random:只随机淘汰设置了过期时间的 key
  • allkeys-ttl:不管 key 是否设置了过期,淘汰即将过期的 key
  • noeviction:不淘汰任何 key,实例内存达到 maxmeory 后,再写入新数据直接返回错误
  • allkeys-lfu:不管 key 是否设置了过期,淘汰访问频率最低的 key(4.0+版本支持)
  • volatile-lfu:只淘汰访问频率最低、并设置了过期时间 key(4.0+版本支持)
具体使用哪种策略需要根据场景来配置,一般都是使用allkeys-lru或volatile-lru淘汰策略。

如果此时 Redis 的实例中还存储了 bigkey,那么在淘汰删除 bigkey 释放内存时也会耗时比较久。

fork子进程导致响应变慢

如果 Redis 开启了自动生成 RDB 和 AOF 重写功能,那么有可能在后台生成 RDB 和 AOF 重写时导致 Redis 的访问延迟增大,而等这些任务执行完毕后,延迟情况消失。

如果遇到这种情况,一般就是执行生成RDB和AOF重写任务导致的。

生成RDB和AOF都需要父进程fork出一个子进程进行数据的持久化,在fork执行过程中,父进程需要拷贝内存页表给子进程,如果整个实例内存占用很大,那么需要拷贝的内存页表会比较耗时,此过程会消耗大量的CPU资源,在完成fork之前,整个实例会被阻塞住,无法处理任何请求,如果此时CPU资源紧张,那么fork的时间会更长,甚至达到秒级。这会严重影响Redis的性能。

除了因为备份的原因生成 RDB 之外,在主从节点第一次建立数据同步时,主节点也会生成 RDB 文件给从节点进行一次全量同步,这时也会对 Redis 产生性能影响。

可以在 Redis 上面执行 info 命令查看 latest_fork_usec 是多少,单位是微秒。

# Stats
total_connections_received:2
total_commands_processed:4
instantaneous_ops_per_sec:0
rejected_connections:0
sync_full:0
sync_partial_ok:0
sync_partial_err:0
expired_keys:0
evicted_keys:0
keyspace_hits:0
keyspace_misses:0
pubsub_channels:0
pubsub_patterns:0
# 上一次 fork 耗时,单位微秒
latest_fork_usec:0
migrate_cached_sockets:0

要想避免 Redis 在 fork 的时候耗时太久,可以采取一下方案进行:

  • redis内存不要设置太大,最好不要超过10G,因为fork子进程的时候跟实例大小有关,实例越大,fork耗时越久
  • 配置redis持久化策略的时候,注意RDB的操作尽量在业务低峰期进行,对于可以从其他非内存数据库恢复的数据,可以关闭AOF rewrite,这样可以增强性能
  • 如果有条件,尽量不要把redis部署在虚拟机上,因为虚拟机fork子进程比物理机更耗时
  • 调大主从库全量数据同步的概率,可以调到repl-backlog-size参数,避免主从全量同步

开启了AOF存储

关于数据持久化方面,还有影响 Redis 性能的因素,这次我们重点来看 AOF 数据持久化。

如果了解 Redis 执行 AOF 命令的原理,就知道为什么开启 AOF 会导致 Redis 响应慢了。

当 Redis 开启 AOF 后,其工作原理如下:

  • 在redis执行了写操作命令后,会先把这个命令写入到AOF文件内存中,由write系统调用
  • 根据redis配置的AOF刷盘策略,再把内存数据刷到磁盘中,由fsync系统调用

为了保证 AOF 文件数据的安全性,提供了3中刷盘策略,其中各有利弊:

  • appendfsync always:主线程每次执行写操作后立即刷盘,此方案会占用比较大的磁盘 IO 资源,但数据安全性最高。这种操作会阻塞redis操作命令,redis的写命令执行需要等数据刷盘了才能返回结果,这样会严重拖慢redis的执行性能,生产环境一般都不会这么玩。
  • appendfsync no:主线程每次写操作只写内存就返回,内存数据什么时候刷到磁盘,交由操作系统决定,此方案对性能影响最小,但数据安全性也最低,Redis 宕机时丢失的数据取决于操作系统刷盘时机。如果我们的redis存储的这些数据只用作缓存,丢失了可以去关系型数据库查,对系统无影响,那可以考虑这种方式,如果不是这样,那生产环境也不会采用这种策略。
  • appendfsync everysec:主线程每次写操作只写内存就返回,然后由后台线程每隔 1 秒执行一次刷盘操作(触发fsync系统调用),此方案对性能影响相对较小,但当 Redis 宕机时会丢失 1 秒的数据。这种策略取了中间平衡,但是仍然存在Redis 延迟变大的情况,甚至会阻塞整个 Redis。
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值