ZooKeeper 深入浅出之五:Zookeeper 应用程序二(弹性应用和可靠配置)
分布式计算设计的第一谬误就是认为“网络是稳定的”。我们所实现的程序目前都是假设网络稳定的情况下实现的,所以当我们在一个真实的网络环境下,会有很多原因可以使程序执行失败。下面我们将阐述一些可能造成失败的场景,并且讲述如何正确的处理这些失败,让我们的程序在面对这些异常时更具韧性。
在 ZooKeeper 的 API 中,每一个 ZooKeeper 的操作都会声明抛出两个异常:InterruptedException
和 KeeperException
。
InterrupedException
当一个操作被中断时,会抛出一个 InterruptedException。在 JAVA 中有一个标准的阻塞机制用来取消程序的执行,就是在需要阻塞的的地方调用 interrupt()
。如果取消执行成功,会以抛出一个 InterruptedException 作为结果。ZooKeeper 坚持了这个标准,所以我们可以用这种方式来取消 client 的对 ZooKeeper 的操作。用到 ZooKeeper 的类和库需要向上抛出 InterruptedException,才能使我们的 client 实现取消操作。
InterruptedException 并不意味着程序执行失败,可能是人为设计中断的,所以在上面配置应用的例子中,当向上抛出 InterruptedException 时,会引起应用终止。
KeeperException
当 ZooKeeper 服务器出现错误信号,或者出现了通信方面的问题,就会抛出一个 KeeperException。由于错误的不同原因,所以 KeeperException 有很多子类。例如,KeeperException.NoNodeException
当操作一个 znode 时,而这个 znode 并不存在,就会抛出这个异常。
每一个之类都有一个异常码作为异常的类型。例如,KeeperException.NoNodeException
的异常码就是 KeeperException.Code.NONODE
(一个枚举值)。
有两种方法来处理 KeeperException。一种是直接捕获 KeeperException,然后根据异常码进行不同类型异常处理。另一种是捕获具体的子类,然后根据不同类型的异常进行处理。
KeeperException 包含了3大类异常。
状态异常
当无法操作 znode 树造成操作失败时,会产生状态异常。通常引起状态异常的原因是有另外的程序在同时改变 znode。例如,一个 setData()
操作时,会抛出 KeeperException.BadVersionException
。因为另外的一个程序已经在 setData()
操作之前修改了 znode,造成 setData()
操作时版本号不匹配了。程序员必须了解,这种情况是很有可能发生的,我们必须靠编写处理这种异常的代码来解决他。
有的一些异常是编写代码时的疏忽造成的,例如 KeeperException.NoChildrenForEphemeralsException
。这个异常是当我们给一个 enphemeral 类型的znode添加子节点时抛出的。
重新获取异常
重新获取异常来至于那些能够获得同一个 ZooKeeper session 的应用。伴随的表现是抛出 KeeperException.ConnectionLossException
,表示与 ZooKeeper 的连接丢失。ZooKeeper 将会尝试重新连接,大多数情况下重新连接都会成功并且能够保证 session 的完整性。
然而,ZooKeeper无法通知客户端操作由于 KeeperException.ConnectionLossException
而失败。这就是一个部分失败的例子。只能依靠程序员编写代码来处理这个不确定性。
在这点上,幂等操作和非幂等操作的差别就会变得非常有用了。一个幂等操作是指无论运行一次还是多次结果都是一样的,例如一个读请求,或者一个不设置任何值得 setData
操作。这些操作可以不断的重试。
一个非幂等操作不能被不分青红皂白的不停尝试执行,就像一些操作执行一次的效率和执行多次的效率是不同。我们将在之后会讨论如何利用非幂等操作来处理 Recovreable Exception。
不能重新获取异常
在一些情况下,ZooKeeper 的 session 可能会变成不可用的——比如 session 过期,或者因为某些原因 session 被 close 掉(都会抛出 KeeperException.SessionExpiredException
),或者鉴权失败(KeeperException.AuthFailedException
)。无论何种情况,ephemeral 类型的 znode 上关联的 session 都会丢失,所以应用在重新连接到ZooKeeper之前都需要重新构建他的状态。
一个可靠的配置服务
回过头来看一下 ActiveKeyValueStore
中的 write()
方法,其中调用了 exists()
方法来判断 znode 是否存在,然后决定是创建一个 znode 还是调用 setData
或 create
来更新或创建数据。
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 服务恢复可用为止。
这只是一个重复尝试的策略。还有很多的策略,比如指数补偿策略,每次尝试之间的间隔时间会被乘以一个常数,间隔时间会逐渐变长,直到与集群建立连接为止间隔时间才会恢复到一个正常值,来预备一下次连接异常使用。
旁注: 为什么要使用指数补偿策略呢?这是为了避免反复的尝试连接而消耗资源。在一次较短的时间后第二次尝试连接不成功后,延长第三次尝试的等待时间,这期间服务恢复的几率可能会更大。第四次尝试的机会就变小了,从而达到减少尝试的次数。