目录

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

实践要点

  1. slowlog-max-len 默认 128 太小,高 QPS 下几秒就被冲掉了,建议 1000
  2. 一定要让应用 CLIENT SETNAME,否则慢日志里只有 IP,无法定位是哪个服务;
  3. 慢日志是环形的,只保留最近 N 条,要定期采集到监控系统(否则重启就丢了);
  4. 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 ~ 秒 SLOWLOGcommandstats SCAN 系列、控制 key 大小
集合运算SINTERSTORE/ZUNIONSTORE O(N*M) 同上 控制规模、移到从节点、业务层算
DEL 大 key 百 ms ~ 秒 SLOWLOGLATENCY LATEST UNLINK + lazyfree-* 全开
FLUSHALL/FLUSHDB 秒级 ASYNC
fork(RDB/AOF 重写) 每 GB 20~100ms latest_fork_usecLATENCY LATEST 的 fork 事件 控制实例 <= 10GB、关 THPvm.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 statstotal_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 碎片严重 activedefragMEMORY PURGE、或重启
明显偏大但 allocator_frag_ratio 正常 是 RSS 里包含了非分配器的内存(如复制缓冲区、堆栈) MEMORY STATS 定位

碎片产生的原因

  1. jemalloc 的分档分配:申请 17 字节实际分配 32 字节(内部碎片);
  2. 删除/修改导致的空洞:大量 key 被删除后,空闲内存散落在各个 page 里,无法归还给操作系统(外部碎片);
  3. 大小不一的 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

最常见的三个内存泄漏原因

  1. 忘了给 key 设 TTL(尤其是用 SET key val 覆盖了原本有 TTL 的 key——第 2 篇的 TTL 陷阱);
  2. 动态生成 Lua 脚本(每个变体占一份缓存,永不淘汰);
  3. Stream/List 没有裁剪XADD 不带 MAXLENLPUSH 不配 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 压测的注意事项

  1. 不要在生产实例上压测(会污染数据、影响业务);
  2. redis-benchmark 自己可能成为瓶颈:单个 benchmark 进程受限于单核,测高 QPS 时要开多个进程或用 memtier_benchmark
  3. 要模拟真实的 value 大小和 key 分布-d-r 参数);
  4. 压测机和 Redis 之间的网络要和生产一致(同机房 vs 跨机房差别巨大);
  5. 关注 P99 而不只是平均值
  6. 压测时同时观察服务端指标(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 会清除 TTLGETSET 也会),这是最容易造成"缓存变永久"的坑。要么显式带 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 -Hiostat -xfree -m、swap 使用量、网卡流量。

核心认知:大多数"性能问题"其实是某个具体的错误用法(一个大 key、一条 KEYS *、忘了用 pipeline),而不是需要调参的系统性问题。

Q2:SLOWLOG 里没有慢命令,但客户端大量超时,可能是什么原因?

关键前提:SLOWLOG 只记录"命令执行时间",不包含网络传输、排队等待、结果写回。

客户端观测到的总延迟 = 网络 RTT + 服务端排队 + 命令执行 + 客户端处理。

所以可能的原因

  1. 排队:某个慢命令或阻塞操作卡住了主线程,后面的正常命令全在排队。此时慢日志里可能只有那一条(或者阻塞源不是命令,比如 fork);
  2. fork 阻塞BGSAVE/BGREWRITEAOF 的 fork(每 GB 20~100ms),不会出现在慢日志里 → 查 latest_fork_usecLATENCY LATEST 的 fork 事件;
  3. AOF fsync 阻塞:磁盘慢导致主线程被 AOF 的 write 阻塞 → 查 aof_delayed_fsync
  4. 内存 swap:延迟从微秒恶化到毫秒 → 查 mem_fragmentation_ratio < 1
  5. 网络问题:丢包、重传、带宽打满(大 value 传输) → netstat -s | grep retrans、看网卡流量;
  6. 客户端问题:GC 停顿、连接池耗尽(PoolTimeout)、序列化慢;
  7. 机器固有延迟: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)删除大 keyDEL 一个千万级集合要同步释放内存,可能阻塞数秒 → 用 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 碎片严重 activedefragMEMORY 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

最常见的三个泄漏原因

  1. 忘了给 key 设 TTL——尤其是用不带 TTL 的 SET 覆盖了原本有 TTL 的 keySET 会清除 TTL!用 KEEPTTL 或改成删除);
  2. 动态生成 Lua 脚本(把参数拼进脚本文本,每个变体占一份永不淘汰的缓存);
  3. Stream/List 没裁剪XADD 不带 MAXLENLPUSH 不配 LTRIM)。

Q7:怎么优化 Redis 的内存占用?

按收益排序

  1. 用 hash 代替多个 string key(省 3~5 倍):每个 key 至少有 robj 16 字节 + dictEntry 32 字节 + key 的 SDS 的固定开销;
  2. 大 hash 分桶保持 listpack 编码(省 5~10 倍):100 万字段的大 hash(hashtable,每字段 60~90 字节)拆成 1000 个各 1000 字段的小 hash(listpack,每字段 5~10 字节)。这是 Instagram 的经典优化(第 4 篇);
  3. 布尔状态用 Bitmap(1 亿用户 12.5MB);
  4. 海量基数统计用 HyperLogLog(固定 12KB,误差 0.81%);
  5. 小整数用 Bitfield 打包
  6. key 名尽量短(1 亿 key 每个省 27 字节 = 2.7GB);
  7. 应用层压缩大 value(JSON 用 zstd 能压到 20%~30%);
  8. 适度调大编码阈值(如 hash-max-listpack-entries 到 512,但不要超过 1000,否则 O(N) 操作的常数会进入毫秒级);
  9. 设置合理的 TTL,清理无用数据;
  10. 治理碎片activedefrag)。

Q8:为什么一定要关闭 THP(透明大页)?

THP 把内存页从 4KB 变成 2MB,本意是减少 TLB miss。但对 Redis 有害:

fork 后父进程写入触发写时复制(COW),而 COW 的粒度是"页"。 4KB 页时改一个 key 只复制 4KB;2MB 页时要复制 2MB——复制量放大 512 倍

后果:

  1. RDB/AOF 重写期间内存暴涨(可能几倍于预期);
  2. 每次缺页异常要复制 2MB,造成明显的延迟毛刺(可达几十毫秒);
  3. 极端情况下 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

其他重要配置

  • 关闭 swapswapoff -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 错误、延迟升高(请求排队等连接)。

池太大的问题

  1. Redis 端接近 maxclientsrejected_connections 增长;
  2. 每个连接在 Redis 端都有输入输出缓冲区(输入上限 1GB,输出固定 16KB + 可增长的链表),连接多了内存开销可观;
  3. 更多的 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?

注意事项

  1. 不要在生产实例上压测
  2. redis-benchmark 自己可能成为瓶颈(单进程受限于单核),测高 QPS 要开多进程或用 memtier_benchmark
  3. 要模拟真实的 value 大小(-d)和 key 分布(-r——所有请求打同一个 key 的测试结果没有意义;
  4. 压测机与 Redis 的网络要和生产一致(同机房 vs 跨机房差别巨大);
  5. 关注 P99 而不只是平均值
  6. 同时观察服务端指标(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

两个维度看

  1. usec_per_call → 单次执行慢(大 key、O(N) 命令、复杂 Lua);
  2. 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 DOCTORMEMORY 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 陷阱导致内存泄漏。


小结

  • 排查方法论:先测量再优化。顺序是「确认现象 → 区分是谁的问题(--latency vs --intrinsic-latency vs 客户端埋点)→ 定位服务端瓶颈(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 开 activedefragactive-defrag-cycle-max 建议 50 而非默认 75,因为整理在主线程做)。allocator_frag_ratio 更准确。
  • 内存泄漏三大原因忘了设 TTL(尤其是不带 TTL 的 SET 清掉了原有 TTL)、动态生成 Lua 脚本、Stream/List 没裁剪。用 INFO keyspace 对比 keysexpires 快速识别第一种。
  • 内存优化收益排序:hash 代替多 key(3~5 倍)→ 大 hash 分桶保 listpack(5~10 倍) → Bitmap/HLL/Bitfield → 短 key 名 → 压缩 value → 适度调编码阈值(不超过 1000)。
  • 系统四项必做vm.overcommit_memory=1somaxconn 调大、关闭 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 < 1aof_last_write_status != okmaster_link_status != upcluster_state != okDBSIZE 归零。