You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

194 lines
16 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 19 | 波动的响应延迟如何应对变慢的Redis
你好,我是蒋德钧。
上节课我介绍了判断Redis变慢的两种方法分别是响应延迟和基线性能。除此之外我还给你分享了从Redis的自身命令操作层面排查和解决问题的两种方案。
但是如果在排查时你发现Redis没有执行大量的慢查询命令也没有同时删除大量过期keys那么我们是不是就束手无策了呢
当然不是!我还有很多“锦囊妙计”,准备在这节课分享给你呢!
如果上节课的方法不管用,那就说明,你要关注影响性能的其他机制了,也就是文件系统和操作系统。
Redis会持久化保存数据到磁盘这个过程要依赖文件系统来完成所以文件系统将数据写回磁盘的机制会直接影响到Redis持久化的效率。而且在持久化的过程中Redis也还在接收其他请求持久化的效率高低又会影响到Redis处理请求的性能。
另一方面Redis是内存数据库内存操作非常频繁所以操作系统的内存机制会直接影响到Redis的处理效率。比如说如果Redis的内存不够用了操作系统会启动swap机制这就会直接拖慢Redis。
那么接下来我再从这两个层面继续给你介绍如何进一步解决Redis变慢的问题。
![](https://static001.geekbang.org/resource/image/cd/06/cd026801924e197f5c79828c368cd706.jpg?wh=4242*3039)
## 文件系统AOF模式
你可能会问Redis是个内存数据库为什么它的性能还和文件系统有关呢
我在前面讲过为了保证数据可靠性Redis会采用AOF日志或RDB快照。其中AOF日志提供了三种日志写回策略no、everysec、always。这三种写回策略依赖文件系统的两个系统调用完成也就是write和fsync。
write只要把日志记录写到内核缓冲区就可以返回了并不需要等待日志实际写回到磁盘而fsync需要把日志记录写回到磁盘后才能返回时间较长。下面这张表展示了三种写回策略所执行的系统调用。
![](https://static001.geekbang.org/resource/image/9f/a4/9f1316094001ca64c8dfca37c2c49ea4.jpg?wh=2720*598)
当写回策略配置为everysec和always时Redis需要调用fsync把日志写回磁盘。但是这两种写回策略的具体执行情况还不太一样。
在使用everysec时Redis允许丢失一秒的操作记录所以Redis主线程并不需要确保每个操作记录日志都写回磁盘。而且fsync的执行时间很长如果是在Redis主线程中执行fsync就容易阻塞主线程。所以当写回策略配置为everysec时Redis会使用后台的子线程异步完成fsync的操作。
而对于always策略来说Redis需要确保每个操作记录日志都写回磁盘如果用后台子线程异步完成主线程就无法及时地知道每个操作是否已经完成了这就不符合always策略的要求了。所以always策略并不使用后台子线程来执行。
另外在使用AOF日志时为了避免日志文件不断增大Redis会执行AOF重写生成体量缩小的新的AOF日志文件。AOF重写本身需要的时间很长也容易阻塞Redis主线程所以Redis使用子进程来进行AOF重写。
但是这里有一个潜在的风险点AOF重写会对磁盘进行大量IO操作同时fsync又需要等到数据写到磁盘后才能返回所以当AOF重写的压力比较大时就会导致fsync被阻塞。虽然fsync是由后台子线程负责执行的但是主线程会监控fsync的执行进度。
当主线程使用后台子线程执行了一次fsync需要再次把新接收的操作记录写回磁盘时如果主线程发现上一次的fsync还没有执行完那么它就会阻塞。所以如果后台子线程执行的fsync频繁阻塞的话比如AOF重写占用了大量的磁盘IO带宽主线程也会阻塞导致Redis性能变慢。
为了帮助你理解我再画一张图来展示下在磁盘压力小和压力大的时候fsync后台子线程和主线程受到的影响。
![](https://static001.geekbang.org/resource/image/2a/a6/2a47b3f6fd7beaf466a675777ebd28a6.jpg?wh=3000*1557)
好了说到这里你已经了解了由于fsync后台子线程和AOF重写子进程的存在主IO线程一般不会被阻塞。但是如果在重写日志时AOF重写子进程的写入量比较大fsync线程也会被阻塞进而阻塞主线程导致延迟增加。现在我来给出排查和解决建议。
首先你可以检查下Redis配置文件中的appendfsync配置项该配置项的取值表明了Redis实例使用的是哪种AOF日志写回策略如下所示
![](https://static001.geekbang.org/resource/image/ba/e9/ba770d1f25ffae79a101c13b9f8aa9e9.jpg?wh=2738*601)
如果AOF写回策略使用了everysec或always配置请先确认下业务方对数据可靠性的要求明确是否需要每一秒或每一个操作都记日志。有的业务方不了解Redis AOF机制很可能就直接使用数据可靠性最高等级的always配置了。其实在有些场景中例如Redis用于缓存数据丢了还可以从后端数据库中获取并不需要很高的数据可靠性。
如果业务应用对延迟非常敏感但同时允许一定量的数据丢失那么可以把配置项no-appendfsync-on-rewrite设置为yes如下所示
```
no-appendfsync-on-rewrite yes
```
这个配置项设置为yes时表示在AOF重写时不进行fsync操作。也就是说Redis实例把写命令写到内存后不调用后台线程进行fsync操作就可以直接返回了。当然如果此时实例发生宕机就会导致数据丢失。反之如果这个配置项设置为no也是默认配置在AOF重写时Redis实例仍然会调用后台线程进行fsync操作这就会给实例带来阻塞。
如果的确需要高性能,同时也需要高可靠数据保证,我建议你考虑**采用高速的固态硬盘作为AOF日志的写入设备。**
高速固态盘的带宽和并发度比传统的机械硬盘的要高出10倍及以上。在AOF重写和fsync后台线程同时执行时固态硬盘可以提供较为充足的磁盘IO资源让AOF重写和fsync后台线程的磁盘IO资源竞争减少从而降低对Redis的性能影响。
## 操作系统swap
如果Redis的AOF日志配置只是no或者就没有采用AOF模式那么还会有什么问题导致性能变慢吗
接下来,我就再说一个潜在的瓶颈:**操作系统的内存swap**。
内存swap是操作系统里将内存数据在内存和磁盘间来回换入和换出的机制涉及到磁盘的读写所以一旦触发swap无论是被换入数据的进程还是被换出数据的进程其性能都会受到慢速磁盘读写的影响。
Redis是内存数据库内存使用量大如果没有控制好内存的使用量或者和其他内存需求大的应用一起运行了就可能受到swap的影响而导致性能变慢。
这一点对于Redis内存数据库而言显得更为重要正常情况下Redis的操作是直接通过访问内存就能完成一旦swap被触发了Redis的请求操作需要等到磁盘数据读写完成才行。而且和我刚才说的AOF日志文件读写使用fsync线程不同swap触发后影响的是Redis主IO线程这会极大地增加Redis的响应时间。
说到这儿我想给你分享一个我曾经遇到过的因为swap而导致性能降低的例子。
在正常情况下我们运行的一个实例完成5000万个GET请求时需要300s但是有一次这个实例完成5000万GET请求花了将近4个小时的时间。经过问题复现我们发现当时Redis处理请求用了近4小时的情况下该实例所在的机器已经发生了swap。从300s到4个小时延迟增加了将近48倍可以看到swap对性能造成的严重影响。
那么什么时候会触发swap呢
通常触发swap的原因主要是**物理机器内存不足**对于Redis而言有两种常见的情况
* Redis实例自身使用了大量的内存导致物理机器的可用内存不足
* 和Redis实例在同一台机器上运行的其他进程在进行大量的文件读写操作。文件读写本身会占用系统内存这会导致分配给Redis实例的内存量变少进而触发Redis发生swap。
针对这个问题,我也给你提供一个解决思路:**增加机器的内存或者使用Redis集群**。
操作系统本身会在后台记录每个进程的swap使用情况即有多少数据量发生了swap。你可以先通过下面的命令查看Redis的进程号这里是5332。
```
$ redis-cli info | grep process_id
process_id: 5332
```
然后进入Redis所在机器的/proc目录下的该进程目录中
```
$ cd /proc/5332
```
最后运行下面的命令查看该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实例所用的一块内存大小而Size下方的Swap和它相对应表示这块Size大小的内存区域有多少已经被换出到磁盘上了。如果这两个值相等就表示这块内存区域已经完全被换出到磁盘了。
作为内存数据库Redis本身会使用很多大小不一的内存块所以你可以看到有很多Size行有的很小就是4KB而有的很大例如462044KB。**不同内存块被换出到磁盘上的大小也不一样**例如刚刚的结果中的第一个4KB内存块它下方的Swap也是4KB这表示这个内存块已经被换出了另外462044KB这个内存块也被换出了462008KB差不多有462MB。
这里有个重要的地方我得提醒你一下当出现百MB甚至GB级别的swap大小时就表明此时Redis实例的内存压力很大很有可能会变慢。所以swap的大小是排查Redis性能变慢是否由swap引起的重要指标。
一旦发生内存swap最直接的解决方法就是**增加机器内存**。如果该实例在一个Redis切片集群中可以增加Redis集群的实例个数来分摊每个实例服务的数据量进而减少每个实例所需的内存量。
当然如果Redis实例和其他操作大量文件的程序例如数据分析程序共享机器你可以将Redis实例迁移到单独的机器上运行以满足它的内存需求量。如果该实例正好是Redis主从集群中的主库而从库的内存很大也可以考虑进行主从切换把大内存的从库变成主库由它来处理客户端请求。
## 操作系统:内存大页
除了内存swap还有一个和内存相关的因素即内存大页机制Transparent Huge Page, THP也会影响Redis性能。
Linux内核从2.6.38开始支持内存大页机制该机制支持2MB大小的内存页分配而常规的内存页分配是按4KB的粒度来执行的。
很多人都觉得“Redis是内存数据库内存大页不正好可以满足Redis的需求吗而且在分配相同的内存量时内存大页还能减少分配次数不也是对Redis友好吗?”
其实系统的设计通常是一个取舍过程我们称之为trade-off。很多机制通常都是优势和劣势并存的。Redis使用内存大页就是一个典型的例子。
虽然内存大页可以给Redis带来内存分配方面的收益但是不要忘了Redis为了提供数据可靠性保证需要将数据做持久化保存。这个写入过程由额外的线程执行所以此时Redis主线程仍然可以接收客户端写请求。客户端的写请求可能会修改正在进行持久化的数据。在这一过程中Redis就会采用写时复制机制也就是说一旦有数据要被修改Redis并不会直接修改内存中的数据而是将这些数据拷贝一份然后再进行修改。
如果采用了内存大页那么即使客户端请求只修改100B的数据Redis也需要拷贝2MB的大页。相反如果是常规内存页机制只用拷贝4KB。两者相比你可以看到当客户端请求修改或新写入数据较多时内存大页机制将导致大量的拷贝这就会影响Redis正常的访存操作最终导致性能变慢。
那该怎么办呢?很简单,关闭内存大页,就行了。
首先我们要先排查下内存大页。方法是在Redis实例运行的机器上执行如下命令:
```
cat /sys/kernel/mm/transparent_hugepage/enabled
```
如果执行结果是always就表明内存大页机制被启动了如果是never就表示内存大页机制被禁止。
在实际生产环境中部署时,我建议你不要使用内存大页机制,操作也很简单,只需要执行下面的命令就可以了:
```
echo never /sys/kernel/mm/transparent_hugepage/enabled
```
## 小结
这节课我从文件系统和操作系统两个维度给你介绍了应对Redis变慢的方法。
为了方便你应用我给你梳理了一个包含9个检查点的Checklist希望你在遇到Redis性能变慢时按照这些步骤逐一检查高效地解决问题。
1. 获取Redis实例在当前环境下的基线性能。
2. 是否用了慢查询命令?如果是的话,就使用其他命令替代慢查询命令,或者把聚合计算命令放在客户端做。
3. 是否对过期key设置了相同的过期时间对于批量删除的key可以在每个key的过期时间上加一个随机数避免同时删除。
4. 是否存在bigkey 对于bigkey的删除操作如果你的Redis是4.0及以上的版本可以直接利用异步线程机制减少主线程阻塞如果是Redis 4.0以前的版本可以使用SCAN命令迭代删除对于bigkey的集合查询和聚合操作可以使用SCAN命令在客户端完成。
5. Redis AOF配置级别是什么业务层面是否的确需要这一可靠性级别如果我们需要高性能同时也允许数据丢失可以将配置项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实例和网络中断处理程序运行在同一个CPU Socket上。
实际上,影响系统性能的因素还有很多,这两节课给你讲的都是应对最常见问题的解决方案。
如果你遇到了一些特殊情况也不要慌我再给你分享一个小技巧仔细检查下有没有恼人的“邻居”具体点说就是Redis所在的机器上有没有一些其他占内存、磁盘IO和网络IO的程序比如说数据库程序或者数据采集程序。如果有的话我建议你将这些程序迁移到其他机器上运行。
为了保证Redis高性能我们需要给Redis充足的计算、内存和IO资源给它提供一个“安静”的环境。
## 每课一问
这两节课我向你介绍了系统性定位、排查和解决Redis变慢的方法。所以我想请你聊一聊你遇到过Redis变慢的情况吗如果有的话你是怎么解决的呢
欢迎你在留言区分享一下自己的经验,如果觉得今天的内容对你有所帮助,也欢迎分享给你的朋友或同事,我们下节课见。