1.Redis为什么变慢了

1.Redis真的变慢了吗?

  • 对Redis进行基准性能测试

例如,我的机器配置比较低,当延迟为2ms时,我就认为Redis变慢了,但是如果你的硬件配置比较高,那么在你的运行环境下,可能延迟是0.5ms时就可以认为Redis变慢了。

所以,你只有了解了你的Redis在生产环境服务器上的基准性能,才能进一步评估,当其延迟达到什么程度时,才认为Redis确实变慢了。

为了避免业务服务器到Redis服务器之间的网络延迟,你需要直接在Redis服务器上测试实例的响应延迟情况。执行以下命令,就可以测试出这个实例60秒内的最大响应延迟:

./redis-cli--intrinsic-latency120Maxlatencysofar:17microseconds.Maxlatencysofar:44microseconds.Maxlatencysofar:94microseconds.Maxlatencysofar:110microseconds. Maxlatencysofar:119microseconds.36481658totalruns(avglatency:3.2893microseconds/3289.32nanosecondsperrun).Worstruntook36xlongerthantheaveragelatency.

从输出结果可以看到,这60秒内的最大响应延迟为119微秒(0.119毫秒)。你还可以使用以下命令,查看一段时间内Redis的最小、最大、平均访问延迟。

$redis-cli-h127.0.0.1-p6379--latency-history-i1min:0,max:1,avg:0.13(100samples)--1.01secondsrangemin:0,max:1,avg:0.12(99samples)--1.01secondsrangemin:0,max:1,avg:0.13(99samples)--1.01secondsrangemin:0,max:1,avg:0.10(99samples)--1.01secondsrangemin:0,max:1,avg:0.13(98samples)--1.00secondsrangemin:0,max:1,avg:0.08(99samples)--1.01secondsrange

如果你观察到的Redis运行时延迟是其基线性能的2倍及以上,就可以认定Redis变慢了。

网络对Redis性能的影响,一个简单的方法是用iPerf这样的工具测试网络极限带宽。

服务器端

# iperf -s -p 12345 -i 1 -Miperf: option requires an argument -- M------------------------------------------------------------Server listening on TCP port 12345TCP window size: 4.00 MByte (default)------------------------------------------------------------[4] local 172.20.0.113 port 12345 connected with 172.20.0.114 port 56796[ ID] Interval Transfer Bandwidth [4]0.0- 1.0 sec 614 MBytes5.15 Gbits/sec[4]1.0- 2.0 sec 622 MBytes5.21 Gbits/sec[4]2.0- 3.0 sec 647 MBytes5.42 Gbits/sec[4]3.0- 4.0 sec 644 MBytes5.40 Gbits/sec[4]4.0- 5.0 sec 651 MBytes5.46 Gbits/sec[4]5.0- 6.0 sec 652 MBytes5.47 Gbits/sec[4]6.0- 7.0 sec 669 MBytes5.61 Gbits/sec[4]7.0- 8.0 sec 670 MBytes5.62 Gbits/sec[4]8.0- 9.0 sec 667 MBytes5.59 Gbits/sec[4]9.0-10.0 sec 667 MBytes5.60 Gbits/sec[4]0.0-10.0 sec6.35 GBytes5.45 Gbits/sec

客户端

# iperf -c服务器端IP -p 12345 -i 1 -t 10 -w 20K------------------------------------------------------------Client connecting to 172.20.0.113, TCP port 12345TCP window size: 40.0 KByte (WARNING: requested 20.0 KByte)------------------------------------------------------------[3] local 172.20.0.114 port 56796 connected with 172.20.0.113 port 12345[ ID] Interval Transfer Bandwidth[3]0.0- 1.0 sec 614 MBytes5.15 Gbits/sec[3]1.0- 2.0 sec 622 MBytes5.21 Gbits/sec[3]2.0- 3.0 sec 646 MBytes5.42 Gbits/sec[3]3.0- 4.0 sec 644 MBytes5.40 Gbits/sec [3]4.0- 5.0 sec 651 MBytes5.46 Gbits/sec[3]5.0- 6.0 sec 652 MBytes5.47 Gbits/sec[3]6.0- 7.0 sec 669 MBytes5.61 Gbits/sec[3]7.0- 8.0 sec 670 MBytes5.62 Gbits/sec[3]8.0- 9.0 sec 667 MBytes5.59 Gbits/sec[3]9.0-10.0 sec 668 MBytes5.60 Gbits/sec[3]0.0-10.0 sec6.35 GBytes5.45 Gbits/sec

2.使用复杂度过高的命令

首先,第一步,你需要去查看一下Redis的慢日志(slowlog)。

Redis提供了慢日志命令的统计功能,它记录了有哪些命令在执行时耗时比较久。

查看Redis慢日志之前,你需要设置慢日志的阈值。例如,设置慢日志的阈值为5毫秒,并且保留最近500条慢日志记录:

#命令执行耗时超过5毫秒,记录慢日志CONFIG SET slowlog-log-slower-than 5000#只保留最近500条慢日志CONFIG SET slowlog-max-len 500

1)经常使用O(N)以上复杂度的命令,例如SORT、SUNION、ZUNIONSTORE聚合类命令。

2)使用O(N)复杂度的命令,但N的值非常大。

第一种情况导致变慢的原因在于,Redis在操作内存数据时,时间复杂度过高,要花费更多的CPU资源。

第二种情况导致变慢的原因在于,Redis一次需要返回给客户端的数据过多,更多时间花费在数据协议的组装和网络传输过程中。

另外,我们还可以从资源使用率层面来分析,如果你的应用程序操作Redis的OPS不是很大,但Redis实例的CPU使用率却很高,那么很有可能是使用了复杂度过高的命令导致的。

3.操作bigkey

如果你查询慢日志发现,并不是复杂度过高的命令导致的,而都是SET/DEL这种简单命令出现在慢日志中,那么你就要怀疑你的实例否写入了bigkey。

redis-cli-h127.0.0.1-p6379--bigkeys-i1--------summary-------Sampled829675keysinthekeyspace!Totalkeylengthinbytesis10059825(avglen12.13)Biggeststringfound'key:291880'has10bytesBiggestlistfound'mylist:004'has40itemsBiggestsetfound'myset:2386'has38membersBiggesthashfound'myhash:3574'has37fields Biggestzsetfound'myzset:2704'has42members36313stringswith363130bytes(04.38%ofkeys,avgsize10.00)787393listswith896540items(94.90%ofkeys,avgsize1.14)1994setswith40052members(00.24%ofkeys,avgsize20.09)1990hashswith39632fields(00.24%ofkeys,avgsize19.92)1985zsetswith39750members(00.24%ofkeys,avgsize20.03)

这里我需要提醒你的是,当执行这个命令时,要注意2个问题:

1)对线上实例进行bigkey扫描时,Redis的OPS会突增,为了降低扫描过程中对Redis的影响,最好控制一下扫描的频率,指定-i参数即可,它表示扫描过程中每次扫描后休息的时间间隔,单位是秒。

2)扫描结果中,对于容器类型(List、Hash、Set、ZSet)的key,只能扫描出元素最多的key。但一个key的元素多,不一定表示占用内存也多,你还需要根据业务情况,进一步评估内存占用情况。

4.集中过期

如果你发现,平时在操作Redis时,并没有延迟很大的情况发生,但在某个时间点突然出现一波延时,其现象表现为:变慢的时间点很有规律,例如某个整点,或者每间隔多久就会发生一波延迟。

如果是出现这种情况,那么你需要排查一下,业务代码中是否存在设置大量key集中过期的情况。

如果有大量的key在某个固定时间点集中过期,在这个时间点访问Redis时,就有可能导致延时变大。

Redis的过期数据采用被动过期+主动过期两种策略:

1)被动过期:只有当访问某个key时,才判断这个key是否已过期,如果已过期,则从实例中删除。

2)主动过期:Redis内部维护了一个定时任务,默认每隔100毫秒(1秒10次)就会从全局的过期哈希表中随机取出20个key,然后删除其中过期的key,如果过期key的比例超过了25%,则继续重复此过程,直到过期key的比例下降到25%以下,或者这次任务的执行耗时超过了25毫秒,才会退出循环。

注意,这个主动过期key的定时任务,是在Redis主线程中执行的。

也就是说如果在执行主动过期的过程中,出现了需要大量删除过期key的情况,那么此时应用程序在访问Redis时,必须要等待这个过期任务执行结束,Redis才可以服务这个客户端请求。

如果此时需要过期删除的是一个bigkey,那么这个耗时会更久。而且,这个操作延迟的命令并不会记录在慢日志中。

因为慢日志中只记录一个命令真正操作内存数据的耗时,而Redis主动删除过期key的逻辑,是在命令真正执行之前执行的。

5.实例内存达到上限

当我们把Redis当做纯缓存使用时,通常会给这个实例设置一个内存上限maxmemory,然后设置一个数据淘汰策略。

当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是否设置了过期,淘汰访问频率最低的key(4.0+版本支持)
  • volatile-lfu:只淘汰访问频率最低、并设置了过期时间key(4.0+版本支持)

一般最常使用的是allkeys-lru/volatile-lru淘汰策略,它们的处理逻辑是,每次从实例中随机取出一批key(这个数量可配置),然后淘汰一个最少访问的key,之后把剩下的key暂存到一个池子中,继续随机取一批key,并与之前池子中的key比较,再淘汰一个最少访问的key。以此往复,直到实例内存降到maxmemory之下。

需要注意的是,Redis的淘汰数据的逻辑与删除过期key的一样,也是在命令真正执行之前执行的,也就是说它也会增加我们操作Redis的延迟,而且,写OPS越高,延迟也会越明显。

如果此时你的Redis实例中还存储了bigkey,那么在淘汰删除bigkey释放内存时,也会耗时比较久。

6.fork耗时严重

当Redis开启了后台RDB和AOFrewrite后,在执行时,它们都需要主进程创建出一个子进程进行数据的持久化。

主进程创建子进程,会调用操作系统提供的fork函数。

而fork在执行过程中,主进程需要拷贝自己的内存页表给子进程,如果这个实例很大,那么这个拷贝的过程也会比较耗时。

而且这个fork过程会消耗大量的CPU资源,在完成fork之前,整个Redis实例会被阻塞住,无法处理任何客户端请求。

如果此时你的CPU资源本来就很紧张,那么fork的耗时会更长,甚至达到秒级,这会严重影响Redis的性能。

那如何确认确实是因为fork耗时导致的Redis延迟变大呢?

你可以在Redis上执行INFO命令,查看latest_fork_usec项,单位微秒。

#上一次fork耗时,单位微秒latest_fork_usec:59477

这个时间就是主进程在fork子进程期间,整个实例阻塞无法处理客户端请求的时间。

如果你发现这个耗时很久,就要警惕起来了,这意味在这期间,你的整个Redis实例都处于不可用的状态。

除了数据持久化会生成RDB之外,当主从节点第一次建立数据同步时,主节点也创建子进程生成RDB,然后发给从节点进行一次全量同步,所以,这个过程也会对Redis产生性能影响。

7.开启内存大页

除了上面讲到的子进程RDB和AOFrewrite期间,fork耗时导致的延时变大之外,这里还有一个方面也会导致性能问题,这就是操作系统是否开启了内存大页机制

什么是内存大页?

我们都知道,应用程序向操作系统申请内存时,是按内存页进行申请的,而常规的内存页大小是4KB。

Linux内核从2.6.38开始,支持了内存大页机制,该机制允许应用程序以2MB大小为单位,向操作系统申请内存。

应用程序每次向操作系统申请的内存单位变大了,但这也意味着申请内存的耗时变长。

这对Redis会有什么影响呢?

当Redis在执行后台RDB,采用fork子进程的方式来处理。但主进程fork子进程后,此时的主进程依旧是可以接收写请求的,而进来的写请求,会采用CopyOnWrite(写时复制)的方式操作内存数据。

也就是说,主进程一旦有数据需要修改,Redis并不会直接修改现有内存中的数据,而是先将这块内存数据拷贝出来,再修改这块新内存的数据,这就是所谓的「写时复制」。

写时复制你也可以理解成,谁需要发生写操作,谁就需要先拷贝,再修改。

这样做的好处是,父进程有任何写操作,并不会影响子进程的数据持久化(子进程只持久化fork这一瞬间整个实例中的所有数据即可,不关心新的数据变更,因为子进程只需要一份内存快照,然后持久化到磁盘上)。

但是请注意,主进程在拷贝内存数据时,这个阶段就涉及到新内存的申请,如果此时操作系统开启了内存大页,那么在此期间,客户端即便只修改10B的数据,Redis在申请内存时也会以2MB为单位向操作系统申请,申请内存的耗时变长,进而导致每个写请求的延迟增加,影响到Redis性能。

同样地,如果这个写请求操作的是一个bigkey,那主进程在拷贝这个bigkey内存块时,一次申请的内存会更大,时间也会更久。可见,bigkey在这里又一次影响到了性能。

8.开启AOF

前面我们分析了RDB和AOFrewrite对Redis性能的影响,主要关注点在fork上。

其实,关于数据持久化方面,还有影响Redis性能的因素,这次我们重点来看AOF数据持久化。

如果你的AOF配置不合理,还是有可能会导致性能问题。

当Redis开启AOF后,其工作原理如下:

1)Redis执行写命令后,把这个命令写入到AOF文件内存中(write系统调用)

2)Redis根据配置的AOF刷盘策略,把AOF内存数据刷到磁盘上(fsync系统调用)

为了保证AOF文件数据的安全性,Redis提供了3种刷盘机制:

1)appendfsyncalways:主线程每次执行写操作后立即刷盘,此方案会占用比较大的磁盘IO资源,但数据安全性最高。

2)appendfsyncno:主线程每次写操作只写内存就返回,内存数据什么时候刷到磁盘,交由操作系统决定,此方案对性能影响最小,但数据安全性也最低,Redis宕机时丢失的数据取决于操作系统刷盘时机。

3)appendfsynceverysec:主线程每次写操作只写内存就返回,然后由后台线程每隔1秒执行一次刷盘操作(触发fsync系统调用),此方案对性能影响相对较小,但当Redis宕机时会丢失1秒的数据。

看到这里,我猜你肯定和大多数人的想法一样,选比较折中的方案appendfsynceverysec就没问题了吧?

这个方案优势在于,Redis主线程写完内存后就返回,具体的刷盘操作是放到后台线程中执行的,后台线程每隔1秒把内存中的数据刷到磁盘中。

这种方案既兼顾了性能,又尽可能地保证了数据安全,是不是觉得很完美?

但是,这里我要给你泼一盆冷水了,采用这种方案你也要警惕一下,因为这种方案还是存在导致Redis延迟变大的情况发生,甚至会阻塞整个Redis。

你试想这样一种情况:当Redis后台线程在执行AOF文件刷盘时,如果此时磁盘的IO负载很高,那这个后台线程在执行刷盘操作(fsync系统调用)时就会被阻塞住。

此时的主线程依旧会接收写请求,紧接着,主线程又需要把数据写到文件内存中(write系统调用),当主线程使用后台子线程执行了一次fsync,需要再次把新接收的操作记录写回磁盘时,如果主线程发现上一次的fsync还没有执行完,那么它就会阻塞。

所以,如果后台子线程执行的fsync频繁阻塞的话(比如AOF重写占用了大量的磁盘IO带宽),主线程也会阻塞,导致Redis性能变慢。

看到了么?在这个过程中,主线程依旧有阻塞的风险。

所以,尽管你的AOF配置为appendfsynceverysec,也不能掉以轻心,要警惕磁盘压力过大导致的Redis有性能问题。

那什么情况下会导致磁盘IO负载过大?以及如何解决这个问题呢?

我总结了以下几种情况,你可以参考进行问题排查:

1)进程正在执行AOFrewrite,这个过程会占用大量的磁盘IO资源

2)有其他应用程序在执行大量的写文件操作,也会占用磁盘IO资源

对于情况1,说白了就是,Redis的AOF后台子线程刷盘操作,撞上了子进程AOFrewrite!

9.绑定CPU

很多时候,我们在部署服务时,为了提高服务性能,降低应用程序在多个CPU核心之间的上下文切换带来的性能损耗,通常采用的方案是进程绑定CPU的方式提高性能。

我们都知道,一般现代的服务器会有多个CPU,而每个CPU又包含多个物理核心,每个物理核心又分为多个逻辑核心,每个物理核下的逻辑核共用L1/L2Cache。

而RedisServer除了主线程服务客户端请求之外,还会创建子进程、子线程。

其中子进程用于数据持久化,而子线程用于执行一些比较耗时操作,例如异步释放fd、异步AOF刷盘、异步lazy-free等等。

如果你把Redis进程只绑定了一个CPU逻辑核心上,那么当Redis在进行数据持久化时,fork出的子进程会继承父进程的CPU使用偏好。

而此时的子进程会消耗大量的CPU资源进行数据持久化(把实例数据全部扫描出来需要耗费CPU),这就会导致子进程会与主进程发生CPU争抢,进而影响到主进程服务客户端请求,访问延迟变大。

这就是Redis绑定CPU带来的性能问题。

10.使用Swap

如果你发现Redis突然变得非常慢,每次的操作耗时都达到了几百毫秒甚至秒级,那此时你就需要检查Redis是否使用到了Swap,在这种情况下Redis基本上已经无法提供高性能的服务了。

什么是Swap?为什么使用Swap会导致Redis的性能下降?

如果你对操作系统有些了解,就会知道操作系统为了缓解内存不足对应用程序的影响,允许把一部分内存中的数据换到磁盘上,以达到应用程序对内存使用的缓冲,这些内存数据被换到磁盘上的区域,就是Swap。

问题就在于,当内存中的数据被换到磁盘上后,Redis再访问这些数据时,就需要从磁盘上读取,访问磁盘的速度要比访问内存慢几百倍!

尤其是针对Redis这种对性能要求极高、性能极其敏感的数据库来说,这个操作延时是无法接受的。

此时,你需要检查Redis机器的内存使用情况,确认是否存在使用了Swap。

你可以通过以下方式来查看Redis进程是否使用到了Swap:

#先找到Redis的进程ID$ps-aux|grepredis-server#查看RedisSwap使用情况$cat/proc/$pid/smaps|egrep'^(Swap|Size)'

输出结果如下:

Size:1256kBSwap:0kBSize:4kB Swap:0kBSize:132kBSwap:0kBSize:63488kBSwap:0kBSize:132kBSwap:0kBSize:65404kBSwap:0kBSize:1921024kBSwap:0kB

每一行Size表示Redis所用的一块内存大小,Size下面的Swap就表示这块Size大小的内存,有多少数据已经被换到磁盘上了,如果这两个值相等,说明这块内存的数据都已经完全被换到磁盘上了。

如果只是少量数据被换到磁盘上,例如每一块Swap占对应Size的比例很小,那影响并不是很大。如果是几百兆甚至上GB的内存被换到了磁盘上,那么你就需要警惕了,这种情况Redis的性能肯定会急剧下降。

11.碎片整理

Redis的数据都存储在内存中,当我们的应用程序频繁修改Redis中的数据时,就有可能会导致Redis产生内存碎片。

内存碎片会降低Redis的内存使用率,我们可以通过执行INFO命令,得到这个实例的内存碎片率:

#Memoryused_memory:5709194824used_memory_human:5.32Gused_memory_rss:8264855552used_memory_rss_human:7.70G...mem_fragmentation_ratio:1.45

这个内存碎片率是怎么计算的?

很简单,mem_fragmentation_ratio=used_memory_rss/used_memory。

其中used_memory表示Redis存储数据的内存大小,而used_memory_rss表示操作系统实际分配给Redis进程的大小。

如果mem_fragmentation_ratio>1.5,说明内存碎片率已经超过了50%,这时我们就需要采取一些措施来降低内存碎片了。

解决的方案一般如下:

1)如果你使用的是Redis4.0以下版本,只能通过重启实例来解决

2)如果你使用的是Redis4.0版本,它正好提供了自动碎片整理的功能,可以通过配置开启碎片自动整理。

但是,开启内存碎片整理,它也有可能会导致Redis性能下降。

原因在于,Redis的碎片整理工作是也在主线程中执行的,当其进行碎片整理时,必然会消耗CPU资源,产生更多的耗时,从而影响到客户端的请求。

所以,当你需要开启这个功能时,最好提前测试评估它对Redis的影响。

Redis碎片整理的参数配置如下:

#开启自动内存碎片整理(总开关)activedefragyes#内存使用100MB以下,不进行碎片整理active-defrag-ignore-bytes100mb#内存碎片率超过10%,开始碎片整理active-defrag-threshold-lower10#内存碎片率超过100%,尽最大努力碎片整理active-defrag-threshold-upper100 #内存碎片整理占用CPU资源最小百分比active-defrag-cycle-min1#内存碎片整理占用CPU资源最大百分比active-defrag-cycle-max25#碎片整理期间,对于List/Set/Hash/ZSet类型元素一次Scan的数量active-defrag-max-scan-fields1000

2.Redis如何优化

1.慢查询优化

1)尽量不使用O(N)以上复杂度过高的命令,对于数据的聚合操作,放在客户端做。

2)执行O(N)命令,保证N尽量的小(推荐N<=300),每次获取尽量少的数据,让Redis可以及时处理返回。

2.集中过期优化

一般有两种方案来规避这个问题:

1.集中过期key增加一个随机过期时间,把集中过期的时间打散,降低Redis清理过期key的压力

2.如果你使用的Redis是4.0以上版本,可以开启lazy-free机制,当删除过期key时,把释放内存的操作放到后台线程中执行,避免阻塞主线程。

第一种方案,在设置key的过期时间时,增加一个随机时间,伪代码可以这么写:

#在过期时间点之后的5分钟内随机过期掉

redis.expireat(key,expire_time+random(300))

第二种方案,Redis4.0以上版本,开启lazy-free机制:

#释放过期key的内存,放到后台线程执行

lazyfree-lazy-expireyes

运维层面,你需要把Redis的各项运行状态数据监控起来,在Redis上执行INFO命令就可以拿到这个实例所有的运行状态数据。

在这里我们需要重点关注expired_keys这一项,它代表整个实例到目前为止,累计删除过期key的数量。

你需要把这个指标监控起来,当这个指标在很短时间内出现了突增,需要及时报警出来,然后与业务应用报慢的时间点进行对比分析,确认时间是否一致,如果一致,则可以确认确实是因为集中过期key导致的延迟变大。

3.实例内存达到上限优化

1)避免存储bigkey,降低释放内存的耗时

2)淘汰策略改为随机淘汰,随机淘汰比LRU要快很多(视业务情况调整)

3)拆分实例,把淘汰key的压力分摊到多个实例上

4)如果使用的是Redis4.0以上版本,开启layz-free机制,把淘汰key释放内存的操作放到后台线程中执行(配置lazyfree-lazy-eviction=yes)

4.fork耗时严重优化

1)控制Redis实例的内存:尽量在10G以下,执行fork的耗时与实例大小有关,实例越大,耗时越久。

2)合理配置数据持久化策略:在slave节点执行RDB备份,推荐在低峰期执行,而对于丢失数据不敏感的业务(例如把Redis当做纯缓存使用),可以关闭AOF和AOFrewrite。

3)Redis实例不要部署在虚拟机上:fork的耗时也与系统也有关,虚拟机比物理机耗时更久。

4)降低主从库全量同步的概率:适当调大repl-backlog-size参数,避免主从全量同步。

从建立同步时,优先检测是否可以尝试只同步部分数据,这种情况就是针对于之前已经建立好了复制链路,只是因为故障导致临时断开,故障恢复后重新建立同步时,为了避免全量同步的资源消耗,Redis会优先尝试部分数据同步,如果条件不符合,才会触发全量同步。

这个判断依据就是在master上维护的复制缓冲区大小,如果这个缓冲区配置的过小,很有可能在主从断开复制的这段时间内,master产生的写入导致复制缓冲区的数据被覆盖,重新建立同步时的slave需要同步的offset位置在master的缓冲区中找不到,那么此时就会触发全量同步。

如何避免这种情况?解决方案就是适当调大复制缓冲区repl-backlog-size的大小,这个缓冲区的大小默认为1MB,如果实例写入量比较大,可以针对性调大此配置。

5.多核CPU优化

那如何解决这个问题呢?

如果你确实想要绑定CPU,可以优化的方案是,不要让Redis进程只绑定在一个CPU逻辑核上,而是绑定在多个逻辑核心上,而且,绑定的多个逻辑核心最好是同一个物理核心,这样它们还可以共用L1/L2Cache。

当然,即便我们把Redis绑定在多个逻辑核心上,也只能在一定程度上缓解主线程、子进程、后台线程在CPU资源上的竞争。

因为这些子进程、子线程还是会在这多个逻辑核心上进行切换,存在性能损耗。

如何再进一步优化?

可能你已经想到了,我们是否可以让主线程、子进程、后台线程,分别绑定在固定的CPU核心上,不让它们来回切换,这样一来,他们各自使用的CPU资源互不影响。

其实,这个方案Redis官方已经想到了。

Redis在6.0版本已经推出了这个功能,我们可以通过以下配置,对主线程、后台线程、后台RDB进程、AOFrewrite进程,绑定固定的CPU逻辑核心:

  • Redis6.0前绑定CPU核

taskset -c 0 ./redis-server
  • Redis6.0后绑定CPU核

#RedisServer和IO线程绑定到CPU核心0,2,4,6server_cpulist0-7:2#后台子线程绑定到CPU核心1,3bio_cpulist1,3#后台AOFrewrite进程绑定到CPU核心8,9,10,11aof_rewrite_cpulist8-11#后台RDB进程绑定到CPU核心1,10,11#bgsave_cpulist1,10-1

如果你使用的正好是Redis6.0版本,就可以通过以上配置,来进一步提高Redis性能。

这里我需要提醒你的是,一般来说,Redis的性能已经足够优秀,除非你对Redis的性能有更加严苛的要求,否则不建议你绑定CPU。

6.查看Redis内存是否发生Swap

$redis-cli info | grep process_idprocess_id: 5332

然后,进入Redis所在机器的/proc目录下的该进程目录中:

$cd /proc/5332

最后,运行下面的命令,查看该Redis进程的使用情况。在这儿,我只截取了部分结果:

$cat smaps | egrep '^(Swap|Size)'Size: 584 kBSwap: 0 kBSize: 4 kBSwap: 4 kBSize: 4 kBSwap: 0 kBSize: 462044 kBSwap: 462008 kBSize: 21392 kBSwap: 0 kB

一旦发生内存swap,最直接的解决方法就是增加机器内存。如果该实例在一个Redis切片集群中,可以增加Redis集群的实例个数,来分摊每个实例服务的数据量,进而减少每个实例所需的内存量。

7.内存大页

如果采用了内存大页,那么,即使客户端请求只修改100B的数据,Redis也需要拷贝2MB的大页。相反,如果是常规内存页机制,只用拷贝4KB。两者相比,你可以看到,当客户端请求修改或新写入数据较多时,内存大页机制将导致大量的拷贝,这就会影响Redis正常的访存操作,最终导致性能变慢。

首先,我们要先排查下内存大页。方法是:在Redis实例运行的机器上执行如下命令:

$cat /sys/kernel/mm/transparent_hugepage/enabled[always] madvise never

如果执行结果是always,就表明内存大页机制被启动了;如果是never,就表示,内存大页机制被禁止。

在实际生产环境中部署时,我建议你不要使用内存大页机制,操作也很简单,只需要执行下面的命令就可以了:

echonever/sys/kernel/mm/transparent_hugepage/enabled

其实,操作系统提供的内存大页机制,其优势是,可以在一定程序上降低应用程序申请内存的次数。

但是对于Redis这种对性能和延迟极其敏感的数据库来说,我们希望Redis在每次申请内存时,耗时尽量短,所以我不建议你在Redis机器上开启这个机制。

8.删除使用LazyFree

支持版本:Redis4.0+

1)主动删除键使用lazyfree

  • UNLINK命令

127.0.0.1:7000>LLENmylist(integer)2000000127.0.0.1:7000>UNLINKmylist (integer)1127.0.0.1:7000>SLOWLOGget1)1)(integer)12)(integer)15054651883)(integer)304)1)"UNLINK"2)"mylist"5)"127.0.0.1:17015"6)""

注意:DEL命令,还是并发阻塞的删除操作

  • FLUSHALL/FLUSHDBASYNC

127.0.0.1:7000> DBSIZE(integer) 1812295127.0.0.1:7000>flushall//同步清理实例数据,180万个key耗时1020毫秒OK(1.02s) 127.0.0.1:7000> DBSIZE(integer) 1812637127.0.0.1:7000>flushallasync//异步清理实例数据,180万个key耗时约9毫秒OK127.0.0.1:7000> SLOWLOGget 1) 1) (integer) 29961092) (integer) 15054659893) (integer) 9274 //指令运行耗时9.2毫秒4) 1) "flushall" 2) "async"5) "127.0.0.1:20110"6) ""

2)被动删除键使用lazyfree

lazyfree应用于被动删除中,目前有4种场景,每种场景对应一个配置参数;默认都是关闭。

lazyfree-lazy-eviction no lazyfree-lazy-expire nolazyfree-lazy-server-del noslave-lazy-flush no
  • lazyfree-lazy-eviction

针对redis内存使用达到maxmeory,并设置有淘汰策略时;在被动淘汰键时,是否采用lazyfree机制;因为此场景开启lazyfree,可能使用淘汰键的内存释放不及时,导致redis内存超用,超过maxmemory的限制。此场景使用时,请结合业务测试。(生产环境不建议设置yes)

  • lazyfree-lazy-expire

针对设置有TTL的键,达到过期后,被redis清理删除时是否采用lazyfree机制;此场景建议开启,因TTL本身是自适应调整的速度。

  • lazyfree-lazy-server-del

针对有些指令在处理已存在的键时,会带有一个隐式的DEL键的操作。如rename命令,当目标键已存在,redis会先删除目标键,如果这些目标键是一个bigkey,那就会引入阻塞删除的性能问题。此参数设置就是解决这类问题,建议可开启。

  • slave-lazy-flush

针对slave进行全量数据同步,slave在加载master的RDB文件前,会运行flushall来清理自己的数据场景,参数设置决定是否采用异常flush机制。如果内存变动不大,建议可开启。可减少全量同步耗时,从而减少主库因输出缓冲区爆涨引起的内存使用增长。

3)lazyfree的监控

lazyfree能监控的数据指标,只有一个值:lazyfree_pending_objects,表示redis执行lazyfree操作,在等待被实际回收内容的键个数。并不能体现单个大键的元素个数或等待lazyfree回收的内存大小。所以此值有一定参考值,可监测redislazyfree的效率或堆积键数量;比如在flushallasync场景下会有少量的堆积。

# info memory# Memorylazyfree_pending_objects:0

注意事项:unlink命令入口函数unlinkCommand()和del调用相同函数delGenericCommand()进行删除KEY操作,使用lazy标识是否为lazyfree调用。如果是lazyfree,则调用dbAsyncDelete()函数。

但并非每次unlink命令就一定启用lazyfree,redis会先判断释放KEY的代价(cost),当cost大于LAZYFREE_THRESHOLD(64)才进行lazyfree.

释放key代价计算函数lazyfreeGetFreeEffort(),集合类型键,且满足对应编码,cost就是集合键的元数个数,否则cost就是1。

举例:

  • 一个包含100元素的listkey,它的freecost就是100
  • 一个512MB的stringkey,它的freecost是1所以可以看出,redis的lazyfree的cost计算主要时间复杂度相关。

9.AOF优化

Redis提供了一个配置项,当子进程在AOFrewrite期间,可以让后台子线程不执行刷盘(不触发fsync系统调用)操作。

这相当于在AOFrewrite期间,临时把appendfsync设置为了none,配置如下:

# AOF rewrite期间,AOF后台子线程不进行刷盘操作#相当于在这期间,临时把appendfsync设置为了noneno-appendfsync-on-rewrite yes

当然,开启这个配置项,在AOFrewrite期间,如果实例发生宕机,那么此时会丢失更多的数据,性能和数据安全性,你需要权衡后进行选择。

如果占用磁盘资源的是其他应用程序,那就比较简单了,你需要定位到是哪个应用程序在大量写磁盘,然后把这个应用程序迁移到其他机器上执行就好了,避免对Redis产生影响。

当然,如果你对Redis的性能和数据安全都有很高的要求,那么建议从硬件层面来优化,更换为SSD磁盘,提高磁盘的IO能力,保证AOF期间有充足的磁盘资源可以使用。同时尽可能让Redis运行在独立的机器上。

10.Swap优化

1)增加机器的内存,让Redis有足够的内存可以使用

2)整理内存空间,释放出足够的内存供Redis使用,然后释放Redis的Swap,让Redis重新使用内存

释放Redis的Swap过程通常要重启实例,为了避免重启实例对业务的影响,一般会先进行主从切换,然后释放旧主节点的Swap,重启旧主节点实例,待从库数据同步完成后,再进行主从切换即可。

预防的办法就是,你需要对Redis机器的内存和Swap使用情况进行监控,在内存不足或使用到Swap时报警出来,及时处理。

3.Redis变慢了排查步骤

1.获取Redis实例在当前环境下的基线性能。

2.是否用了慢查询命令?如果是的话,就使用其他命令替代慢查询命令,或者把聚合计算命令放在客户端做。

3.是否对过期key设置了相同的过期时间?对于批量删除的key,可以在每个key的过期时间上加一个随机数,避免同时删除。

4.是否存在bigkey?对于bigkey的删除操作,如果你的Redis是4.0及以上的版本,可以直接利用异步线程机制减少主线程阻塞;如果是Redis4.0以前的版本,可以使用SCAN命令迭代删除;对于bigkey的集合查询和聚合操作,可以使用SCAN命令在客户端完成。

5.RedisAOF配置级别是什么?业务层面是否的确需要这一可靠性级别?如果我们需要高性能,同时也允许数据丢失,可以将配置项no-appendfsync-on-rewrite设置为yes,避免AOF重写和fsync竞争磁盘IO资源,导致Redis延迟增加。当然,如果既需要高性能又需要高可靠性,最好使用高速固态盘作为AOF日志的写入盘。

6.Redis实例的内存使用是否过大?发生swap了吗?如果是的话,就增加机器内存,或者是使用Redis集群,分摊单机Redis的键值对数量和内存压力。同时,要避免出现Redis和其他内存需求大的应用共享机器的情况。

7.在Redis实例的运行环境中,是否启用了透明大页机制?如果是的话,直接关闭内存大页机制就行了。

8.是否运行了Redis主从集群?如果是的话,把主库实例的数据量大小控制在2~4GB,以免主从复制时,从库因加载大的RDB文件而阻塞。

9.是否使用了多核CPU或NUMA架构的机器运行Redis实例?使用多核CPU时,可以给Redis实例绑定物理核;使用NUMA架构时,注意把Redis实例和网络中断处理程序运行在同一个CPUSocket上。