Redis删除大key引发的线上事故

Redis大Key删除引发的线上事故

  有一次公司运维在删除redis相关业务key时,由于没有全面评估风险,直接在生产环境执行了del,导致 Redis 进程被阻塞了十几秒,最终导致集群的容量和请求出现“倾斜问题”,进而引发了线上各种依赖该redis集群的业务出现异常。

  线上事故一定要引起反思,现在我们来看看redis大key的相关问题

一、什么是Redis 大key?

1.  所谓的大key问题是某个key的value比较大,所以本质上是大value问题。key往往是程序可以自行设置的,value往往不受程序控制,因此可能导致value很大

  2.  不同数据结构的定义

  redis中有常见的几种数据结构,每种结构对大key的定义不同,比如:

  value 是String类型时,size超过10KB

  value 是ZSET、Hash、List、Set等集合类型时,它的成员数量超过1w个

  上述的定义并不绝对,主要是根据value的成员数量和字节数来确定,业务可以根据自己的场景也确定标准。

二、大key是如何产生的?

大 key 的产生往往是业务方设计不合理,没有预见vaule的动态增长问题:

 1.  一直往value塞数据,没有删除机制,迟早要爆炸

 2.  数据没有合理做分片,将大key变成小key

三、大key会造成什么问题?

1.  客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
 2.  引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
 3.  阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
 4.  内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大。

四、如何找到大key?

1. redis-cli --bigkeys 查找大key

redis 删除sorted set redis 删除大key_redis

使用的时候注意事项:

1)最好选择在从节点上执行该命令。因为主节点上执行时,会阻塞主节点;
2)如果没有从节点,那么可以选择在 Redis 实例业务压力的低峰阶段进行扫描查询,以免影响到实例的正常运行;或者可以使用 -i 参数控制扫描间隔,避免长时间扫描降低 Redis 实例的性能。

该方式的不足之处:

1)这个方法只能返回每种类型中最大的那个 bigkey,无法得到大小排在前 N 位的 bigkey;
2)对于集合类型来说,这个方法只统计集合元素个数的多少,而不是实际占用的内存量。但是,一个集合中的元素个数多,并不一定占用的内存就多。因为,有可能每个元素占用的内存很小,这样的话,即使元素个数有很多,总内存开销也不大;

2. 使用 memory 命令查看 key 的大小

语法:MEMORY USAGE key [SAMPLES count]

可用版本:>=4.0.0

时间复杂度:O(N),N 为 SAMPLES 的个数

返回使用的内存的字节数。

redis 删除sorted set redis 删除大key_数据库_02

3.  使用 RdbTools 工具查找大 key

从 dump.rdb 按照文件统计, 将所有 > 10kb 的 key 输出到一个 csv 文件

rdb dump.rdb -c memory --bytes 10240 -f live_redis.csv

五、Redis的删除原理以及删除命令分析

1.  redis的key删除原理

删除操作的本质是要释放键值对占用的内存空间,不要小瞧内存的释放过程。释放内存只是第一步,为了更加高效地管理内存空间,在应用程序释放内存时,操作系统需要把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配。这个过程本身需要一定时间,而且阻塞当前释放内存的应用程序。

所以,如果一下子释放了大量内存,空闲内存块链表操作时间就会增加,相应地就会造成Redis主线程的阻塞,如果主线程发生了阻塞,其他所有请求可能都会超时,超时越来越多,会造成Redis连接耗尽,产生各种异常。

因此,删除大key这一动作,要非常小心。

2. Del

关于redis的del这一操作的时间复杂度,Redis官方文档是这么描述的:

Time complexity:

O(N) where N is the number of keys that will be removed. When a key to remove holds a value other than a string, the individual complexity for this key is O(M) where M is the number of elements in the list, set, sorted set or hash. Removing a single key that holds a string value is O(1).
翻译-时间复杂度:O(N), N 为被删除的 key 的数量,其中删除单个字符串类型的 key ,时间复杂度为O(1);删除单个列表、集合、有序集合或哈希表类型的 key ,时间复杂度为O(M), M 为以上数据结构内的元素数量。

如果对这类大key直接使用 del 命令进行删除,会导致长时间阻塞,甚至崩溃。因为 del 命令在删除集合类型数据时,时间复杂度为 O(M),M 是集合中元素的个数。

Redis 是单线程的,单个命令执行时间过长就会阻塞其他命令,容易引起雪崩。

3. UNLINK

Redis 4.0 推出了一个重要命令 UNLINK,用来拯救 del 删大key的困境。

语法:UNLINK key_name1 key_name2 ...

可用版本>= 4.0.0

官方文档描述:

Time complexity:
O(1) for each key removed regardless of its size. Then the command does O(N) work in a different thread in order to reclaim memory, where N is the number of allocations the deleted objects where composed of.
1) 在所有命名空间中把 key 删掉,立即返回,不阻塞。

2) 后台线程执行真正的释放空间的操作。

UNLINK 基本可以替代 del,但个别场景还是需要 del 的,例如在空间占用积累速度特别快的时候就不适合使用UNLINK,因为 UNLINK 不是立即释放空间。UNLINK 命令只是将键与键空间断开连接。实际的删除将稍后异步进行

六、如何删除大key

1.  分批次删除

  1)hash key:通过hscan命令,每次获取500个字段,再用hdel命令;

  2)set key:使用sscan命令,每次扫描集合中500个元素,再用srem命令每次删除一个元素;

  3)list key:删除大的List键,未使用scan命令; 通过ltrim命令每次删除少量元素。

  4)sorted set key:删除大的有序集合键,和List类似,使用sortedset自带的zremrangebyrank命令,每次删除top 100个元素。

  下面是list结构的分批次删除的伪代码:
/**
在redis集群中,scan命令需要指定节点。还需要注意,要连接主节点。
**/
$redis = new \Redis();
$timeout = 2.5;
$ip = '127.0.0.1';
$redisConfArr = [
[$ip, 8001],
[$ip, 8002],
[$ip, 8003]
];
foreach ($redisConfArr as $redisConf) {
$redis->pconnect($redisConf[0], $redisConf[1], $timeout);
//默认SCAN_NORETRY情况下有可能会返回空数组,设置成SCAN_RETRY,如果是空数组的话,将不返回继续扫描下去
$redis->setOption(\Redis::OPT_SCAN, \Redis::SCAN_RETRY);
$it = NULL;
while ($arr_keys = $redis->scan($it, CacheKeyConfig::CachePre.'*')) {
if (is_array($arr_keys)) {
//推荐使用unlink函数,非阻塞删除,删除大key时很好用,但是它需要redis版本>=4.0
$result = $redis->del($arr_keys);
echo $result . PHP_EOL;
}
}
echo "OK!" . PHP_EOL;
}
2.  异步删除

 Redis 4.0版本以后可以使用 UNLINK 命令,后台线程执行,释放空间

 3. 不建议采用的方式

 1)执行rename重命名

  官方文档描述:

1 Rename key newkey:
2 Renames key to newkey. It returns an error when key does not exist. If newkey already exists it is overwritten, when this happens RENAMEexecutes an implicit DEL operation, so if the deleted key contains a very big value it may cause high latency even if RENAME itself is usually a constant-time operation
翻译-newkey如果本就存在,redis会用key的值覆盖掉newkey的值,而newkey原本的值会被redis隐式地删除(del)。我们知道大key的删除伴随着高延迟(redis是单进程服务,服务器会在删除大key期间block住接下来其他命令的执行),这就导致时间复杂度本为 O(1) 的rename也有可能卡住redis。

另外需要注意的是:

 在集群模式下,key 和newkey 需要在同一个 hash slot。key 和newkey有相同的 hash tag 才能重命名。

 2)过期key删除策略

 可能大家会有这样的想法:既然在线删除大key会造成阻塞,那么就对这个key设置一个TTL,交给redis自己去删。

 但是,不管是定期删除、惰性删除、淘汰策略这三种方式哪个触发的删除,它都是同步的。所以就算加个TTL,redis也是同步删除的,大key还是会造成阻塞。

参考链接:

 https://www.51cto.com/article/715874.html

 https://www.modb.pro/db/98385