我做了下面的实验来证明KEYS命令有多么危险。
当带有 KEYS 的一个命令运行时,其他 KEYS 命令正在等待运行时间。 KEYS 命令的一次运行有两个阶段,第一个阶段是从 Redis 获取信息,第二个阶段是将其发送到客户端。
$ time src/redis-cli keys "*" | wc -l
1450832
real 0m17.943s
user 0m8.341s
$ src/redis-cli
127.0.0.1:6379> slowlog get
1) 1) (integer) 0
2) (integer) 1621437661
3) (integer) 8321405
4) 1) "keys"
2) "*"
因此,它在 Redis 上运行了 8 秒,然后通过管道传输到“wc”命令。 Redis 在 8 秒内完成了该命令,但“wc”命令需要 17 秒的数据才能完成计算。因此内存缓冲区必须存在至少 17 秒。现在,让我们想象一下网络上的客户端,这些数据也必须发送到客户端。如果我们有 10 个键命令,这些命令将在 Redis 上逐个运行,当第一个命令完成并且下一个命令运行时,第一个命令的结果必须先存储在内存中,然后客户端才会使用它们。这一切都需要内存,所以我可以想象一种情况,第五个客户端正在运行 KEYS 命令,但我们仍然需要保留第一个客户端的数据,因为它们仍然没有通过网络传输。
我们来测试一下。
场景:我们有一个 200M 大小(1000M 物理内存)的 Redis DB,检查一次执行 KEYS 需要多少内存,以及通过网络执行时需要多长时间。然后模拟5个相同的KEYS命令运行,看看是否会杀死Redis。
$ src/redis-cli info memory
used_memory_human:214.17M
total_system_memory_human:926.08M
When run from the same node:
$ time src/redis-cli keys "*" | wc -l
1450832
real 0m17.702s
user 0m8.278s
$ free -m
total used free shared buff/cache available
Mem: 926 301 236 24 388 542
Mem: 926 336 200 24 388 507
Mem: 926 368 168 24 388 475
Mem: 926 445 91 24 388 398
Mem: 926 480 52 24 393 363
Mem: 926 491 35 24 399 352
-> looks like it consumed 190M for the KEYS command
-> 所以,Redis 忙于该命令 8s,但该命令消耗内存 17s。
-> 仅运行一个 KEYS 命令只会阻塞 Redis 8 秒,但不会导致 OOM
让我们(几乎)同时运行 2 个 KEYS 命令(无论如何都会一个接一个地运行)
$ time src/redis-cli keys "*" | wc -l &
$ time src/redis-cli keys "*" | wc -l &
$ free -m
total used free shared buff/cache available
Mem: 926 300 430 24 194 546
Mem: 926 370 361 24 194 477
Mem: 926 454 276 24 194 393
Mem: 926 589 141 24 194 258
Mem: 926 693 37 24 194 154
-> now we used 392M memory for 26s, while Redis is hung for 17s
-> but we still have a running Redis
让我们(几乎)同时运行 3 个 KEYS 命令(无论如何都会一个接一个地运行)
$ time src/redis-cli keys "*" | wc -l &
$ time src/redis-cli keys "*" | wc -l &
$ time src/redis-cli keys "*" | wc -l &
$ free -m
total used free shared buff/cache available
Mem: 926 299 474 23 152 549
Mem: 926 385 388 23 152 463
Mem: 926 512 261 23 152 336
Mem: 926 573 200 23 152 275
Mem: 926 711 61 23 152 136
Mem: 926 842 21 21 62 17
-> now we used 532M memory for 36s, while Redis is hung for 26s
-> but we still have a running Redis
Let's run 4 KEYS commands at the (almost) same time (that will run one after another anyway)
$ time src/redis-cli keys "*" | wc -l &
$ time src/redis-cli keys "*" | wc -l &
$ time src/redis-cli keys "*" | wc -l &
$ time src/redis-cli keys "*" | wc -l &
-> that kills Redis
Redis 日志中没有任何内容:
2251:C 19 May 16:03:05.355 * DB saved on disk
2251:C 19 May 16:03:05.379 * RDB: 2 MB of memory used by copy-on-write
1853:M 19 May 16:03:05.432 * Background saving terminated with success
在 /var/log/messages 中
May 19 16:08:01 consumer2 kernel: [454881.744017] redis-cli invoked oom-killer: gfp_mask=0x6200ca(GFP_HIGHUSER_MOVABLE), nodemask=(null), order=0, oom_score_adj=0
May 19 16:08:01 consumer2 kernel: [454881.744180] [<8023bdb8>] (oom_kill_process) from [<8023c6e8>] (out_of_memory+0x134/0x36c)
结论:
- 我们可以杀死健康的 Redis 实例,消耗 200M RAM,其中操作系统上有 70% 的 RAM 可用,只需运行 4 个 KEYS 命令,一个接一个地发出并一个接一个地运行。只是因为即使 Redis 执行完毕,结果也必须被缓冲。
- 无法使用 maxmemory 保护 Redis 免受这种行为的影响,因为内存使用不是 SET 命令的结果