10.使用Swap
如果你发现 Redis 突然变得非常慢,每次的操作耗时都达到了几百毫秒甚至秒级,那此时你就需要检查 Redis 是否使用到了 Swap,在这种情况下 Redis 基本上已经无法提供高性能的服务了。
什么是 Swap?为什么使用 Swap 会导致 Redis 的性能下降?
如果你对操作系统有些了解,就会知道操作系统为了缓解内存不足对应用程序的影响,允许把一部分内存中的数据换到磁盘上,以达到应用程序对内存使用的缓冲,这些内存数据被换到磁盘上的区域,就是 Swap。
牛逼啊!接私活必备的 N 个开源项目!赶快收藏
问题就在于,当内存中的数据被换到磁盘上后,Redis 再访问这些数据时,就需要从磁盘上读取,访问磁盘的速度要比访问内存慢几百倍!
尤其是针对 Redis 这种对性能要求极高、性能极其敏感的数据库来说,这个操作延时是无法接受的。
此时,你需要检查 Redis 机器的内存使用情况,确认是否存在使用了 Swap。
你可以通过以下方式来查看 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 大小的内存,有多少数据已经被换到磁盘上了,如果这两个值相等,说明这块内存的数据都已经完全被换到磁盘上了。
如果只是少量数据被换到磁盘上,例如每一块 Swap 占对应 Size 的比例很小,那影响并不是很大。如果是几百兆甚至上 GB 的内存被换到了磁盘上,那么你就需要警惕了,这种情况 Redis 的性能肯定会急剧下降。
11.碎片整理
Redis 的数据都存储在内存中,当我们的应用程序频繁修改 Redis 中的数据时,就有可能会导致 Redis 产生内存碎片。
内存碎片会降低 Redis 的内存使用率,我们可以通过执行 INFO 命令,得到这个实例的内存碎片率:
# Memory used_memory:5709194824 used_memory_human:5.32G used_memory_rss:8264855552 used_memory_rss_human:7.70G ... mem_fragmentation_ratio:1.45
这个内存碎片率是怎么计算的?
很简单,mem_fragmentation_ratio = used_memory_rss / used_memory。
其中 used_memory 表示 Redis 存储数据的内存大小,而 used_memory_rss 表示操作系统实际分配给 Redis 进程的大小。
如果 mem_fragmentation_ratio > 1.5,说明内存碎片率已经超过了 50%,这时我们就需要采取一些措施来降低内存碎片了。
解决的方案一般如下:
1)如果你使用的是 Redis 4.0 以下版本,只能通过重启实例来解决
2)如果你使用的是 Redis 4.0 版本,它正好提供了自动碎片整理的功能,可以通过配置开启碎片自动整理。
但是,开启内存碎片整理,它也有可能会导致 Redis 性能下降。
原因在于,Redis 的碎片整理工作是也在主线程中执行的,当其进行碎片整理时,必然会消耗 CPU 资源,产生更多的耗时,从而影响到客户端的请求。
所以,当你需要开启这个功能时,最好提前测试评估它对 Redis 的影响。
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如何优化
1.慢查询优化
1)尽量不使用 O(N) 以上复杂度过高的命令,对于数据的聚合操作,放在客户端做。
2)执行 O(N) 命令,保证 N 尽量的小(推荐 N <= 300),每次获取尽量少的数据,让 Redis 可以及时处理返回。
2.集中过期优化
一般有两种方案来规避这个问题:
1.集中过期 key 增加一个随机过期时间,把集中过期的时间打散,降低 Redis 清理过期 key 的压力
2.如果你使用的 Redis 是 4.0 以上版本,可以开启 lazy-free 机制,当删除过期 key 时,把释放内存的操作放到后台线程中执行,避免阻塞主线程。
第一种方案,在设置 key 的过期时间时,增加一个随机时间,伪代码可以这么写:
# 在过期时间点之后的 5 分钟内随机过期掉
redis.expireat(key, expire_time + random(300))
第二种方案,Redis 4.0 以上版本,开启 lazy-free 机制:
# 释放过期 key 的内存,放到后台线程执行
lazyfree-lazy-expire yes
运维层面,你需要把 Redis 的各项运行状态数据监控起来,在 Redis 上执行 INFO 命令就可以拿到这个实例所有的运行状态数据。
在这里我们需要重点关注 expired_keys 这一项,它代表整个实例到目前为止,累计删除过期 key 的数量。
你需要把这个指标监控起来,当这个指标在很短时间内出现了突增,需要及时报警出来,然后与业务应用报慢的时间点进行对比分析,确认时间是否一致,如果一致,则可以确认确实是因为集中过期 key 导致的延迟变大。
3.实例内存达到上限优化
1)避免存储 bigkey,降低释放内存的耗时
2)淘汰策略改为随机淘汰,随机淘汰比 LRU 要快很多(视业务情况调整)
3)拆分实例,把淘汰 key 的压力分摊到多个实例上
4)如果使用的是 Redis 4.0 以上版本,开启 layz-free 机制,把淘汰 key 释放内存的操作放到后台线程中执行(配置 lazyfree-lazy-eviction = yes)
4.fork耗时严重优化
1)控制 Redis 实例的内存:尽量在 10G 以下,执行 fork 的耗时与实例大小有关,实例越大,耗时越久。
2)合理配置数据持久化策略:在 slave 节点执行 RDB 备份,推荐在低峰期执行,而对于丢失数据不敏感的业务(例如把 Redis 当做纯缓存使用),可以关闭 AOF 和 AOF rewrite。
3)Redis 实例不要部署在虚拟机上:fork 的耗时也与系统也有关,虚拟机比物理机耗时更久。
4)降低主从库全量同步的概率:适当调大 repl-backlog-size 参数,避免主从全量同步。
从建立同步时,优先检测是否可以尝试只同步部分数据,这种情况就是针对于之前已经建立好了复制链路,只是因为故障导致临时断开,故障恢复后重新建立同步时,为了避免全量同步的资源消耗,Redis会优先尝试部分数据同步,如果条件不符合,才会触发全量同步。
这个判断依据就是在master上维护的复制缓冲区大小,如果这个缓冲区配置的过小,很有可能在主从断开复制的这段时间内,master产生的写入导致复制缓冲区的数据被覆盖,重新建立同步时的slave需要同步的offset位置在master的缓冲区中找不到,那么此时就会触发全量同步。
如何避免这种情况?解决方案就是适当调大复制缓冲区repl-backlog-size的大小,这个缓冲区的大小默认为1MB,如果实例写入量比较大,可以针对性调大此配置。
5.多核CPU优化
那如何解决这个问题呢?
如果你确实想要绑定 CPU,可以优化的方案是,不要让 Redis 进程只绑定在一个 CPU 逻辑核上,而是绑定在多个逻辑核心上,而且,绑定的多个逻辑核心最好是同一个物理核心,这样它们还可以共用 L1/L2 Cache。
当然,即便我们把 Redis 绑定在多个逻辑核心上,也只能在一定程度上缓解主线程、子进程、后台线程在 CPU 资源上的竞争。另外,搜索公众号Linux就该这样学后台回复“猴子”,获取一份惊喜礼包。
因为这些子进程、子线程还是会在这多个逻辑核心上进行切换,存在性能损耗。
如何再进一步优化?
可能你已经想到了,我们是否可以让主线程、子进程、后台线程,分别绑定在固定的 CPU 核心上,不让它们来回切换,这样一来,他们各自使用的 CPU 资源互不影响。
其实,这个方案 Redis 官方已经想到了。
Redis 在 6.0 版本已经推出了这个功能,我们可以通过以下配置,对主线程、后台线程、后台 RDB 进程、AOF rewrite 进程,绑定固定的 CPU 逻辑核心:
Redis6.0 前绑定CPU核
taskset -c 0 ./redis-server
Redis6.0 后绑定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 6.0 版本,就可以通过以上配置,来进一步提高 Redis 性能。
这里我需要提醒你的是,一般来说,Redis 的性能已经足够优秀,除非你对 Redis 的性能有更加严苛的要求,否则不建议你绑定 CPU。
6.查看Redis内存是否发生Swap
$ redis-cli info | grep process_id process_id: 5332
然后,进入 Redis 所在机器的 /proc 目录下的该进程目录中:
$ cd /proc/5332
最后,运行下面的命令,查看该 Redis 进程的使用情况。在这儿,我只截取了部分结果:
$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
一旦发生内存 swap,最直接的解决方法就是增加机器内存。如果该实例在一个 Redis 切片集群中,可以增加 Redis 集群的实例个数,来分摊每个实例服务的数据量,进而减少每个实例所需的内存量。
7.内存大页
如果采用了内存大页,那么,即使客户端请求只修改 100B 的数据,Redis 也需要拷贝 2MB 的大页。相反,如果是常规内存页机制,只用拷贝 4KB。两者相比,你可以看到,当客户端请求修改或新写入数据较多时,内存大页机制将导致大量的拷贝,这就会影响 Redis 正常的访存操作,最终导致性能变慢。
首先,我们要先排查下内存大页。方法是:在 Redis 实例运行的机器上执行如下命令:
$ cat /sys/kernel/mm/transparent_hugepage/enabled [always] madvise never
如果执行结果是 always,就表明内存大页机制被启动了;如果是 never,就表示,内存大页机制被禁止。
在实际生产环境中部署时,我建议你不要使用内存大页机制,操作也很简单,只需要执行下面的命令就可以了:
echo never /sys/kernel/mm/transparent_hugepage/enabled
其实,操作系统提供的内存大页机制,其优势是,可以在一定程序上降低应用程序申请内存的次数。
但是对于 Redis 这种对性能和延迟极其敏感的数据库来说,我们希望 Redis 在每次申请内存时,耗时尽量短,所以我不建议你在 Redis 机器上开启这个机制。
8.删除使用Lazy Free
支持版本:Redis 4.0+
1)主动删除键使用lazy free
UNLINK命令
127.0.0.1:7000> LLEN mylist (integer) 2000000 127.0.0.1:7000> UNLINK mylist (integer) 1 127.0.0.1:7000> SLOWLOG get 1) 1) (integer) 1 2) (integer) 1505465188 3) (integer) 30 4) 1) "UNLINK" 2) "mylist" 5) "127.0.0.1:17015" 6) ""
注意:DEL命令,还是并发阻塞的删除操作
FLUSHALL/FLUSHDB ASYNC
127.0.0.1:7000> DBSIZE (integer) 1812295 127.0.0.1:7000> flushall //同步清理实例数据,180万个key耗时1020毫秒 OK (1.02s) 127.0.0.1:7000> DBSIZE (integer) 1812637 127.0.0.1:7000> flushall async //异步清理实例数据,180万个key耗时约9毫秒 OK 127.0.0.1:7000> SLOWLOG get 1) 1) (integer) 2996109 2) (integer) 1505465989 3) (integer) 9274 //指令运行耗时9.2毫秒 4) 1) "flushall" 2) "async" 5) "127.0.0.1:20110" 6) ""
2)被动删除键使用lazy free
lazy free应用于被动删除中,目前有4种场景,每种场景对应一个配置参数;默认都是关闭。
lazyfree-lazy-eviction no lazyfree-lazy-expire no lazyfree-lazy-server-del no slave-lazy-flush no
针对redis内存使用达到maxmeory,并设置有淘汰策略时;在被动淘汰键时,是否采用lazy free机制;因为此场景开启lazy free, 可能使用淘汰键的内存释放不及时,导致redis内存超用,超过maxmemory的限制。此场景使用时,请结合业务测试。(生产环境不建议设置yes)
lazyfree-lazy-expire
针对设置有TTL的键,达到过期后,被redis清理删除时是否采用lazy free机制;此场景建议开启,因TTL本身是自适应调整的速度。
lazyfree-lazy-server-del
针对有些指令在处理已存在的键时,会带有一个隐式的DEL键的操作。如rename命令,当目标键已存在,redis会先删除目标键,如果这些目标键是一个big key,那就会引入阻塞删除的性能问题。此参数设置就是解决这类问题,建议可开启。
slave-lazy-flush
针对slave进行全量数据同步,slave在加载master的RDB文件前,会运行flushall来清理自己的数据场景, 参数设置决定是否采用异常flush机制。如果内存变动不大,建议可开启。可减少全量同步耗时,从而减少主库因输出缓冲区爆涨引起的内存使用增长。
3)lazy free的监控
lazy free能监控的数据指标,只有一个值:lazyfree_pending_objects,表示redis执行lazy free操作,在等待被实际回收内容的键个数。并不能体现单个大键的元素个数或等待lazy free回收的内存大小。所以此值有一定参考值,可监测redis lazy free的效率或堆积键数量;比如在flushall async场景下会有少量的堆积。
# info memory # Memory lazyfree_pending_objects:0
注意事项:unlink命令入口函数unlinkCommand()和del调用相同函数delGenericCommand()进行删除KEY操作,使用lazy标识是否为lazyfree调用。如果是lazyfree,则调用dbAsyncDelete()函数。
但并非每次unlink命令就一定启用lazy free,redis会先判断释放KEY的代价(cost),当cost大于LAZYFREE_THRESHOLD(64)才进行lazy free.
释放key代价计算函数lazyfreeGetFreeEffort(),集合类型键,且满足对应编码,cost就是集合键的元数个数,否则cost就是1。
举例:
一个包含100元素的list key, 它的free cost就是100
一个512MB的string key, 它的free cost是1 所以可以看出,redis的lazy free的cost计算主要时间复杂度相关。
9.AOF优化
Redis 提供了一个配置项,当子进程在 AOF rewrite 期间,可以让后台子线程不执行刷盘(不触发 fsync 系统调用)操作。
这相当于在 AOF rewrite 期间,临时把 appendfsync 设置为了 none,配置如下:
# AOF rewrite 期间,AOF 后台子线程不进行刷盘操作 # 相当于在这期间,临时把 appendfsync 设置为了 none no-appendfsync-on-rewrite yes
当然,开启这个配置项,在 AOF rewrite 期间,如果实例发生宕机,那么此时会丢失更多的数据,性能和数据安全性,你需要权衡后进行选择。
如果占用磁盘资源的是其他应用程序,那就比较简单了,你需要定位到是哪个应用程序在大量写磁盘,然后把这个应用程序迁移到其他机器上执行就好了,避免对 Redis 产生影响。
当然,如果你对 Redis 的性能和数据安全都有很高的要求,那么建议从硬件层面来优化,更换为 SSD 磁盘,提高磁盘的 IO 能力,保证 AOF 期间有充足的磁盘资源可以使用。同时尽可能让Redis运行在独立的机器上。
10.Swap优化
1)增加机器的内存,让 Redis 有足够的内存可以使用
2)整理内存空间,释放出足够的内存供 Redis 使用,然后释放 Redis 的 Swap,让 Redis 重新使用内存
释放 Redis 的 Swap 过程通常要重启实例,为了避免重启实例对业务的影响,一般会先进行主从切换,然后释放旧主节点的 Swap,重启旧主节点实例,待从库数据同步完成后,再进行主从切换即可。
预防的办法就是,你需要对 Redis 机器的内存和 Swap 使用情况进行监控,在内存不足或使用到 Swap 时报警出来,及时处理。
三、Redis变慢了排查步骤
1.获取 Redis 实例在当前环境下的基线性能。
2.是否用了慢查询命令?如果是的话,就使用其他命令替代慢查询命令,或者把聚合计算命令放在客户端做。
3.是否对过期 key 设置了相同的过期时间?对于批量删除的 key,可以在每个 key 的过期时间上加一个随机数,避免同时删除。
4.是否存在 bigkey?对于 bigkey 的删除操作,如果你的 Redis 是 4.0 及以上的版本,可以直接利用异步线程机制减少主线程阻塞;如果是 Redis 4.0 以前的版本,可以使用 SCAN 命令迭代删除;对于 bigkey 的集合查询和聚合操作,可以使用 SCAN 命令在客户端完成。
5.Redis AOF 配置级别是什么?业务层面是否的确需要这一可靠性级别?如果我们需要高性能,同时也允许数据丢失,可以将配置项 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 实例和网络中断处理程序运行在同一个 CPU Socket 上。