vault backup: 2026-06-01 00:35:50
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
---
|
||||
tags: [redis, memory, troubleshooting, monitoring]
|
||||
create time: 2026-06-01 10:30
|
||||
---
|
||||
|
||||
# Redis 内存持续增长排查
|
||||
|
||||
## 概述
|
||||
|
||||
生产环境中 Redis 内存持续增长是一个常见的告警场景。它不一定意味着 Bug——但如果不及时处理,最终会导致 OOM 或频繁 Swap。本节提供一套**系统性的排查思路**,对应你的薄弱点 F10(TTL 命令检查 Key 过期状态)。
|
||||
|
||||
## 一、排查流程图
|
||||
|
||||
```mermaid
|
||||
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:定位增长来源
|
||||
|
||||
```bash
|
||||
# 查看所有数据库的大小
|
||||
INFO keyspace
|
||||
→ db0:keys=12345,expires=12000,avg_ttl=3600000
|
||||
```
|
||||
|
||||
输出解析:
|
||||
| 字段 | 含义 |
|
||||
|------|------|
|
||||
| `keys` | 该数据库中 Key 的总数 |
|
||||
| `expires` | 设置了过期时间的 Key 数量 |
|
||||
| `avg_ttl` | 这些 Key 的平均剩余生存时间(毫秒) |
|
||||
|
||||
如果 `keys` 在持续增长而 `expires` 不变 → 有大量未设置过期的 Key
|
||||
|
||||
### Step 2:查找大 Key
|
||||
|
||||
```bash
|
||||
# 方式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 是否已经过期**。
|
||||
|
||||
```bash
|
||||
# 检查单个 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
|
||||
|
||||
```go
|
||||
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` |
|
||||
|
||||
## 四、预防性措施
|
||||
|
||||
```mermaid
|
||||
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
|
||||
```
|
||||
|
||||
### 推荐配置
|
||||
|
||||
```conf
|
||||
# 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`(同步删除),避免阻塞主线程:
|
||||
> ```bash
|
||||
> # 同步删除(阻塞)
|
||||
> DEL rate:sliding:large_key
|
||||
>
|
||||
> # 异步删除(不阻塞)
|
||||
> UNLINK rate:sliding:large_key
|
||||
> ```
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[分布式限流]] — 排错指南中的内存问题章节
|
||||
- [[僵尸Key防护]] — 针对僵尸 Key 的具体防护措施
|
||||
- [[SCAN命令]] — SCAN 是排查内存问题的必备工具
|
||||
Reference in New Issue
Block a user