百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

炸了!Redis bigkey导致生产事故(redis常见故障及解决方案)

mhr18 2024-11-17 13:37 16 浏览 0 评论

目录

  • 什么是 bigkey?
  • 危害是什么?
  • 怎么产生的?
  • 如何发现线上是否存在 bigkey?
  • 如何消除 bigkey?
  • 如何优雅删除 bigkey

这篇文章给大家分享一个 Redis 生产事故的复盘,主要分析 Redis 中的 bigkey 相关问题。

什么是 bigkey?

在 Redis 中数据都是 key-value 的形式存储的。bigkey 是指 key 对应的 value 所占的内存空间比较大。

例如一个 String 类型的 value 最大可以存 512MB 的数据,一个 list 类型的 value 最多可以存储 2^32-1 个元素。

如果按照数据结构来细分的话,一般分为字符串类型 bigkey 和非字符串类型 bigkey。也有叫 bigvalue 的,被问到时不要惊讶。

但在实际生产环境中出现下面两种情况,我们就可以认为它是 bigkey:

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

一般来说,string 类型控制在 10KB 以内,hash、list、set、zset 元素个数不要超过 5000。

bigkey 的危害

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

| 内存空间不均匀(平衡)

例如在 Redis Cluster 中,大量 bigkey 落在其中一个 Redis 节点上,会造成该节点的内存空间使用率比其他节点高,造成内存空间使用不均匀。

| 请求倾斜

对于非字符串类型的 bigkey 的请求,由于其元素较多,很可能对于这些元素的请求都落在 Redis cluster 的同一个节点上,造成请求不均匀,压力过大。

| 超时阻塞

由于 Redis 单线程的特性,操作 bigkey 比较耗时,也就意味着阻塞 Redis 可能性增大。这就是造成生产事故的罪魁祸首!导致 Redis 间歇性卡死、影响线上正常下单!

| 网络拥塞

每次获取 bigkey 产生的网络流量较大,假设一个 bigkey 为 1MB,每秒访问量为 1000,那么每秒产生 1000MB 的流量,对于普通的千兆网卡(按照字节算是 128MB/s)的服务器来说简直是灭顶之灾。

而且一般服务器会采用单机多实例的方式来部署,也就是说一个 bigkey 可能会对其他实例造成影响,其后果不堪设想。

| 过期删除

有个 bigkey,它安分守己(只执行简单的命令,例如 hget、lpop、zscore 等),但它设置了过期时间,当它过期后,会被删除,如果没有使用 Redis 4.0 的过期异步删除(lazyfree-lazy-expire yes),就会存在阻塞 Redis 的可能性。

bigkey 的产生

一般来说,bigkey 的产生都是由于程序设计不当,或者对于数据规模预料不清楚造成的。

来看几个例子:

  • 社交类:如果对于某些明星或者大 v 的粉丝列表不精心设计下,必是 bigkey。
  • 统计类:例如按天存储某项功能或者网站的用户集合,除非没几个人用,否则必是 bigkey。
  • 缓存类:将数据从数据库 load 出来序列化放到 Redis 里,这个方式很常用,但有两个地方需要注意,第一:是不是有必要把所有字段都缓存;第二:有没有相互关联的数据,有的同学为了图方便把相关数据都存一个 key 下,产生 bigkey。

如何发现 bigkey

redis-cli --bigkeys 可以命令统计 bigkey 的分布。

但是在生产环境中,开发和运维人员更希望自己可以定义 bigkey 的大小,而且更希望找到真正的 bigkey 都有哪些,这样才可以去定位、解决、优化问题。

判断一个 key 是否为 bigkey,只需要执行 debug object key 查看 serializedlength 属性即可,它表示 key 对应的 value 序列化之后的字节数。

例如我们执行如下操作:

> debug object test:bigkey:hash
Value at:00007FCD1AC28870 refcount:1 encoding:hashtable serializedlength:3122200 lru:5911519 lru_seconds_idle:8788

可以发现 serializedlength=3122200 字节,约为 2.97M,同时可以看到 encoding 是 hashtable,也就是 hash 类型。

那么可以通过 strlen 来看一下字符串的字节数为 2247394 字节,约为 2MB。

再来看一个 string 类型的,执行如下操作:

> debug object test:bigkey:string
Value at:0x7fc06c1b1430 refcount:1 encoding:raw serializedlength:1256350 lru:11686193
lru_seconds_idle:20

可以发现 serializedlength=1256350 字节,约为 1.19M,同时可以看到 encoding 是 raw,也就是字符串类型。

那么可以通过 strlen 来看一下字符串的字节数为 2247394 字节,约为 2MB:

> strlen test:bigkey:string
(integer) 2247394

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

实际生产的操作方式

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

| 被动收集

许多开发人员确实可能对 bigkey 不了解或重视程度不够,但是这种 bigkey 一旦大量访问,很可能就会带来命令慢查询和网卡跑满问题,开发人员通过对异常的分析通常能找到异常原因可能是 bigkey。

这种方式虽然不是被笔者推荐的,但是在实际生产环境中却大量存在,建议修改 Redis 客户端,当抛出异常时打印出所操作的 key,方便排 bigkey 问题。

| 主动检测

scan+debug object:如果怀疑存在 bigkey,可以使用 scan 命令渐进地扫描出所有的 key,分别计算每个 key 的 serializedlength,找到对应 bigkey 进行相应的处理和报警,这种方式是比较推荐的方式。

如何优化 bigkey

由于开发人员对 Redis 的理解程度不同,在实际开发中出现 bigkey 在所难免,重要的是,能通过合理的检测机制及时找到它们,进行处理。

作为开发人员在业务开发时应注意不能将 Redis 简单暴力的使用,应该在数据结构的选择和设计上更加合理,避免出现 bigkey。

| 拆分

基本思路就是,让 key/value 更加小。在设计之初就思考可不可以做一些优化(例如拆分数据结构)尽量让这些 bigkey 消失在业务中。

当出现 bigkey 已经影响到正常使用了,则考虑重新构建自己的业务 key,对 bigkey 进行拆分。

对于 list 类型,可以将一个大的 list 拆成若干个小 list:list1、list2、…listN。对于 hash 类型,可以将数据分段存储,比如一个大的 key,假设存了 1 百万的用户数据,可以拆分成 200 个 key,每个 key 下面存放 5000 个用户数据。

| 局部操作

如果 bigkey 不可避免,也要思考一下要不要每次把所有元素都取出来。

例如,对于 hash 类型有时候仅仅需要 hmget,而不是 hgetall;对于 list 类型可以使用 range 取一个范围内的元素;删除也是一样,尽量使用优雅的方式来处理,而不是暴力的使用 del 删除。(下面会重点讲如何优雅删除 bigkey)

| lazy free

可喜的是,Redis 在 4.0 版本支持 lazy delete free 的模式,删除 bigkey 不会阻塞 Redis。

如何优雅删除 bigkey

因为 redis 是单线程的,删除比较大的 keys 就会阻塞其他的请求。

当发现 Redis 中有 bigkey 并且确认要删除时(业务上需要把 key 删除时),如何优雅地删除 bigkey?其实在 Redis 中,无论是什么数据结构,del 命令都能将其删除。

但是相信通过上面的分析后你一定不会这么做,因为删除 bigkey 通常来说会阻塞 Redis 服务。

下面给出一组测试数据分别对 string、hash、list、set、sorted set 五种数据结构的 bigkey 进行删除,bigkey 的元素个数和每个元素的大小不尽相同。

| 删除时间测试

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

①字符串类删除测试

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

②非字符串类删除测试

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

从上分析可见,除了 string 类型,其他四种数据结构删除的速度有可能很慢,这样增大了阻塞 Redis 的可能性。

| 如何提升删除的效率

既然不能用 del 命令,那有没有比较优雅的方式进行删除呢?Redis 提供了一些和 scan 命令类似的命令:sscan、hscan、zscan。

①string

字符串删除一般不会造成阻塞:


②hash、list、set、sorted set

下面以 hash 为例子,使用 hscan 命令,每次获取部分(例如 100 个)fieldvalue,再利用 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 操作。

| 实战代码

①JedisCluster 示例:


实现:

JedisCluster jedisCluster = redisClusterTemplate.getJedisClusterInstance();
// 游标初始值为0
String cursor = ScanParams.SCAN_POINTER_START;
ScanParams scanParams = new ScanParams();
scanParams.count(scanCount);
while (true) {
 // 每次扫描后获取新的游标
 ScanResult<Map.Entry<String, String>> scanResult = jedisCluster.hscan(key, cursor, scanParams);
 cursor = scanResult.getStringCursor();
 // 获取扫描结果为空
 List<Map.Entry<String, String>> list = scanResult.getResult();
 if (CollectionUtils.isEmpty(list)) {
  break;
 }
 // 构建多个删除的 key
 String[] fields = getFieldsKeyArray(list);
 jedisCluster.hdel(key, fields);
 // 游标为0时停止
 if (ScanParams.SCAN_POINTER_START.equals(cursor)) {
  break;
 }
 // 沉睡等待,避免对 redis 压力太大
 DateUtil.sleepInterval(intervalMills, TimeUnit.MILLISECONDS);
}
// 执行 key 本身的删除
jedisCluster.del(key);

构建的 key:

①redisTemplate 的写法

估计是 redis 进行了一次封装,发现还是存在很多坑。

语法如下:


②注意的坑

实际上这个方法存在很多需要注意的坑:

  • cursor 要关闭,否则会内存泄漏
  • cursor 不要重复关闭,或者会报错
  • cursor 经测试,直接指定的 count 设置后,返回的结果其实是全部,所以需要自己额外处理

参考代码如下:

声明 StringRedisTemplate:

核心代码:

public void removeBigKey(String key, int scanCount, long intervalMills) throws CacheException {
     final ScanOptions scanOptions = ScanOptions.scanOptions().count(scanCount).build();
     //TRW 避免内存泄漏
     try(Cursor<Map.Entry<Object,Object>> cursor =
                    template.opsForHash().scan(key, scanOptions)) {
         if(ObjectUtil.isNotNull(cursor)) {
                // 执行循环删除
                List<String> fieldKeyList = new ArrayList<>();
                while (cursor.hasNext()) {
                    String fieldKey = String.valueOf(cursor.next().getKey());
                    fieldKeyList.add(fieldKey);
                    if(fieldKeyList.size() >= scanCount) {
                        // 批量删除
                        Object[] fields = fieldKeyList.toArray();
                        template.opsForHash().delete(key, fields);
                        logger.info("[Big key] remove key: {}, fields size: {}",
                                key, fields.length);
                        // 清空列表,重置操作
                        fieldKeyList.clear();
                        // 沉睡等待,避免对 redis 压力太大
                        DateUtil.sleepInterval(intervalMills, TimeUnit.MILLISECONDS);
                    }
                }
            }
            // 最后 fieldKeyList 中可能还有剩余,不过一般数量不大,直接删除速度不会很慢
      // 执行 key 本身的删除
      this.opsForValueDelete(key);
     } catch (Exception e) {
      // log.error();
     }
}

这里我们使用 TRW 保证 cursor 被关闭,自己实现 scanCount 一次进行删除,避免一个一个删除网络交互较多。使用睡眠保证对 Redis 压力不要过大。

以上就是本期的全部内容,再回顾一下,本期带大家一起分析了 redis bigkey 的定义、如何产生、危害以及如何发现线上是否存在 bigkey、如何消除 bigkey,最后详细分析了如何优雅删除 bigkey,并给出了删除的解决方案,希望工作中遇到类似问题时能给你提供一个解决思路。

相关推荐

【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...

Pure Storage推出统一数据管理云平台及新闪存阵列

PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...

对Java学习的10条建议(对java课程的建议)

不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...

SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!

官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...

JDK21有没有什么稳定、简单又强势的特性?

佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...

「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了

在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...

Java面试题及答案最全总结(2025版)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...

数据库日常运维工作内容(数据库日常运维 工作内容)

#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...

分布式之系统底层原理(上)(底层分布式技术)

作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...

oracle 死锁了怎么办?kill 进程 直接上干货

1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

取消回复欢迎 发表评论: