Redis Bigkey 问题的讨论和解决方案
概述
本文将分为四个部分讨论 bigkey,首先介绍什么是 bigkey,其次介绍 bigkey 引发的问题,之后我们需要定位 bigkey,最后讨论如何解决 bigkey 问题。
什么是 Bigkey
key 存储的内容过大或者存储的元素过多(例如:list/hash/set ...),这样的 key 便是 bigkey。阿里巴巴给了一个参考标准:
- STRING key 存储的内容大于 5 MB;
- LIST key 存储的元素个数大于 20,000;
- ZSET key 存储的元素个数大于 10,000;
- HASH key 占用的空间大于 100MB 即便只包含了 1,000 元素。
注:这里所有的都是参考值,不要较真,只需要记住一点:所谓 bigkey 就是 key 下面存储的内容过大或者 key 里面存储的元素个数过多。
Bigkey 带来的问题
为什么我们要关心 bigkey,自然是因为 bigkey 会给 redis 带来一些负面影响,以下是一些可能产生的危害:
- Redis 阻塞:bigkey 的某些操作可能耗时比较久,阻塞其他命令的执行;
- Key 过期删除或者主动删除可能阻塞 Redis:如果 bigkey 设置了过期时间,key 过期删除时可能会阻塞 Redis(Redis 4.0 引入了惰性删除,即 lazy-free,只要开启相关配置,便不用担心该问题)。
- 数据倾斜:在集群模式中,由于 bigkey 的存在,会造成不同节点上的数据分布不均匀。
- bigkey 可能引发网络拥堵: 获取 bigkey 产生的网络流量较大,假设某热点 bigkey 大小为 5MB,QPS 为 100,那么每秒产生 500MB 的网络流量,千兆网卡每秒只能处理 128MB 流量(1000Mb = 1000/8 = 128MB/s),此时网络就发生拥堵了。
注:千兆网卡,是 1000Mb,注意是小 b,换算成字节才 128MB。
如何查找 Bigkey
利用 redis-cli 配合 --bigkeys
选项可以扫描 bigkey,方法如下:
Redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 1
对线上实例进行 bigkey 扫描时,Redis 的 OPS 会突增,为了降低扫描过程中对 Redis 的影响,最好控制一下扫描的频率,指定
-i
参数即可,它表示扫描过程中每次扫描后休息的时间间隔,单位是秒;
扫描结果中,对于容器类型(List、Hash、Set、ZSet)的 key,只能扫描出元素最多的 key。但一个 key 的元素多,不一定表示占用内存也多,你还需要根据业务情况,进一步评估内存占用情况。
注:根据定义,如果元素多,即便占用的空间不大,也是 bigkey。一般情况下,元素多,占用空间也小不了。删除这类型的 bigkey,使用 del
命令需要注意,有可能阻塞 redis。以下是官方对 DEL key [key ...]
命令的时间复杂度说明:
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).
可以看出,对于 list/set/sorted set/hash 这类 key,时间复杂度和元素个数直接相关。
注:除了这种方式,还可以通过 rdb 文件查找 bigkey,这种方式的好处是不会对线上 redis 造成影响。此外也可以借助一些第三方工具定位 bigkey。
Bigkey 的解决方案
bigkey 有时候不可避免,我们可以将 bigkey 拆分成多个 key。可以采取“取模法“,假设有一个 hash 的 key 为 someKey,我们预期 someKey 可能会成为 bigkey,根据业务数据量评估,决定将 someKey 拆分为 10 个,即 someKey0、somekey1 ... someKey9。当我们向 hash 里面放一个字段的时候,可以计算字段的哈希值并模 10,由此得到目标 key。目标 key 计算公式如下:
newHashKey = hashKey + hash(field) mod split_num
上面是防患于未然,但有时候 bigkey 已经成为既定事实,这个时候,我们该如何正确的操作 bigkey 才不会阻塞 redis 呢?
利用 scan 系列命令批量操作
scan 系列命令包括 scan、hscan、sscan 和 zscan。例如下面的 ruby 代码使用 hscan 扫描一个大 hash,每次扫描 100 个字段,然后将这 100 个字段从 hash 中移除。
cursor = "0"
loop do
cursor, fields = $redis.hscan("someKey", cursor, count: 100)
hkeys = fields.map { |pair| pair[0] }
$redis.hdel("someKey", hkeys) if hkeys.size > 0
break if cursor == "0"
end
使用 unlink
前面提到 del 命令操作 bigkey,可能会阻塞 redis,针对这种情况,redis 提供了 unlink 命令解决该问题。unlink 命令会在其他线程中回收内存,因此不会阻塞主线程。
开启 Lazy Free
Lazy free 即惰性删除,以下是一些有关惰性删除的一些配置选项。
# 在内存到达 maxmemory 配置的值,是否使用惰性删除,默认值 no
# 建议关闭,因为内存都达到 maxmemory的值了,应该及时驱逐一些 key 来满足新的数据写入请求
lazyfree-lazy-eviction no
# Key 过期了是否开启惰性删除
# 建议开启
lazyfree-lazy-expire no
# 隐式删除服务器数据时,如 RENAME 操作
# 建议开启
lazyfree-lazy-server-del no
# 在对从库进行全量数据同步时
# 建议关闭
slave-lazy-flush no
# 开启了这个选项后,del 命令的行为就和 unlink 一样。
lazyfree-lazy-user-del no
replica-lazy-flush no
参考
温馨提示:反馈需要登录