文章目录
【关于作者】
关于作者,目前在蚂蚁金服搬砖任职,在支付宝营销投放领域工作了多年,目前在专注于内存数据库相关的应用学习,如果你有任何技术交流或大厂内推及面试咨询,都可以从我的个人博客(https://0522-isniceday.top/)联系我
1.问题认定
1.1.查看Redis的响应延迟
这种方法是看Redis延迟的绝对值,但是,在不同的软硬件环境下,Redis本身的绝对性能并不相同
1.2.当前环境下的Redis基线判断
基线性能:redis实例在系统低压力、无干扰下的基本性能。
redis 2.8.7版本提供了–intrinsic-latency
选项,用来测试和统计测试期间内的最大延迟,这个延迟可以作为Redis的基线性能
其中,测试时长可以用–intrinsic-latency选项的参数来指定。
举个例子,比如说,我们运行下面的命令,该命令会打印120秒内监测到的最大延迟。可以看到,这里的最大延迟是119微秒,也就是基线性能为119微秒。一般情况下,运行120秒就足够监测到最大延迟了,所以,我们可以把参数设置为120
./redis-cli --intrinsic-latency 120
Max latency so far: 17 microseconds.
Max latency so far: 44 microseconds.
Max latency so far: 94 microseconds.
Max latency so far: 110 microseconds.
Max latency so far: 119 microseconds.
36481658 total runs (avg latency: 3.2893 microseconds / 3289.32 nanoseconds per run).
Worst run took 36x longer than the average latency.
1.3.比较两者下结论
比较响应延迟和基线性能,如果响应延迟比基线性能慢很多,那就可以认定服务端响应确实很慢
基线性能也只是考虑了服务器的软硬件环境的影响,还可能存在网络方面的影响
1.3.网络对Redis的性能影响
利用iPerf,测量Redis客户端到服务端的延迟情况
2.系统性排查及应对Redis变慢的方案
当我们定位到Redis确实变慢了之后,我们之后该如何查找原因并解决呢?
从下图中Redis自身特性、操作系统、文件特性三个角度出发,排查和解决Redis变慢的问题
2.1.Redis自身操作特性的影响
Redis键值对操作对延迟性能的影响,重点是两类操作:慢查询命令和过期key操作
2.1.1.慢查询命令
慢指令:需要提前知道不同指令的一个操作复杂度,根据复杂度能够很快的定位到慢指令,具体复杂度看Redis官方文档
如何查看慢查询(再结合具体命令的时间复杂度):
-
Redis日志
查看slowlog是否存在SORT/SUION/ZUNIONSTORE/KEYS、或slowlog出现很多SET/DELETE变慢命令,代表在操作bigkey
-
latency monitor工具
解决措施:
- 用其他高效命令替代,例如使用SCAN代替全量查询
- 执行排序、交集、并集等操作是,建议不使用SORT、SUNION、SINTER,可以在代码中处理
不建议在线上使用Keys
命令,因为KEYS命令需要遍历存储的键值对,所以操作延时高
【额外】:keys命令的替代方案:
SCAN
命令代替(也支持正则匹配),SCAN $cursor [match "[searchcontent]"] COUNT $count
命令会返回下一个游标,依次遍历会将所有key都查询出来,用该方式不会存在漏key,但是可能会存在返回多次同一个key
1、为什么不会漏key?Redis在SCAN遍历全局哈希表时,采用*高位进位法*的方式遍历哈希桶(可网上查询图例,一看就明白),当哈希表扩容后,通过这种算法遍历,旧哈希表中的数据映射到新哈希表,依旧会保留原来的先后顺序,这样就可以保证遍历时不会遗漏也不会重复。
2、为什么SCAN会得到重复的key?这个情况主要发生在哈希表缩容。已经遍历过的哈希桶在缩容时,会映射到新哈希表没有遍历到的位置,所以继续遍历就会对同一个key返回多次
针对Hash/Set/Sorted Set提供的HSCAN/SSCAN/ZSCAN
命令:
针对key的SCAN
和具体结构的SCAN
再返回数量方面会有区别,针对Hash/Set/Sorted Set
元素数量比较少时,底层会采用intset/ziplist
方式存储,在执行HSCAN/SSCAN/ZSCAN
命令时,会无视count参数,直接把所有元素一次性返回,也就是说,得到的元素数量是会大于count参数的。当底层转为哈希表或跳表存储时,才会真正使用发count参数,最多返回count个元素
2.1.2.过期key操作
过期key的自动删除机制:该机制会导致主线程阻塞,导致性能变慢
redis删除过期key的算法如下,默认是100ms执行一次:
(1)采样ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
个数的key,将过期key删除
(2)如果步骤1超过25%的key过期了,则重复上述过程,直至过期key的占比低于25%
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
是redis的一个参数,默认是20。正常1秒中内基本有200个key被删除,删除200个key并不会对主进程造成多大影响,但是可能会触发步骤2,重复执行删除操作,删除操作是阻塞的。而第二条触发的场景主要是在同一秒内大量的key同时过期
解决措施:
- 根据业务场景,如果确实有一批key设置的过期时间参数是一致的,看是否允许存在一定大小范围内随机数的误差,允许的话就调整过期时间,这样就避免了1s内大量key同时过期,减少阻塞的风险
2.2.文件系统:AOF模式
AOF的三种写回策略,依赖文件系统的两个调用完成:write和fsync
write只需要将数据写到内核的数据缓冲区就可返回,并不需要等待日志实际写到磁盘;而fsync需要把日志写回磁盘才可返回,如下表:
写回策略everysec和always虽然都使用的fsync,但是还是有区别,由于everysec运行日志存在1s的丢失,因此采取子线程异步执行fsync(fsync执行较慢),主线程并不关心执行的结果,但是alaways的场景关注每一次的执行结果,因此不会采取异步子线程,而是主线程去执行fsync
由于AOF重写是交由fork出的子进程去执行,但是AOF重写这个过程会对磁盘进行大量的IO操作,同事fsync需要等到数据写到磁盘才会返回,因此这里也存在一个阻塞风险点:
虽然主线程不关系AOF子线程fsync写入的结果,但是会监控fsync的执行进度,当主线程下一次fsync发现上一次的fsync还未执行完成,这个时候就会阻塞(主线程)等到fsync完成。因此,如果后台子线程fsync频繁阻塞的换,主线程也会阻塞,导致redis性能变慢。
该过程如下图:
2.2.1.排查:
检查Redis配置文件中的appendfsync
配置项:
-
首先确实是否有必要使用always
-
再确认是否对延迟敏感,但是对丢失数据不敏感,此时可将配置项
no-appendfsync-on-rewrite
设置为yes
no-appendfsync-on-rewrite
:yes时,表示AOF重写时,不进行fsync操作。Redis实例将写请求放到缓存,不调用后台线程进行fsync操作,就可以直接返回。当然,如果此时实例发生宕机,则会导致数据丢失(AOF重写的是两处日志没有写入)
no时。表示会调用后台线程进行fsync操作,此时会存在阻塞主线程的风险
2.2.2.解决措施
采用高速的固态,固态的带宽和并发度大约是机械硬盘的十倍,这个时候可以减少AOF重写和fsync后台线程并发带来的磁盘竞争从而阻塞主进程
2.3.操作系统:swap
**概念:**内存swap是内存和磁盘间来回换入换出的机制,涉及磁盘的读写。遇到swap(操心系统将 Redis 所用的内存分页移至 swap 空间时),无论换出还是换入的进程都收到慢速磁盘读写的影响。
Swap空间的作用可简单描述为:当系统的物理内存不够用的时候,就需要将物理内存中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间被临时保存到Swap空间中,等到那些程序要运行时,再从Swap中恢复保存的数据到内存中。这样,系统总是在物理内存不够时,才进行Swap交换Swap空间的作用可简单描述为:当系统的物理内存不够用的时候,就需要将物理内存中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间被临时保存到Swap空间中,等到那些程序要运行时,再从Swap中恢复保存的数据到内存中。这样,系统总是在物理内存不够时,才进行Swap交换
如下图:
**对redis的影响:**当Redis和其他对内存需求大的应用一起运行时,redis是访问内存才能完成,一旦发生swap,redis的请求操作需要等待磁盘的读写操作执行完成才能继续,并且与fsync不同的是,swap影响的是redis的主线程
**swap发生的场景:**物理机器内存不足
发生原因:
- redis实例占用了大量内存,导致物理机内存不足
- 同一机器上的其他应用在进行大量的文件读写操作,操作系统的文件读写也会占用内存,导致分配给redis的内存减少,进而触发redis的swap
查看redis的swap情况:
-
查找redis的进程号
-
进入redis所在的proc目录/proc/pid
-
运行命令:
cat smaps | egrep '^(Swap|Size)'
redis本身就有很多内存块,大小不一,不同内存块被换到磁盘上的大小也不一致,当出现百MB、GB级别的swap时,代表此时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 //其中size表示redis实例所使用的一块内存大小,swap代表换入到磁盘的内存大小
解决措施:
- 最直接的就是增大内存
- 使用切片集群,部署多个redis实例,分担内存的压力
2.4.操作系统:内存大页
Linux内核从2.6.38开始支持内存大页机制,该机制支持2MB大小的内存页分配,而常规的内存页分配是按4KB的粒度来执行的
实际的生产环境中不建议开启大页机制,因为redis在fork子进程生成快照以及重写AOF日志时,主线程仍然可以接收写请求,因为采取了写时复制的机制,如果开启了大页机制,则可能对100k的数据进行写操作,此时需要复制一页也就是2MB的内存,且此时会阻塞主进程
如何查看是否开启大页:
//如果执行结果是always,就表明内存大页机制被启动了;如果是never,就表示,内存大页机制被禁止。
cat /sys/kernel/mm/transparent_hugepage/enabled
关闭:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
为了使机器重启后 THP 配置依然生效
/etc/rc.local 中追加 echo never > /sys/kernel/mm/transparent_hugepage/enabled
3.检查checklist
-
**检查基线性能:**检查redis的基线性能并与当前延迟进行比较
-
**慢查询问题定位+解决:**慢查询问题的定位+解决(用scan等替代、聚合计算放在客户端)
- 定位:a) 查看slowlog是否存在这些命令 b) Redis进程CPU使用率是否飙升(聚合运算命令导致)
- 解决:a) 不使用复杂度过高的命令,或用其他方式代替实现(放在客户端做) b) 数据尽量分批查询(LRANGE key 0 N,建议N<=100,查询全量数据建议使用HSCAN/SSCAN/ZSCAN)
-
**大量key同时过期:**是否对key设置了相同的过期时间,key的删除会阻塞主进程,在key上新增业务运行的浮动过期数值
- 定位:a) 业务使用EXPIREAT/PEXPIREAT命令 b) Redis info中的expired_keys指标短期突增
- 解决:a) 优化业务,过期增加随机时间,把时间打散,减轻删除过期key的压力 b) 运维层面,监控expired_keys指标,有短期突增及时报警排查
-
**bigkey的操作:**redis 4.0则执行异步删除,4.0之前执行scan批量删除
- 定位:a) slowlog出现很多SET/DELETE变慢命令(bigkey分配内存和释放内存变慢) b) 使用redis-cli -h $host -p $port --bigkeys扫描出很多bigkey
- 解决:a) 优化业务,避免存储bigkey b) Redis 4.0+可开启lazy-free机制
-
**AOF日志的写入策略:**避免AOF重写和fsync竞争磁盘IO资源,导致Redis延迟增加。根据业务设置no-appendfsync-on-rewrite为no,可能会丢失数据
-
**Redis实例是否会产生Swap的风险:**定位+解决问题(扩大内存或切片集群)
- 定位:a) 所有请求全部开始变慢 b) slowlog大量慢日志 c) 查看Redis进程是否使用到了Swap
- 解决:a) 增加机器内存 b) 集群扩容 c) Swap使用时监控报警
-
是否启用大页机制:
- 定位:查看是否启用
- 解决:关闭
-
**主从部署避免数据过大:**因为主从同步会导致从库加载大的RDB文件造成阻塞或数据丢失(同fork耗时过长)
-
是否使用了多核CPU或NUMA架构的机器运行Redis实例:
- 定位:使用多核CPU时,可以给Redis实例绑定物理核;使用NUMA架构时,注意把Redis实例和网络中断处理程序运行在同一个CPU Socket上
- 解决:a) Redis进程绑定多个CPU逻辑核 b) 网络中断处理程序和Redis进程绑定在同一个Socket下
-
**fork耗时过长:**查看AOF重写和RDB快照的fork流程是否过长
- 定位:a) Redis变慢只发生在生成RDB和AOF重写期间 b) 实例占用内存越大,fork拷贝内存页表越久 c) Redis info中latest_fork_usec耗时变长
- 解决:a) 实例尽量小 b) Redis尽量部署在物理机上 c) 优化备份策略(例如低峰期备份) d) 合理配置repl-backlog和slave client-output-buffer-limit,避免主从全量同步 e) 视情况考虑关闭AOF f) 监控latest_fork_usec耗时是否变长
-
Redis内存达到maxmemory
- 定位:a) 实例内存达到maxmemory,且写入量大,淘汰key压力变大 b) Redis info中的evicted_keys指标短期突增
- 解决:a) 业务层面,根据情况调整淘汰策略(随机比LRU快) b) 运维层面,监控evicted_keys指标,有短期突增及时报警 c) 集群扩容,多个实例减轻淘汰key的压力
内存淘汰策略在 Redis 4.0 之后有 8 种:
- noeviction:不淘汰任何数据,当内存不足时,新增操作会报错,Redis 默认内存淘汰策略;
- allkeys-lru:淘汰整个键值中最久未使用的键值;
- allkeys-random:随机淘汰任意键值;
- volatile-lru:淘汰所有设置了过期时间的键值中最久未使用的键值;
- volatile-random:随机淘汰设置了过期时间的任意键值;
- volatile-ttl:优先淘汰更早过期的键值。
在 Redis 4.0 版本中又新增了 2 种淘汰策略:
- volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值;
- allkeys-lfu:淘汰整个键值中最少使用的键值。
其中 allkeys-xxx 表示从所有的键值中淘汰数据,而 volatile-xxx 表示从设置了过期键的键值中淘汰数据。
我们可以根据实际的业务情况进行设置,默认的淘汰策略不淘汰任何数据,在新增时会报错。