Redis05 - 性能调优和缓存问题

Redis性能调优和缓存问题

文章目录

一:链路追踪判断是不是redis出了问题

如果发现服务的访问十分的慢,需要通过进行链路追踪定位到到底是哪一个环节出了问题。

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

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

从你的业务服务到 Redis 这条链路变慢的原因可能也有 2 个:

  • 业务服务器到 Redis 服务器之间的网络存在问题,例如网络线路质量不佳,丢包等情况
  • Redis 本身存在问题,需要进一步排查是什么原因导致 Redis 变慢

为了避免业务服务器到 Redis 服务器之间的网络延迟,需要直接在 Redis 服务器上测试实例的响应延迟情况

sh 复制代码
# 在 60s 内的最大相应延迟
> redis-cli -h 127.0.0.1 -p 6379 --intrinsic-latency 60
# 可以发现下面的结果中最大的响应延迟是72微秒(0.072ms)
Max latency so far: 1 microseconds.
Max latency so far: 15 microseconds.
Max latency so far: 17 microseconds.
Max latency so far: 18 microseconds.
Max latency so far: 31 microseconds.
Max latency so far: 32 microseconds.
Max latency so far: 59 microseconds.
Max latency so far: 72 microseconds.
 
1428669267 total runs (avg latency: 0.0420 microseconds / 42.00 nanoseconds per run).
Worst run took 1429x longer than the average latency.

# 查看一段时间中的最大,最小,平均响应延迟。
# 每间隔 1 秒,采样 Redis 的平均操作耗时,其结果分布在 0.08 ~ 0.13 毫秒之间
> redis-cli -h 127.0.0.1 -p 6379 --latency-history -i 1
min: 0, max: 1, avg: 0.13 (100 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.12 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.13 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.10 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.13 (98 samples) -- 1.00 seconds range
min: 0, max: 1, avg: 0.08 (99 samples) -- 1.01 seconds range

按照以下几步,来判断你的 Redis 是否真的变慢了

  1. 在相同配置的服务器上,测试一个正常 Redis 实例的基准性能
  2. 找到你认为可能变慢的 Redis 实例,测试这个实例的基准性能
  3. 如果你观察到,这个实例的运行延迟是正常 Redis 基准性能的 2 倍以上,即可认为这个 Redis 实例确实变慢了

二:redis变慢原因

1:使用复杂度过高的命令(*)

1.1:查看redis慢日志

查看 Redis 慢日志之前,你需要设置慢日志的阈值。

例如,设置慢日志的阈值为 5 毫秒,并且保留最近 500 条慢日志记录

sh 复制代码
# 命令执行耗时超过 5 毫秒,记录慢日志
CONFIG SET slowlog-log-slower-than 5000
# 只保留最近 500 条慢日志
CONFIG SET slowlog-max-len 500

设置完成之后,所有执行的命令如果操作耗时超过了 5 毫秒,都会被 Redis 记录下来。

此时,你可以执行以下命令,就可以查询到最近记录的慢日志

sh 复制代码
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) "user_info:1000"
...

通过查看慢日志,我们就可以知道在什么时间点,执行了哪些命令比较耗时。

1.2:延迟变大原因分析

如果你的应用程序执行的 Redis 命令有以下特点,那么有可能会导致操作延迟变大:

  • 经常使用 O(N) 以上复杂度的命令,例如 SORT、SUNION、ZUNIONSTORE 聚合类命令
  • 使用 O(N) 复杂度的命令,但 N 的值非常大

第一种情况导致变慢的原因在于,Redis 在操作内存数据时,时间复杂度过高,要花费更多的 CPU 资源。

第二种情况导致变慢的原因在于,Redis 一次需要返回给客户端的数据过多,更多时间花费在组装和网络传输过程中。

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

2:操作big_key(*)

⏳ 如果你查询慢日志发现,并不是复杂度过高的命令导致的,而都是 SET / DEL 这种简单命令出现在慢日志中,那么你就要怀疑你的实例否写入了 bigkey

2.1:什么是big_key

如果一个 key 写入的 value 非常大,那么 Redis 在分配内存时就会比较耗时。

当删除这个 key 时,释放内存也会比较耗时,这种类型的 key 我们一般称之为 bigKey

此时,你需要检查你的业务代码,是否存在写入 bigkey 的情况。你需要评估写入一个 key 的数据大小,尽量避免一个 key 存入过大的数据

2.2:查看big_key分布

Redis 提供了扫描 bigkey 的命令,执行以下命令就可以扫描出,一个实例中 bigkey 的分布情况

sh 复制代码
# 是以类型为维度进行展示的
# 从输出结果我们可以很清晰地看到,每种数据类型所占用的最大内存 / 拥有最多元素的 key 是哪一个
# 每种数据类型在整个实例中的占比和平均大小 / 元素数量
$ redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.01
 
...
-------- summary -------
 
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 命令,遍历整个实例中所有的 key,然后针对 key 的类型,分别执行 STRLENLLENHLENSCARDZCARD 命令,来获取 String 类型的长度、容器类型(List、Hash、Set、ZSet)的元素个数。

⚠️ 注意下面两点

  • 对线上实例进行 bigkey 扫描时,Redis 的 OPS 会突增,为了降低扫描过程中对 Redis 的影响,最好控制一下扫描的频率,指定 -i 参数即可,它表示扫描过程中每次扫描后休息的时间间隔,单位是秒
  • 扫描结果中,对于容器类型(List、Hash、Set、ZSet)的 key,只能扫描出元素最多的 key。但一个 key 的元素多,不一定表示占用内存也多,你还需要根据业务情况,进一步评估内存占用情况
2.3:解决方案
  • 业务应用尽量避免写入 bigkey
  • 拆分。将一个bigkey拆分成为多个小key
  • 如果你使用的 Redis 是 4.0 以上版本,用 UNLINK 命令替代 DEL,此命令可以把释放 key 内存的操作,放到后台线程中去执行,从而降低对 Redis 的影响
  • 如果你使用的 Redis 是 6.0 以上版本,可以开启 lazy-free 机制(lazyfree-lazy-user-del = yes),在执行 DEL 命令时,释放内存也会放到后台线程中执行

3:集中过期问题

如果你发现,平时在操作 Redis 时,并没有延迟很大的情况发生,但在某个时间点突然出现一波延时,其现象表现为:变慢的时间点很有规律,例如某个整点,或者每间隔多久就会发生一波延迟。如果是出现这种情况,那么你需要排查一下,业务代码中是否存在设置大量 key 集中过期的情况。

3.1:过期数据的策略
  • 被动过期:只有当访问某个 key 时,才判断这个 key 是否已过期,如果已过期,则从实例中删除
  • 主动过期:Redis 内部维护了一个定时任务,默认每隔 100ms就会从全局的过期哈希表中随机取出 20 个 key,然后删除其中过期的 key,如果过期 key 的比例超过了 25%,则继续重复此过程,直到过期key的比例下降到25%以下,或者这次任务的执行耗时超过了 25 毫秒,才会退出循环

⚠️ -> 主动过期 key 的定时任务,是在 Redis 主线程中执行的

复制代码
如果在执行主动过期的过程中,出现了需要大量删除过期 key 的情况,那么此时应用程序在访问 Redis 时,必须要等待这个过期任务执行结束,Redis 才可以服务这个客户端请求。

此时就会出现,应用访问 Redis 延时变大

如果此时需要过期删除的是一个 bigkey,那么这个耗时会更久。
3.2:集中过期问题的排查和分析

一般集中过期使用的是 expireat / pexpireat 命令,你需要在代码中搜索这个关键字。看是否有集中过期处理的逻辑。

  • 集中过期 key 增加一个随机过期时间,把集中过期的时间打散,降低 Redis 清理过期 key 的压力
java 复制代码
/在过期时间点之后的 5 分钟内随机过期掉
redis.expireat(key, expire_time + random(300))
  • 如果你使用的 Redis 是 4.0 以上版本,可以开启 lazy-free 机制,当删除过期 key 时,把释放内存的操作放到后台线程中执行,避免阻塞主线程
sh 复制代码
# 释放过期 key 的内存,放到后台线程执行
lazyfree-lazy-expire yes

你需要把这个指标监控起来,当这个指标在很短时间内出现了突增,需要及时报警出来,然后与业务应用报慢的时间点进行对比分析,确认时间是否一致,如果一致,则可以确认确实是因为集中过期 key 导致的延迟变大

4:实例内存达到上限

如果你的 Redis 实例设置了内存上限 maxmemory,那么也有可能导致 Redis 变慢。

4.1:变慢原理

当 Redis 内存达到 maxmemory 后,每次写入新的数据之前

Redis 必须先从实例中踢出一部分数据,让整个实例的内存维持在 maxmemory 之下,然后才能把新数据写进来。

这个踢出旧数据的逻辑也是需要消耗时间的,而具体耗时的长短,要取决于你配置的淘汰策略:

  • 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+版本支持)
4.2:解决方案
  • 避免存储 bigkey,降低释放内存的耗时
  • 淘汰策略改为随机淘汰,随机淘汰比 LRU 要快很多(视业务情况调整)
  • 拆分实例,把淘汰 key 的压力分摊到多个实例上
  • 如果使用的是 Redis 4.0 以上版本,开启 layz-free 机制,把淘汰 key 释放内存的操作放到后台线程中执行

5:fork耗时严重

5.1:变慢原因

主进程创建子进程,会调用操作系统提供的 fork 函数。

而 fork 在执行过程中,主进程需要拷贝自己的内存页表给子进程,如果实例很大,那么这个拷贝的过程也会比较耗时。

fork 过程会消耗大量的 CPU 资源,在完成 fork 之前,整个 Redis 实例会被阻塞住,无法处理任何客户端请求

5.2:分析

可以在 Redis 上执行 INFO 命令,查看 latest_fork_usec 项,单位微秒

sh 复制代码
# 上一次 fork 耗时,单位微秒
latest_fork_usec:59477
5.3:解决方案
  • 控制 Redis 实例的内存:尽量在 10G 以下,执行 fork 的耗时与实例大小有关,实例越大,耗时越久
  • 合理配置数据持久化策略:在 slave 节点执行 RDB 备份,推荐在低峰期执行,而对于丢失数据不敏感的业务(例如把 Redis 当做纯缓存使用),可以关闭 AOF 和 AOF rewrite
  • Redis 实例不要部署在虚拟机上:fork 的耗时也与系统也有关,虚拟机比物理机耗时更久
  • 降低主从库全量同步的概率:适当调大 repl-backlog-size 参数,避免主从全量同步

三:缓存问题

1:缓存穿透NN

1.1:问题描述

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。

由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

1.2:解决方案
  • 添加接口层的校验,如用户鉴权检验,如id检验,id <= 0 直接拦截
  • 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击。
  • 布隆过滤器。bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小

2:缓存击穿NO

2.1:问题描述

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期)

这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。

2.2:解决方案
  • 设置热点数据永不过期
  • 接口限流与熔断,降级。重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些服务 不可用时候,进行熔断,失败快速返回机制
  • 加互斥锁

3:缓存雪崩N'O

3.1:问题描述:

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机

和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

3.2:解决方案:
  • 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
  • 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中
  • 设置热点数据永远不过期

4:缓存污染(或者满了)

4.1:问题描述:

缓存中一些只会被访问一次或者几次的的数据,被访问完后,再也不会被访问到,但这部分数据依然留存在缓存中,消耗缓存空间

缓存污染会随着数据的持续增加而逐渐显露,随着服务的不断运行,缓存中会存在大量的永远不会再次被访问的数据。

缓存空间是有限的,如果缓存空间满了,再往缓存里写数据时就会有额外开销,影响Redis性能。

这部分额外开销主要是指写的时候判断淘汰策略,根据淘汰策略去选择要淘汰的数据,然后进行删除操作

4.2:最大缓存的设置

大容量缓存是能带来性能加速的收益,但是成本也会更高,而小容量缓存不一定就起不到加速访问的效果。一般来说,我会建议把缓存容量设置为总数据量的 15% 到 30%,兼顾访问性能和内存空间开销

对于redis来说,一旦确定要设置的大小,可以使用下面的命令进行实现:

sh 复制代码
config set maxmemory 4gb

不过,缓存被写满是不可避免的, 所以需要数据淘汰策略

相关推荐
IvorySQL2 分钟前
PostgreSQL 分区表的 ALTER TABLE 语句执行机制解析
数据库·postgresql·开源
·云扬·11 分钟前
MySQL 8.0 Redo Log 归档与禁用实战指南
android·数据库·mysql
IT邦德14 分钟前
Oracle 26ai DataGuard 搭建(RAC到单机)
数据库·oracle
惊讶的猫40 分钟前
redis分片集群
数据库·redis·缓存·分片集群·海量数据存储·高并发写
不爱缺氧i1 小时前
完全卸载MariaDB
数据库·mariadb
期待のcode1 小时前
Redis的主从复制与集群
运维·服务器·redis
纤纡.1 小时前
Linux中SQL 从基础到进阶:五大分类详解与表结构操作(ALTER/DROP)全攻略
linux·数据库·sql
jiunian_cn1 小时前
【Redis】渐进式遍历
数据库·redis·缓存
橙露1 小时前
Spring Boot 核心原理:自动配置机制与自定义 Starter 开发
java·数据库·spring boot
冰暮流星1 小时前
sql语言之分组语句group by
java·数据库·sql