Redis `bigkeys` 工具:扫描并发现大 Key

好的,没问题。

各位观众,晚上好!欢迎来到“Redis 那些事儿”特别节目。今天,咱们聊聊 Redis 里的“大胃王”—— bigkeys,以及如何揪出这些家伙,避免它们撑爆你的 Redis。

开场白:谁动了我的内存?

你有没有遇到过这种情况:Redis 跑着跑着,突然就慢了?或者更糟糕,直接 OOM 了?这时候,你可能一脸懵圈,心想:“我明明没存多少数据啊,内存都去哪儿了?”

别慌,十有八九是你的 Redis 里藏着几个“大胃王”,它们悄悄地吞噬了你的内存资源。这些“大胃王”就是我们今天要聊的 bigkeys。

什么是 bigkeys?

所谓 bigkeys,就是指在 Redis 中存储了大量数据,占用过多内存的 Key。这些 Key 可能是:

  • 超大的字符串: 例如,一个存储了巨长 JSON 字符串的 Key。
  • 包含大量元素的集合: 例如,一个拥有数百万成员的 Set 或 Sorted Set。
  • 包含大量字段的 Hash: 例如,一个拥有成千上万个字段的 Hash。
  • 包含大量元素的 List: 例如,一个存储了大量消息的 List。

这些 bigkeys 会带来一系列问题:

  • 内存占用过高: 这是最直接的问题,导致 Redis 内存使用率飙升,影响其他服务的正常运行。
  • 性能瓶颈: 操作 bigkeys 会消耗大量 CPU 和 I/O 资源,导致 Redis 响应变慢。
  • 阻塞主线程: 在单线程的 Redis 环境中,处理 bigkeys 可能会阻塞主线程,导致所有请求都受到影响。
  • 迁移困难: 在进行数据迁移或扩容时,bigkeys 会成为瓶颈,导致迁移速度变慢,甚至失败。

Redis 自带的 redis-cli --bigkeys

幸运的是,Redis 提供了一个内置的工具,可以帮助我们快速找到这些 bigkeys,那就是 redis-cli --bigkeys

redis-cli --bigkeys 会扫描整个 Redis 数据库,并报告以下信息:

  • 每个数据类型中最大的 Key。
  • 每个数据类型的 Key 的平均大小。
  • 扫描过程中发现的 Key 的总数。

如何使用 redis-cli --bigkeys

使用 redis-cli --bigkeys 非常简单,只需在命令行中输入以下命令:

redis-cli --bigkeys -h <host> -p <port> -a <password>

其中:

  • <host> 是 Redis 服务器的地址。
  • <port> 是 Redis 服务器的端口。
  • <password> 是 Redis 服务器的密码(如果设置了密码)。

例如:

redis-cli --bigkeys -h 127.0.0.1 -p 6379 -a mypassword

运行命令后,你会看到类似下面的输出:

# Scanning the entire keyspace to find biggest keys as well as
# average sizes.

# Sampled 100 keys in parallel!
# Overall SCAN times: 0

Biggest string found so far 'big_string_key' with 1048576 bytes
Biggest list   found so far 'big_list_key' with 10000 elements
Biggest set    found so far 'big_set_key' with 5000 members
Biggest hash   found so far 'big_hash_key' with 2000 fields
Biggest zset   found so far 'big_zset_key' with 1000 members

# Summary:

Sampled 5 keys in parallel!
strings:1 (20.00%) of the keys take 1048576 bytes (50.00% of all bytes)
lists:1 (20.00%) of the keys take 40000 bytes (1.91% of all bytes)
sets:1 (20.00%) of the keys take 12000 bytes (0.57% of all bytes)
hashes:1 (20.00%) of the keys take 8000 bytes (0.38% of all bytes)
zsets:1 (20.00%) of the keys take 32000 bytes (1.53% of all bytes)

5 total keys (5 sampled)
1100576 total bytes sampled
1 keys per second
0.00 bytes per second

这个输出会告诉你:

  • 最大的 String Key 是 big_string_key,大小为 1048576 字节。
  • 最大的 List Key 是 big_list_key,包含 10000 个元素。
  • 最大的 Set Key 是 big_set_key,包含 5000 个成员。
  • 最大的 Hash Key 是 big_hash_key,包含 2000 个字段。
  • 最大的 Sorted Set Key 是 big_zset_key,包含 1000 个成员。
  • 各个数据类型 Key 的数量和占用内存的比例。
  • 扫描速度。

--bigkeys 的工作原理

--bigkeys 命令的底层原理是使用了 Redis 的 SCAN 命令。SCAN 命令可以增量地遍历 Redis 数据库中的所有 Key,而不会阻塞主线程。--bigkeys 会在扫描过程中记录每个数据类型中最大的 Key,并计算平均大小。

优化 --bigkeys 命令

--bigkeys 命令提供了一些选项,可以帮助你优化扫描过程:

  • --threads <n>: 使用多线程扫描,可以提高扫描速度。例如,redis-cli --bigkeys --threads 4 使用 4 个线程进行扫描。
  • --db <dbid>: 指定要扫描的数据库。例如,redis-cli --bigkeys --db 1 只扫描数据库 1。
  • -i <interval>: 指定扫描的间隔时间,单位为秒。可以控制扫描的速度,避免对 Redis 造成过大的压力。例如,redis-cli --bigkeys -i 0.1 每隔 0.1 秒扫描一次。
  • --latency: 显示扫描过程中的延迟信息,可以帮助你了解扫描对 Redis 性能的影响。

示例:使用多线程加速扫描

redis-cli --bigkeys --threads 4 -h 127.0.0.1 -p 6379 -a mypassword

示例:只扫描数据库 0

redis-cli --bigkeys --db 0 -h 127.0.0.1 -p 6379 -a mypassword

发现 bigkeys 之后怎么办?

找到 bigkeys 只是第一步,接下来我们需要分析这些 bigkeys 的用途,并采取相应的措施。

一般来说,处理 bigkeys 的方法有以下几种:

  1. 删除 bigkeys: 如果这个 bigkey 已经不再需要,或者可以通过其他方式获取数据,那么最简单的方法就是直接删除它。使用 DEL 命令可以删除一个 Key。

    redis-cli DEL <big_key>

    注意: 删除 bigkeys 可能会阻塞主线程,尤其是当 bigkey 包含大量数据时。为了避免阻塞,可以使用 UNLINK 命令。UNLINK 命令会异步地删除 Key,不会阻塞主线程。

    redis-cli UNLINK <big_key>
  2. 拆分 bigkeys: 如果 bigkey 存储的是一个集合(例如 List、Set、Hash、Sorted Set),可以考虑将它拆分成多个小的 Key。

    • List: 可以将一个大的 List 拆分成多个小的 List,每个 List 包含一定数量的元素。
    • Set: 可以将一个大的 Set 拆分成多个小的 Set,每个 Set 包含一定数量的成员。
    • Hash: 可以将一个大的 Hash 拆分成多个小的 Hash,每个 Hash 包含一定数量的字段。
    • Sorted Set: 可以将一个大的 Sorted Set 拆分成多个小的 Sorted Set,每个 Sorted Set 包含一定数量的成员。

    示例:拆分 Hash Key

    假设我们有一个名为 user:123 的 Hash Key,包含 10000 个字段。我们可以将它拆分成 10 个小的 Hash Key,每个 Hash Key 包含 1000 个字段。

    import redis
    
    # 连接 Redis
    r = redis.Redis(host='127.0.0.1', port=6379, password='mypassword')
    
    # 原来的 Hash Key
    big_hash_key = 'user:123'
    
    # 拆分的数量
    num_shards = 10
    
    # 获取 Hash Key 的所有字段
    all_fields = r.hkeys(big_hash_key)
    
    # 计算每个 shard 的大小
    shard_size = len(all_fields) // num_shards
    
    # 拆分 Hash Key
    for i in range(num_shards):
        # 新的 Hash Key
        shard_key = f'{big_hash_key}:shard:{i}'
    
        # 获取 shard 的字段
        start = i * shard_size
        end = (i + 1) * shard_size if i < num_shards - 1 else len(all_fields)
        shard_fields = all_fields[start:end]
    
        # 创建新的 Hash Key
        shard_data = {}
        for field in shard_fields:
            shard_data[field] = r.hget(big_hash_key, field)
        r.hmset(shard_key, shard_data)
    
    # 删除原来的 Hash Key
    r.delete(big_hash_key)

    注意: 拆分 bigkeys 会增加代码的复杂性,需要修改应用程序的代码来适应新的数据结构。

  3. 压缩 bigkeys: 如果 bigkey 存储的是一个大的字符串,可以考虑使用压缩算法来减少内存占用。例如,可以使用 Gzip 或 Snappy 算法来压缩字符串。

    示例:使用 Gzip 压缩 String Key

    import redis
    import gzip
    
    # 连接 Redis
    r = redis.Redis(host='127.0.0.1', port=6379, password='mypassword')
    
    # 原来的 String Key
    big_string_key = 'big_string'
    
    # 获取 String Key 的值
    string_value = r.get(big_string_key)
    
    # 使用 Gzip 压缩字符串
    compressed_value = gzip.compress(string_value)
    
    # 存储压缩后的字符串
    r.set(big_string_key + ':compressed', compressed_value)
    
    # 删除原来的 String Key
    r.delete(big_string_key)
    
    # 读取压缩后的字符串
    compressed_value = r.get(big_string_key + ':compressed')
    
    # 解压字符串
    string_value = gzip.decompress(compressed_value)

    注意: 压缩和解压会消耗 CPU 资源,需要在性能和内存占用之间进行权衡。

  4. 使用 Redis Modules: Redis Modules 提供了更高级的数据结构和算法,可以更有效地存储和处理大数据。例如,可以使用 RedisBloom 模块来存储大规模的 Set 数据,或者使用 RedisJSON 模块来存储复杂的 JSON 数据。

  5. 数据分片:将数据水平拆分到多个 Redis 实例中,每个实例负责存储一部分数据。

预防胜于治疗

预防 bigkeys 的最好方法是在设计应用程序时就考虑到数据规模,并采取相应的措施。

以下是一些预防 bigkeys 的建议:

  • 限制集合的大小: 在应用程序中限制 List、Set、Hash、Sorted Set 的大小,避免它们无限增长。
  • 使用合适的数据结构: 根据数据的特点选择合适的数据结构。例如,如果只需要存储键值对,可以使用 String;如果需要存储有序的集合,可以使用 Sorted Set。
  • 定期清理过期数据: 定期清理不再需要的过期数据,释放内存空间。
  • 监控 Redis 内存使用情况: 监控 Redis 的内存使用情况,及时发现潜在的 bigkeys。可以使用 Redis 的 INFO memory 命令查看内存使用情况。也可以使用 Redis 的监控工具,例如 RedisInsight 或 Prometheus。

编写脚本自动化 bigkeys 检测

虽然 redis-cli --bigkeys 很方便,但如果想定期检测 bigkeys,最好编写一个脚本来自动化这个过程。

以下是一个使用 Python 编写的脚本,可以定期扫描 Redis 数据库,并报告 bigkeys:

import redis
import time
import os

def find_big_keys(host='127.0.0.1', port=6379, password=None, db=0, threshold=1024 * 1024): # 1MB
    """
    Finds big keys in a Redis database.

    Args:
        host (str): Redis host.
        port (int): Redis port.
        password (str): Redis password.
        db (int): Redis database number.
        threshold (int): Minimum size of a key to be considered "big" (in bytes).

    Returns:
        dict: A dictionary of big keys, grouped by type.
    """

    r = redis.Redis(host=host, port=port, db=db, password=password)
    big_keys = {'string': [], 'list': [], 'set': [], 'hash': [], 'zset': []}

    for key_type in big_keys.keys():
        cursor = '0'
        while cursor != 0:
            cursor, keys = r.scan(cursor=cursor, match='*', count=100)  # Adjust count as needed

            for key in keys:
                try:
                    key = key.decode('utf-8')  # Decode bytes to string
                    data_type = r.type(key).decode('utf-8')

                    if data_type == key_type:
                        if data_type == 'string':
                            size = r.strlen(key)
                        elif data_type == 'list':
                            size = r.llen(key) * 8  # Approximate size
                        elif data_type == 'set':
                            size = r.scard(key) * 8  # Approximate size
                        elif data_type == 'hash':
                            size = r.hlen(key) * 8  # Approximate size
                        elif data_type == 'zset':
                            size = r.zcard(key) * 8  # Approximate size
                        else:
                            continue

                        if size > threshold:
                            big_keys[key_type].append({'key': key, 'size': size})

                except redis.exceptions.ConnectionError as e:
                    print(f"Connection error: {e}")
                    return None  # Or raise the exception if appropriate
                except Exception as e:
                    print(f"Error processing key {key}: {e}")

    return big_keys

def main():
    host = os.environ.get('REDIS_HOST', '127.0.0.1')
    port = int(os.environ.get('REDIS_PORT', 6379))
    password = os.environ.get('REDIS_PASSWORD')
    db = int(os.environ.get('REDIS_DB', 0))
    threshold_mb = int(os.environ.get('REDIS_THRESHOLD_MB', 1)) #Threshold in MB
    threshold = threshold_mb * 1024 * 1024 # converting MB to bytes

    big_keys = find_big_keys(host=host, port=port, password=password, db=db, threshold=threshold)

    if big_keys:
        print("Big Keys Found:")
        for key_type, keys in big_keys.items():
            if keys:
                print(f"  {key_type.capitalize()}:")
                for key_info in keys:
                    print(f"    Key: {key_info['key']}, Size: {key_info['size']} bytes")
    else:
        print("No big keys found.")

if __name__ == "__main__":
    start_time = time.time()
    main()
    end_time = time.time()
    print(f"Script execution time: {end_time - start_time:.2f} seconds")

使用方法:

  1. 保存上面的代码为 bigkeys_scanner.py
  2. 安装 redis Python 库:pip install redis
  3. 运行脚本:python bigkeys_scanner.py

你也可以将 Redis 连接信息设置为环境变量,避免硬编码:

export REDIS_HOST=your_redis_host
export REDIS_PORT=your_redis_port
export REDIS_PASSWORD=your_redis_password
export REDIS_DB=your_redis_db_number
export REDIS_THRESHOLD_MB=2 # Example: Set threshold to 2MB

python bigkeys_scanner.py

脚本说明:

  • find_big_keys() 函数连接到 Redis 数据库,并使用 SCAN 命令扫描所有 Key。
  • 对于每个 Key,函数会获取它的数据类型和大小。
  • 如果 Key 的大小超过了阈值,函数会将它添加到 big_keys 字典中。
  • main() 函数调用 find_big_keys() 函数,并打印结果。
  • 使用环境变量配置 Redis 连接信息和阈值,增加灵活性。
  • 代码使用try…except结构来处理连接错误和其它潜在的异常,提高代码的健壮性。

总结:

Bigkeys 是 Redis 性能的潜在威胁,我们需要定期扫描 Redis 数据库,找出这些“大胃王”,并采取相应的措施。redis-cli --bigkeys 是一个非常有用的工具,可以帮助我们快速找到 bigkeys。同时,我们也可以编写脚本来自动化这个过程。最重要的,还是在设计应用程序时就考虑到数据规模,并采取相应的预防措施。

希望今天的分享对你有所帮助!下次再见!

发表回复

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