在生产环境中 ,一般要保证服务的高可用,有时候只需要选出一台机器来执行,其余机器处于备用状态,比如,在分布式系统中很常见的一个问题就是定时任务的执行。如果多台机器同时执行相同的定时任务,业务复杂则可能出现灾难性的后果。我使用的是当当网的elastic-job分布式定时任务框架,分一片,部署两台机器,其中一台处于备用状态,只有一台机器是工作的,当这台机器宕机了,备用机器才开始工作。
分布式锁和Master选举相似点
上一篇博客讲了curator的分布式锁应用,分布式锁和 Master选举有几种相似点,实际上其实现机制也相近:
同一时刻只有一个获取锁 / 只能有一个leader
对于分布式排他锁来说,任意时刻,只能有一个进程(对于单进程内的锁是单线程)可以获得锁。
对于领导选举来说,任意时刻,只能有一个成功当选为leader。否则就会出现脑裂。
锁重入 / 确认自己是leader
对于分布式锁,需要保证获得锁的进程在释放锁之前可再次获得锁,即锁的可重入性。
对于领导选举,Leader需要能够确认自己已经获得领导权,即确认自己是Leader。
释放锁 / 放弃领导权
锁的获得者应该能够正确释放已经获得的锁,并且当获得锁的进程宕机时,锁应该自动释放,从而使得其它竞争方可以获得该锁,从而避免出现死锁的状态。
领导应该可以主动放弃领导权,并且当领导所在进程宕机时,领导权应该自动释放,从而使得其它参与者可重新竞争领导而避免进入无主状态。
感知锁释放 / 感知领导权释放
当获得锁的一方释放锁时,其它对于锁的竞争方需要能够感知到锁的释放,并再次尝试获取锁。
原来的Leader放弃领导权时,其它参与方应该能够感知该事件,并重新发起选举流程。
Curator中选举分为两种:
Leader Latch和Leader Election
Leader Latch
LeaderLatch方式就是以一种抢占方式来决定选主,是一种非公平的领导选举,谁抢到就是谁,会随机从候选者中选择一台作为leader, 选中后除非leader自己 调用close()释放leadership,否则其他的候选者不能成为leader。
选主过程
假设现在又三个zookeeper的客户端,如下图所示,同时竞争leader。这三个客户端同时向zookeeper集群注册Ephemeral且Non-sequence类型的节点,路径都为/zkroot/leader。
如上图所示,由于是Non-sequence节点,这三个客户端只会有一个创建成功,其它节点均创建失败。此时,创建成功的客户端(即上图中的Client 1)即成功竞选为 Leader 。其它客户端(即上图中的Client 2和Client 3)此时匀为 Follower。
放弃领导权
如果Leader打算主动放弃领导权,直接删除/zkroot/leader节点即可。
如果Leader进程意外宕机,其与Zookeeper间的Session也结束,该节点由于是Ephemeral类型的节点,因此也会自动被删除。
此时/zkroot/leader节点不复存在,对于其它参与竞选的客户端而言,之前的Leader已经放弃了领导权。
感知领导权的放弃
由上图可见,创建节点失败的节点,除了成为 Follower 以外,还会向/zkroot/leader注册一个 Watch ,一旦 Leader 放弃领导权,也即该节点被删除,所有的 Follower 会收到通知。
重新选举
感知到旧 Leader 放弃领导权后,所有的 Follower 可以再次发起新一轮的领导选举,如下图所示。
从上图中可见
新一轮的领导选举方法与最初的领导选举方法完全一样,都是发起节点创建请求,创建成功即为Leader,否则为Follower,且Follower会Watch该节点。
新一轮的选举结果,无法预测,与它们在第一轮选举中的顺序无关。这也是该方案被称为非公平模式的原因。
Leader Latch模式总结
Leader Latch实现很简单,每一轮的选举算法都一样。
非公平模式,每一次选举都是随机,谁抢到就是谁的,假如是第二次选举,每个 Follower 通过 Watch 感知到节点被删除的时间不完全一样,只要有一个 Follower 得到通知即发起竞选。
给zookeeper造成的负载大,假如有上万个客户端都参与竞选,意味着同时会有上万个写请求发送给 Zookeper。同时一旦 Leader 放弃领导权,Zookeeper 需要同时通知上万个 Follower,负载较大。
使用过程
相关的类
LeaderLatch
构造LeaderLatch ,构造方法如下:
public LeaderLatch(CuratorFramework client, String latchPath);
public LeaderLatch(CuratorFramework client, String latchPath, String id);
启动
通过start()方法启动之后,再等待几秒钟后,Curator会自动从中选举出Leader。
public void start() throws Exception;
可以调用实例的hasLeadership()判断该实例是否为leader。
public boolean hasLeadership();
尝试获取leadership
调用await()方法会使线程一直阻塞到获得leadership为止。
public void await() throws InterruptedException, EOFException;
public boolean await(long timeout, TimeUnit unit) throws InterruptedException;
释放leadership
只能通过close()释放leadership, 只有leader将leadership释放时,其他的候选者才有机会被选为leader
public void close() throws IOException;
public synchronized void close(CloseMode closeMode) throws IOException;
示例代码
public class TestLeaderLatch {
private static final String PATH = "/demo/leader";
/** 5个客户端 */
private static final Integer CLIENT_COUNT = 5;
public static void main(String[] args) throws Exception {
//5个线程,5个客户端
ExecutorService service = Executors.newFixedThreadPool(CLIENT_COUNT);
for (int i = 0; i < CLIENT_COUNT ; i++) {
final int index = i;
service.submit(new Runnable() {
@Override
public void run() {
try {
new TestLeaderLatch().schedule(index);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
//休眠50秒之后结束main方法
Thread.sleep(30 * 1000);
service.shutdownNow();
}
private void schedule(int thread) throws Exception {
//获取一个client
CuratorFramework client = this.getClient(thread);
//获取一个latch
LeaderLatch latch = new LeaderLatch(client, PATH,String.valueOf(thread));
//给latch添加监听,在
latch.addListener(new LeaderLatchListener() {
@Override
public void notLeader() {
//如果不是leader
System.out.println("Client [" + thread + "] I am the follower !");
}
@Override
public void isLeader() {
//如果是leader
System.out.println("Client [" + thread + "] I am the leader !");
}
});
//开始选取 leader
latch.start();
//每个线程 休眠时间不一样,但是最大不能超过 main方法中的那个休眠时间,那个是50秒 到时候main方法结束 会中断休眠时间
Thread.sleep(2 * (thread + 5) * 1000);
if (latch != null) {
//释放leadership
//CloseMode.NOTIFY_LEADER 节点状态改变时,通知LeaderLatchListener
latch.close(LeaderLatch.CloseMode.NOTIFY_LEADER);
}
if (client != null) {
client.close();
}
System.out.println("Client [" + latch.getId() + "] Server closed...");
}
private CuratorFramework getClient(final int thread) {
RetryPolicy rp = new ExponentialBackoffRetry(1000, 3);
// Fluent风格创建
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("192.168.58.42:2181")
.sessionTimeoutMs(1000000)
.connectionTimeoutMs(3000)
.retryPolicy(rp)
.build();
client.start();
System.out.println("Client [" + thread + "] Server connected...");
return client;
}
}
程序运行,输出以下结果:
Client [3] Server connected…
Client [2] Server connected…
Client [4] Server connected…
Client [0] Server connected…
Client [1] Server connected…
Client [1] I am the leader !
Client [0] Server closed…
Client [1] I am the follower !
Client [1] Server closed…
Client [2] I am the leader !
Client [2] I am the follower !
Client [2] Server closed…
Client [4] I am the leader !
Client [3] Server closed…
Client [4] I am the follower !
Client [4] Server closed…
在上面的程序中,启动了5个zookeeper客户端,程序会随机选中其中一个作为leader。通过注册监听的方式来判断自己是否成为leader。调用close()方法释放当前领导权。有可能优先close的并不是leader节点,但是当leader节点close的时候,可以继续在已有的节点中重新选举leader节点。
LeaderElection
上面讲了怎么使用LeaderLatch方式进行master选举,Curator提供了两种选举,一种是LeaderLatch,提供的另一种Leader选举策略是Leader Election。
跟LeaderLatch选举策略相比,LeaderElection选举策略不同之处在于每个实例都能公平获取领导权,而且当获取领导权的实例在释放领导权之后,该实例还有机会再次获取领导权。
另外,选举出来的leader不会一直占有领导权,当 takeLeadership(CuratorFramework client) 方法执行结束之后会自动释放领导权。LeaderElection属于公平的选举方式,通过LeaderSelectorListener可以对领导权进行控制, 在适当的时候释放领导权,这样每个节点都有可能获得领导权。 而LeaderLatch则一直持有leadership, 除非调用close方法,否则它不会释放领导权。
选主过程
如下图所示,LeaderElection选举中,各客户端均创建/zkroot/leader节点,且其类型为Ephemeral与Sequence。
由于是Sequence类型节点,故上图中三个客户端均创建成功,只是序号不一样。此时,每个客户端都会判断自己创建成功的节点的序号是不是当前最小的。如果是,则该客户端为 Leader,否则即为 Follower。
在上图中,Client1 创建的节点序号为1 ,Client2创建的节点序号为2,Client3创建的节点序号为3。由于最小序号为 1 ,且该节点由Client1创建,故Client 1为 Leader 。
放弃领导权
Leader 如果主动放弃领导权,直接删除其创建的节点即可。
如果 Leader 所在进程意外宕机,其与 Zookeeper 间的 Session 结束,由于其创建的节点为Ephemeral类型,故该节点自动被删除。
感知领导权的放弃
与LeaderLatch方式不同,每个 Follower 并非都 Watch 由 Leader 创建出来的节点,而是 Watch 序号刚好比自己序号小的节点。
在上图中,总共有 1、2、3 共三个节点,因此Client 2 Watch /zkroot/leader1,Client 3 Watch /zkroot/leader2。(注:序号应该是10位数字,而非一位数字,序号最大为int最大值)。
一旦Leader弃权或者宕机,/zkroot/leader1被删除,Client2可得到通知。此时Client3由于 Watch 的是/zkroot/leader2,故不会得到通知。
重新选举
Client2得到/zkroot/leader1被删除的通知后,不会立即成为新的 Leader 。而是先判断自己的序号2是不是当前最小的序号。在该场景下,其序号确为最小。因此Client 2成为新的 Leader 。
这里要注意,如果在Client1放弃领导权之前,Client2就宕机了,Client3会收到通知。此时Client3不会立即成为Leader,而是要先判断自己的序号3是否为当前最小序号。很显然,由于Client1创建的/zkroot/leader1还在,因此Client 3不会成为新的 Leader ,并向Client2序号2 前面的序号,也即 1 创建 Watch。该过程如下图所示。
LeaderElection模式总结
扩展性好,每个客户端都只Watch 一个节点且每次节点被删除只须通知一个客户端
旧 Leader 放弃领导权时,其它客户端根据竞选的先后顺序(也即节点序号)成为新 Leader,这也是公平模式的由来。
延迟相对非公平模式要高,因为它必须等待特定节点得到通知才能选出新的 Leader。
使用过程
相关的类
LeaderSelector
LeaderSelectorListener
LeaderSelectorListenerAdapter
CancelLeadershipException
使用方法 创建 LeaderSelector
public LeaderSelector(CuratorFramework client, String leaderPath, LeaderSelectorListener listener);
public LeaderSelector(CuratorFramework client, String leaderPath, ExecutorService executorService, LeaderSelectorListener listener);
public LeaderSelector(CuratorFramework client, String leaderPath, CloseableExecutorService executorService, LeaderSelectorListener listener);
启动
leaderSelector.start();
1
一旦启动,如果获取了leadership的话,takeLeadership()会被调用,只有当leader释放了leadership的时候,takeLeadership()才会返回。
释放
调用close()释放 leadership
leaderSelector.close();
1
示例代码
LeaderSelectorListener的实现类
实现LeaderSelectorListener 或者 继承LeaderSelectorListenerAdapter,用于定义获取领导权后的业务逻辑:
public class CustomLeaderSelectorListenerAdapter extends LeaderSelectorListenerAdapter implements Closeable {
/** 客户端名称 */
private String name;
/** leaderSelector */
private LeaderSelector leaderSelector;
/** 原子性的 用来记录获取 leader的次数 */
public AtomicInteger leaderCount = new AtomicInteger(1);
public CustomLeaderSelectorListenerAdapter(CuratorFramework client,String path,String name ){
this.name = name;
this.leaderSelector = new LeaderSelector(client, path, this);
/**
* 自动重新排队
* 该方法的调用可以确保此实例在释放领导权后还可能获得领导权
*/
leaderSelector.autoRequeue();
}
/**
* 启动 调用leaderSelector.start()
* @throws IOException
*/
public void start() throws IOException {
leaderSelector.start();
}
/**
* 获取领导权之后执行的业务逻辑,执行完自动放弃领导权
* @param client
* @throws Exception
*/
@Override
public void takeLeadership(CuratorFramework client) throws Exception {
final int waitSeconds = 2;
System.out.println(name + "成为当前leader" + " 共成为leader的次数:" + leaderCount.getAndIncrement() + "次");
try{
//模拟业务逻辑执行2秒
Thread.sleep(TimeUnit.SECONDS.toMillis(waitSeconds));
}catch ( InterruptedException e ){
System.err.println(name + "已被中断");
Thread.currentThread().interrupt();
}finally{
System.out.println(name + "放弃领导权");
}
}
@Override
public void close() throws IOException {
leaderSelector.close();
}
}
多个客户端测试
public class TestLeaderElection {
private static final String PATH = "/demo/leader";
/** 3个客户端 */
private static final Integer CLIENT_COUNT = 3;
public static void main(String[] args) throws Exception {
ExecutorService service = Executors.newFixedThreadPool(CLIENT_COUNT);
for (int i = 0; i < CLIENT_COUNT; i++) {
final int index = i;
service.submit(new Runnable() {
@Override
public void run() {
try {
new TestLeaderElection().schedule(index);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
Thread.sleep(30 * 1000);
service.shutdownNow();
}
private void schedule(final int thread) throws Exception {
CuratorFramework client = this.getClient(thread);
CustomLeaderSelectorListenerAdapter leaderSelectorListener =
new CustomLeaderSelectorListenerAdapter(client, PATH, "Client #" + thread);
leaderSelectorListener.start();
}
private CuratorFramework getClient(final int thread) {
RetryPolicy rp = new ExponentialBackoffRetry(1000, 3);
// Fluent风格创建
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("192.168.58.42:2181")
.sessionTimeoutMs(1000000)
.connectionTimeoutMs(3000)
.retryPolicy(rp)
.build();
client.start();
System.out.println("Client [" + thread + "] Server connected...");
return client;
}
}
运行程序,输出以下内容:
Client [0] Server connected…
Client [1] Server connected…
Client [2] Server connected…
Client #2成为当前leader 共成为leader的次数:1次
Client #2放弃领导权
Client #0成为当前leader 共成为leader的次数:1次
Client #0放弃领导权
Client #1成为当前leader 共成为leader的次数:1次
Client #1放弃领导权
Client #2成为当前leader 共成为leader的次数:2次
Client #2放弃领导权
Client #0成为当前leader 共成为leader的次数:2次
Client #0放弃领导权
Client #1成为当前leader 共成为leader的次数:2次
Client #1放弃领导权
Client #2成为当前leader 共成为leader的次数:3次
Client #2放弃领导权
Client #0成为当前leader 共成为leader的次数:3次
Client #0放弃领导权
Client #1成为当前leader 共成为leader的次数:3次
Client #1放弃领导权
Client #2成为当前leader 共成为leader的次数:4次
Client #2放弃领导权
Client #0成为当前leader 共成为leader的次数:4次
Client #0放弃领导权
Client #1成为当前leader 共成为leader的次数:4次
Client #1放弃领导权
上面只是简单测试代码,并没有关闭client等操作,每个实例在获取领导权后,如果 takeLeadership(CuratorFramework client) 方法执行结束,将会释放其领导权。而且获取领导权 也是按照 Client #2, Client #0 ,Client #1 顺序来的,正好验证了它的公平性。
LeaderSelectorListener类继承了ConnectionStateListener。一旦LeaderSelector启动,它会向curator客户端添加监听器。 使用LeaderSelector必须时刻注意连接的变化。一旦出现连接问题如SUSPENDED,curator实例必须确保它可能不再是leader,直至它重新收到RECONNECTED。如果LOST出现,curator实例不再是leader并且其takeLeadership()应该直接退出。
推荐的做法是,如果发生SUSPENDED或者LOST连接问题,最好直接抛CancelLeadershipException,此时,leaderSelector实例会尝试中断并且取消正在执行takeLeadership()方法的线程。
建议扩展LeaderSelectorListenerAdapter, LeaderSelectorListenerAdapter中已经提供了推荐的处理方式 。
参考
http://curator.apache.org/curator-recipes/leader-latch.html
http://curator.apache.org/curator-recipes/leader-election.html
http://www.jasongj.com/zookeeper/distributedlock/