0%

记一次线上查找和删除 Redis 大 key

昨天凌晨,因为占用容量峰值为 218 G,DBA 将 Redis 的容量由 500G 调低到了 320G。在调低容量后,Redis 开始频繁报 HighMemoryUsage 的警告。而查看 Redis 集群的监控后发现,总容量用掉的比例不足 60%,不应该频繁报警,因此开始了排查之路。

排查原因

首先,我们已经排除了是总容量大小的问题。

由于我们的 Redis 是阿里云的集群版,有多个节点,因此我查看了各个节点的大小。果然,最大的节点已经超过 90%,而小的节点的占用则在 30% 左右。这说明 Redis 发生了非常严重的倾斜。

我们现有的程序已经充分考虑到了 Redis 集群的特性,key 设计策略没有问题,不应该出现如此大的倾斜,所以问题在其他方面。

我们用阿里云的 python 脚本来查询 Redis 大 key,果真发现有一些很老的废弃的大 key,key 为 hash 类型,每个的长度在 500 万以上,最长的接近 1000 万。因此,倾斜的原因是这些大 key 的存在,且这些大 key 在不同节点分布不均匀。

找出大 key

由于阿里云的 python 脚本非常慢,因此我写了一个 golang 的脚本来查找大 key。

因为阿里云集群版 Redis 做了一些改造,因此需要使用 iscan 命令(阿里云Redis命令列表)。

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
for i := 0; i < 32; i++ {
fmt.Printf("node %d start\n", i)
var cursor int
for {
values, err := redis.Values(r.Do("ISCAN", i, cursor, "COUNT", 500, "MATCH", "rc:2019*"))
if err != nil {
fmt.Println(err)
return
}

cursor, err = redis.Int(values[0], nil)
//fmt.Println(cursor)
if err != nil {
fmt.Println(err)
return
}

items, err := redis.Strings(values[1], nil)
if err != nil {
fmt.Println(err)
return
}

for i := range items {
t, err := redis.String(r.Do("TYPE", items[i]))
if err != nil {
fmt.Println(t)
}

var l int

switch t {
case "string":
l, _ = redis.Int(r.Do("STRLEN", items[i]))
case "hash":
l, _ = redis.Int(r.Do("HLEN", items[i]))
case "set":
l, _ = redis.Int(r.Do("SCARD", items[i]))
case "zset":
l, _ = redis.Int(r.Do("ZCARD", items[i]))
case "list":
l, _ = redis.Int(r.Do("LLEN", items[i]))
}
fmt.Printf("key: %s, length: %d\n", items[i], l)
if l > bigKeyLen {
fmt.Printf("find big key: %s, len: %d\n", items[i], l)
} else {
err := r.Send("DEL", items[i])
if err != nil {
fmt.Println(err)
}
}
}
err = r.Flush()
if err != nil {
fmt.Println(err)
}
if cursor == 0 {
fmt.Printf("node %d end\n", i)
break
}
}
}

在这里,如果发现了应该删除的小 key ,我们直接删掉,并将大 key 找了出来。

删除大 key

因为 Redis 的特性,删除大 key 可能会导致线程阻塞,其他请求无法接收造成应用闪崩。因此我需要将大 key 的元素逐步清空,再移除这个 key(在 Redis 4.0 以上版本中,可以直接使用 unlink 命令另外启动一个线程来删除大 key)。

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
for {
values, err := redis.Values(r.Do("HSCAN", bigKey, cursor, "COUNT", 100))
if err != nil {
fmt.Println(err)
return
}

cursor, err = redis.Int64(values[0], nil)
if err != nil {
fmt.Println(err)
return
}

items, err = redis.Int64Map(values[1], nil)
if err != nil {
fmt.Println(err)
return
}

for key := range items {
err := r.Send("HDEL", bigKey, key)
if err != nil {
fmt.Println(err)
}
}

err = r.Flush()
if err != nil {
fmt.Println(err)
}

if cursor == 0 {
break
}
}
_, err = redis.Do("DEL", bigKey)

if err != nil {
fmt.Println(err)
}

这样便完成了大 key 的删除。

总结

在确定 Redis 分布不均匀后,我们要首先检查是否是有大 key,如果有的话要逐步清除,不能影响业务。之后需要选择合适的方案设计 Redis key,使其均匀的分布在各个节点。