当前位置: 首页 > 科技观察

为什么Redis很慢?常见延迟问题定位及分析

时间:2023-03-12 05:57:17 科技观察

Redis作为内存数据库,性能非常高,单实例QPS可达10W左右。但是我们在使用Redis的时候,经常会时不时的出现访问延迟比较大的情况。如果不了解Redis的内部实现原理,在排错的时候就会一头雾水。很多时候,Redis的访问延迟变大,是我们使用不当或者运维不合理造成的。在本文中,我们将分析Redis在使用过程中经常遇到的延迟问题,以及如何定位分析。复杂命令访问时延突然变大怎么办?首先第一步,建议大家查看一下Redis的慢日志。Redis提供了慢日志命令的统计功能。通过下面的设置,我们可以查看哪些命令的执行延迟比较大。首先设置Redis的慢日志阈值。只有超过阈值的命令才会被记录。这里的单位是微秒。例如设置慢日志阈值为5毫秒,并设置只保留最后1000条慢日志记录:#命令执行超过5毫秒记录慢日志CONFIGSETslowlog-log-slower-than5000#只保留最后1000条慢日志.设置CONFIGSETslowlog-max-len1000后,如果延迟大于5毫秒,所有执行过的命令都会被Redis记录下来。我们执行SLOWLOGget5查询最近5条慢日志127.0.0.1:6379>SLOWLOGget51)1)(integer)32693#SlowlogID2)(integer)1593763337#Executiontime3)(integer)5299#Executiontime(microseconds)4)1)"LRANGE"#具体执行命令和参数2)"user_list_2000"3)"0"4)"-1"2)1)(integer)326922)(integer)15937633373)(integer)50444)1)"GET"2)"book_price_1000"...通过查看慢日志记录,我们可以知道哪些命令在什么时候执行是耗时的。如果你的业务经常使用复杂度在O(n)以上的命令,比如sort、sunion、zunionstore,或者执行O(n)命令时,操作的数据量比较大。在这些情况下,Redis处理数据将非常耗时。如果您的服务请求量不大,但Redis实例的CPU占用率很高,很可能是使用了复杂的命令导致的。解决的办法就是不要使用这些复杂的命令,也不要一次获取太多的数据,尽量每次操作少量的数据,让Redis及时处理返回。存储大键如果查询慢日志,发现不是复杂命令引起的,比如慢日志记录中出现了SET、DELETE操作,那么就要怀疑Redis是否写入了大键。Redis在写数据的时候,需要为新的数据分配内存。Redis删除数据时,会释放相应的内存空间。如果一个key写入的数据非常大,Redis需要时间分配内存。同样,当这个key的数据被删除后,释放内存也需要很长时间。你需要检查你的业务代码,看是否有写入大key的情况,你需要评估写入的数据量。业务层应避免在一个键中存储大量数据。那么现在有什么办法可以扫描Redis中是否有大key数据呢?Redis也提供了扫描bigkey的方式:redis-cli-h$host-p$port--bigkeys-i0.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性能的影响。即便如此,我们也不建议使用大键。在集群迁移过程中,大键也会影响迁移的性能。这个在后面集群相关的文章中会详细介绍。集中过期有时候你会发现在使用Redis的时候并没有比较大的延迟,而是在某个时间点突然出现了一波延迟,而且慢报的时间点是很有规律的,比如某个小时,或者它多久发生一次。如果出现这种情况,需要考虑是否存在大量密钥过期。如果大量键在固定时间点过期,则在该时间点访问Redis可能会导致延迟增加。Redis的过期策略采用主动过期+惰性过期两种策略:主动过期:Redis内部维护一个定时任务。默认情况下,每100毫秒从过期字典中随机取出20个键,过期键被删除。如果比例超过25%,继续获取20个key,删除过期key,如此循环,直到过期key比例下降到25%或者本次任务执行时间超过25毫秒,退出循环。Lazyexpiration:只有访问一个key的时候,才判断key是否过期。如果它已过期,它将从实例中删除。注意,Redis的主动过期定时任务也是在Redis主线程中执行的,也就是说,如果在实现主动过期的过程中,需要删除大量的过期键。那么在业务访问时,必须先执行过期任务,才能处理业务请求。这时候就会出现服务访问延迟增加的问题,最大延迟25毫秒。而这个访问延迟是不会记录在slowlog中的。慢日志只记录某条命令的实际执行时间。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是否过期都随机剔除剔除即将过期的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暂存在一个pool中,随机抽取一批key取出来,与之前池中的key相比,淘汰访问最少的一个key。以这种方式循环,直到内存下降到maxmemory以下。如果使用allkeys-random或者volatile-random策略会快很多,因为是随机淘汰,所以比较key访问频率的时间消耗更少,随机选择后可以直接淘汰一批key,所以这个策略比上面的LRU策略更快。但是上面的逻辑是在访问Redis的时候在真正的命令执行之前执行的,也就是说会影响到我们访问Redis时执行的命令。另外,如果此时Redis实例中存有大key,淘汰大key释放内存的时间会更长,延迟也会更大,需要我们特别注意。如果你的业务访问量非常大,必须设置maxmemory来限制实例内存的上限,同时又面临key淘汰导致延迟增加的情况,为了缓解这种情况,在除了上面提到的避免存储大键和使用随机淘汰策略之外,还可以考虑拆分实例的方法来缓解。拆分实例可以将一个实例的压力消散到多个实例上,可以在一定程度上降低延迟。Fork需要很多时间。如果你的Redis有自动生成RDB和AOF重写的功能,有可能在后台生成RDB和AOF重写时,Redis的访问延迟会增加,等这些工作完成后,延迟就会消失。.这种情况一般是生成RDB任务执行,AOF重写导致的。生成RDB和AOF需要父进程fork一个子进程进行数据持久化。fork执行过程中,父进程需要将内存页表拷贝给子进程。如果整个实例内存占用很大,需要复制内存页表会比较耗时。这个过程会消耗大量的CPU资源。在分叉完成之前,整个实例将被阻塞,无法处理任何请求。如果此时CPU资源紧张,fork时间会更长,甚至达到秒级。这会严重影响Redis的性能。我们可以执行info命令查看上次fork执行的耗时latest_fork_usec,单位为微秒。这个时间是整个实例被阻塞,无法处理请求的时间。除了因为备份原因而生成RDB外,当主从节点第一次建立数据同步时,主节点也会生成一个RDB文件供从节点进行全量同步,这也会对性能产生影响雷迪斯。为了避免这种情况,我们需要规划好数据备份的周期。建议在从节点上进行备份,最好在低峰时段进行。如果业务对数据丢失不敏感,不建议开启AOF和AOF重写功能。另外,fork的耗时也和系统有关。如果Redis部署在虚拟机上,这个时间也会增加。所以在使用Redis的时候,建议部署在物理机上,减少fork的影响。绑定CPU很多时候,我们在部署服务的时候,为了提高性能,减少程序使用多CPU时上下文切换带来的性能损失,我们一般会使用进程绑定CPU的操作。但在使用Redis时,我们不建议这样做,原因如下:Redis绑定CPU,在进行数据持久化时,fork出子进程,子进程会继承父进程的CPU使用偏好,以及此时子进程会消耗大量的CPU资源进行数据持久化,子进程会与主进程竞争CPU,也会导致主进程CPU资源不足导致访问延迟增加.所以在部署Redis进程时,如果需要开启RDB和AOF重写机制,一定不要进行CPU绑定操作!开启AOF前文提到,在执行AOF文件重写时,由于fork执行耗时,Redis延迟会增加。除此之外,如果开启了AOF机制,设置的策略不合理,也会造成性能问题。开启AOF后,Redis会将写入的命令实时写入文件,但写入文件的过程是先写入内存。当内存中的数据超过某个阈值或达到某个时间后,内存中的内容就会被真正写入磁盘。为了保证文件写入磁盘的安全性,AOF提供了3种磁盘刷新机制:?appendfsyncalways:每次写入都会刷新磁盘,对性能影响最大,占用磁盘IO比较高,具有最高的数据安全性?appendfsynceverysec:每秒刷新一次磁盘,对性能影响相对较小。当一个节点宕机时,数据最多会丢失1秒。appendfsyncno:根据操作系统的机制刷盘,对性能影响最小,数据安全性低,节点宕机时会丢失数据。取决于操作系统的刷机机制当使用第一种机制appendfsyncalways时,Redis每处理一次写命令都会将这条命令写到磁盘中,这个操作是在主线程中执行的。将内存中的数据写入磁盘,会增加磁盘的IO负担,而且操作磁盘的成本远高于操作内存的成本。如果写入量很大,每次更新都会写入磁盘,此时机器的磁盘IO会很高,会拖慢Redis的性能,所以不推荐使用这种机制。与第一种机制相比,appendfsynceverysec会每秒刷新一次磁盘,而appendfsyncno依赖于操作系统的磁盘刷新时间,安全性不高。因此,我们建议每秒使用appendfsync。在最坏的情况下,只会丢失1秒的数据,但可以保持良好的访问性能。当然,对于一些业务场景,对数据丢失不敏感,可以不开启AOF。使用Swap,如果发现Redis突然变得很慢,每次访问都要几百毫秒甚至几秒,那么这时候检查一下Redis是否使用了Swap。在这种情况下,Redis基本无法提供高速访问。性能服务。我们知道,操作系统提供了Swap机制,目的是在内存不足的时候,将内存中的一部分数据交换到磁盘中,缓冲内存的使用。但是当内存中的数据交换到磁盘时,访问数据需要从磁盘读取,比内存慢很多!特别是像Redis这种高性能的内存数据库,如果把Redis中的内存交换到磁盘上,这个操作时间对于像Redis这种对性能敏感的数据库来说是无法接受的。我们需要查看机器的内存使用情况,确认是否确实是因为内存不足导致使用了Swap。如果确实使用Swap,需要及时整理内存空间,释放足够的内存给Redis使用,然后释放RedisSwap,让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运行的稳定性。