☰
Redis生产环境稳定性排查:内存、阻塞、持久化与复制实践
2026/10/28 11:39:51 网站建设 项目流程

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 yes

replica-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 DOCTOR

LATENCY 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 成本的办法,按效果排序:

  1. 控制单实例内存规模。这是我反复强调的一条:单实例内存不要超过 16G,最好控制在 8~12G。超过这个量级,fork 耗时、全量同步耗时、故障恢复耗时都会成倍增长。宁可多拆几个实例,用集群或者业务分片来管理。
  2. 关闭 THP,前面说过,能显著减少 COW 的内存放大。
  3. 减少 RDB 触发频率。改save配置,比如从900 1 / 300 10 / 60 10000改成3600 1 / 300 100,代价是故障时可能丢更多数据,这个取舍要结合业务来定。
  4. 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 64mb

appendfsync 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 5

repl-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 -20

INFO commandstats里的usec_per_call特别有用,它能告诉你哪类命令平均耗时最高。如果发现keys或者hgetall排在前列,基本就锁定原因了。

第三步,看慢查询和客户端列表:

redis-cli SLOWLOG GET 20 redis-cli CLIENT LIST | awk '{print $2, $3, $6, $12}' | head -30

CLIENT 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 同时消失集中过期、误执行 FLUSHDBINFO keyspace看 key 数量断崖加随机 TTL、确认是否误操作
写入报错OOM command not allowedmaxmemory 打满且策略为 noevictionINFO memory清理数据、调整策略、扩容

这张表建议打印出来贴在工位上,比临时翻文档快得多。当然表里给的是方向,具体定位还是要结合当时的现场数据。

5.3 几条用真金白银换来的经验

最后分享几条我认为最值钱的经验,都是踩过坑之后总结的。

第一条:监控要比告警早,告警要比故障早。很多团队只配了“实例挂了”的告警,等这个告警响的时候,业务已经损失了。真正应该配的是趋势告警:内存使用率超过 75% 就提醒、fork 耗时超过 100ms 就提醒、慢查询数量环比涨 3 倍就提醒。这些指标在故障发生前几小时甚至几天就会开始变化,留给你处置的时间窗口大得多。

第二条:任何参数变更都要有回滚方案。我见过太多“改了个参数想优化性能,结果把服务改挂了”的案例。改maxmemory-policy、改appendfsync、改client-output-buffer-limit,这些都会实质影响行为。改之前记下原值,改之后观察 30 分钟,出问题立刻改回去。

第三条:不要迷信“官方推荐值”。默认配置是给通用场景设计的,你的业务有自己的特征。maxmemory-samples 5对大多数场景够用,但如果你的访问分布特别不均匀,调到 10 会有明显改善。反过来,如果实例 CPU 已经很紧张,调大采样数只会雪上加霜。参数要结合实际负载调,别看别人写多少就抄多少。

第四条:定期做故障演练。主动 kill 一个从库看主库表现、手动触发一次全量同步观察耗时、模拟一次网络分区验证脑裂防护是否生效。这些演练在业务低峰做,成本很低,但能提前发现配置里的坑。等真出事的时候再验证,代价就完全不一样了。

第五条:把“这个 Redis 用来干什么”写进文档。这条听起来像是管理建议,但实际排查价值极高。一个实例里混了缓存、分布式锁、排行榜、消息队列四种用途,出问题时你根本不知道该保哪个、能丢哪个。把这些用途写清楚,标注清楚哪些数据可丢、哪些必须保住,故障时的决策速度能快好几倍。

至于集群模式下的槽迁移、热 key 打散、以及完整的监控体系搭建,涉及的细节太多了,放到下一篇单独聊。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询