ToB企服应用市场:ToB评测及商务社交产业平台

标题: Redis 大 Key 分析利器:支持 TOP N、批量分析与从节点优先 [打印本页]

作者: 我爱普洱茶    时间: 5 天前
标题: Redis 大 Key 分析利器:支持 TOP N、批量分析与从节点优先
背景

Redis 大 key 分析工具主要分为两类:
1. 离线分析
基于 RDB 文件举行解析,常用工具是 redis-rdb-tools(https://github.com/sripathikrishnan/redis-rdb-tools)。
不过这个工具已近 5 年未更新,不支持 Redis 7,而且由于使用 Python 开发,解析速度较慢。
目前较为活泼的替代工具是 https://github.com/HDT3213/rdb ,该工具支持 Redis 7,并使用 Go 开发。
2. 在线分析
常用工具是 redis-cli,提供两种分析方式:
这两种方式的优缺点如下:
本文要介绍的工具(redis-find-big-key)也是一个在线分析工具,其实现思路与redis-cli --memkeys类似,但功能更为强大实用。主要体如今:
测试时间对比

测试环境:Redis 6.2.17,单实例,used_memory_human 为 9.75G,key 数量 100w,RDB 文件大小 3GB。
以下是上面提到的四个工具在获取内存占用最多的 100 个 key 时的耗时情况:
工具耗时redis-rdb-tools25m38.68shttps://github.com/HDT3213/rdb50.68sredis-cli --memkeys40.22sredis-find-big-key29.12s工具效果
  1. # ./redis-find-big-key -addr 10.0.1.76:6379 -cluster-mode
  2. Log file not specified, using default: /tmp/10.0.1.76:6379_20250222_043832.txt
  3. Scanning keys from node: 10.0.1.76:6380 (slave)

  4. Node: 10.0.1.76:6380
  5. -------- Summary --------
  6. Sampled 8 keys in the keyspace!
  7. Total key length in bytes is 2.96 MB (avg len 379.43 KB)

  8. Top biggest keys:
  9. +------------------------------+--------+-----------+---------------------+
  10. |             Key              |  Type  |   Size    | Number of elements  |
  11. +------------------------------+--------+-----------+---------------------+
  12. | mysortedset_20250222043729:1 |  zset  | 739.6 KB  |    8027 members     |
  13. |   myhash_20250222043741:2    |  hash  | 648.12 KB |     9490 fields     |
  14. | mysortedset_20250222043741:1 |  zset  | 536.44 KB |    5608 members     |
  15. |    myset_20250222043729:1    |  set   | 399.66 KB |    8027 members     |
  16. |    myset_20250222043741:1    |  set   | 328.36 KB |    5608 members     |
  17. |   myhash_20250222043729:2    |  hash  | 222.65 KB |     3917 fields     |
  18. |   mylist_20250222043729:1    |  list  | 160.54 KB |     8027 items      |
  19. |    mykey_20250222043729:2    | string | 73 bytes  | 7 bytes (value len) |
  20. +------------------------------+--------+-----------+---------------------+
  21. Scanning keys from node: 10.0.1.202:6380 (slave)

  22. Node: 10.0.1.202:6380
  23. -------- Summary --------
  24. Sampled 8 keys in the keyspace!
  25. Total key length in bytes is 3.11 MB (avg len 398.23 KB)

  26. Top biggest keys:
  27. +------------------------------+--------+------------+---------------------+
  28. |             Key              |  Type  |    Size    | Number of elements  |
  29. +------------------------------+--------+------------+---------------------+
  30. | mysortedset_20250222043741:2 |  zset  | 1020.13 KB |    9490 members     |
  31. |    myset_20250222043741:2    |  set   | 588.81 KB  |    9490 members     |
  32. |   myhash_20250222043729:1    |  hash  |  456.1 KB  |     8027 fields     |
  33. | mysortedset_20250222043729:2 |  zset  |  404.5 KB  |    3917 members     |
  34. |   myhash_20250222043741:1    |  hash  | 335.79 KB  |     5608 fields     |
  35. |    myset_20250222043729:2    |  set   | 195.87 KB  |    3917 members     |
  36. |   mylist_20250222043741:2    |  list  | 184.55 KB  |     9490 items      |
  37. |    mykey_20250222043741:1    | string |  73 bytes  | 7 bytes (value len) |
  38. +------------------------------+--------+------------+---------------------+
  39. Scanning keys from node: 10.0.1.147:6380 (slave)

  40. Node: 10.0.1.147:6380
  41. -------- Summary --------
  42. Sampled 4 keys in the keyspace!
  43. Total key length in bytes is 192.9 KB (avg len 48.22 KB)

  44. Top biggest keys:
  45. +-------------------------+--------+-----------+---------------------+
  46. |           Key           |  Type  |   Size    | Number of elements  |
  47. +-------------------------+--------+-----------+---------------------+
  48. | mylist_20250222043741:1 |  list  | 112.45 KB |     5608 items      |
  49. | mylist_20250222043729:2 |  list  | 80.31 KB  |     3917 items      |
  50. | mykey_20250222043729:1  | string | 73 bytes  | 7 bytes (value len) |
  51. | mykey_20250222043741:2  | string | 73 bytes  | 7 bytes (value len) |
  52. +-------------------------+--------+-----------+---------------------+
复制代码
工具地址

项目地址:https://github.com/slowtech/redis-find-big-key
可直接下载二进制包,也可举行源码编译。
直接下载二进制包
  1. # wget https://github.com/slowtech/redis-find-big-key/releases/download/v1.0.0/redis-find-big-key-linux-amd64.tar.gz
  2. # tar xvf redis-find-big-key-linux-amd64.tar.gz 
复制代码
解压后,会在当前目录生成一个名为redis-find-big-key的可执行文件。
源码编译
  1. # wget https://github.com/slowtech/redis-find-big-key/archive/refs/tags/v1.0.0.tar.gz
  2. # tar xvf v1.0.0.tar.gz 
  3. # cd redis-find-big-key-1.0.0
  4. # go build
复制代码
编译完成后,会在当前目录生成一个名为redis-find-big-key的可执行文件。
参数解析
  1. # ./redis-find-big-key --help
  2. Usage of ./redis-find-big-key:
  3.   -addr string
  4.         Redis server address in the format <hostname>:<port>
  5.   -cluster-mode
  6.         Enable cluster mode to get keys from all shards in the Redis cluster
  7.   -concurrency int
  8.         Maximum number of nodes to process concurrently (default 1)
  9.   -direct
  10.         Perform operation on the specified node. If not specified, the operation will default to executing on the slave node
  11.   -log-file string
  12.         Log file for saving progress and intermediate result
  13.   -master-yes
  14.         Execute even if the Redis role is master
  15.   -password string
  16.         Redis password
  17.   -samples uint
  18.         Samples for memory usage (default 5)
  19.   -skip-lazyfree-check
  20.         Skip check lazyfree-lazy-expire
  21.   -sleep float
  22.         Sleep duration (in seconds) after processing each batch
  23.   -tls
  24.         Enable TLS for Redis connection
  25.   -top int
  26.         Maximum number of biggest keys to display (default 100)
复制代码
各个参数的具体寄义如下:
常见用法

分析单个节点
  1. ./redis-find-big-key -addr 10.0.1.76:6379
  2. Scanning keys from node: 10.0.1.202:6380 (slave)
复制代码
注意,在上面的示例中,指定的节点和实际扫描的节点并不相同。这是因为 10.0.1.76:6379 是主节点,而该工具默认会选择从库举行分析。只有当指定的主节点没有从库时,工具才会直接扫描该主节点。
分析单个 Redis 集群
  1. ./redis-find-big-key -addr 10.0.1.76:6379 -cluster-mode
复制代码
只需提供集群中任意一个节点的地址,工具会自动获取集群中其它节点的地址。同时,工具会优先选择从节点举行分析,只有在某个分片没有从节点时,才会选择该分片的主节点举行分析。
分析多个节点
  1. ./redis-find-big-key -addr 10.0.1.76:6379,10.0.1.202:6379,10.0.1.147:6379
复制代码
节点之间是相互独立的,可以来自同一个集群,也可以来自不同的集群。注意,如果 -addr 参数指定了多个节点地址,则不能再使用 -cluster-mode 参数。
对主节点举行分析

如果需要对主节点举行分析,可指定主节点并使用-direct参数。
  1. ./redis-find-big-key -addr 10.0.1.76:6379 -direct -master-yes
复制代码
注意事项

1. 该工具仅实用于 Redis 4.0 及以上版本,因为MEMORY USAGE和lazyfree-lazy-expire是从 Redis 4.0 开始支持的。
2. 同一个 key 在 redis-find-big-key 和 redis-cli 中显示的大小可能不一致,这是正常现象。原因在于,redis-find-big-key 默认选择从库举行分析,因此通常显示的是从库中的 key 大小,而  redis-cli 只能对主库举行分析,显示的是主库中的 key 大小。看下面这个示例。
  1. # ./redis-find-big-key -addr 10.0.1.76:6379 -top 1
  2. Scanning keys from node: 10.0.1.202:6380 (slave)
  3. ...
  4. Top biggest keys:
  5. +------------------------------+------+------------+--------------------+
  6. |             Key              | Type |    Size    | Number of elements |
  7. +------------------------------+------+------------+--------------------+
  8. | mysortedset_20250222043741:2 | zset | 1020.13 KB |    9490 members    |
  9. +------------------------------+------+------------+--------------------+

  10. # redis-cli -h 10.0.1.76 -p 6379 -c MEMORY USAGE mysortedset_20250222043741:2
  11. (integer) 1014242

  12. # echo "scale=2; 1014242 / 1024" | bc
  13. 990.47
复制代码
一个是 1020.13 KB,一个是 990.47 KB。
如果直接通过 redis-find-big-key 查看主库中该 key 的大小,结果与 redis-cli 完全一致:
  1. # ./redis-find-big-key -addr 10.0.1.76:6379 -direct --master-yes -top 1 --skip-lazyfree-check
  2. Scanning keys from node: 10.0.1.76:6379 (master)
  3. ...
  4. Top biggest keys:
  5. +------------------------------+------+-----------+--------------------+
  6. |             Key              | Type |   Size    | Number of elements |
  7. +------------------------------+------+-----------+--------------------+
  8. | mysortedset_20250222043741:2 | zset | 990.47 KB |    9490 members    |
  9. +------------------------------+------+-----------+--------------------+
复制代码
实现原理

该工具是参考redis-cli --memkeys实现的。
实际上,无论是redis-cli --bigkeys照旧redis-cli --memkeys,调用的都是findBigKeys函数,只不过传入的参数不一样。
  1. /* Find big keys */
  2. if (config.bigkeys) {
  3.     if (cliConnect(0) == REDIS_ERR) exit(1);
  4.     findBigKeys(0, 0);
  5. }

  6. /* Find large keys */
  7. if (config.memkeys) {
  8.     if (cliConnect(0) == REDIS_ERR) exit(1);
  9.     findBigKeys(1, config.memkeys_samples);
  10. }
复制代码
接下来,我们看一下这个函数的具体实现逻辑。
  1. static void findBigKeys(int memkeys, unsigned memkeys_samples) {
  2.     ...
  3.     // 通过 DBSIZE 命令获取 key 的总数量
  4.     total_keys = getDbSize();

  5.     /* Status message */
  6.     printf("\n# Scanning the entire keyspace to find biggest keys as well as\n");
  7.     printf("# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec\n");
  8.     printf("# per 100 SCAN commands (not usually needed).\n\n");

  9.     /* SCAN loop */
  10.     do {
  11.         /* Calculate approximate percentage completion */
  12.         pct = 100 * (double)sampled/total_keys;
  13.       
  14.         // 通过 SCAN 命令扫描 key
  15.         reply = sendScan(&it);
  16.         scan_loops++;
  17.         // 获取当前批次的 key 名称。
  18.         keys  = reply->element[1];
  19.         ...
  20.         // 使用 pipeline 技术批量发送 TYPE 命令,获取每个 key 的类型
  21.         getKeyTypes(types_dict, keys, types);
  22.         // 使用 pipeline 技术批量发送相应命令获取每个 key 的大小
  23.         getKeySizes(keys, types, sizes, memkeys, memkeys_samples);

  24.         // 逐个处理 key,更新统计信息
  25.         for(i=0;i<keys->elements;i++) {
  26.             typeinfo *type = types[i];
  27.             /* Skip keys that disappeared between SCAN and TYPE */
  28.             if(!type)
  29.                 continue;

  30.             type->totalsize += sizes[i]; // 累计每个类型 key 的总大小
  31.             type->count++; // 累计每个类型 key 的数量
  32.             totlen += keys->element[i]->len; // 累计 key 的长度
  33.             sampled++; // 累计扫描的 key 的数量
  34.             // 如果当前 key 的大小超过该类型的最大值,则会更新该类型的最大键大小,并打印统计信息。
  35.             if(type->biggest<sizes[i]) {
  36.                 if (type->biggest_key)
  37.                     sdsfree(type->biggest_key);
  38.                 type->biggest_key = sdscatrepr(sdsempty(), keys->element[i]->str, keys->element[i]->len);
  39.                 ...
  40.                 printf(
  41.                    "[%05.2f%%] Biggest %-6s found so far '%s' with %llu %s\n",
  42.                    pct, type->name, type->biggest_key, sizes[i],
  43.                    !memkeys? type->sizeunit: "bytes");

  44.                 type->biggest = sizes[i];
  45.             }

  46.             // 每扫描 100 万个 key,还会输出当前进度和扫描的 key 数量。
  47.             if(sampled % 1000000 == 0) {
  48.                 printf("[%05.2f%%] Sampled %llu keys so far\n", pct, sampled);
  49.             }
  50.         }

  51.         // 如果设置了 interval,则每执行 100 次 SCAN 命令,都会 sleep 一段时间。
  52.         if (config.interval && (scan_loops % 100) == 0) {
  53.             usleep(config.interval);
  54.         }

  55.         freeReplyObject(reply);
  56.     } while(force_cancel_loop == 0 && it != 0);
  57.     .. 
  58.     // 输出总的统计信息
  59.     printf("\n-------- summary -------\n\n");
  60.     if (force_cancel_loop) printf("[%05.2f%%] ", pct); // 如果循环被取消,则显示进度百分比
  61.     printf("Sampled %llu keys in the keyspace!\n", sampled); // 打印已经扫描的 key 的数量
  62.     printf("Total key length in bytes is %llu (avg len %.2f)\n\n",
  63.        totlen, totlen ? (double)totlen/sampled : 0); // 打印 key 名的总长度及平均长度

  64.     // 输出每种类型最大键的信息
  65.     di = dictGetIterator(types_dict);
  66.     while ((de = dictNext(di))) {
  67.         typeinfo *type = dictGetVal(de);
  68.         if(type->biggest_key) {
  69.             printf("Biggest %6s found '%s' has %llu %s\n", type->name, type->biggest_key,
  70.                type->biggest, !memkeys? type->sizeunit: "bytes");
  71.         } // type->name 是 key 的类型名称,type->biggest_key 是最大键的名称
  72.     } // type->biggest 是最大键的大小,!memkeys? type->sizeunit: "bytes" 是大小单位。
  73.     ..
  74.     // 输出每种类型的统计信息
  75.     di = dictGetIterator(types_dict);
  76.     while ((de = dictNext(di))) {
  77.         typeinfo *type = dictGetVal(de);
  78.         printf("%llu %ss with %llu %s (%05.2f%% of keys, avg size %.2f)\n",
  79.            type->count, type->name, type->totalsize, !memkeys? type->sizeunit: "bytes",
  80.            sampled ? 100 * (double)type->count/sampled : 0,
  81.            type->count ? (double)type->totalsize/type->count : 0);
  82.     } // sampled ? 100 * (double)type->count/sampled : 0 是当前类型的 key 的数量在总扫描的 key 数量中的百分比。
  83.     ..
  84.     exit(0);
  85. }
复制代码
该函数的实现逻辑如下:

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4