一、问题背景 1.环境背景 容器化部署一个哨兵集群与多个主从集群,用户通过哨兵对外暴露的地址与<MasterName>
获取主从集群地址进行业务操作。
注:业务端使用jedis的JedisSentinelPool
进行接入。
2.模拟故障 手动删除一个主从集群中的所有主从实例pod,等待operator介入自愈。
operator检测到全量宕机,按顺序执行如下操作:
拉起进程
进行选主
恢复复制关系
移除并重新添加哨兵监控配置
等待自愈完成后,pod地址已全部变化,故哨兵中<MasterName>
对应的主地址也已经变化。
3.业务异常 全量宕机故障注入后,jedis业务操作出现异常,持续重试且失败。
operator介入自愈完成后,集群状态已恢复正常,但存量的jedis业务客户端并未恢复,扔在重试且失败。
重启jedis业务客户端或新启动业务客户端,均可以正常操作主从集群。
使用lettuce客户端接入模拟此流程,可以正常恢复。
二、相关源码分析
redis源码版本: 6.2.x jedis源码版本: 5.2.0
这种故障场景,lettuce客户端可以正常自愈,但是jedis客户端不行,说明lettuce没有刷新主从实例拓扑。
之前看过lettuce源码,可以得知lettuce的主从拓扑刷新是通过订阅哨兵事件来实现的;所以首先推测jedis也是类似思路,猜测这次异常是因为jedis没有收到事件导致的。
1. jedis源码 首先先找下JedisSentinelPool
相关的源码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 protected class MasterListener extends Thread { ... @Override public void run () { ... while (running.get()) { try { if (!running.get()) { break ; } final HostAndPort hostPort = new HostAndPort (host, port); j = new Jedis (hostPort, sentinelClientConfig); List<String> masterAddr = j.sentinelGetMasterAddrByName(masterName); if (masterAddr == null || masterAddr.size() != 2 ) { LOG.warn("Can not get master addr, master name: {}. Sentinel: {}." , masterName, hostPort); } else { initMaster(toHostAndPort(masterAddr)); } j.subscribe(new JedisPubSub () { @Override public void onMessage (String channel, String message) { LOG.debug("Sentinel {} published: {}." , hostPort, message); String[] switchMasterMsg = message.split(" " ); if (switchMasterMsg.length > 3 ) { if (masterName.equals(switchMasterMsg[0 ])) { initMaster(toHostAndPort(Arrays.asList(switchMasterMsg[3 ], switchMasterMsg[4 ]))); } else { LOG.debug( "Ignoring message on +switch-master for master name {}, our master name is {}" , switchMasterMsg[0 ], masterName); } } else { LOG.error("Invalid message received on Sentinel {} on channel +switch-master: {}" , hostPort, message); } } }, "+switch-master" ); } catch (JedisException e) { if (running.get()) { LOG.error("Lost connection to Sentinel at {}:{}. Sleeping 5000ms and retrying." , host, port, e); try { Thread.sleep(subscribeRetryWaitTimeMillis); } catch (InterruptedException e1) { LOG.error("Sleep interrupted: " , e1); } } else { LOG.debug("Unsubscribing from Sentinel at {}:{}" , host, port); } } finally { if (j != null ) { j.close(); } } } } ... }
可以看出来,jedis这边刷新拓扑仅能通过+switch-master
事件触发,没有像lettuce一样订阅多种事件。
而且拓扑刷新的操作也比较简单,就是把哨兵channel的msg拆解后直接更新。
2. redis源码 然后再看下+switch-master
事件相关的源码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 void sentinelProcessHelloMessage (char *hello, int hello_len) { ... if (numtokens == 8 ) { ... if (si && master->config_epoch < master_config_epoch) { master->config_epoch = master_config_epoch; if (master_port != master->addr->port || !sentinelAddrEqualsHostname(master->addr, token[5 ])) { sentinelAddr *old_addr; sentinelEvent(LL_WARNING,"+config-update-from" ,si,"%@" ); sentinelEvent(LL_WARNING,"+switch-master" , master,"%s %s %d %s %d" , master->name, announceSentinelAddr(master->addr), master->addr->port, token[5 ], master_port); old_addr = dupSentinelAddr(master->addr); sentinelResetMasterAndChangeAddress(master, token[5 ], master_port); sentinelCallClientReconfScript(master, SENTINEL_OBSERVER,"start" , old_addr,master->addr); releaseSentinelAddr(old_addr); } } ... } ... } ... void sentinelFailoverSwitchToPromotedSlave (sentinelRedisInstance *master) { sentinelRedisInstance *ref = master->promoted_slave ? master->promoted_slave : master; sentinelEvent(LL_WARNING,"+switch-master" ,master,"%s %s %d %s %d" , master->name, announceSentinelAddr(master->addr), master->addr->port, announceSentinelAddr(ref->addr), ref->addr->port); sentinelResetMasterAndChangeAddress(master,ref->addr->hostname,ref->addr->port); }
在redis中,+switch-master
事件只会在两种情况下被发出,相关源码如上。
哨兵配置过期,通过其余哨兵同步配置后,会发出+switch-master
事件。
哨兵触发failover,且走到了需要将从实例提升为主的步骤后,会发出+switch-master
事件。
三、问题原因分析 通过上文,可以较为清晰的汇总出如下信息:
使用jedis作为业务客户端接入哨兵主从,jedis通过+switch-master
事件刷新拓扑。
当redis哨兵间不存在过期配置时,redis仅在failover
流程中发出+switch-master
事件。
全量宕机场景operator介入自愈时,未触发failover
流程。
由此问题已经被发现,即jedis使用哨兵接入时,并未考虑到全量宕机恢复这种场景,jedis仅能通过+switch-master
事件来感知拓扑变化。
lettuce在这方面显然做的更全面一些,会监听多种哨兵事件来刷新拓扑,所以在operator介入自愈后,可以通过感知其他事件来刷新拓扑。
四、问题解决 上文已知问题根因为jedis未感知拓扑变化,所以我们手动触发一次failover
流程让jedis感知即可。
经过测试,在手动触发failover
流程后,jedis业务客户端恢复,可以正常执行业务操作。
从operator视角来看,全量宕机自愈之后,自动触发一次failover
流程理论上也能解决这个问题,但是failover
流程相对不可控因素太多了,不太稳定。
所以operator这边暂时不进行适配,考虑到这种场景概率还是比较低的,我觉得提供SOP手册即可。