Redis 内存溢出(OOM):故障排查与预防措施

好,下面我们就开始讲讲 Redis 内存溢出(OOM)这个让人头疼,但又不得不面对的问题。

大家好,今天咱们来聊聊 Redis 内存溢出,也就是传说中的 OOM(Out Of Memory)。这玩意儿就像你家的冰箱,东西塞太多了,不光门关不上,还会把好好的食材都给挤坏了。Redis 也是一样,内存满了,性能下降是小事,数据丢失就是大事了。

啥是 Redis OOM?

简单来说,就是 Redis 用光了分配给它的所有内存。这就像你去银行取钱,结果银行告诉你:“不好意思,没钱了。”Redis 如果没内存了,就没法存新的数据,也没法好好处理请求,轻则响应变慢,重则直接崩溃。

为啥会 OOM?

原因有很多,咱们一点点来扒:

  1. 数据量太大: 这就像你家冰箱塞满了东西,最直接的原因就是东西太多了。Redis 存储的数据量超过了 maxmemory 配置的值,就会触发 OOM。

  2. 内存碎片: Redis 在频繁进行数据的增删改查操作时,会产生内存碎片。这些碎片就像冰箱里乱七八糟的包装盒,占据了空间,但又没法用来存放有效数据。

  3. 大 Key: 有些 Key 特别大,比如一个 List 里面存了几百万个元素,或者一个 Hash 里面有几百万个 field。这种大 Key 会一下子占用大量的内存,更容易导致 OOM。

  4. 使用不当: 比如一次性写入大量数据,或者使用复杂度很高的命令(比如 SORT),这些都会消耗大量的内存。

  5. 过期键清理不及时: 如果过期键没有及时清理,也会占用大量的内存。

  6. Redis 自身 Bug: 虽然这种情况比较少见,但也不能排除 Redis 自身存在 Bug 导致内存泄漏的可能性。

OOM 了会咋样?

OOM 发生时,Redis 会根据 maxmemory-policy 配置的策略来处理:

策略 描述
noeviction 默认策略。当内存满了,不删除任何数据,直接返回错误。就像你往冰箱里硬塞东西,冰箱告诉你:“塞不下了,别塞了!”
allkeys-lru 当内存满了,移除最近最少使用的 Key。就像你清理冰箱,把很久没用的东西扔掉。
volatile-lru 当内存满了,移除设置了过期时间的 Key 中,最近最少使用的 Key。就像你清理冰箱,只扔掉快过期的东西。
allkeys-random 当内存满了,随机移除 Key。就像你清理冰箱,随便扔掉一些东西。
volatile-random 当内存满了,随机移除设置了过期时间的 Key。就像你清理冰箱,随便扔掉一些快过期的东西。
volatile-ttl 当内存满了,移除设置了过期时间的 Key 中,剩余 TTL 时间最短的 Key。就像你清理冰箱,优先扔掉马上就要过期的东西。
allkeys-lfu 当内存满了,移除最近最不常用的 Key (Least Frequently Used)。
volatile-lfu 当内存满了,移除设置了过期时间的 Key 中,最近最不常用的 Key (Least Frequently Used)。

如果配置的是 noeviction,那么客户端在尝试写入新数据时会收到 (error) OOM command not allowed when used memory > 'maxmemory'. 错误。其他的策略会尝试删除一些数据,但如果删除速度跟不上写入速度,最终还是会 OOM。

怎么排查 OOM?

排查 OOM 就像侦探破案,需要收集各种线索:

  1. 查看 Redis 日志: Redis 日志会记录 OOM 相关的信息,比如触发 OOM 的命令、删除了哪些 Key 等。仔细分析日志可以找到一些线索。

  2. 使用 INFO 命令: INFO 命令可以查看 Redis 的各种状态信息,包括内存使用情况、Key 的数量、连接数等。

    redis-cli info memory
    redis-cli info stats

    关注以下几个指标:

    • used_memory:Redis 实际使用的内存大小。
    • used_memory_rss:Redis 占用的物理内存大小(包括 Redis 进程自身占用的内存和 Redis 使用的共享库占用的内存)。
    • used_memory_peak:Redis 曾经使用过的最大内存大小。
    • mem_fragmentation_ratioused_memory_rss / used_memory 的比值。如果这个值大于 1.5,说明内存碎片比较严重。
    • total_connections_received:连接总数,如果连接数暴增,需要排查是否存在连接泄漏。
  3. 使用 redis-cli --bigkeys 命令: 这个命令可以找出 Redis 中最大的 Key。

    redis-cli --bigkeys

    输出结果会显示最大 Key 的类型、大小等信息。

  4. 使用 MEMORY USAGE 命令: 这个命令可以查看指定 Key 占用的内存大小。

    redis-cli memory usage mykey
  5. 使用 SLOWLOG 命令: SLOWLOG 可以记录执行时间超过指定阈值的命令。分析 SLOWLOG 可以找出执行时间长的命令,这些命令可能消耗大量的内存。

    redis-cli slowlog get 10  # 获取最近 10 条慢查询日志
  6. 使用 redis-rdb-tools 工具: 这是一个开源工具,可以分析 Redis 的 RDB 文件,找出占用内存最多的 Key。

    # 安装 redis-rdb-tools
    pip install rdbtools
    
    # 分析 RDB 文件
    rdb -c memory /path/to/dump.rdb
  7. 监控 Redis 性能: 使用监控工具(比如 Prometheus + Grafana)监控 Redis 的内存使用情况、CPU 使用率、QPS 等指标。通过监控可以及时发现潜在的 OOM 风险。

怎么预防 OOM?

预防胜于治疗,咱们得提前做好准备:

  1. 合理设置 maxmemory maxmemory 是 Redis 可以使用的最大内存。设置 maxmemory 时要考虑以下因素:

    • 服务器的物理内存大小。
    • 操作系统和其他应用程序需要的内存。
    • Redis 存储的数据量。
    • Redis 的性能要求。

    一般来说,建议将 maxmemory 设置为物理内存的 50% ~ 75%。

  2. 选择合适的 maxmemory-policy 根据业务场景选择合适的 maxmemory-policy。如果对数据丢失比较敏感,可以选择 noeviction。如果允许删除一些数据,可以选择 allkeys-lruvolatile-lru

  3. 避免大 Key: 尽量避免存储大 Key。如果必须存储大 Key,可以考虑以下方案:

    • 将大 Key 分解成多个小 Key。
    • 使用压缩算法对大 Key 进行压缩。
    • 使用 Redis 的数据结构(比如 List、Hash)存储复杂的数据。
  4. 设置 Key 的过期时间: 给 Key 设置合理的过期时间,让 Redis 可以自动清理过期数据。

  5. 优化 Redis 配置: 调整 Redis 的配置参数,比如 hash-max-ziplist-entrieslist-max-ziplist-entries 等,可以减少内存的使用。

  6. 使用 Redis 集群: 将数据分散存储到多个 Redis 节点上,可以降低单个节点的内存压力。

  7. 监控 Redis 性能: 实时监控 Redis 的内存使用情况,及时发现潜在的 OOM 风险。

  8. 定期进行内存碎片整理: Redis 提供了 MEMORY PURGE 命令,可以尝试进行内存碎片整理。但是,这个命令会阻塞 Redis 的正常运行,所以要谨慎使用。

  9. 代码优化: 优化代码,避免一次性写入大量数据,或者使用复杂度很高的命令。

代码示例:

  • 避免一次性写入大量数据:

    # 不推荐
    import redis
    
    r = redis.Redis(host='localhost', port=6379)
    data = {}
    for i in range(100000):
        data[f'key:{i}'] = f'value:{i}'
    r.mset(data)  # 一次性写入 10 万条数据,容易导致 OOM
    
    # 推荐
    import redis
    
    r = redis.Redis(host='localhost', port=6379)
    pipe = r.pipeline()
    for i in range(100000):
        pipe.set(f'key:{i}', f'value:{i}')
        if i % 1000 == 0:
            pipe.execute()  # 每 1000 条数据批量写入
    pipe.execute() # 写入剩余数据
  • 使用压缩算法对大 Key 进行压缩:

    import redis
    import zlib
    
    r = redis.Redis(host='localhost', port=6379)
    
    # 压缩数据
    data = 'This is a very long string that needs to be compressed.' * 1000
    compressed_data = zlib.compress(data.encode('utf-8'))
    
    # 存储压缩后的数据
    r.set('compressed_key', compressed_data)
    
    # 获取压缩后的数据并解压缩
    compressed_data_from_redis = r.get('compressed_key')
    if compressed_data_from_redis:
        decompressed_data = zlib.decompress(compressed_data_from_redis).decode('utf-8')
        print(decompressed_data[:100]) # 打印部分数据
  • 使用 Lua 脚本原子性地更新数据:

    -- Lua 脚本
    local key = KEYS[1]
    local increment = tonumber(ARGV[1])
    local current_value = redis.call("GET", key)
    if current_value then
        current_value = tonumber(current_value)
        local new_value = current_value + increment
        redis.call("SET", key, new_value)
        return new_value
    else
        redis.call("SET", key, increment)
        return increment
    end
    import redis
    
    r = redis.Redis(host='localhost', port=6379)
    
    # 加载 Lua 脚本
    script = """
    local key = KEYS[1]
    local increment = tonumber(ARGV[1])
    local current_value = redis.call("GET", key)
    if current_value then
        current_value = tonumber(current_value)
        local new_value = current_value + increment
        redis.call("SET", key, new_value)
        return new_value
    else
        redis.call("SET", key, increment)
        return increment
    end
    """
    increment_script = r.register_script(script)
    
    # 执行 Lua 脚本
    result = increment_script(keys=['mykey'], args=[10])
    print(result)

总结:

Redis OOM 是一个复杂的问题,需要综合考虑各种因素。排查 OOM 需要耐心和细致,预防 OOM 需要提前做好准备。希望今天的分享能帮助大家更好地理解和解决 Redis OOM 问题。记住,就像对待你家的冰箱一样,定期清理,合理存放,才能让 Redis 更好地为我们服务。

好了,今天的分享就到这里。大家有什么问题,可以随时提问。感谢大家的聆听!

发表回复

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