Redis处理BigKey

内容纲要

标签:RedisBigKey

什么是BigKey

bigkey是指key对应的value所占的内存空间比较大,例如一个字符串类型的value可以最大存到512MB,一个列表类型的value最多可以存储232-1个元素。如果按照数据结构来细分的话,一般分为字符串类型bigkey和非字符串类型bigkey。

  • 字符串类型:体现在单个value值很大,一般认为超过10KB就是bigkey,但这个值和具体的OPS相关。
  • 非字符串类型:哈希、列表、集合、有序集合,体现在元素个数过多。

OPS 是 Operations Per Second 的缩写,中文翻译为“每秒操作数”,通常用于衡量计算机系统、软件或硬件设备的性能。

在 Redis 中,OPS 指的是 Redis 服务器每秒能够处理的操作数,例如读写请求、键值对的添加、删除等。

如果系统的 OPS 较高,说明系统的性能较好,可以处理更多的请求。
如果 OPS 较低,则说明系统的性能较差,需要进行优化或升级硬件设备。

注意:因为非字符串数据结构中,每个元素实际上也是一个字符串,但这里只讨论元素个数过多的情况。

bigkey无论是空间复杂度和时间复杂度都不太友好,下面我们将介绍它的危害。

BigKey的危害

bigkey的危害体现在三个方面:

  • 内存空间不均匀(平衡):例如在Redis Cluster中,bigkey会造成节点的内存空间使用不均匀。
  • 超时阻塞:由于Redis单线程的特性,操作bigkey比较耗时,也就意味着阻塞Redis可能性增大。
  • 网络拥塞:每次获取bigkey产生的网络流量较大,假设一个bigkey为1MB,每秒访问量为1000,那么每秒产生1000MB的流量,对于普通的千兆网卡(按照字节算是128MB/s)的服务器来说简直是灭顶之灾,而且一般服务器会采用单机多实例的方式来部署,也就是说一个bigkey可能会对其他实例造成影响,其后果不堪设想。图1演示了网络带宽被bigkey占用的瞬间。

图1 - bigkey造成网络拥塞示意图

bigkey的存在并不是完全致命的,如果这个bigkey存在但是几乎不被访问,那么只有内存空间不均匀的问题存在,相对于另外两个问题没有那么重要紧急,但是如果bigkey是一个热点key(频繁访问),那么其带来的危害不可想象,所以在实际开发和运维时一定要密切关注bigkey的存在。

如何发现BigKey

redis-cli --bigkeys 可以命令统计bigkey的分布,但是在生产环境中,开发和运维人员更希望自己可以定义bigkey的大小,而且更希望找到真正的bigkey都有哪些,这样才可以去定位、解决、优化问题。判断一个key是否为bigkey,只需要执行 debug object key 查看 serializedlength 属性即可,它表示key对应的value序列化之后的字节数,例如我们执行如下操作:

127.0.0.1:6379> debug object key
Value at:00007FA5E90003C0 refcount:1 encoding:embstr serializedlength:8 lru:10127498 lru_seconds_idle:24571
  1. Value at:00007FA5E90003C0:这表示该键的值在内存中的地址。

  2. refcount:1:这表示该键的引用计数。在 Redis 中,每当一个键被引用(例如在客户端或者 Redis 内部使用)时,它的引用计数会增加。当键不再被引用时,它的引用计数会减少。当引用计数降至0时,键就会被自动删除。

  3. encoding:embstr:这表示用于存储键值的内部编码类型。在这种情况下,embstr 表示值被存储为一个在一定长度范围内的字符串。

  4. serializedlength:8:这表示序列化后的长度。

  5. lru:10127498:这是一个使用最少的近期未使用(LRU)算法计算的值。它表示该键是多久以前被使用的。

  6. lru_seconds_idle:24571:这表示该键最后一次被访问距离现在的秒数,也就是该键闲置的时间。

注意:debug object 是一个调试命令,它不应在生产环境中使用,因为它使用的CPU资源较多。

127.0.0.1:6379> debug object key

Value at:0x7fc06c1b1430 refcount:1 encoding:raw serializedlength:1256350 lru:11686193

lru_seconds_idle:20
  1. Value at:0x7fc06c1b1430:这是该键的值在内存中的地址。

  2. refcount:1:这是引用计数,表示该键被引用的次数。当该值为0时,键将被自动删除。

  3. encoding:raw:这表示该键值的内部编码类型。在这种情况下,raw 表示键的值是一个字符串。

  4. serializedlength:1256350:这是该键值被序列化后的长度,单位为字节。在这里,键值的长度为 1256350 字节,大约为 1.2 MB。

  5. lru:11686193:这是使用最近最少使用(LRU)算法计算的一个时间戳。它表示该键最后一次被访问的时间。

  6. lru_seconds_idle:20:这表示键最后一次被访问和现在的时间差,也就是该键的闲置时间,单位是秒。

从这段信息中可以看出,这个键的值相当大(约1.2MB),这可能会影响 Redis 的性能。如果这个键经常被访问,那么将这个大的键值从内存中移入和移出可能会消耗大量的CPU资源。如果这个键不常被访问,那么它可能占用了大量的内存资源。如果可能的话,你可能需要考虑将这个大键分割成多个小键,或者尝试使用不同的数据结构来存储你的数据,以更有效地利用内存。

可以发现serializedlength=11686193字节,约为1M,同时可以看到encoding是raw,也就是字符串类型,那么可以通过strlen来看一下字符串的字节数为2247394字节,约为2MB:

127.0.0.1:6379> strlen key

(integer) 2247394

serializedlength不代表真实的字节大小,它返回对象使用RDB编码序列化后的长度,值会偏小,但是对于排查bigkey有一定辅助作用,因为不是每种数据结构都有类似strlen这样的方法。

:star: ⭐在实际生产环境中发现bigkey的两种方式如下:

  • 被动收集:许多开发人员确实可能对bigkey不了解或重视程度不够,但是这种bigkey一旦大量访问,很可能就会带来命令慢查询和网卡跑满问题,开发人员通过对异常的分析通常能找到异常原因可能是bigkey,这种方式虽然不是被笔者推荐的,但是在实际生产环境中却大量存在,建议修改Redis客户端,当抛出异常时打印出所操作的key,方便排查bigkey问题。
  • 主动检测:scan+debug object:如果怀疑存在bigkey,可以使用scan命令渐进的扫描出所有的key,分别计算每个key的serializedlength,找到对应bigkey进行相应的处理和报警,这种方式是比较推荐的方式。

开发提示

  • 如果键值个数比较多,scan+debug object会比较慢,可以利用Pipeline机制完成。
  • 对于元素个数较多的数据结构,debug object执行速度比较慢,存在阻塞Redis的可能。
  • 如果有从节点,可以考虑在从节点上执行。

如何删除BigKey

当发现Redis中有bigkey并且确认要删除时,如何优雅地删除bigkey?无论是什么数据结构,del命令都可以将其删除。但是相信通过上面的分析后你一定不会这么做,因为删除bigkey通常来说会阻塞Redis服务。下面给出一组测试数据分别对string、hash、list、set、sorted set五种数据结构的bigkey进行删除,bigkey的元素个数和每个元素的大小不尽相同。

注意

下面测试和服务器硬件、Redis版本比较相关,可能在不同的服务器上执行速度不太相同,但是能提供一定的参考价值

表1展示了删除512KB~10MB的字符串类型数据所花费的时间,总体来说由于字符串类型结构相对简单,删除速度比较快,但是随着value值的不断增大,删除速度也逐渐变慢。

表1 - 删除字符串类型耗时

表2展示了非字符串类型的数据结构在不同数量级、不同元素大小下对bigkey执行del命令的时间,总体上看元素个数越多、元素越大,删除时间越长,相对于字符串类型,这种删除速度已经足够可以阻塞Redis。

表2 - 删除hash、list、set、sorted set四种数据结构不同数量不同元素大小的耗时

图2折线图,可以更加方便的发现趋势。

图2 - 删除hash、list、set、sorted set四种数据结构不同数量不同元素大小的耗时

从上分析可见,除了string类型,其他四种数据结构删除的速度有可能很慢,这样增大了阻塞Redis的可能性。既然不能用del命令,那有没有比较优雅的方式进行删除呢?这时候就需要将scan命令的若干类似命令拿出来:sscan、hscan、zscan。

1、string

对于string类型使用del命令一般不会产生阻塞:

2、hash、list、set、sorted set

下面以hash为例子,使用hscan命令,每次获取部分(例如100个)field-value,再利用hdel删除每个field(为了快速可以使用Pipeline):

public void delBigHash(String bigKey) {
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    // 游标
    String cursor = "0";
    while (true) {
        ScanResult<Map.Entry<String, String>> scanResult = jedis.hscan(bigKey, cursor,
            new ScanParams().count(100));
        // 每次扫描后获取新的游标
        cursor = scanResult.getStringCursor();
        // 获取扫描结果
        List<Entry<String, String>> list = scanResult.getResult();
        if (list == null || list.size() == 0) {
            continue;
        }
        String[] fields = getFieldsFrom(list);
        // 删除多个field
        jedis.hdel(bigKey, fields);
        // 游标为0时停止
        if (cursor.equals("0")) {
            break;
        }
    }
    // 最终删除key
    jedis.del(bigKey);
}

/**
 * 获取field数组
 * @param list
 * @return
 */
private String[] getFieldsFrom(List<Entry<String, String>> list) {
    List<String> fields = new ArrayList<String>();
    for(Entry<String, String> entry : list) {
        fields.add(entry.getKey());
    }
    return fields.toArray(new String[fields.size()]);
}

开发提示

请勿忘记每次执行到最后执行del key操作。

最佳实践思路

由于开发人员对Redis的理解程度不同,在实际开发中出现bigkey在所难免,重要的是,能通过合理的检测机制及时找到它们,进行处理。作为开发人员在业务开发时应注意不能将Redis简单暴力的使用,应该在数据结构的选择和设计上更加合理,例如出现了bigkey,要思考一下可不可以做一些优化(例如拆分数据结构)尽量让这些bigkey消失在业务中,如果bigkey不可避免,也要思考一下要不要每次把所有元素都取出来(例如有时候仅仅需要hmget,而不是hgetall)。最后,可喜的是,Redis将在4.0版本支持lazy delete free的模式,那时删除bigkey不会阻塞Redis。

总结

文章首先介绍了bigkey的定义,它指的是键值对应的内存空间较大的情况,可分为字符串类型和非字符串类型。根据Redis中的OPS(每秒操作数),一般认为超过10KB的字符串类型可以被视为bigkey。

bigkey对Redis系统带来的危害主要包括:内存空间使用不均匀,超时阻塞和网络拥塞。当bigkey被频繁访问时,可能导致系统性能下降。

要发现bigkey,可以使用redis-cli的--bigkeys命令或者debug object key 命令查看serializedlength属性。此外,开发和运维人员可以通过主动检测或被动收集的方式来发现bigkey。

在删除bigkey时,需要考虑到可能引发的问题。对于字符串类型的bigkey,可以直接使用del命令,但对于非字符串类型的bigkey,建议使用scan命令或其类似的sscan、hscan、zscan等命令进行分段删除以防止Redis阻塞。

在业务开发中,开发人员应合理选择和设计数据结构,尽量避免出现bigkey。如果出现bigkey是无法避免的,也应该考虑优化处理,如拆分数据结构或者只取出部分元素等。文章最后指出,从Redis 4.0版本开始,Redis将支持lazy delete free的模式,可以避免删除bigkey时阻塞Redis。

bigkey的危害不容忽视:数据倾斜、超时阻塞、网络拥塞,可能是Redis生产环境中的一颗定时炸弹,删除bigkey时通常使用渐进式遍历的方式,防止出现Redis阻塞的情况。

Leave a Comment

您的电子邮箱地址不会被公开。 必填项已用*标注

close
arrow_upward