Redis性能优化
在互联网行业中,你要说使用最广泛最流行的中间件有哪些,名单列表中必定少不了Redis, 无论是大厂小厂,无论是前端、后端、运维,多多少少都要懂点Redis的知识;如果你和reids打交道的时间久了,我相信你多多少少都遇到过以下的问题:
- 在redis上执行相同的命令,为什么时快时慢?
- 为什么redis突然变慢,过一会儿又恢复了,循环重复这个问题
- 为什么redis运行很久了,突然某一天开始变慢了
- 等等…
相信大家在排查这类问题的时候,大多数是一头雾水.
Redis怎样才算慢?
怎样判断redis变慢了?从哪些指标得知redis变慢了?
首先你需要了解redis的基准性能,什么是基准性能?
简单来讲,基准性能就是redis在一台负载正常的机器上,其最大的响应延迟和平均响应延迟分别是多少.
因为在不同的系统,不同的硬件环境下,redis的性能都是不同的,对别人不算慢的响应时间,在你这里可能就是很慢了.
所以只有了解redis在你的服务器上的基准性能,才能判断当响应时间大于多少时,认为redis变慢了.
如何测试redis基准性能
redis本身提供了相关的工具来测试基准性能,大家在测试基准性能的同时,为了避免服务器之间的网络因素对测试结果的影响,应该在一台服务器进行测试,执行以下命令,就可以得出该实例10秒内的最大最小响应延迟:
redis-cli -h 127.0.0.1 -p 6379 --intrinsic-latency 60
Max latency so far: 3 microseconds.
Max latency so far: 10 microseconds.
Max latency so far: 13 microseconds.
Max latency so far: 15 microseconds.
Max latency so far: 20 microseconds.
Max latency so far: 22 microseconds.
Max latency so far: 39 microseconds.
Max latency so far: 42 microseconds.
1367169924 total runs (avg latency: 0.0420 microseconds / 42.00 nanoseconds per run).
Worst run took 1429x longer than the average latency.
从结果可以看到,这 10 秒内的最大响应延迟为 42微秒(0.042毫秒)。
该命令还可以测试一段时间内redis的最大,最小,平均访问延迟:
redis-cli -h 127.0.0.1 -p 6379 --latency-history -i 1
min: 0, max: 1, avg: 0.13 (100 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.12 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.14 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.10 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.13 (98 samples) -- 1.00 seconds range
min: 0, max: 1, avg: 0.09 (99 samples) -- 1.01 seconds range
...
输出结果时每间隔1秒,redis的平均耗时分布在0.09~0.14毫秒之间
知道了基准性能测试方法,那么就很容易判断redis是否真的变慢了:
在你认为变慢的redis服务器上测试该redis的基准性能,或者在相同配置的服务器上测试相同配置的redis, 如果目标实例redis响应时间是正常长时间保持在redis基准性能的2倍以上,则考虑这个redis实例变慢了.
那么哪些因素会导致redis变慢呢?
大量使用复杂度高的命令
可以通过查看redis slowlog查看哪些命令在执行时耗时比较久. redis slowlog可以自定义阈值,执行时间超过阈值的redis命令将被记录在slowlog中.
CONFIG SET slowlog-log-slower-than 2000
执行时间超过2毫秒命令记录慢日志.
可以查看slowlog日志文件,也可以登录进redis中通过SLOWLOG命令查看:
SLOWLOG get 10
有哪些耗时的redis命令?
ORT、SUNION、ZUNIONSTORE 聚合类命令
以上这类命令,在操作redis内存时,时间复杂度过高,需要花费更多的CPU资源,另外如果一次性返回给客户端的数据过多,将会花费更多的时间在协议的组装和数据的传输过程中.
如果你的应用程序操作 Redis 的 OPS 不是很大,但 Redis 实例的 CPU 使用率却很高,那么很有可能是使用了复杂度过高的命令导致的.
redis是单线程应用,所以如果一个命令比较耗时,将会导致后续的命令堆积,后续的命令如果也存在过多耗时命令,将很容易发生雪崩效应.
如何解决?
尽量不要使用时间复杂度高的命令,当你发现不得不使用搞复杂度的命令时,想想能不能通过优化业务逻辑绕过该命令.
处理bigkey
redis在写入数据时需要为新数据分配内存,当从redis中删除数据时,它会释放内存.如果一个key写入的value非常大,在分配和释放内存时将会耗费更多时间,这种类型的key称之为bigkey. 在分片集群下,bigkey的数据迁移也会造成性能影响.
如何扫描redis中的bigkey:
redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.02
-------- summary -------
Sampled 329 keys in the keyspace!
Total key length in bytes is 2120234 (avg len 21.15)
Biggest string found 'key:32433' has 20 bytes
...
在对线上环境执行bigkey扫描时,OPS会暴增,为了降低对线上业务的影响,请在业务低峰期运行扫描.
如何解决bigkey问题?
- 业务中尽量少用bigkey
- 使用UNLINK命令替代DEL, 此命令可以把释放内存操作放到后台线程中执行,降低对redis主进程的影响.(redis4.0版本以上)
- 开始lazy-free, 在执行DEL命令时,释放内存会放到后台线程中执行.(redis6.0版本以上)
集中过期
如果发现在平时使用redis中没有延迟发生,但在某一个时间点突然有一波延迟,并且重复出现, 如果这种情况,需要排查下代码中是否设置了大量的key在集中过期的情况.
为什么集中过期会导致redis延迟变大?
redis过期策略有两种:
- 被动过期: 当访问某个key时,才判断这个key是否过期,过期则删除.
- 主动过期: redis维护了一个定时任务,每隔一段时间就会从全局的过期哈希表中随机取出20个key,然后删除其中过期的key,如果过期key的比例超过了25%,则继续重复此过程,直到过期key的比例下降到25%以下,或者这次任务的执行耗时超过了25毫秒,才会退出循环。这个主动过期key的定时任务,是在 Redis 主线程中执行的
也就是说如果在执行主动过期的过程中,出现了需要大量删除过期key的情况,那么此时应用程序在访问Redis时,必须要等待这个过期任务执行结束,Redis才可以服务这个客户端请求。如果此时需要过期删除的是一个bigkey,那么这个耗时会更久。而且,这个操作延迟的命令并不会记录在慢日志中。
检查你的业务代码中是否存在集中过期key的逻辑.
般集中过期使用的是expireat/pexpireat 命令,你需要在代码中搜索这个关键字。
排查代码后,如果确实存在集中过期key的逻辑存在,但这种逻辑又是业务所必须的,那此时如何优化,同时又不对Redis有性能影响呢?
- 集中过期key增加一个随机过期时间,把集中过期的时间打散,降低Redis清理过期key的压力;
- 开启lazy-free机制,当删除过期key时,把释放内存的操作放到后台线程中执行,避免阻塞主线程。
- 监控redis info中的expired_keys指标,他表示整个实例已经过期删除的key总和,如果这个指标短时间内徒增,需要及时报警出来,redis变慢开始时间如果和该项报警时间吻合,基本可确定时集中过期导致的redis变慢.
实例内存达上限
如果你Redis实例设置了内存上限maxmemory,那么也有可能导致Redis变慢。
原因在于,当Redis内存达到maxmemory后,每次写入新的数据之前,Redis必须先从实例中踢出一部分数据,让整个实例的内存维持在maxmemory之下,然后才能把新数据写进来。
这个踢出旧数据的逻辑也是需要消耗时间的,而具体耗时的长短,要取决于你配置的淘汰策略:
- allkeys-lru:不管key是否设置了过期,淘汰最近最少访问的key
- volatile-lru:只淘汰最近最少访问、并设置了过期时间的key
- allkeys-random:不管key是否设置了过期,随机淘汰 key
- volatile-random:只随机淘汰设置了过期时间的 key
- allkeys-ttl:不管key是否设置了过期,淘汰即将过期的 key
- noeviction:不淘汰任何key,实例内存达到 maxmeory 后,再写入新数据直接返回错误
- allkeys-lfu:不管key是否设置了过期,淘汰访问频率最低的key4.0+版本支持)
- volatile-lfu:只淘汰访问频率最低、并设置了过期时间key(4.0+版本支持)
具体使用哪种策略,我们需要根据具体的业务场景来配置。
一般最常使用的是allkeys-lru/volatile-lru 淘汰策略,它们的处理逻辑是,每次从实例中随机取出一批key这个数量可配置),然后淘汰一个最少访问的key,之后把剩下的key暂存到一个池子中,继续随机取一批key,并与之前池子中的key比较,再淘汰一个最少访问的key。以此往复,直到实例内存降到maxmemory之下。
需要注意的是,Redis的淘汰数据的逻辑与删除过期 key 的一样,也是在命令真正执行之前执行的,也就是说它也会增加我们操作 Redis 的延迟,而且,写 OPS 越高,延迟也会越明显。
另外,如果此时你的Redis实例中还存储了bigkey,那么在淘汰删除bigkey释放内存时,也会耗时比较久。
如何解决呢?
- 增加系统物理内存, 配置更大的maxmemory阈值
- 淘汰策略改为随机淘汰
RDB/AOF持久化耗时严重
当Redis开启了后台RDB和AOF rewrite 后,在执行时,它们都需要主进程创建出一个子进程进行数据的持久化。
而fork在执行过程中,主进程需要拷贝自己的内存页表给子进程,如果这个实例很大,那么这个拷贝的过程也会比较耗时。
而且这个fork过程会消耗大量的CPU资源,在完成fork之前,整个Redis实例会被阻塞住,无法处理任何客户端请求。
除了数据持久化会生成RDB之外,当主从节点第一次建立数据同步时,主节点也创建子进程生成RDB,然后发给从节点进行一次全量同步,所以,这个过程也会对Redis产生性能影响。
你可以在Redis上执行INFO命令,查看latest_fork_usec项,单位微秒。
如何解决:
- 避免redis实例内存使用过大
- 合理配置数据持久化策略,在slave节点执行RDB备份,如果数据不重要可关闭RDB和AOF
- 降低主从库全量同步的概率:适当调大repl-backlog-size参数,避免主从全量同步。
开启内存大页
什么是内存大页?
应用程序向操作系统申请内存时,是按内存页进行申请的,而常规的内存页大小是4KB。Linux内核从2.6.38开始支持了内存大页机制,该机制允许应用程序以2MB大小为单位,向操作系统申请内存,这也意味着申请内存的耗时变长
当Redis在执行后台RDB和AOF rewrite时,采用fork子进程的方式来处理。但主进程fork子进程后,此时的主进程依旧是可以接收写请求的,而进来的写请求,会采用Copy On Write(写时复制)的方式操作内存数据。
也就是说,主进程一旦有数据需要修改,Redis并不会直接修改现有内存中的数据,而是先将这块内存数据拷贝出来,再修改这块新内存的数据,这样做的目的就是保证主进程不影响子进程的数据持久化.
问题来了,主进程再拷贝数据时,会向操作系统申请新内存,如果操作系统开启了内存大页,即使redis客户端只修改了10B的数据,redis再申请内存时,操作系统也会以2MB为单位处理redis内存请求,申请内存耗时增长,进而导致redis每个写延迟增长,影响到redis性能.
如果这个写请求操作的是一个bigkey,那主进程在拷贝这个bigkey内存块时,一次申请的内存会更大,时间也会更久。可见,bigkey在这里又一次影响到了性能。
如何解决?
- 关闭内存大页
# 查看系统是否开启内存大页
cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never
# 关闭
echo never > /sys/kernel/mm/transparent_hugepage/enabled
绑定了CPU
很多时候,我们在部署服务时,为了提高服务性能,降低应用程序在多个CPU核心之间的上下文切换带来的性能损耗,通常采用的方案是进程绑定CPU的方式提高性能。
但是针对redis进行CPU绑定后,可能并不能提高redis性能,反而带来相反效果,因为redis除了主进程之外还会创建子进程,子线程来处理其他操作,如前面提到的异步释放内存,数据持久化等等,如果将redis只绑定到一个CPU上,可能会发生主进程和子进程抢夺CPU情况,导致主进程处理请求延迟.
如何解决?
- 让主进程子进程分别绑定不同的CPU核心上, 绑定的多个逻辑核心最好时同一个物理核心,这样不同的进程可以共享CPU高速缓存
- redis6.0版本可以对主线程、后台线程、后台 RDB 进程、AOF rewrite 进程,绑定固定的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性能已经足够优秀,除非你真对redis有更高需求,而且非常了解操作系统知识,一般不建议你绑定CPU.
使用了Swap
swap的设计是为了解决系统物理内存不足时的应对方案, 允许系统将内存中不常用的数据放到磁盘上,磁盘上划分出来的用于存放该类数据的区域就是swap.
了解内存和硬盘的同学知道,这两者之间的速度差距时巨大的,当redis中的数据被放到swap后,当访问相关数据时,系统需要从先从磁盘取出数据载入到内存中,这个过程是极慢的,也就导致redis操作延迟.
查看redis进程是否使用了swap
# 获取redis进程号
ps -aux | grep redis-server
# 查看swap使用情况
cat /proc/$pid/smaps | egrep '^(Swap|Size)'
输出结果:
Size: 5635 kB
Swap: 0 kB
Size: 517 kB
Swap: 0 kB
Size: 363 kB
Swap: 0 kB
Size表示redis所用的内存块大小,Swap表示该块内存有多少数据被交换到swap上了.
如何解决?
- 增加物理内存,关闭系统swap
执行swapoff -a关闭系统swap分区
碎片整理
当我们频繁修改redis数据时,会导致redis产生内存碎片, 内存碎片会降低redis内存使用率, 通过INFO命令可以查看碎片率:
Memory
used_memory:5468523614
used_memory_human:5.46G
used_memory_rss:8632145822
used_memory_rss_human:7.70G
...
mem_fragmentation_ratio:1.59
内存碎片率的计算方法:
mem_fragmentation_ratio = used_memory_rss / used_memory
mem_fragmentation_ratio > 1.5,说明内存碎片率已经超过了 50%,这时我们就需要采取一些措施来降低内存碎片了。
解决方案:
- redis版本4.0以下只能通过重启redis服务来解决
- redis4.0版本及以上提供了碎片整理功能
开启碎片整理的参数配置:
# 开启自动内存碎片整理(总开关)
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碎片整理是由主线程去执行的,因此开启碎片整理会影响redis处理客户端需求,推荐将碎片整理安排在业务低峰窗口期运行.
网络带宽
网络堵塞情况下,服务器会发生网络数据包发送延迟、丢包的情况,当出现这类情况时,网络IO就是redis的瓶颈了.
在排查这类问题时,首先需要了解redis服务器的网络带宽是多少,以及数据包的收发量大小 - 网卡每秒包转发数量(packets per second)
通过iftop或者pidstat等工具排查网络情况,确定是否时redis进程占满网络带宽,从而进一步解决.
具体解决思路有:
- 排查业务请求是否正常
- 扩容网络带宽
- 使用redis集群模式或者扩容集群节点
总结
Redis的性能问题,既涉及到了开发人员的使用方面,也涉及到了运维方面。
作为开发人员,需要了解redis的基本原理,各种命令执行的复杂度,数据淘汰策略,从而结合业务场景进行优化.
作为运维人员,需要了解redis运行机制,如数据持久化,内存碎片,进程绑定等,还需要搭建好监控系统对redis的各项指标进行实时的监控,准确的报警,做好提早预防问题,提早发现问题.提早解决问题.