Redis常见延迟问题排查手册!附33条优化建议
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis常见延迟问题排查手册!附33条优化建议相关的知识,希望对你有一定的参考价值。
参考技术ARedis作为内存数据库,拥有非常高的性能,单个实例的QPS能够达到10W左右。但我们在使用Redis时,经常时不时会出现访问延迟很大的情况,如果你不知道Redis的内部实现原理,在排查问题时就会一头雾水。
很多时候,Redis出现访问延迟变大,都与我们的使用不当或运维不合理导致的。
下面我们就来分析一下Redis在使用过程中,经常会遇到的延迟问题以及如何定位和分析。
如果在使用Redis时,发现访问延迟突然增大,如何进行排查?
首先,第一步,建议你去查看一下Redis的慢日志。Redis提供了慢日志命令的统计功能,我们通过以下设置,就可以查看有哪些命令在执行时延迟比较大。
首先设置Redis的慢日志阈值,只有超过阈值的命令才会被记录,这里的单位是微妙,例如设置慢日志的阈值为5毫秒,同时设置只保留最近1000条慢日志记录:
# 命令执行超过5毫秒记录慢日志
CONFIG SET slowlog-log-slower-than 5000
# 只保留最近1000条慢日志
CONFIG SET slowlog-max-len 1000
设置完成之后,所有执行的命令如果延迟大于5毫秒,都会被Redis记录下来,我们执行SLOWLOG get 5查询最近5条慢日志:
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) \'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的主动过期的定时任务,也是在Redis主线程中执行的 ,也就是说如果在执行主动过期的过程中,出现了需要大量删除过期key的情况,那么在业务访问时,必须等这个过期任务执行结束,才可以处理业务请求。此时就会出现,业务访问延时增大的问题,最大延迟为25毫秒。
而且这个访问延迟的情况, 不会记录在慢日志里。 慢日志中 只记录真正执行某个命令的耗时 ,Redis主动过期策略执行在操作命令之前,如果操作命令耗时达不到慢日志阈值,它是不会计算在慢日志统计中的,但我们的业务却感到了延迟增大。
此时你需要检查你的业务,是否真的存在集中过期的代码,一般集中过期使用的命令是expireat或pexpireat命令,在代码中搜索这个关键字就可以了。
如果你的业务确实需要集中过期掉某些key,又不想导致Redis发生抖动,有什么优化方案?
解决方案是, 在集中过期时增加一个随机时间,把这些需要过期的key的时间打散即可。
伪代码可以这么写:
# 在过期时间点之后的5分钟内随机过期掉
redis.expireat(key, expire_time + random(300))
这样Redis在处理过期时,不会因为集中删除key导致压力过大,阻塞主线程。
另外,除了业务使用需要注意此问题之外,还可以通过运维手段来及时发现这种情况。
我们需要对这个指标监控,当在 很短时间内这个指标出现突增 时,需要及时报警出来,然后与业务报慢的时间点对比分析,确认时间是否一致,如果一致,则可以认为确实是因为这个原因导致的延迟增大。
有时我们把Redis当做纯缓存使用,就会给实例设置一个内存上限maxmemory,然后开启LRU淘汰策略。
当实例的内存达到了maxmemory后,你会发现之后的每次写入新的数据,有可能变慢了。
导致变慢的原因是,当Redis内存达到maxmemory后,每次写入新的数据之前,必须先踢出一部分数据,让内存维持在maxmemory之下。
这个踢出旧数据的逻辑也是需要消耗时间的,而具体耗时的长短,要取决于配置的淘汰策略:
具体使用哪种策略,需要根据业务场景来决定。
我们最常使用的一般是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的性能。
具体原理也可以参考我之前写的文章:Redis持久化是如何做的?RDB和AOF对比分析。
我们可以执行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时,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的稳定运行。
在上文中,主要讲解了 Redis 常见的导致变慢的场景以及问题定位和分析,主要是由业务使用不合理和运维不当导致的。
scrapy_redis去重优化(已有7亿条数据),附Demo福利
背景:
前些天接手了上一位同事的爬虫,一个全网爬虫,用的是scrapy+redis分布式,任务调度用的scrapy_redis模块。
大家应该知道scrapy是默认开启了去重的,用了scrapy_redis后去重队列放在redis里面。我接手过来的时候爬虫已经有7亿多条URL的去重数据了,再加上一千多万条requests的种子,redis占用了一百六十多G的内存(服务器,Centos7),总共才一百七十五G好么。去重占用了大部分的内存,不优化还能跑?
一言不合就用Bloomfilter+Redis优化了一下,内存占用立马降回到了二十多G,保证漏失概率小于万分之一的情况下可以容纳50亿条URL的去重,效果还是很不错的!在此记录一下,最后附上Scrapy+Redis+Bloomfilter去重的Demo(可将去重队列和种子队列分开!),希望对使用scrapy框架的朋友有所帮助。
接下来还会对种子队列进行优化,详见:《scrapy_redis种子优化》。
记录:
我们要优化的是去重,首先剥丝抽茧查看框架内部是如何去重的。
- 因为scrapy_redis会用自己scheduler替代scrapy框架的scheduler进行任务调度,所以直接去scrapy_redis模块下查看scheduler.py源码即可。
- 在open()方法中有句
self.df = RFPDupeFilter(...)
,可见去重应该是用了RFPDupeFilter这个类;再看下面的enqueue_request()方法,里面有句if not request.dont_filter and self.df.request_seen(request):return
,看来self.df.request_seen()这就是用来去重的了。 - 按住Ctrl再左键点击request_seen查看它的代码,可看到下面的代码:
def request_seen(self, request):
fp = request_fingerprint(request)
added = self.server.sadd(self.key, fp)
return not added
- 可见scrapy_redis是利用set数据结构来去重的,去重的对象是request的fingerprint。至于这个fingerprint到底是什么,可以再深入去看request_fingerprint()方法的源码(其实就是用hashlib.sha1()对request对象的某些字段信息进行压缩)。我们用调试也可以看到,其实fp就是request对象加密压缩后的一个字符串(40个字符,0~f)。
是否可用Bloomfilter进行优化?
以上步骤可以看出,我们只要在这个request_seen()
方法上面动些手脚即可。由于现有的七亿多去重数据存的都是这个fingerprint,所有Bloomfilter去重的对象仍然是request对象的fingerprint。更改后的代码如下:
def request_seen(self, request):
fp = request_fingerprint(request)
if self.bf.isContains(fp): # 如果已经存在
return True
else:
self.bf.insert(fp)
return False
self.bf是类Bloomfilter()的实例化,关于这个Bloomfilter()类,详见《基于Redis的Bloomfilter去重(附Python代码)》。
以上,优化的思路和代码就是这样;以下将已有的七亿多的去重数据转成Bloomfilter去重。
- 内存将爆,动作稍微大点机器就能死掉,更别说Bloomfilter在上面申请内存了。当务之急肯定是将那七亿多个fingerprint导出到硬盘上,而且不能用本机导,并且先要将redis的自动持久化给关掉。
- 因为常用Mongo,所以习惯性首先想到Mongodb,从redis取出2000条再一次性插入Mongo,但速度还是不乐观,瓶颈在于MongoDB。(猜测是MongoDB对_id的去重导致的,也可能是物理硬件的限制)
- 后来想用SSDB,因为SSDB和Redis很相似,用list存肯定速度快很多。然而SSDB唯独不支持Centos7,其他版本的系统都可。。
- 最后才想起来用txt,这个最傻的方法,却是非常有效的方法。速度很快,只是为了防止读取时内存不足,每100万个fingerprint存在了一个txt,四台机器txt总共有七百个左右。
- fingerprint取出来后redis只剩下一千多万的Request种子,占用内存9G+。然后用Bloomfilter将txt中的fingerprint写回Redis,写完以后Redis占用内存25G,开启redis自动持久化后内存占用49G左右。
福利福利:
献上Demo一个,链接:使用Bloomfilter去重的scrapy_redis。
Demo功能:启动spider1(或spider2),start_urls中有10条URL,其中4条是重复的,可以看到 parse1() 只处理了去重后的6条URL。
Demo去重功能的迁移:
- 将BloomfilterOnRedis_Demo目录下的 scrapy_redis 文件夹拷贝到你项目中settings.py的同级目录,在settings.py中增加几个字段:
FILTER_URL = None
FILTER_HOST = 'localhost'
FILTER_PORT = 6379
FILTER_DB = 0
# REDIS_QUEUE_NAME = 'OneName' # 如果不设置或者设置为None,则使用默认的,每个spider使用不同的去重队列和种子队列。如果设置了,则不同spider共用去重队列和种子队列
"""
这是去重队列的Redis信息。
原先的REDIS_HOST、REDIS_PORT只负责种子队列;由此种子队列和去重队列可以分布在不同的机器上。
"""
- 以上两个步骤即可实现BloomfilterOnRedis去重。(注意
import scrapy_redis
要改成import 项目名.scrapy_redis
,即导入这个新的scrapy_redis,不要导错了!) - 特别说明一下
REDIS_QUEUE_NAME
这个字段。刚才放在demo里面的有spider1和spider2,分别启动一下两个爬虫,可以看到两个爬虫的去重队列和种子队列的名字是不一样的,即不是共用一个去重队列和种子队列的。如果项目需要,不同spider也要使用同一个去重队列和种子队列,则将这个REDIS_QUEUE_NAME
设置成你想要的名字,此时同一个项目下的不同爬虫也会使用同一个去重队列和种子队列。 - 如果待去重的数据量比较大,需要修改scrapy_redis/dupefilter.py中第14行的blockNum值,默认blockNum=1。Bloomfilter算法是有漏失概率的(即不存在的会误判为存在),在保证漏失率小于万分之一的情况下,一个blockNum可满足7千万条数据的去重,一个blockNum占用256M内存(注意Linux如果开了自动持久化,redis占用内存会加倍)。
转载请注明出处,谢谢!(原文链接:http://blog.csdn.net/bone_ace/article/details/53099042)
以上是关于Redis常见延迟问题排查手册!附33条优化建议的主要内容,如果未能解决你的问题,请参考以下文章
万字长文解析删除Topic流程领导再也不用担心我排查生产环境问题了(附教学视频,建议收藏!!!)
scrapy_redis去重优化(已有7亿条数据),附Demo福利