网络故障可以说是分布式系统天生的宿敌。如果永远不发生网络故障,我们实际上可以设计出高可用强一致的分布式系统。可惜的是不发生网络故障的分布式环境还不存在,ZK 使用过程中也需要小心的应付网络故障。

让我们先忘掉故障发生的情况,首先来看到 ZK 对网络连接的处理。ZK 客户端启动时带有所有可用的服务器的信息,它会随机选择和其中一台服务器尝试连接,在正常的成功连接的情况下,ZK 客户端和服务端会建立起一个会话(session),在会话超时之前服务端会响应客户端的请求,每次新的请求都会刷新会话超时的时间。当 ZK 客户端和当前服务器失联时,它会试着从可用的服务器列表中重新连接到一台服务器上。

总体地看过 ZK 正常的网络连接处理之后,我们来看看网络故障在 ZK 世界中的抽象。网络故障在 ZK 的层面被转换为两种异常,一种是 ConnectionLossException ,一种是 SessionExpireException。前者发生在超时时间之前 ZK 客户端与某台服务器断开之后,后者发生在服务端通知客户端会话超时的时候。

ConnectionLossException

这个异常肯定是 ZK 中最让人头痛的异常之一了。ZK 客户端通过 socket 和 ZK 服务端的某台服务器连接,在客户端由 ClientCnxn 管理,在服务端由 ServerCnxn 管理。ConnectionLossException 发生在 ZK 客户端失去与 ZK 服务器的连接的时候,它仅仅表明 ZK 客户端发现自己失去了和当前服务器的连接,除此之外什么也不知道。这里存在三个重要的问题。

从可恢复的故障中恢复

ConnectionLossException 是一个可恢复的异常,它仅仅代表着与当前服务器的连接失效,客户端完全有可能稍后连接上另一个服务器并重新开始发送请求。在客户端与 ZK 连接不稳定的情况下,我们需要特别小心的处理这类异常。否则,因为网络抖动而使上层应用崩溃是不可接受的。此外,重新创建 ZK 客户端,开启一个新的会话则只会加剧网络的不稳定性。这是因为客户端不重连的情况下服务端只能通过会话超时来释放与客户端的连接,如果由于连接过多导致响应不稳定,开启新的会话只会恶化这个情况。

一种常见的容忍 ConnectionLossException 的方式是重做动作,也就是形如下面代码的处理逻辑

operation(...) {
zk.create(path, data, ids, mode, callback, data);
} callback = (rc, path, ctx, name) -> {
switch (Code.get(rc)) {
case CONNECTIONLOSS:
operation(...);
break; // ...
}
}

服务端上操作可能已经成功

上面提到从可恢复的故障中回复的时候,介绍了一种通过重做动作的方法。然而,重做动作是有风险的。这是因为先前的动作可能在客户端上已经成功。

ConnectionLossException 只表明 ZK 客户端发现自己与服务端的连接断开,但是在断开之前,对应的请求完全可能已经发送出去,已经到达服务端并被处理。只是由于客户端与服务端的连接断开而收不到回应而是触发 ConnectionLossException 罢了。

对于读操作,重试通常没有什么问题,因为我们总能得到重试成功的时候读操作应有的返回值(或异常)。对于写操作,情况稍微微妙一些。

对于 setData 操作,在重试成功的情况下,不考虑具体的业务逻辑,我们可以认为问题不大。因为两次把节点设置为同一个值是幂等操作,对于前一次操作更新了 version 从而导致重试操作 version 不匹配的情况,我们也可以由吞掉异常或者触发业务相关的异常逻辑。

对于 delete 操作,重试可能导致意外的 NoNodeException,我们可以吞掉这个异常或者触发业务相关的异常逻辑。

对于 create 操作,情况稍微复杂一点。在非 sequential 的情况下,create 可能成功或者触发一个 NodeExistException,这跟 delete 大约是对应的处理方式。但是在 sequential 的情况下,有可能先前的操作已经成功,而重试的操作也成功。由于我们丢失了先前操作的返回值,因此先前操作的 sequential 节点就成了孤儿,这有可能导致资源泄露或者更严重的一致性问题。例如,基于 ZK 的 leader 选举算法依赖于 sequential 节点的排序,一个序号最小的孤儿节点将导致整个算法失败。这是因为孤儿节点成为了 leader 其 callback 却被 ConnectionLossException 触发了,任何客户端也没有对它的所有权,因此它也不会被删除以推动算法继续进行。

客户端可能错过状态变化

ZK 的 Watcher 是单次触发的,在前一次 Watcher 被触发到重新设置 Watcher 并被触发的间隔之间的事件可能会丢失。这本身是 ZK 上层应用需要考虑的一个重要的问题。 ConnectionLossException 会触发 Watcher 接收到一个 WatchedEvent(EventType.None, KeeperState.Disconnected) 的事件。一旦收到这个事件,ZK 客户端必须假定 ZK 上的状态可能发生任意变化。对于依赖于某些状态例如自己是应用程序中的 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 好处理的地方在于它是严格不可恢复的故障,ZK 客户端会话超时之后无法重新和服务端成功连接,因此我们通常只需要重新创建一个 ZK 客户端实例并重新开始开始工作。但是会话超时会导致 ephemeral 节点被删除,如果上层应用逻辑与此相关的话,就需要仔细的处理 SessionExpiredException

会话超时的检测

ZK 客户端与服务器成功建立连接后,ClientCnxn.SendThread 会周期性的向服务器发送 ping 信息,服务器在处理 ping 信息的时候重置会话超时的时间。如果服务器在超时时间内没有收到客户端发来的任何新的信息,那么它就会宣布这个会话超时,并显式的关掉对应的链接。ZK 会话超时相关的逻辑在 SessionTracker 中,所有会话检查和超时的判断都是由 leader 作出的,也就是所谓的仲裁动作(quorum operation),因此客户端超时是所有服务器一致的共识。

在 ZK 客户端的连接被服务端关闭后,客户端尝试重新连接服务器,仅当它重新连接上某个服务器时,该服务器查询服务端的会话列表,发现这个重连请求属于超时会话,通过返回非正整数的超时剩余时间通知客户端会话已超时。随后,客户端得知自己已经超时并执行相应的退出逻辑。

这里有一个非常 tricky 的事情,就是 ZK 客户端的会话超时永远是由服务端通知的。那么,在一种很合理的超时情况,即服务端挂了或者客户端与服务端彻底分区的情况下,实际上 ZK 客户端是无法得知自己的会话已经超时的。ZK 目前没有办法处理这一情况,只能依赖上层应用自己去处理。例如,在确定之后主动地关闭 ZK 客户端并重启。在 Curator 中通过 ConnectionStateManager#processEvents 周期性的检查在收到最后一个 disconnect 事件后过去的时间,从而从客户端的角度在必然超时的时候注入会话超时事件。

ephemeral 节点的删除

跟 ephemeral 节点的删除相关的最大的问题是关于基于 ZK 的 leader 选举的。ZK 提供了 leader 选举的 recipe 参考[1],总的来说是基于一系列 ephemral sequential 节点的排序来做的。当上层应用基于 ZK 做 leader 选举时,如果 ZK 客户端与服务端超时,由于 ZK 相关的操作和相应往往和上层应用的主线程是分开的,这样在上层应用得知自己不是 leader 之前就有可能作出很多越权的操作。

例如,在 FLINK 中,理论上只有成为 leader 的 JobManager 才有权限写入 checkpoint,但是由于 ZK 上产生丢失 leadership 的消息,到客户端得知这一消息,再到通知上层应用,这几个步骤之间都是异步的,所以此前的 leader 并不能第一时间得知自己丢失 leadership 了。同时,其他的 JobManager 可能在同一时间被通知当选 leader。此时,集群中就会有两个 JobManager 认为自己是 leader。如果对它们写入 checkpoint 的动作不做其他限制,即只要 JobManager 认为自己有权限,就是有权限的话,就可能导致两个 leader 并发的写入 checkpoint 从而导致状态不一致。这个由于响应时间带来的问题 Curator 的技术注意事项中已有提及[2],由于发生概率较小,而且实现上依赖于”及时地“响应远端信息,因此虽然不少系统都有这个理论上的 BUG,但是很多时候只是作为注意事项帮助开发者和使用者在极端情况下理解发生了什么。

FLINK-10333[3] 和 ZK 邮件列表上我发起的这个讨论[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@<user.zookeeper.apache.org>

ZK 网络故障应对法的更多相关文章

  1. IEEP部署企业级网络工程-网络故障-环路故障

    网络故障 1.环路故障 概念 1).以太网是一个支持广播的网络, 在没有环路的环境中,广播报文在网络中以泛洪的形式被送达到网络的第一个角落,以保证每个设备都能够接受到它.每台二层设备在接收到广播报文以 ...

  2. 记一次诡异的网络故障排除 - tpc_tw_recycle参数引起的网络故障

    一.故障现象 我们团队访问腾讯云上部署的测试环境中的Web系统A时,偶尔会出现类似于网络闪断的情况,浏览器卡很久没有反应,最终报Connection Timeout. 不过奇怪的是,当团队中的某个人无 ...

  3. React Native 在用户网络故障时自动调取缓存

    App往往都有缓存功能,例如常见的新闻类应用,如果你关闭网络,你上次打开App加载的数据还在,只是不能加载新的数据了. 我的博客bougieblog.cn,欢迎前来尬聊. 集中处理请求 如果你fetc ...

  4. traceroute排查网络故障 www.qq.com排查网络故障网络不通 先ping自己

    网络不通 先ping自己 在ping网关 再ping外网 再ping别人的ip 背景需求 Linux 因为其强大的网络处理能力,被广泛用于网关(实例链接)和服务器(实例链接).实际工作中,快速排查这些 ...

  5. 一次“不负责任”的 K8s 网络故障排查经验分享

    作者 | 骆冰利 来源 | Erda 公众号 ​ 某天晚上,客户碰到了这样的问题:K8s 集群一直扩容失败,所有节点都无法正常加入集群.在经过多番折腾无解后,客户将问题反馈到我们这里,希望得到技术支持 ...

  6. linux篇-公司网络故障那些事(路由器变交换机)

    首先这次网络故障是断电引起的 我给大家画个模型 三层的为八口交换机 一层的为五口打印机 笔记本代表两台无线打印机 首先八口的连接了公司采购电脑一台,业务电脑一台,其他电脑三台 第二个五口交换的连接财务 ...

  7. ping网络故障

    网络的应用已渐渐深入我们的工作和生活,它带给了我们各方面的便利.因此,这种种的便利致使很多人对网络产生依赖性.那么,当电脑不能上网时,我们如何才能准确地判断电脑问题出在哪里?又如何能快捷地解决这故障? ...

  8. Linux 网络故障排查

    1.第一步是要确认网卡本身是否工作正常?利用ping工具可以确认这点.输入ping 127.0.0.1 ,然后看是否正常ping 通? 这里的127.0.0.1 被称作主机的回环接口,是TCP/IP协 ...

  9. IP-MAC绑定导致网络故障

    前段时间将一台服务器A的服务迁移至了另外一台服务器B,外网IP地址也顺带迁移过来了,结果网络出现了问题. 其中内网是畅通的,但是外网IP怎么都连不上另外一台路由C(B和C是在一个交换机下的,网段也相同 ...

随机推荐

  1. Liunx学习总结(六)--进程

    进程概述 简单来讲程序是一个包含可以执行代码的静态的文件.进程是一个开始执行但是还没有结束的程序的实例.当程序被系统调用到内存以后,系统会给程序分配一定的资源(内存,设备等等)然后进行一系列的复杂操作 ...

  2. linux 7忘记密码找回

    一.linux 7忘记密码二种更改方法 centos7/rhel7进入单用户方式和重置密码方式发生了较大变化,GRUB由b引导变成了ctrl+x引导.重置密码主要有rd.break和init两种方法. ...

  3. 实战jmeter入门压测接口性能

    什么是Jmeter? 是Apache组织开发的基于Java的压力测试工具. 准备工作: 一.安装配置好环境及压测工具 Jmeter下载地址:http://mirrors.tuna.tsinghua.e ...

  4. Leetcode之深度优先搜索(DFS)专题-329. 矩阵中的最长递增路径(Longest Increasing Path in a Matrix)

    Leetcode之深度优先搜索(DFS)专题-329. 矩阵中的最长递增路径(Longest Increasing Path in a Matrix) 深度优先搜索的解题详细介绍,点击 给定一个整数矩 ...

  5. 配置springboot项目使用外部tomcat

    配置springboot项目使用外部tomcat 1.在pom文件中添加依赖 <!--使用自带的tomcat--> <dependency> <groupId>or ...

  6. vue实现对语言的切换,结合vue-il8n。

    1.安装vue-i18n: npm install vue-i18n 如果npm长时间无反应,或安装失败,可以换成淘宝镜像安装: cnpm install vue-i18n 2.在main.js中引用 ...

  7. xcode简介及安装

    1. 简介 Xcode 是运行在操作系统Mac OS X上的集成开发工具(IDE),由苹果公司开发. Xcode是开发OS X 和 iOS 应用程序的最快捷的方式. Xcode 具有统一的用户界面设计 ...

  8. PHP. 02®. Ajax异步处理、常见的响应状态、XMLHttpRequest对象及API、ajax的get/post方法、

    异步对象 a)创建异步对象 b)设置请求的url等参数 c)  发送请求 d)注册时间 e)在注册的事件中获取返回的内容并修改页面显示的内容 布尔类型不能直接用echo输出 常见的响应状态 Ajax概 ...

  9. 【Offer】[58-2] 【左旋转字符串】

    题目描述 思路分析 测试用例 Java代码 代码链接 题目描述 字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部.请定义一个函数实现字符串左旋转操作的功能.比如,输入字符串"a ...

  10. STL中的unique和unique_copy函数

    一.unique函数 这个函数的功能就是删除相邻的重复元素,然后重新排列输入范围内的元素,并返回一个最后一个无重复值的迭代器(并不改变容器长度). 例如: vector<); ; i < ...