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出现阻塞。
案例警示:
- 合理制度规范(风险操作需要审核,多沟通和多确认),能有效的减少故障。
- 避免使用bigkey。
- 控制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。