ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Redis Cluster故障转移全解析:从PFAIL到新主上线,生产环境踩坑实录

Redis Cluster故障转移全解析:从PFAIL到新主上线,生产环境踩坑实录 如果你在线上维护过Redis大概率碰过这种场面某个分片的主节点悄无声息地宕了业务侧报错刷屏而集群日志里只留下几行不起眼的PFAIL、FAIL记录。分片集群的故障转移说白了就是主节点挂了之后集群怎么保证还能继续读写、怎么选出新主、以及“看起来不干活”的那几秒里到底发生了什么。这篇文章不聊八股只讲我在生产环境里反复核对过的原理、完整流程和踩坑经验适合正在用或者准备上Redis Cluster的运维、DBA和后端同学。1. 故障转移前的准备先想清楚Cluster靠什么扛住一次宕机1.1 单机、哨兵、Cluster是三种不同档位的“高可用”很多人上来就把哨兵模式和Cluster混在一起比其实这俩解决的问题不一样。单机Redis不用说了再强的机器也有天花板CPU、内存、网络任何一个到了瓶颈你都只能干看着。主从加哨兵解决的是“高可用”也就是主节点挂了之后哨兵去挑一个新的主节点让业务尽量不中断但它不解决容量问题——所有的写还是落在同一个主节点上。Cluster不一样。它把数据库拆成16384个槽均匀分摊到多个主节点上每个主节点再挂一个或多个从节点。数据分散到多台机器写压力被拆开容量也能水平扩展。Cluster自带了故障转移能力相当于“分片”和“高可用”一揽子方案。所以选型的时候逻辑很简单单机内存够、QPS不高用主从哨兵就够了一旦数据量超过单机内存、写并发顶不住才需要上Cluster。这里必须提醒一句Cluster不是万金油。节点越多Gossip心跳、槽位迁移、故障检测的复杂度是跟着涨的。小型业务强行上Cluster运维成本可能比收益还大。1.2 16384个槽位是怎么把主从编排起来的Cluster里每个key是怎么定位到具体节点的流程是CRC16(key) 16383得到一个0到16383之间的槽位编号然后由负责这个槽区间的主节点来处理。创建集群的时候redis-cli会把这些槽尽量平均分到各个主节点上比如三主节点就是0-5460、5461-10922、10923-16383这种区间。每个主节点底下挂着从节点从节点的作用很简单备份。主节点挂了从节点顶上主节点活着从节点平时不承担写请求。这里有一个常见误区——很多人以为Cluster里的从节点可以像主从模式一样分担读流量实际上如果没开READONLY命令普通客户端不会把读请求路由到从节点。想分担读需要客户端层面显式做读写分离而且还要处理好数据延迟的问题。还有一个细节值得说为什么槽位数量是16384而不是65536或者更多因为Cluster的节点之间靠Gossip消息互相通信槽位分布信息会用bitmap压缩在心跳包里。16384个槽位对应2KB的bitmap足够覆盖上千节点规模的设计目标再往上只会增加心跳包体积收益却不大。所以官方就用16384作为约定你要在网上看到有人问“为什么不是65536”答案基本就在这个带宽考量上。1.3 什么样的故障才需要“故障转移”不是所有Redis进程退出都需要转移。如果挂掉的是从节点整个集群完全不受影响只是这个分片少了一层冗余数据还在主节点上。哪怕这个从节点一直不恢复也只是容错能力下降了。真正需要转移的是主节点挂了。主节点挂了意味着它负责的那段槽位区间没了写入口读请求如果命中也只能靠从节点碰运气。还有一种情况需要特别注意某个槽区间既没有主节点、也没有从节点接管或者主节点和从节点同时不可用这个分片的数据就彻底不能访问了。如果cluster-require-full-coverage还是默认的yes整个集群都会对外报CLUSTERDOWN所有key读写全部拒绝。我在生产环境里习惯把cluster-require-full-coverage改成no这样单个分片故障不会拖垮整个集群至少其他分片的数据还能正常访问。代价是客户端会收到针对缺失槽位的MOVED或CLUSTERDOWN错误业务侧要做好这种局部不可用的降级方案。这个取舍很重要后面排查部分会再展开。2. 故障转移链路拆解从PFAIL到新主上线的完整剧本2.1 第一阶段PFAIL主观下线节点说“我觉得它可能挂了”Cluster里的每个节点都在做两件事一是服务客户端的读写二是和其他节点通过Gossip协议交换状态。节点之间会定时发送PING消息默认情况下每秒会随机挑几个节点做一次心跳探测。如果某个节点在cluster-node-timeout时间内没有返回有效响应发现方就会在自己的视角里把这个节点标记为PFAIL——也就是subjective fail主观下线。注意“主观”这个词很关键。PFAIL只是单个节点自己的判断它不会立刻广播出去而是会在后续的心跳包里携带这些信息让其他节点知道“A节点报告B节点可能挂了”。这个过程不是秒级的因为cluster-node-timeout默认值是15000毫秒等于一个节点要等15秒没有任何响应才会被某个节点记为PFAIL。这个参数直接影响后面故障转移的速度后面第3章我专门讲调整经验。这里有个小坑如果节点所在的内存满了、CPU打满、或者网络抖动PING响应变慢就很容易被其他节点误判为PFAIL。尤其是大key操作把节点卡住的时候心跳线程和服务线程之间相互影响极端情况下会出现集群把活着的节点当成死了在处理。所以监控里加节点存活率和心跳RTT很重要。2.2 第二阶段FAIL客观下线多数派主节点达成共识单个节点说PFAIL没有效力要多数主节点都确认才行。Cluster里有一个规矩只有主节点有投票资格从节点没有。当一个节点收到足够多的其他主节点投票认定某个节点确实不可达时就会把这个节点从PFAIL升级为FAIL也就是客观下线。这个“足够多”是有讲究的通常理解为超过半数的主节点。多数派机制的意义很直接如果只有少数几个节点认为A挂了可能是这几个节点之间的网络有问题而不是A真的挂了只有多数节点都认为A不可达A才大概率是真的没救了。这种设计也是后面防止脑裂的基础。一旦节点被标记为FAIL这个消息会通过Gossip扩散到整个集群。每个节点都知道了某分片的主节点没有了。这个时候集群并不会自动宣告只读而是进入下一个更关键的环节——让从节点站出来竞选新主。2.3 第三阶段从节点发起选举用epoch机制选出一个新主从节点发现自己的主节点已经是FAIL状态第一步不是立刻切换而是先检查自己有没有资格接管。这里主要看两个指标一是自己的数据落后主节点多少二是主从断连了多久。如果落后太多或者断连时间超过了cluster-replica-validity-factor乘以cluster-node-timeout从节点会放弃这次选举因为没有意义——上任之后数据缺了一大截业务更没法接受。有资格的从节点会等待一段错峰时间然后发起选举。它会把currentEpoch加1向其他主节点广播请求投票。每个主节点在一个epoch里只能投一票得票数超过半数主节点的从节点才能成为新主。这个过程本质上和Raft里的Leader选举很像都是靠多数派票数来保证唯一性。当选之后新主会广播PONG消息告诉整个集群自己接管了这一段槽位区间然后把原主节点名和IP替换掉。从我实际观察看从节点选举不是瞬间完成的整个过程里面既有心跳等待又有投票交互通常需要几秒到十几秒。这段时间里旧主节点负责的槽位是不可写的客户端会感受到一段明显超时窗口。2.4 为什么不会出现“双主脑裂”这是Cluster设计里最漂亮的部分。假设发生了网络分区旧主节点在一侧从节点和大多数主节点在另一侧。从节点成功选主需要超过半数主节点投票。如果从节点所在的分区只是少数派没有集合到过半主节点它就永远无法完成选举自然也就不会出现两个主节点同时对外服务的脑裂情况。但这里有个容易被忽视的代价如果客户端在少数派那一侧还继续往旧主节点写数据这些写入最终会随着旧主节点被集群排除而丢失。旧主节点恢复通信之后会发现自己已经不在集群的多数派里只能重新以从节点身份同步新主的数据。那个时间窗口内产生的写入就是实实在在的数据丢失。这个特性直接引出分布式锁的经典痛点——后面第4章我会单独说。3. 亲手把主节点打挂一次完整的故障转移演练3.1 用6个实例搭一个最小Cluster理论讲再多不如亲手触发一次。我先用6个Redis进程在本地搭一个最简集群端口7000到7005三主三从。每个实例的redis.conf里必须开启这些配置port 7000 cluster-enabled yes cluster-config-file nodes-7000.conf cluster-node-timeout 5000 appendonly yes daemonize yescluster-enabled必须为yes否则节点启动后不会进入Cluster模式。cluster-config-file是每个节点维护集群元数据的文件千万不能多个实例共用同一个文件。如果开了密码还要注意所有节点必须用同一个requirepass并且设置masterauth否则节点间同步认证过不去故障转移一定失败——这个坑我见过太多人踩。6个实例都启动后用一条命令完成集群创建redis-cli --cluster create \ 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 \ 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \ --cluster-replicas 1这个命令会自动做三件事分配主节点、分配从节点、迁移槽位。执行完会输出每个主节点负责的槽位区间和从节点归属。之后用cluster nodes确认布局redis-cli -p 7000 -c cluster nodes输出里每个节点一行包含节点ID、IP:端口、角色标志master/slave、负责槽位等。我把7000、7001、7002设为主节点它们底下分别挂着7003、7004、7005作为从节点。记住主从之间的对应关系后面要有用。3.2 关闭主节点观察故障转移过程选7000这个主节点下手。先往集群里写几个带前缀的key确认数据能正常路由然后直接杀掉进程kill -9 pid-of-7000这里我故意用kill -9模拟最极端情况让它连优雅退出的机会都没有。紧接着观察集群状态redis-cli -p 7002 -c cluster infocluster_state会从ok变成failcluster_slots_assigned还是16384但cluster_slots_ok会变成不完整。再看cluster nodes你会发现7000被标记为FAIL而7003的状态开始变化。在cluster-node-timeout设置为5000毫秒的情况下从杀掉主节点到新主上线我实测大约在8到13秒之间。这个窗口期业务写入会超时读请求如果命中了由7000负责的槽区也会部分失败。如果你用的是默认15000毫秒整个转移过程拉到30秒甚至更久也很正常。等新主上线后再查一次cluster nodes你会看到7003变成了master并且接管了原本属于7000的所有槽位。集群的cluster_state重新恢复为ok槽位分配也回到了完整状态。整个过程不需要任何人工干预这就是Cluster自愈能力。3.3 原主节点回归集群后发生了什么现在把7000重新启动你是不是以为它会拿回自己的主节点位置并不会。7000启动后会先以从节点身份找自己的新主节点——也就是7003——做全量或增量同步然后变成7003的从节点。槽位不会自动迁回原来的主节点。这是一个非常重要的认知Cluster的故障转移没有“自动回切”机制主挂了从顶上原主回来只能当从哪怕你原来的主性能更强、优先级更高也要靠人工或调度逻辑才能把主身份切回去。我在生产环境里的做法是故障转移后如果原主节点所在机器指标没问题我会在低峰期手动执行一次CLUSTER FAILOVER把主身份优雅地切回去让节点布局回到初始状态。这里又分两种情况# 平滑切换从节点先复制完最新数据再提升 redis-cli -p 7003 -c cluster failover # 强制切换不等数据完全对齐 redis-cli -p 7003 -c cluster failover force常规场景用第一种就好第二种适合原主节点已经活过来但网络又不稳的极端场景。注意CLUSTER FAILOVER是在从节点上执行的目标就是让自己成为主节点。3.4 想让故障转移更快调整三个关键参数如果默认15秒的超时让你觉得转移太慢可以从下面三个参数入手。cluster-node-timeout是最核心的。它决定了从“节点没响应”到“PFAIL”的时间默认15000毫秒。我生产环境一般压到5000到8000毫秒再低就别了网络一抖动就可能大面积误判。这个参数最好在创建集群前就定好改的时候所有节点一起改并且优先改配置文件再滚动重启。cluster-replica-validity-factor控制从节点允许落后多少才不参加选举。默认10配合cluster-node-timeout5000就是允许从节点和主节点断连50秒内都还有资格接管。如果这个值设得太小数据传输稍微慢一点从节点就直接放弃竞选集群反而失去冗余能力。replica-priority是老命令slave-priority的换代版数字越小优先级越高。如果你有多个从节点可以通过它控制哪个从节点最优先被选为新主。合理设置可以让性能更好、数据更全的从节点接管而不是每次随机挑一个。4. 故障转移里的翻车现场与调优手段4.1 数据丢了怎么办异步复制的客观窗口故障转移最容易让人崩溃的问题是转移结束数据少了。原因不复杂Redis主从复制默认是异步的主节点写成功就返回客户端成功但从节点什么时候拿到这条写是不确定的。主节点突然宕机最后那几秒的写如果还没同步到从节点选主之后这些数据就永久丢了。这个窗口有多小正常情况下主从同步延迟是毫秒级的只要网络健康丢的数据量极少。但一旦主节点卡顿、主从之间网络抖动、或者backlog不够导致从节点在做全量同步窗口就会急剧放大。我处理过一次事故主节点在做RDB持久化时卡了几秒期间业务写入高峰从节点没跟上切换完一核对丢了将近两万条订单状态更新。缓解手段是有的把appendonly打开并设成always能让每一条写先落盘再返回主节点宕机的瞬间数据至少还留在AOF文件里。但这只保证主节点本地不丢不保证复制不丢。想要让这次写也被从节点确认只能用WAIT命令强制等待同步拿响应时间和吞吐量去换一致性。Redis Cluster在CAP里本身就是优先保可用性的如果你的业务对写入丢零容忍架构上就得考虑别把所有宝押在一个Redis集群上。4.2 分布式锁在主从切换时到底安不安全热词里“redis分布式锁”常年是搜索重点这里必须说清楚在主从加故障转移的场景里用setnx实现的分布式锁是有漏洞的。线程A在旧主节点上拿到锁写入还没同步到从节点旧主挂了新主顶上锁数据根本不存在的线程B再来setnx就能成功拿锁。这时候线程A和线程B同时认为自己持有锁互斥就失效了。Redisson的watchdog续期只能保证持有锁的线程在释放前一直持有但解决不了主从复制窗口带来的锁丢失。网上有红锁方案但红锁本身也有争议它需要客户端同时写入多个独立Redis实例容错模型和Cluster根本不一样别拿单集群的多个节点冒充红锁。我的建议很朴素如果你的业务真的犟到这个程度要么接受极端场景下锁可能失效要么把分布式锁独立部署成至少三套互相隔离的Redis节点要么换etcd或者ZooKeeper这类把一致性做进共识机制的系统。对大部分场景Redis锁合理的业务幂等才是性价比更高的组合。4.3 “脑裂”的真实表现客户端还在写旧主网络分区不一定总是机器宕机。两个机房的网络断掉旧主节点所在的机房可能还在正常对外服务客户端照样往它里面写而另一个机房的从节点已经完成选举变成新主。分区恢复之后旧主节点会发现自己被孤立只能降级为从节点同步新主数据把分区期间写入的数据全部丢弃。这种场景比“主节点宕机”更隐蔽也更难排查。你去看集群状态一切正常你去看监控没有节点宕机但数据就是悄悄少了。关键就在于多数派判断。旧主所在的少数派无法被集群整体接受它写进去的数据最终会被抛弃。排查这类问题不能只看集群层更要把网络分区、交换机故障、专线抖动纳入故障预案里。我在做演练时会刻意模拟网络分区而不仅仅是kill进程用iptables把某台主节点的所有集群端口和客户端端口都DROP掉观察集群会不会正确地把这台节点踢出以及恢复网络后能不能平滑回归。这种演练比单纯kill更能暴露真实问题。4.4 客户端为什么迟迟感知不到新主节点集群自己转移成功了不代表业务侧就恢复了。很多人的体验是Redis集群看起来一切正常但应用日志里还是大量JedisConnectionException、Lettuce Command timed out隔了很长时间才自动恢复。问题往往出在客户端对拓扑变化的感知上。JedisCluster相对好一点它收到MOVED重定向后会更新本地槽位到节点的映射下一次命令就会去找新节点。Lettuce的情况就比较棘手如果没开启拓扑刷新连接池里还留着旧主节点的连接请求一直往已经不存在的地址上打只能等连接超时然后重建连接期间业务自然报错。解决方式很直接用Lettuce做Spring Data Redis底层时显式配置ClusterTopologyRefreshOptionsClusterTopologyRefreshOptions refreshOptions ClusterTopologyRefreshOptions.builder() .enablePeriodicRefresh(Duration.ofSeconds(30)) .enableAllAdaptiveTriggers() .build();自适应触发会在收到MOVED、ASK、连接断开时主动刷新拓扑比固定周期更灵敏。再把刷新周期调到30秒左右别设太频繁避免每个客户端都在高频轮询集群元数据。Redisson也有类似的机制默认会周期更新节点连接但同样建议把更新间隔调小一些具体视业务规模而定。这些配置我都是先在自己项目里压测过一遍才上生产的。客户端拓扑刷新没配好等于是集群故障转移成功业务侧却还在原地等死这个锅不能甩给Redis。4.5 参数速查表参数默认值作用我的生产建议cluster-node-timeout15000ms判断节点不可达的超时时间直接影响故障检测速度5000-8000ms再低容易误判cluster-replica-validity-factor10限制从节点断连多久后仍可参与选举乘cluster-node-timeout得到允许时长保持10别低于5replica-priority0从节点优先级数字越小越优先被选为新主按机器性能分别设置cluster-require-full-coverageyes槽位不全时是否让整个集群对外拒绝服务业务可容忍局部失败时改norepl-backlog-size1MB主节点保存最近的复制数据backlog不足会导致从节点全量同步至少64MB按写入量估算5. 常见问题排查实录与个人心得5.1 高发故障排查速查表症状可能原因排查方向某个分片key全部报CLUSTERDOWN该分片主从都挂了或槽位未覆盖查cluster info、cluster nodes确认槽位归属故障转移后丢了最近几秒写入异步复制窗口主从未同步完成检查主从repl offset差距考虑WAIT或AOF always从节点一直不发起选举cluster-replica-validity-factor导致从节点放弃查从节点日志中“Failover auth denied”类信息集群转移成功但业务侧持续超时客户端没有刷新集群拓扑配置Lettuce/Jedis/Redisson的拓扑刷新机制某个节点频繁被PFAIL内存、CPU打满心跳响应慢压大key、调内存淘汰策略排查节点负载集群整体变成只读require-full-coverageyes且部分槽不可用确认故障范围临时改参数或恢复故障分片5.2 演练前必须做的三件事第一确认数据可回滚。故障转移演练不是闹着玩的任何一次选举切换都有极小概率触发旧主数据问题。演练前手动触发一次BGSAVE把RDB备份下来心里才有底。第二确认客户端配置。如果业务应用用的还是默认拓扑配置演练大概率会变成压测客户端超时能力。先把拓扑刷新和重试策略调好再动手。第三确认变更窗口和通知口径。低峰期执行提前通知业务研发准备好一条“如果30秒内没自动恢复立即执行XX方案”的回退路径。演练的第一种姿势推荐用CLUSTER FAILOVER而不是kill -9。它是优雅切换从节点会尽量把数据追平再接管业务影响最小适合例行验证。等这套跑顺了再考虑在预发环境做kill -9这种破坏性演练。5.3 故障转移后还有什么容易被忽略转移完成、业务恢复不代表事情结束了。我最在意的是三件事一是槽位分布切主之后槽位可能落在一台性能不太够的机器上要尽快评估是否回切或做reshard二是从节点同步状态新主节点上线后原本的从节点要重新挂上去做全量同步如果数据量大这个同步过程会在主从之间产生不小的网络和磁盘压力三是复制积压情况看看repl_backlog是不是被覆盖如果覆盖了说明从节点断的时间太久需要提前准备全量同步的资源。另外我一直强调故障转移和缓存治理是联动的。热词里的“缓存穿透”“缓存治理”经常和Redis集群问题一起出现。缓存穿透打的是单一分片如果这个分片恰好承担了大量热点keyCPU会被打高心跳响应变慢进而被误判为故障。我处理过几次线上抖动根因都不是节点宕机而是热key把节点CPU顶到90%以上导致Gossip超时。所以排查分片集群问题时先看一眼节点CPU和慢查询日志再判断是不是真的“挂了”。故障转移是一个机制但它不是一个终点。真正的稳定运行靠的是对每一个环节的敬畏参数是不是合理的客户端是不是有感知能力的网络是不是足够稳定的容灾演练是不是定期在做的。等这些都形成了习惯你再看PFAIL、FAIL这些日志就会发现它们不过是系统在正常工作而已。最后说一个实用小技巧。修改cluster-node-timeout这类参数时千万别只在一两个节点上改Cluster节点之间的判断标准必须一致否则会出现A节点觉得B挂了、B节点觉得A还活着的尴尬状态。我的做法是写成一个配置模板下发到每个节点用脚本统一校验配置哈希然后再滚动重启。这件事我踩过机会不大、但影响不小的坑保持所有节点配置一致才是分片集群长期稳定运行的第一步。
返回列表