Redis-15 性能优化与线上问题排查
1. 排查方法论
线上出现"Redis 慢"时,不要一上来就乱调参数。按这个顺序定位:
1. 【确认现象】到底是什么慢?
├─ 所有请求都慢 → 服务端问题(阻塞、swap、CPU 打满)
├─ 部分请求慢 → 特定命令/key 的问题(大 key、慢命令)
├─ 偶发毛刺 → fork、AOF fsync、过期删除、rehash
└─ 单个客户端慢 → 客户端问题(连接池、GC、网络)
2. 【区分是谁的问题】
├─ redis-cli --latency 测服务端延迟
├─ redis-cli --intrinsic-latency 测机器固有延迟
└─ 客户端埋点的 P99 对比服务端的耗时
→ 两者差值大 = 网络或客户端问题
3. 【定位服务端瓶颈】
├─ SLOWLOG 有没有慢命令
├─ INFO commandstats 哪个命令占用时间最多
├─ INFO memory 是否用了 swap、碎片率
├─ INFO stats latest_fork_usec、evicted_keys、拒绝连接
├─ INFO persistence aof_delayed_fsync、rewrite 状态
└─ INFO clients 连接数、缓冲区
4. 【定位数据问题】
├─ --bigkeys / --memkeys
└─ --hotkeys / 客户端埋点
5. 【检查系统层】
└─ top / vmstat / iostat / free / dmesg
核心原则:先测量,再优化。 大多数"性能问题"其实是某个具体的错误用法(一个大 key、一条 KEYS *、忘了 pipeline),而不是需要调参的系统性问题。
2. 延迟分析
2.1 三个层次的延迟
客户端观测到的总延迟
= 网络往返时间(RTT)
+ 服务端排队时间 ← 前面有慢命令时会很大
+ 命令执行时间 ← SLOWLOG 只记录这一段
+ 客户端处理时间 ← GC、序列化、连接池等待
关键认识:SLOWLOG 只记录"命令执行时间",不包含网络传输、排队等待、结果写回。这解释了一个常见困惑——客户端大量超时,但慢日志里干干净净。这种情况说明问题在排队(前面有慢命令或阻塞)或网络/客户端。
2.2 测量服务端延迟
# 持续 PING 并统计延迟(毫秒)
$ redis-cli --latency
min: 0, max: 3, avg: 0.11 (1429 samples)
# 按 15 秒区间输出历史(观察趋势和毛刺)
$ redis-cli --latency-history
min: 0, max: 2, avg: 0.10 (1435 samples) -- 15.01 seconds range
min: 0, max: 87, avg: 0.35 (1201 samples) -- 15.00 seconds range ← 有毛刺!
# 延迟分布彩色图(看长尾)
$ redis-cli --latency-dist
--latency 测的是 PING 的往返时间,所以它包含了网络 RTT + 排队 + 执行。如果 avg 很低但 max 很高,说明有偶发的阻塞。
2.3 测量机器固有延迟
这一步非常重要——它能区分"Redis 慢"和"机器本身就慢"。
# 在【Redis 所在的机器上】运行,跑 100 秒
$ redis-cli --intrinsic-latency 100
Max latency so far: 1 microseconds.
Max latency so far: 15 microseconds.
Max latency so far: 3120 microseconds. ← 3ms!机器本身有问题
...
Max latency so far: 3120 microseconds.
1957014 total runs (avg latency: 0.0510 microseconds / 51.02 nanoseconds per run).
Worst run took 61154x longer than the average latency.
这个命令完全不涉及 Redis 的数据操作——它只是在一个循环里反复测量"执行一个空操作需要多久",测的是内核调度、CPU 争抢、虚拟化开销、SMI 中断造成的延迟。
判断标准:
- 正常物理机:最大延迟通常在 几十微秒以内;
- 虚拟机/容器:可能到几百微秒;
- > 1ms:机器有严重问题(CPU 超卖、宿主机争抢、节能模式、虚拟化开销大)。
如果固有延迟就有几毫秒,那么再怎么优化 Redis 都没用——要换机器或者排查宿主机。
2.4 延迟监控(Latency Monitor)
Redis 内置了一个延迟事件记录器:
latency-monitor-threshold 100 # 单位毫秒,超过这个值的事件会被记录,0 = 关闭
redis-cli CONFIG SET latency-monitor-threshold 100
# 查看有哪些延迟事件
127.0.0.1:6379> LATENCY LATEST
1) 1) "command" # 事件类型
2) (integer) 1785657600 # 最近一次发生的时间戳
3) (integer) 350 # 最近一次的延迟(毫秒)
4) (integer) 1200 # 【历史最大延迟】
2) 1) "fork"
2) (integer) 1785657500
3) (integer) 180
4) (integer) 450
# 查看某个事件的历史(最多 160 个样本)
127.0.0.1:6379> LATENCY HISTORY command
# 【非常有用】生成一份人类可读的延迟诊断报告
127.0.0.1:6379> LATENCY DOCTOR
Dave, I have observed latency spikes in this Redis instance.
You don't mind talking about it, do you Dave?
1. command: 12 latency spikes (average 350ms, mean deviation 120ms,
period 45.20 sec). Worst all time event 1200ms.
I have a few advices for you:
- Check your Slow Log to understand what are the commands you are running
which are too slow to execute. ...
127.0.0.1:6379> LATENCY RESET # 清空
Redis 会记录的延迟事件类型(看到哪个就知道该往哪查):
| 事件 | 含义 |
|---|---|
command |
某条命令执行太慢 → 查 SLOWLOG |
fast-command |
本该 O(1)/O(logN) 的命令变慢了 → 通常是大 key 或系统问题 |
fork |
fork 耗时过长 → 实例太大 / 开了 THP / 虚拟化 |
expire-cycle |
过期删除周期耗时长 → 大量 key 同时过期 |
eviction-del / eviction-cycle |
淘汰过程耗时 → 内存不足 + 大 key |
aof-write / aof-fsync-always / aof-write-pending-fsync |
AOF 写盘慢 → 磁盘性能不足 |
aof-rewrite-diff-write |
AOF 重写期间追加缓冲区(7.0 前) |
rdb-unlink-temp-file |
删除临时 RDB 文件慢 |
active-defrag-cycle |
内存碎片整理耗时 |
LATENCY DOCTOR 是排查毛刺问题的第一站,它会直接告诉你是哪类事件造成的延迟。
2.5 慢查询日志
slowlog-log-slower-than 10000 # 微秒,默认 10000 = 10ms
slowlog-max-len 128 # 最多保留条数,建议调大到 1000
redis-cli CONFIG SET slowlog-log-slower-than 5000 # 排查时可以调到 5ms
redis-cli CONFIG SET slowlog-max-len 1000
127.0.0.1:6379> SLOWLOG GET 5
1) 1) (integer) 14 # 唯一 ID
2) (integer) 1785657600 # 发生时间戳
3) (integer) 152300 # 【耗时:微秒】= 152ms
4) 1) "HGETALL" # 命令及参数
2) "user:big:hash"
5) "10.0.0.9:52133" # 客户端地址
6) "order-service-1" # 客户端名字(CLIENT SETNAME 设置的)
127.0.0.1:6379> SLOWLOG LEN
127.0.0.1:6379> SLOWLOG RESET
实践要点:
slowlog-max-len默认 128 太小,高 QPS 下几秒就被冲掉了,建议 1000;- 一定要让应用
CLIENT SETNAME,否则慢日志里只有 IP,无法定位是哪个服务; - 慢日志是环形的,只保留最近 N 条,要定期采集到监控系统(否则重启就丢了);
slowlog-log-slower-than 0会记录所有命令(调试用,生产不要开)。
2.6 命令级统计
127.0.0.1:6379> INFO commandstats
cmdstat_get:calls=1000000,usec=1500000,usec_per_call=1.50,rejected_calls=0,failed_calls=0
cmdstat_hgetall:calls=1000,usec=15000000,usec_per_call=15000.00,...
# ↑ 只调用 1000 次 ↑ 但总耗时 15 秒!平均 15ms
cmdstat_keys:calls=5,usec=8000000,usec_per_call=1600000.00,...
# ↑ 平均 1.6 秒!
# 6.2+ 还有延迟百分位分布
127.0.0.1:6379> INFO latencystats
latency_percentiles_usec_get:p50=1.003,p99=3.007,p99.9=8.031
latency_percentiles_usec_hgetall:p50=1200.5,p99=45000.0,p99.9=180000.0
# 重置统计(做基线对比时用)
127.0.0.1:6379> CONFIG RESETSTAT
INFO commandstats 是找"总时间黑洞"的最佳工具:
usec_per_call高的命令 → 单次执行慢(大 key、O(N) 命令);calls × usec_per_call总和大的命令 → 即使单次不慢,总量占用了大部分 CPU 时间(比如一个平均 0.1ms 但每秒调用 10 万次的命令)。
排查手法:
# 按总耗时排序,找出占用时间最多的命令
redis-cli INFO commandstats | \
sed 's/cmdstat_//' | \
awk -F'[:,=]' '{print $6, $2}' | sort -rn | head -10
# ↑usec ↑命令名
2.7 实时监控
# 类似 top,每秒刷新核心指标
$ redis-cli --stat
------- data ------ --------------------- load -------------------- - child -
keys mem clients blocked requests connections
1000000 1.20G 52 0 12345678 (+1234) 5678
1000012 1.20G 52 0 12346912 (+1234) 5678
# 实时打印所有命令(【生产慎用】,官方压测显示吞吐下降 50%+)
$ redis-cli monitor
# 抓一小段样本就 Ctrl+C
$ timeout 5 redis-cli monitor > /tmp/monitor.log
# 统计样本里最频繁的命令和 key
$ awk '{print $4}' /tmp/monitor.log | sort | uniq -c | sort -rn | head -20
$ awk '{print $4, $5}' /tmp/monitor.log | sort | uniq -c | sort -rn | head -20
3. 阻塞源的定位与消除
Redis 单线程执行命令,任何阻塞都会影响所有请求(第 5 篇)。这是排查性能问题的核心视角。
3.1 阻塞源清单与对策
| 阻塞源 | 典型耗时 | 定位方法 | 对策 |
|---|---|---|---|
O(N) 命令(KEYS/HGETALL/SMEMBERS/LRANGE 0 -1) |
百 ms ~ 秒 | SLOWLOG、commandstats |
换 SCAN 系列、控制 key 大小 |
集合运算(SINTERSTORE/ZUNIONSTORE) |
O(N*M) | 同上 | 控制规模、移到从节点、业务层算 |
DEL 大 key |
百 ms ~ 秒 | SLOWLOG、LATENCY LATEST |
UNLINK + lazyfree-* 全开 |
FLUSHALL/FLUSHDB |
秒级 | — | 加 ASYNC |
| fork(RDB/AOF 重写) | 每 GB 20~100ms | latest_fork_usec、LATENCY LATEST 的 fork 事件 |
控制实例 <= 10GB、关 THP、vm.overcommit_memory=1 |
AOF appendfsync always |
每次写等磁盘 | aof_delayed_fsync |
改 everysec |
| 磁盘慢导致 AOF write 阻塞 | 不确定 | aof_delayed_fsync 持续增长 |
SSD、AOF 独立磁盘、no-appendfsync-on-rewrite yes |
| Lua 脚本执行慢 | 直到脚本结束 | SLOWLOG(记录为 eval) |
脚本必须简短、循环有小上限 |
| 内存 swap | ms ~ 十 ms | mem_fragmentation_ratio < 1、/proc/pid/smaps |
关 swap、设 maxmemory |
| 大量 key 同时过期 | 每周期 25ms | expired_time_cap_reached_count |
TTL 加随机抖动 |
| 淘汰大量 key | 累积可观 | evicted_keys 快速增长 |
扩容、lazyfree-lazy-eviction yes |
| 渐进式 rehash | 分摊后较小 | 难直接观测 | 预估容量、避免瞬间插入海量 key |
| 大 value 的网络传输 | 与带宽相关 | INFO stats 的 total_net_output_bytes |
拆分大 key、压缩 |
MIGRATE 大 key(集群扩容) |
秒 ~ 十秒 | 扩容期间的超时 | 扩容前先处理大 key |
| 从节点加载 RDB | 秒级 | master_sync_in_progress |
控制实例大小、错峰 |
activedefrag 碎片整理 |
视力度 | LATENCY LATEST 的 defrag 事件 |
降低 active-defrag-cycle-max |
3.2 排查"偶发毛刺"的标准流程
这是最难排查的一类问题(平时都好,偶尔卡一下):
# 1. 先确认是不是 fork(最常见的毛刺来源)
redis-cli INFO stats | grep latest_fork_usec
# 如果 > 100000(100ms),且毛刺时间点和 bgsave/bgrewriteaof 吻合 → 就是它
redis-cli INFO persistence | grep -E 'rdb_last_save_time|aof_rewrite'
# 2. 看延迟事件记录(最直接)
redis-cli CONFIG SET latency-monitor-threshold 50
# 等待一段时间后
redis-cli LATENCY LATEST
redis-cli LATENCY DOCTOR
# 3. 看是否有周期性的慢命令
redis-cli SLOWLOG GET 100
# 4. 排查系统层
# CPU 是否被抢占
top -H -p $(pgrep -f redis-server)
# 是否在 swap
cat /proc/$(pgrep -f redis-server)/smaps | grep -i swap | awk '{s+=$2} END {print s" kB"}'
# 磁盘 IO 是否饱和
iostat -x 1
# 网络是否有丢包/重传
netstat -s | grep -i retrans
sar -n DEV 1
# 5. 排查是否有其他进程争抢资源
#(常见:同一台机器上跑了多个 Redis 实例,同时 bgsave)
3.3 消除阻塞的配置清单
# ---------- 惰性删除(必开)----------
lazyfree-lazy-eviction yes # 淘汰时异步释放
lazyfree-lazy-expire yes # 过期删除异步释放
lazyfree-lazy-server-del yes # 隐式删除(rename 覆盖等)异步
lazyfree-lazy-user-del yes # 让 DEL 表现得像 UNLINK
lazyfree-lazy-user-flush yes # 7.0+ FLUSHALL/FLUSHDB 默认异步
replica-lazy-flush yes # 从节点全量同步前清空数据异步
# ---------- 持久化 ----------
appendfsync everysec # 不要用 always
no-appendfsync-on-rewrite yes # 机械盘/IO 紧张时开,换稳定延迟
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 256mb # 调大避免频繁重写
# ---------- 内存 ----------
maxmemory <物理内存的 60%~70%>
maxmemory-policy allkeys-lfu
activedefrag yes # 碎片率高时开
active-defrag-cycle-max 50 # 限制碎片整理的 CPU 占用
# ---------- 危险命令 ----------
rename-command KEYS ""
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command MONITOR ""
rename-command DEBUG ""
rename-command CONFIG CONFIG_9f2b7c
# ---------- 慢查询与延迟监控 ----------
slowlog-log-slower-than 10000
slowlog-max-len 1000
latency-monitor-threshold 100
4. 内存优化与诊断
4.1 内存都花在哪
127.0.0.1:6379> MEMORY STATS
1) "peak.allocated" # 历史峰值
2) (integer) 5368709120
3) "total.allocated" # 当前总分配
4) (integer) 4294967296
5) "startup.allocated" # 启动时的基础开销(约几 MB)
7) "replication.backlog" # 【复制积压缓冲区】
9) "clients.slaves" # 【从节点客户端缓冲区总和】
11) "clients.normal" # 【普通客户端缓冲区总和】
13) "cluster.links" # 集群链路缓冲
15) "aof.buffer" # AOF 缓冲区
17) "lua.caches" # 【Lua 脚本缓存】(动态生成脚本会泄漏)
19) "functions.caches"
21) "db.0"
22) 1) "overhead.hashtable.main" # 主字典的元数据开销
3) "overhead.hashtable.expires" # 【过期字典的开销】(带 TTL 的 key 多时很大)
5) "overhead.hashtable.slot-to-keys"
...
"overhead.total" # 所有非数据开销总和
"keys.count" # key 总数
"keys.bytes-per-key" # 【平均每个 key 占多少字节】
"dataset.bytes" # 纯数据占用
"dataset.percentage" # 数据占总内存的比例
"peak.percentage"
"allocator.allocated" / "allocator.active" / "allocator.resident"
"allocator-fragmentation.ratio" # 【分配器层面的碎片率】(比 mem_fragmentation_ratio 更准)
"fragmentation" # 总碎片率
# Redis 自己的诊断建议
127.0.0.1:6379> MEMORY DOCTOR
Sam, I detected a few issues in this Redis instance memory implants:
* High allocator fragmentation: This instance has an allocator external
fragmentation greater than 1.1. ...
* Big keys found: ...
关键指标解读:
| 指标 | 含义与阈值 |
|---|---|
keys.bytes-per-key |
平均每 key 的开销。如果远大于你的实际数据大小,说明key 数量太多(每个 key 至少有 robj 16 字节 + dictEntry 32 字节 + SDS 头的固定开销) |
overhead.total / total.allocated |
元数据占比。超过 30% 说明 key 太碎(应该合并成 hash) |
overhead.hashtable.expires |
过期字典开销。很大说明带 TTL 的 key 非常多 |
replication.backlog |
复制积压缓冲区(repl-backlog-size)。这是固定占用,要算进预算 |
clients.normal / clients.slaves |
客户端缓冲区。异常大说明有大 key 读取或慢消费者 |
lua.caches |
Lua 脚本缓存。持续增长说明动态生成脚本导致泄漏(第 8 篇) |
4.2 内存优化清单
# ---------- 1. 用 hash 代替多个 string key(最有效)----------
# 差:3 个 key,每个都有 robj(16) + dictEntry(32) + key的SDS + value的SDS
SET user:1001:name tom
SET user:1001:age 20
SET user:1001:city beijing
# 好:1 个 key,listpack 编码下每个字段只占几字节
HSET user:1001 name tom age 20 city beijing
# 内存可省 3~5 倍
# ---------- 2. 大 hash 分桶保持 listpack 编码 ----------
# 100 万字段的大 hash(hashtable 编码,每字段 60~90 字节)
# → 拆成 1000 个各 1000 字段的小 hash(listpack 编码,每字段 5~10 字节)
HSET bucket:$(id/1000) $(id%1000) value
CONFIG SET hash-max-listpack-entries 1000 # 配合调大阈值
# 内存可省 5~10 倍(Instagram 的经典优化,第 4 篇)
# ---------- 3. key 名尽量短(但保持可读)----------
# 差:user:information:detail:profile:1001 (35 字节)
# 好:u:d:1001 (8 字节)
# 1 亿个 key,每个省 27 字节 = 省 2.7GB
# ---------- 4. 整数尽量存整数 ----------
SET count 100 # int 编码,且 0~9999 用共享对象
SET count "100" # 同样是 int 编码(Redis 会识别)
# ---------- 5. 布尔状态用 Bitmap ----------
SETBIT active:20260729 1001 1 # 1 亿用户只要 12.5MB(第 3 篇)
# ---------- 6. 海量基数统计用 HyperLogLog ----------
PFADD uv:20260729 user1001 # 固定 12KB,误差 0.81%
# ---------- 7. 小整数打包用 Bitfield ----------
BITFIELD counters SET u8 #1001 42 # 1 亿个 u8 计数器只要 100MB
# ---------- 8. 应用层压缩大 value ----------
# JSON 用 gzip/zstd 通常能压到 20%~30%
# 代价:CPU 开销 + 无法在服务端做部分操作
# ---------- 9. 检查并调优编码阈值 ----------
OBJECT ENCODING mykey
CONFIG GET hash-max-listpack-entries
# 如果集合通常在 200~500,可以把阈值调到 512(但不要超过 1000,第 4 篇)
# ---------- 10. 清理无用数据 ----------
# 找出没有 TTL 的 key(可能是忘了设过期的泄漏源)
redis-cli --scan --pattern 'cache:*' | while read k; do
[ "$(redis-cli TTL "$k")" = "-1" ] && echo "$k"
done
4.3 内存碎片治理
127.0.0.1:6379> INFO memory
used_memory_human:3.85G # Redis 认为自己用了多少
used_memory_rss_human:5.20G # 操作系统看到的物理内存
mem_fragmentation_ratio:1.35 # rss / used
allocator_frag_ratio:1.12 # 【分配器层面的碎片率,更准确】
allocator_frag_bytes:412316860
mem_allocator:jemalloc-5.2.1
碎片率的解读:
mem_fragmentation_ratio |
含义 | 处理 |
|---|---|---|
| < 1 | 部分内存被换到 swap | 紧急! 关 swap、减少数据量、检查 maxmemory |
| 1.0 ~ 1.5 | 正常范围 | 无需处理 |
| > 1.5 | 碎片严重 | 开 activedefrag、MEMORY PURGE、或重启 |
明显偏大但 allocator_frag_ratio 正常 |
是 RSS 里包含了非分配器的内存(如复制缓冲区、堆栈) | 看 MEMORY STATS 定位 |
碎片产生的原因:
- jemalloc 的分档分配:申请 17 字节实际分配 32 字节(内部碎片);
- 删除/修改导致的空洞:大量 key 被删除后,空闲内存散落在各个 page 里,无法归还给操作系统(外部碎片);
- 大小不一的 value 频繁变更。
治理:
# 4.0+ 自动碎片整理(要求编译时用 jemalloc,标准发行版都是)
activedefrag yes
# 碎片达到多少字节才开始整理
active-defrag-ignore-bytes 100mb
# 碎片率达到多少百分比开始(10 = 10%)
active-defrag-threshold-lower 10
# 碎片率达到多少时用最大力度(100 = 100%)
active-defrag-threshold-upper 100
# CPU 时间占比的下限/上限(整理在主线程做,必须限制!)
active-defrag-cycle-min 5
active-defrag-cycle-max 50 # 默认 75 太激进,建议 50
# 手动让 jemalloc 把空闲页归还给 OS(不是真正的碎片整理,但常常有效)
127.0.0.1:6379> MEMORY PURGE
# 动态开启碎片整理
redis-cli CONFIG SET activedefrag yes
# 观察效果
watch -n 2 'redis-cli INFO memory | grep -E "fragmentation|used_memory_rss_human"'
重要提醒:碎片整理是在主线程里做的(要移动对象并更新所有指向它的指针,必须原子),会消耗 CPU 并造成延迟。所以:
active-defrag-cycle-max不要设太高(建议 50,默认 75 太激进);- 在低峰期开启效果最好;
- 如果碎片率极高(> 2)且业务允许,重启实例(先切主从)是最快的解法。
4.4 排查内存持续增长
# 1. 是数据涨了还是开销涨了
MEMORY STATS # 比较 dataset.bytes 和 overhead.total 的变化
# 2. key 数量在涨?
DBSIZE
INFO keyspace # 对比 keys 和 expires——如果 keys 涨而 expires 没涨,
# 说明有大量 key 没设 TTL(常见泄漏源!)
# 3. 有大 key 在涨?
redis-cli --bigkeys
# 定期记录关键 key 的大小
MEMORY USAGE somekey
# 4. 客户端缓冲区在涨?
CLIENT LIST | awk '{for(i=1;i<=NF;i++) if($i ~ /^omem=/) print $i}' | sort -t= -k2 -rn | head
INFO clients
# 5. Lua 脚本缓存在涨?(动态生成脚本的泄漏)
MEMORY STATS | grep -A1 lua.caches
SCRIPT FLUSH # 清空(会导致客户端的 EVALSHA 收到 NOSCRIPT,但客户端会自动回退)
# 6. 复制缓冲区在涨?
INFO replication # 看从节点的 offset 差距
CLIENT LIST TYPE replica
# 7. 是不是碎片
INFO memory | grep fragmentation
最常见的三个内存泄漏原因:
- 忘了给 key 设 TTL(尤其是用
SET key val覆盖了原本有 TTL 的 key——第 2 篇的 TTL 陷阱); - 动态生成 Lua 脚本(每个变体占一份缓存,永不淘汰);
- Stream/List 没有裁剪(
XADD不带MAXLEN、LPUSH不配LTRIM)。
5. 系统与内核调优
5.1 必做的四项(第 1 篇提过,这里给完整版)
# ---------- 1. 内存过量分配(否则 fork 可能失败)----------
echo 'vm.overcommit_memory = 1' >> /etc/sysctl.conf
# ---------- 2. TCP 全连接队列(默认 128 太小)----------
echo 'net.core.somaxconn = 1024' >> /etc/sysctl.conf
# Redis 的 tcp-backlog 也要相应调大
# redis.conf: tcp-backlog 511
# ---------- 3. 关闭透明大页(COW 粒度从 4KB 变 2MB,放大 512 倍)----------
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
# 持久化(写进 rc.local 或用 tuned profile)
cat >> /etc/rc.local <<'EOF'
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
EOF
# ---------- 4. 文件描述符上限 ----------
echo '* soft nofile 65535' >> /etc/security/limits.conf
echo '* hard nofile 65535' >> /etc/security/limits.conf
# systemd 服务还要加 LimitNOFILE=65535
sysctl -p
验证:
# 启动 Redis 后看日志,如果有 WARNING 就是没配好
grep -i warning /var/log/redis/redis.log
# 常见警告:
# WARNING: The TCP backlog setting of 511 cannot be enforced because
# /proc/sys/net/core/somaxconn is set to the lower value of 128.
# WARNING overcommit_memory is set to 0!
# WARNING you have Transparent Huge Pages (THP) support enabled
5.2 其他内核参数
# ---------- swap ----------
# 【最好完全关闭】
swapoff -a
# 或者至少让内核极不情愿使用 swap
echo 'vm.swappiness = 1' >> /etc/sysctl.conf
# 注意:不是 0!0 在某些内核版本会触发 OOM Killer 而不是用 swap
# ---------- 脏页回写(影响 AOF 的 write 是否会阻塞)----------
echo 'vm.dirty_background_ratio = 5' >> /etc/sysctl.conf # 后台开始回写的阈值
echo 'vm.dirty_ratio = 10' >> /etc/sysctl.conf # 强制同步回写的阈值
# 默认值(10/20)在大内存机器上意味着可能积累几个 GB 脏页,
# 一次性回写会导致长时间的 IO 阻塞
# ---------- TCP 相关 ----------
echo 'net.ipv4.tcp_max_syn_backlog = 8192' >> /etc/sysctl.conf
echo 'net.ipv4.tcp_tw_reuse = 1' >> /etc/sysctl.conf # 复用 TIME_WAIT
echo 'net.ipv4.ip_local_port_range = 10000 65000' >> /etc/sysctl.conf
echo 'net.core.netdev_max_backlog = 5000' >> /etc/sysctl.conf
# ---------- NUMA ----------
# 检查是否有 NUMA
numactl --hardware
# 【不推荐】用 numactl 绑定(可能导致单节点内存不足)
# 【推荐】关闭 NUMA balancing,或在 BIOS 里关闭 NUMA
echo 0 > /proc/sys/kernel/numa_balancing
# ---------- CPU 节能模式(会造成延迟毛刺)----------
# 设为 performance
for cpu in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
echo performance > $cpu
done
5.3 CPU 绑核(多实例部署时)
一台多核机器上跑多个 Redis 实例时,绑核能减少 CPU 缓存失效和线程迁移:
# 用 taskset 绑定到特定核心
taskset -c 0 redis-server /etc/redis/7000.conf
taskset -c 1 redis-server /etc/redis/7001.conf
# systemd 里配置
# CPUAffinity=0
# 【注意】6.0+ 有专门的配置项,比 taskset 更精细
# redis.conf:
server_cpulist 0-3 # 主线程和 IO 线程用哪些核
bio_cpulist 4,5 # bio 后台线程
aof_rewrite_cpulist 6,7 # AOF 重写子进程
bgsave_cpulist 6,7 # RDB 子进程
这个配置的价值:把持久化子进程(fork 出来的)绑到和主线程不同的核上,避免它们抢占主线程的 CPU 和缓存——这对减少持久化期间的延迟毛刺很有效。
5.4 磁盘
# 1. AOF 和 RDB 放独立磁盘(避免和其他 IO 争抢)
# redis.conf: dir /data/redis ← 挂载独立的 SSD
# 2. 检查磁盘性能
fio --name=test --rw=randwrite --bs=4k --size=1G --numjobs=1 --direct=1 --sync=1
# fsync 延迟是关键指标(AOF everysec 依赖它)
# 3. 监控磁盘饱和度
iostat -x 1
# %util 接近 100% 说明磁盘饱和
# await 高说明 IO 排队严重
6. 客户端优化
服务端调优做到极致后,很多性能问题其实在客户端。
6.1 连接池配置
rdb := redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
// 【连接池大小】:不是越大越好
// 经验值:每个 CPU 核心 10 个连接,或者 QPS/1000
// 太大会导致 Redis 端 maxclients 压力和更多的上下文切换
PoolSize: 50,
MinIdleConns: 10, // 预热连接,避免突发流量时建连延迟
// 【超时】必须设置,否则网络问题会导致 goroutine 无限堆积
DialTimeout: 1 * time.Second,
ReadTimeout: 500 * time.Millisecond, // 大部分命令应该 < 1ms
WriteTimeout: 500 * time.Millisecond,
PoolTimeout: 1 * time.Second, // 等待可用连接的超时
// 连接生命周期
ConnMaxIdleTime: 5 * time.Minute,
ConnMaxLifetime: 0, // 0 = 不主动关闭
// 重试
MaxRetries: 2,
MinRetryBackoff: 8 * time.Millisecond,
MaxRetryBackoff: 512 * time.Millisecond,
})
连接池的常见问题:
| 问题 | 症状 | 原因 |
|---|---|---|
| 池太小 | PoolTimeout 错误、延迟高 |
并发超过池大小,请求排队等连接 |
| 池太大 | Redis 端 maxclients 满、内存高 |
每个连接在 Redis 端有输入输出缓冲区 |
| 没设超时 | goroutine/线程无限堆积 → OOM | 网络故障时请求永久挂起 |
| 阻塞命令占用池 | 池被耗尽 | BLPOP/XREAD BLOCK 长期占用连接,必须用独立连接 |
监控连接池:
stats := rdb.PoolStats()
log.Printf("hits=%d misses=%d timeouts=%d total=%d idle=%d stale=%d",
stats.Hits, stats.Misses, stats.Timeouts,
stats.TotalConns, stats.IdleConns, stats.StaleConns)
// Timeouts > 0 说明池不够用
// Misses 高说明连接经常要新建(考虑调大 MinIdleConns)
6.2 减少网络往返
// ❌ N 次 RTT
for _, id := range ids {
rdb.Get(ctx, key(id))
}
// ✅ 1 次 RTT:批量命令
rdb.MGet(ctx, keys...)
// ✅ 1 次 RTT:pipeline(命令类型不同时用)
pipe := rdb.Pipeline()
for _, id := range ids {
pipe.HGetAll(ctx, key(id))
}
cmds, _ := pipe.Exec(ctx)
// 【必须遍历检查每条命令的错误】
for _, cmd := range cmds {
if cmd.Err() != nil { /* ... */ }
}
// ✅ 需要逻辑判断时用 Lua(1 次 RTT + 原子)
script.Run(ctx, rdb, keys, args)
RTT 的量级感知(第 5 篇算过):同机房 RTT 约 0.1~0.5ms,跨机房 2~30ms。一个接口如果串行做 100 次 Redis 调用,同机房要 10~50ms,跨机房就是 200ms~3 秒。减少往返次数通常比优化 Redis 本身收益大得多。
6.3 客户端侧的其他优化
// 1. 【务必设置连接名】,让服务端的 CLIENT LIST 和 SLOWLOG 能定位到服务
rdb := redis.NewClient(&redis.Options{
ClientName: "order-service-" + hostname,
})
// 2. 序列化用更快的格式
// JSON → msgpack / protobuf / sonic(更快的 JSON 库)
// 大 value 加压缩(zstd/snappy)
// 3. 本地缓存挡住热点(第 14 篇)
// 4. 只读命令走从节点(谨慎,有延迟问题,第 10 篇)
// 5. 避免在循环里做 Redis 调用(改成批量)
// 6. 监控客户端侧的 P99 延迟,和服务端的 commandstats 对比
// 两者差值大 = 网络或客户端问题
7. 压测
7.1 redis-benchmark
# 基本压测(默认 50 并发、10 万请求、测所有命令)
redis-benchmark -h 127.0.0.1 -p 6379 -a password
# 只测特定命令
redis-benchmark -t set,get -n 1000000 -c 100 -q
# -t 命令 -n 请求数 -c 并发连接数 -q 只输出汇总
# 测不同的 value 大小(很重要,大 value 的吞吐差别巨大)
redis-benchmark -t set -n 100000 -d 100 # 100 字节
redis-benchmark -t set -n 100000 -d 10240 # 10KB
# 测 pipeline 的效果
redis-benchmark -t set,get -n 1000000 -P 16 -q
# -P 16 = 每次 pipeline 16 个命令
# 用随机 key(避免所有请求都命中同一个 key,更接近真实场景)
redis-benchmark -t set,get -n 1000000 -r 1000000 -q
# -r 1000000 = key 在 100 万个随机值里选
# 测自定义命令
redis-benchmark -n 100000 eval "return redis.call('GET', KEYS[1])" 1 mykey
redis-benchmark -n 100000 hset myhash field:__rand_int__ value
# 集群模式
redis-benchmark --cluster -h 127.0.0.1 -p 7000
# 只保持连接不发命令(测 maxclients)
redis-benchmark -c 10000 -n 1 --keep-alive
典型结果解读:
SET: 89285.71 requests per second, p50=0.279 msec
GET: 92592.59 requests per second, p50=0.271 msec
参考量级(普通云主机、100 字节 value):
| 场景 | QPS |
|---|---|
单机 GET/SET,无 pipeline |
8 万 ~ 12 万 |
单机 GET/SET,pipeline 16 |
50 万 ~ 100 万 |
| 10KB value | 2 万 ~ 4 万(网卡成瓶颈) |
HGETALL 100 字段 |
1 万 ~ 3 万 |
| Lua 脚本(简单) | 5 万 ~ 8 万 |
pipeline 能带来 5~10 倍提升,这是压测最能说明问题的一点。
7.2 压测的注意事项
- 不要在生产实例上压测(会污染数据、影响业务);
redis-benchmark自己可能成为瓶颈:单个 benchmark 进程受限于单核,测高 QPS 时要开多个进程或用memtier_benchmark;- 要模拟真实的 value 大小和 key 分布(
-d和-r参数); - 压测机和 Redis 之间的网络要和生产一致(同机房 vs 跨机房差别巨大);
- 关注 P99 而不只是平均值;
- 压测时同时观察服务端指标(CPU、网卡、
INFO commandstats)。
7.3 更专业的工具
# memtier_benchmark(Redis Labs 出品,功能更强)
memtier_benchmark -s 127.0.0.1 -p 6379 \
--protocol=redis \
-c 50 -t 4 \ # 50 连接 × 4 线程
--ratio=1:10 \ # 写:读 = 1:10
--data-size=100 \
--key-pattern=R:R \ # 随机 key
--pipeline=16 \
--test-time=60
8. 真实事故案例复盘
8.1 案例一:一条 KEYS * 打挂整个服务
现象:某天中午,所有依赖 Redis 的接口 P99 从 5ms 涨到 8 秒,持续 3 分钟后恢复。
排查:
redis-cli SLOWLOG GET 10
# 1) 1) (integer) 152
# 2) (integer) 1785657600
# 3) (integer) 3852000 ← 3.85 秒!
# 4) 1) "KEYS"
# 2) "*"
# 5) "10.0.0.88:41234"
# 6) "" ← 没设置 client name,只能靠 IP 定位
根因:一个运维脚本为了统计 key 数量,直接跑了 KEYS *。实例有 800 万 key,KEYS 是 O(N) 且在主线程执行,阻塞了 3.85 秒。期间所有请求排队,客户端大面积超时重试,进一步加剧堆积。
修复:
# 1. 立即禁用危险命令
CONFIG SET ... # 需要改配置文件后重启,或用 ACL
rename-command KEYS ""
# 6.0+ 可以用 ACL 更灵活地控制
ACL SETUSER readonly on >pwd ~* +@read -keys -flushall -flushdb
# 2. 统计 key 数用 DBSIZE(O(1))
DBSIZE
# 3. 遍历用 SCAN
redis-cli --scan --pattern 'user:*' | wc -l
# 4. 强制所有应用设置 CLIENT SETNAME(便于定位)
教训:生产环境必须 rename 或用 ACL 禁用 KEYS/FLUSHALL/FLUSHDB/MONITOR/DEBUG,不能靠"约定不要用"。
8.2 案例二:一个大 key 导致的雪崩
现象:某接口偶发超时,Redis 网卡流量周期性打满到 1Gbps。
排查:
redis-cli --bigkeys
# [00.00%] Biggest hash found so far 'product:categories:all' with 1200000 fields
redis-cli MEMORY USAGE product:categories:all
# (integer) 268435456 ← 256MB!
redis-cli INFO commandstats | grep hgetall
# cmdstat_hgetall:calls=3600,usec=1080000000,usec_per_call=300000.00
# ↑ 平均 300ms
根因:一个"全量分类树"被存成了一个 256MB 的 hash,某个接口每次都 HGETALL 它。每秒 4 次调用 = 1GB/s 的网络流量,直接打满网卡;同时每次 HGETALL 阻塞主线程 300ms。
修复:
1. 【紧急】给这个接口加本地缓存(TTL 60 秒),QPS 从 4 降到 0.017
2. 【短期】把 HGETALL 改成 HMGET,只取需要的字段
3. 【长期】按父分类拆分成多个小 hash(product:categories:{parentId})
4. 【长期】value 加 zstd 压缩
5. 加监控:每天扫描 --bigkeys,超过 1MB 的 key 告警
教训:大 key 的危害是复合的——网络、阻塞、内存、迁移全都受影响(第 14 篇的六大危害)。
8.3 案例三:主节点无持久化重启导致全量数据丢失
现象:凌晨机器重启后,Redis 里的数据全部消失,且从节点的数据也没了。
排查:
grep -E 'save|appendonly' redis.conf
# save ""
# appendonly no ← 完全没有持久化
systemctl cat redis
# Restart=always ← 自动重启
根因链条(第 10 篇讲过的经典事故):
1. 主节点为了性能关闭了所有持久化
2. 机器重启,systemd 的 Restart=always 让 Redis 自动起来了
3. 主节点启动后是【空的】,但它仍然是主节点
4. 从节点检测到与主节点连接恢复,发起 PSYNC
5. replid 不匹配 → 全量同步
6. 从节点【清空自己的所有数据】,从主节点加载一个空 RDB
→ 全集群数据永久丢失
修复:
# 1. 主节点至少保留低频 RDB 作为兜底
save 3600 1
# 2. 关闭自动重启(改成人工确认)
# systemd: Restart=no 或 Restart=on-failure + StartLimitBurst=1
# 3. 用哨兵管理拓扑,主节点重启后由哨兵决定它的角色
# 4. 加监控:DBSIZE 突然归零立即告警
教训:“纯缓存不需要持久化"是一个危险的误解。即使数据能重建,持久化也能避免"空实例重启引发的数据清空和雪崩”。
8.4 案例四:复制风暴打垮主节点
现象:主节点 CPU 100%,sync_full 每分钟增长几次,从节点反复重连。
排查:
redis-cli INFO stats | grep -E 'sync_full|sync_partial'
# sync_full:127 ← 一小时增长了 127 次!
# sync_partial_err:89
redis-cli CONFIG GET repl-backlog-size
# 1) "repl-backlog-size"
# 2) "1048576" ← 只有 1MB(默认值)
grep 'scheduled to be closed' redis.log | wc -l
# 89 ← 从节点因输出缓冲区超限被断开
根因(第 10 篇的复制风暴):
1. 写入速率 8MB/s,但 repl-backlog-size 只有 1MB(默认值)
→ 从节点断开 0.2 秒就会导致 offset 超出 backlog → 全量同步
2. 全量同步期间主节点为该从节点维护的复制缓冲区快速增长
3. 超过 client-output-buffer-limit replica 256mb → 主节点断开它
4. 从节点重连 → 又是全量同步 → 【无限循环】
修复:
repl-backlog-size 256mb # 从 1MB 调到 256MB
client-output-buffer-limit replica 1gb 256mb 120 # 调大
repl-timeout 300 # 避免大 RDB 传输被误判超时
repl-diskless-sync yes # 无盘复制
教训:repl-backlog-size 的默认值 1MB 在任何生产环境都不够,必须按"写入速率 × 最长断线时间 × 安全系数"计算。
8.5 案例五:Lua 脚本死循环导致必须重启
现象:Redis 完全无响应,所有客户端收到 BUSY Redis is busy running a script。
排查:
redis-cli SCRIPT KILL
# (error) UNKILLABLE Sorry the script already executed write commands
# against the dataset. You can either wait the script termination
# or kill the server in a hard way using the SHUTDOWN NOSAVE command.
根因:一个新上线的 Lua 脚本里有个循环,循环次数由 ARGV 传入且没有上限校验。某次调用传了一个异常大的值,脚本执行了几分钟;而且它在循环里先做了写操作,所以无法 SCRIPT KILL(会造成主从不一致,Redis 拒绝)。
处理:只能 SHUTDOWN NOSAVE(丢失未持久化的数据)+ 从备份/从节点恢复。
修复:
-- 1. 循环次数必须有【硬编码的小上限】
local n = math.min(tonumber(ARGV[1]) or 0, 100)
for i = 1, n do ... end
-- 2. 【所有校验在前,所有写入在后】
-- 这样至少在校验阶段还能被 SCRIPT KILL
-- 3. 上线前必须用生产规模的数据测试
教训(第 8 篇):busy-reply-threshold 不会杀掉脚本,只是开始返回 BUSY;写过数据的脚本无法 kill,唯一出路是重启丢数据。Lua 脚本必须简短、循环有小上限、上线前充分测试。
8.6 案例六:TTL 陷阱导致内存泄漏
现象:内存持续缓慢增长,一个月从 2GB 涨到 12GB,DBSIZE 也在涨。
排查:
redis-cli INFO keyspace
# db0:keys=8000000,expires=1200000,avg_ttl=1800000
# ↑ 800 万 key ↑ 但只有 120 万有 TTL!
# 抽样检查缓存 key 的 TTL
redis-cli --scan --pattern 'cache:user:*' | head -100 | while read k; do
echo "$(redis-cli TTL "$k") $k"
done | sort -n | head
# -1 cache:user:1001 ← 没有过期时间!
# -1 cache:user:1002
根因(第 2 篇的 TTL 陷阱):
// 写入时正确设置了 TTL
rdb.Set(ctx, key, val, time.Hour)
// 但更新时用了不带 TTL 的 SET —— 【TTL 被清除,key 变成永久】
func UpdateCache(ctx context.Context, key string, val []byte) {
rdb.Set(ctx, key, val, 0) // ← 0 表示永不过期!
}
修复:
// 方案一:更新时也带 TTL
rdb.Set(ctx, key, val, randomTTL(time.Hour))
// 方案二:用 KEEPTTL 保留原有 TTL(6.0+)
rdb.Set(ctx, key, val, redis.KeepTTL)
// 方案三:更新时干脆删除,让下次读取重建(Cache Aside,第 14 篇)
rdb.Del(ctx, key)
加监控:
# 定期比对 keys 和 expires 的比例,异常时告警
redis-cli INFO keyspace
教训:SET 会清除 TTL(GETSET 也会),这是最容易造成"缓存变永久"的坑。要么显式带 TTL,要么用 KEEPTTL,要么改成删除。
9. 监控体系
9.1 必须监控的指标清单
# ========== 可用性 ==========
PING 是否成功
role(主从角色是否符合预期)
master_link_status(从节点,必须是 up)
cluster_state(集群,必须是 ok)
cluster_slots_assigned(必须 16384)
# ========== 性能 ==========
instantaneous_ops_per_sec # 实时 QPS
latency(redis-cli --latency 或客户端埋点 P99)
slowlog 条数增长
cmdstat_* 的 usec_per_call
# ========== 内存 ==========
used_memory / maxmemory # 使用率,> 80% 告警
used_memory_rss
mem_fragmentation_ratio # < 1 紧急(swap);> 1.5 告警(碎片)
evicted_keys # 快速增长 = 内存不足
keys.bytes-per-key(MEMORY STATS)
# ========== 命中率 ==========
keyspace_hits / (keyspace_hits + keyspace_misses) # < 90% 要排查
# ========== 连接 ==========
connected_clients # 接近 maxclients 告警
blocked_clients # 阻塞客户端数
rejected_connections # > 0 说明超过 maxclients
client_recent_max_input_buffer
client_recent_max_output_buffer # 大 key 或慢消费者的信号
# ========== 持久化 ==========
rdb_last_bgsave_status # 必须 ok
aof_last_bgrewrite_status # 必须 ok
aof_last_write_status # 必须 ok(不 ok 说明正在丢数据!)
rdb_changes_since_last_save
latest_fork_usec # > 100000(100ms)告警
aof_delayed_fsync # 持续增长 = 磁盘慢
# ========== 复制 ==========
connected_slaves # 少于预期告警
master_repl_offset - slave_repl_offset # 复制延迟(字节)
slaveN 的 lag # 复制延迟(秒)
sync_full # 5 分钟内增长 > 1 次告警
sync_partial_err # 任何增长都要关注
# ========== 过期与淘汰 ==========
expired_keys
expired_time_cap_reached_count # 持续增长 = 过期压力大
# ========== 系统 ==========
CPU 使用率(单核,Redis 主线程)
内存使用率
网卡流量(in/out,接近带宽上限告警)
磁盘 IO(%util、await)
是否在 swap
9.2 关键告警规则
| 级别 | 指标 | 阈值 |
|---|---|---|
| P0 紧急 | PING 失败 | 立即 |
| P0 | mem_fragmentation_ratio < 1 |
立即(用了 swap) |
| P0 | aof_last_write_status != ok |
立即(正在丢数据) |
| P0 | master_link_status != up |
立即 |
| P0 | cluster_state != ok |
立即 |
| P0 | DBSIZE 突然归零 |
立即(数据丢失) |
| P1 重要 | used_memory / maxmemory > 85% |
5 分钟 |
| P1 | evicted_keys 快速增长 |
5 分钟 |
| P1 | rdb_last_bgsave_status != ok |
5 分钟 |
| P1 | latest_fork_usec > 500000(500ms) |
5 分钟 |
| P1 | rejected_connections > 0 |
5 分钟 |
| P1 | sync_full 5 分钟内 > 1 |
立即 |
| P1 | 客户端 P99 延迟 > 50ms | 5 分钟 |
| P2 关注 | 命中率 < 90% | 30 分钟 |
| P2 | mem_fragmentation_ratio > 1.5 |
30 分钟 |
| P2 | 慢查询增长 | 30 分钟 |
| P2 | aof_delayed_fsync 增长 |
30 分钟 |
| P2 | 大 key 扫描发现 > 1MB 的 key | 每日 |
9.3 监控工具
# 1. Prometheus + redis_exporter(最主流)
docker run -d -p 9121:9121 oliver006/redis_exporter \
--redis.addr=redis://127.0.0.1:6379 \
--redis.password=xxx
# Grafana 导入 dashboard ID 763 或 11835
# 2. 云厂商自带监控(阿里云、腾讯云、AWS)
# 通常包含热 key/大 key 分析、慢日志采集、性能趋势
# 3. 自建采集脚本(简单场景)
redis-cli INFO all | grep -E 'used_memory:|connected_clients:|...'
# 4. 每日巡检脚本
#!/bin/bash
echo "=== 大 key 扫描 ==="
redis-cli --bigkeys 2>&1 | grep -E 'Biggest|found'
echo "=== 慢查询 Top 10 ==="
redis-cli SLOWLOG GET 10
echo "=== 内存诊断 ==="
redis-cli MEMORY DOCTOR
echo "=== 延迟诊断 ==="
redis-cli LATENCY DOCTOR
echo "=== 无 TTL 的缓存 key 抽样 ==="
redis-cli --scan --pattern 'cache:*' | head -100 | \
while read k; do [ "$(redis-cli TTL "$k")" = "-1" ] && echo "$k"; done
10. 高频面试题
Q1:Redis 变慢了,你怎么排查?
按"先测量、再定位、后优化"的顺序:
第一步:确认现象
- 所有请求都慢 → 服务端问题(阻塞、swap、CPU 打满);
- 部分请求慢 → 特定命令/key(大 key、O(N) 命令);
- 偶发毛刺 → fork、AOF fsync、过期删除、rehash;
- 单个客户端慢 → 客户端问题(连接池、GC、网络)。
第二步:区分是谁的问题
redis-cli --latency # 服务端 + 网络的延迟
redis-cli --intrinsic-latency 100 # 【机器固有延迟】,> 1ms 说明机器本身有问题
# 客户端埋点的 P99 与服务端 commandstats 对比,差值大 = 网络或客户端问题
第三步:定位服务端瓶颈
redis-cli LATENCY DOCTOR # 【最直接】告诉你是哪类延迟事件
redis-cli SLOWLOG GET 100 # 慢命令
redis-cli INFO commandstats # 哪个命令占用总时间最多
redis-cli INFO memory # 碎片率 < 1 说明用了 swap
redis-cli INFO stats # latest_fork_usec、evicted_keys
redis-cli INFO persistence # aof_delayed_fsync
第四步:定位数据问题
redis-cli --bigkeys / --memkeys / --hotkeys
第五步:检查系统层:top -H、iostat -x、free -m、swap 使用量、网卡流量。
核心认知:大多数"性能问题"其实是某个具体的错误用法(一个大 key、一条 KEYS *、忘了用 pipeline),而不是需要调参的系统性问题。
Q2:SLOWLOG 里没有慢命令,但客户端大量超时,可能是什么原因?
关键前提:SLOWLOG 只记录"命令执行时间",不包含网络传输、排队等待、结果写回。
客户端观测到的总延迟 = 网络 RTT + 服务端排队 + 命令执行 + 客户端处理。
所以可能的原因:
- 排队:某个慢命令或阻塞操作卡住了主线程,后面的正常命令全在排队。此时慢日志里可能只有那一条(或者阻塞源不是命令,比如 fork);
- fork 阻塞:
BGSAVE/BGREWRITEAOF的 fork(每 GB 20~100ms),不会出现在慢日志里 → 查latest_fork_usec和LATENCY LATEST的 fork 事件; - AOF fsync 阻塞:磁盘慢导致主线程被 AOF 的 write 阻塞 → 查
aof_delayed_fsync; - 内存 swap:延迟从微秒恶化到毫秒 → 查
mem_fragmentation_ratio < 1; - 网络问题:丢包、重传、带宽打满(大 value 传输) →
netstat -s | grep retrans、看网卡流量; - 客户端问题:GC 停顿、连接池耗尽(
PoolTimeout)、序列化慢; - 机器固有延迟:CPU 超卖、宿主机争抢 →
--intrinsic-latency。
Q3:redis-cli --intrinsic-latency 是干什么的?
它测量机器的固有延迟——在一个循环里反复测"执行一个空操作需要多久",完全不涉及 Redis 的数据操作。测的是内核调度、CPU 争抢、虚拟化开销、SMI 中断造成的延迟。
用途:区分"Redis 慢"和"机器本身就慢"。
判断标准:
- 正常物理机:最大延迟 几十微秒以内;
- 虚拟机/容器:可能到几百微秒;
- > 1ms:机器有严重问题(CPU 超卖、宿主机争抢、节能模式)。
如果固有延迟就有几毫秒,再怎么优化 Redis 都没用——要换机器或排查宿主机。
注意:这个命令要在 Redis 所在的机器上运行(它测的是那台机器的调度延迟,不是网络)。
Q4:Redis 有哪些操作会阻塞主线程?
(1)O(N) 命令:KEYS *、HGETALL/SMEMBERS/LRANGE 0 -1 大集合、SINTERSTORE/ZUNIONSTORE(O(N*M))、SORT 大集合、FLUSHALL/FLUSHDB(不带 ASYNC)。
(2)删除大 key:DEL 一个千万级集合要同步释放内存,可能阻塞数秒 → 用 UNLINK + lazyfree-* 全开。
(3)持久化相关:
- fork(每 GB 20~100ms,开了 THP 更糟);
appendfsync always(每条写命令等磁盘);- 磁盘慢时 AOF 的
write也会阻塞(aof_delayed_fsync增长); - AOF 重写期间主线程 fsync 与子进程抢 IO。
(4)Lua 脚本:执行期间完全不响应其他命令;超过 busy-reply-threshold 只是开始返回 BUSY,脚本仍在跑;写过数据的脚本无法 SCRIPT KILL。
(5)系统层:内存 swap(延迟恶化 1000 倍)、CPU 争抢、NUMA 跨节点访问。
(6)其他:大量 key 同时过期(每周期 25ms 预算被打满)、淘汰大量 key、渐进式 rehash、MIGRATE 大 key(集群扩容时阻塞几十秒)、从节点加载 RDB、activedefrag 碎片整理。
Q5:mem_fragmentation_ratio 的三种情况分别说明什么?
mem_fragmentation_ratio = used_memory_rss / used_memory
| 值 | 含义 | 处理 |
|---|---|---|
| < 1 | 部分内存被换到 swap(OS 看到的物理内存少于 Redis 逻辑用量) | 紧急:内存访问变磁盘访问,延迟恶化 1000 倍。关 swap、减少数据、设 maxmemory |
| 1.0 ~ 1.5 | 正常范围(jemalloc 的分档分配 + 删除产生的空洞) | 无需处理 |
| > 1.5 | 碎片严重 | 开 activedefrag、MEMORY PURGE、或(业务允许时)切主从后重启 |
补充:allocator_frag_ratio(分配器层面的碎片率)比 mem_fragmentation_ratio 更准确,因为后者的 RSS 还包含了复制缓冲区、客户端缓冲区、栈等非分配器内存。如果 mem_fragmentation_ratio 大但 allocator_frag_ratio 正常,说明是那些额外内存导致的,要查 MEMORY STATS。
碎片整理的注意点:它在主线程里做(要移动对象并更新指针),会消耗 CPU 并造成延迟。active-defrag-cycle-max 默认 75 太激进,建议 50;最好在低峰期开。
Q6:Redis 内存持续增长,怎么排查?
# 1. 是数据涨了还是开销涨了
MEMORY STATS # 比较 dataset.bytes 和 overhead.total
# 2. 【最常见】有大量 key 没设 TTL
INFO keyspace # 对比 keys 和 expires——keys 涨而 expires 没涨就是它
redis-cli --scan --pattern 'cache:*' | head -100 | \
while read k; do [ "$(redis-cli TTL "$k")" = "-1" ] && echo "$k"; done
# 3. 有大 key
redis-cli --bigkeys / --memkeys
# 4. 客户端缓冲区
CLIENT LIST # 看 omem
INFO clients
# 5. Lua 脚本缓存泄漏(动态生成脚本)
MEMORY STATS | grep lua.caches
# 6. 复制缓冲区
INFO replication
# 7. 碎片
INFO memory | grep fragmentation
最常见的三个泄漏原因:
- 忘了给 key 设 TTL——尤其是用不带 TTL 的
SET覆盖了原本有 TTL 的 key(SET会清除 TTL!用KEEPTTL或改成删除); - 动态生成 Lua 脚本(把参数拼进脚本文本,每个变体占一份永不淘汰的缓存);
- Stream/List 没裁剪(
XADD不带MAXLEN、LPUSH不配LTRIM)。
Q7:怎么优化 Redis 的内存占用?
按收益排序:
- 用 hash 代替多个 string key(省 3~5 倍):每个 key 至少有 robj 16 字节 + dictEntry 32 字节 + key 的 SDS 的固定开销;
- 大 hash 分桶保持 listpack 编码(省 5~10 倍):100 万字段的大 hash(hashtable,每字段 60~90 字节)拆成 1000 个各 1000 字段的小 hash(listpack,每字段 5~10 字节)。这是 Instagram 的经典优化(第 4 篇);
- 布尔状态用 Bitmap(1 亿用户 12.5MB);
- 海量基数统计用 HyperLogLog(固定 12KB,误差 0.81%);
- 小整数用 Bitfield 打包;
- key 名尽量短(1 亿 key 每个省 27 字节 = 2.7GB);
- 应用层压缩大 value(JSON 用 zstd 能压到 20%~30%);
- 适度调大编码阈值(如
hash-max-listpack-entries到 512,但不要超过 1000,否则 O(N) 操作的常数会进入毫秒级); - 设置合理的 TTL,清理无用数据;
- 治理碎片(
activedefrag)。
Q8:为什么一定要关闭 THP(透明大页)?
THP 把内存页从 4KB 变成 2MB,本意是减少 TLB miss。但对 Redis 有害:
fork 后父进程写入触发写时复制(COW),而 COW 的粒度是"页"。 4KB 页时改一个 key 只复制 4KB;2MB 页时要复制 2MB——复制量放大 512 倍。
后果:
- RDB/AOF 重写期间内存暴涨(可能几倍于预期);
- 每次缺页异常要复制 2MB,造成明显的延迟毛刺(可达几十毫秒);
- 极端情况下 fork 后内存不足被 OOM Killer 杀掉。
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
Redis 启动时会检测并打印 WARNING,看到警告就说明没配好。
Q9:生产环境的 Redis 必须做哪些系统层配置?
四项必做:
# 1. vm.overcommit_memory = 1
# 否则内核按最坏情况估算 fork 需要的内存,已用内存超过物理内存一半时 fork 会失败
echo 'vm.overcommit_memory = 1' >> /etc/sysctl.conf
# 2. net.core.somaxconn 调大(默认 128 太小,高并发建连会丢)
echo 'net.core.somaxconn = 1024' >> /etc/sysctl.conf
# 3. 关闭 THP(见 Q8)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
# 4. 提高文件描述符上限
echo '* soft nofile 65535' >> /etc/security/limits.conf
echo '* hard nofile 65535' >> /etc/security/limits.conf
其他重要配置:
- 关闭 swap(
swapoff -a)或至少vm.swappiness = 1(不是 0,0 在某些内核会直接触发 OOM Killer); vm.dirty_background_ratio = 5/vm.dirty_ratio = 10(默认 10/20 在大内存机器上会积累几 GB 脏页,一次性回写造成长时间 IO 阻塞,影响 AOF);- CPU 设为 performance 模式(节能模式会造成延迟毛刺);
- 关闭 NUMA balancing;
- 6.0+ 用
server_cpulist/bgsave_cpulist绑核,把持久化子进程和主线程分到不同核,减少持久化期间的毛刺。
Q10:Redis 的连接池应该设多大?
不是越大越好。经验值:每个 CPU 核心 10 个连接,或者 QPS / 1000。
池太小的症状:PoolTimeout 错误、延迟升高(请求排队等连接)。
池太大的问题:
- Redis 端接近
maxclients,rejected_connections增长; - 每个连接在 Redis 端都有输入输出缓冲区(输入上限 1GB,输出固定 16KB + 可增长的链表),连接多了内存开销可观;
- 更多的 epoll 事件和上下文切换。
必须配置超时(否则网络故障时 goroutine/线程无限堆积导致 OOM):
DialTimeout: 1 * time.Second
ReadTimeout: 500 * time.Millisecond // 大部分命令应该 < 1ms
WriteTimeout: 500 * time.Millisecond
PoolTimeout: 1 * time.Second
一个关键陷阱:阻塞命令(BLPOP/XREAD BLOCK)会长期占用连接,会耗尽连接池 → 必须为阻塞消费开独立连接(第 5 篇)。
监控:rdb.PoolStats() 的 Timeouts > 0 说明池不够用,Misses 高说明连接经常新建(调大 MinIdleConns)。
Q11:redis-benchmark 压测时要注意什么?单机能到多少 QPS?
注意事项:
- 不要在生产实例上压测;
redis-benchmark自己可能成为瓶颈(单进程受限于单核),测高 QPS 要开多进程或用memtier_benchmark;- 要模拟真实的 value 大小(
-d)和 key 分布(-r)——所有请求打同一个 key 的测试结果没有意义; - 压测机与 Redis 的网络要和生产一致(同机房 vs 跨机房差别巨大);
- 关注 P99 而不只是平均值;
- 同时观察服务端指标(CPU、网卡、
INFO commandstats)。
参考量级(普通云主机、100 字节 value):
| 场景 | QPS |
|---|---|
单机 GET/SET 无 pipeline |
8 万 ~ 12 万 |
单机 GET/SET + pipeline 16 |
50 万 ~ 100 万 |
| 10KB value | 2 万 ~ 4 万(网卡成瓶颈) |
HGETALL 100 字段 |
1 万 ~ 3 万 |
| 简单 Lua 脚本 | 5 万 ~ 8 万 |
最重要的结论:pipeline 能带来 5~10 倍提升。这说明减少网络往返通常比优化 Redis 本身收益大得多。
Q12:命中率多少算正常?命中率低怎么办?
命中率 = keyspace_hits / (keyspace_hits + keyspace_misses)
正常值:> 90%,理想是 95%~99%。
命中率低的原因与对策:
| 原因 | 排查 | 对策 |
|---|---|---|
| 内存不足导致大量淘汰 | evicted_keys 快速增长 |
扩容内存、优化内存占用、检查大 key |
| TTL 设置太短 | 抽查 TTL |
延长 TTL、用逻辑过期 |
| 缓存穿透(查不存在的数据) | miss 的 key 是否大量不存在 | 空值缓存 + 布隆过滤器(第 14 篇) |
| 缓存刚重启/预热不足 | uptime_in_seconds 很小 |
预热、开启持久化避免空启动 |
| 业务本身就是低复用(如唯一 ID 查询) | 分析访问模式 | 可能根本不该用缓存 |
| key 设计问题(缓存粒度太细,命中率天然低) | 分析 key 分布 | 调整缓存粒度 |
注意:命中率低不一定是问题——如果业务本身访问的数据没有重复(比如每次都查不同的唯一 ID),缓存就是无效的,应该重新评估是否需要缓存。
Q13:INFO commandstats 怎么用来定位性能问题?
cmdstat_get:calls=1000000,usec=1500000,usec_per_call=1.50
cmdstat_hgetall:calls=1000,usec=15000000,usec_per_call=15000.00
# ↑只调用1000次 ↑但总耗时15秒,平均15ms
两个维度看:
usec_per_call高 → 单次执行慢(大 key、O(N) 命令、复杂 Lua);calls × usec_per_call(即usec)总量大 → 即使单次不慢,总量占用了大部分 CPU 时间(比如平均 0.1ms 但每秒 10 万次的命令)。
这是找"总时间黑洞"的最佳工具,比 SLOWLOG 更全面(慢日志只记录超过阈值的,而有些命令单次不超阈值但总量巨大)。
# 按总耗时排序
redis-cli INFO commandstats | sed 's/cmdstat_//' | \
awk -F'[:,=]' '{print $6, $2}' | sort -rn | head -10
6.2+ 还有 INFO latencystats 提供每个命令的 P50/P99/P99.9 延迟分布,比平均值更有用。
做基线对比时用 CONFIG RESETSTAT 重置计数器。
Q14:LATENCY DOCTOR 和 MEMORY DOCTOR 有什么用?
这是 Redis 内置的自动诊断工具,会用人类可读的语言告诉你问题在哪并给出建议。
LATENCY DOCTOR(需要先设 latency-monitor-threshold):
分析 Redis 记录的延迟事件,直接告诉你是哪类事件造成的毛刺:
| 事件 | 指向的问题 |
|---|---|
command |
有慢命令 → 查 SLOWLOG |
fast-command |
本该 O(1) 的命令变慢 → 大 key 或系统问题 |
fork |
fork 耗时长 → 实例太大 / THP / 虚拟化 |
expire-cycle |
大量 key 同时过期 |
eviction-cycle |
内存不足在大量淘汰 |
aof-write / aof-fsync-always |
磁盘性能不足 |
active-defrag-cycle |
碎片整理耗时 |
这是排查毛刺问题的第一站——它能直接把你引导到正确的方向,省掉大量盲目排查。
MEMORY DOCTOR:分析内存使用,指出碎片率过高、有大 key、used_memory 接近 maxmemory 等问题。
配合 MEMORY STATS(详细的内存分解)和 LATENCY LATEST/LATENCY HISTORY(具体事件的历史)一起用。
Q15:说一个你排查过的 Redis 线上问题。
(这题要讲自己的经历,这里给一个结构化的回答模板)
用"现象 → 排查 → 根因 → 修复 → 教训"五段式:
现象:某天中午所有依赖 Redis 的接口 P99 从 5ms 涨到 8 秒,持续 3 分钟后自动恢复。
排查:先用
redis-cli --latency确认服务端延迟确实很高;然后SLOWLOG GET发现一条KEYS *耗时 3.85 秒。因为实例有 800 万 key,KEYS是 O(N) 且在主线程执行,阻塞期间所有请求排队,客户端超时重试又加剧了堆积。根因:一个运维脚本为了统计 key 数量直接跑了
KEYS *。修复:① 立即用
rename-command禁用KEYS/FLUSHALL/FLUSHDB/MONITOR;② 统计 key 数改用 O(1) 的DBSIZE,遍历改用SCAN;③ 强制所有应用设置CLIENT SETNAME(当时慢日志里只有 IP,定位花了额外时间);④ 把slowlog-max-len从 128 调到 1000 并接入监控告警。教训:生产环境不能靠"约定不要用危险命令",必须从配置层面禁用(rename 或 ACL)。另外这次也暴露了监控盲区——慢查询日志没有被采集,我们是事后才看到的。
其他可以讲的案例(本文第 8 节):大 key 打满网卡、主节点无持久化重启导致全集群数据丢失、复制风暴、Lua 死循环必须重启、TTL 陷阱导致内存泄漏。
小结
- 排查方法论:先测量再优化。顺序是「确认现象 → 区分是谁的问题(
--latencyvs--intrinsic-latencyvs 客户端埋点)→ 定位服务端瓶颈(LATENCY DOCTOR/SLOWLOG/commandstats/INFO)→ 定位数据问题(--bigkeys/--hotkeys)→ 检查系统层」。 SLOWLOG只记录命令执行时间,不含网络、排队、结果写回——这解释了"客户端大量超时但慢日志干净"的困惑(问题在排队、fork、AOF fsync、swap、网络或客户端)。--intrinsic-latency测机器固有延迟(不涉及 Redis 数据操作),> 1ms 说明机器本身有问题,此时优化 Redis 无用。LATENCY DOCTOR是排查毛刺的第一站:能直接指出是command/fast-command/fork/expire-cycle/aof-write/defrag哪类事件造成的。INFO commandstats是找"总时间黑洞"的最佳工具:既看usec_per_call(单次慢),也看usec总量(单次快但量大)。6.2+ 的INFO latencystats提供 P99 分布。- 阻塞源清单:O(N) 命令、删大 key(用
UNLINK)、fork(每 GB 20~100ms)、appendfsync always、磁盘慢导致 AOF write 阻塞、Lua 脚本、swap、大量 key 同时过期、MIGRATE大 key、碎片整理。 lazyfree-*六项全开是消除阻塞的基础配置。mem_fragmentation_ratio:< 1 是紧急(用了 swap,延迟恶化 1000 倍),1.0~1.5 正常,> 1.5 开activedefrag(active-defrag-cycle-max建议 50 而非默认 75,因为整理在主线程做)。allocator_frag_ratio更准确。- 内存泄漏三大原因:忘了设 TTL(尤其是不带 TTL 的
SET清掉了原有 TTL)、动态生成 Lua 脚本、Stream/List 没裁剪。用INFO keyspace对比keys和expires快速识别第一种。 - 内存优化收益排序:hash 代替多 key(3~5 倍)→ 大 hash 分桶保 listpack(5~10 倍) → Bitmap/HLL/Bitfield → 短 key 名 → 压缩 value → 适度调编码阈值(不超过 1000)。
- 系统四项必做:
vm.overcommit_memory=1、somaxconn调大、关闭 THP(COW 粒度放大 512 倍)、nofile上限。另外关 swap(swappiness=1而非 0)、调dirty_ratio、CPU 设 performance、6.0+ 用bgsave_cpulist把持久化子进程绑到独立核。 - 客户端优化:连接池按"核数 × 10"或"QPS/1000"(太大会撑爆
maxclients和内存)、必须设超时(否则网络故障时 goroutine 无限堆积)、阻塞命令必须用独立连接、用 pipeline/MGET/Lua 减少 RTT(收益常比优化服务端更大)、设ClientName便于定位。 - 压测:
redis-benchmark要用-d(真实 value 大小)和-r(随机 key);单机GET/SET约 8~12 万 QPS,加 pipeline 能到 50~100 万(5~10 倍);benchmark 自身可能成为瓶颈。 - 六个真实事故的共同教训:① 危险命令必须从配置层禁用而非靠约定;② 大 key 的危害是复合的(网络+阻塞+内存+迁移);③ “纯缓存不需要持久化"是危险的误解(空实例重启会清空整个集群);④
repl-backlog-size默认 1MB 在任何生产环境都不够;⑤ Lua 循环必须有硬编码小上限(写过数据的脚本无法 kill);⑥SET会清除 TTL。 - P0 告警:PING 失败、
mem_fragmentation_ratio < 1、aof_last_write_status != ok、master_link_status != up、cluster_state != ok、DBSIZE归零。
xingliuhua