Curator
Curator中提供了Zookeeper各种应用场景(分布式锁、Master选举机制和分布式计算器等)的抽象封装。
项目组件:
| 名称 | 描述 |
|---|---|
| Recipes | Zookeeper典型应用场景的实现,这些实现是基于Curator Framework。 |
| Framework | Zookeeper API的高层封装,大大简化Zookeeper客户端编程,添加了例如Zookeeper连接管理、重试机制等。 |
| Utilities | 为Zookeeper提供的各种实用程序。 |
| Client | Zookeeper client的封装,用于取代原生的Zookeeper客户端(ZooKeeper类),提供一些非常有用的客户端特性。 |
| Errors | Curator如何处理错误,连接问题,可恢复的例外等。 |
创建节点
(1)创建一个初始内容为空的节点:client.create().forPath(path); Curator默认创建的是持久节点,内容为空。
(2)创建一个包含内容的节点:client.create().forPath(path,"我是内容".getBytes());
(3)创建临时节点,并递归创建父节点client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path); 在递归创建父节点时,父节点为持久节点。
删除节点
(1)删除一个子节点client.delete().forPath(path);
(2)删除节点并递归删除其子节点client.delete().deletingChildrenIfNeeded().forPath(path);
(3)指定版本进行删除client.delete().withVersion(1).forPath(path); 如果此版本已经不存在,则删除异常,异常信息如下。
(4)强制保证删除一个节点client.delete().guaranteed().forPath(path); 只要客户端会话有效,那么Curator会在后台持续进行删除操作,直到节点删除成功。比如遇到一些网络异常的情况,此guaranteed的强制删除就会很有效果。
读取数据
读取节点数据内容API相当简单,Curator提供了传入一个Stat,使用节点当前的Stat替换到传入的Stat的方法,查询方法执行完成之后,Stat引用已经执行当前最新的节点Stat。
// 普通查询
client.getData().forPath(path);
// 包含状态查询
Stat stat = new Stat();
client.getData().storingStatIn(stat()).forPath(path);
更新数据
更新数据,如果未传入version参数,那么更新当前最新版本,如果传入version则更新指定version,如果version已经变更,则抛出异常。
// 普通更新
client.setData().forPath(path,"新内容".getBytes());
// 指定版本更新
client.setData().withVersion(1).forPath(path);
版本不一致异常信息:
org.apache.zookeeper.KeeperException$BadVersionException: KeeperErrorCode = BadVersion for
异步接口
在使用以上针对节点的操作API时,我们会发现每个接口都有一个inBackground()方法可供调用。此接口就是Curator提供的异步调用入口。对应的异步处理接口为BackgroundCallback。此接口指提供了一个processResult的方法,用来处理回调结果。其中processResult的参数event中的getType()包含了各种事件类型,getResultCode()包含了各种响应码。重点说一下inBackground的以下接口:public T inBackground(BackgroundCallback callback, Executor executor); 此接口就允许传入一个Executor实例,用一个专门线程池来处理返回结果之后的业务逻辑。
源码分析
LeaderLatch
LeaderLatch的方式,是以一种抢占的方式来决定选主。类似非公平锁的抢占,所以多节点是一个随机产生主节点的过程。
使用
LeaderLatch创建好之后,必须执行:leaderLatch.start();这样,才能让leaderLatch开始参与选主过程。由于LeaderLatch是一个不断抢占的过程,所以需要调用:public boolean hasLeadership()来检测当前参与者是否选主成功。这个方法是非阻塞的(立即返回),其结果只代表调用时的选主结果。所以,可以轮询此方法,或者当执行完本地逻辑后,需要执行分布式任务前检擦此方法。不过,类似JDK中的CountDownLatch,LeaderLatch也提供了阻塞方法:
public void await()
throws InterruptedException,
EOFException
这个方法,会阻塞,直到选主成功。
方法2 为了避免方法1的长时间选主失败
public boolean await(long timeout,
TimeUnit unit)
throws InterruptedException
这个方法会根据参数中指定的时间,作为等待的期限。到期后,返回选主结果。
对于LeaderLatch实例,无论是否轩主成功,最后都应该调用:
leaderLatch.close();
这样,才会把当前参与者的信息从选主分组中移除出去。如果,当前参与者是主,还会释放主的资格。避免死锁。
4. 错误处理
在实际使用中,必须考虑链接问题引起的主身份丢失问题。 例如:当hasLeadership()返回true,之后链接出问题。 强烈建议:使用LeaderLatch时为其添加一个ConnectionStateListener
LeaderLatch实例会添加一个ConnectionStateListener来监听当前zk链接。 如果,链接不可用(SUSPENDED)则LeaderLatch会认为自己不在是主,等到链接恢复可用时,才可继续。 如果,链接断开(LOST),则LeaderLatch会认为自己不在是主,等到链接重新建立后,删除之前的参与者信息,然后重新参与选主。
成员变量
log : caurtor依赖slf4j
client : zk客户端(curator-framework提供)
latchPath : 分组路径(zk中的path)
id : 参与者ID
state
内部枚举
状态
LATENT 休眠
STARTED 已启动
CLOSED 已关闭
使用AtomicReference原子化包装
hasLeadership
是否为主
使用AtomicBoolean原子化包装
ourPath
使用AtomicReference原子化包装
listeners
一组LeaderLatchListener监听器
closeMode
内部枚举
LeaderLatch关闭方式
SILENT : 静默关闭,不触发相关监听器
NOTIFY_LEADER :关闭时触发监听器
startTask
异步Future
使用AtomicReference原子化包装
listener
链接状态监听器
参见 : 4. 错误处理
LOCK_NAME
私有常量
sorter
私有常量
用于锁处理时,规范path
对参与者进行排序
debugResetWaitLatch
volatile 可见性
reset()使用
在测试时控制启动的时机,防止环境未初始化完成就处理了启动逻辑
注意:这些成员变量都是final类型。并且,对于引用类型都进行原子化包装,避免并发问题
启动
LeaderLatch是由start()启动选主过程:
public void start() throws Exception {
Preconditions.checkState(state.compareAndSet(State.LATENT, State.STARTED), "Cannot be started more than once");
startTask.set(AfterConnectionEstablished.execute(client, new Runnable()
{
@Override
public void run()
{
try{
internalStart();
}finally{
startTask.set(null);
}
}
}));
}
可以发现
调用原子性CAS方法,将状态由休眠更新到已启动
执行了一个异步任务来完成启动过程
使用一个链接可用后回调方式
AfterConnectionEstablished.execute()
内部使用了一个ThreadUtils.newSingleThreadExecutor
单线程的线程池
所以本地多个LeaderLatch实例的启动过程是序列化方式执行的
使用成员变量startTask持有异步Future
启动完成后会制空startTask
说明启动过程可能会有状态变化
启动的过程实际是由internalStart()方法来完成
private synchronized void internalStart() {
if ( state.get() == State.STARTED )
{
client.getConnectionStateListenable().addListener(listener);
try{
reset();
} catch ( Exception e )
{
ThreadUtils.checkInterrupted(e);
log.error("An error occurred checking resetting leadership.", e);
}
}
}
internalStart()使用synchronized
同步调用
使用this进行互斥锁对象
同一个LeaderLatch对象的多次启动同样序列化执行
即便绕过第2步,也同样可以保证不会重复启动
进行状态判断
synchronized内部,再次判断
相当于Double check
在当前连接上注册自带的监听器
调用reset()完成启动逻辑
处理了异常
触发线程中断
internalStart()是异步执行,通过中断可以进行更细节的控制
避免粗暴的抛出异常
internalStart()是异步执行
避免当前线程意外中断
同时也避免了单线程的线程池频繁的进行线程开/关所带来的额外开销
void reset() throws Exception {
setLeadership(false);
setNode(null);
BackgroundCallback callback = new BackgroundCallback(){};
client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).inBackground(callback).forPath(ZKPaths.makePath(latchPath, LOCK_NAME), LeaderSelector.getIdBytes(id));
}
private synchronized void setLeadership(boolean newValue)
{
boolean oldValue = hasLeadership.getAndSet(newValue);
if ( oldValue && !newValue )
{ // Lost leadership, was true, now false
listeners.forEach(new Function<LeaderLatchListener, Void>()
{
@Override
public Void apply(LeaderLatchListener listener)
{
listener.notLeader();
return null;
}
});
}
else if ( !oldValue && newValue )
{ // Gained leadership, was false, now true
listeners.forEach(new Function<LeaderLatchListener, Void>()
{
@Override
public Void apply(LeaderLatchListener input)
{
input.isLeader();
return null;
}
});
}
notifyAll();
}
private void setNode(String newValue) throws Exception
{
String oldPath = ourPath.getAndSet(newValue);
if ( oldPath != null )
{
client.delete().guaranteed().inBackground().forPath(oldPath);
}
}
初始化选主状态false
getAndSet设置
根据不同的情况触发不同的监听器
得到
失去
notifyAll()
唤醒所有的synchronized等待
制空上次path
如果上一次path有残留,则delete服务器上的信息
在latchPath下创建一个EPHEMERAL_SEQUENTIAL节点
临时顺序节点
并注册了回调
回掉获取latchPath的子节点
并判断自身是否为主
选主
LeaderLatch的选主判断逻辑, 实际由checkLeadership()方法处理:
当获取到最新的参与者列表后:
对列表进行排序
如果自身处于列表第一位,则当选为主
否则,在latchPath上增加监听/回调
监听列表中上一位参与者
当上一位参与者退出(节点被删除时)
重新getChildren()再次进行选主
当latchPath发生变动(如:删除)
调用reset(),重新进行启动过程
即可导致hasLeadership()失效
Leader Election
Leader Election一种基于选举而非抢占的选主方式。
关键APIorg.apache.curator.framework.recipes.leader.LeaderSelector
主APIorg.apache.curator.framework.recipes.leader.LeaderSelectorListener选主监听器。继承自org.apache.curator.framework.state.ConnectionStateListener,通知主节点选主成功
org.apache.curator.framework.recipes.leader.LeaderSelectorListenerAdapter
选主监听器的一个抽象类
增加了对链接状态监听的默认实现
用以处理ZooKeeper链接问题引发的选主状态不同步
org.apache.curator.framework.recipes.leader.CancelLeadershipException
在 ConnectionStateListener.stateChanged()抛出
会引发LeaderSelector.interruptLeadership()调用
主身份被打断
实现机制
Leader Election内部通过一个分布式锁来实现选主;并且选主结果是公平的,ZooKeeper会按照各节点请求的次序成为主节点。LeaderSelector创建好之后,必须执行:leaderSelector.start();启动后,如果当选为主则会触发监听器中的takeLeadership()方法。和Leader Latch一样,无论结果如何最终应该调用:leaderSelector.close();
错误处理
LeaderSelectorListener继承自ConnectionStateListener。 当LeaderSelector启动后,会自动添加监听。 使用LeaderSelector时,必须关注链接状态的变化。 如果当选为主,应该处理链接中断:SUSPENDED,以及链接丢失:LOST。 当遇到SUSPENDED状态时,实例必须认为自己不再是主了,直到链接恢复到RECONNECTED状态。 当遇到LOST状态,实例不再是主了,并且应该退出takeLeadership方法。
==重要:==建议当遇到SUSPENDED以及LOST时,直接抛出CancelLeadershipException异常 这样,会让LeaderSelector尝试中断任务执行并取消执行线程对takeLeadership 的执行。 正是因为这样,才提供了一个LeaderSelectorListenerAdapter,来处理上述逻辑。 所以,在实际使用中最好继承LeaderSelectorListenerAdapter使用。
源码分析
LeaderSelectorListener
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.state.ConnectionStateListener;
public interface LeaderSelectorListener extends ConnectionStateListener
{
public void takeLeadership(CuratorFramework client) throws Exception;
}
继承自ConnectionStateListener,选主的有效性有链接状态密切关注
takeLeadership方法
当选为主后,此方法被调用
此方法用于执行任务
不用立即返回
直到打算放弃主时,才应该结束此方法
LeaderSelectorListenerAdapter
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.state.ConnectionState;
public abstract class LeaderSelectorListenerAdapter implements LeaderSelectorListener
{
@Override
public void stateChanged(CuratorFramework client, ConnectionState newState)
{
if ( (newState == ConnectionState.SUSPENDED) || (newState == ConnectionState.LOST) )
{
throw new CancelLeadershipException();
}
}
}
抽象类
带有ConnectionStateListener的stateChanged默认实现
CancelLeadershipException
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.state.ConnectionState;
public class CancelLeadershipException extends RuntimeException
{
public CancelLeadershipException()
{}
public CancelLeadershipException(String message)
{
super(message);
}
public CancelLeadershipException(String message, Throwable cause)
{
super(message, cause);
}
public CancelLeadershipException(Throwable cause)
{
super(cause);
}
}
运行时异常
注意:只有LeaderSelectorListener#stateChanged方法中抛出才能引发LeaderSelector#interruptLeadership()
成员变量
log : slf4j
client : zk客户端(curator-framework提供)
listener : 监听选主成功,并被回调
executorService : 线程池,同样实现了java.io.Closeable
mutex : 分布式锁对象
state :
内部枚举
LATENT 休眠
STARTED 已启动
CLOSED 已关闭
状态
原子化引用包装
autoRequeue :
是否自动重新参与选主
原子化对象
ourTask :
任务的异步Future持有
原子化引用包装
hasLeadership
volatile可见性
不同于LeaderLatch
没有采用AtomicBoolean
任务采用线程池
选主回调更新状态
所以对于hasLeadership的并发竞争少
id : 参与者id
debugLeadershipLatch : 测试时使用
debugLeadershipWaitLatch : 试时使用
isQueued :
是否已经在排队中
安全性由synchronized保障
defaultThreadFactory
私有常量
默认线程工厂
选主线程带有"Curator-LeaderSelector"前缀
注意:和LeaderLatch不同,虽然大部分变量采用了final,并采用Atomic进行包装。但是有些只采用volatile,只是保证了可见性。
构造器
与LeaderLatch不同,需要指定一个线程池
异步任务去选主
选主成功后执行listener的回调
client,leaderPath,listener不能为空
对listener进行了一层包装
构造的过程中,初始化了对leaderPath进行了加锁
启动
Leader Election是由start()启动选主过程:
CAS操作,更新状态从休眠到已启动
如果线程池已关闭已经关闭则认定:"Already started"
坚持是否已经当选成功
在链接上添加选主监听器
执行requeue()方法,重新排队选主
确认当前状态是已启动
调用internalRequeue()方法
synchronized同步互斥
如果当前已经在排队中,则返回false
否则
更新isQueued = true
由于synchronized,所以isQueued是安全更新
向线程池提交一个异步任务
由ourTask持有此任务的Future
返回true
继续看看提交的异步任务,做了哪些事:
调用doWorkLoop()
通过finally
调用clearIsQueued();
synchronized
isQueued = false
如果开启自动重新排队,则再次调用internalRequeue()
有点类似递归
但不是递归
不断重新排队
在当前任务结束时,通过重新调用所在方法
重新向线程池中建立一个同样的任务
那么先来看看doWorkLoop()方法:
异常持有,常规套路
可以发现干活的是doWork()
当遇到java.lang.InterruptedException,则线程重新进行锁竞争(synchronized)
如果开启了自动重新排队,则不抛出异常
任务正常结束
重做任务
继续看看doWork():
初始hasLeadership=false
申请对leaderPath加锁,阻塞
加锁成功,则当选为主hasLeadership = true
回调org.apache.curator.framework.recipes.leader.LeaderSelectorListener的takeLeadership方法
此回调是在doWork()中同步调用
也即是在doWorkLoop()中
也就是在executorService线程池中的一个异步任务中调用的
对中断异常进行处理
内层finally清理排队中标识
外层finally进行着主任务执行完成后的清理工作
如果是主
还原主标识
对leaderPath解锁
关闭
CAS操作,将状态从已启动更新为已关闭
清除掉链接上的监听
关闭线程池
制空异步任务Future
监听器包装
在构造器中,可以发现,对于监听器,LeaderSelector是做了一层包装的:
一个手工代理的套路
关键的对listener.stateChanged进行了代理增强
处理了CancelLeadershipException
进行了leaderSelector.interruptLeadership()
这就是上文说在takeLeadership中抛出CancelLeadershipException,才会得到妥善的清理
中断
leaderSelector是如何处理中断的:
synchronized
就是拿到线程池中正在执行的任务的Future
调用Future的cancel取消执行
可以看出这里对任务的处理要好于Leader Latch的方式。在Leader Latch需要自行处理中断
小结
通过源码发现LeaderSelector选主,完全是通过一个分布式公平锁来实现的。内部使用线程池来执行选主任务以及业务逻辑。而且,是可以重新排队的。和Leader Latch对比:
| 方式 | 任务调用 | 重新选主 | 公平性 | 适用场景 |
|---|---|---|---|---|
| Leader Election | 异步 | 自动 | 公平 | 分布式任务 |
| Leader Latch | 同步 | 手工实现 | 非公平 | 热备 |
Leader Latch 采用有序临时节点,重入时顺序控制可能缺失公平性。Leader Latch 对于节点的监听,为避免惊群效应,采用的对参与者排序后,逐个监听上一位参与者
本文详细介绍了Curator框架中的LeaderLatch和Leader Election两种分布式选主机制,包括创建、删除、读取和更新Zookeeper节点的操作。通过源码分析,揭示了LeaderLatch的抢占式选主过程和Leader Election的公平选举实现,强调了在实际使用中处理连接问题和错误处理的重要性。
1543

被折叠的 条评论
为什么被折叠?



