返回介绍

一个稳定的配置服务 A reliable configuration service

发布于 2025-01-04 01:04:23 字数 6522 浏览 0 评论 0 收藏 0

回过头来看一下 ActiveKeyValueStore 中的 write() 方法,其中调用了 exists() 方法来判断 znode 是否存在,然后决定是创建一个 znode 还是调用 setData 来更新数据。

public void write(String path, String value) throws InterruptedException,
      KeeperException {
    Stat stat = zk.exists(path, false);
    if (stat == null) {
      zk.create(path, value.getBytes(CHARSET), Ids.OPEN_ACL_UNSAFE,
          CreateMode.PERSISTENT);
    } else {
      zk.setData(path, value.getBytes(CHARSET), -1);
    }
  }

从整体上来看, write() 方法是一个幂等方法,所以我们可以不断的尝试执行它。我们来修改一个新版本的 write() 方法,实现在循环中不断的尝试 write 操作。我们为尝试操作设置了一个最大尝试次数参数( MAX_RETRIES )和每次尝试间隔的休眠( RETRY_PERIOD_SECONDS ) 时长:

public void write(String path, String value) throws InterruptedException,
      KeeperException {
    int retries = 0;
    while (true) {
      try {
        Stat stat = zk.exists(path, false);
        if (stat == null) {
          zk.create(path, value.getBytes(CHARSET), Ids.OPEN_ACL_UNSAFE,
              CreateMode.PERSISTENT);
        } else {
          zk.setData(path, value.getBytes(CHARSET), stat.getVersion());
        }
        return;
      } catch (KeeperException.SessionExpiredException e) {
        throw e;
      } catch (KeeperException e) {
        if (retries++ == MAX_RETRIES) {
          throw e;
        }
        // sleep then retry
        TimeUnit.SECONDS.sleep(RETRY_PERIOD_SECONDS);
      }
    }
  }

细心的读者可能会发现我们并没有在捕获 KeeperException.SessionExpiredException 时继续重新尝试操作,这是因为当 session 过期后,ZooKeeper 会变为 CLOSED 状态,就不能再重新连接了。我们只是简单的抛出一个异常,通知调用者去创建一个新的 ZooKeeper 实例,所以 write() 方法可以不断的尝试执行。一个简单的方式来创建一个 ZooKeeper 实例就是重新 new 一个 ConfigUpdater 实例。

public static void main(String[] args) throws Exception {
    while (true) {
      try {
        ResilientConfigUpdater configUpdater =
          new ResilientConfigUpdater(args[0]);
        configUpdater.run();
      } catch (KeeperException.SessionExpiredException e) {
        // start a new session
      } catch (KeeperException e) {
        // already retried, so exit
        e.printStackTrace();
        break;
      }
    }
  }

另一个可以替代处理 session 过期的方法就是使用 watcher 来监控 ExpiredKeeperState ,然后重新建立一个连接。这种方法下,我们只需要不断的尝试执行 write() ,如果我们得到了 KeeperException.SessionExpiredException`异常,连接最终也会被重新建立起来。那么我们抛开如何从一个过期的 session 中恢复问题,我们的重点是连接丢失的问题也可以这样解决,只是处理方法不同而已。

注意
我们这里忽略了另外一种情况,在 zookeeper 实例不断的尝试连接了 ensemble 中的所有节点后发现都无法连接成功,就会抛出一个 IOException,说明所有的集群节点都不可用。而有一些应用被设计为不断的尝试连接,直到 ZooKeeper 服务恢复可用为止。

这只是一个重复尝试的策略。还有很多的策略,比如指数补偿策略,每次尝试之间的间隔时间会被乘以一个常数,间隔时间会逐渐变长,直到与集群建立连接为止间隔时间才会恢复到一个正常值,来预备一下次连接异常使用。

译者:为什么要使用指数补偿策略呢?这是为了避免反复的尝试连接而消耗资源。在一次较短的时间后第二次尝试连接不成功后,延长第三次尝试的等待时间,这期间服务恢复的几率可能会更大。第四次尝试的机会就变小了,从而达到减少尝试的次数。

锁服务 A Lock Service

分布式锁用来为一组程序提供互斥机制。任意一个时刻仅有一个进程能够获得锁。分布式锁可以用来实现大型分布式系统的 leader 选举算法,即 leader 就是获取到锁的那个进程。

注意
不要把 ZooKeeper 的原生 leader 选举算法和我们这里所说的通用 leader 选举服务搞混淆了。ZooKeeper 的原生 leader 选举算法并不是公开的算法,并不能向我们这里所说的通用 leader 选举服务那样,为一个分布式系统提供主进程选举服务。

为了使用 ZooKeeper 实现分布式锁,我们使用可排序的 znode 来实现进程对锁的竞争。思路其实很简单:首先,我们需要一个表示锁的 znode,获得锁的进程就表示被这把锁给锁定了(命名为,/leader)。然后,client 为了获得锁,就需要在锁的 znode 下创建 ephemeral 类型的子 znode。在任何时间点上,只有排序序号最小的 znode 的 client 获得锁,即被锁定。例如,如果两个 client 同时创建 znode /leader/lock-1/leader/lock-2 ,所以创建 /leader/lock-1 的 client 获得锁,因为他的排序序号最小。ZooKeeper 服务被看作是排序的权威管理者,因为是由他来安排排序的序号的。

锁可能因为删除了 /leader/lock-1 znode 而被简单的释放。另外,如果相应的客户端死掉,使用 ephemeral znode 的价值就在这里,znode 可以被自动删除掉。创建 /leader/lock-2 的 client 就获得了锁,因为他的序号现在最小。当然客户端需要启动观察模式,在 znode 被删除时才能获得通知:此时他已经获得了锁。

获得锁的伪代码如下:

  1. 在 lock 的 znode 下创建名字为 lock- 的 ephemeral 类型 znode,并记录下创建的 znode 的 path(会在创建函数中返回)。
  2. 获取 lock znode 的子节点列表,并开启对 lock 的子节点的 watch 模式。
  3. 如果创建的子节点的序号最小,则再执行一次第 2 步,那么就表示已经获得锁了。退出。
  4. 等待第 2 步的观察模式的通知,如果获得通知,则再执行第 2 步。

羊群效应

虽然这个算法是正确的,但是还是有一些问题。第一个问题是羊群效应。试想一下,当有成千成百的 client 正在试图获得锁。每一个 client 都对 lock 节点开启了观察模式,等待 lock 的子节点的变化通知。每次锁的释放和获取,观察模式将被触发,每个 client 都会得到消息。那么羊群效应就是指像这样,大量的 client 都会获得相同的事件通知,而只有很小的一部分 client 会对事件通知有响应。我们这里,只有一个 client 将获得锁,但是所有的 client 都得到了通知。那么这就像在网络公路上撒了把钉子,增加了 ZooKeeper 服务器的压力。

为了避免羊群效应,通知的范围需要更精准。我们通过观察发现,只有当序号排在当前 znode 之前一个 znode 离开时,才有必要通知创建当前 znode 的 client,而不必在任意一个 znode 删除或者创建时都通知 client。在我们的例子中,如果 client1、client2 和 client3 创建了 znode /leader/lock-1/leader/lock-2leader/lock-3 ,client3 仅在 /leader/lock-2 消失时,才获得通知。而不需要在 /leader/lock-1 消失时,或者新建 /leader/lock-4 时,获得通知。

重新获取异常 Recoverable Exception

这个锁算法的另一个问题是没有处理当连接中断造成的创建失败。在这种情况下,我们根本就不知道之前的创建是否成功了。创建一个可排序的 znode 是一个非等幂操作,所以我们不能简单重试,因为如果第一次我们创建成功了,那么第一次创建的 znode 就成了一个孤立的 znode 了,将永远不会被删除直到会话结束。

那么问题的关键在于,在重新连接以后,client 不能确定是否之前创建过 lock 节点的子节点。我们在 znode 的名字中间嵌入一个 client 的 ID,那么在重新连接后,就可以通过检查 lock znode 的子节点 znode 中是否有名字包含 client ID 的节点。如果有这样的节点,说明之前创建节点操作成功了,就不需要再创建了。如果没有这样的节点,那就重新创建一个。

Client 的会话 ID 是一个长整型数据,并且在 ZooKeeper 中是唯一的。我们可以使用会话的 ID 在处理连接丢失事件过程中作为 client 的 id。在 ZooKeeper 的 JAVA API 中,我们可以调用 getSessionId() 方法来获得会话的 ID。

那么 Ephemeral 类型的可排序 znode 不要命名为 lock-<sessionId>- ,所以当加上序号后就变成了 lock-<sessionId>-<sequenceNumber> 。那么序号虽然针对上一级名字是唯一的,但是上一级名字本身就是唯一的,所以这个方法既可以标记 znode 的创建者,也可以实现创建的顺序排序。

不能恢复异常 Unrecoverable Exception

如果 client 的会话过期,那么他创建的 ephemeral znode 将被删除,client 将立即失去锁(或者至少放弃获得锁的机会)。应用需要意识到他不再拥有锁,然后清理一切状态,重新创建一个锁对象,并尝试再次获得锁。注意,应用必须在得到通知的第一时间进行处理,因为应用不知道如何在 znode 被删除事后判断是否需要清理他的状态。

实现 Implementation

考虑到所有的失败模式的处理的繁琐,所以实现一个正确的分布式锁是需要做很多细微的设计工作。好在 ZooKeeper 为我们提供了一个 产品级质量保证的锁的实现,我们叫做 WriteLock 。我们可以轻松的在 client 中应用。

更多的分布式数据结构和协议 More Distribute Data Structures and Protocols

我们可以用 ZooKeeper 来构建很多分布式数据结构和协议,例如,barriers,queues 和 two-phase commit。有趣的是我们注意到这些都是同步协议,而我们却使用 ZooKeeper 的原生异步特征(比如通知机制)来构建他们。

在 ZooKeeper 官网上提供了一些数据结构和协议的伪代码。并且提供了实现这些的数据结构和协议的标准教程(包括 locks、leader 选举和队列);你可以在 recipes 目录中找到。

Apache Curator project 也提供了一些简单客户端的教程。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文