如何找到Redis大Key?

2023-11-08 15:17:23 浏览数 (1)

作者简介

马听,多年 DBA 实战经验,对MySQL、 Redis、MongoDB、Go等有一定了解,书籍《MySQL DBA精英实战课》作者,慕课网DBA体系课(https://class.imooc.com/sale/dba)讲师。

有时候,可能因为业务设计不合理,或者其他的一些原因,导致Redis某个key非常大。

而在查询这类大key的时候,往往响应比较慢,并且也会影响其他正常查询。

所以,我们应该关注Redis里面的key详情,及时发现大key并治理,提前预防大key产生的影响。

1 什么是 Bigkey

下面这两种情况,在很多互联网公司都被认为是 Bigkey:

字符串类型超过10KB

当然,这也不是硬性规定,主要看我们的业务场景

非字符串类型元素个数超过5000个

比如哈希、列表、集合、有序集合,元素个数超过 5000 的,就可以认为是Bigkey。

2 Bigkey的危害

Bigkey 存在很多危害,具体体现在以这些方面:

内存空间不均匀

比如在 Redis cluster 或者其他集群架构中,会造成节点的内存使用不均匀。

查询时阻塞

因为 Redis 单线程特性,如果操作某个 Bigkey 耗时比较久,则后面的请求会被阻塞。

过期时阻塞

如果 Bigkey 设置了过期时间,当过期后,这个 key 会被删除,假如没有使用过期异步删除,就会存在阻塞 Redis 的可能性,并且慢查询中查不到(因为这个删除是内部循环事件)。

3 用ChatGPT编写制造Bigkey的程序

构造一些大key,方便我们等下测试。

发送给ChatGPT,提示语为:

帮忙写一个Go程序 往Redis里面写入一些测试的key string类型大小超过10kbkey hash、list、sets、zsets等key,元素超过5000 key的名字都是以large开头的 Redis的IP和端口192.168.12.161:7001,密码:xxx

生成的程序如下:

编辑一个文件,writeBigkey.go

代码语言:javascript复制
package main

import (
   "context"
   "fmt"
   "log"
   "math/rand"
   "strconv"
   "time"

   "github.com/go-redis/redis/v8"
)

func main() {
   // 创建Redis客户端连接
   client := redis.NewClient(&redis.Options{
      Addr:     "192.168.12.161:7001", // Redis服务器地址
      Password: "xxx",               // 如果有密码,请填写密码
      DB:       0,                // 默认数据库
   })

   // 使用完后关闭连接
   defer client.Close()

   // 写入字符串类型的键,大小大于10KB
   largeStringValue := generateLargeString(10240) // 生成大于10KB的字符串
   err := client.Set(context.Background(), "large_string_key", largeStringValue, 0).Err()
   if err != nil {
      log.Fatalf("Error writing large string key: %v", err)
   }

   // 写入哈希类型的键,元素个数大于5000
   hashData := make(map[string]interface{})
   for i := 0; i < 6000; i   {
      field := fmt.Sprintf("field%d", i)
      value := fmt.Sprintf("value%d", i)
      hashData[field] = value
   }
   err = client.HMSet(context.Background(), "large_hash_key", hashData).Err()
   if err != nil {
      log.Fatalf("Error writing large hash key: %v", err)
   }

   // 写入列表类型的键,元素个数大于5000
   listData := make([]interface{}, 6000)
   for i := 0; i < 6000; i   {
      listData[i] = strconv.Itoa(i)
   }
   err = client.RPush(context.Background(), "large_list_key", listData...).Err()
   if err != nil {
      log.Fatalf("Error writing large list key: %v", err)
   }

   fmt.Println("Keys have been written successfully!")
}

// 生成指定大小的随机字符串
func generateLargeString(size int) string {
   rand.Seed(time.Now().UnixNano())
   const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
   result := make([]byte, size)
   for i := range result {
      result[i] = charset[rand.Intn(len(charset))]
   }
   return string(result)
}

运行程序,就可以生成一些大key。

4 找到大key

我们来讲一下几种常见的找打大key的方法

4.1 Redis客户端自带的参数查找Bigkey

执行

代码语言:javascript复制
redis-cli -p 7001 -a xxx --bigkeys

大致结果和注释如下:

代码语言:javascript复制
[00.00%] Biggest string found so far '"large_string_key"' with 10240 bytes
[00.00%] Biggest set    found so far '"set_test"' with 1 members
[00.00%] Biggest hash   found so far '"user:01"' with 1 fields
[00.00%] Biggest list   found so far '"large_list_key"' with 6000 items
[00.00%] Biggest hash   found so far '"large_hash_key"' with 6000 fields

-------- summary -------

Sampled 8 keys in the keyspace!
# 一共扫描了 8 个key
Total key length in bytes is 82 (avg len 10.25)
# 所有key的总长度是 82 字节,平均长度是 10.25 字节

Biggest   list found '"large_list_key"' has 6000 items
Biggest   hash found '"large_hash_key"' has 6000 fields
Biggest string found '"large_string_key"' has 10240 bytes
Biggest    set found '"set_test"' has 1 members

 # 每一种key情况的总览,某如某个类型,扫描出多少个key,占比多少,平均大小。
2 lists with 6001 items (25.00% of keys, avg size 3000.50)
2 hashs with 6001 fields (25.00% of keys, avg size 3000.50)
3 strings with 10246 bytes (37.50% of keys, avg size 3415.33)
0 streams with 0 entries (00.00% of keys, avg size 0.00)
1 sets with 1 members (12.50% of keys, avg size 1.00)
0 zsets with 0 members (00.00% of keys, avg size 0.00)

4.2 通过ChatGPT编写代码查找Bigkey

用ChatGPT编写查询Bigkey的程序:

帮忙写一个Go程序 扫描出Redis里面string类型大小超过10kb的key hash、list、sets、zsets等key,扫描出元素超过5000的key Redis的IP和端口192.168.12.161:7001,密码:xxx

生成的代码如下:

代码语言:javascript复制
package main

import (
   "context"
   "fmt"
   "github.com/go-redis/redis/v8"
   "log"
)

func main() {
   // 创建Redis客户端连接
   client := redis.NewClient(&redis.Options{
      Addr:     "192.168.12.161:7001", // Redis服务器地址
      Password: "xxx",               // 如果有密码,请填写密码
      DB:       0,                // 默认数据库
   })

   // 使用完后关闭连接
   defer client.Close()

   // 获取所有键
   keys, err := client.Keys(context.Background(), "*").Result()
   if err != nil {
      log.Fatalf("Error getting keys: %v", err)
   }

   // 遍历所有键
   for _, key := range keys {
      keyType, err := client.Type(context.Background(), key).Result()
      if err != nil {
         log.Printf("Error getting key type for key '%s': %v", key, err)
         continue
      }

      switch keyType {
      case "string":
         // 检查字符串类型键的大小
         valueSize, err := client.StrLen(context.Background(), key).Result()
         if err != nil {
            log.Printf("Error getting size for string key '%s': %v", key, err)
            continue
         }
         //fmt.Printf("String key '%s' size: %d bytesn", key, valueSize)

         if valueSize >= 10240 { // 大于10KB
            fmt.Printf("String key '%s' size: %d bytesn", key, valueSize)
         }

      case "hash":
         // 检查哈希类型键的元素数量
         hashSize, err := client.HLen(context.Background(), key).Result()
         if err != nil {
            log.Printf("Error getting size for hash key '%s': %v", key, err)
            continue
         }
         if hashSize > 5000 {
            fmt.Printf("Hash key '%s' has %d elementsn", key, hashSize)
         }

      case "list":
         // 检查列表类型键的元素数量
         listSize, err := client.LLen(context.Background(), key).Result()
         if err != nil {
            log.Printf("Error getting size for list key '%s': %v", key, err)
            continue
         }
         if listSize > 5000 {
            fmt.Printf("List key '%s' has %d elementsn", key, listSize)
         }

      case "set":
         // 检查集合类型键的元素数量
         setSize, err := client.SCard(context.Background(), key).Result()
         if err != nil {
            log.Printf("Error getting size for set key '%s': %v", key, err)
            continue
         }
         if setSize > 5000 {
            fmt.Printf("Set key '%s' has %d elementsn", key, setSize)
         }

      case "zset":
         // 检查有序集合类型键的元素数量
         zsetSize, err := client.ZCard(context.Background(), key).Result()
         if err != nil {
            log.Printf("Error getting size for zset key '%s': %v", key, err)
            continue
         }
         if zsetSize > 5000 {
            fmt.Printf("Zset key '%s' has %d elementsn", key, zsetSize)
         }
      }
   }
}

4.3 通过rdbtools来找到Bigkey

大概过程就是:

通过 rdbtools 分析 rdb 生成 csv 文件,再导入 MySQL 或其他数据库中进行分析统计,根据 size_in_bytes 统计 Bigkey。

获取Redis的RDB文件

在Redis中执行

代码语言:javascript复制
bgsave

创建一个rdb的分析目录

代码语言:javascript复制
mkdir /data/analyserdb

把rdb复制一份到分析目录

代码语言:javascript复制
cp /data/redis7001/data/dump.rdb /data/analyserdb/192.168.12.161_dump.rdb

通过rdbtools分析RDB中的key

安装Python3

代码语言:javascript复制
yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel libffi-devel

cd /usr/src
wget https://www.python.org/ftp/python/3.7.3/Python-3.7.3.tgz
tar -xvf Python-3.7.3.tgz
cd Python-3.7.3/
./configure
make && make install

安装 redis-rdb-tools,参考 GitHub(https://github.com/sripathikrishnan/redis-rdb-tools)。

代码语言:javascript复制
yum install python3-devel
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python3 get-pip.py
pip3 install python-lzf
pip3 install rdbtools

rdb --help

我们可以通过官网的Python示例获取到大key,比如

https://github.com/sripathikrishnan/redis-rdb-tools

编辑文件analyse_rdb.py

代码语言:javascript复制
from rdbtools import RdbParser, RdbCallback
from rdbtools.encodehelpers import bytes_to_unicode

class MyCallback(RdbCallback):
    ''' Simple example to show how callback works.
        See RdbCallback for all available callback methods.
        See JsonCallback for a concrete example
    '''

    def __init__(self):
        super(MyCallback, self).__init__(string_escape=None)

    def encode_key(self, key):
        return bytes_to_unicode(key, self._escape, skip_printable=True)

    def encode_value(self, val):
        return bytes_to_unicode(val, self._escape)

    def set(self, key, value, expiry, info):
        print('%s = %s' % (self.encode_key(key), self.encode_value(value)))

    def hset(self, key, field, value):
        print('%s.%s = %s' % (self.encode_key(key), self.encode_key(field), self.encode_value(value)))

    def sadd(self, key, member):
        print('%s has {%s}' % (self.encode_key(key), self.encode_value(member)))

    def rpush(self, key, value):
        print('%s has [%s]' % (self.encode_key(key), self.encode_value(value)))

    def zadd(self, key, score, member):
        print('%s has {%s : %s}' % (str(key), str(member), str(score)))


callback = MyCallback()
parser = RdbParser(callback)
parser.parse('./dump.rdb')

我们再来运行Python代码

代码语言:javascript复制
python3 analyse_rdb.py >result.txt

也可以使用命令进行 RDB 分析

代码语言:javascript复制
cd /data/analyserdb

rdb -c memory 192.168.12.161_dump.rdb >192.168.12.161_rdb_result.csv

各个字段的解释如下

  • database(库编号)
  • type(键类型)
  • key(键名字)
  • size_in_bytes(键大小)
  • encoding(键编码)
  • num_elements(元素个数)
  • len_largest_element(最大元素大小)
  • expiry(过期时间)

把分析结果写入MySQL

登录MySQL

创建一个用户

代码语言:javascript复制
create database rdb;
create user 'u_rdb'@'%' IDENTIFIED WITH mysql_native_password BY '1g18_pDgnd12' ; 
GRANT update,delete,insert,select ON rdb.* TO 'u_rdb'@'%' ;

创建MySQL表

代码语言:javascript复制
USE rdb;
CREATE TABLE rdb_result (
  id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Primary Key',
  ip_add VARCHAR(20) NOT NULL DEFAULT '' COMMENT 'IP Address',
  key_database INT NOT NULL DEFAULT '0' COMMENT 'Database Number',
  key_type VARCHAR(10) NOT NULL DEFAULT '' COMMENT 'Key Type',
  key_name VARCHAR(50) NOT NULL DEFAULT '' COMMENT 'Key Name',
  key_size INT NOT NULL DEFAULT '0' COMMENT 'Key Size',
  key_encoding VARCHAR(10) NOT NULL DEFAULT '' COMMENT 'Key Encoding',
  num_elements INT NOT NULL DEFAULT '0' COMMENT 'Number of Elements',
  len_largest_element INT NOT NULL DEFAULT '0' COMMENT 'Largest Element Size',
  key_expiry VARCHAR(20) NOT NULL DEFAULT '' COMMENT 'Key Expiry Time',
  create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Record Creation Time',
  update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Record Update Time',
  status TINYINT NOT NULL DEFAULT '1' COMMENT '1 represents a valid record, 0 represents an invalid record',
  PRIMARY KEY (id),
  KEY idx_stu_score (key_size)
) ENGINE = INNODB CHARSET = utf8mb4 COMMENT 'Redis RDB Analysis Result Table';

修改MySQL配置文件

代码语言:javascript复制
local-infile=on

这个参数控制是否允许从客户端本地文件加载数据到 MySQL 服务器中

重启MySQL

登录MySQL

代码语言:javascript复制
mysql -uroot -p --local-infile=1

导入数据

代码语言:javascript复制
use rdb;

LOAD DATA LOCAL INFILE '/data/analyserdb/192.168.12.161_rdb_result.csv'
INTO TABLE rdb_result
FIELDS TERMINATED BY ','
LINES TERMINATED BY 'n'
IGNORE 1 LINES
(key_database, key_type, key_name, key_size, key_encoding, num_elements, len_largest_element, key_expiry)
SET ip_add = '192.168.12.161';

比如我们想找到bigkey,可以执行语句

代码语言:javascript复制
select * from rdb_result where key_size >  10240;

5 优化Bigkey

找到 Bigkey 后,怎么优化呢?

这里介绍几种常见的优化方式:

删除不用的Bigkey

有些 Bigkey 业务不需要使用了,因此可以考虑删除掉。但是要注意的是:如果直接 del,可能会阻塞 Redis 服务。大致有下面几种处理办法:

如果 key 类型为 string,则直接删除;

如果 key 类型为 hash、list、set、sorted set,使用 hscan 命令,每次获取部分(例如100个)field-value,再利用 hdel 删除每个 field;

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

控制大小或拆分Bigkey

处理 Bigkey 的另外一种方法就是控制大小,比如 string 减少字符串长度,list、hash、set、zset 等减少成员数。

有时也可以考虑对 Bigkey 进行拆分,具体方法如下:

对于 string 类型的 Bigkey,可以考虑拆分成多个 key-value。

对于 hash 或者 list 类型,可以考虑拆分成多个 hash 或者 list。

0 人点赞