Curator框架

本文详细介绍了Curator框架中的LeaderLatch和Leader Election两种分布式选主机制,包括创建、删除、读取和更新Zookeeper节点的操作。通过源码分析,揭示了LeaderLatch的抢占式选主过程和Leader Election的公平选举实现,强调了在实际使用中处理连接问题和错误处理的重要性。

Curator

Curator中提供了Zookeeper各种应用场景(分布式锁、Master选举机制和分布式计算器等)的抽象封装。
项目组件:

名称描述
RecipesZookeeper典型应用场景的实现,这些实现是基于Curator Framework。
FrameworkZookeeper API的高层封装,大大简化Zookeeper客户端编程,添加了例如Zookeeper连接管理、重试机制等。
Utilities为Zookeeper提供的各种实用程序。
ClientZookeeper client的封装,用于取代原生的Zookeeper客户端(ZooKeeper类),提供一些非常有用的客户端特性。
ErrorsCurator如何处理错误,连接问题,可恢复的例外等。

创建节点
(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 对于节点的监听,为避免惊群效应,采用的对参与者排序后,逐个监听上一位参与者

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值