Redis 性能优化

概述

当我们操作 Redis 发现耗时较长时,原因可能有两个:

  • 服务间存在网络延迟
  • Redis 服务本身存在问题

如果是第一种情况,那么所有服务都会发生网络延迟,只需要联系运维处理即可,这里主要讨论第二种情况


Redis 基准性能测试

基准性能指 Redis 在一台负载正常的机器上的最大响应延迟和平均响应延迟,我们可以找一台同配置的机器,与原机器比较基准性能,看看 Redis 是不是真的变慢了

从 Redis 2.8.7 开始,redis-cli 命令可以追加 –intrinsic-latency 选项,用于监测和统计某个时间段内 Redis 的最大延迟

# 60 指的是测试时长为 60s,可以任意指定
redis-cli -h 127.0.0.1 -p 6379 --intrinsic-latency 60
Max latency so far: 1 microseconds.
Max latency so far: 157 microseconds.
Max latency so far: 173 microseconds.
Max latency so far: 323 microseconds.
Max latency so far: 324 microseconds.
Max latency so far: 325 microseconds.
Max latency so far: 334 microseconds.
Max latency so far: 520 microseconds.
Max latency so far: 527 microseconds.
Max latency so far: 591 microseconds.
Max latency so far: 1178 microseconds.
Max latency so far: 2299 microseconds.
Max latency so far: 2881 microseconds.
Max latency so far: 4113 microseconds.

597018388 total runs (avg latency: 0.1005 microseconds / 100.50 nanoseconds per run).
Worst run took 40926x longer than the average latency.

从输出结果可以看出 60s 内最大延迟是 4113 微秒

使用以下命令,查看 Redis 的最小、最大、平均访问延迟

``shell
redis-cli -h 127.0.0.1 -p 6379 --latency
min: 0, max: 6, avg: 0.24 (6064 samples)

按 ctrl + c 结束命令,可见平均延迟为0.24ms,共统计了 6064 个样本数据

还可以使用以下命令,查看一段时间内 Redis 的最小、最大、平均访问延迟

```shell
redis-cli -h 127.0.0.1 -p 6379 --latency-history -i 1
min: 0, max: 1, avg: 0.29 (98 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.25 (96 samples) -- 1.00 seconds range
min: 0, max: 1, avg: 0.28 (96 samples) -- 1.00 seconds range
min: 0, max: 1, avg: 0.26 (96 samples) -- 1.00 seconds range
min: 0, max: 1, avg: 0.27 (97 samples) -- 1.00 seconds range
...

以上输出结果是,每隔 1 秒采样 Redis 的平均操作耗时,其结果分布在 0.25 ~ 0.29ms 之间

通过以上命令,我们可以在相同配置的服务器上测试比较 Redis 的基准性能,找到可能变慢了的 Redis 实例


原因分析

找到目标,接下来分析可能导致 Redis 变慢的因素

1. 使用慢日志查询耗时命令

Redis 提供了慢日志命令的统计功能,它记录了有哪些命令在执行时耗时比较久

查看 Redis 慢日志之前,你需要设置慢日志的阈值,例如,设置慢日志的阈值为 10ms,并且保留最近 128 条慢日志记录

在 redis.conf 中设置,重启 Redis 实例来生效

slowlog-log-slower-than 10000
slowlog-max-len 128

也通过 CONFIG SET 命令动态设置

CONFIG SET slowlog-log-slower-than 10000
CONFIG SET slowlog-max-len 128

执行 slowlog get 命令查询最近记录的慢日志,可以指定返回条数,不指定默认十条

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"
.....

通过查看慢日志,我们就可以知道在什么时间点,执行了哪些命令比较耗时,一般原因有以下两个:

  • 使用复杂度过高的命令,如 SORT、SUNION、ZUNIONSTORE 等聚合类命令,
  • 查询返回数据量过大

2. 操作 bigkey

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

Redis 在写入数据时,需要为新的数据分配内存,相对应的,当从 Redis 中删除数据时,它会释放对应的内存空间。如果一个 key 写入的 value 非常大,那么 Redis 在分配内存时就会比较耗时。同样的,当删除这个 key 时,释放内存也会比较耗时,这种类型的 key 我们一般称之为 bigkey

Redis 提供了扫描 bigkey 的命令,用于扫描一个实例 bigkey 的分布情况

redis-cli -h 127.0.0.1 -p 6379 --bigkeys

...
-------- 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)
  • 该命令使用 scan 方式对 key 进行统计,所以无需担心造成阻塞,但可能会对 Redis 实例造成一定的负担
  • 输出大概分为两部分,summary 之上的部分只是显示了扫描的过程,summary 部分给出了每种数据结构中最大的 Key
  • 统计的 bigkey 只有 string 类型是以字节长度来衡量,list、set、zset 等都是以元素个数作为衡量,不能说明其占用内存就一定多

针对 bigkey 导致延迟的问题,有两点可以优化:

  • 将 bigkey 拆分成多个小的 key,需要修改应用程序的代码
  • 使用压缩算法来减小对象的大小
  • 如果你使用的 Redis 是 4.0 以上版本,用 unlink 命令替代 del 命令异步删除
  • 如果你使用的 Redis 是 6.0 以上版本,可以开启 lazy-free 机制(lazyfree-lazy-user-del = yes),在执行 del 命令时,释放内存也会放到后台线程中执行

3. 集中过期

如果有大量的 key 在某个固定时间点集中过期,在这个时间点访问 Redis 时,就有可能导致延时变大,如果此时需要过期删除的是一个 bigkey,那么这个耗时会更久

解决办法是分散过期时间,可以为集中过期 key 增加一个随机过期时间。如果你使用的 Redis 是 4.0 以上版本,可以开启 lazy-free 机制(lazyfree-lazy-expire yes),当删除过期 key 时,把释放内存的操作放到后台线程中执行,避免阻塞主线程

4. 可用内存达到上限

Redis 可以设置最大可用内存(maxmemory),默认是实例内存。当 Redis 达到 maxmemory 时,Redis 必须清除部分数据,从而造成延迟。如果存储了 bigkey,那么耗时会更久

使用 info memory 命令查看 Redis 内存占用情况,比较 used_memory 和 maxmemory

localhost:6379> info memory
used_memory:692264  # redis 服务器分配的内存总量,也就是内部存储数据的内存占用量
used_memory_human:676.04K  # 以可读形式返回 used_memory
used_memory_rss:655336  # 从操作系统的角度返回 redis 进程占用的物理内存总量
.....
maxmemory:0  # redis 能使用的最大内存上限,0 表示没有上限
maxmemory_human:0B  # 以可读形式返回 maxmemory

可以调整 Redis 的内存淘汰策略,比如改为随机淘汰,随机淘汰速度要快很多,尽可能减少耗时。如果使用的是 Redis 4.0 以上版本,开启 layz-free 机制,把淘汰 key 释放内存的操作放到后台线程中执行(lazyfree-lazy-eviction = yes)

5. fork 耗时

当 Redis 开启了后台 RDB 和 AOF rewrite 后,需要主进程创建出一个子进程进行数据的持久化。主进程创建子进程,会调用操作系统提供的 fork 函数。而 fork 在执行过程中,主进程需要拷贝自己的内存页表给子进程,如果这个实例很大,那么这个拷贝的过程也会比较耗时

可以执行 info stats 命令获取到 latest_fork_usec 指标,表示 Redis 最近一次 fork 操作耗时,如果耗时很大,比如超过1秒,则需要做出优化调整

localhost:6379> info stats
...
latest_fork_usec:59477  # 上一次 fork 耗时,单位微秒
...

推荐在低峰期进行备份,而对于丢失数据不敏感的业务(例如把 Redis 当做纯缓存使用)可以关闭持久化

热门相关:重生隐婚:Hi,高冷权少!   重生隐婚:Hi,高冷权少!   惹火甜妻:老公大人,宠上瘾!   天河大帝   顺明