【zookeeper】Apache curator5.0原理分析 《连接处理》

1. 前言

通过之前的文章《Apache curator优点介绍》,我们知道curator的一个优点就是封装ZooKeeper client与ZooKeeper server之间的连接处理。

都说Curator的连接机制比较牛逼,所以在分析Curator的连接和重试机制之前,我想先搞清楚原生的ZooKeeper的连接存在哪些问题。

Curator虽然提供所谓的高层抽象API来简化了ZooKeeper的使用,但更重要的是封装了管理到ZooKeeper集群的连接以及重试机制的复杂性,下面我们来详细分析一下Curator在这方面都是怎么做的,不过在这之前先要搞清楚ZooKeeper目前在连接方面有哪些问题。

2. zookeeper的连接有什么问题

在这里插入图片描述

2.1 会话建立

当你new出一个ZooKeeper对象的时候,客户端就建立了与ZooKeeper服务端之间的一个Session(注意这个session是线程安全的,即多个线程可以共用一个ZooKeeper实例),这个Session会有一个超时时限,即失效时间,通过ZooKeeper的构造函数传递进去,客户端会不断发送心跳(多久发送一次和设置的会话超时时限有关)到ZooKeeper服务端,以保持与ZooKeeper服务端的有效连接和Session的有效性。

连接丢失

如果客户端与服务端的网络断开,或客户端连接的ZooKeeper server挂掉,或者与Server的Session的建立后连接还未建立完成,都会出现CONNECTION_LOSS现象,客户端所有watcher都会收到一个disconnected event,客户端连接状态从CONNECTED变为CONNECTING。

2.2 自动重连

此时客户端库会自动从ZooKeeper服务器列表中选择一个server来进行重连。

备注:创建zk实例的时候就会新建两个后台线程,一个用于维护watcher,一个用户数据传输

  • A. 如果重新成功建立与服务端的TCP连接,并且没有超过session的超时时限,那么ZooKeeper客户端将会收到一个SyncConnected event,客户端连接状态就又会变为CONNECTED,连接恢复正常且临时节点和注册的watch事件也不会被删除掉。即使重连在一个很短的时间内完成,也会收到了两个事件。

  • B. 如果过了很久还是不能重新成功建立与服务端的TCP连接,客户端将会一直保持在disconnected状态,也就永远不会收到Expired event,只会有disconnectedevent(因为事件是来自服务端)。

    此时服务器也会删该客户端创建临时节点和监听器吧?

  • C. 如果重新成功建立与服务端的TCP连接,但是发现已经超过了session设置的超时时限,那么客户端将会收到一个Expired event,表示会话已经终止SESSION_EXPIRED,此时服务器会将这个客户端注册的所有watcher,以及创建的临时节点全部删除,同时客户端持有的ZooKeeper句柄也会被关闭,唯一能做的就是重建ZooKeeper对象。发生SESSION_EXPIRED的watcher将会看到如下状态转换:

'connected': 会话建立,客户端与ZooKeeper集群正常通信

....client is partitioned from the cluster

'disconnected': 客户端丢失与ZooKeeper集群的连接

....time elapses, 时间逐渐流逝,在'timeout'时限后ZooKeeper集群将会终止这个会话,此时处于disconnected状态的客户端将会什么都看不到。

....time elapses, 时间流逝,客户端重新建立与ZooKeeper集群的连接

'expired': 最终客户端重连到ZooKeeper集群,将会收到expiration 通知

特别注意: 关于ZooKeeper会话终止我这里有一篇文章详细讲解!!! 《会话-连接-重连 深入分析,演示断开的场景》

2.3 连接丢失的处理

CONNECTION_LOSS发生的时候,任何正在运行的ZooKeeper操作(like getChildren,get/setData, create, delete)都将会抛出ConnectionLossexception,你必须处理这个异常,一种处理方式就是重试这些操作直到成功为止,即直到ZooKeeper连接自动重新建立回来。

但是当一个操作抛出ConnectionLossexception,你无法确定是否这个操作是不是已经成功了,CONNECTION_LOSS意味着客户端和服务器端的TCP连接断开,但是并不意味着请求失败。假如正在执行一个create请求,然后在请求到达服务器以及response返回之前,连接断开,这个create请求就会执行成功,假如在数据包发送到线路之前断开那么create请求就会执行失败。很不幸客户端没办法知道在CONNECTION_LOSS发生后自己的请求是否执行成功。

那么怎么办呢?

个人认为,这个问题在于需要明白什么时候需要重试,比如发生连接丢失和操作超时这类异常时是肯定需要重试的,至于其他异常(create操作可能引起NodeExists 异常, delete操作可能引起NoNode异常)我们无需关心,无需捕获这些异常进行重试,只需要简单将它们抛出即可。

2.4 会话终止的处理:

SESSION_EXPIRED将会自动关闭ZooKeeper句柄,如果正确操作ZooKeeper集群,会话终止现象很难出现,如果客户端强制关闭一个连接倒是一定会出现这个事件,因为服务器认为客户端已经死掉了。

如果真的出现会话失效该如何处理呢?

首先当发生SESSION_EXPIRED时,客户端持有的ZooKeeper句柄会被关闭,如果还想应用程序继续执行,就必须要重建ZooKeeper对象。

其次当发生SESSION_EXPIRED时,服务端会把客户端注册的所有watcher,以及创建的临时节点全部删除,如果这些东西对应用程序来说至关重要,那么你唯一能做的就是根据实际情况自己重新创建这些临时节点以及注册对应的watcher。.

3. Curator是如何处理连接丢失和会话终止的

3.1 连接丢失的处理

Curator中利用类org.apache.curator.ConnectionState来管理客户端到ZooKeeper集群的连接状态,其中用到原子布尔型变量来标识当前连接是否已经建立:

private final AtomicBoolean isConnected=new AtomicBoolean(false);

事件处理函数中( ConnectionState 实现了Watcher接口)修改 isConnected 的值:


    public void process(WatchedEvent event) {
        if (LOG_EVENTS) {
            log.debug("ConnectState watcher: " + event);
        }

        if (event.getType() == EventType.None) {
            boolean wasConnected = this.isConnected.get();
            //状态变更
            boolean newIsConnected = this.checkState(event.getState(), wasConnected);

其中checkState函数获取当前连接状态是否为已连接:

  private boolean checkState(KeeperState state, boolean wasConnected) {
        boolean isConnected = wasConnected;
        boolean checkNewConnectionString = true;
        switch(state) {
        case Disconnected:
        default:
            isConnected = false;
            break;
        case SyncConnected:
        case ConnectedReadOnly:
            isConnected = true;
            break;
        case AuthFailed:
            isConnected = false;
            log.error("Authentication failed");
            break;
        case Expired:
            isConnected = false;
            checkNewConnectionString = false;
            this.handleExpiredSession();
        case SaslAuthenticated:
        }

下面以SetData操作来看,下面是Curator执行SetData操作的代码:

public class SetDataBuilderImpl {
  public Stat forPath(String path, byte[] data){
   ...
   resultStat = pathInForeground(path, data);
 } 
 
  private Stat pathInForeground(final String path, final byte[] data) throws Exception
    {
        OperationTrace   trace = client.getZookeeperClient().startAdvancedTracer("SetDataBuilderImpl-Foreground");
        //调用RetryLoop.callWithRetry
        Stat        resultStat = RetryLoop.callWithRetry
        (
            client.getZookeeperClient(),
            new Callable<Stat>()
            {
                @Override
                public Stat call() throws Exception
                {
                    return client.getZooKeeper().setData(path, data, version);
                }
            }
        );
        trace.setRequestBytesLength(data).setPath(path).setStat(resultStat).commit();
        return resultStat;
    }
}


  

可以看到真正的setData操作被包装到了callWithRetry函数中:

public abstract class RetryLoop {
   public static <T> T callWithRetry(CuratorZookeeperClient client, Callable<T> proc) throws Exception {
      //检测当前连接状态,若未连接则等待一定时间直到连接完成
        client.internalBlockUntilConnectedOrTimedOut();
        T result = null;
        ThreadLocalRetryLoop threadLocalRetryLoop = new ThreadLocalRetryLoop();
        Objects.requireNonNull(client);
        RetryLoop retryLoop = threadLocalRetryLoop.getRetryLoop(client::newRetryLoop);

        try {
        //如果  retryLoop.takeException再次尝试,就一直循环尝试再执行
            while(retryLoop.shouldContinue()) {  
            //与原文相比,循环体内没有看到断链等待的代码
                try {
                 //调用带返回值的Callable方法
                    result = proc.call();
                    retryLoop.markComplete();
                } catch (Exception var9) {
                    ThreadUtils.checkInterrupted(var9);
                    //若执行ZooKeeper操作的时候发生任何异常,将会执行takeException函数
                    retryLoop.takeException(var9);
                }
            }
        } finally {
            threadLocalRetryLoop.release();
        }
//执行成功,返回该结果
        return result;
    }

与原文相比,循环体内没有看到断链等待的代码,不知道5.0怎么实现的

这个函数其实很简单,步骤如下:
(1) 检测当前是否已连接,若已连接则执行下一句代码,否则等待一定时间;

(2) 执行真正的ZooKeeper操作;

(3) 执行成功,标记为执行完成。

若执行ZooKeeper操作的时候发生任何异常,将会执行takeException函数,其实里面就是判断是否允许重试,允许的话,就不抛异常,这样回到上层循环体继续,如果不允许,则抛出异常,就结束外层的循环体:

class RetryLoopImpl {
  public void takeException(Exception exception) throws Exception {
        boolean rethrow = true;
        if (this.retryPolicy.allowRetry(exception)) {
            if (!Boolean.getBoolean("curator-dont-log-connection-problems")) {
                this.log.debug("Retry-able exception received", exception);
            }

            if (this.retryPolicy.allowRetry(this.retryCount++, System.currentTimeMillis() - this.startTimeMs, sleeper)) {
                (new EventTrace("retries-allowed", (TracerDriver)this.tracer.get())).commit();
                if (!Boolean.getBoolean("curator-dont-log-connection-problems")) {
                    this.log.debug("Retrying operation");
                }

                rethrow = false;
            } else {
                (new EventTrace("retries-disallowed", (TracerDriver)this.tracer.get())).commit();
                if (!Boolean.getBoolean("curator-dont-log-connection-problems")) {
                    this.log.debug("Retry policy not allowing retry");
                }
            }
        }

        if (rethrow) {
            throw exception;
        }
    }
  • 如果 isRetryException 函数判断抛出的异常是否是“连接丢失异常/会话终止异常”,如果是则判断是否允许重试(其实传递进来的重试策略就是简单地进行三次重试),允许重试的话就不抛出异常,返回,继续下一轮循环。
  • 如果isRetryException函数判断不属于“连接丢失异常/会话终止异常”,比如是其他的一些其他异常(create操作可能引起NodeExists 异常, delete操作可能引起NoNode异常),那么将继续把异常抛出,callWithRetry函数将因为异常而结束返回。

这就是Curator处理连接丢失的策略,平时仅仅是通过在watch事件响应函数中记录连接状态isConnected,执行ZooKeeper操作的时候,先等待连接状态isConnected变为true再执行操作,若执行期间若发生异常,仅仅在当异常类型为“连接丢失/会话终止”时进行重试,反复几次。

3.2 会话终止的处理

这种机制个人认为已经足够应付所有场景。

和连接丢失一样,我们需要分别来分析平时和执行ZooKeeper操作时发生“会话终止”异常Curator怎么来处理。

还是看ConnectionState类中watch事件响应函数,其中有这么一段代码:

//会话终止
case Expired:
{
   isConnected = false;
   checkNewConnectionString = false;
   handleExpiredSession();
   break;
}

关键是看handleExpiredSession函数:

    private void handleExpiredSession() {
        log.warn("Session expired event received");
        (new EventTrace("session-expired", (TracerDriver)this.tracer.get(), this.getSessionId())).commit();

        try {
            this.reset();
        } catch (Exception var2) {
            ThreadUtils.checkInterrupted(var2);
            this.queueBackgroundException(var2);
        }

    }

就是一个reset函数:

    synchronized void reset() throws Exception {
        log.debug("reset");
        this.instanceIndex.incrementAndGet();
        this.isConnected.set(false);
        this.connectionStartMs = System.currentTimeMillis();
        this.handleHolder.closeAndReset();
        this.handleHolder.getZooKeeper();
    }

关键是看最后两句代码,先是执行HandleHolder的closeAndReset函数:

    void closeAndReset() throws Exception {
        this.internalClose(0);
        final Data data = new Data();
        this.helper = new Helper(data) {
            ZooKeeper getZooKeeper() throws Exception {
                synchronized(this) {
                    if (data.zooKeeperHandle == null) {
                        this.resetConnectionString(HandleHolder.this.ensembleProvider.getConnectionString());
                        data.zooKeeperHandle = HandleHolder.this.zookeeperFactory.newZooKeeper(data.connectionString, HandleHolder.this.sessionTimeout, HandleHolder.this.watcher, HandleHolder.this.canBeReadOnly);
                    }

                    HandleHolder.this.helper = new Helper(data);
                    return super.getZooKeeper();
                }
            }
        };
    }

如果对这个函数不清楚,可以回过头看看之前文章讲的Curator的初始化和启动的源码分析,HandleHolder是原生ZooKeeper对象的持有者,维护ZooKeeper对象的单例就是通过这个函数。

一旦发生会话终止异常,ZooKeeper句柄会被自动关闭,所以之前初始化的helper对象中的zooKeeperHandle变量将会变得不可用,所以需要调用这个closeAndReset函数重新初始化helper对象,然后再调用一次getZooKeeper函数执行zookeeperFactory.newZooKeeper初始化好ZooKeeper句柄。

注意:会话终止后ZooKeeper句柄会被自动关闭,但并不是被置为null了,所以在用原来的helper对象的getZooKeeper方法返回的句柄是不可用的。

再看看当执行ZooKeeper操作时发生了会话终止时怎么处理。如果执行ZooKeeper时发生了会话终止,watch事件响应函数中会重新构建ZooKeeper句柄,callWithRetry函数中不但判断当前发生的是连接丢失异常时会进行重试,判断是会话终止异常也会进行重试。

所以说Curator处理会话终止的方法,就是在收到Expired事件时候重新构建HandleHolder中维护的ZooKeeper句柄

3.3 关于临时节点和watch事件

特别注意,有的应用程序创建的临时节点和注册的watch事件至关重要,无法忍受丢失的情况,若发生会话终止,它们必定会被ZooKeeper服务器端删除掉,并且Curator无法帮助你重新还原回来。

参考:
《Curator源码解析(四)ZooKeeper存在的连接问题》

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值