内容简介:网络故障可以说是分布式系统一生之敌。如果永远不发生网络故障,我们实际上可以设计出高可用强一致的分布式系统。可惜的是不发生网络故障的分布式环境还不存在,ZooKeeper 使用过程中也需要小心的应付网络故障。在介绍应对网络故障之前,我们首先看到没有故障的时候 ZooKeeper 对网络连接的处理。ZooKeeper 客户端启动时从配置中读取所有可用的服务器的信息,它会随机尝试和其中一台服务器连接,如果成功地建立起连接,ZooKeeper 客户端和服务端会建立起一个会话(session)。在会话超时之前,服务器
网络故障可以说是分布式系统一生之敌。如果永远不发生网络故障,我们实际上可以设计出高可用强一致的分布式系统。可惜的是不发生网络故障的分布式环境还不存在,ZooKeeper 使用过程中也需要小心的应付网络故障。
在介绍应对网络故障之前,我们首先看到没有故障的时候 ZooKeeper 对网络连接的处理。ZooKeeper 客户端启动时从配置中读取所有可用的服务器的信息,它会随机尝试和其中一台服务器连接,如果成功地建立起连接,ZooKeeper 客户端和服务端会建立起一个会话(session)。在会话超时之前,服务器会响应客户端的请求,每次新的请求都会刷新会话超时的时间,在没有业务请求的情况下,客户端也会通过定期的心跳来维持会话。当 ZooKeeper 客户端和当前连接的服务器失联时,客户端会试着从可用的服务器列表中重新连接到一台服务器上。
总体地看过 ZooKeeper 正常的网络连接处理之后,我们来看看网络故障在 ZooKeeper 中的抽象。网络故障在 ZooKeeper 的层面被转换为两种异常,一种是 ConnectionLossException ,一种是 SessionExpireException。前者发生在 ZooKeeper 客户端与某台服务器断开之后,后者发生在 ZooKeeper 服务器通知客户端会话超时的时候。
ConnectionLossException
这个异常肯定是 ZooKeeper 中最让人头痛的异常之一了。 ZooKeeper 客户端通过 socket 和 ZooKeeper 集群的某台服务器连接,这个连接在客户端由 ClientCnxn 管理,在服务端由 ServerCnxn 管理。ConnectionLossException 在 ZooKeeper 客户端与服务器的连接异常关闭的时候抛出,它仅仅表明 ZooKeeper 客户端发现自己与当前服务器的连接断开,除此之外什么也不知道。因为这个不知道,我们要对不同的断开原因进行探测和处理。
从可恢复的故障中恢复
ConnectionLossException 是一个可恢复的异常,它仅代表 ZooKeeper 客户端与当前服务器的连接断开。ZooKeeper 客户端完全有可能稍后连接上另一个服务器并重新开始发送请求。在 ZooKeeper 集群网络不稳定的情况下,我们要特别小心地处理这类异常,而不是直接层层外抛。否则,因为网络抖动导致上层应用崩溃是不可接受的。此外,在这种异常情况下,重新创建一个 ZooKeeper 客户端开启一个新的会话,只会加剧网络的不稳定性。这是因为 ZooKeeper 客户端不重连的情况下,服务器只能通过会话超时来释放与客户端的连接。如果由于连接过多导致响应不稳定,开启新的会话只会恶化这个情况。这种情况有点类似于 DDOS 攻击。
一种常见的容忍 ConnectionLossException 的方式是重做动作,也就是形如下面代码的处理逻辑
operation(...) {
zk.create(path, data, ids, mode, callback, data);
}
callback = (rc, path, ctx, name) -> { switch (Code.get(rc)) { case CONNECTIONLOSS:
operation(...); break; // ...
}
}
服务端上操作可能已经成功
在上一节中,我们介绍了通过重做动作来从 ConnectionLossException 中恢复的方法。然而,重做动作是有风险的,这是因为先前的动作可能在客户端上已经成功。如果当前动作是写动作且不幂等,就可能在应用层面观察到意图与实际执行的操作不一致。
ConnectionLossException 仅代表 ZooKeeper 客户端与当前服务器的连接断开。但是,在断开之前,对应的请求完全可能已经发送出去,已经到达服务端并被处理。只是由于客户端与服务器的连接断开,导致 ZooKeeper 客户端没收到回应而抛出 ConnectionLossException 罢了。
如前所述,对于读操作,重试通常没有什么问题,因为我们总能得到重试成功的时候读操作应有的返回值或异常。对于写操作,我们具体展开讨论。
对于 setData 操作,在重试成功的情况下,不考虑具体的业务逻辑,我们可以认为问题不大。因为两次把节点设置为同一个值是幂等操作,对于前一次操作更新了 version 从而导致重试操作 version 不匹配的情况,我们也可以对应处理这种可解释的异常。这类似于 HTTP 中 PUT 方法的语义。
对于 delete 操作,重试可能导致意外的 NoNodeException,我们可以吞掉这个异常或者触发业务相关的异常逻辑。
对于 create 操作,情况稍微复杂一点。在不带 sequential 要求的情况下,create 可能成功或者触发一个 NodeExistException,可以采取跟 delete 对应的处理方式;在 sequential 的情况下,有可能先前的操作已经成功,而重试的操作也成功,也就是创建了两个 sequential 节点。由于我们丢失了先前操作的返回值,因此先前操作的 sequential 节点就成了孤儿,这有可能导致资源泄露或者更严重的一致性问题。例如,基于 ZooKeeper 的一种 leader 选举算法依赖于 sequential 节点的排序,一个序号最小的孤儿节点将导致整个算法失败。在这种情况下,孤儿节点获取了 leader 权限,但其 callback 却在早前被 ConnectionLossException 触发了,因此当选 leader 及后续响应无法触发。同时,由于该节点成为孤儿,它也不会被删除,从而算法不再往下运行。
客户端可能错过状态变化
ZooKeeper 的 Watcher 是单次触发的,在前一次 Watcher 被触发到重新设置 Watcher 并被触发的间隔之间的事件可能会丢失。这本身是 ZooKeeper 上层应用需要考虑的一个重要的问题。 ConnectionLossException 触发 Watcher 接收到一个 WatchedEvent(EventType.None, KeeperState.Disconnected) 的事件。一旦收到这个事件,ZooKeeper 客户端必须假定 ZooKeeper 上的状态可能发生任意变化。对于依赖于某些状态例如自己是应用程序中的 leader 的动作,需要挂起动作,在恢复链接后确认状态之后再重新执行动作。
这里有一个设计上的细节需要注意,不同于一般的 WatchedEvent 会在触发 Watcher 后将其移除,EventType.None 的 WatchedEvent 在不设置系统属性 zookeeper.disableAutoWatchReset=true 的情况下只会触发 Watcher 而不将其移除。同时,在成功重新连接服务器之后会将当前的所有 Watcher 通过 setWatches 请求重新注册到服务器上。服务器通过对比 zxid 的数值来判断是否触发 Watcher。从而避免了由于网络抖动而强迫用户代码在 Watcher 的处理逻辑中处理 ConnectionLossException 并重新执行操作设置 Watcher 的负担。特别的,当前客户端上注册的所有的 Watcher 都将受到网络抖动的影响。但是要注意重新注册的 Watcher 中监听 NodeCreated 事件的 Watcher 可能会错过该事件,这是因为在重新建立连接的过程中该节点由于其他客户端的动作可能先被创建后被删除,由于仅就有无节点判断而没有 zxid 来帮助判断,这被称为 ABA 问题。
SessionExpiredException
这个异常比 ConnectionLossException 稍好处理一些,因为它是不可恢复的故障。 ZooKeeper 客户端会话超时之后无法重新和服务器成功连接。因此,我们通常只需要重新创建一个 ZooKeeper 客户端实例并重新开始开始工作。但是会话超时会导致 ephemeral 节点被删除,如果上层应用逻辑与此相关的话,就需要仔细的处理 SessionExpiredException。
会话超时的检测
ZooKeeper 客户端与服务器成功建立连接后,ClientCnxn.SendThread 会周期性的向服务器发送 ping 信息,服务器在处理 ping 信息的时候重置会话超时的时间。如果服务器在超时时间内没有收到客户端发来的任何新的信息,那么它就会宣布这个会话超时,并显式的关掉对应的链接。 ZooKeeper 会话超时相关的逻辑在 SessionTracker 中。所有会话检查和超时的判断都是由 ZooKeeper 集群的 leader 作出的,也就是所谓的仲裁动作(quorum operation),因此客户端超时是所有服务器一致的共识。
如果 ZooKeeper 客户端尝试重新连接服务器,当它重新连接上某个服务器时,该服务器查询会话列表,发现这个重连请求属于超时会话,通过返回非正整数的超时剩余时间通知客户端会话已超时。随后, ZooKeeper 客户端得知自己已经超时并执行相应的退出逻辑。
这里有一个非常 tricky 的事情,就是 ZooKeeper 客户端的会话超时永远是由服务端通知的。考虑这样一种超时情况,在服务器挂了或者客户端与服务器网络分区的情况下, ZooKeeper 客户端是无法得知自己的会话已经超时的。 ZooKeeper 目前没有办法处理这一情况,只能依赖上层应用自己去处理。例如,通过其他逻辑确定会话已超时之后,主动地关闭 ZooKeeper 客户端并重启。在 Curator 中,这种检测是通过 ConnectionStateManager#processEvents 周期性的检测在收到最后一个 disconnect 事件后过去的时间,从而在必然超时的时候通过反射向 ZooKeeper 客户端注入会话超时事件。
ephemeral 节点的删除
ephemeral 节点的删除相关的最大的问题是关于基于 ZooKeeper 的 leader 选举的。 ZooKeeper 提供了 leader 选举的 recipe 参考[1],总的来说是基于一系列 ephemral sequential 节点的 排序 来做的。当上层应用采用这种算法做 leader 选举时,如果 ZooKeeper 客户端与服务器超时,由于 ZooKeeper 相关的操作和相应往往和上层应用的主线程是分开的,这样在上层应用得知自己不是 leader 之前就有可能作出很多越权的操作。
例如,在 FLINK 的设计中,只有成为 leader 的 JobManager 才有权限写 checkpoint 数据。但是,由于丢失 leadership 的消息从 ZooKeeper 集群传递到 ZooKeeper 客户端再到通知上层应用,这几个步骤之间都是异步的,所以此前的 leader 并不能第一时间得知自己丢失 leadership 了。同时,其他的 JobManager 可能并发地被通知当选 leader。此时,集群中就会有两个 JobManager 认为自己是 leader,也就是所谓的双主情形。如果对它们写 checkpoint 数据的动作不做其他限制,就可能导致两个 leader 并发地写 checkpoint 数据而导致状态不一致。这个由于响应时间带来的问题在 Curator 的技术注意事项中已有提及[2],由于发生概率较小,而且仅在未能及时响应 ZooKeeper 服务器信息时才会发生,因此虽然不少系统都有这个理论上的 BUG,但是很多时候只是作为注意事项帮助开发者和使用者在极端情况下理解发生了什么。
FLINK-10333[3] 和 ZooKeeper 邮件列表上我发起的这个讨论[4]详细讨论了这种情况下面临的挑战和解决方法。
[1] https://zookeeper.apache.org/doc/r3.5.5/recipes.html#sc_leaderElection [2] https://cwiki.apache.org/confluence/display/CURATOR/TN10 [3] https://issues.apache.org/jira/browse/FLINK-10333 [4] https://lists.apache.org/x/thread.html/594b66ecb1d60b560a5c4c08ed1b2a67bc29143cb4e8d368da8c39b2@%3Cuser.zookeeper.apache.org%3E
HBase 官方社区推荐必读好文
HBase 原理|HBase 内存管理之 MemStore 进化论
HBase 实践|说好不哭,但 HBase 2.0 真的好用到哭
HBase 抗战总结|阿里巴巴 HBase 高可用8年抗战回忆录
关注 HBase 技术社区,获取更多技术干货
你也「 在看 」吗? :point_down:
以上所述就是小编给大家介绍的《ZooKeeper原理|ZooKeeper网络故障应对法》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。