如何优雅的删除一个超过3000多万成员且内存占用超过1.8G的bigkey?

2023-12-22 17:45:08 浏览数 (1)

Redis被广泛的应用,得益于它支持高性能访问(微秒级)。作为一个DBA,经常需要去维护bigkey。如果现在业务方需要你去删除一个hash类型的key,且这个key有3000多万个成员,内存占用超过1.8G。如何优雅的删除这个bigkey呢?下面让我来简单的介绍一下。

bigkey定义: 

key本身的数据量过大:一个string类型的key,它的值为5 MB。

key中的成员数过多:一个zset类型的key,它的成员数量为10万个。

key中成员的数据量过大:一个hash类型的key,它的成员数量虽然只有1000个但这些成员的value(值)总大小为100 MB。     

bigkey危害:

长尾延迟,客户端执行命令的时长变慢。   

对bigkey执行读请求,会使Redis实例的带宽使用率被占满,导致自身服务变慢,同时易波及相关的服务。  

对bigkey执行删除操作,易造成主库较长时间的阻塞,进而可能引发同步中断或主从切换。  

Redis内存达到maxmemory参数定义的上限引发操作阻塞或重要的key被逐出,甚至引发内存溢出(Out Of Memory)。   

集群架构下,容易导致数据分片的内存资源倾斜、CPU使用率倾斜、带宽倾斜。 

案例描述:

生产环境,DBA错误的使用了DEL命令删除一个bigkey,导致Redis出现阻塞。

案例警示:

  1. 合理制度规范(风险操作需要审核,多沟通和多确认),能有效的减少故障。
  2. 避免使用bigkey。
  3. 控制Redis实例容量。

技术回放

代码语言:python代码运行次数:0复制
redis_version:4.0.14,支持unlink,异步操作。
Redis 4.0及之后版本:可以通过UNLINK命令安全地删除大key甚至特大key,
该命令能够以非阻塞的方式,逐步地清理传入的key)

127.0.0.1:6379[3]> info Keyspace
# Keyspace  整个实例只有一个key,在db3  这是测试环境,生产推荐使用db0
db3:keys=1,expires=0,avg_ttl=0

127.0.0.1:6379> select 3   
OK

127.0.0.1:6379[3]> SCAN 0
1) "0"
2) 1) "hash_bigkey_test"

127.0.0.1:6379[3]> type  hash_bigkey_test
hash
 
127.0.0.1:6379[3]> ttl hash_bigkey_test
(integer) -1   表示为设置过期时间

127.0.0.1:6379[3]> HLEN hash_bigkey_test
(integer) 31414065   hash_bigkey_test成员数量为3000多万

127.0.0.1:6379[3]> info memory
# Memory
used_memory:2028479472
used_memory_human:1.89G  hash_bigkey_test 占用内存为1.89G,是非常的大了
used_memory_rss:2076168192
used_memory_rss_human:1.93G   

拓展:我喜欢在slave上做bgsave,然后分析bigkey;
rdb.py可以分析出成员的个数、过期时间、占用内存和类型等

/root/rdbtools-0.1.15/rdbtools/cli/rdb.py -c utf-8  -c memory dump.rdb  > dump1221.log  --查找bigkey的一种方法,优点:从库执行,对线上服务影响小;缺点:时效性差,RDB文件较大时耗时较长。
more  dump1221.log
database,type,key,size_in_bytes,encoding,num_elements,len_largest_element,expiry
3,hash,hash_bigkey_test,2161840836,hashtable,31414065,13,

你猜一下3000多万的key,占用内存为1.89G的bigkey,
通过del删除,需要多少时间呢?
       

执行 time redis-cli -p 6379 -a 密码 -n 3 del hash_bigkey_test 
(integer) 1
real    0m26.527s  ---答案是26秒 (我是用空闲的物理机做测试的,如果规格是:CPU 2C 内存 4GB,阻塞时间会更长))
user    0m0.002s
sys     0m0.000s

127.0.0.1:6379> SLOWLOG get  ---查询慢日志
 1) 1) (integer) 10   ---slow log唯一的id,重启后会被重置
    2) (integer) 1703158202  ---以unix时间戳表示的日志记录时间  2023-12-21 19:30:02
    3) (integer) 26524988   ---命令执行时间,单位微秒,26秒
    4) 1) "del"
       2) "hash_bigkey_test"
    5) "127.0.0.1:18390"
    6) ""

证明Redis确实阻塞了26秒,如下:
编写一个脚本redis_ping.py,不断对Redis发起ping操作,返回的结果输入到redis_ping.log中 
tailf -n 20 redis_ping.log   ---输出响应结果和时间
ping  执行时间 2023-12-21 19:29:35.931984: 返回结果:pong, 耗时: 0:00:00.000152
ping  执行时间 2023-12-21 19:29:35.932159: 返回结果:pong, 耗时: 0:00:00.000164
ping  执行时间 2023-12-21 19:29:35.932321: 返回结果:pong, 耗时: 0:00:00.000151
ping  执行时间 2023-12-21 19:29:35.932483: 返回结果:pong, 耗时: 0:00:00.000151
ping  执行时间 2023-12-21 19:30:02.460248: 返回结果:pong, 耗时: 0:00:26.527754   ---ping命令发送后,pong返回时间隔了26秒,说明Redis阻塞了26秒
ping  执行时间 2023-12-21 19:30:02.463208: 返回结果:pong, 耗时: 0:00:00.002367
ping  执行时间 2023-12-21 19:30:02.463781: 返回结果:pong, 耗时: 0:00:00.000479


优雅的删除hash_bigkey_test这个key:
1.和业务方项目组沟通,确认hash_bigkey_test可以删除。(做好沟通,很重要)
2.将key改名字:RENAME hash_bigkey_test hash_bigkey_test_20231221。
(key改名后,保留一段时间,二次确认无异常访问)
3.业务低峰期,渐进式遍历hash_bigkey_test_20231221,每次取出100个成员,
然后删除,直到全部删除。

nohup  python hdel_big_key.py >> hdel_big_key.log &

more hdel_big_key.log
Key: field1633170, Value: value1633170
Response: 1, Execution Time: 0.000195026397705 seconds
Key: field87462, Value: value87462
Response: 1, Execution Time: 0.000221014022827 seconds
Key: field818659, Value: value818659
Response: 1, Execution Time: 0.000204086303711 seconds

优雅的删除hash_bigkey_test hdel_big_key.py 代码如下:

#!/usr/bin/python
# coding=utf-8
import redis
import sys
import time

db_host = "110.110.110.110"
db_port = 6379
pwd = "密码"
r = redis.StrictRedis(host=db_host, port=db_port, password=pwd, db=3)

cursor = 0

while True:
    # 使用 HSCAN 命令获取 100 个成员及下一个游标位置
    result = r.hscan("hash_bigkey_test_20231221", cursor, count=100)

    # 获取返回结果中的成员和下一个游标位置
    elements = result[1]
    cursor = result[0]

    # 处理获取的成员
    for key, value in elements.items():
        # 输出成员
        print("Key: {}, Value: {}".format(key, value))

        # 测量删除操作的执行时间
        start_time = time.time()
        response = r.hdel("hash_bigkey_test_20231221", key)
        end_time = time.time()

        # 输出删除操作的执行时间
        execution_time = end_time - start_time
        print("Response: {}, Execution Time: {} seconds".format(response, execution_time))

    # 如果游标为 0,表示已经遍历完所有成员,结束循环
    if cursor == 0:
        break


其他类型的key,比如list、set也可以采取渐进式遍历,并小批量的删除bigkey。

0 人点赞