5.3 判例三:业务集群的主节点选举 本节摘要:让业务集群自己选出唯一主节点:各实例在选举目录下创建临时节点占位,主节点意外退出后由监听者自动补位。本判例对比手写占位版与 Curator 的 LeaderLatch、LeaderSelector 两种风格,并给出"主节点该干什么"的清单。 背景:定时任务谁来做 十台实例组成的推送服务,每晚的对账任务只该跑一份——这与 5.2 的锁判例需求相仿,但语义不同:锁是"一次性资格",选主是长期身份。主节点除了跑任务,通常还负责全局节流、消费分发、状态汇报这类"同一时刻只该有一个角色"的工作。业务选主要求三件事:常态下全集群有且仅有一个主;主挂了秒级补位;补位过程不产生双主。
本节摘要:让业务集群自己选出唯一主节点:各实例在选举目录下创建临时节点占位,主节点意外退出后由监听者自动补位。本判例对比手写占位版与 Curator 的 LeaderLatch、LeaderSelector 两种风格,并给出"主节点该干什么"的清单。
十台实例组成的推送服务,每晚的对账任务只该跑一份——这与 5.2 的锁判例需求相仿,但语义不同:锁是"一次性资格",选主是长期身份。主节点除了跑任务,通常还负责全局节流、消费分发、状态汇报这类"同一时刻只该有一个角色"的工作。业务选主要求三件事:常态下全集群有且仅有一个主;主挂了秒级补位;补位过程不产生双主。
最小实现只需要一个临时节点加一次 exists 监听:
public class MiniLeader { private final CuratorFramework client; private final String path = "/election/push-master"; public void campaign() throws Exception { try { // 抢座:原子创建,成者为王 client.create().withMode(CreateMode.EPHEMERAL) .forPath(path, InetAddress.getLocalHost().getHostName().getBytes()); onBecomeLeader(); } catch (NodeExistsException e) { // 败者为臣:监听王座,等它空出来 client.checkExists().usingWatcher(evt -> { try { campaign(); } // 王座消失,重新参选 catch (Exception ex) { /* 退避重试 */ } }).forPath(path); } } private void onBecomeLeader() { System.out.println("我是主节点:启动对账调度与全局节流"); // 主节点职责在此挂载;同时要挂自己的连接状态监听, // LOST 时必须停止主节点行为——资格已随会话吊销 } }
骨架成立,但有两个粗糙处:败者重试没有退避(主节点频繁切换时全员疯狂参选);主节点 LOST 后如果旧实例还活着(GC 停顿假死),恢复后的它并不知道"王座已经易主",需要重新走一遍 campaign 并先校验自己的临时节点是否还在。这两个粗糙处,正是 Curator 组件存在的理由。
Curator 提供两种风格,区别在"任期"的语义。LeaderLatch 是"把门人":所有实例 latch 到同一个门上,门内同时只有一人,start 后通过 hasLeadership() 或监听器得知自己是否当选,close 或挂掉即让出,其余排队者自动补位:
LeaderLatch latch = new LeaderLatch(client, "/election/push-master", "inst-" + myId); latch.addListener(new LeaderLatchListener() { @Override public void isLeader() { startScheduling(); // 当选:启动主节点职责 } @Override public void notLeader() { stopScheduling(); // 让位:必须干净地停掉全部主节点行为 } }); latch.start(); // start 后自动参选,无需手写重试 // 状态查询:运维接口里暴露当前是否主节点 boolean isMaster = latch.hasLeadership();
LeaderSelector 是"轮班制":你申请的任务不绑定身份,而是抢到后执行 takeLeadership 方法,方法返回即让位,且 autoRequeue 可让下岗者自动重新排队。适合"主节点任务做完就该换人"的公平轮转场景:
LeaderSelector selector = new LeaderSelector(client, "/election/push-master", new LeaderSelectorListenerAdapter() { @Override public void takeLeadership(CuratorFramework c) throws Exception { runOneRoundOfSettlement(); // 做一轮主节点工作 Thread.sleep(60_000); // 本轮任期一分钟,返回即让位 } }); selector.autoRequeue(); // 让位后自动重新排队 selector.start();
选型口诀:身份型用 Latch,任务型用 Selector。长期主节点(调度器、节流器)用前者;排队干活(一批批处理轮着来)用后者。两者的底层都是 5.2 的临时顺序节点排号,Curator 在上面补了退避、状态机与让位语义。

验证脚本:起三个实例参选,确认日志只有一个"当选";kill 主实例,观察补位时间(会话超时加参选延迟,通常几秒);最狠的一招——对主实例触发一次 30 秒的 GC 停顿(用压测工具造内存压力),观察它醒来后是否正确让位。三道题都过,选举才算投产合格。
最后一个坑属于"过度设计":给主节点挂太多职责,一次补位要重建连接池、重放积压队列、预热缓存,补位时间从秒级拖到分钟级。主节点职责清单的第一条永远是:能拆就拆,主节点只留真正全局唯一的活。
问:业务选主和 4.2 节 ZooKeeper 自己的选举什么关系? 同源不同层:ZooKeeper 内部选举用完整的 ZAB 规则选日志最新的节点当 Leader;业务选主只是"借它的强一致存储做占位竞争",谁的请求先到谁占位。理解这层区别,就不会把两者混为一谈。
第四个判例回到"名单":实例们如何登记报到,消费者如何拿到一份始终新鲜的名单。