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

技术 Redis

昨天凌晨,因为占用容量峰值为 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命令列表)。

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)。

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,使其均匀的分布在各个节点。

创建于2019年04月25日 07:55
阅读量 663
留言列表

暂时没有留言

添加留言