大key和高流量导致redis挂掉的故障

故障回顾

问题redis的配置如下:

(1)因为大KEY调用量,随着白天自然流量趋势增长而增长,最终在业务高峰最高点期占满带宽使用100%

(2)从而引发redis的内存使用率,在5min之内从0%->100%

(3)最终全面GET SET timeout崩溃(11点22分02秒)

(4)最终导致页面返回timeout,故障发生。

排查思路

内存使用率100% 就等同于redis不可用吗?
解答:正常使用情况下,不是。因为redis有【缓存淘汰机制】。Redis 在内存使用率达到 100% 时不会直接崩溃。相反,它依赖内存淘汰策略来释放内存,确保系统的稳定性。

这个配置在哪里呢?如图:

大部分开发都是不会主动去调整这里的参数的。

你根据实际需求配置适当的内存淘汰策略,以便在内存达到上限时,系统能够稳定地处理新请求,而不会出现写操作失败的情况(只要不是noeviction)。也就是说,照理SET and GET都应该没啥问题才对(先不考虑其他复杂命令)。

尽管 Redis 本身不会轻易崩溃,但如果内存耗尽且没有淘汰策略或者淘汰策略未能生效,Redis 可能拒绝新的写操作,并返回错误:OOM command not allowed when used memory > 'maxmemory'。如果系统的配置或者操作系统的内存管理不当,可能会导致 Redis 进程被操作系统杀死。

但是事故现象就是:内存使用率100% 时,redis不可用,怎么解释?

【猜测1】会是淘汰不及时导致的性能瓶颈吗?也就是说 写入的速度>>淘汰的速度。

解答:如果是正常的业务写入,不可能!因为redis纯内存,淘汰速度是非常快的。而且这个业务特性,也并非高频写入。这个redis实例其实里面存储的KEY很少,查了一下,最终占了整个实例的内存使用率<5%。如图:

不太符合正常使用下KEY不断增多,最终挤爆内存使用率的问题。

因此,初步结论:Redis 的崩溃一般不会是由于单纯写入速度超过淘汰速度引起的,尤其是使用了合理的内存淘汰策略时;如果写入速度非常高,而淘汰策略无法及时清除旧数据,Redis 可能会非常频繁地进行键的查找和淘汰操作,从而导致性能下降。

那么问题来了,到底是什么导致了内存使用率激增那??因此查阅了资料,发现最为贴近的答案:

证据支撑

果然是这样,说明内存是被【缓冲区】挤爆的。为了验证,使用info memory进行分析(我随便模拟了一个缓冲区溢出的case,并非事故现场):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# Memory
used_memory:1072693248
used_memory_human:1023.99M
used_memory_rss:1090519040
used_memory_rss_human:1.02G
used_memory_peak:1072693248
used_memory_peak_human:1023.99M
used_memory_peak_perc:100.00%
used_memory_overhead:1048576000
used_memory_startup:1024000
used_memory_dataset:23929848
used_memory_dataset_perc:2.23%
allocator_allocated:1072693248
allocator_active:1090519040
allocator_resident:1090519040
total_system_memory:16777216000
total_system_memory_human:16.00G
used_memory_lua:37888
used_memory_lua_human:37.89K
used_memory_scripts:1024000
used_memory_scripts_human:1.00M
maxmemory:1073741824
maxmemory_human:1.00G
maxmemory_policy:noeviction
allocator_frag_ratio:1.02
allocator_frag_bytes:17825792
allocator_rss_ratio:1.00
allocator_rss_bytes:0
rss_overhead_ratio:1.00
rss_overhead_bytes:0
mem_fragmentation_ratio:1.02
mem_fragmentation_bytes:17825792
mem_not_counted_for_evict:0
mem_replication_backlog:0
mem_clients_slaves:0
mem_clients_normal:1048576000
mem_aof_buffer:0
mem_allocator:jemalloc-5.1.0
active_defrag_running:0
lazyfree_pending_objects:0

从上面的INFO memory输出中,我们可以看到一些关键信息,这些信息表明大部分内存被缓冲区占用殆尽:
1.内存使用情况:
used_memory: 1072693248 (1.02 GB)
maxmemory: 1073741824 (1.00 GB)
上面的输出表明,当前内存使用几乎达到了配置的最大内存限制,内存已接近耗尽。
2.缓冲区占用:
used_memory_overhead: 1048576000 (1.00 GB)
这个值表示 Redis 开销的内存,包括缓冲区、连接和其他元数据。在这种情况下,大部分 used_memory (1.02 GB) 被 used_memory_overhead (1.00 GB) 占用,这意味着大部分内存都被缓冲区等开销占据。
3.数据集占用:
used_memory_dataset: 23929848 (23.93 MB)
used_memory_dataset_perc: 2.23%
这里显示,实际存储的数据只占了非常少的一部分内存(约 23.93 MB),而绝大部分内存被缓冲区占据。
4.客户端缓冲区:
mem_clients_normal: 1048576000 (1.00 GB)
这表明普通客户端连接占用了约 1.00 GB 内存,这通常意味着输出缓冲区可能已经接近或达到了设定的限制。
5.内存碎片:
allocator_frag_ratio: 1.02
mem_fragmentation_ratio: 1.02
碎片率不高,表明内存被合理使用但被缓冲区占用过多。

总结:
从上面的例子可以看出,Redis 的内存几乎被缓冲区占用殆尽。以下是具体的结论:

  1. 当前内存使用 (used_memory) 已经接近最大内存限制 (maxmemory),即 1.02 GB 接近 1.00 GB 的限制。
  2. 内存开销 (used_memory_overhead) 很大,主要被客户端普通连接使用(可能是输出缓冲区),而实际的数据仅占用了很少的内存。
  3. 分配器和 RSS 碎片率 (allocator_frag_ratio 和 mem_fragmentation_ratio) 较低,表明碎片不是问题。

为啥要有这个缓存区,先看这个原理图:

缓冲区的功能其实很简单,主要就是用一块内存空间来暂时存放命令数据,以免出现因为数据和命令的处理速度慢于发送速度而导致的数据丢失和性能问题。

那么缓冲区的大小可以通过参数调整么?答案是没有。因为这个是在代码里写死的,1GB。也就是说redis客户端最多可以暂存1GB的数据,在生产环境里1GB其实挺合适了,对绝大多数的请求够用了,而且如果再大的话,就会对应用所在的容器内存占用过多,导致应用无法正常使用而崩溃。

最后的结论就是:

  1. 对象存储的部分因为是有过期时间的,过期了自然被清理了;
  2. 【缓冲内存】↑ (涌入);
  3. 【对象内存】↓ (定时清理);
  4. 并受MAX内存掣肘(上限);
    最终的结局:Redis 的内存完全被缓冲区占据。

自然,每当有SET请求进来的时候,SET却不进来——因为「内存淘汰策略」(maxmemory-policy) 淘汰的是【对象内存】,对缓存区压根起不到作用!!!

结论:
Redis 的内存完全被缓冲区占据,实际上 Redis 将无法正常工作,包括数据存储(SET 操作)和数据读取(GET 操作)。

还原真相

  1. 当时自然增长导致流出带宽不断变大直至96MB/s。

  2. 流出带宽超过96MB/s,输出缓冲区内存占用激增甚至溢出 (300setMaxTotal*10机器ip数量个客户端,之前推导过可以到9G)。

  3. 导致输出缓冲区爆了,redis客户端连接不得不关闭。

  4. 客户端连接关闭后,导致请求都走DB。

  5. DB走完之后都会执行SET。

  6. SET流量飙升,且因都是大KEY,导致流入带宽激增(别看写QPS只有50,但是如果每个写都是2MB,就可以做到瞬间占满流入带宽)。

  7. Redis主线程模型,处理请求的速度过慢(大KEY),出现了间歇性阻塞,无法及时处理正常发送的请求,导致客户端发送的请求在输入缓冲区越积越多。

  8. 输入缓冲区内存随即激增。

  9. 最终,redis内存被缓冲区内存(输入、输出)完全侵占。

  10. 后续的SET GET命令甚至都进不了输入缓冲区,阻塞持续到客户端配置的SoTimeout时间。

  11. 造成最终的不可用(后续的命令想进场,要依赖当前输入缓冲区里的命令被执行给你腾出来位置,但是还是那句话Redis主线程处理消化的速度,实在是太慢了,此时可以结合监控看到Redis的QPS骤降。

归根揭底,造成问题原因是因为大key并且是热点数据,最终导致输出缓冲区到达限制,进而引发雪崩。

大KEY的问题

首先,大KEY其实并不是长度过长的KEY,而是存放了慢查询命令的KEY。对于String类型,慢查询的本质在于value的大小。对于其他类型,慢查询的本质在于集合的大小(时间复杂度带来)。

如何解决大key,可以看阿里云的这篇文章:https://help.aliyun.com/zh/redis/user-guide/identify-and-handle-large-keys-and-hotkeys/?spm=a2c4g.11186623.0.i1 ,除了这个文章之外,可以再加一个本地缓存来解决大key风险。

感谢您请我喝咖啡~O(∩_∩)O,如果要联系请直接发我邮箱chenx1242@163.com,我会回复你的
-------------本文结束感谢您的阅读-------------