- 自序
- 概述
- 安装和运行 Zookeeper
- Zookeeper 开发实例
- ZooKeeper 中的组和成员
- 创建组
- 加入组
- 成员列表
- 删除分组
- Zookeeper 服务
- 数据模型 Data Model
- 操作 Operations
- 实现 Implementation
- 数据一致性 Consistency
- 会话 Sessions
- ZooKeeper 应用程序 Building Applications with ZooKeeper
- 配置服务 Configuration Service
- 坚韧的 ZooKeeper 应用 The Resilient ZooKeeper Application
- 一个稳定的配置服务 A reliable configuration service
- 生产环境中的 ZooKeeper ZooKeeper in Production
- 韧性和性能 Resilience and Performance
- 配置
一个稳定的配置服务 A reliable configuration service
回过头来看一下 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 来监控 Expired
的 KeeperState
,然后重新建立一个连接。这种方法下,我们只需要不断的尝试执行 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 被删除时才能获得通知:此时他已经获得了锁。
获得锁的伪代码如下:
- 在 lock 的 znode 下创建名字为
lock-
的 ephemeral 类型 znode,并记录下创建的 znode 的 path(会在创建函数中返回)。 - 获取 lock znode 的子节点列表,并开启对 lock 的子节点的 watch 模式。
- 如果创建的子节点的序号最小,则再执行一次第 2 步,那么就表示已经获得锁了。退出。
- 等待第 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-2
和 leader/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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论