Files
cs-note/hzh/REDIS/内存持续增长排查.md
T

6.5 KiB
Raw Blame History

tags, create time
tags create time
redis
memory
troubleshooting
monitoring
2026-06-01 10:30

Redis 内存持续增长排查

概述

生产环境中 Redis 内存持续增长是一个常见的告警场景。它不一定意味着 Bug——但如果不及时处理,最终会导致 OOM 或频繁 Swap。本节提供一套系统性的排查思路,对应你的薄弱点 F10(TTL 命令检查 Key 过期状态)。

一、排查流程图

flowchart TD
    Start["⚠️ 内存持续增长告警"] --> Step1["Step 1: MEMORY USED 确认增长幅度"]
    
    Step1 --> Q1{"增长是持续的还是突发的?"}
    Q1 -- "持续增长" --> SLOW["缓慢增长路径"]
    Q1 -- "突然飙升" --> SPIKE["突发峰值路径"]
    
    subgraph SLOW["慢增长排查"]
        S1["检查 Key 总数变化<br/>DBSIZE / SCAN"] --> S2["找出增长最快的 Key 前缀<br/>SCAN + TTL 检查"] --> S3["确认 EXPIRE 是否生效<br/>TTL key → -1 表示未过期"] --> S4["僵尸 Key 清理"]
    end
    
    subgraph SPIKE["爆发增长排查"]
        P1["监控 BigKey 列表<br/>MEMORY USAGE + SORTED BY size"] --> P2["检查是否有大量写入并发<br/>Slowlog / Monitoring"] --> P3["排查是否为临时批处理任务<br/>定时任务/数据同步"]
    end
    
    S4 --> Action{"找到原因了?"}
    P3 --> Action
    
    Action -- "是" --> Fix["修复"]
    Action -- "否" --> Dump["MEMORY ANALYZER<br/>深入分析内存分布"]
    
    style Start fill:#fff3e0
    style Fix fill:#c8e6c9
    style Dump fill:#e3f2fd

二、逐步排查方法

Step 1:定位增长来源

# 查看所有数据库的大小
INFO keyspace
→ db0:keys=12345,expires=12000,avg_ttl=3600000

输出解析:

字段 含义
keys 该数据库中 Key 的总数
expires 设置了过期时间的 Key 数量
avg_ttl 这些 Key 的平均剩余生存时间(毫秒)

如果 keys 在持续增长而 expires 不变 → 有大量未设置过期的 Key

Step 2:查找大 Key

# 方式1: redis-cli --bigkeys(推荐快速使用)
redis-cli --bigkeys

# 扫描中...
# --- STRINGS ---
# biggest   rate:sliding:user123 (6 MB)
# count        15 (total 15 keys)
# ...

# 方式2: 手动用 SCAN + MEMORY USAGE
SCAN 0 MATCH rate:* TYPE string | xargs -I{} redis-cli MEMORY USAGE {}

[!tip]- bigkeys 工具的坑

redis-cli --bigkeys 会遍历所有 Key,对大库来说仍然可能阻塞 Redis。生产环境慎用,建议在从库或低峰期运行。

Step 3:检查过期状态(F10 考点 🔑)

这是排查内存问题的核心技能——判断 Key 是否已经过期。

# 检查单个 Key 的 TTL
TTL rate:limit:user123
→ 86400      ← 剩余 86400 秒,正常

TTL rate:limit:user456
→ -1         ← ❌ 没有过期时间!这就是僵尸 Key

TTL rate:limit:user789
→ -2         ← ❌ Key 已不存在,已被删除
TTL 返回值 含义 处理方式
> 0 Key 存在,还有 N 秒过期 正常,等待自动清理
-1 Key 存在但没有设置过期时间 ❌ 检查代码中是否漏了 EXPIRE,手动 DEL
-2 Key 已不存在 正常,无需操作

Step 4:批量检查僵尸 Key

func findZombieKeys(ctx context.Context, rdb *redis.Client, pattern string) []string {
    var zombies []string
    cursor := uint64(0)
    
    for {
        keys, nextCursor, _ := rdb.Scan(ctx, cursor, pattern, 200).Result()
        for _, key := range keys {
            ttl, err := rdb.TTL(ctx, key).Result()
            if err == nil && ttl == -1 {
                zombies = append(zombies, key)
            }
        }
        if nextCursor == 0 {
            break
        }
        cursor = nextCursor
    }
    return zombies
}

三、常见内存增长原因及对策

原因 表现 解决方式
EXPIRE 未生效 TTL 返回 -1 检查 Lua 脚本或 Go 代码中的 EXPIRE 调用
Sorted Set 规模过大 个别 Key 占几 MB~几十 MB 切换到令牌桶方案(O(1) 空间)
批量写入无上限 DBSIZE 快速增长 添加写入限流和 Key 数量上限
大 Hash/List 结构 单 Key 占用巨大 拆分或使用更合适的数据结构
副本同步延迟 Master 已过期,Slave 还没删 配置 replica-ignore-maxmemory
Redis 缓存淘汰策略不当 maxmemory-policy 设为 noeviction 改为 allkeys-lru 或 volatile-ttl

四、预防性措施

flowchart TD
    Monitor["📊 持续监控"] --> A["MEMORY USAGE per Key 告警"]
    Monitor --> B["DBSIZE 增长速率告警"]
    Monitor --> C["Evicted Keys 计数告警"]
    
    Policy["⚙️ 合理的淘汰策略"] --> D["maxmemory-policy: allkeys-lru"]
    Policy --> E["maxmemory: 设为可用内存的 70%"]
    
    Design["🔧 设计时考虑"] --> F["所有 Key 必须有 EXPIRE"]
    Design --> G["高频场景用 O(1) 算法(令牌桶)"]
    Design --> H["定期 SCAN 清理残留数据"]
    
    style Monitor fill:#e3f2fd
    style Policy fill:#fff3e0
    style Design fill:#e8f5e9

推荐配置

# redis.conf

# 最大内存限制(建议设置为服务器总内存的 70%,留余量给 Swap)
maxmemory 2gb

# 淘汰策略:LRU(最近最少使用)
maxmemory-policy allkeys-lru

# 或者:优先删除有过期时间的 Key 中最早过期的
# maxmemory-policy volatile-ttl

# RDB/AOF 混合持久化
rdb-save-incremental-mem YES
aof-use-rdb-preamble YES

五、紧急止血步骤

当内存已经接近上限时的应急操作:

1. 停止所有非必要的写入(暂停定时任务、数据同步)
2. 找到并删除最大的几个 Key:
   redis-cli --bigkeys
   DEL <bigkey1> <bigkey2> ...
3. 如果无法确定哪些 Key 该删,先批量减少 Key 总量:
   SCAN 0 MATCH rate:* COUNT 1000 → DEL 随机抽取的部分 Key(谨慎评估影响)
4. 临时提升 maxmemory(如果服务器还有物理内存可用)
5. 事后复盘:为什么没及时发现?监控阈值要不要调低?

[!warning]- DEL vs UNLINK

删除大 Key 时用 UNLINK(异步删除)替代 DEL(同步删除),避免阻塞主线程:

# 同步删除(阻塞)
DEL rate:sliding:large_key

# 异步删除(不阻塞)
UNLINK rate:sliding:large_key

关联笔记