6.5 KiB
6.5 KiB
tags, create time
| tags | create time | ||||
|---|---|---|---|---|---|
|
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