1. 先把场景立起来:Redis 在生产环境里到底会怎么“翻车”
凌晨两点被电话叫醒,这种事我经历过不止一次,绝大多数时候对面第一句话都是“Redis 好像挂了”。等你连上去一看,实例没挂,进程还在,端口还通,PING 也有回应,但业务侧的请求已经全线超时。这种“活着但没法用”的状态,才是 Redis 在生产环境里最折磨人的地方。
Redis 生产环境遇到的问题,本质上可以归到四类:内存相关(OOM、碎片、大 key)、阻塞相关(慢命令、fork、单线程排队)、持久化与复制相关(数据丢失窗口、全量同步风暴)、集群与治理相关(脑裂、槽迁移、热 key 打散)。这篇作为系列第一篇,重点放在前三类上,因为这几类问题出现频率最高、排查路径最像、也最容易在生产上造成实际损失。集群和监控体系那一块篇幅太大,后面单独展开。
这篇文章适合谁看?如果你只是拿 Redis 当本地缓存玩一玩,可能用处不大;如果你手上管着几个到几十个 Redis 实例,被内存曲线、慢查询、主从延迟折腾过,或者正在为“要不要把 maxmemory 调大一点”纠结,那这篇里的参数、计算公式和排查顺序,基本可以直接抄。我会尽量把每个数字背后的“为什么”讲清楚,因为生产环境最怕的就是抄了个参数但不知道它为什么是这个值,一出事就抓瞎。
我个人的判断标准很朴素:一个 Redis 实例到底健不健康,不看 QPS 曲线好不好看,看三件事——RSS 与 maxmemory 的差值、fork 耗时、以及 P99 延迟和 slowlog 的匹配度。这三样稳了,剩下的都是优化;这三样抖了,QPS 再漂亮也只是还没爆而已。
2. 内存类问题:从“看起来还有余量”到被系统直接干掉
2.1 maxmemory 设了为什么还会被系统杀掉
这是新手最容易踩的第一个坑:明明maxmemory设成了 8G,机器 16G,结果某天早上进程没了,dmesg里一行 OOM Killer 记录。原因很简单,maxmemory管不住全部内存开销。
maxmemory约束的是数据集本身以及部分内部结构,但下面这几块内存它是管不到的:主从复制时给从库准备的输出缓冲区(replica output buffer)、AOF 重写缓冲区、内存碎片带来的额外开销、fork 出子进程后父子进程共享页表对应的物理内存增量、以及客户端连接缓冲区。尤其是从库输出缓冲区和 fork 期间的 COW 增量,在写入量大的实例上能吃掉几个 G。
我的经验配置是:maxmemory 最多给到物理内存的 60%~70%,剩下 30% 留给上面这些“管不到”的部分和操作系统本身。如果这台机器上还跑了别的服务,比例还得往下压。对于开了 RDB 持久化、写入又比较猛的实例,我一般按 60% 来设,宁可浪费一点内存,也不想凌晨被 OOM 叫醒。
还有一个隐蔽的坑是vm.overcommit_memory。这个内核参数如果设成 0(启发式),在内存吃紧的时候,fork会因为申请不到虚拟内存而失败,表现为 RDB 保存直接报错、AOF 重写失败,主库日志里刷一堆Can't save in background: fork: Cannot allocate memory。标准做法是设成 1:
# 临时生效,重启失效 sysctl vm.overcommit_memory=1 # 永久生效,写入配置文件 echo "vm.overcommit_memory = 1" >> /etc/sysctl.conf sysctl -p注意:这个参数改完不会立刻回收内存,它只是允许内核在
fork时“乐观地”分配虚拟地址空间。真正的物理内存还是要靠 maxmemory 和监控来兜底。
另外 THP(透明大页)也要关掉。THP 会让 fork 时的 COW 粒度从 4KB 变成 2MB,写时复制的内存放大效应会非常明显,一个 10G 的实例 fork 之后 RSS 短时间内可能翻倍。关闭方式:
echo never > /sys/kernel/mm/transparent_hugepage/enabled echo never > /sys/kernel/mm/transparent_hugepage/defrag同样建议写进开机脚本,因为这个设置在重启后会恢复。
2.2 内存碎片率到底该怎么读
INFO memory里的mem_fragmentation_ratio是很多人盯着看但经常误读的指标。它的定义是used_memory_rss / used_memory,也就是操作系统看到的物理内存除以 Redis 自己统计的分配内存。
读法我总结成三档:
| 比值范围 | 含义 | 处置建议 |
|---|---|---|
| 1.0 ~ 1.5 | 正常范围,存在一定分配器开销 | 无需处理 |
| > 1.5 | 碎片偏多,物理内存被浪费 | 观察趋势,必要时开启主动整理或择机重启 |
| < 1.0 | 有部分内存被换出到 swap | 立刻排查,这是严重问题 |
比值小于 1 是最危险的信号,说明操作系统已经把 Redis 的一部分内存换到磁盘上了。Redis 访问 swap 的延迟是内存的几万倍,表现出来就是 P99 延迟突然从 1ms 跳到几十毫秒甚至上百毫秒,而且这种抖动是间歇性的,非常难查。遇到这种情况第一件事是确认 swap 使用量:
# 看具体进程的 swap 使用 grep VmSwap /proc/$(pgrep -f redis-server | head -1)/status # 或者看整机 free -m如果确实在用 swap,最直接的办法是把实例迁走或者降低 maxmemory 后重启,别指望在线能把它“调回来”。jemalloc 有主动整理碎片的能力,activedefrag yes配合active-defrag-ignore-bytes 100mb、active-defrag-threshold-lower 10、active-defrag-threshold-upper 100,可以在碎片率超过 10% 时启动整理。但要注意,主动整理本身是有 CPU 开销的,业务高峰期打开可能会让延迟曲线变差,我一般选在低峰期临时开启,整理到 1.3 以下再关掉。
2.3 淘汰策略的选择与容量估算
maxmemory-policy有八种取值,选错策略的后果比设错容量还严重。我的选择逻辑是这样的:如果 Redis 里放的是纯缓存数据(丢了可以从数据库重建),用 allkeys-lru 或 allkeys-lfu;如果里面混了不能丢的数据(比如分布式锁、队列、会话),必须用 noeviction 并且单独拆实例。
把可淘汰和不可淘汰的数据混在一个实例里,然后设一个 volatile-lru 或者干脆 allkeys-lru,是生产事故的经典起因。锁被淘汰了,两个客户端同时拿到锁;队列的 key 被淘汰了,消息直接消失。这类问题的排查成本极高,因为现象是“偶发业务异常”,不落到 Redis 日志上。
LRU 和 LFU 怎么选?Redis 的 LRU 是近似 LRU,靠采样实现,maxmemory-samples默认 5,也就是随机抽 5 个 key 挑最久没访问的淘汰。采样数越大越接近真实 LRU,但 CPU 开销也越大,一般 5 到 10 就够用。LFU 是 4.0 引入的,按访问频率淘汰,适合“少数热点数据长期被访问,大量冷数据偶尔被扫”的场景,比如商品详情缓存。LFU 有两个参数要调:lfu-log-factor控制计数器增长速度,默认 10,越大增长越慢;lfu-decay-time控制计数器衰减周期,默认 60 秒。
容量估算这块,我习惯用下面这个公式,简单但比拍脑袋靠谱:
所需内存 ≈ 平均 key+value 大小 × key 总数 × 1.2(内部结构开销)× 1.3(碎片与缓冲)× 峰值系数举个实际例子。假设业务有 2000 万个缓存 key,平均每个 key 名 40 字节、value 序列化后 500 字节,那么裸数据是2000万 × 540B ≈ 10.8GB。乘以 1.2 的结构开销是 13GB,乘以 1.3 是 16.9GB,再考虑促销期间热点数据翻倍的峰值系数 1.5,最终需要约 25GB。这时候如果机器只有 32G,maxmemory 设 20G,那基本就是给事故埋雷。
提示:这个公式里的 1.2 和 1.3 不是精确值,不同数据结构差异很大。hash 在小元素时用 listpack 存储,开销远小于每个 field 独立对象;而 zset 的跳表结构开销就明显偏大。做容量规划时,最好先用
MEMORY USAGE抽样几十个 key 算平均值,再套公式。
2.4 大 key:识别、拆分与安全删除
大 key 是 Redis 生产环境里的万恶之源。它会导致:删除时阻塞主线程、主从同步时单条命令撑爆复制缓冲区、集群模式下槽数据倾斜、HGETALL这类命令直接把网络打满。
先定义什么叫大。我的经验阈值是:string 类型 value 超过 10KB,或者集合类型元素数量超过 5000,就属于需要关注的大 key;string 超过 100KB 或者集合元素超过 5 万,属于必须处理的大 key。
识别方式有三种,各有适用场景:
# 方式一:在线扫描,基于 SCAN 实现,对线上影响小,但只能找到每种类型最大的那个 redis-cli -h 127.0.0.1 -p 6379 --bigkeys # 方式二:精确查单个 key 的内存占用,SAMPLES 0 表示全量采样,最准但最慢 redis-cli -h 127.0.0.1 -p 6379 MEMORY USAGE user:profile:10086 SAMPLES 0 # 方式三:离线分析 RDB 文件,对线上零影响,适合定期巡检 # 用 rdb_bigkeys 之类的工具解析 dump.rdb,按大小排序输出--bigkeys的局限要清楚:它是采样扫描,每种类型只报告最大的那个 key,而且扫描过程中会占用一定的 CPU。在几十 G 的实例上跑一次可能要几分钟,别在业务高峰跑。
拆分大 key 的思路按类型分:
- 大 hash:按 field 的哈希值分桶,
hash_key = base + ":" + (crc32(field) % 16),拆成 16 个子 hash。代价是读单个 field 时客户端要先算桶号。 - 大 list:如果当队列用,直接按时间或者按段拆成多个 list,客户端维护段索引;如果当“最新 N 条”用,用
LTRIM控制长度是成本最低的方案。 - 大 set:如果是用来做去重判断,考虑换成布隆过滤器;如果必须保留全量,按元素哈希分桶。
- 大 zset:按分数区间拆分,比如按天分片,查询时合并结果。
- 大 string:这个最麻烦,要么压缩(gzip/snappy,注意 CPU 换内存的取舍),要么真的拆成多个 key 分片存储。
删除大 key 千万别直接用DEL。DEL是同步释放内存,一个百万元素的 hash 删起来能让主线程卡几百毫秒甚至几秒。用UNLINK交给后台线程异步释放:
# 同步删除,大 key 慎用 DEL big:hash:key # 异步删除,4.0 及以上版本支持 UNLINK big:hash:key更进一步,可以在配置里把几类内部删除操作也改成异步,这组配置我基本是必开的:
lazyfree-lazy-expire yes lazyfree-lazy-server-del yes lazyfree-lazy-user-del yes lazyfree-lazy-user-flush yes replica-lazy-flush yesreplica-lazy-flush特别值得说一句,它控制的是从库在全量同步前清空自己数据的动作,默认是同步的。从库数据量大的时候,这个 flush 能卡住从库好几秒,期间从库完全不可用,主从延迟瞬间拉高。改成 yes 之后,从库清空是异步的,全量同步的完成时间会明显缩短。
2.5 过期键的那些“反直觉”行为
很多人以为 Redis 的过期是“到点就删”,实际上它是定时删除 + 惰性删除的混合策略。定时删除默认每 100ms 跑一轮,随机抽 20 个设了过期时间的 key,删掉其中已过期的,如果这一轮过期比例超过 25%,就立刻再来一轮。惰性删除是客户端访问这个 key 时才检查是否过期。
这个机制带来两个后果。第一,大量 key 同一时刻过期时,内存不会立刻释放,可能出现“明明都过期了,内存还是满的”的现象。第二,从库不会主动删除过期 key,它依赖主库发过来的DEL命令。在主库没触发删除之前,从库上这个 key 依然占着内存,但读的时候会返回空(3.2 版本之后的行为)。如果业务在从库上做全量扫描类的操作,可能看到大量“已过期但仍存在”的 key,不要慌,这是正常现象。
真正需要警惕的是过期时间集中设置。比如一批缓存 key 统一用EXPIRE key 3600,那么一小时后它们会同时过期,缓存集体失效,请求全部打到数据库上——这就是缓存雪崩的典型成因。解决办法很简单,在 TTL 上加随机抖动:
import random base_ttl = 3600 # 在基础 TTL 上浮动 ±10% ttl = base_ttl + random.randint(-360, 360) redis_client.setex(cache_key, ttl, value)这十几行代码不增加任何复杂度,但能把一次性的流量洪峰摊平到十分钟的窗口里,是我认为性价比最高的一处改动。
2.6 缓存穿透、击穿、雪崩在代码层的落地
这三个词被讲烂了,但真到排查现场,很多人还是分不清。我用一句话区分:穿透是查不存在的数据,击穿是热点 key 失效,雪崩是大量 key 同时失效。
穿透的防御,最有效的是布隆过滤器前置 + 空值缓存兜底。布隆过滤器放在 Redis 前面,判断“这个 key 一定不存在”就直接返回,误判率可以通过调整位数组大小和哈希函数个数控制在 1% 以内。空值缓存是给查不到的数据也写一个短 TTL(比如 60 秒)的空值,防止同一个不存在的 ID 被反复打穿。
击穿的防御核心是同一时刻只放一个请求去回源。两种实现:一是互斥锁,回源前SET lock:key 1 NX EX 10,抢到锁的去查库并写缓存,没抢到的短暂等待后重试;二是逻辑过期,缓存里存的是“数据 + 逻辑过期时间”,物理上永不过期,发现逻辑过期后异步起一个线程去刷新,其他请求继续返回旧数据。后者牺牲了一致性换取了可用性,适合对实时性要求不高的场景。
雪崩除了随机 TTL,还应该考虑多级缓存。本地缓存(Caffeine 之类)挡第一层,Redis 挡第二层,数据库兜底。本地缓存的好处是即使 Redis 整体不可用,热点数据还能撑一段时间,给运维争取恢复窗口。代价是要处理本地缓存的一致性问题,一般用“失效通知 + 短 TTL”的组合,别追求强一致。
3. 阻塞类问题:单线程模型下那些隐形的“卡顿炸弹”
3.1 哪些命令是必须拉黑的危险操作
Redis 的命令执行是单线程的,一条命令跑多久,后面排队的请求就等多久。所以任何 O(N) 且 N 不可控的命令,都是生产环境的定时炸弹。
必须拉黑的清单我列一下:
| 命令 | 危险原因 | 替代方案 |
|---|---|---|
KEYS pattern | 全库遍历,百万 key 能卡几秒 | SCAN cursor MATCH pattern COUNT 1000分批遍历 |
FLUSHALL/FLUSHDB | 同步释放全部内存 | 配合lazyfree-lazy-user-flush yes,或改用SCAN+UNLINK |
DEL bigkey | 同步释放大对象 | UNLINK |
HGETALL/SMEMBERS/LRANGE 0 -1 | 大集合一次性全量返回,撑爆网络缓冲 | HSCAN/SSCAN分批,或者只取需要的字段HMGET |
SORT | 排序复杂度 O(N log N),还可能带STORE | 排序放数据库或应用层做 |
SINTERSTORE/ZUNIONSTORE | 大集合运算,CPU 和内存双杀 | 拆成多次小规模运算,或者改用其他方案 |
DEBUG SLEEP | 直接阻塞整个实例 | 生产环境用rename-command禁掉 |
| Lua 脚本里的循环 | 脚本执行期间整个实例不可用 | 控制脚本复杂度,设置lua-time-limit |
rename-command是成本最低的防护手段,把危险命令直接改名成空字符串禁用掉:
rename-command KEYS "" rename-command FLUSHALL "" rename-command FLUSHDB "" rename-command DEBUG "" rename-command CONFIG "CONFIG_a8f3c1"注意CONFIG别直接禁,运维偶尔要用,改成一个不常见的名字就行。这个改动需要重启生效,但它能挡住 80% 的“误操作导致的线上事故”。我在接手任何一个存量集群时,第一件事就是确认这几个命令是不是被禁了,没禁的话先改再谈其他。
3.2 慢查询日志和延迟监控怎么配合用
SLOWLOG是排查阻塞的第一入口,但它有个特点:只记录命令执行时间,不记录排队时间。也就是说,如果实例已经堵住了,你看到的慢日志可能是几百条正常的命令,因为它们在队列里等了很久,但真正执行很快。这时候光看 slowlog 会误判。
配置上我用这几个值:
# 超过 10 毫秒的命令记入慢日志,单位是微秒 slowlog-log-slower-than 10000 # 保留最近 256 条 slowlog-max-len 256取值原则:默认的 10000 微秒(10ms)对大多数业务是合适的,如果业务对延迟极敏感(比如要求 P99 在 5ms 以内),可以调到 5000。别调得太低,比如 1000,那样日志量会非常大,SLOWLOG GET本身也是个消耗资源的操作。
LATENCY MONITOR是 slowlog 的补充,它记录的是时间轴上所有可能造成延迟的事件,包括命令执行、fork、过期键集中删除、AOF fsync 等。开启方式:
# 运行时开启,阈值 100 毫秒 redis-cli CONFIG SET latency-monitor-threshold 100 # 查看最近的事件 redis-cli LATENCY LATEST # 看某个事件的历史曲线 redis-cli LATENCY HISTORY fork # 让 Redis 自己给诊断意见 redis-cli LATENCY DOCTORLATENCY DOCTOR是我最喜欢的命令,它会用自然语言给你一段诊断,比如“fork 事件在过去 10 分钟内发生了 3 次,每次耗时超过 200ms,建议检查内存使用量和 THP 配置”。虽然表述比较机械,但方向基本是对的,比对着指标瞎猜高效得多。
3.3 fork 阻塞:持久化背后的真实成本
RDB 保存和 AOF 重写都要 fork 子进程。fork 本身是阻塞的,耗时和页表大小成正比,也就是和实例占用的内存量正相关。业内经验值是每 GB 内存大约 10~20ms,但这只是 fork 本身的耗时,后面还有 COW 带来的内存增长。
监控 fork 耗时的指标是INFO stats里的latest_fork_usec:
redis-cli INFO stats | grep latest_fork_usec这个值超过 200ms 就要警惕,超过 1 秒基本可以确定业务会感受到明显的延迟毛刺。我见过的极端案例是一个 40G 的实例,fork 一次要 2.5 秒,每次 RDB 保存业务都会出现一批超时。
降低 fork 成本的办法,按效果排序:
- 控制单实例内存规模。这是我反复强调的一条:单实例内存不要超过 16G,最好控制在 8~12G。超过这个量级,fork 耗时、全量同步耗时、故障恢复耗时都会成倍增长。宁可多拆几个实例,用集群或者业务分片来管理。
- 关闭 THP,前面说过,能显著减少 COW 的内存放大。
- 减少 RDB 触发频率。改
save配置,比如从900 1 / 300 10 / 60 10000改成3600 1 / 300 100,代价是故障时可能丢更多数据,这个取舍要结合业务来定。 - AOF 重写用混合持久化,
aof-use-rdb-preamble yes(7.0 之后默认开启),重写后的文件前半部分是 RDB 格式,加载速度快很多。
注意:
latest_fork_usec只记录最近一次 fork,不代表历史最坏情况。要做长期观测的话,得靠外部监控定时采集INFO并落库,或者用LATENCY HISTORY fork看事件分布。
3.4 多线程 IO 该不该开
Redis 6.0 引入了多线程 IO,但它只解决网络读写的瓶颈,命令执行依然是单线程。所以这个特性只在“网络 IO 打满但 CPU 没打满”的场景下有效,典型表现是redis-cli --stat看到 QPS 上不去,top看到 CPU 只有 60%,但网卡已经跑满。
配置方式是:
# 开启多线程读 io-threads-do-reads yes # 线程数,建议不超过 CPU 核数的 3/4 io-threads 4几个坑要注意。第一,线程数不是越多越好,超过 8 之后收益急剧下降,因为主线程的协调开销上来了。第二,如果实例只有 4 核,io-threads设成 4 反而会让主线程抢不到 CPU,一般设 2~3。第三,这个配置需要重启生效,不能CONFIG SET动态调整(部分版本可以,但改了之后行为不一定符合预期)。
还有一种情况是 CPU 绑核。在高负载场景下,把 Redis 主进程绑到固定的几个核上,避免被其他进程抢占,能降低延迟抖动。用taskset或者配置server_cpulist/bio_cpulist(7.0+ 支持)来做。但这招在容器环境下要小心,容器看到的 CPU 核数和实际的 cgroup 限制可能不一致,绑错了反而更糟。
4. 持久化与复制:数据安全边界上的取舍
4.1 RDB、AOF 与混合持久化怎么选
这三种方式的取舍,本质上是在性能、数据安全性、恢复速度之间做三角平衡。
| 方式 | 数据丢失窗口 | 恢复速度 | 性能影响 | 适用场景 |
|---|---|---|---|---|
| RDB | 可能丢几分钟 | 快,直接加载二进制 | fork 时有毛刺 | 纯缓存,可重建 |
| AOF everysec | 最多 1 秒 | 慢,要重放命令 | 轻微 | 有数据可靠性要求 |
| AOF always | 几乎不丢 | 慢 | 每条命令都 fsync,性能下降明显 | 极少数强一致场景 |
| 混合持久化 | 最多 1 秒 | 较快 | 同 AOF | 生产环境推荐 |
我的默认配置是混合持久化 + everysec:
appendonly yes appendfsync everysec aof-use-rdb-preamble yes auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mbappendfsync everysec意味着极端情况下会丢 1 秒的数据。这个取舍我认为对绝大多数业务是可接受的,但如果你的场景是“一笔都不能丢”(比如扣款流水),那 Redis 本身就不是合适的选择,应该用支持强一致持久化的存储。把 Redis 硬拗成数据库,最后一定是两头不讨好。
no-appendfsync-on-rewrite这个参数值得一提。默认是 no,意思是 AOF 重写期间依然每秒 fsync。如果磁盘 IO 紧张,重写期间 fsync 可能被阻塞,导致主线程卡住。设成 yes 的话,重写期间不 fsync,相当于放宽了数据安全边界换取稳定性。我的做法是:用 SSD 的话保持 no,用机械盘或者云上共享存储的话设成 yes。
4.2 主从全量同步风暴
新增一个从库,或者从库因为网络抖动重连,都会触发全量同步。全量同步的过程是:主库 fork 生成 RDB、传输 RDB、从库加载 RDB、主库补发增量命令。这个过程对主库的 CPU、内存、网络都是压力。
如果同时有多个从库重连,就会形成“全量同步风暴”。最常见的原因是网络抖动导致多个从库同时断开,恢复后一起重连。另一个原因是repl-backlog-size设得太小,从库短暂断开后增量数据已经被覆盖,只能走全量同步。
backlog 的大小怎么估?公式是:
backlog_size ≈ 主库写入速率 × 可容忍的从库断线时长假设主库写入 QPS 是 2 万,平均每条命令加上协议开销约 200 字节,那写入速率约 4MB/s。如果希望从库断线 60 秒内还能走增量同步,backlog 至少要 240MB。默认的 1MB 显然远远不够。
# 建议值 64mb ~ 256mb,按上面的公式算 repl-backlog-size 256mb另外两个参数能显著缓解同步压力:
# 无盘复制,主库直接把 RDB 通过网络写给从库,不落盘 repl-diskless-sync yes # 等待 5 秒再开始,让多个从库尽量合并到同一次同步 repl-diskless-sync-delay 5repl-diskless-sync-delay这个设计很巧妙:主库收到第一个从库的同步请求后,先等 5 秒,期间其他从库也会被合并进来,一次 fork 服务多个从库。代价是单个从库的同步启动会延迟 5 秒,但换来的是主库 fork 次数的大幅减少。在从库数量多、重连频繁的环境里,这个参数的价值极高。
4.3 输出缓冲区溢出导致的从库反复断开
这个问题的现象很诡异:从库不停地断开重连,主库日志里刷Client ... scheduled to be closed ASAP for overcoming of output buffer limits。根因是client-output-buffer-limit配置。
主库给每个从库维护一个输出缓冲区,用来暂存待发送的增量数据。如果从库消费速度跟不上(网络慢、从库在跑慢查询、从库在做持久化),缓冲区就会堆积。超过限制后,主库会强制断开这个从库,然后从库重连、全量同步、再次堆积、再次断开,形成死循环。
默认配置是client-output-buffer-limit replica 256mb 64mb 60,意思是硬限制 256MB,软限制 64MB 且持续 60 秒就断开。在写入量大的主库上,64MB 的软限制很容易被突破。
调整思路:
# 硬限制调大,软限制也要相应放宽 client-output-buffer-limit replica 512mb 128mb 60 # 普通客户端也要限制,防止某个客户端阻塞拖垮主库 client-output-buffer-limit normal 0 0 0但调大缓冲区只是治标,真正要解决的是从库消费慢的问题。排查方向:从库是不是在跑KEYS或者大 key 操作、从库的网络带宽是否足够、从库是不是和数据主库共享了磁盘 IO。我遇到过一例,从库所在机器的磁盘被另一个服务占满,导致 AOF fsync 极慢,从库整体处理能力下降,主库缓冲区就一路堆积。
4.4 脑裂:数据丢失的隐形杀手
主从架构下,如果主库因为网络分区被隔离,哨兵会从从库里选一个新的主库。但此时老主库还活着,还能接收客户端写入(如果客户端没断开的话)。等网络恢复,老主库降级为从库,它上面那段时间写入的数据就被清空覆盖了,这部分数据彻底丢失。
防御手段是两个参数:
# 至少要有 1 个从库在线,且延迟不超过 10 秒,主库才允许写入 min-replicas-to-write 1 min-replicas-max-lag 10这两个参数的含义是:主库在执行写命令前,会检查当前有几个从库、复制延迟多少。如果不满足条件,直接返回错误。这样在网络分区期间,被隔离的主库会拒绝写入,避免产生“只有它自己知道”的数据。
代价是可用性下降:如果从库确实全挂了,主库就写不进去了。这个取舍要业务自己判断——是宁可写不进去,还是宁可丢数据。我个人的倾向是金融、订单类业务用这组参数,纯缓存业务不用,因为缓存数据丢了可以重建,不让写反而影响面更大。
提示:这两个参数要配合哨兵或集群的故障转移时间一起看。
min-replicas-max-lag设得太小(比如 3 秒),遇到网络轻微抖动就会触发写拒绝,反而制造故障。10 秒是个比较稳的起点。
5. 排查流程与速查表:告警响了你该按什么顺序动手
5.1 收到告警后的前十五分钟
我在多个团队推行过一套固定的处置顺序,效果比“凭直觉乱查”好很多。这套流程的核心是先确认影响面,再定位根因,最后才动手改配置。
第一步,花 30 秒确认现象。业务是全部超时还是部分超时?所有实例都异常还是单个实例?如果只有单个实例异常,先看这个实例的PING是否正常响应。PING都不通,是网络或者进程挂了;PING通但业务超时,是阻塞或者内存问题。
第二步,看四个核心指标,一分钟内完成:
# 内存与会话概况 redis-cli -h xxx INFO memory | grep -E "used_memory_human|mem_fragmentation_ratio|maxmemory_human" # 客户端连接数,是否打满 redis-cli -h xxx INFO clients | grep connected_clients # 阻塞相关 redis-cli -h xxx INFO stats | grep -E "latest_fork_usec|rejected_connections|expired_keys" # 命令层面的耗时分布 redis-cli -h xxx INFO commandstats | sort -t= -k3 -rn | head -20INFO commandstats里的usec_per_call特别有用,它能告诉你哪类命令平均耗时最高。如果发现keys或者hgetall排在前列,基本就锁定原因了。
第三步,看慢查询和客户端列表:
redis-cli SLOWLOG GET 20 redis-cli CLIENT LIST | awk '{print $2, $3, $6, $12}' | head -30CLIENT LIST的输出里,addr是来源地址,age是连接存活秒数,cmd是最近执行的命令,omem是输出缓冲区占用。omem特别大的客户端,往往就是拖慢整个实例的元凶。
第四步,在动手之前先留证据。把INFO all的完整输出 dump 下来、把SLOWLOG存下来、把CLIENT LIST存下来。很多事故的根因分析是在事后做的,现场不保留数据,事后只能靠猜。
5.2 常见现象与根因对照表
这张表是我这些年积累下来的,基本覆盖了 80% 的日常问题:
| 现象 | 高概率根因 | 快速验证方式 | 处置动作 |
|---|---|---|---|
| P99 延迟周期性抖动,间隔约几分钟 | RDB fork 阻塞 | INFO stats看latest_fork_usec,LATENCY HISTORY fork | 降低保存频率、拆分实例、关闭 THP |
| 内存突然打满,淘汰量飙升 | 大 key 写入、缓存 key 数量突增 | --bigkeys扫描、INFO stats看evicted_keys | 定位业务侧异常写入、临时调大 maxmemory |
| 从库频繁断开重连 | 输出缓冲区溢出 | 主库日志看 output buffer 相关报错 | 调大client-output-buffer-limit,排查从库消费慢 |
| 连接数打满,新连接被拒 | 客户端连接池配置过大或连接泄漏 | INFO clients看connected_clients,CLIENT LIST看来源分布 | 调maxclients、排查客户端是否用完不还 |
| QPS 不高但 CPU 打满 | 慢命令、大 key 操作、Lua 脚本 | INFO commandstats、SLOWLOG | 从业务侧改命令,禁用危险命令 |
| 主从延迟持续增长 | 从库执行慢、网络带宽不足、大 key 同步 | INFO replication看master_repl_offset差值 | 排查从库负载,拆分大 key,升级带宽 |
| 大量 key 同时消失 | 集中过期、误执行 FLUSHDB | INFO keyspace看 key 数量断崖 | 加随机 TTL、确认是否误操作 |
写入报错OOM command not allowed | maxmemory 打满且策略为 noeviction | INFO memory | 清理数据、调整策略、扩容 |
这张表建议打印出来贴在工位上,比临时翻文档快得多。当然表里给的是方向,具体定位还是要结合当时的现场数据。
5.3 几条用真金白银换来的经验
最后分享几条我认为最值钱的经验,都是踩过坑之后总结的。
第一条:监控要比告警早,告警要比故障早。很多团队只配了“实例挂了”的告警,等这个告警响的时候,业务已经损失了。真正应该配的是趋势告警:内存使用率超过 75% 就提醒、fork 耗时超过 100ms 就提醒、慢查询数量环比涨 3 倍就提醒。这些指标在故障发生前几小时甚至几天就会开始变化,留给你处置的时间窗口大得多。
第二条:任何参数变更都要有回滚方案。我见过太多“改了个参数想优化性能,结果把服务改挂了”的案例。改maxmemory-policy、改appendfsync、改client-output-buffer-limit,这些都会实质影响行为。改之前记下原值,改之后观察 30 分钟,出问题立刻改回去。
第三条:不要迷信“官方推荐值”。默认配置是给通用场景设计的,你的业务有自己的特征。maxmemory-samples 5对大多数场景够用,但如果你的访问分布特别不均匀,调到 10 会有明显改善。反过来,如果实例 CPU 已经很紧张,调大采样数只会雪上加霜。参数要结合实际负载调,别看别人写多少就抄多少。
第四条:定期做故障演练。主动 kill 一个从库看主库表现、手动触发一次全量同步观察耗时、模拟一次网络分区验证脑裂防护是否生效。这些演练在业务低峰做,成本很低,但能提前发现配置里的坑。等真出事的时候再验证,代价就完全不一样了。
第五条:把“这个 Redis 用来干什么”写进文档。这条听起来像是管理建议,但实际排查价值极高。一个实例里混了缓存、分布式锁、排行榜、消息队列四种用途,出问题时你根本不知道该保哪个、能丢哪个。把这些用途写清楚,标注清楚哪些数据可丢、哪些必须保住,故障时的决策速度能快好几倍。
至于集群模式下的槽迁移、热 key 打散、以及完整的监控体系搭建,涉及的细节太多了,放到下一篇单独聊。