来源:http://kaito-kidd.com/2020/07...redis
Redis做为内存数据库,拥有很是高的性能,单个实例的QPS可以达到10W左右。但咱们在使用Redis时,常常时不时会出现访问延迟很大的状况,若是你不知道Redis的内部实现原理,在排查问题时就会一头雾水。数据库
不少时候,Redis出现访问延迟变大,都与咱们的使用不当或运维不合理致使的。缓存
这篇文章咱们就来分析一下Redis在使用过程当中,常常会遇到的延迟问题以及如何定位和分析。安全
若是在使用Redis时,发现访问延迟忽然增大,如何进行排查?网络
首先,第一步,建议你去查看一下Redis的慢日志。Redis提供了慢日志命令的统计功能,咱们经过如下设置,就能够查看有哪些命令在执行时延迟比较大。app
首先设置Redis的慢日志阈值,只有超过阈值的命令才会被记录,这里的单位是微秒,例如设置慢日志的阈值为5毫秒,同时设置只保留最近1000条慢日志记录:运维
# 命令执行超过5毫秒记录慢日志 CONFIG SET slowlog-log-slower-than 5000 # 只保留最近1000条慢日志 CONFIG SET slowlog-max-len 1000
设置完成以后,全部执行的命令若是延迟大于5毫秒,都会被Redis记录下来,咱们执行SLOWLOG get 5
查询最近5条慢日志dom
127.0.0.1:6379> SLOWLOG get5 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、zunionstore,或者在执行O(n)命令时操做的数据量比较大,这些状况下Redis处理数据时就会很耗时。异步
若是你的服务请求量并不大,但Redis实例的CPU使用率很高,颇有多是使用了复杂度高的命令致使的。性能
解决方案就是,不使用这些复杂度较高的命令,而且一次不要获取太多的数据,每次尽可能操做少许的数据,让Redis能够及时处理返回。
若是查询慢日志发现,并非复杂度较高的命令致使的,例如都是SET、DELETE操做出如今慢日志记录中,那么你就要怀疑是否存在Redis写入了大key的状况。
Redis在写入数据时,须要为新的数据分配内存,当从Redis中删除数据时,它会释放对应的内存空间。
若是一个key写入的数据很是大,Redis在分配内存时也会比较耗时
。一样的,当删除这个key的数据时,释放内存也会耗时比较久
。
你须要检查你的业务代码,是否存在写入大key的状况,须要评估写入数据量的大小,业务层应该避免一个key存入过大的数据量。
那么有没有什么办法能够扫描如今Redis中是否存在大key的数据吗?
Redis也提供了扫描大key的方法:
redis-cli -h $host -p $port --bigkeys -i 0.01
使用上面的命令就能够扫描出整个实例key大小的分布状况,它是以类型维度来展现的。
须要注意的是当咱们在线上实例进行大key扫描时,Redis的QPS会突增,为了下降扫描过程当中对Redis的影响,咱们须要控制扫描的频率,使用-i
参数控制便可,它表示扫描过程当中每次扫描的时间间隔,单位是秒。
使用这个命令的原理,其实就是Redis在内部执行scan
命令,遍历全部key,而后针对不一样类型的key执行strlen、llen、hlen、scard、zcard来获取字符串的长度以及容器类型(list/dict/set/zset)的元素个数。
而对于容器类型的key,只能扫描出元素最多的key,但元素最多的key不必定占用内存最多,这一点须要咱们注意下。不过使用这个命令通常咱们是能够对整个实例中key的分布状况有比较清晰的了解。
针对大key的问题,Redis官方在4.0版本推出了lazy-free的机制,用于异步释放大key的内存,下降对Redis性能的影响。即便这样,咱们也不建议使用大key,大key在集群的迁移过程当中,也会影响到迁移的性能,这个后面在介绍集群相关的文章时,会再详细介绍到。
有时你会发现,平时在使用Redis时没有延时比较大的状况,但在某个时间点忽然出现一波延时,并且报慢的时间点颇有规律,例如某个整点,或者间隔多久就会发生一次。
若是出现这种状况,就须要考虑是否存在大量key集中过时的状况。
若是有大量的key在某个固定时间点集中过时,在这个时间点访问Redis时,就有可能致使延迟增长。
Redis的过时策略采用主动过时+懒惰过时两种策略:
•主动过时:Redis内部维护一个定时任务,默认每隔100毫秒会从过时字典中随机取出20个key,删除过时的key,若是过时key的比例超过了25%,则继续获取20个key,删除过时的key,循环往复,直到过时key的比例降低到25%或者此次任务的执行耗时超过了25毫秒,才会退出循环
•懒惰过时:只有当访问某个key时,才判断这个key是否已过时,若是已通过期,则从实例中删除
注意,Redis的主动过时的定时任务,也是在Redis**主线程**中执行的
,也就是说若是在执行主动过时的过程当中,出现了须要大量删除过时key的状况,那么在业务访问时,必须等这个过时任务执行结束,才能够处理业务请求。此时就会出现,业务访问延时增大的问题,最大延迟为25毫秒。
并且这个访问延迟的状况,不会记录在慢日志里
。慢日志中只记录真正执行某个命令的耗时
,Redis主动过时策略执行在操做命令以前,若是操做命令耗时达不到慢日志阈值,它是不会计算在慢日志统计中的,但咱们的业务却感到了延迟增大。
此时你须要检查你的业务,是否真的存在集中过时的代码,通常集中过时使用的命令是expireat
或pexpireat
命令,在代码中搜索这个关键字就能够了。
若是你的业务确实须要集中过时掉某些key,又不想致使Redis发生抖动,有什么优化方案?
解决方案是,在集中过时时增长一个
`随机时间`,把这些须要过时的key的时间打散便可
。
伪代码能够这么写:
# 在过时时间点以后的5分钟内随机过时掉 redis.expireat(key, expire_time + random(300))
这样Redis在处理过时时,不会由于集中删除key致使压力过大,阻塞主线程。
另外,除了业务使用须要注意此问题以外,还能够经过运维手段来及时发现这种状况。
作法是咱们须要把Redis的各项运行数据监控起来,执行info
能够拿到全部的运行数据,在这里咱们须要重点关注expired_keys
这一项,它表明整个实例到目前为止,累计删除过时key的数量。
咱们须要对这个指标监控,当在很短期内这个指标出现突增
时,须要及时报警出来,而后与业务报慢的时间点对比分析,确认时间是否一致,若是一致,则能够认为确实是由于这个缘由致使的延迟增大。
有时咱们把Redis当作纯缓存使用,就会给实例设置一个内存上限maxmemory
,而后开启LRU淘汰策略。
当实例的内存达到了maxmemory
后,你会发现以后的每次写入新的数据,有可能变慢了。
致使变慢的缘由是,当Redis内存达到maxmemory
后,每次写入新的数据以前,必须先踢出一部分数据,让内存维持在maxmemory之下。
这个踢出旧数据的逻辑也是须要消耗时间的,而具体耗时的长短,要取决于配置的淘汰策略:
•allkeys-lru:无论key是否设置了过时,淘汰最近最少访问的key
•volatile-lru:只淘汰最近最少访问并设置过时的key
•allkeys-random:无论key是否设置了过时,随机淘汰
•volatile-random:只随机淘汰有设置过时的key
•allkeys-ttl:无论key是否设置了过时,淘汰即将过时的key
•noeviction:不淘汰任何key,满容后再写入直接报错
•allkeys-lfu:无论key是否设置了过时,淘汰访问频率最低的key(4.0+支持)
•volatile-lfu:只淘汰访问频率最低的过时key(4.0+支持)
备注: allkeys-xxx表示从 全部的键值中淘汰数据,而 volatile-xxx表示从设置了 过时键的键值中淘汰数据。
具体使用哪一种策略,须要根据业务场景来决定。
咱们最常使用的通常是allkeys-lru
或volatile-lru
策略,它们的处理逻辑是,每次从实例中随机取出一批key(可配置),而后淘汰一个最少访问的key,以后把剩下的key暂存到一个池子中,继续随机取出一批key,并与以前池子中的key比较,再淘汰一个最少访问的key。以此循环,直到内存降到maxmemory之下。
若是使用的是allkeys-random
或volatile-random
策略,那么就会快不少,由于是随机淘汰,那么就少了比较key访问频率时间的消耗了,随机拿出一批key后直接淘汰便可,所以这个策略要比上面的LRU策略执行快一些。
但以上这些逻辑都是在访问Redis时,真正命令执行以前执行的
,也就是它会影响咱们访问Redis时执行的命令。
另外,若是此时Redis实例中有存储大key,那么在淘汰大key释放内存时,这个耗时会更加久,延迟更大
,这须要咱们格外注意。
若是你的业务访问量很是大,而且必须设置maxmemory限制实例的内存上限,同时面临淘汰key致使延迟增大的的状况,要想缓解这种状况,除了上面说的避免存储大key、使用随机淘汰策略以外,也能够考虑拆分实例的方法来缓解,拆分实例能够把一个实例淘汰key的压力分摊到多个实例
上,能够在必定程度下降延迟。
若是你的Redis开启了自动生成RDB和AOF重写功能,那么有可能在后台生成RDB和AOF重写时致使Redis的访问延迟增大,而等这些任务执行完毕后,延迟状况消失。
遇到这种状况,通常就是执行生成RDB和AOF重写任务致使的。
生成RDB和AOF都须要父进程fork
出一个子进程进行数据的持久化,在fork执行过程当中,父进程须要拷贝**内存页表**给子进程,若是整个实例内存占用很大,那么须要拷贝的内存页表会比较耗时,此过程会消耗大量的CPU资源,在完成fork以前,整个实例会被阻塞住,没法处理任何请求,若是此时CPU资源紧张,那么fork的时间会更长,甚至达到秒级。这会严重影响Redis的性能
。
咱们能够执行info命令,查看最后一次fork执行的耗时latest_fork_usec,单位微秒。这个时间就是整个实例阻塞没法处理请求的时间。
除了由于备份的缘由生成RDB以外,在主从节点第一次创建数据同步时
,主节点也会生成RDB文件给从节点进行一次全量同步,这时也会对Redis产生性能影响。
要想避免这种状况,咱们须要规划好数据备份的周期,建议在从节点
上执行备份,并且最好放在**低峰期**执行
。若是对于丢失数据不敏感的业务,那么不建议开启AOF和AOF重写功能。
另外,fork的耗时也与系统有关,若是把Redis部署在虚拟机上,那么这个时间也会增大。因此使用Redis时建议部署在物理机上,下降fork的影响。
不少时候,咱们在部署服务时,为了提升性能,下降程序在使用多个CPU时上下文切换的性能损耗,通常会采用进程绑定CPU的操做。
但在使用Redis时,咱们不建议这么干,缘由以下:
绑定CPU的Redis,在进行数据持久化时,fork出的子进程,子进程会继承父进程的CPU使用偏好,而此时子进程会消耗大量的CPU资源进行数据持久化, 子进程会与主进程发生CPU争抢,这也会致使主进程的CPU资源不足访问延迟增大。
因此在部署Redis进程时,若是须要开启RDB和AOF重写机制,必定不能进行CPU绑定操做!
上面提到了,当执行AOF文件重写时会由于fork执行耗时致使Redis延迟增大,除了这个以外,若是开启AOF机制,设置的策略不合理,也会致使性能问题。
开启AOF后,Redis会把写入的命令实时写入到文件中,但写入文件的过程是先写入内存,等内存中的数据超过必定阈值或达到必定时间后,内存中的内容才会被真正写入到磁盘中。
AOF为了保证文件写入磁盘的安全性,提供了3种刷盘机制:
•appendfsync always:每次写入都刷盘,对性能影响最大,占用磁盘IO比较高,数据安全性最高
•appendfsync everysec:1秒刷一次盘,对性能影响相对较小,节点宕机时最多丢失1秒的数据
•appendfsync no:按照操做系统的机制刷盘,对性能影响最小,数据安全性低,节点宕机丢失数据取决于操做系统刷盘机制
当使用第一种机制appendfsync always时,Redis每处理一次写命令,都会把这个命令写入磁盘,并且这个操做是在主线程中执行的
。
内存中的的数据写入磁盘,这个会加剧磁盘的IO负担,操做磁盘成本要比操做内存的代价大得多。若是写入量很大,那么每次更新都会写入磁盘,此时机器的磁盘IO就会很是高,拖慢Redis的性能,所以咱们不建议使用这种机制。
与第一种机制对比,appendfsync everysec会每隔1秒刷盘,而appendfsync no取决于操做系统的刷盘时间,安全性不高。所以咱们推荐使用appendfsync everysec这种方式,在最坏的状况下,只会丢失1秒的数据,但它能保持较好的访问性能。
固然,对于有些业务场景,对丢失数据并不敏感,也能够不开启AOF。
若是你发现Redis忽然变得很是慢,每次访问的耗时都达到了几百毫秒甚至秒级,那此时就检查Redis是否使用到了Swap,这种状况下Redis基本上已经没法提供高性能的服务。
咱们知道,操做系统提供了Swap机制,目的是为了当内存不足时,能够把一部份内存中的数据换到磁盘上,以达到对内存使用的缓冲。
但当内存中的数据被换到磁盘上后,访问这些数据就须要从磁盘中读取,这个速度要比内存慢太多!
尤为是针对Redis这种高性能的内存数据库来讲,若是Redis中的内存被换到磁盘上,对于Redis这种性能极其敏感的数据库,这个操做时间是没法接受的。
咱们须要检查机器的内存使用状况,确认是否确实是由于内存不足致使使用到了Swap。
若是确实使用到了Swap,要及时整理内存空间,释放出足够的内存供Redis使用,而后释放Redis的Swap,让Redis从新使用内存。
释放Redis的Swap过程一般要重启实例,为了不重启实例对业务的影响,通常先进行主从切换,而后释放旧主节点的Swap,从新启动服务,待数据同步完成后,再切换回主节点便可。
可见,当Redis使用到Swap后,此时的Redis的高性能基本被废掉,因此咱们须要提早预防这种状况。
咱们须要对Redis机器的内存和Swap使用状况进行监控,在内存不足和使用到Swap时及时报警出来,及时进行相应的处理。
若是以上产生性能问题的场景,你都规避掉了,并且Redis也稳定运行了很长时间,但在某个时间点以后开始,访问Redis开始变慢了,并且一直持续到如今,这种状况是什么缘由致使的?
以前咱们就遇到这种问题,特色就是从某个时间点以后就开始变慢,而且一直持续。这时你须要检查一下机器的网卡流量,是否存在网卡流量被跑满的状况。
网卡负载太高,在网络层和TCP层就会出现数据发送延迟、数据丢包等状况。Redis的高性能除了内存以外,就在于网络IO,请求量突增会致使网卡负载变高。
若是出现这种状况,你须要排查这个机器上的哪一个Redis实例的流量过大占满了网络带宽,而后确认流量突增是否属于业务正常状况,若是属于那就须要及时扩容或迁移实例,避免这个机器的其余实例受到影响。
运维层面,咱们须要对机器的各项指标增长监控,包括网络流量,在达到阈值时提早报警,及时与业务确认并扩容。
以上咱们总结了Redis中常见的可能致使延迟增大甚至阻塞的场景,这其中既涉及到了业务的使用问题,也涉及到Redis的运维问题。
可见,要想保证Redis高性能的运行,其中涉及到CPU、内存、网络,甚至磁盘的方方面面,其中还包括操做系统的相关特性的使用。
做为开发人员,咱们须要了解Redis的运行机制,例如各个命令的执行时间复杂度、数据过时策略、数据淘汰策略等,使用合理的命令,并结合业务场景进行优化。
做为DBA运维人员,须要了解数据持久化、操做系统fork原理、Swap机制等,并对Redis的容量进行合理规划,预留足够的机器资源,对机器作好完善的监控,才能保证Redis的稳定运行。