3万字聊聊什么是Redis(四)

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

大家好,我是Leo

继上篇Redis技术总结三,我们继续聊聊Redis的相关技术!

上一篇我们介绍了

  1. 主要介绍了Redis的类型的底层实现以及技术,类型选择的依据
  2. 通过时间序列数据引出多种类型的搭配使用思路以及扩展一下RedisTimeSeries模块的使用。
  3. Redis作为消息队列也是高频的面试问题,通过这一问题延伸了List的优劣和Streams的应用

这篇主要是介绍一下 Redis 有哪些阻塞原因

推荐阅读

3万字聊聊什么是MySQL

3万字聊聊什么是Redis(一)

3万字聊聊什么是Redis(二)

3万字聊聊什么是Redis(三)

单线程模型阻塞

聊到阻塞原因,这应该是单线程的噩梦吧。平时在使用中,想了解Redis的阻塞原因,必然是要从它的交互对象入手的,下面我列举了Redis常见的交互场景

  • 客户端
  • 磁盘
  • 主从节点
  • 切片集群实例

客户端

与客户端主要是有网络IO的开销,键值对增删改查操作,数据库操作等。

Redis使用了IO多路复用机制,避免了主线程一直处在等待网络连接或请求到来的状态,所以网络IO不是导致Redis阻塞的因素。

查询:键值对查询可以通过时间复杂度来判断,当使用集合类型时,一般的时间复杂度都是O(n)。所以 集合类型的列表查询,以及聚合统计操作将是Redis的第一个阻塞点

删除:删除也是Redis阻塞的重要因素之一,很多人不明白,删除不就是直接删掉指针的索引吗?其实不然,这里和MySQL是有点区别的。Redis这里为了保证高效的管理内存,操作系统需要把释放掉的内存,插入一个空闲内存块,以便后续进行管理和分配。所以如果一下子释放大量内存,插入空闲内存块的效率就被拉下来了。这里也可以称为 bigkey删除,也为第二个阻塞点

数据库:如果删除bigkey为阻塞Redis的因素之一的话,清空数据库也是名副其实的第三个阻塞点了

磁盘

与磁盘的话主要是生成RDB快照保存到本地,记录AOF日志,AOF日志重写等。

Redis意识到了磁盘IO带来的阻塞影响,所以采用子线程生成RDB快照,以及子线程执行AOF重写操作。

唯一的一个阻塞点就是Redis记录AOF日志时,会根据不同的写会策略对数据进行保存。AOF日志同步写

主从节点

与从库主要就是在做数据同步时,主库需要生成RDB快照发给从库,从库接收,从库清空数据库,加载RDB文件等。

通过子线程生成RDB不会阻塞,清空数据库在上述已经被列为阻塞点了,主从节点这里主要说一下这个加载RDB文件,如果RDB文件过大同样会阻塞。所以 加载RDB文件也成了阻塞点之一

切片集群实例

与切片集群主要就是查询一个数据时,当前数据不在这台实例上,会向其他实例传输哈希槽信息,数据迁移,哈希槽的信息量不会太大,而数据的迁移又是渐进式的。所以哈希槽和数据迁移对Redis的阻塞影响不大。

不过如果使用的是Redis Cluster方案,同时又在迁移bigkey时,就会造成阻塞了。

调优方案

如何调优?我们可以把不是必须等待的操作全部改成异步执行。

大查询,聚合查询显然是必须操作完成之后才会继续操作的,所以这个无法调优,只能从数据类型下手,或者借助其他方案。

bigkey和清空数据库都不需要等待返回后再继续执行,所以这两个阻塞点就可以优化成异步执行。

加载RDB文件的话为了保证从库数据接收完成所以必须要等待的。严格来说从库会给主库发一个ack的信息。

AOF日志同步写也可以异步操作,它并不需要返回结果给实例。

子线程机制

上述聊到五个阻塞点,三个阻塞点都可以通过异步的方式优化Redis的整体性能,那我们聊什么子线程机制吧。

Redis主线程启动后,会使用操作系统提供的pthread_create函数创建3个子线程,分别由他们负责AOF日志同步写,bigkey删除,清空数据库进行异步执行。(下图来自蒋德钧老师)

3万字聊聊什么是Redis(四)

客户端请求Redis实例,Redis通过一个链表存储一系列异步任务,任务列表与子线程交互进行异步执行AOF日志同步写,删除bigkey,文件关闭(清空数据库)操作。

随后Redis会立刻给客户端返回执行完成。表明删除已经完成了。

CPU结构阻塞Redis性能

很多人聊到CPU是比较懵逼的,会问为什么CPU影响性能呢?

不好意思各位,实在没看懂CPU类的相关知识,计算机组成原理底子不够,这块知识我们后续再输出吧!

Redis变慢排查思路

Redis突然变慢了,你会如何排查呢?一定不要病急乱投医,因为代码这个东西就像自来水一样,补了一块漏了另一块。最后有可能窟窿越来越大

步入正题!

首先第一步就是要查看Redis的响应延迟。大部分Redis延迟很低,但是有些实例会出现很高的延迟。

Redis的延迟与硬件有着很大的关系,

第二步就是基于当前环境下的Redis基线做判断。所谓的基线性能呢,就是一个系统在低压力,无干扰下的基本性能,这个性能只能由当前的软硬件决定。

我们可以使用redis-cli命令提供的 intrinsic-latency 选项,用来监测和统计测试期间内的最大延迟,这个延迟可以作为基线性能。

第三步就是可以通过iPef这样的工具测试从客户端到服务端的网络延迟,如果这个延迟有几十毫秒甚至几百毫秒,说明Redis的运行的网络环境有很大流量在其他的应用程序在运行导致阻塞

如何解决Redis变慢

Redis变慢这个应该属于T0级事故了。为什么这么说呢?

一旦Redis延迟就会引起业务系统中的一串连锁反应,我们举个例子吧。

我目前自己负责的千万级跨境电商中,订单号的生成采用Redis存取,我的逻辑就是今天的第一个订单,并且Redis中没有以当前日期定义的key时,我会在程序中通过当前时间戳+雪花算法+随机函数+当天订单数生成一串数字并且写入Redis中。

等下次生成订单号后发送Redis中存在当前的订单号就会直接取value并且执行incrdy命令进行加1。示例如下。

订单号:2021120121523033473

如果在Redis这边阻塞了或者延迟了,那么订单系统就无法正常提供服务了,因为要等待这个订单号才能插入数据表中,积分模块,用户模块,信息通知模块,商品库存模块都将会受到或多收少的影响。

那么如何解决Redis变慢问题?我们要基于Redis本身的工作原理的理解并且结合和它交互的操作系统机制,再借助一些辅助工具来定位原因,再指定一套有效的解决方案。

自身影响

不管干啥,先反思自己!

Redis中的 慢查询 会导致实例延迟,比如查询一个大数据量的集合列表。如果请求量不大还好,一旦请求量较大就要优化操作命令了。

有两种解决思路

  • 用其他高效命令代替
  • 当执行列表的聚合统计时,为了不影响整个系统,可以选择在客户端执行。

keys命令

少用keys命令,因为需要遍历存储的键值对,所以操作延时高。keys命令一般不建议用于生产环境中。

过期key操作

聊到过期key,我们还是先来了解一下Redis的过期删除机制吧

过期删除机制是Redis用来回收内存空间的常用机制,可以对键值对设置过期时间,默认情况下,Redis每100毫秒就会删除一些过期的key,具体算法如下:

  • 采样 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 个数的 key,并将其中过期的 key 全部删除;
  • 如果超过 25% 的 key 过期了,则重复删除的过程,直到过期 key 的比例降至 25% 以下。

ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 默认为20。毫秒的概念可能不太明显,我们换算一下也就是每秒删除200个过期key(20个/100毫秒)=(200个/秒)

如果按照第一种的话,并不是造成太大的影响。如果命中了第二种,就会造成key大面积失效过期,过期的key超过了25%,会一直删除直至降至25%。这段删除期间会大量释放内存空间,大量插入链表填补。Redis就变慢了。

建议

其一:在生产环境不要频繁使用带有相同时间参数的expireat命令设置过期key。会导致一秒内有大量的key同时过期。

其二:在使用 EXPIREAT 命令设置 key 过期时间时,是否使用了相同的 UNIX 时间戳,有没有使用 EXPIRE 命令给批量的 key 设置相同的过期秒数。因为,这都会造成大量 key 在同一时间过期,导致性能变慢。

其三:首先要根据实际业务的使用需求,决定 EXPIREAT 和 EXPIRE 的过期时间参数。其次,如果一批 key 的确是同时过期,你还可以在 EXPIREAT 和 EXPIRE 的过期时间参数上,加上一个一定大小范围内的随机数,这样,既保证了 key 在一个邻近时间范围内被删除,又避免了同时过期造成的压力。

外在影响

除了Redis的内在影响,继续介绍一下外在的影响。

  1. Redis要想持久化肯定是要通过文件系统的,所以,文件系统将数据写回磁盘的机制,会直接影响到 Redis 持久化的效率。而且,在持久化的过程中,Redis 也还在接收其他请求,持久化的效率高低又会影响到 Redis 处理请求的性能。
  2. Redis 是内存数据库,内存操作非常频繁,所以,操作系统的内存机制会直接影响到 Redis 的处理效率。比如说,如果 Redis 的内存不够用了,操作系统会启动 swap 机制,这就会直接拖慢 Redis

文件系统

Redis是内存数据库,但是想要数据持久化必然离不开文件系统,我们知道RDB快照和AOF日志作为Redis持久化机制的两大招牌。就从这里说起吧。

AOF日志的写回策略主要有三种no,everysec,always。这三种写回策略主要依托 write 和 fsync 。

write:把数据写入缓冲区(内核缓冲区)等待数据刷新到磁盘。

fsync:把缓冲区(内核缓冲区)的数据写入到磁盘。

fsync的执行时间大于write,也比write更耗时!

no:调用write写日志文件,由操作系统周期性地将日志写会磁盘

everysec:每秒调用一次fsync,将日志写回磁盘

always:每执行一个操作,就调用一次fsync将日志写回磁盘

当AOF的策略为everysec时,Redis允许丢失1秒中的操作记录,由后台子线程异步完成fsyc操作。

当AOF的策略为always时,就代表每一个操作记录都要保证写入磁盘后才能进行下一操作,所以无法通过后台子线程异步执行。

在使用AOF日志时,为了避免日志不断增大,Redis提供了AOF重写,生成体量较小的AOF日志文件,AOF重写需要很长的时间,也容易阻塞Redis主线程,所以会采用异步子线程重写。

但是,AOF重写会对磁盘进行大量的IO操作,fsync需要等数据写入到磁盘之后才能返回,所以这个地方就会容易造成阻塞。如果上一个fsync没有执行完成,Redis主线新来了一些数据需要调用fsync系统这个时候就阻塞了从而导致Redis性能变慢。

根据不同的业务需求来决定AOF的写入策略,在Redis.config 配置文件中可以通过 appendfsync 配置项来决定当前Redis实例写入策略,从而达到性能的优化!

如果不了解Redis AOF机制的话,往往我们会用一个大杯子会装很小的水。这样不就很浪费性能嘛!

如果业务应用对延迟非常敏感,但同时允许一定量的数据丢失,那么,可以把配置项 no-appendfsync-on-rewrite 设置为 yes ,表示AOF重写时,不进行fsync操作。write写入到内核缓冲区之后就可以直接返回继续提供服务了。

swap机制

内存swap是操作系统里将内存数据在内存和磁盘间来回换入和换出的机制,涉及到磁盘的读写。所以,一旦触发swap,无论是被换入数据的进程,还是被换出数据的进程,其性能都会受到慢速磁盘读写的影响。

如果不掌握好Redis的内存使用,万一误撞上swap机制,Redis 的请求操作需要等到磁盘数据读写完成才行。而且,和我刚才说的 AOF 日志文件读写使用 fsync 线程不同,swap 触发后影响的是 Redis 主 IO 线程,这会极大地增加 Redis 的响应时间。也是影响Redis性能的因素之一。

什么时候触发swap机制呢?

  • Redis 实例自身使用了大量的内存,导致物理机器的可用内存不足;
  • 和 Redis 实例在同一台机器上运行的其他进程,在进行大量的文件读写操作。文件读写本身会占用系统内存,这会导致分配给 Redis 实例的内存量变少,进而触发 Redis 发生 swap。

内存大页机制

内存大页机制也会影响Redis性能。

任何机制的设定几乎都是取舍的一个过程,优势和劣势并存的。我们只需要了解Redis对应的不足,加上业务的需求把性能优化到极致即可!

Redis在做持久化时,由子线程执行写入磁盘,在此期间主线程依然可以处理读写请求。一旦有新的写请求,Redis会采用写时复制机制将这些数据拷贝一份,然后再进行修改。而不会直接修改内存中的数据。

如果采用内存大页机制,客户端修改了100B的数据,Redis还是需要2MB的大页。

如果不采用内存大页机制,客户端修改了100B的数据,Redis只需要拷贝4KB就够了。

两者相比使用内存大页机制会影响Redis正常的访存操作,最终影响性能变慢。

说完劣势,优势呢?肯定也是有的!

Redis 是内存数据库,在分配相同的内存量时,内存大页能减少分配次数,可以给 Redis 带来内存分配方面的收益。

关闭内存大页方法 cat /sys/kernel/mm/transparent_hugepage/enabled

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

莫名其妙的内存占用率

聊一下为什么数据已经删除了,但内存却闲置着没有用以及对应的解决方案。

通常情况下,内存空间闲置往往是因为操作系统发生较为严重的内存碎片。

什么是内存碎片呢?

我们举一个类似的例子吧。我们平时住的房子,有一个房间是5 * 5 m。这个时候放了一个床是 2*2 m。如下图。

3万字聊聊什么是Redis(四)

再放一个衣柜有地方嘛?这个房间剩余20平方米,为什么还放不下16平方米的柜子呢?这个就是内存碎片的大概意思。

如何形成内存碎片?

主要有两个原因

  • 操作系统的内存分配机制
  • Redis的负载特征

内存分配机制

操作系统的内存分配机制的分配策略决定了操作系统无法做到按需分配。这是因为内存分配器一般是按固定大小来分配内存,而不是按照应用程序申请的内存空间大小分配的。这就导致每次分配都会存在部分空隙。

Redis采用jemalloc内存分配器来分配内存。

分配策略:首先会给出固定大小划分的内存空间,例如8,16,32,48字节,2,4,8KB等。当前程序申请的内存最接近某个固定值时,jemalloc会给他分配相应大小的空间。

这样的分配好处是为了减少分配次数。假如Redis申请了一个20字节的空间保存数据,jemalloc就会开辟32字节,如果应用还要写入10字节数据,Redis就不用再向操作系统申请空间也可以够存放了。

Redis的负载特征

第一点就是分配的空间大小没有完成利用

如果Redis每次向分配器申请的内存空间大小不一样,这种分配方式就会有形成碎片的风险!从而降低内存空间存储效率。

比如应用A保存6字节数据,jemalloc按照分配策略会分配8字节,还剩余2字节。如果后续不再使用就形成了内存碎片!

第二点就是就是修改和删除操作,会导致内存空间的扩容和释放。 (这里引用一下蒋德均老师的图)

3万字聊聊什么是Redis(四)

  1. 一开始,应用 A、B、C、D 分别保存了 3、1、2、4 字节的数据,并占据了相应的内存空间。
  2. 然后,应用 D 删除了 1 个字节,这个 1 字节的内存空间就空出来了。
  3. 紧接着,应用 A 修改了数据,从 3 字节变成了 4 字节。为了保持 A 数据的空间连续性,操作系统就需要把 B 的数据拷贝到别的空间,比如拷贝到 D 刚刚释放的空间中。此时,应用 C 和 D 也分别删除了 2 字节和 1 字节的数据,整个内存空间上就分别出现了 2 字节和 1 字节的空闲碎片。

如果应用 E 想要一个 3 字节的连续空间,显然是不能得到满足的。因为,虽然空间总量够,但却是碎片空间,并不是连续的。

解决方案

知道了什么是内存碎片,如何形成内存碎片,下面自然到了解决方案了。

  1. 首先我们要知道如何判断有内存碎片
  2. 知道了之后如何清理

如何判断内存碎片

我们可以通过Redis提供INFO命令来监控各项指标。

used_memory:917544
used_memory_human:896.04K
used_memory_rss:11309056
used_memory_rss_human:10.79M
used_memory_peak:1033864
used_memory_peak_human:1009.63K
used_memory_peak_perc:88.75%
used_memory_overhead:853072
used_memory_startup:810088
used_memory_dataset:64472
used_memory_dataset_perc:60.00%
allocator_allocated:956776
allocator_active:1314816
allocator_resident:3551232
total_system_memory:1918222336
total_system_memory_human:1.79G
used_memory_lua:37888
used_memory_lua_human:37.00K
used_memory_scripts:0
used_memory_scripts_human:0B
number_of_cached_scripts:0
maxmemory:228000000
maxmemory_human:217.44M
maxmemory_policy:noeviction
allocator_frag_ratio:1.37
allocator_frag_bytes:358040
allocator_rss_ratio:2.70
allocator_rss_bytes:2236416
rss_overhead_ratio:3.18
rss_overhead_bytes:7757824
mem_fragmentation_ratio:12.90
mem_fragmentation_bytes:10432528
mem_not_counted_for_evict:0
mem_replication_backlog:0
mem_clients_slaves:0
mem_clients_normal:41008
mem_aof_buffer:0
mem_allocator:jemalloc-5.1.0
active_defrag_running:0
lazyfree_pending_objects:0
lazyfreed_objects:0

可以通过 mem_fragmentation_ratio 指标来判断Redis当前的内存碎片率。

来着蒋德均老师(课就不发了,怕被误以为卖课)

内存碎片率 = 操作系统给Redis的物理内存空间 / Redis为了保护数据实际申请使用的空间

mem_fragmentation_ratio = used_memory_rss / used_memory

mem_fragmentation_ratio 大于 1 但小于 1.5。这种情况是合理的。这是因为,刚才我介绍的那些因素是难以避免的。毕竟,内因的内存分配器是一定要使用的,分配策略都是通用的,不会轻易修改;而外因由 Redis 负载决定,也无法限制。所以,存在内存碎片也是正常的。

mem_fragmentation_ratio 大于 1.5 。这表明内存碎片率已经超过了 50%。一般情况下,这个时候,我们就需要采取一些措施来降低内存碎片率了。

如何清理内存碎片

最简单的方式就是重启Redis实例,使内存重新合理的分配!但是如果有数据没持久化就嗝屁了,我们不能因为要提供性能而影响真实的生产数据!

第二种方式就是Redis自身提供了一种内存碎片自动清理的方法,我们来看看这个方法的基本机制吧!

内存清理简单来说,页面A的所有数据都紧邻的拷贝到页面B上。

操作系统在清理碎片时,会把不连续的内存拷贝到紧邻的内存空间,并释放原先所占的空间。这样一来,这个页上的所有已用空间就都聚在一起了,剩余的空间就形成了一块连续的空间。碎片清理就结束了。

事事没有完美的!碎片清理是有代价的,操作系统需要把多份数据拷贝到新位置,把原有空间释放出来,这会带来时间开销。因为Redis是单线程的在拷贝数据时,只能等待!这就导致Redis无法及时处理请求,性能就会降低!

如何优化清理内存碎片带来的性能影响

Redis专门为自动内存碎片清理机制设置了参数。

我们可以通过设置参数,来控制碎片清理的开始和结束时机,以及占用的 CPU 比例,从而减少碎片清理对 Redis 本身请求处理的性能影响。首先,Redis 需要启用自动内存碎片清理,可以把 activedefrag 配置项设置为 yes

config set activedefrag yes

这个命令只是启用了自动清理功能,但是,具体什么时候清理,会受到下面这两个参数的控制。这两个参数分别设置了触发内存清理的一个条件,如果同时满足这两个条件,就开始清理。在清理的过程中,只要有一个条件不满足了,就停止自动清理。

  • active-defrag-ignore-bytes 100mb:表示内存碎片的字节数达到 100MB 时,开始清理;
  • active-defrag-threshold-lower 10:表示内存碎片空间占操作系统分配给 Redis 的总空间比例达到 10% 时,开始清理。

为了尽可能减少碎片清理对 Redis 正常请求处理的影响,自动内存碎片清理功能在执行时,还会监控清理操作占用的 CPU 时间,而且还设置了两个参数,分别用于控制清理操作占用的 CPU 时间比例的上、下限,既保证清理工作能正常进行,又避免了降低 Redis 性能。这两个参数具体如下:

  • active-defrag-cycle-min 25:表示自动清理过程所用 CPU 时间的比例不低于 25%,保证清理能正常开展;
  • active-defrag-cycle-max 75:表示自动清理过程所用 CPU 时间的比例不高于 75%,一旦超过,就停止清理,从而避免在清理时,大量的内存拷贝阻塞 Redis,导致响应延迟升高。

自动内存碎片清理机制在控制碎片清理启停的时机上,既考虑了碎片的空间占比、对 Redis 内存使用效率的影响,还考虑了清理机制本身的 CPU 时间占比、对 Redis 性能的影响。而且,清理机制还提供了 4 个参数,让我们可以根据实际应用中的数据量需求和性能要求灵活使用,建议你在实践中好好地把这个机制用起来。

结尾

这篇文章主要介绍了

  1. 外在原因,内在原因对Redis的影响,外内在原因的调优方案。
  2. Redis的变慢之后的排查思路,变慢的解决方案。
  3. 面试高频:内存占用率怎么那么高的一系列分析

每个知识点都是自己整理浓缩表达出来的,部分有些不容易懂的地方请及时指出,我们一起共同进步!

非常欢迎大家加我个人微信有关后端方面的问题我们在群内一起讨论! 我们下期再见!

长按上方扫码二维码,加我微信,拉你进群

原文始发于微信公众号(欢少的成长之路):3万字聊聊什么是Redis(四)