文章

ZooKeeper

ZooKeeper 笔记

一句话理解

ZK 就是分布式系统的管家——集群里各节点需要配合、同步状态,ZK 负责维持秩序、记录状态、通知变化。


场景一:分布式锁(怕大家打架)

猫趣场景:用户给帖子点赞,同一毫秒点了 10 次,多台服务器同时判断”还没赞过”,结果点赞数 +10。

ZK 怎么解决

1
2
3
请求到服务器 A → 去 ZK 创建 /lock/like_{userId}_{postId} → 成功 → 执行点赞
请求到服务器 B → 去 ZK 创建 /lock/like_{userId}_{postId} → 失败(名字重复)→ 排队等
服务器 A 处理完 → 删掉节点 → ZK 通知 B → B 创建成功 → 发现已赞过 → 跳过

核心原理:ZK 同一路径下不允许两个同名节点,谁先创建谁拿到锁。

代码实现

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
public void likePost(Long userId, Long postId) {
    String lockPath = "/lock/like_" + userId + "_" + postId;

    // 1. 抢锁
    boolean locked = tryLock(lockPath);
    if (!locked) {
        waitForLock(lockPath);       // 等通知
        locked = tryLock(lockPath);  // 重试
    }

    try {
        likeService.doLike(userId, postId);
    } finally {
        zkClient.delete(lockPath);   // 释放锁
    }
}

/** 尝试创建锁节点,创建成功 = 拿到锁 */
private boolean tryLock(String lockPath) {
    try {
        zkClient.create(lockPath, CreateMode.EPHEMERAL);
        return true;
    } catch (NodeExistsException e) {
        return false;  // 节点已存在,锁被别人占了
    }
}

/** 注册 Watch 监听,等前一个节点被删除 */
private void waitForLock(String lockPath) {
    CountDownLatch latch = new CountDownLatch(1);

    Stat stat = zkClient.exists(lockPath, event -> {
        if (event.getType() == Event.EventType.NodeDeleted) {
            latch.countDown();  // 锁释放了,放行
        }
    });

    if (stat == null) return;  // 节点已不存在,不用等

    try {
        latch.await(30, TimeUnit.SECONDS);  // 阻塞等待,最多 30 秒
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

面试怎么答:利用 ZK 节点路径唯一的特性,多个请求同时创建同一路径的临时节点,只有一个能成功(拿到锁),其余注册 Watch 等待。锁释放后 ZK 自动通知等待者重试。


场景二:Master 选举(怕老大挂了)

2.1 业务选举(应用层)

猫趣场景:AI 诊断定时任务(每天凌晨批量分析猫咪健康数据),Master 节点挂了没人干活。

ZK 怎么解决

1
2
3
4
5
6
7
8
9
服务 A 启动 → 创建临时节点 /election/ai-health-00000001
服务 B 启动 → 创建临时节点 /election/ai-health-00000002
服务 C 启动 → 创建临时节点 /election/ai-health-00000003

规则:编号最小的是 Master → A 是老大,负责跑批量 AI 诊断
B、C 监听 A 的节点(Watch)

A 断电 → 临时节点自动消失 → ZK 通知 B、C
B、C 重新看列表 → 002 最小 → B 自动晋升为 Master → 接管 AI 诊断任务

核心原理:临时节点(人走节点灭)+ 顺序节点(排队编号)= 自动选老大。

代码实现

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
/**
 * 服务启动时调用,参与 Master 选举
 */
@PostConstruct
public void joinElection() {
    String electionPath = "/election/ai-health";

    // 1. 创建临时顺序节点
    String myNode = zkClient.create(electionPath + "/node-", CreateMode.EPHEMERAL_SEQUENTIAL);
    // myNode = "/election/ai-health/node-00000001"

    // 2. 判断自己是不是 Master(编号最小)
    if (isMaster(electionPath, myNode)) {
        becomeMaster();
    } else {
        // 3. 不是 Master,监听当前 Master 节点
        watchMaster(electionPath, myNode);
    }
}

/** 判断自己是不是编号最小的节点 */
private boolean isMaster(String electionPath, String myNode) {
    List<String> children = zkClient.getChildren(electionPath);
    Collections.sort(children);
    String masterNode = electionPath + "/" + children.get(0);
    return myNode.equals(masterNode);
}

/** 成为 Master,开始干活 */
private void becomeMaster() {
    log.info("我是 Master,开始执行 AI 诊断任务");
    // 启动定时任务:每天凌晨批量分析猫咪健康数据
    scheduler.scheduleAtFixedRate(this::runAiDiagnosis, 0, 24, TimeUnit.HOURS);
}

/** 监听 Master 节点,挂了我就顶上 */
private void watchMaster(String electionPath, String myNode) {
    List<String> children = zkClient.getChildren(electionPath);
    Collections.sort(children);
    String masterNode = electionPath + "/" + children.get(0);

    zkClient.exists(masterNode, event -> {
        if (event.getType() == Event.EventType.NodeDeleted) {
            // Master 挂了,重新选举
            if (isMaster(electionPath, myNode)) {
                becomeMaster();
            } else {
                watchMaster(electionPath, myNode);  // 继续监听新的 Master
            }
        }
    });
}

面试怎么答:每个服务启动时创建临时顺序节点,编号最小的是 Master,其余节点 Watch 监听 Master 节点。Master 挂了临时节点自动消失,ZK 通知其余节点重新选举,编号最小的自动晋升。


2.2 Kafka 集群选举(中间件层)

概念

概念 说明
Broker Kafka 集群的一台服务器实例
Controller 其中一台 Broker 兼任的“调度员”,负责管理分区 和 Leader 分配
排班表 元数据,存在 ZK 里,记录每个 Partition 的 Leader 是谁
消息数据 存在各 Broker 磁盘上

Controller 的职责

平时不管事:消息写入是生产者直接找 Leader Broker。

1
2
生产者发消息 → 查本地缓存的元数据 → 直接发给 Leader Broker
Leader Broker 写磁盘 + 同步给 Follower → 返回成功

排班表变了才介入

1
2
3
4
5
新 Topic 创建 → Controller 决定每个分区的 Leader 是哪个 Broker → 写入 ZK
Broker 挂了    → Controller 从 Follower 里选新 Leader → 更新 ZK
               → 生产者发失败 → 刷新元数据 → 自动切换到新 Leader
扩容加 Broker  → Controller 感知新节点 → 新 Topic 会自动分配到新 Broker
               → 已有 Topic 不会自动平衡,需运维手动触发分区迁移

ZK 怎么选举 Controller

1
2
3
4
5
6
Broker 1/2/3 启动 → 都去 ZK 抢创建 /controller(临时节点,同路径)
只有 1 个成功 → 成为 Controller
其余 Broker 监听 /controller(Watch)

Controller 挂了 → /controller 消失 → ZK 通知其余 Broker
其余 Broker 再次抢创建 → 新的 Controller 诞生

和业务选举的区别

  业务选举 Kafka 选举
方式 顺序节点排队 同路径抢创建
老大挂了 编号第二的直接晋升 剩下的重新抢
为什么 希望有序递补 谁当调度员无所谓

面试怎么答:Kafka 的 Controller 选举靠 ZK 临时节点唯一性——所有 Broker 同时创建 /controller,只有一个成功,其余 Watch 等待。挂了重新抢。和业务选举的区别是:业务用顺序节点排队递补,Kafka 用同路径抢创建,因为谁当调度员无所谓。注意 Controller 不在消息链路上,生产者直接找 Leader Broker 写入。


2.3 ZK 自身选举(底层协议)

场景:ZK 集群自己也要选 Leader(3 台 ZK 服务器之间)。

怎么选

1
2
3
4
5
6
7
8
ZK 启动时互相通信,交换两个值:
  - myid:服务器编号(1、2、3)
  - ZXID:事务 ID(谁的数据最新)

投票规则:
  1. 先比 ZXID,大的优先(数据越新越有资格当 Leader)
  2. ZXID 相同,比 myid,大的优先
  3. 超过半数投给同一个 → 当选

ZAB 协议

ZAB(ZooKeeper Atomic Broadcast)是 ZK 内部协议,干两件事:

1
2
3
4
5
6
7
8
1. Leader 选举(崩溃恢复)
   集群启动或 Leader 挂了 → 互相投票 → 过半当选

2. 数据同步(原子广播)
   客户端写数据 → 发给 Leader
   Leader 广播给所有 Follower
   过半 Follower 确认 → 写成功
   “原子” = 要么都成功,要么都不成功,不会有的写了有的没写

和业务选举的区别:这是 ZK 内部的协议,业务服务感知不到。靠的是过半机制防止脑裂。

为什么过半能防止脑裂

1
2
3
4
5
6
7
8
9
10
11
网络分区 = 网络断了,集群被切成两拨

5 台 ZK,网络断了:
  拨A:ZK1、ZK2       → 最多 2 票,需要 3 票 → 选不出 Leader
  拨B:ZK3、ZK4、ZK5  → 能凑 3 票 → 选出 Leader ✅

只有一边能凑够半数 → 不会出现两个 Leader

为什么部署奇数台(3、5、7):
  偶数台(4 台)对半切 → 两边各 2 票 → 都选不出 → 集群不可用
  奇数台(5 台)怎么切 → 总有一边过半 → 至少一边能工作

面试怎么答:ZK 自身 Leader 选举用 ZAB 协议——比较 ZXID(数据新旧)和 myid(服务器编号),过半投票当选。ZAB 还负责数据同步:Leader 广播写操作,过半确认才成功。网络分区时只有一边能凑够半数,不会出现两个 Leader。


场景三:配置中心(怕改配置麻烦)

猫趣场景:运营要把”每日免费 AI 诊断次数”从 3 次改成 5 次(促销活动),50 台服务实例不能逐个重启。

ZK 怎么解决

1
2
3
4
5
6
7
配置存在 ZK:/config/biz/daily_free_diagnosis = "3"
50 台服务实例启动时读取这个值,并死死盯着它(Watch)

运营改配置 → ZK 值变成 "5"
ZK 主动推送给 50 台实例:"配置变了"
实例内存里配置瞬间更新,不用重启
用户马上就能多领 2 次免费诊断

核心原理:Watch 机制 = 一处修改,处处生效。

面试怎么答:Watch 就像餐厅等位——你跟服务员说”有空桌叫我”(注册监听),有空桌了他来通知你(一次性回调),你还想等下一桌得重新说(Watch 是一次性的)。

本文由作者按照 CC BY 4.0 进行授权