ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Redis高可用、持久化与性能管理:生产环境实战避坑指南

Redis高可用、持久化与性能管理:生产环境实战避坑指南

Redis 这套东西,刚上手的时候觉得就是个内存缓存,set/get 一把梭,能有多难?可真到了生产环境,尤其是流量一上来、机器一重启、主从一切换,各种幺蛾子全冒出来了。我见过太多团队,Redis 用着用着就变成了"单点定时炸弹"——没做高可用,主节点一挂整个业务雪崩;没配持久化,重启之后缓存全空,数据库瞬间被击穿;性能不管理,慢查询堆着堆着就把内存打爆了。这篇就围绕Redis 高可用、持久化及性能管理这三块,把我这些年踩过的坑、调过的参数、验证过的方案,从头到尾捋一遍。不管你是刚接触 Redis 的新手,还是已经管着几十个实例的老手,应该都能从里面找到能直接抄作业的东西。

1. 先把整体思路理清楚:为什么这三件事必须一起做

1.1 高可用、持久化、性能管理不是三个独立话题

很多人学 Redis 是割裂着学的:今天看主从复制,明天看 RDB 和 AOF,后天调 maxmemory。结果真到线上出问题,发现这三者是互相咬合的。举个最典型的场景:主节点宕机,哨兵把从节点提升为主——这个"高可用"动作能不能顺利完成,取决于从节点的数据是不是足够新,而数据新不新,直接跟持久化和复制积压缓冲区挂钩。再比如,你为了性能把 RDB 的 save 频率调得很低,结果宕机时丢了一大段数据,高可用切换过去之后数据是残缺的,业务照样出问题。

所以我的建议是,把这三块当成一个整体来设计。高可用解决的是"服务别断",持久化解决的是"数据别丢",性能管理解决的是"别把自己撑死"。三者优先级怎么排,取决于你的业务:如果是纯缓存场景,丢了能回源重建,那持久化可以弱化,重点保高可用和性能;如果是拿 Redis 当存储用(比如做排行榜、计数器、分布式锁),那持久化就是命根子,必须重点保障。

我一般会先问自己三个问题:这份数据丢了能不能从别处恢复?服务中断几分钟业务能不能忍?内存和 QPS 的天花板在哪?把这三个问题回答清楚,方案基本就定型了。

1.2 三种部署形态的取舍逻辑

Redis 的部署形态,从简单到复杂大致是:单机、主从、哨兵、集群。选哪个不是拍脑袋,得看数据量和可用性要求。

单机就不用说了,只适合开发测试,生产环境上单机基本等于给自己埋雷。主从复制解决了读扩展和数据备份的问题,但主挂了需要人工介入切换,做不到自动故障转移。哨兵在主观下线、客观下线、领导者选举这一套机制下,能实现自动切换,是中小规模场景下性价比最高的高可用方案。集群则是为了解决单机内存上限和写扩展问题,把数据分片到多个主节点上,每个分片再配从节点。

这里有个常见的误区:以为集群就一定比哨兵高级。其实不是。集群的运维复杂度、客户端兼容性要求、跨槽操作的限制,都是实打实的成本。如果你的数据量没超过单机内存上限(比如 32G 以内),QPS 也没到单节点扛不住的程度,哨兵方案往往更省心。我见过有团队数据才几个 G,硬上集群,结果为了处理跨槽的 mget 问题折腾了好几周,纯属自找麻烦。

1.3 一个我常用的决策参考表

场景特征推荐形态持久化策略关键理由
开发测试、数据可丢单机可关闭省资源,快速迭代
读多写少、数据量小主从 + 哨兵RDB 为主自动切换,运维简单
数据重要、不能丢主从 + 哨兵AOF everysec兼顾安全与性能
数据量大、写并发高集群RDB + AOF分片扩展,双重保障
纯缓存、可回源主从 + 哨兵可弱化优先保性能和可用

这张表不是死的,实际选型还要结合团队运维能力。一个只有两三个人的小团队,硬上集群,出了问题没人能快速定位,反而更危险。

2. 高可用方案落地:从主从复制到哨兵自动切换

2.1 主从复制的完整握手过程

主从复制是 Redis 高可用的地基,搞不清楚它的握手流程,后面排查问题就是抓瞎。整个复制过程分几个阶段:

第一阶段是建立连接、协商同步。从节点执行replicaof命令后,会跟主节点建立 socket 连接,然后发送 PING 确认对方活着,接着进行身份验证(如果配了 requirepass),再发送自己的监听端口信息。这一步做完,从节点会发送PSYNC命令,带上自己记录的 runid 和复制偏移量。

第二阶段是全量同步或部分同步。主节点收到 PSYNC 后判断:如果从节点带的 runid 跟自己不一致,或者偏移量已经不在复制积压缓冲区里了,就触发全量同步(full resync);否则走部分同步(continue),只补发缺失的那部分数据。全量同步的过程是:主节点 fork 一个子进程生成 RDB 快照,同时把 fork 之后的新写命令攒在缓冲区里,RDB 生成完发给从节点,从节点先清空自己的数据再加载 RDB,最后主节点把缓冲区里的增量命令补发过去。

第三阶段是命令传播。全量同步完成后,主节点每执行一条写命令,就异步发给所有从节点,从节点执行同样的命令保持数据一致。同时从节点会周期性发送REPLCONF ACK上报自己的偏移量,主节点据此判断从节点是否掉队。

提示:repl-backlog-size这个参数非常关键,它决定了部分同步能不能成功。默认 1MB,在高写入场景下很容易被写满,导致从节点重连时被迫全量同步。我一般会按"平均写入速率 × 断连容忍秒数"来估算,比如每秒写 2MB、容忍断 60 秒,那就设成 128MB 比较稳妥。

2.2 哨兵机制的核心:三个定时任务和两次下线判断

哨兵(Sentinel)本质是一个独立的进程,它通过三个定时任务来监控整个主从体系:

  • 每 10 秒一次:向所有主从节点发送 INFO 命令,获取拓扑结构,发现新的从节点或哨兵。
  • 每 2 秒一次:向所有节点的__sentinel__:hello频道发送自己的信息和判断,同时订阅这个频道,实现哨兵之间互相发现和状态交换。
  • 每 1 秒一次:向所有主从节点和其他哨兵发送 PING,做心跳检测。

故障判断分两步。主观下线(SDOWN)是单个哨兵自己的判断:如果 PING 超过down-after-milliseconds没收到有效回复,就认为这个节点挂了。但单个哨兵可能因为网络抖动误判,所以需要客观下线(ODOWN):当认为主节点下线的哨兵数量超过 quorum(法定票数)时,才真正判定主节点客观下线,触发故障转移。

故障转移的流程是:哨兵之间选举出一个领导者(基于 Raft 算法),由领导者从从节点里挑一个最合适的提升为主节点。挑选规则依次是:先过滤掉不健康的从节点,然后比较replica-priority(越小优先级越高),相同则比较复制偏移量(越大数据越新),再相同就比较 runid(越小越优先)。选出来之后,执行slaveof no one提升为主,再让其他从节点指向新主,最后通过发布订阅通知客户端。

2.3 哨兵配置实操与关键参数

下面是一份我常用的哨兵配置模板,可以直接改改就用:

port 26379 daemonize yes logfile "/var/log/redis/sentinel.log" dir "/var/lib/redis/sentinel" # 监控的主节点,quorum 设为 2 sentinel monitor mymaster 192.168.1.10 6379 2 # 主节点多久无响应判定主观下线,单位毫秒 sentinel down-after-milliseconds mymaster 30000 # 故障转移时最多同时有几个从节点同步新主 sentinel parallel-syncs mymaster 1 # 故障转移超时时间 sentinel failover-timeout mymaster 180000 # 主节点密码 sentinel auth-pass mymaster yourpassword

几个参数的选择逻辑我解释一下。down-after-milliseconds设 30 秒是经验值,设太短容易误判(比如一次 GC 停顿就触发切换),设太长故障恢复慢。parallel-syncs设 1 是为了避免所有从节点同时向新主发起全量同步,把新主 IO 打满,宁可慢一点也要稳。failover-timeout设 180 秒,给故障转移留足时间,尤其是数据量大的时候全量同步很耗时。

注意:哨兵节点至少部署 3 个,且分布在不同物理机上。如果只部署 2 个,一个挂了另一个无法形成多数派,故障转移就失效了。这是很多人踩过的坑。

2.4 客户端如何正确接入哨兵

哨兵方案里,客户端不能直连主节点 IP,因为主节点会变。正确的做法是客户端连接哨兵集群,通过哨兵获取当前主节点地址。以常见的客户端为例,连接时传入的是哨兵节点列表和 master 名称,客户端内部会订阅哨兵的切换事件,主节点变了自动更新连接。

这里有个容易忽略的点:故障转移期间客户端会短暂报错。因为切换需要时间(通常几秒到几十秒),这期间写请求会失败。业务代码必须做好重试和降级,不能假设 Redis 永远可用。我一般会在客户端配置里加上重试次数和重试间隔,同时业务层对写失败做本地缓存或消息队列缓冲,等 Redis 恢复后再补。

3. 持久化机制:RDB 与 AOF 的取舍和组合

3.1 RDB 的触发时机与 fork 代价

RDB 是把某一时刻的内存数据快照写成二进制文件。触发方式有三种:手动执行SAVE(阻塞主线程,生产禁用)和BGSAVE(fork 子进程,不阻塞主线程)、根据配置自动触发、主从全量同步时自动触发。

自动触发的配置长这样:

save 900 1 # 900 秒内至少 1 个 key 变化 save 300 10 # 300 秒内至少 10 个 key 变化 save 60 10000 # 60 秒内至少 10000 个 key 变化

这三个条件是"或"的关系,满足任意一个就触发 BGSAVE。RDB 的优点是文件紧凑、恢复速度快,适合做备份和灾难恢复。缺点是两次快照之间的数据会丢,而且 fork 子进程时如果内存很大,fork 本身会阻塞主线程。

fork 的代价跟内存页表大小成正比。在 Linux 上,fork 采用写时复制(COW),理论上很快,但如果内存有几十 G,页表映射本身就要花几百毫秒甚至更久。我实测过一个 32G 的实例,fork 耗时大概在 300-500ms 之间,这期间主线程是卡住的。所以单实例内存不建议超过 32G,这也是集群分片的一个现实理由。

3.2 AOF 的三种刷盘策略对比

AOF 是把每条写命令追加到日志文件里,恢复时重放这些命令。刷盘策略由appendfsync控制,有三种:

策略行为数据安全性性能
always每条命令都 fsync最高,几乎不丢最差,QPS 大幅下降
everysec每秒 fsync 一次最多丢 1 秒较好,推荐
no交给操作系统决定不可控,可能丢很多最好

生产环境我基本都用everysec,它在安全性和性能之间取得了很好的平衡。always虽然安全,但每次写都要落盘,QPS 会掉到几千甚至更低,除非是金融级场景否则没必要。no则太不可控,操作系统可能几十秒才刷一次,宕机丢数据量太大。

AOF 文件会随着时间不断膨胀,所以需要重写(rewrite)来压缩。重写也是 fork 子进程完成的,子进程根据当前内存数据生成一份最精简的命令集,比如一个 key 被 set 了 100 次,重写后只保留最后一次。重写期间的新命令会同时写入旧 AOF 缓冲和新 AOF 缓冲,重写完成后合并。

auto-aof-rewrite-percentage 100 # 文件比上次重写后增长 100% 触发 auto-aof-rewrite-min-size 64mb # 文件至少 64MB 才触发

3.3 混合持久化:兼顾恢复速度和数据安全

Redis 4.0 之后引入了混合持久化,配置aof-use-rdb-preamble yes开启。它的原理是:AOF 重写时,先把当前内存数据以 RDB 格式写入 AOF 文件的开头,之后的增量命令再以 AOF 格式追加。这样恢复时先加载 RDB 部分(快),再重放 AOF 部分(补全),兼顾了 RDB 的恢复速度和 AOF 的数据安全。

我现在的默认配置就是混合持久化 + everysec,实测下来恢复一个 10G 的实例,纯 AOF 要几分钟,混合持久化只要几十秒,差距非常明显。

3.4 持久化选型的实操建议

到底怎么选,我给几个具体场景的建议:

  • 纯缓存场景:可以只开 RDB,甚至关闭持久化。因为数据能从数据库回源,丢了无所谓,重点是别让持久化拖累性能。
  • 数据不能丢但能容忍秒级损失:AOF everysec + 混合持久化,这是最通用的方案。
  • 数据绝对不能丢:AOF always,但要接受性能下降,同时做好主从和哨兵。
  • 需要快速恢复的大实例:RDB 为主 + AOF 兜底,或者混合持久化。

提示:无论选哪种,都要定期验证备份文件的可用性。我见过太多团队配了持久化,结果真出事时发现 RDB 文件损坏或者 AOF 文件不完整,白配了。建议定期做恢复演练,把备份文件拉到测试环境实际恢复一次。

4. 性能管理:内存、慢查询与连接数调优

4.1 内存管理:maxmemory 与淘汰策略

Redis 是内存数据库,内存管理是性能的生命线。核心参数是maxmemory,它限制 Redis 能用的最大内存。如果不设,Redis 会一直吃内存直到把机器撑爆,触发 OOM Killer,进程被杀,这比主动淘汰还糟糕。

maxmemory 8gb maxmemory-policy allkeys-lru

淘汰策略有八种,常用的几种:

  • noeviction:不淘汰,写入报错。适合当存储用、数据不能丢的场景。
  • allkeys-lru:所有 key 里淘汰最近最少使用的。适合纯缓存。
  • volatile-lru:只淘汰设了过期时间的 key。适合缓存和持久数据混存的场景。
  • allkeys-lfu / volatile-lfu:按访问频率淘汰,比 LRU 更精准,适合热点数据明显的场景。

选策略的逻辑:如果所有数据都是缓存,用 allkeys-lru;如果一部分是缓存一部分是持久数据,用 volatile-lru,并给缓存 key 都设过期时间;如果热点集中,用 lfu 系列。

这里有个坑:淘汰是采样近似算法,不是精确的。maxmemory-samples控制采样数量,默认 5,调大更精确但更耗 CPU。一般 5-10 就够了,不用调太高。

4.2 慢查询日志:定位性能瓶颈的利器

Redis 的慢查询日志记录执行时间超过阈值的命令。配置两个参数:

slowlog-log-slower-than 10000 # 超过 10000 微秒(10ms)记录 slowlog-max-len 128 # 最多保留 128 条

slowlog-log-slower-than设 10ms 是个合理的起点。注意单位是微秒,别设错了。设 0 会记录所有命令,性能影响大;设负数则关闭。

查看慢查询用SLOWLOG GET,会返回命令、执行时间、时间戳等信息。我排查性能问题时,第一步就是看慢查询日志,往往能直接定位到问题命令。常见的慢命令有:keys *(全量扫描,生产禁用)、hgetall大 hash、smembers大 set、sort大列表、zrange大范围等。

注意:慢查询日志只记录命令执行时间,不包括网络传输和排队时间。所以如果慢查询日志里没有慢命令,但客户端还是感觉慢,那问题可能出在网络、连接数或者阻塞命令上。

4.3 连接数与客户端缓冲区管理

连接数方面,maxclients控制最大客户端连接数,默认 10000。如果连接数打满,新连接会被拒绝。排查连接泄漏要看CLIENT LIST,找出空闲连接和异常连接。

客户端缓冲区是另一个容易被忽视的点。Redis 为每个客户端维护输入缓冲和输出缓冲,如果客户端消费慢,输出缓冲会不断增长,吃光内存。相关参数:

client-output-buffer-limit normal 0 0 0 client-output-buffer-limit replica 256mb 64mb 60 client-output-buffer-limit pubsub 32mb 8mb 60

格式是"硬限制 软限制 软限制持续秒数"。超过硬限制立即断开,超过软限制且持续超过指定秒数则断开。对于从节点和发布订阅客户端,一定要设限制,否则一个慢从节点就能把主节点内存拖垮。

4.4 大 key 和热 key 的治理

大 key 是性能杀手。一个几 MB 的 hash 或 list,操作一次就可能阻塞主线程几百毫秒。治理方法:一是拆分,把大 key 拆成多个小 key,比如按 hash 字段分片;二是用SCAN系列命令渐进式遍历,替代keys和hgetall;三是定期用redis-cli --bigkeys扫描发现大 key。

热 key 则是访问过于集中,单个 key 的 QPS 过高,可能打满单个分片。治理方法:一是本地缓存,在客户端缓存热 key;二是读写分离,把读请求分散到多个从节点;三是 key 拆分,给热 key 加随机后缀分散到多个 key 上。

5. 常见问题排查与避坑经验实录

5.1 故障转移后数据丢失怎么办

这是哨兵方案最常被问到的问题。原因通常是:主节点宕机时,有一部分写命令还没同步到从节点,切换后这部分数据就丢了。Redis 的复制是异步的,这个窗口无法完全消除,只能缩小。

缓解手段:一是调小repl-backlog-size相关的断连容忍时间,让从节点尽量跟上;二是用min-replicas-to-write和min-replicas-max-lag参数,强制主节点在有足够健康从节点时才接受写入,牺牲可用性换一致性。

min-replicas-to-write 1 min-replicas-max-lag 10

意思是至少 1 个从节点延迟不超过 10 秒,主节点才接受写。这样能大幅减少切换时的数据丢失,但代价是如果从节点都挂了,主节点会拒绝写入。

5.2 内存突然暴涨的排查思路

内存暴涨一般有几个原因:大 key 写入、客户端输出缓冲堆积、复制积压缓冲过大、碎片率过高。排查步骤:

  1. INFO memory看 used_memory、used_memory_rss、mem_fragmentation_ratio。
  2. 如果碎片率超过 1.5,考虑开启 activedefrag 或重启实例。
  3. CLIENT LIST看有没有输出缓冲异常的客户端。
  4. redis-cli --bigkeys扫描大 key。
  5. 检查是否有大量 key 没设过期时间,导致只增不减。

5.3 常见问题速查表

现象可能原因排查命令解决方向
主从切换后数据缺失异步复制窗口INFO replication配置 min-replicas
内存持续增长大 key / 无过期INFO memory, --bigkeys拆分 key,设过期
响应变慢慢命令 / 阻塞SLOWLOG GET优化命令,禁用 keys
连接被拒连接数打满CLIENT LIST调大 maxclients,查泄漏
从节点频繁全量同步backlog 太小INFO stats调大 repl-backlog-size
fork 卡顿内存过大LATENCY DOCTOR控制单实例内存

5.4 我踩过的几个真实坑

第一个坑是持久化文件把磁盘写满。AOF 文件增长很快,如果磁盘没监控,写满之后 Redis 会拒绝写入。后来我加了磁盘告警,并且把 AOF 重写阈值调低,让它更频繁地压缩。

第二个坑是哨兵和主从配置不一致。有一次主节点设了密码,哨兵配置里忘了加sentinel auth-pass,导致哨兵无法监控,故障转移失效。配置变更一定要同步到所有相关组件。

第三个坑是客户端没处理重连。故障转移期间连接断开,客户端没做重试,业务直接报错。后来在客户端加了连接池和重试逻辑,才稳定下来。

第四个坑是大 key 导致的阻塞。有个业务往一个 list 里塞了几十万条数据,一次lrange就卡住主线程。后来改成用lrange分批取,并且限制 list 长度,问题才解决。

5.5 监控指标清单

最后给一份我常用的监控指标清单,这些指标建议都接入告警:

  • 可用性:主从状态、哨兵数量、故障转移次数。
  • 性能:QPS、命中率、慢查询数量、平均响应时间。
  • 内存:used_memory、碎片率、淘汰 key 数量。
  • 持久化:RDB 最后保存时间、AOF 文件大小、重写次数。
  • 连接:当前连接数、拒绝连接数、输出缓冲大小。
  • 复制:主从偏移量差、全量同步次数。

命中率低于 90% 就要警惕了,可能是缓存穿透或者 key 设计有问题。淘汰 key 数量持续增长,说明内存不够用,要么扩容要么优化数据结构。

这套东西说起来多,但真正落地之后,Redis 的稳定性会有质的提升。我个人的体会是,高可用、持久化、性能管理这三块,任何一块偷懒,最后都会以线上故障的形式还回来。与其出事之后救火,不如一开始就把配置做扎实,把监控做全,把演练做够。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进