故障发生的主要点有三个:
- ZooKeeper服务
- 网络
- 应用程序
如图5-1所展示的简单结构
三个服务器组成了ZooKeeper的服务。进程会随机连接到其中一个服务器,也可能断开后再次连接到另一个不同的服务器,服务器使用内部协议来保持客户端之间状态的同步,对客户端呈现一致性视图。
图5-2展示了系统的不同组件中可能发生的一些故障
一、可恢复的故障
一个客户端从ZooKeeper获得响应时,客户端可以非常肯定这个响应信息与其他响应信息或其他客户端所接收的响应均保持一致性。有时,ZooKeeper客户端库与ZooKeeper服务的连接会丢失,而且无法提供一致性保障的信息,当客户端库发现自己处于这种情况时,就会使用Disconnected事件和ConnectionLossException异常来表示自己无法了解当前的系统状态。
ZooKeeper客户端库会积极地尝试,使自己离开这种情况,它会不断尝试重新连接另一个ZooKeeper服务器,直到最终重新建立了会话。一旦会话重新建立,ZooKeeper会产生一个SyncConnected事件,并开始处理请求。
ZooKeeper还会注册之前已经注册过的监视点,并会对失去连接这段时间发生的变更产生监视点事件。
Disconnected事件和ConnectionLossException异常的产生的一个典型原因是因为ZooKeeper服务器故障。
图5-3展示了这种故障的一个示例
客户端连接到服务器s2 ,其中s2 是两个活动ZooKeeper服务器中的一个,当s2 发生故障,客户端的Watcher对象就会收到Disconnected事件,并且,所有进行中的请求都会返回ConnectionLossException异常。
如果此时客户端正在进行某些请求,比如刚刚提交了一个create操作的请求,当连接丢失发生时,对于同步请求,客户端会得到ConnectionLossException异常,对于异步请求,会得到CONNECTIONLOSS返回码。然而,客户端无法通过这些异常或返回码来判断请求是否已经被处理。一个非常糟糕的方法是简单处理,当接收到ConnectionLossException异常或CONNECTIONLOSS返回码时,客户端停止所有工作,并重新启动,虽然这样可以使代码更加简单,但是,本可能是一个小影响,却变为重要的系统事件。
当一个进程失去连接后就无法收到ZooKeeper的更新通知,尽管这听起来很没什么,但是一个进程也许会在会话丢失时错过了某些重要的状态变化。
图5-4展示了这种情况的例子
客户端c1作为群首,在t2 时刻失去了连接,但是并没发现这个情况,直到t4 时刻才声明为终止状态,同时,会话在t2 时刻过期,在t3 时刻另一个进程成为群首,从t 2 到t 4 时刻旧的群首并不知道它自己被声明为终止状态,而另一个群首已经接管控制。如果开发者不仔细处理,旧的群首会继续担当群首,并且其操作可能与新的群首相冲突。因此,当一个进程接收到Disconnected事件时,在重新连接之前,进程需要挂起群首的操作。正常情况下,重新连接会很快发生,如果客户端失去连接持续了一段时间,进程也许会选择关闭会话,当然,如果客户端失去连接,关闭会话也不会使ZooKeeper更快地关闭会话,ZooKeeper服务依然会等待会话过期时间过去以后才声明会话已过期。
注意:很长的延时与过期
已存在的监视点与Disconnected事件
为了使连接断开与重现建立会话之间更加平滑,ZooKeeper客户端库会在新的服务器上重新建立所有已经存在的监视点。当客户端连接ZooKeeper的服务器,客户端会发送监视点列表和最后已知的zxid(最终状态的时间戳),服务器会接受这些监视点并检查znode节点的修改时间戳与这些监视点是否对应,如果任何已经监视的znode节点的修改时间戳晚于最后已知的zxid,服务器就会触发这个监视点。每个ZooKeeper操作都完全符合该逻辑,除了exists。exists操作与其他操作不同,因为这个操作可以在一个不存在的节点上设置监视点,如果仔细看前一段中所说的注册监视点逻辑,会发现存在一种错过监视点事件的特殊情况。
图5-5说明了这种特殊情况
导致错过了一个设置了监视点的znode节点的创建事件,客户端监视/event节点的创建事件,然而就在/event被另一个客户端创建时,设置了监视点的客户端与ZooKeeper间失去连接,在这段时间,其他客户端删除了/event,因此当设置了监视点的客户端重新与ZooKeeper建立连接并注册监视点,ZooKeeper服务器已经不存在/event节点了,因此,当处理已经注册的监视点并判断/event的监视时,发现没有/event这个节点,所以就只是注册了这个监视点,最终导致客户端错过了/event的创建事件。因为这种特殊情况,需要
尽量避免监视一个znode节点的创建事件,如果一定要监视创建事件,应尽量监视存活期更长的znode节点,否则这种特殊情况可能会伤害你。
二、不可恢复的故障
两种情况下,ZooKeeper都会丢弃会话的状态:
- 会话过期
- 已认证的会话无法再次与ZooKeeper完成认证
三、群首选举和外部资源
【ZooKeeper无法保护与外部设备的交互操作】
当运行客户端进程的主机发生过载,就会开始发生交换、系统颠簸或因已经超负荷的主机资源的竞争而导致的进程延迟,这些都会影响与ZooKeeper交互的及时性:
- 一方面,ZooKeeper无法及时地与ZooKeeper服务器发送心跳信息,导致ZooKeeper的会话超时。
- 另一方面,主机上本地线程的调度会导致不可预知的调度:一个应用线程认为会话仍然处于活动状态,并持有主节点,即使ZooKeeper线程有机会运行时才会通知会话已经超时。
图5-6通过时间轴展示了这个棘手的问题
在这个例子中,应用程序通过使用ZooKeeper来确保每次只有一个主节点可以独占访问一个外部资源,这是一个很普遍的资源中心化管理的方法,用来确保一致性。在时间轴的开始,客户端c1 为主节点并独占方案外部资源。
事件发生顺序如下:
1、在 t1 时刻,因为超载导致与ZooKeeper的通信停止,c1 没有响应,c1 已经排队等候对外部资源的更新,但是还没收到CPU时钟周期来发送这些更新。
2、在t2 时刻,ZooKeeper声明了c1 与ZooKeeper的会话已经终止,同时删除了所有与c1 会话关联的临时节点,包括用于成为主节点而创建的临时性节点。
3、在t3 时刻,c2 成为主节点。
4、在t4 时刻,c2 改变了外部资源的状态。
5、在t5 时刻,c1 的负载下降,并发送已队列化的更新到外部资源上。
6、在t6 时刻,c1 与ZooKeeper重现建立连接,发现其会话已经过期且丢掉了管理权。遗憾的是,破坏已经发生,在t5 时刻,已经在外部资源进行了更新,最后导致系统状态损坏。解决这个问题有几个方法:
- 一个方法是确保应用不会在超载或时钟偏移的环境中运行,小心监控系统负载可以检测到环境出现问题的可能性,良好设计的多线程应用也可以避免超载,时钟同步程序可以保证系统时钟的同步。
- 另一个方法是通过ZooKeeper扩展对外部设备协作的数据,使用一种名为隔离(fencing)的技巧,分布式系统中常常使用这种方法用于确保资源的独占访问。
用一个例子来说明如何通过隔离符号来实现一个简单的隔离,只有持有最新符号的客户端,才可以访问资源:
在创建代表群首的节点时,可以获得Stat结构的信息,其中该结构中的成员之一,czxid,表示创建该节点时的zxid,zxid为唯一
的单调递增的序列号,因此可以使用czxid作为一个隔离的符号。当对外部资源进行请求时,或在连接外部资源时,还需要提供这个隔离符号,如果外部资源已经接收到更高版本的隔离符号的请求或连接时,我们的请求或连接就会被拒绝。
也就是说如果一个主节点连接到外部资源开始管理时,若旧的主节点尝试对外币资源进行某些处理,其请求将会失败,这些请求会被隔离开。即使出现系统超载或时钟偏移,隔离技巧依然可以可靠地工作。
图5-7展示了如何通过该技巧解决图5-6的情况。
1、当c 1 在t 1 时刻成为群首,创建/leader节点的zxid为3(真实环境中,zxid为一个很大的数字),在连接数据库时使用创建的zxid值作为隔离符号。
2、之后,c 1 因超载而无法响应,在t 2 时刻,ZooKeeper声明c 1 终止,c 2 成为新的群首。
3、c 2 使用4所作为隔离符号,因为其创建/leader节点的创建zxid为4。
4、在t 3时刻,c 2 开始使用隔离符号对数据库进行操作请求。
5、在t 4 时刻,c 1 '的请求到达数据库,请求会因传入的隔离符号(3)小于已知的隔离符号(4)而被拒绝,因此避免了系统的破坏。
不过,隔离方案需要修改客户端与资源之间的协议,需要在协议中添加zxid,外部资源也需要持久化保存来跟踪接收到的最新的zxid。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。