Skip to content

第 13 章 性能优化、监控与排障

学习目标:掌握一套自上而下、有据可依的 Redis 排障方法论,能熟练使用 SLOWLOG / INFO / MEMORY 三大利器定位问题,看懂 Prometheus + Grafana 监控大盘,并能背出 13 条生产环境优化清单。看完之后能从容应对「线上 Redis 突然变慢」「内存涨疯了」「QPS 上不去」这类紧急工单。


13.0 概览:从「玄学排障」到「方法论排障」

很多同学碰到「Redis 慢」第一反应就是「重启大法」「加内存」「换机器」——这是典型的没有方法论。本章建立一套生产可落地的「望闻问切」流程:

┌───────────────────────────────────────────────────────────────────┐
│                  Redis 排障 4 层模型(自上而下)                    │
├───────────────────────────────────────────────────────────────────┤
│                                                                     │
│   Layer 1:客户端     ← 连接池?序列化?业务侧批量?                │
│        │                                                            │
│        ▼                                                            │
│   Layer 2:网络       ← RTT?带宽?TCP 重传?                        │
│        │                                                            │
│        ▼                                                            │
│   Layer 3:Redis 主线程 ← 慢命令?大 Key?fork?AOF fsync?           │
│        │                                                            │
│        ▼                                                            │
│   Layer 4:操作系统    ← CPU 满?SWAP?THP?磁盘 IO?                │
│                                                                     │
└───────────────────────────────────────────────────────────────────┘

   📌 排障原则:先定位 Layer,再下钻到具体指标

🩺 生活类比:医生看病不会一上来就开刀,而是先「望」(看气色 = 业务监控)→「闻」(听呼吸 = 慢日志)→「问」(问症状 = INFO)→「切」(号脉 = 火焰图)。Redis 排障也一样,先收集证据再下结论

本章配套实战 4 个脚本和 1 个交互演示页面,建议边看边跑。


13.1 排障的整体方法论:自上而下 4 层定位

13.1.1 Layer 1:客户端——80% 的问题其实在自己代码里

很多「Redis 慢」其实是客户端用法错误

┌────────────────────────┬─────────────────────────────────────┐
│  现象                   │  根因                                │
├────────────────────────┼─────────────────────────────────────┤
│ 一个简单 GET 耗时 50ms   │ 没用连接池,每次都 TCP 三次握手       │
│ 批量插入 1w 条要 30s     │ 没用 Pipeline,1w 次 RTT             │
│ JSON 序列化耗时          │ 客户端 CPU 瓶颈,不是 Redis 慢        │
│ 连接数报警               │ 用完没还给池,连接泄漏                │
└────────────────────────┴─────────────────────────────────────┘

第一步永远是问自己 4 个问题

  1. 用连接池了吗?
  2. 用 Pipeline / MGET 了吗?
  3. 客户端 CPU 是不是打满了?(序列化、解压都是 CPU)
  4. 业务代码里是不是有循环里的 for k in keys: r.get(k)

13.1.2 Layer 2:网络——RTT 是物理上限

Redis 单次往返时延 = 网络 RTT + Redis 处理时间。同机房 0.1~0.5ms,跨机房可达 5~30ms。Pipeline 的本质就是省 RTT

排障命令:

bash
# 测延迟
redis-cli --latency -h 127.0.0.1
redis-cli --latency-history -h 127.0.0.1   # 历史模式
redis-cli --latency-dist -h 127.0.0.1      # 分布图
redis-cli --intrinsic-latency 5            # 测系统本身的内核调度延迟

# 测网络
ping <redis-host>
mtr <redis-host>                            # 排查中间路由跳点
ss -ti                                      # 看 TCP 重传/RTT

💡 一个常被忽略的细节:同 VPC 不同可用区之间也有 ms 级延迟,对延迟敏感的业务要把客户端和 Redis 部署到同 AZ。

13.1.3 Layer 3:Redis 主线程——本章重点

Redis 是单线程处理命令的(Redis 6 之后 IO 多线程,但命令执行依然单线程)。意味着:任何一条慢命令都会阻塞所有其他请求

                Redis 主线程时间轴
   ┌────┬──────────────────┬────┬────┐
   │GET │  KEYS *  (200ms) │SET │GET │
   └────┴──────────────────┴────┴────┘

        这 200ms 内所有客户端排队等

排查 Layer 3 的核心 4 件套:

  • SLOWLOG :慢命令记录器(13.2)
  • INFO:全维度指标快照(13.6)
  • MONITOR:命令录像机(开发可用,生产严禁)
  • LATENCY:延迟事件历史

13.1.4 Layer 4:操作系统——基础设施异常

Redis 也是 Linux 进程,OS 的问题直接传导:

┌──────────────────┬──────────────────────────────────────┐
│  OS 异常           │  对 Redis 的影响                      │
├──────────────────┼──────────────────────────────────────┤
│ SWAP 触发          │ 任意命令延迟飙升 100x                │
│ THP(透明大页)    │ fork 时 COW 复制变慢                  │
│ vm.overcommit_memory=0 │ fork 时报 OOM 直接挂              │
│ CPU 软中断飙高     │ 网络包处理跟不上                      │
│ 磁盘 await 高      │ AOF fsync 阻塞主线程                 │
└──────────────────┴──────────────────────────────────────┘

排查命令:topvmstat 1iostat -x 1pidstat -d 1cat /sys/kernel/mm/transparent_hugepage/enabled


13.2 慢查询日志 SLOWLOG

Redis 自带的「慢命令记录器」,类似 MySQL 的 slow_log。线上必开

13.2.1 配置

bash
# 配置文件 redis.conf
slowlog-log-slower-than 10000     # 单位微秒,默认 10000us = 10ms
slowlog-max-len         128       # 最多保留 128 条(环形缓冲)

# 在线修改(不用重启)
CONFIG SET slowlog-log-slower-than 1000   # 改成 1ms
CONFIG SET slowlog-max-len 1024
CONFIG REWRITE                            # 持久化到配置文件

💡 生产建议:阈值设为 1ms (1000us)——10ms 太宽松,绝大多数慢命令会漏掉;保留条数设为 1024,便于回溯。

13.2.2 命令

bash
SLOWLOG GET             # 取全部
SLOWLOG GET 10          # 取最近 10 条
SLOWLOG LEN             # 记录条数
SLOWLOG RESET           # 清空
SLOWLOG HELP            # 帮助

13.2.3 字段解读

127.0.0.1:6379> SLOWLOG GET 1
1) 1) (integer) 14                          ← id(自增)
   2) (integer) 1713244800                  ← UNIX 时间戳
   3) (integer) 213000                      ← 执行耗时(微秒)= 213ms
   4) 1) "KEYS"                             ← 命令
      2) "*"
   5) "127.0.0.1:51234"                     ← 客户端 ip:port(4.0+)
   6) "service-payment"                     ← 客户端 name(CLIENT SETNAME)

6 个字段记忆口诀:「ID-时间-耗时-命令-哪台机-哪个服务」。重点关注第 5、6 字段——能直接定位到肇事业务方。

13.2.4 实战流程

1. 报警触发 → SSH 到机器
2. redis-cli SLOWLOG GET 50    → 找耗时 Top 的命令
3. 看命令参数 → 定位「大 Key」「KEYS *」「LRANGE 大 List」等
4. 看客户端 IP → 找出业务方
5. 通知业务方修改(用 SCAN/HSCAN/分批/Pipeline 替代)
6. SLOWLOG RESET → 验证修改后是否还有

13.3 大 Key / 热 Key 排查

13.3.1 大 Key 的危害(回顾)

第 4 章讲过,这里只回顾结论:

  • 网络阻塞:单 Key MB 级别,传输打满网卡
  • 主线程卡顿:DEL 一个 1GB 的 Hash 能阻塞 1s+
  • 集群迁移失败:CLUSTER 槽位迁移单 Key 不可拆分
  • 持久化抖动:RDB / AOF 写入大 Key 时慢

13.3.2 大 Key 排查工具链

① redis-cli --bigkeys

bash
redis-cli -h <host> --bigkeys
# 实时扫描,对每种类型输出最大的 Key

输出示例:

[00.00%] Biggest hash   found so far 'user:profile:1001' with 50000 fields
[03.21%] Biggest list   found so far 'queue:order' with 120000 items
...
-------- summary -------
Biggest string found 'session:xx'    has 8294400 bytes
Biggest list   found 'queue:order'   has 120000 items
Biggest hash   found 'user:profile:1001' has 50000 fields

⚠️ --bigkeys采样性质的(用 SCAN 遍历),结果是「最大的」之一,不是绝对最大。 ⚠️ 它对每种类型的「大」定义不同:String 看字节数,其他看元素数,不能直接比内存

② redis-cli --memkeys(5.0+)

bash
redis-cli --memkeys           # 按内存占用算
redis-cli --memkeys-samples 0 # 全量扫描

--bigkeys 更准——直接调 MEMORY USAGE,所有类型统一按字节数比较。

③ rdb-tools 离线分析

bash
pip install rdbtools
rdb -c memory dump.rdb --bytes 10240 -f bigkey.csv
# 找出占用 > 10KB 的 Key,输出 CSV

强烈推荐生产用法:每天夜里下载 RDB 离线分析,完全零开销

④ 业务侧拦截(最优解)

在客户端 SDK 里加拦截器:

python
def safe_set(r, key, value, max_size=10240):
    if len(value) > max_size:
        raise ValueError(f"big key denied: {key} size={len(value)}")
    r.set(key, value)

「事后排查」不如「事前拦截」——架构组应该在公司基础库里默认禁止 1MB 以上的写入。

13.3.3 热 Key 排查

热 Key(Hot Key):访问频次极高的 Key,会让单分片 CPU 打满。

工具优缺点
redis-cli --hotkeys需要 maxmemory-policy=allkeys-lfu;轻量但需配置
MONITOR简单粗暴;生产严禁(QPS 减半)
Facebook redis-faina解析 MONITOR 输出做统计,开发利器
客户端打点在 SDK 里上报每次 GET 的 key 哈希;最稳健
抓包分析tcpdump + 自研解析;离线
Proxy 层统计有 Codis / Twemproxy 的可以直接出报表

redis-cli --hotkeys 实战

bash
# 先改策略
redis-cli CONFIG SET maxmemory-policy allkeys-lfu
# 等几分钟让 LFU 计数器累积
redis-cli --hotkeys

13.3.4 治理方案

┌───────────────────┬──────────────────────────────────────┐
│  方案               │  适用场景                              │
├───────────────────┼──────────────────────────────────────┤
│ 拆分大 Key          │ user:1 → user:1:base / user:1:settings │
│ 本地缓存(多级缓存) │ 读多写少的热 Key(首页配置)             │
│ 读写分离             │ 主写从读;适合写少                      │
│ 多副本散列           │ key_1 / key_2 / ... / key_N,随机访问 │
│ 客户端分片           │ 大 Hash 哈希取模到 N 个 Hash           │
│ 改用 ZSet/SortedSet │ 需要排行榜场景拆分                      │
└───────────────────┴──────────────────────────────────────┘

13.4 BIO 后台线程

13.4.1 为什么需要后台线程

Redis 4.0 之前是「纯」单线程,所有 IO 都同步做。结果:

  • AOF fsync 调用 fsync(fd) 时阻塞主线程
  • DEL 大 Key 时释放内存阻塞主线程
  • 关闭被动连接的 fd 时也可能阻塞

Redis 4.0 引入 BIO(Background IO)线程,把这些「可异步」的工作扔到后台。

┌─────────────────────────────────────────────────────────┐
│                    Redis 4.0+ 线程模型                   │
├─────────────────────────────────────────────────────────┤
│                                                            │
│   ┌──────────────┐                                         │
│   │ 主线程        │ ← 命令执行、IO 多路复用                 │
│   └──────┬───────┘                                         │
│          │                                                  │
│          │ 投递任务                                         │
│          ▼                                                  │
│   ┌──────────────────────────────────────────┐              │
│   │   BIO 任务队列(按类型分 3 个队列)        │              │
│   ├──────────────────────────────────────────┤              │
│   │   ① BIO_CLOSE_FILE  ← 关闭被动 fd        │              │
│   │   ② BIO_AOF_FSYNC   ← AOF fsync         │              │
│   │   ③ BIO_LAZY_FREE   ← UNLINK / 异步释放 │              │
│   └────┬──────────┬──────────┬───────────────┘              │
│        ▼          ▼          ▼                              │
│   [bio thread] [bio thread] [bio thread]                    │
│                                                              │
└─────────────────────────────────────────────────────────┘
bash
DEL bigkey       # 同步删,可能阻塞主线程几百 ms
UNLINK bigkey    # 异步删!主线程只解链,真正释放在 BIO 线程

老 Redis 想异步删 Hash/Set 元素,可以打开「lazy free」:

bash
CONFIG SET lazyfree-lazy-eviction      yes   # 内存淘汰用异步
CONFIG SET lazyfree-lazy-expire        yes   # 过期清理用异步
CONFIG SET lazyfree-lazy-server-del    yes   # 隐式删除(如 RENAME 覆盖)
CONFIG SET lazyfree-lazy-user-del      yes   # 让 DEL 也走异步

13.4.3 怎么观察后台线程是否积压

bash
INFO persistence | grep -E "aof_(pending|delayed_fsync)"
# aof_pending_bio_fsync       ← 待 fsync 的请求数(积压则警告)
# aof_delayed_fsync           ← 因 fsync 慢被延后的次数

INFO stats | grep lazyfree
# lazyfree_pending_objects    ← 待异步释放的对象数

INFO persistence | grep latest_fork_usec
# latest_fork_usec            ← 上次 fork 耗时(微秒)

⚠️ 如果 aof_delayed_fsync 持续增长,说明磁盘 IO 跟不上——该换 SSD 了


13.5 阻塞主线程的「定时炸弹」

13.5.1 慢命令清单(背下来)

┌──────────────────────┬─────────┬───────────────────────────┐
│  危险命令              │ 复杂度   │ 替代方案                    │
├──────────────────────┼─────────┼───────────────────────────┤
│ KEYS pattern         │ O(N)    │ SCAN(游标式遍历)          │
│ HGETALL big_hash     │ O(N)    │ HSCAN + 限制单次返回数      │
│ SMEMBERS big_set     │ O(N)    │ SSCAN                      │
│ SORT big_list        │ O(N+M log M) │ 业务侧排序             │
│ SUNIONSTORE big_set  │ O(N)    │ 拆批处理                    │
│ ZRANGE 0 -1          │ O(N+M)  │ 分页 ZRANGE 0 99           │
│ FLUSHALL / FLUSHDB   │ O(N)    │ FLUSHDB ASYNC              │
│ LRANGE 0 -1 big_list │ O(N+M)  │ 分页                        │
│ EVAL 长 Lua 脚本      │ 取决于脚本 │ 拆分逻辑 / 设置超时         │
│ DEBUG SLEEP n        │ O(N)    │ 仅用于测试                   │
└──────────────────────┴─────────┴───────────────────────────┘

13.5.2 用 DEBUG SLEEP 测试主线程被阻塞的影响

开两个终端:

bash
# 终端 A:模拟慢命令
redis-cli DEBUG SLEEP 5

# 终端 B:尝试任意操作
redis-cli SET foo bar
# 会卡 5 秒才返回 OK!

🧨 这就是单线程的代价——一个慢命令把整个 Redis 「冻住」。

13.5.3 SCAN 系列:渐进式遍历的正确姿势

bash
SCAN 0 MATCH user:* COUNT 100
# 返回:[next_cursor, [k1, k2, ...]]
# 持续调用直到 next_cursor = 0

SCAN 的特性

  • 游标式,不会一次返回所有 Key
  • 单次 O(1);总开销 O(N),但分散到多次调用
  • 可能返回重复元素,业务侧需自行去重
  • COUNT 是「建议值」,不是精确值

Python 示例:

python
for k in r.scan_iter(match="user:*", count=200):
    process(k)   # 一次只阻塞 200 个 key 的扫描时间

SCAN 家族SCAN(顶层 Key)/ HSCAN(Hash)/ SSCAN(Set)/ ZSCAN(ZSet)。


13.6 INFO 命令详解(重点)

INFO 是 Redis 的「全身体检报告」,所有监控系统底层都靠它。

13.6.1 9 大 section

bash
INFO server          # 进程基本信息
INFO clients         # 客户端连接
INFO memory          # 内存
INFO persistence     # RDB / AOF
INFO stats           # 全局统计
INFO replication     # 主从复制
INFO cpu             # CPU 占用
INFO commandstats    # 每个命令的累计耗时
INFO cluster         # 集群信息
INFO keyspace        # 每个 db 的 key 数
INFO                 # 全部
INFO all             # 全部 + 包含默认隐藏的 commandstats / latencystats

13.6.2 关键监控指标(必背)

内存类

used_memory                ← Redis 申请的内存(包含碎片)
used_memory_rss            ← OS 视角看 Redis 实际占用(RSS)
used_memory_peak           ← 历史峰值
mem_fragmentation_ratio    ← 碎片率 = rss / used_memory
maxmemory                  ← 上限

碎片率解读

> 1.5     严重碎片,考虑重启或 MEMORY PURGE
1.0~1.5   正常
< 1.0     部分内存被换到 SWAP(最危险!)

💡 Redis 4.0+ 支持主动碎片整理CONFIG SET activedefrag yes

吞吐与连接

instantaneous_ops_per_sec        ← 当前 QPS(实时)
instantaneous_input_kbps         ← 当前入向带宽(KB/s)
instantaneous_output_kbps        ← 当前出向带宽
total_connections_received       ← 累计连接数
connected_clients                ← 当前在线客户端
rejected_connections             ← 因超 maxclients 被拒绝的连接数(非 0 报警)
blocked_clients                  ← 阻塞客户端数(BLPOP / WAIT 等)

命中率

keyspace_hits                    ← 命中次数
keyspace_misses                  ← 未命中次数
命中率 = hits / (hits + misses)   ← 应 ≥ 95%
python
hit_ratio = hits / (hits + misses) if (hits + misses) else 0

过期 / 淘汰

expired_keys      ← 累计被过期清理的 key 数
evicted_keys      ← 累计被淘汰的 key 数(非 0 说明 maxmemory 不够)

Fork 相关(最容易被忽略)

latest_fork_usec  ← 上次 fork 耗时(微秒),> 1_000_000 说明 1s+
rdb_last_save_time
rdb_changes_since_last_save
aof_current_size
aof_base_size

⚠️ fork > 500ms 就要报警,> 1s 必须查根因(可能是大内存 + THP 没关)。

CPU

used_cpu_sys      ← 内核态 CPU 时间
used_cpu_user     ← 用户态 CPU 时间

将相邻两次采样做差,可以算出 CPU 占用百分比。

13.6.3 INFO 的查看小技巧

bash
# 只看一行
redis-cli INFO memory | grep used_memory_human

# 用 redis-cli --stat 看一个滚动统计
redis-cli --stat
# ------- data ------ --------------------- load ------------ - child -
# keys mem clients blocked requests connections
# 18    1.05M  1     0     157 (+0)  3

13.7 监控系统接入

13.7.1 Prometheus + redis_exporter

bash
# 启动 redis_exporter(开源项目 oliver006/redis_exporter)
docker run -d -p 9121:9121 \
  -e REDIS_ADDR=redis://your-host:6379 \
  -e REDIS_PASSWORD=xxx \
  oliver006/redis_exporter

# Prometheus scrape config
scrape_configs:
  - job_name: redis
    static_configs:
      - targets: ['redis-exporter:9121']

redis_exporter 会自动把 INFO 的所有指标转成 Prometheus metrics(命名以 redis_ 开头)。

13.7.2 Grafana Dashboard

开源大盘 ID(直接在 Grafana 导入):

  • 17507 — 综合 Redis Dashboard(推荐入门)
  • 763 — 老牌 Redis Dashboard
  • 11835 — Redis Cluster Dashboard
  • 15740 — Redis Sentinel Dashboard

13.7.3 告警规则示例(Prometheus rule)

yaml
groups:
- name: redis
  rules:
  - alert: RedisMemoryHigh
    expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.8
    for: 5m
    annotations: { summary: "Redis 内存使用率 > 80%" }

  - alert: RedisHitRatioLow
    expr: rate(redis_keyspace_hits_total[5m]) / 
          (rate(redis_keyspace_hits_total[5m]) + rate(redis_keyspace_misses_total[5m])) < 0.9
    for: 10m
    annotations: { summary: "Redis 命中率 < 90%" }

  - alert: RedisForkSlow
    expr: redis_latest_fork_usec > 500000
    for: 1m
    annotations: { summary: "Redis fork 耗时 > 500ms" }

  - alert: RedisRejectedConn
    expr: increase(redis_rejected_connections_total[5m]) > 0
    annotations: { summary: "有连接被拒绝(maxclients 不够)" }

  - alert: RedisEvictionRising
    expr: rate(redis_evicted_keys_total[5m]) > 100
    annotations: { summary: "Redis 淘汰速率过高(内存不够)" }

🎯 告警分级:内存 / fork / 命中率 → P1;连接拒绝 / 淘汰 → P2;QPS 突变 → P3。


13.8 性能优化清单(13 条)

这 13 条是「生产经过验证」的最佳实践,全部命中能让 P99 延迟下降一个量级

┌───┬─────────────────────────────┬─────────────────────────┐
│ # │ 优化项                        │ 预期收益                  │
├───┼─────────────────────────────┼─────────────────────────┤
│ 1 │ 客户端用连接池                │ 单次 GET 从 5ms → 0.5ms   │
│ 2 │ 用 Pipeline / MGET / HMGET    │ 批量场景 10x ~ 100x       │
│ 3 │ Lua 下沉「读-改-写」复合逻辑  │ 减少 RTT、保证原子        │
│ 4 │ 设置合理 TTL,避免内存堆积    │ 内存稳定 / 命中率提升     │
│ 5 │ 用 listpack 友好的小数据结构  │ 内存节省 50%+,速度更快   │
│ 6 │ 控制 Key 长度                 │ 节省内存 + 哈希更均匀     │
│ 7 │ 禁用 KEYS / FLUSHALL          │ 杜绝主线程长时间阻塞      │
│ 8 │ 用 UNLINK 替代 DEL            │ 大 Key 删除不阻塞         │
│ 9 │ 拆分大 Key                    │ 网络 / 集群迁移友好        │
│10 │ 业务隔离(不同业务不同实例)   │ 避免互相影响               │
│11 │ 错峰主从全量同步              │ fork 不冲击业务高峰        │
│12 │ maxmemory 留 30% 给 fork      │ 不 OOM、不 SWAP            │
│13 │ Linux 调优                    │ overcommit / THP / SWAP   │
└───┴─────────────────────────────┴─────────────────────────┘

1. 客户端用连接池

python
import redis
pool = redis.ConnectionPool(host="...", max_connections=100)
r = redis.Redis(connection_pool=pool)

2. Pipeline / MGET / HMGET

python
pipe = r.pipeline(transaction=False)
for k in keys:
    pipe.get(k)
results = pipe.execute()      # 一次 RTT 拿全部

3. Lua 下沉

lua
-- check_and_decr.lua
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock and stock > 0 then
  redis.call('DECR', KEYS[1])
  return 1
end
return 0

4. TTL:永远要给缓存设过期时间

bash
SET cache:user:1 "..." EX 3600
HSET ... + EXPIRE   # Hash 的 TTL 是整个 Key 级别的

5. listpack 友好

让 Hash / Set / ZSet 的元素数 < 128 且元素长度 < 64 字节,能保持紧凑编码,内存节省 50% 以上

6. 控制 Key 长度

不要写 user:profile:detail:settings:notification:email:enabled:1001, 推荐 up:nfn:e:1001 之类短代号。百万 Key 累积下来差几个 GB

7. 禁用危险命令

bash
# redis.conf 重命名为空字符串 = 彻底禁用
rename-command KEYS     ""
rename-command FLUSHALL ""
rename-command FLUSHDB  ""
rename-command CONFIG   "CONFIG_a8b3f2"   # 改名而非禁用
rename-command DEBUG    ""

业务代码里所有 DEL 都改成 UNLINK(除非业务依赖 DEL 的同步语义)。

9. 拆分大 Key

大 Hash 哈希分片:

python
def field_to_shard(field, n=16):
    return crc32(field) % n

key = f"user:profile:1001:{field_to_shard(field)}"

10. 业务隔离

坚决不要把业务 A 和业务 B 塞同一个 Redis 实例。一个业务出大 Key 把整个实例阻塞,全公司业务连坐。

11. 错峰主从同步

bash
CONFIG SET save ""             # 关闭 RDB 自动触发
# 凌晨 3 点 cron 触发 BGSAVE

12. maxmemory 留 30% 给 fork

机器 32GB → maxmemory 设 22GB(不是 30GB)。fork 时 COW 最坏会再用一份内存

13. Linux 调优清单

bash
# 必做
echo 1 > /proc/sys/vm/overcommit_memory      # fork 不报 OOM
echo never > /sys/kernel/mm/transparent_hugepage/enabled    # 关闭 THP
sysctl -w net.core.somaxconn=65535            # TCP backlog
sysctl -w vm.swappiness=1                     # 不到迫不得已不 swap

# 持久化到 /etc/sysctl.conf
echo "vm.overcommit_memory=1" >> /etc/sysctl.conf
echo "net.core.somaxconn=65535" >> /etc/sysctl.conf
echo "vm.swappiness=1" >> /etc/sysctl.conf
sysctl -p

13.9 实操:跑一遍配套代码

实战代码见 13_optimization/code/

  • 01_slowlog_analyzer.py:触发 DEBUG SLEEP / LRANGE 大 List,再 SLOWLOG GET 解析展示
  • 02_info_dashboard.py:解析 INFO 输出,按 section 美化打印 + 健康度评分
  • 03_memory_analyzer.py:MEMORY STATS / USAGE / OBJECT IDLETIME 综合分析
  • 04_optimization_checklist.py:自动检查 Redis 配置 vs 13 条最佳实践

浏览器演示见 13_optimization/demo.html

  • INFO 监控大盘:模拟实例 + 进度条/仪表盘 + 一键模拟内存暴涨/fork 慢
  • 慢查询排查实验室:点命令按钮看模拟耗时 + SLOWLOG 记录 + 优化建议
  • 13 条优化清单 self-check:勾选当前配置自动打分 + Top 3 优化建议

13.10 本章小结

┌───────────────────────────────────────────────────────────┐
│                    本章核心要点                              │
├───────────────────────────────────────────────────────────┤
│                                                              │
│  ① 排障 4 层模型:客户端 → 网络 → 主线程 → 操作系统          │
│                                                              │
│  ② SLOWLOG 必开,阈值设 1ms,6 个字段重点看 IP & 命令         │
│                                                              │
│  ③ 大 Key 排查:--bigkeys / --memkeys / rdbtools / 业务拦截 │
│                                                              │
│  ④ 热 Key 排查:--hotkeys / 客户端打点 / 多副本散列治理       │
│                                                              │
│  ⑤ BIO 后台线程:异步 fsync / lazy free / UNLINK              │
│                                                              │
│  ⑥ 慢命令清单:KEYS / HGETALL / SMEMBERS 用 SCAN 替代         │
│                                                              │
│  ⑦ INFO 6 个核心指标:碎片率 / QPS / 命中率 / 拒绝连接 /      │
│                       fork 耗时 / 淘汰速率                    │
│                                                              │
│  ⑧ 监控选 Prometheus + redis_exporter + Grafana 17507       │
│                                                              │
│  ⑨ 13 条优化清单:连接池 / Pipeline / Lua / TTL / listpack /   │
│      短 Key / 禁危险命令 / UNLINK / 拆 Key / 隔离 /           │
│      错峰同步 / 留 30% fork / Linux 调优                     │
│                                                              │
└───────────────────────────────────────────────────────────┘

13.11 面试高频题

Q1:怎么定位 Redis 变慢?给出排查思路

考察点:是否有方法论,而不是「重启大法」。

标准答案——按 4 层模型自上而下:

  1. 客户端层:先看业务监控 P99 延迟、RT 分布;看是不是只有某个业务慢;查代码有没有 N+1(循环里调 GET)、有没有用连接池、有没有 Pipeline。
  2. 网络层redis-cli --latency 测延迟;ping/mtr 看 RTT;ss -ti 看 TCP 重传率。
  3. Redis 主线程层
    • SLOWLOG GET 50 找慢命令
    • INFO commandstats 看哪个命令累计耗时高
    • INFO memory 看碎片率、used_memory_rss 是否异常
    • INFO persistencelatest_fork_usecaof_delayed_fsync
    • CLIENT LIST 看是否有阻塞客户端
    • redis-cli --bigkeys / --hotkeys 排查大热 Key
  4. OS 层top 看 CPU;vmstat 1 看 SWAP;iostat -x 1 看磁盘 await;查 THP 是否关闭。

加分项:提到 LATENCY DOCTOR 命令(Redis 内置的「医生」会给出排查建议)。


Q2:SLOWLOG 怎么用?看哪些字段?

考察点:实战经验。

标准答案

配置

  • slowlog-log-slower-than:阈值(微秒),生产建议 1000us = 1ms
  • slowlog-max-len:环形缓冲条数,建议 1024
  • 可在线 CONFIG SET 修改

命令

  • SLOWLOG GET [N]:取最近 N 条
  • SLOWLOG LEN:当前条数
  • SLOWLOG RESET:清空

字段(共 6 个)

  1. id(自增)
  2. UNIX 时间戳
  3. 执行耗时(微秒)—— 这个数字越大越严重
  4. 命令及参数 —— 看是不是 KEYS / HGETALL / LRANGE 等
  5. 客户端 ip:port —— 直接定位肇事方
  6. 客户端 name(CLIENT SETNAME 设置的)—— 配合 CMDB 找业务

加分项:提到 SLOWLOG 是纯内存的环形缓冲,不会持久化,重启就丢;要长期保留需要监控系统定期采集。


Q3:内存碎片率多少算正常?怎么处理?

考察点:内存底层理解。

标准答案

指标定义mem_fragmentation_ratio = used_memory_rss / used_memory

判断

< 1.0    SWAP 触发!RSS < 内存 = 部分被换到磁盘,必须立即处理
1.0~1.5  正常范围
> 1.5    严重碎片,需要处理

碎片产生原因:jemalloc 按固定 size class 分配(8B、16B、32B、64B...),频繁 SET/DEL 不同大小的 Key 会留下空洞。

处理方式

  1. 主动碎片整理(推荐):Redis 4.0+
    CONFIG SET activedefrag yes
    CONFIG SET active-defrag-ignore-bytes 100mb
    CONFIG SET active-defrag-threshold-lower 10
    CONFIG SET active-defrag-threshold-upper 100
  2. 手动MEMORY PURGE(仅释放 jemalloc 内部碎片,不一定全释放)。
  3. 重启大法:从节点先重启,再切换主从。

加分项:解释 jemalloc 的 size class 分级、为什么 SWAP 触发会让 Redis 慢 100x(内存→磁盘性能差距)。


Q4:Redis 为什么要 fork?fork 耗时长的原因?

考察点:持久化 + Linux 内存模型。

标准答案

为什么要 fork:RDB / AOF 重写需要一份「数据快照」。直接遍历主进程数据会阻塞主线程;用 fork() 创建子进程,利用 COW(Copy On Write)让子进程「秒级」拥有快照,主进程继续服务。

fork 耗时的原因

  1. fork 不是真的复制内存,但要复制页表
  2. 页表大小约为 内存大小 / 4096 * 8 字节(每页 4KB,每个 PTE 8 字节)。
  3. 32GB Redis 实例的页表约 64MB,复制页表耗时约 几百 ms 到 1s+

优化

  1. 关闭 THP(透明大页):THP 让 PTE 变成 2MB 一个,节省 PTE 数量;但 THP 触发 COW 时是 2MB 一次复制(vs 4KB 一次),写少量数据会放大 IO 500x。所以必须关闭
    bash
    echo never > /sys/kernel/mm/transparent_hugepage/enabled
  2. 不要把实例搞太大:单实例建议 < 10GB。
  3. vm.overcommit_memory=1:fork 时 Linux 默认会检查「物理内存够不够再复制一遍」,不够直接 OOM 让 fork 失败。开 overcommit 之后允许 fork 成功,COW 时再实际分配。
  4. 错峰:避开业务高峰再 BGSAVE。
  5. 监控 latest_fork_usec:> 500ms 就报警。

加分项:提到「fork 不阻塞主线程的网络 IO,但调用 fork() 本身那一瞬间是阻塞的」——这就是 fork 慢会导致延迟尖刺的原因。


Q5:哪些命令会阻塞主线程?怎么避免?

考察点:单线程模型 + 慢命令清单。

标准答案

典型阻塞命令

命令复杂度替代
KEYS patternO(N)SCAN
HGETALL bigO(N)HSCAN + 分批
SMEMBERS bigO(N)SSCAN
LRANGE 0 -1O(N+M)分页 LRANGE
SORT bigO(N+M log M)业务侧排序
FLUSHALL/FLUSHDBO(N)FLUSHDB ASYNC
DEL big_keyO(N)UNLINK
EVAL 长 Lua视脚本拆分 / 设超时
DEBUG SLEEP nO(N)测试用
大集合的 SUNIONSTORE/SINTERSTOREO(N)拆分

避免方式

  1. SDK 拦截层禁用危险命令
  2. 大 Key 拆分 + 本地缓存
  3. 用 SCAN 系列代替遍历命令
  4. UNLINK 代替 DEL
  5. Lua 脚本要带超时(script-timeout
  6. 重要命令在服务端 rename-command 重命名为空字符串

加分项:提 Redis 6 的 IO 多线程只解决了网络 IO 多线程,命令执行依然单线程;并提到 latency-monitor-threshold 配置,可以让 Redis 自己记录延迟事件(LATENCY HISTORY / LATENCY DOCTOR)。


Q6:一个 8 核 32G 的 Redis 单机最多能扛多少 QPS?

考察点:对 Redis 性能有量级感。

标准答案

理论上限

  • 简单 GET/SET:单实例 8~10w QPS(Redis 单线程主要受 CPU 单核限制)
  • 用 IO 多线程(Redis 6.0+,io-threads 4)可达 15~20w QPS
  • 网络满千兆(125MB/s),按平均 100B 一次响应:125w QPS 是网卡上限
  • 实测:Redis 官方 benchmark 在主流硬件上 GET 可到 10w QPS / 单线程

32GB 内存可存数据量

  • 单 Key + Value 平均 100B、含 redisObject 等开销约 150B
  • 可存约 2 亿条简单 KV
  • 但真实业务里 Hash / List 等结构开销更大,按 1KB/条算,约 3000w 条

实际生产经验

  • 稳定 QPS 不要超过理论上限的 60%:8~10w 留余量到 5w
  • 8 核机器:1 核给主线程 + 4 核给 IO 线程 + 剩余给系统/AOF/网络
  • 维度估算:8 核 32G 机器 ≈ 5~10w QPS / 30~50GB 内存利用率上限

加分项

  • 提到「QPS 不止看绝对数字,还要看命令复杂度」:MGET 100 个 key 算 1 个 QPS 还是 100 个?
  • 提到「单实例瓶颈在单核」,所以集群水平扩展是高 QPS 的唯一答案
  • 真实场景下要结合 INFO commandstats 看实际「命令耗时累计」反推可承载 QPS

📌 下一章预告:第 14 章我们用前面 13 章的所有知识,手撸一个完整的实战项目——可能是秒杀系统、短链服务或 Feed 流,把 String/ZSet/Lua/Pipeline/缓存设计/集群/监控全部串起来,让你毕业即「Redis 中级工程师」。

🎬 可视化演示

演示加载缓慢或样式异常?点此在新标签页打开 ↗

💻 示例代码

python
"""
Ch13 配套代码 1 / 4 —— 慢查询日志分析器

演示:
  1. 配置 SLOWLOG 阈值
  2. 故意触发几个慢命令(DEBUG SLEEP、LRANGE 大 List)
  3. SLOWLOG GET 解析 6 个字段,按耗时排序输出 Top N
  4. 给出每条慢命令的优化建议
"""

import time
import redis

r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)


def section(title: str) -> None:
    print("\n" + "=" * 64)
    print(title)
    print("=" * 64)


def setup_slowlog(threshold_us: int = 1000, max_len: int = 1024) -> None:
    section(f"Step 1: 配置 SLOWLOG 阈值 = {threshold_us}us,容量 = {max_len}")
    r.config_set("slowlog-log-slower-than", threshold_us)
    r.config_set("slowlog-max-len", max_len)
    r.execute_command("SLOWLOG", "RESET")
    print(f"  当前 SLOWLOG 长度:{r.execute_command('SLOWLOG', 'LEN')}")


def trigger_slow_commands() -> None:
    section("Step 2: 故意触发慢命令(请稍等 ~6 秒)")

    print("  - 模拟 DEBUG SLEEP 0.05(50ms)")
    try:
        r.execute_command("DEBUG", "SLEEP", "0.05")
    except redis.ResponseError as e:
        print(f"    DEBUG 命令被禁用:{e}")

    print("  - 准备一个 5w 元素的 List,再 LRANGE 0 -1")
    big_list = "demo:slowlog:biglist"
    r.delete(big_list)
    pipe = r.pipeline(transaction=False)
    for i in range(50000):
        pipe.rpush(big_list, f"item-{i}")
    pipe.execute()
    r.lrange(big_list, 0, -1)

    print("  - 准备一个 1w 字段的 Hash,再 HGETALL")
    big_hash = "demo:slowlog:bighash"
    r.delete(big_hash)
    mapping = {f"f{i}": f"v{i}" for i in range(10000)}
    r.hset(big_hash, mapping=mapping)
    r.hgetall(big_hash)

    print("  - 模拟 KEYS *(不要在生产用!)")
    r.keys("*")

    r.delete(big_list, big_hash)


SUGGESTION_RULES = [
    ("KEYS",      "✗ 全库扫描,O(N)。改用 SCAN 0 MATCH pattern COUNT 100"),
    ("HGETALL",   "✗ 大 Hash 全量返回。改用 HSCAN 渐进式遍历,或拆分 Hash"),
    ("SMEMBERS",  "✗ 大 Set 全量返回。改用 SSCAN 渐进式遍历"),
    ("LRANGE",    "✗ 大 List 范围读。LRANGE 0 -1 改成分页 LRANGE 0 99"),
    ("SORT",      "✗ 服务端排序复杂度高。建议业务侧排序"),
    ("FLUSHALL",  "✗ 清空全库。生产应禁用,或改为 FLUSHDB ASYNC"),
    ("FLUSHDB",   "✗ 清空当前 db。改为 FLUSHDB ASYNC(4.0+)"),
    ("DEL",       "△ 大 Key DEL 阻塞主线程。改用 UNLINK"),
    ("DEBUG",     "△ DEBUG 命令仅开发环境,生产应 rename-command 禁用"),
    ("SUNIONSTORE", "△ 大集合求并集 O(N)。考虑拆批"),
    ("SINTERSTORE", "△ 大集合求交集 O(N)。考虑拆批"),
    ("EVAL",      "△ Lua 脚本未限时,可能长时间阻塞主线程"),
]


def suggest(cmd: str) -> str:
    upper = cmd.upper()
    for keyword, msg in SUGGESTION_RULES:
        if keyword in upper:
            return msg
    return "(无特定建议,关注耗时是否合理)"


def show_slowlog(top_n: int = 10) -> None:
    section(f"Step 3: SLOWLOG GET —— 取耗时 Top {top_n}")
    raw = r.execute_command("SLOWLOG", "GET", top_n)
    if not raw:
        print("  (SLOWLOG 为空,可能是阈值太高或没有触发)")
        return

    rows = []
    for entry in raw:
        sid       = entry[0]
        ts        = entry[1]
        duration  = entry[2]
        cmd_parts = [str(p) for p in entry[3]]
        client_ip = entry[4] if len(entry) > 4 else "-"
        client_nm = entry[5] if len(entry) > 5 else "-"
        cmd_str   = " ".join(cmd_parts)
        if len(cmd_str) > 60:
            cmd_str = cmd_str[:57] + "..."
        rows.append((sid, ts, duration, cmd_str, client_ip, client_nm))

    rows.sort(key=lambda x: -x[2])

    print(f"\n  {'ID':>4}  {'时间':<19}  {'耗时(us)':>10}  {'命令':<60}  {'客户端':<22}  名字")
    print("  " + "-" * 130)
    for sid, ts, dur, cmd, ip, nm in rows:
        ts_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts))
        print(f"  {sid:>4}  {ts_str:<19}  {dur:>10}  {cmd:<60}  {ip:<22}  {nm}")

    print("\n  ─── 优化建议 ───")
    seen = set()
    for sid, ts, dur, cmd, ip, nm in rows:
        cmd_head = cmd.split(" ", 1)[0]
        if cmd_head in seen:
            continue
        seen.add(cmd_head)
        print(f"  [{cmd_head:>10}] {suggest(cmd)}")


def main() -> None:
    try:
        setup_slowlog(threshold_us=1000)
        trigger_slow_commands()
        show_slowlog(top_n=20)

        section("Step 4: 总结")
        print("  ① SLOWLOG 阈值生产建议设为 1000us(1ms)")
        print("  ② 关注 6 个字段:id / 时间 / 耗时 / 命令 / 客户端 IP / 客户端 name")
        print("  ③ KEYS / HGETALL / LRANGE 0 -1 是最常见的「定时炸弹」")
        print("  ④ 用 SCAN 系列 + UNLINK + Pipeline 改造慢命令")
    except redis.ConnectionError as e:
        print(f"❌ Redis 连接失败:{e}")


if __name__ == "__main__":
    main()
python
"""
Ch13 配套代码 2 / 4 —— INFO 监控大盘 + 健康度评分

演示:
  1. 解析 INFO 输出
  2. 按 9 大 section 美化打印关键指标
  3. 计算健康度评分(满分 100,按 6 大维度扣分)
  4. 输出诊断报告
"""

import redis

r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)


def section(title: str) -> None:
    print("\n" + "=" * 64)
    print(title)
    print("=" * 64)


def fmt_bytes(n: int) -> str:
    if n is None:
        return "-"
    n = float(n)
    for unit in ["B", "KB", "MB", "GB", "TB"]:
        if n < 1024:
            return f"{n:.2f}{unit}"
        n /= 1024
    return f"{n:.2f}PB"


def kv(label: str, value, unit: str = "", color_warn: bool = False) -> None:
    mark = "⚠ " if color_warn else "  "
    print(f"  {mark}{label:<32} {value}{unit}")


def show_server(info: dict) -> None:
    section("Section 1: SERVER 进程信息")
    kv("redis_version",       info.get("redis_version"))
    kv("redis_mode",          info.get("redis_mode"))
    kv("os",                  info.get("os"))
    kv("process_id",          info.get("process_id"))
    kv("tcp_port",            info.get("tcp_port"))
    kv("uptime_in_days",      info.get("uptime_in_days"))


def show_clients(info: dict, score: dict) -> None:
    section("Section 2: CLIENTS 客户端连接")
    connected = int(info.get("connected_clients", 0))
    blocked   = int(info.get("blocked_clients", 0))
    rejected  = int(info.get("rejected_connections", 0) or 0)
    kv("connected_clients",       connected)
    kv("blocked_clients",         blocked, color_warn=blocked > 10)
    kv("rejected_connections",    rejected, color_warn=rejected > 0)
    kv("maxclients",              info.get("maxclients"))
    if rejected > 0:
        score["rejected"] = -15
    if blocked > 10:
        score["blocked"] = -5


def show_memory(info: dict, score: dict) -> None:
    section("Section 3: MEMORY 内存")
    used     = int(info.get("used_memory", 0))
    rss      = int(info.get("used_memory_rss", 0))
    peak     = int(info.get("used_memory_peak", 0))
    maxmem   = int(info.get("maxmemory", 0) or 0)
    frag     = float(info.get("mem_fragmentation_ratio", 1.0))
    used_pct = (used / maxmem * 100) if maxmem else 0

    kv("used_memory",             fmt_bytes(used))
    kv("used_memory_rss (OS看)",  fmt_bytes(rss))
    kv("used_memory_peak",        fmt_bytes(peak))
    kv("maxmemory",               fmt_bytes(maxmem) if maxmem else "无限制(危险!)",
       color_warn=not maxmem)
    kv("内存使用率",              f"{used_pct:.1f}%",
       color_warn=used_pct > 80)
    kv("mem_fragmentation_ratio", f"{frag:.2f}",
       color_warn=frag > 1.5 or frag < 1.0)

    if frag > 1.5:
        score["frag_high"] = -10
    if frag < 1.0:
        score["swap"] = -25
    if used_pct > 80:
        score["mem_high"] = -10
    if not maxmem:
        score["no_maxmem"] = -10


def show_persistence(info: dict, score: dict) -> None:
    section("Section 4: PERSISTENCE 持久化")
    fork_us = int(info.get("latest_fork_usec", 0))
    aof_on  = int(info.get("aof_enabled", 0))
    aof_pending = int(info.get("aof_pending_bio_fsync", 0) or 0)
    aof_delayed = int(info.get("aof_delayed_fsync", 0) or 0)
    rdb_in_progress = int(info.get("rdb_bgsave_in_progress", 0))

    kv("rdb_changes_since_last_save", info.get("rdb_changes_since_last_save"))
    kv("rdb_bgsave_in_progress",      rdb_in_progress)
    kv("latest_fork_usec",            f"{fork_us}us ({fork_us/1000:.1f}ms)",
       color_warn=fork_us > 500000)
    kv("aof_enabled",                 aof_on)
    if aof_on:
        kv("aof_current_size",        fmt_bytes(int(info.get("aof_current_size", 0))))
        kv("aof_pending_bio_fsync",   aof_pending, color_warn=aof_pending > 0)
        kv("aof_delayed_fsync",       aof_delayed, color_warn=aof_delayed > 0)

    if fork_us > 1_000_000:
        score["fork_slow"] = -15
    elif fork_us > 500_000:
        score["fork_slow"] = -5
    if aof_delayed > 0:
        score["aof_delayed"] = -10


def show_stats(info: dict, score: dict) -> None:
    section("Section 5: STATS 全局统计")
    qps   = int(info.get("instantaneous_ops_per_sec", 0))
    in_kb = float(info.get("instantaneous_input_kbps", 0))
    out_kb = float(info.get("instantaneous_output_kbps", 0))
    hits   = int(info.get("keyspace_hits", 0))
    misses = int(info.get("keyspace_misses", 0))
    evict  = int(info.get("evicted_keys", 0))
    expired = int(info.get("expired_keys", 0))
    total = hits + misses
    hit_ratio = (hits / total * 100) if total else 0

    kv("instantaneous_ops_per_sec",   f"{qps} QPS")
    kv("instantaneous_input_kbps",    f"{in_kb:.2f} KB/s")
    kv("instantaneous_output_kbps",   f"{out_kb:.2f} KB/s")
    kv("keyspace_hits",               hits)
    kv("keyspace_misses",             misses)
    kv("命中率",                       f"{hit_ratio:.2f}%",
       color_warn=hit_ratio < 90 and total > 100)
    kv("expired_keys",                expired)
    kv("evicted_keys",                evict, color_warn=evict > 0)

    if total > 100 and hit_ratio < 90:
        score["hit_low"] = -10
    if evict > 0:
        score["evict"] = -5


def show_replication(info: dict) -> None:
    section("Section 6: REPLICATION 主从复制")
    role = info.get("role")
    kv("role",                  role)
    if role == "master":
        kv("connected_slaves",  info.get("connected_slaves"))
        kv("master_repl_offset", info.get("master_repl_offset"))
    else:
        kv("master_host",       info.get("master_host"))
        kv("master_link_status", info.get("master_link_status"))
        kv("master_last_io_seconds_ago", info.get("master_last_io_seconds_ago"))


def show_cpu(info: dict) -> None:
    section("Section 7: CPU")
    kv("used_cpu_sys",    info.get("used_cpu_sys"))
    kv("used_cpu_user",   info.get("used_cpu_user"))
    kv("used_cpu_sys_children",  info.get("used_cpu_sys_children"))
    kv("used_cpu_user_children", info.get("used_cpu_user_children"))


def show_keyspace(info_keyspace: dict) -> None:
    section("Section 8: KEYSPACE 各 DB 的 Key 数")
    if not info_keyspace:
        kv("(empty)", "—")
        return
    for db, stats in info_keyspace.items():
        kv(db, stats)


def health_score(score: dict) -> None:
    section("Section 9: 健康度评分")
    base = 100
    deduct = sum(score.values())
    final = max(0, base + deduct)
    print(f"  基础分:{base}")
    if score:
        for reason, d in score.items():
            print(f"    {reason:<20} {d:+d}")
    else:
        print("    (无扣分项)")
    print(f"\n  最终得分:{final} / 100")
    if final >= 90:
        print("  ✅ 健康")
    elif final >= 70:
        print("  ⚠ 亚健康,建议关注")
    else:
        print("  ❌ 重病!立即排查")


def main() -> None:
    try:
        info = r.info()
        info_ks = r.info("keyspace")
        score: dict = {}

        show_server(info)
        show_clients(info, score)
        show_memory(info, score)
        show_persistence(info, score)
        show_stats(info, score)
        show_replication(info)
        show_cpu(info)
        show_keyspace(info_ks)
        health_score(score)
    except redis.ConnectionError as e:
        print(f"❌ Redis 连接失败:{e}")


if __name__ == "__main__":
    main()
python
"""
Ch13 配套代码 3 / 4 —— 内存综合分析器

演示:
  1. MEMORY STATS  —— 全局内存视图
  2. MEMORY USAGE  —— 抽样 Top N 大 Key
  3. OBJECT IDLETIME —— 找出长时间未访问的「冷 Key」
  4. 给出整体诊断(碎片率、Top 大 Key、冷 Key 占比)
"""

import random
import redis

r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)


def section(title: str) -> None:
    print("\n" + "=" * 64)
    print(title)
    print("=" * 64)


def fmt_bytes(n) -> str:
    if n is None:
        return "-"
    n = float(n)
    for unit in ["B", "KB", "MB", "GB", "TB"]:
        if n < 1024:
            return f"{n:.2f}{unit}"
        n /= 1024
    return f"{n:.2f}PB"


def prepare_demo_data() -> None:
    section("Step 0: 准备一批演示数据")
    print("  - 100 个普通 String / 5 个大 Hash / 1 个超大 List")

    pipe = r.pipeline(transaction=False)
    for i in range(100):
        pipe.set(f"demo:mem:str:{i}", "x" * random.randint(50, 200))
    for i in range(5):
        mapping = {f"f{j}": "y" * 100 for j in range(200)}
        pipe.hset(f"demo:mem:hash:{i}", mapping=mapping)
    pipe.rpush("demo:mem:biglist", *[f"item{i}" for i in range(5000)])
    pipe.execute()
    print("  ✓ 已写入约 100+5+1 个 demo Key(前缀 demo:mem:)")


def cleanup_demo_data() -> None:
    keys = list(r.scan_iter(match="demo:mem:*", count=500))
    if keys:
        r.unlink(*keys)


def show_memory_stats() -> None:
    section("Step 1: MEMORY STATS —— 全局内存视图")
    stats = r.execute_command("MEMORY", "STATS")

    pairs = {}
    it = iter(stats)
    for k in it:
        v = next(it)
        pairs[k] = v

    interesting = [
        "peak.allocated",
        "total.allocated",
        "startup.allocated",
        "replication.backlog",
        "clients.slaves",
        "clients.normal",
        "aof.buffer",
        "lua.caches",
        "overhead.total",
        "keys.count",
        "keys.bytes-per-key",
        "dataset.bytes",
        "dataset.percentage",
        "allocator.allocated",
        "allocator.active",
        "allocator.resident",
        "allocator-fragmentation.ratio",
        "allocator-fragmentation.bytes",
        "allocator-rss.ratio",
        "rss-overhead.ratio",
        "fragmentation",
    ]
    print(f"  {'指标':<32} 值")
    print("  " + "-" * 60)
    for k in interesting:
        if k in pairs:
            v = pairs[k]
            if isinstance(v, (int, float)) and "ratio" not in k and "percentage" not in k and "count" not in k and "per-key" not in k:
                v = fmt_bytes(v)
            elif isinstance(v, float):
                v = f"{v:.4f}"
            print(f"  {k:<32} {v}")


def show_top_keys(top_n: int = 10) -> None:
    section(f"Step 2: MEMORY USAGE 抽样找 Top {top_n} 大 Key")

    sampled = []
    for k in r.scan_iter(match="demo:mem:*", count=500):
        try:
            usage = r.memory_usage(k)
            if usage is not None:
                sampled.append((k, usage))
        except redis.ResponseError:
            continue

    if not sampled:
        print("  (未发现 demo Key,先跑 prepare_demo_data)")
        return

    sampled.sort(key=lambda x: -x[1])
    print(f"  共扫描 {len(sampled)} 个 Key\n")
    print(f"  {'Key':<40} {'类型':<10} {'内存':<12} {'IdleTime(s)':<12}")
    print("  " + "-" * 80)
    for k, usage in sampled[:top_n]:
        try:
            t = r.type(k)
            idle = r.object("idletime", k) or 0
        except redis.ResponseError:
            t = idle = "-"
        print(f"  {k:<40} {t:<10} {fmt_bytes(usage):<12} {idle:<12}")

    total = sum(u for _, u in sampled)
    top_total = sum(u for _, u in sampled[:top_n])
    print(f"\n  Top {top_n} 占比:{top_total/total*100:.1f}%({fmt_bytes(top_total)} / {fmt_bytes(total)})")
    if top_total / total > 0.5:
        print("  ⚠ Top 10 个 Key 占总内存超过 50%,存在大 Key 风险!")


def show_idle_keys(threshold_sec: int = 0) -> None:
    section(f"Step 3: OBJECT IDLETIME 找冷 Key(阈值 = {threshold_sec}s)")

    cold = []
    for k in r.scan_iter(match="demo:mem:*", count=500):
        try:
            idle = r.object("idletime", k) or 0
            if idle >= threshold_sec:
                cold.append((k, idle))
        except redis.ResponseError:
            continue

    cold.sort(key=lambda x: -x[1])
    if not cold:
        print("  (没有冷 Key)")
        return
    print(f"  共发现冷 Key {len(cold)} 个,前 10 个:\n")
    for k, idle in cold[:10]:
        print(f"    {k:<40} idle={idle}s")
    print("\n  💡 冷 Key 处理策略:")
    print("     ① 设置 TTL 让其自动过期")
    print("     ② 用 UNLINK 异步删除")
    print("     ③ 持久化策略改为 LRU/LFU 让其自动淘汰")


def diagnosis() -> None:
    section("Step 4: 诊断结论")
    info = r.info("memory")
    used  = int(info.get("used_memory", 0))
    rss   = int(info.get("used_memory_rss", 0))
    frag  = float(info.get("mem_fragmentation_ratio", 1.0))

    print(f"  used_memory    = {fmt_bytes(used)}")
    print(f"  rss            = {fmt_bytes(rss)}")
    print(f"  碎片率          = {frag:.2f}")

    print()
    if frag > 1.5:
        print("  ❌ 碎片率 > 1.5:建议开启 activedefrag 或重启从节点")
    elif frag < 1.0:
        print("  🚨 碎片率 < 1.0:可能触发 SWAP!立即 free -m 检查内存")
    else:
        print("  ✅ 碎片率正常")

    if int(info.get("evicted_keys", 0)) > 0:
        print(f"  ⚠ 累计淘汰 {info['evicted_keys']} 个 Key:maxmemory 设置过小")


def main() -> None:
    try:
        prepare_demo_data()
        show_memory_stats()
        show_top_keys(top_n=10)
        show_idle_keys(threshold_sec=0)
        diagnosis()
    except redis.ConnectionError as e:
        print(f"❌ Redis 连接失败:{e}")
    finally:
        cleanup_demo_data()


if __name__ == "__main__":
    main()
python
"""
Ch13 配套代码 4 / 4 —— 13 条优化清单 self-check

演示:
  自动检查当前 Redis 配置 vs 13 条最佳实践
  对每一条给出「✅ 通过 / ⚠ 警告 / ❌ 不通过」+ 改进建议

  注意:部分条目(如客户端是否用连接池、是否用 Pipeline)只能在客户端
  侧检查,本脚本会在这些条目上提示「需业务自检」。
"""

import redis

r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)


def section(title: str) -> None:
    print("\n" + "=" * 64)
    print(title)
    print("=" * 64)


PASS = "✅ PASS"
WARN = "⚠  WARN"
FAIL = "❌ FAIL"
SELF = "📋 SELF"


def cfg_get(key: str):
    try:
        items = r.config_get(key)
        return items.get(key)
    except redis.ResponseError:
        return None


def parse_bytes(s: str) -> int:
    if not s or s == "0":
        return 0
    try:
        return int(s)
    except ValueError:
        return 0


def check_1_pool() -> tuple:
    return (SELF, "客户端连接池", "客户端代码自检:使用 redis.ConnectionPool / Jedis Pool / Lettuce")


def check_2_pipeline() -> tuple:
    return (SELF, "批量场景用 Pipeline / MGET / HMGET", "客户端代码自检:批量操作用 pipeline.execute()")


def check_3_lua() -> tuple:
    return (SELF, "复合逻辑用 Lua 下沉", "客户端代码自检:『读-改-写』复合逻辑用 EVAL")


def check_4_ttl() -> tuple:
    info = r.info("keyspace")
    if not info:
        return (WARN, "TTL 设置", "Keyspace 为空,无法判断")
    no_ttl_dbs = []
    for db, stats in info.items():
        if isinstance(stats, dict):
            keys = stats.get("keys", 0)
            expires = stats.get("expires", 0)
            ratio = expires / keys if keys else 0
            if keys > 100 and ratio < 0.5:
                no_ttl_dbs.append((db, keys, expires, ratio))
    if no_ttl_dbs:
        msg = "; ".join(f"{db} TTL 覆盖率 {r*100:.0f}%" for db,_,_,r in no_ttl_dbs)
        return (WARN, "TTL 设置", f"以下 db 大量 Key 无 TTL:{msg}")
    return (PASS, "TTL 设置", "Key 普遍设置了 TTL")


def check_5_listpack() -> tuple:
    e = int(cfg_get("hash-max-listpack-entries") or 128)
    v = int(cfg_get("hash-max-listpack-value") or 64)
    if e <= 128 and v <= 64:
        return (PASS, "listpack 阈值合理",
                f"hash-max-listpack-entries={e}, hash-max-listpack-value={v}")
    return (WARN, "listpack 阈值偏大",
            f"entries={e}, value={v}(建议 ≤128 / ≤64,让小 Hash 用紧凑编码)")


def check_6_keylen() -> tuple:
    sample = []
    for k in r.scan_iter(count=500):
        sample.append(len(k))
        if len(sample) >= 1000:
            break
    if not sample:
        return (WARN, "Key 长度", "未发现 Key,无法采样")
    avg = sum(sample) / len(sample)
    mx = max(sample)
    if avg > 50:
        return (WARN, "Key 长度", f"平均 Key 长度 {avg:.0f}(建议 < 50),最大 {mx}")
    return (PASS, "Key 长度", f"平均 Key 长度 {avg:.0f},最大 {mx}")


def check_7_dangerous_cmds() -> tuple:
    danger = ["KEYS", "FLUSHALL", "FLUSHDB", "DEBUG", "CONFIG"]
    enabled = []
    for cmd in danger:
        try:
            res = r.execute_command("COMMAND", "INFO", cmd)
            if res and res[0] is not None:
                enabled.append(cmd)
        except redis.ResponseError:
            continue
    if enabled:
        return (WARN, "危险命令未禁用",
                f"以下命令仍可用:{', '.join(enabled)}(生产建议 rename-command 禁用)")
    return (PASS, "危险命令已禁用", "KEYS/FLUSHALL/DEBUG 等已禁用")


def check_8_unlink_lazy() -> tuple:
    flags = {
        "lazyfree-lazy-eviction":   cfg_get("lazyfree-lazy-eviction"),
        "lazyfree-lazy-expire":     cfg_get("lazyfree-lazy-expire"),
        "lazyfree-lazy-server-del": cfg_get("lazyfree-lazy-server-del"),
        "lazyfree-lazy-user-del":   cfg_get("lazyfree-lazy-user-del"),
    }
    off = [k for k, v in flags.items() if (v or "no").lower() != "yes"]
    if off:
        return (WARN, "lazy free 未全开",
                f"未开启:{', '.join(off)}(建议全设 yes,让删除走 BIO 异步)")
    return (PASS, "lazy free 全开", "DEL/EXPIRE/淘汰 都异步释放")


def check_9_bigkey() -> tuple:
    big = []
    for k in r.scan_iter(count=500):
        try:
            usage = r.memory_usage(k)
            if usage and usage > 100 * 1024:
                big.append((k, usage))
        except redis.ResponseError:
            continue
        if len(big) >= 5:
            break
    if big:
        msg = "; ".join(f"{k}={u//1024}KB" for k, u in big[:3])
        return (FAIL, "存在大 Key", f"≥100KB 的 Key 抽样:{msg}(应拆分)")
    return (PASS, "大 Key 检查", "抽样未发现 ≥100KB 的 Key")


def check_10_isolation() -> tuple:
    info = r.info("keyspace")
    db_count = len([k for k in info if k.startswith("db")])
    if db_count > 4:
        return (WARN, "业务隔离",
                f"使用了 {db_count} 个 DB,混用业务的可能性高(建议拆实例而非用多 DB)")
    return (SELF, "业务隔离",
            "需架构层面确认:不同业务是否使用了独立 Redis 实例")


def check_11_rdb_save() -> tuple:
    save = cfg_get("save")
    if save and save.strip():
        return (WARN, "RDB 自动保存",
                f"save = '{save}'。生产环境建议关闭自动 RDB,改 cron 错峰触发 BGSAVE")
    return (PASS, "RDB 自动保存", "已禁用,可走 cron 错峰触发")


def check_12_maxmemory() -> tuple:
    mm = parse_bytes(cfg_get("maxmemory") or "0")
    info = r.info("memory")
    used = int(info.get("used_memory", 0))
    if mm == 0:
        return (FAIL, "maxmemory 未限制",
                "maxmemory=0 危险!请设置上限(建议留 30% 内存给 fork)")
    if used / mm > 0.7:
        return (WARN, "maxmemory 利用率高",
                f"已用 {used/mm*100:.0f}% / {mm//1024//1024}MB(>70% 进入告警区)")
    return (PASS, "maxmemory 设置合理",
            f"已用 {used/mm*100:.0f}% / {mm//1024//1024}MB")


def check_13_linux() -> tuple:
    notes = []
    try:
        with open("/proc/sys/vm/overcommit_memory") as f:
            v = f.read().strip()
            if v != "1":
                notes.append(f"overcommit_memory={v}(建议 1)")
    except OSError:
        notes.append("无法读取 /proc/sys/vm/overcommit_memory")
    try:
        with open("/sys/kernel/mm/transparent_hugepage/enabled") as f:
            v = f.read().strip()
            if "[never]" not in v:
                notes.append(f"THP={v}(建议 never)")
    except OSError:
        notes.append("无法读取 transparent_hugepage")
    try:
        with open("/proc/sys/vm/swappiness") as f:
            v = int(f.read().strip())
            if v > 10:
                notes.append(f"swappiness={v}(建议 ≤10)")
    except OSError:
        notes.append("无法读取 swappiness")

    if notes:
        return (WARN, "Linux 内核参数", "; ".join(notes))
    return (PASS, "Linux 内核参数", "overcommit/THP/swappiness 配置 OK")


CHECKS = [
    ("1.  连接池",            check_1_pool),
    ("2.  Pipeline / MGET",   check_2_pipeline),
    ("3.  Lua 下沉",          check_3_lua),
    ("4.  TTL 设置",          check_4_ttl),
    ("5.  listpack 阈值",     check_5_listpack),
    ("6.  Key 长度",          check_6_keylen),
    ("7.  禁用危险命令",      check_7_dangerous_cmds),
    ("8.  UNLINK / lazy free", check_8_unlink_lazy),
    ("9.  大 Key 检查",       check_9_bigkey),
    ("10. 业务隔离",          check_10_isolation),
    ("11. 错峰主从同步",      check_11_rdb_save),
    ("12. maxmemory 留余量",  check_12_maxmemory),
    ("13. Linux 内核调优",    check_13_linux),
]


def main() -> None:
    try:
        section("Redis 13 条优化清单 self-check")
        results = []
        for name, fn in CHECKS:
            try:
                status, _, msg = fn()
            except Exception as e:
                status, msg = FAIL, f"check error: {e}"
            results.append((name, status, msg))
            print(f"\n  [{status}] {name}")
            print(f"          {msg}")

        section("最终诊断报告")
        n_pass = sum(1 for _, s, _ in results if s == PASS)
        n_warn = sum(1 for _, s, _ in results if s == WARN)
        n_fail = sum(1 for _, s, _ in results if s == FAIL)
        n_self = sum(1 for _, s, _ in results if s == SELF)
        print(f"  ✅ 通过:{n_pass}  ⚠ 警告:{n_warn}  ❌ 不通过:{n_fail}  📋 需自检:{n_self}")

        score = (n_pass + n_self * 0.5) / len(results) * 100
        print(f"  自动评分:{score:.0f} / 100\n")

        critical = [n for n, s, _ in results if s == FAIL]
        if critical:
            print("  🚨 必须立即处理:")
            for n in critical:
                print(f"     - {n}")
        warn_items = [n for n, s, _ in results if s == WARN]
        if warn_items:
            print("\n  ⚠ 建议改进:")
            for n in warn_items:
                print(f"     - {n}")
        if not critical and not warn_items:
            print("  🎉 全部通过,配置堪称完美!")
    except redis.ConnectionError as e:
        print(f"❌ Redis 连接失败:{e}")


if __name__ == "__main__":
    main()

01_slowlog_analyzer.py ↗ · 02_info_dashboard.py ↗ · 03_memory_analyzer.py ↗ · 04_optimization_checklist.py ↗