知识点
- CPU 相关:使用复杂度过高命令、数据的持久化,都与耗费过多的 CPU 资源有关
- 内存相关:bigkey 内存的申请和释放、数据过期、数据淘汰、碎片整理、内存大页、内存写时复制都与内存息息相关
- 磁盘相关:数据持久化、AOF 刷盘策略,也会受到磁盘的影响
- 网络相关:短连接、实例流量过载、网络流量过载,也会降低 Redis 性能
- 计算机系统:CPU 结构、内存分配,都属于最基础的计算机系统知识
- 操作系统:写时复制、内存大页、Swap、CPU 绑定,都属于操作系统层面的知识
排查
排查点
业务服务 API 响应延迟变长,服务内部可集成链路追踪排查,记录下每次请求外部依赖的响应延时,定位至响应慢服务。
Redis可能变慢的原因:
- 业务服务器到 Redis 服务器之间的网络存在问题,例如网络线路质量不佳,网络数据包在传输时存在延迟、丢包等情况
- Redis 本身存在问题,需要进一步排查是什么原因导致 Redis 变慢
重点关注的是第二种情况。
判断原因
Redis 进行基准性能测试。
基准性能就是指 Redis 在一台负载正常的机器上,其最大的响应延迟和平均响应延迟。
Redis 在不同的软硬件环境下,它的性能是各不相同的。
具体测试实践
为了避免业务服务器到 Redis 服务器之间的网络延迟,你需要直接在 Redis 服务器上测试实例的响应延迟情况。执行以下命令,就可以测试出这个实例 60 秒内的最大响应延迟:
$ redis-cli -h 127.0.0.1 -p 6379 --intrinsic-latency 60
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.
60 秒内的最大响应延迟为 72 微秒(0.072毫秒)。
查看一段时间内 Redis 的最小、最大、平均访问延迟:
$ 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
...
每间隔 1 秒,采样 Redis 的平均操作耗时,其结果分布在 0.08 ~ 0.13 毫秒之间。
参考对比:
- 在相同配置的服务器上,测试一个正常 Redis 实例的基准性能
- 找到可能变慢的 Redis 实例,测试这个实例的基准性能
- 如果这个实例的运行延迟是正常 Redis 基准性能的 2 倍以上,即可认为这个 Redis 实例确实变慢
问题与排查
慢日志
关于Redis的性能指标与使用:Redis 性能监控指标
慢日志:统计功能,记录有哪些命令在执行时耗时比较久。
需要设置慢日志的阈值。例如,设置慢日志的阈值为 5 毫秒,并且保留最近 500 条慢日志记录:
# 命令执行耗时超过 5 毫秒,记录慢日志
CONFIG SET slowlog-log-slower-than 5000
# 只保留最近 500 条慢日志
CONFIG SET slowlog-max-len 500
使用复杂度过高的命令
查询最近记录的慢日志:
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"
...
导致操作延迟变大命令:
- 经常使用 O(N) 以上复杂度的命令,例如 SORT、SUNION、ZUNIONSTORE 聚合类命令
- 使用 O(N) 复杂度的命令,但 N 的值非常大
第一种情况导致变慢的原因在于,Redis 在操作内存数据时,时间复杂度过高,要花费更多的 CPU 资源。
第二种情况导致变慢的原因在于,Redis 一次需要返回给客户端的数据过多,更多时间花费在数据协议的组装和网络传输过程中。
资源使用率层面分析,如果应用程序操作 Redis 的 OPS 不是很大,但 Redis 实例的CPU 使用率却很高,很有可能是使用了复杂度过高的命令导致的。
Redis 是单线程处理客户端请求的,经常使用以上命令,一旦前面某个命令发生耗时,就会导致后面的请求发生排队,对于客户端来说,响应延迟也会变长。
解决:
- 尽量不使用 O(N) 以上复杂度过高的命令,对于数据的聚合操作,放在客户端做
- 执行 O(N) 命令,保证 N 尽量的小(推荐 N <= 300),每次获取尽量少的数据,让 Redis 可以及时处理返回
bigkey
像SET / DEL 这种简单命令出现在慢日志中,判断实例是否是 bigkey。
Redis 在写入数据时,需要为新的数据分配内存,相对应的,当从 Redis 中删除数据时,它会释放对应的内存空间。 如果value 非常大,分配与释放是消耗大量资源的。
扫描实例中 bigkey 的分布情况
Redis 提供了扫描 bigkey 的命令,执行以下命令就可以扫描出,一个实例中 bigkey 的分布情况,输出结果是以类型维度展示的:
$ 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 的类型,分别执行 STRLEN、LLEN、HLEN、SCARD、ZCARD 命令,来获取 String 类型的长度、容器类型(List、Hash、Set、ZSet)的元素个数。
当执行这个命令时,要注意 2 个问题:
- 对线上实例进行 bigkey 扫描时,Redis 的 OPS 会突增,为了降低扫描过程中对 Redis 的影响,最好控制一下扫描的频率,指定 -i 参数即可,它表示扫描过程中每次扫描后休息的时间间隔,单位是秒
- 扫描结果中,对于容器类型(List、Hash、Set、ZSet)的 key,只能扫描出元素最多的 key。但一个 key 的元素多,不一定表示占用内存也多,你还需要根据业务情况,进一步评估内存占用情况
解决方案
有两点可以优化:
- 业务应用尽量避免写入 bigkey
- 使用的 Redis 是 4.0 以上版本,用 UNLINK 命令替代 DEL,此命令可以把释放 key 内存的操作,放到后台线程中去执行,从而降低对 Redis 的影响
- 使用的 Redis 是 4.0 以上版本,可以开启 lazy-free 机制(lazyfree-lazy-user-del = yes),在执行 DEL 命令时,释放内存也会放到后台线程中执行
bigkey 在分片集群模式下,对于数据的迁移也会有性能影响.
集中过期
现象表现为:变慢的时间点很有规律,例如某个整点,或者每间隔多久就会发生一波延迟。
原因
Redis 的过期数据采用被动过期 + 主动过期两种策略:
- 被动过期:只有当访问某个 key 时,才判断这个 key 是否已过期,如果已过期,则从实例中删除
- 主动过期:Redis 内部维护了一个定时任务,默认每隔 100 毫秒(1秒10次)就会从全局的过期哈希表中随机取出 20 个 key,然后删除其中过期的 key,如果过期 key 的比例超过了 25%,则继续重复此过程,直到过期 key 的比例下降到 25% 以下,或者这次任务的执行耗时超过了 25 毫秒,才会退出循环
主动过期 key 的定时任务,是在 Redis 主线程中执行的,出现了需要大量删除过期 key 的情况,堵塞后续请求。
由于慢日志中只记录一个命令真正操作内存数据的耗时,所以主动过期操作需想到。
分析和排查
检查你的业务代码,是否存在集中过期 key 的逻辑。
出现 expireat / pexpireat 命令。
规避方案:
- 集中过期 key 增加一个随机过期时间,把集中过期的时间打散,降低 Redis 清理过期 key 的压力
- 使用的 Redis 是 4.0 以上版本,可以开启 lazy-free 机制,当删除过期 key 时,把释放内存的操作放到后台线程中执行,避免阻塞主线程
随机时间伪代码:
# 在过期时间点之后的 5 分钟内随机过期掉
redis.expireat(key, expire_time + random(300))
开启 lazy-free 机制:
# 释放过期 key 的内存,放到后台线程执行
lazyfree-lazy-expire yes
info命令指标:
expired_keys:代表整个实例到目前为止,累计删除过期 key 的数量。
监控此指标,当这个指标在很短时间内出现了突增,需要及时报警出来,然后与业务应用报慢的时间点进行对比分析,确认时间是否一致,如果一致,则可以确认确实是因为集中过期 key 导致的延迟变大。
实例内存达到上限
Redis 实例设置了内存上限 maxmemory + 延迟策略,那么也有可能导致 Redis 变慢。
达到了 maxmemory 后,在此之后每次写入新数据,操作延迟变大。
原因:当 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+版本支持)
一般最常使用的是 allkeys-lru / volatile-lru 淘汰策略,它们的处理逻辑是,每次从实例中随机取出一批 key(这个数量可配置),然后淘汰一个最少访问的 key,之后把剩下的 key 暂存到一个池子中,继续随机取一批 key,并与之前池子中的 key 比较,再淘汰一个最少访问的 key。以此往复,直到实例内存降到 maxmemory 之下。
Redis 的淘汰数据的逻辑与删除过期 key 的一样,也是在命令真正执行之前执行的,也就是说它也会增加我们操作 Redis 的延迟,而且,写 OPS 越高,延迟也会越明显。
如果此时Redis 实例中还存储了 bigkey,那么在淘汰删除 bigkey 释放内存时,也会耗时比较久。
如何解决
- 避免存储 bigkey,降低释放内存的耗时
- 淘汰策略改为随机淘汰,随机淘汰比 LRU 要快很多(视业务情况调整)
- 拆分实例,把淘汰 key 的压力分摊到多个实例上
- 使用的是 Redis 4.0 以上版本,开启 layz-free 机制,把淘汰 key 释放内存的操作放到后台线程中执行(配置 lazyfree-lazy-eviction = yes)
fork耗时严重
当 Redis 开启了后台 RDB 和 AOF rewrite 后,在执行时,它们都需要主进程创建出一个子进程进行数据的持久化。
主进程创建子进程,会调用操作系统提供的 fork 函数。fork 在执行过程中,主进程需要拷贝自己的内存页表给子进程,如果实例很大,那么拷贝的过程也会比较耗时。在完成 fork 之前,整个 Redis 实例会被阻塞住,无法处理任何客户端请求。
定位:在 Redis 上执行 INFO 命令,查看 latest_fork_usec 项,单位微秒。
# 上一次 fork 耗时,单位微秒 主进程在 fork 子进程期间,整个实例阻塞无法处理客户端请求的时间。
latest_fork_usec:59477
当主从节点第一次建立数据同步时,主节点也创建子进程生成 RDB,然后发给从节点进行一次全量同步。
解决方案
- 控制 Redis 实例的内存:尽量在 10G 以下,执行 fork 的耗时与实例大小有关,实例越大,耗时越久
- 合理配置数据持久化策略:在 slave 节点执行 RDB 备份,推荐在低峰期执行,而对于丢失数据不敏感的业务(例如把 Redis 当做纯缓存使用),可以关闭 AOF 和 AOF rewrite
- Redis 实例不要部署在虚拟机上:fork 的耗时也与系统也有关,虚拟机比物理机耗时更久
- 降低主从库全量同步的概率:适当调大 repl-backlog-size 参数,避免主从全量同步
在虚拟机中运行 Redis 服务器,因为和物理机共享一个物理网口,并且一台物理机可能有多个虚拟机在运行,因此在内存占用上和网络延迟方面都会有很糟糕的表现。
通过命令查看延迟时间
./redis-cli --intrinsic-latency 100
开启内存大页
应用程序向操作系统申请内存时,是按内存页进行申请的,而常规的内存页大小是 4KB。Linux 内核从 2.6.38 开始,支持了内存大页机制,该机制允许应用程序以 2MB 大小为单位,向操作系统申请内存。应用程序每次向操作系统申请的内存单位变大了,但这也意味着申请内存的耗时变长。
影响
fork时,主进程在拷贝内存数据时涉及到新内存的申请,如果此时操作系统开启了内存大页,客户端即便只修改 10B 的数据,Redis 在申请内存时也会以 2MB 为单位向操作系统申请,申请内存的耗时变长。
bigkey同样会有影响。
解决方案
需要查看 Redis 机器是否开启了内存大页:
$ cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never
输出选项是 always,就表示目前开启了内存大页机制,需要关掉它:
$ echo never > /sys/kernel/mm/transparent_hugepage/enabled
操作系统提供的内存大页机制的目的:可以在一定程序上降低应用程序申请内存的次数。
Redis 是对性能和延迟极其敏感的数据库,Redis 在每次申请内存时,耗时尽量短,建议关闭。
开启AOF
AOF 配置不合理导致性能问题。
AOF其工作原理如下:
- Redis 执行写命令后,把这个命令写入到 AOF 文件内存中(write 系统调用)
- Redis 根据配置的 AOF 刷盘策略,把 AOF 内存数据刷到磁盘上(fsync 系统调用)
为了保证 AOF 文件数据的安全性,Redis 提供了 3 种刷盘机制:
- always:主线程每次执行写操作后立即刷盘,此方案会占用比较大的磁盘 IO 资源,但数据安全性最高
- no:主线程每次写操作只写内存就返回,内存数据什么时候刷到磁盘,交由操作系统决定,此方案对性能影响最小,但数据安全性也最低,Redis 宕机时丢失的数据取决于操作系统刷盘时机
- everysec:主线程每次写操作只写内存就返回,然后由后台线程每隔 1 秒执行一次刷盘操作(触发fsync系统调用),此方案对性能影响相对较小,但当 Redis 宕机时会丢失 1 秒的数据
everysec、always都存在导致 Redis 延迟变大的情况发生。当 Redis 后台线程在执行 AOF 文件刷盘时,如果此时磁盘的 IO 负载很高,后台线程在执行刷盘操作(fsync系统调用)时就会被阻塞住。
always时,主线程接收写请求,主线程又需要把数据写到文件内存中(write 系统调用),但此时的后台子线程由于磁盘负载过高,导致 fsync 发生阻塞,迟迟不能返回,那主线程在执行 write 系统调用时,也会被阻塞住,直到后台线程 fsync 执行完成后,主线程执行 write 才能成功返回。
磁盘 IO 负载过大及解决
- 子进程正在执行 AOF rewrite,这个过程会占用大量的磁盘 IO 资源
- 有其他应用程序在执行大量的写文件操作,也会占用磁盘 IO 资源
Redis 的 AOF 后台子线程刷盘操作与子进程 AOF rewrite同时进行
解决方案:
-
不能关闭AOF rewrite。Redis 提供了一个配置项,当子进程在 AOF rewrite 期间,可以让后台子线程不执行刷盘(不触发 fsync 系统调用)操作。 相当于在 AOF rewrite 期间,临时把 appendfsync 设置为了 none。
缺点:在 AOF rewrite 期间,如果实例发生宕机,那么此时会丢失更多的数据,性能和数据安全性。
# AOF rewrite 期间,AOF 后台子线程不进行刷盘操作 # 相当于在这期间,临时把 appendfsync 设置为了 none no-appendfsync-on-rewrite yes -
如果占用磁盘资源的是其他应用程序,迁移这个程序。
建议从硬件层面来优化,更换为 SSD 磁盘,提高磁盘的 IO 能力,保证 AOF 期间有充足的磁盘资源可以使用。
绑定CPU
为提高服务性能,降低应用程序在多个 CPU 核心之间的上下文切换带来的性能损耗,通常采用的方案是进程绑定 CPU 的方式提高性能。
一般现代的服务器会有多个 CPU,而每个 CPU 又包含多个物理核心,每个物理核心又分为多个逻辑核心,每个物理核下的逻辑核共用 L1/L2 Cache。而 Redis Server 除了主线程服务客户端请求之外,还会创建子进程、子线程。其中子进程用于数据持久化,而子线程用于执行一些比较耗时操作,例如异步释放 fd、异步 AOF 刷盘、异步 lazy-free 等等。
如果把 Redis 进程只绑定了一个 CPU 逻辑核心上,那么当 Redis 在进行数据持久化时,fork 出的子进程会继承父进程的 CPU 使用偏好。
而此时的子进程会消耗大量的 CPU 资源进行数据持久化(把实例数据全部扫描出来需要耗费CPU),这就会导致子进程会与主进程发生 CPU 争抢,进而影响到主进程服务客户端请求,访问延迟变大。
这就是 Redis 绑定 CPU 带来的性能问题。
解决
不要让 Redis 进程只绑定在一个 CPU 逻辑核上,而是绑定在多个逻辑核心上,而且,绑定的多个逻辑核心最好是同一个物理核心,这样它们还可以共用 L1/L2 Cache。
当然,即便我们把 Redis 绑定在多个逻辑核心上,也只能在一定程度上缓解主线程、子进程、后台线程在 CPU 资源上的竞争。
因为这些子进程、子线程还是会在这多个逻辑核心上进行切换,存在性能损耗。
进一步优化
可以让主线程、子进程、后台线程,分别绑定在固定的 CPU 核心上,不让它们来回切换,各自使用的 CPU 资源互不影响。
Redis 在 6.0 版本可以通过以下配置,对主线程、后台线程、后台 RDB 进程、AOF rewrite 进程,绑定固定的 CPU 逻辑核心:
# Redis Server 和 IO 线程绑定到 CPU核心 0,2,4,6
server_cpulist 0-7:2
# 后台子线程绑定到 CPU核心 1,3
bio_cpulist 1,3
# 后台 AOF rewrite 进程绑定到 CPU 核心 8,9,10,11
aof_rewrite_cpulist 8-11
# 后台 RDB 进程绑定到 CPU 核心 1,10,11
# bgsave_cpulist 1,10-1
除非对 Redis 的性能有更加严苛的要求,否则不建议你绑定 CPU。
使用Swap
swap:允许把一部分内存中的数据换到磁盘上,以达到应用程序对内存使用的缓冲,这些内存数据被换到磁盘上的区域。
问题就在于,当内存中的数据被换到磁盘上后,Redis 再访问这些数据时,就需要从磁盘上读取,访问磁盘的速度要比访问内存慢。
检查 Redis 机器的内存使用情况,确认是否存在使用了 Swap。
# 先找到 Redis 的进程 ID
$ ps -aux | grep redis-server
# 查看 Redis Swap 使用情况
$ cat /proc/$pid/smaps | egrep '^(Swap|Size)'
输出结果如下:
Size: 1256 kB
Swap: 0 kB
Size: 4 kB
Swap: 0 kB
Size: 132 kB
Swap: 0 kB
Size: 63488 kB
Swap: 0 kB
Size: 132 kB
Swap: 0 kB
Size: 65404 kB
Swap: 0 kB
Size: 1921024 kB
Swap: 0 kB
...
每一行 Size 表示 Redis 所用的一块内存大小,Size 下面的 Swap 就表示这块 Size 大小的内存,有多少数据已经被换到磁盘上,如果两值相等说明这块内存的数据都已经完全被换到磁盘上。
如果是几百兆甚至上 GB 的内存被换到了磁盘上,Redis 的性能会急剧下降。
解决方案
- 增加机器的内存,让 Redis 有足够的内存可以使用
- 整理内存空间,释放出足够的内存供 Redis 使用,然后释放 Redis 的 Swap,让 Redis 重新使用内存
释放 Redis 的 Swap 过程通常要重启实例,为了避免重启实例对业务的影响,一般会先进行主从切换,然后释放旧主节点的 Swap,重启旧主节点实例,待从库数据同步完成后,再进行主从切换即可。
当 Redis 使用到 Swap 后,此时的 Redis 性能基本已达不到高性能的要求,所以你也需要提前预防这种情况。
预防的办法就是,你需要对 Redis 机器的内存和 Swap 使用情况进行监控,在内存不足或使用到 Swap 时报警出来,及时处理。
碎片整理
Redis 的数据都存储在内存中,当应用程序频繁修改 Redis 中的数据时,可能会导致 Redis 产生内存碎片。
内存碎片会降低 Redis 的内存使用率,通过执行 INFO 命令,得到这个实例的内存碎片率:
# Memory
used_memory:5709194824 // Redis 存储数据的内存大小
used_memory_human:5.32G
used_memory_rss:8264855552 // 操作系统实际分配给 Redis 进程的大小。
used_memory_rss_human:7.70G
...
mem_fragmentation_ratio:1.45
内存碎片率 = mem_fragmentation_ratio = used_memory_rss / used_memory
如果 mem_fragmentation_ratio > 1.5,说明内存碎片率已经超过了 50%,需要采取一些措施来降低内存碎片。
解决的方案
- Redis 4.0 以下版本,只能通过重启实例来解决
- Redis 4.0 版本,提供了自动碎片整理的功能,通过配置开启碎片自动整理
开启内存碎片整理,有可能会导致 Redis 性能下降:Redis 的碎片整理工作是也在主线程中执行的,当其进行碎片整理时,必然会消耗 CPU 资源,产生更多的耗时,从而影响到客户端的请求。
Redis 碎片整理的参数配置:
# 开启自动内存碎片整理(总开关)
activedefrag yes
# 内存使用 100MB 以下,不进行碎片整理
active-defrag-ignore-bytes 100mb
# 内存碎片率超过 10%,开始碎片整理
active-defrag-threshold-lower 10
# 内存碎片率超过 100%,尽最大努力碎片整理
active-defrag-threshold-upper 100
# 内存碎片整理占用 CPU 资源最小百分比
active-defrag-cycle-min 1
# 内存碎片整理占用 CPU 资源最大百分比
active-defrag-cycle-max 25
# 碎片整理期间,对于 List/Set/Hash/ZSet 类型元素一次 Scan 的数量
active-defrag-max-scan-fields 1000
网络带宽过载
如果以上产生性能问题的场景,你都规避掉了,而且 Redis 也稳定运行了很长时间,但在某个时间点之后开始,操作 Redis 突然开始变慢了,而且一直持续下去,这种情况又是什么原因导致?
此时你需要排查一下 Redis 机器的网络带宽是否过载,是否存在某个实例把整个机器的网路带宽占满的情况。
网络带宽过载的情况下,服务器在 TCP 层和网络层就会出现数据包发送延迟、丢包等情况。
Redis 的高性能,除了操作内存之外,就在于网络 IO 了,如果网络 IO 存在瓶颈,那么也会严重影响 Redis 的性能。
如果确实出现这种情况,你需要及时确认占满网络带宽 Redis 实例,如果属于正常的业务访问,那就需要及时扩容或迁移实例了,避免因为这个实例流量过大,影响这个机器的其他实例。
运维层面,你需要对 Redis 机器的各项指标增加监控,包括网络流量,在网络流量达到一定阈值时提前报警,及时确认和扩容。
其他原因
以上这些方面就是如何排查 Redis 延迟问题的思路和路径。
还有一些比较小的点,你也需要注意一下:
-
频繁短连接
业务应用应该使用长连接操作 Redis,避免频繁的短连接。
频繁的短连接会导致 Redis 大量时间耗费在连接的建立和释放上,TCP 的三次握手和四次挥手同样也会增加访问延迟。
-
运维监控
要想提前预知 Redis 变慢的情况发生,必不可少的就是做好完善的监控。
监控其实就是对采集 Redis 的各项运行时指标,通常的做法是监控程序定时采集 Redis 的 INFO 信息,然后根据 INFO 信息中的状态数据做数据展示和报警。
这里我需要提醒你的是,在写一些监控脚本,或使用开源的监控组件时,也不能掉以轻心。
在写监控脚本访问 Redis 时,尽量采用长连接的方式采集状态信息,避免频繁短连接。同时,你还要注意控制访问 Redis 的频率,避免影响到业务请求。
在使用一些开源的监控组件时,最好了解一下这些组件的实现原理,以及正确配置这些组件,防止出现监控组件发生 Bug,导致短时大量操作 Redis,影响 Redis 性能的情况发生。
我们当时就发生过,DBA 在使用一些开源组件时,因为配置和使用问题,导致监控程序频繁地与 Redis 建立和断开连接,导致 Redis 响应变慢。
-
其它程序争抢资源
Redis 机器最好专项专用,只用来部署 Redis 实例,不要部署其他应用程序,尽量给 Redis 提供一个相对「安静」的环境,避免其它程序占用 CPU、内存、磁盘资源,导致分配给 Redis 的资源不足而受到影响。
总结
使用优化
key
-
使用短的key
当然在精简的同时,不要为了key的“见名知意”。对于value有些也可精简,比如性别使用0、1。
-
设置key有效期
尽可能的利用key有效期。比如一些临时数据(短信校验码)。
-
禁用长耗时的查询命令
- 禁止使用 keys 命令
- 避免一次查询所有的成员,要使用 scan 命令进行分批的,游标式的遍历;
- 严格控制 Hash、Set、Sorted Set 等结构的数据大小;
- 将排序、并集、交集等操作放在客户端执行,以减少 Redis 服务器运行压力;
- 删除 (del) 一个大数据的时候,可能会需要很长时间,所以建议用异步删除的方式 unlink,它会启动一个新的线程来删除目标数据,而不阻塞 Redis 的主线程。
Value
-
在存到Redis之前先把你的数据压缩下
Redis 对于同一种数据类型会使用不同的内部编码进行存储,比如字符串的内部编码就有三种:int(整数编码)、raw(优化内存分配的字符串编码)、embstr(动态字符串编码),这是因为 Redis 是想通过不同编码实现效率和空间的平衡,然而数据量越大使用的内部编码就越复杂,而越是复杂的内部编码存储的性能就越低。
必要时要对数据进行序列化和压缩再存储,序列化可以使用 protostuff,压缩可以使用 snappy。
-
使用lazy free,建议都开启
lazyfree-lazy-eviction no # 当 Redis 运行内存超过 maxmeory 时,是否开启 lazy free 机制删除; lazyfree-lazy-expire no # 设置了过期时间的键值,当过期之后是否开启 lazy free 机制删除; lazyfree-lazy-server-del no # 有些指令在处理已存在的键时,会带有一个隐式的 del 键的操作,比如 rename 命令,当目标键已存在,Redis 会先删除目标键,如果这些目标键是一个 big key,就会造成阻塞删除的问题,此配置表示在这种场景中是否开启 lazy free 机制删除; slave-lazy-flush no # 针对 slave(从节点) 进行全量数据同步,slave 在加载 master 的 RDB 文件前,会运行 flushall 来清理自己的数据,它表示此时是否开启 lazy free 机制删除。
客户端
- Pipeline (管道技术) 是客户端提供的一种批处理技术,用于一次处理多个 Redis 命令,从而提高整个交互的性能。
- 尽量使用 Redis 连接池,而不是频繁创建销毁 Redis 连接,这样就可以减少网络传输次数和减少了非必要调用指令。