引言
zookeeper(动物管理员)设计目标为分布式系统的任务执行提供协同支持,包括Hadoop,Storm,Hive等分布式系统,解决分布式系统常见问题
分布式系统常见问题
1、主节点崩溃:如果主节点发送崩溃,就无法给从节点分配任务或重新分配已失败的任务;
2、从节点崩溃:如果从节点崩溃,就无法执行分配的任务;
3、通信故障:如果主节点和从节点之间无法通信,从节点就收不到主节点分配给它的任务;
主节点崩溃的解决方案:
主节点崩溃后需要有一个备份主节点,当主节点崩溃后,备份主节点接管主节点的角色,进行故障转移。这样会导致出现3个新的的问题:
1、备份节点需要恢复到主节点崩溃前的状态,而主节点的原状态信息不能从已崩溃的主节点上获取。
fan2、假如主节点并未崩溃,但是备份节点却认为主节点已崩溃,备份节点会成为主节点的角色,成为第二个主节点。主要的场景就是主节点负载很高,导致通讯延迟,备份节点会认为主节点已崩溃;
3、从节点无法与主节点通讯,由于网络分区的原因,部分从节点会与主节点断开联系,而与第二个主节点建立通讯。这样会导致系统中会有多个部分独立工作,导致整体行为不一致,这种情况称为脑裂。
从节点崩溃解决方案:
1、如果从节点崩溃了,所有分配给从节点未完成的任务需要重新分配给其他从节点,这样的话,主节点需要能够检测从节点是否崩溃,哪些从节点有效可以派发未完成的任务;
2、从节点崩溃时,该节点执行的任务可能部分完成,也许全部执行完但没有报告执行结果。如果整个运算过程产生了其他作用,我们还有必要执行某些恢复过程来清除之前的状态;
通信故障解决方案:
1、网络通信故障,有可能会导致主节点不知道子节点执行的已分配任务的状态,可能会重新分配任务,这样就会导致任务重新执行。若该任务幂等,则可重复执行;若不幂等,则需要考虑多个节点执行同个任务的情况;
2、采用分布式锁控制的话,会出现从节点正常执行并释放了锁,但是主节点认为从节点已失效,重新分配了任务并且新的从节点也可以正常的获取锁。
解决这种情况,zk需要客户端告诉某些数据的状态是临时的,同时,zk需要客户端定时发送是否存活的通知,若一个客户端未能及时发送通知,那么所有属于这个客户端的临时状态的数据都将全部被删除。通过这两个机制,在崩溃或发生通信故障时,我们可以预防客户端独立运行而发生的应用宕机
总结上面的问题,zk的主要任务:
1、主节点选举:关键的一步,使得主节点可以给从节点分配任务
2、崩溃检测:主节点必须具备检测从节点崩溃或者失去连接的能力
3、组成员管理:主节点必须具备知道哪一个从节点可以执行任务的能力
4、元数据管理:主节点和从节点必须具有通过某种可靠的方式保存分配状态和执行状态的能力
一、基础内容
1、数据模型
zookeeper维护一个树形目录结构,树上的节点成为znode,znode可以用来存储数据,由于zookeeper设计初衷是为了协调分布式服务,因此znode中通常用来存储少量的元数据,通常大小不超过1MB。znode的树形结构如下图。
2、znode类型
持久节点:只能通过/delete命令删除
临时节点:客户端崩溃或关闭了与zk的连接,这个节点就会被删除。或者客户端主动删除。(临时节点可以用于检测节点状态)
有序节点:一个有序znode节点被分配唯一一个单调递增的整数,当创建有序节点时,一个序号会被追加到路径之后。可以通过序号判断节点创建的顺序
znode一共有4种类型:持久型,临时型,持久有序,临时有序
3、常用命令
create /path data:创建一个名为/path的znode节点,并包含数据data;创建临时节点:create -e /path data
delete /path:删除名为/path的节点
exists /path:检查是否存在名为/path的节点
setData /path data:设置名为/path的znode的数据为data
getData /path:返回名为/path的节点的数据
getChildren /path:返回所有/path节点下的所有子节点
stat /path:查询节点状态
stat查询的数据属性:
czxid:节点创建的zxid
mzxid:节点最新一次更新发生时的zxid
ctime:节点创建时的时间戳
mtime:节点最新一次更新发生时的时间戳
dataversion:节点数据的更新次数
cversion:子节点的更新次数
aclVersion:节点ACL授权信息的更新次数
ephemeralOwner:如果该节点为临时节点,该值表示与该节点绑定的sessionid;如果不是临时节点,该值为0
dataLength:节点数据字节数
numChildren:子节点个数
4、znode监听
客户端通过向zk注册需要接受通知的znode,通过对znode设置监视点(watch)来接受通知。监视点是一个单次触发的操作,意即监视点会触发一个通知。为了接受多个通知,客户端必须在每次通知后设置一个新的监视点。
通知机制的一个重要保障:对于同一个znode操作,先向客户端传送通知,然后在对该节点进行更新。如果客户端对一个znode设置了监视点,而该znode发生了两个连续的更新。第一次更新后,客户端在观察第二次变化前就收到了通知,然后读取znode中的数据。
zk监视点对应的通知类型:znode的数据变化;znode子节点的变化;znode的创建或删除。
使用ls /path true表示监听该节点,当该节点发生变化时,会收到事件通知
5、znode的版本号:
每个znode都有一个版本号,它随着每次数据变化而自增。当使用setData和delete对节点进行更新操作时,会以版本号作为转入参数,只有当转入参数的版本号与服务器上的版本号一致时调用才会成功。当版本号为-1时,表示匹配全部版本号,默认操作指令的版本号为-1
6、znode权限控制
znode的访问权限的检查是基于每个znode的,如果一个客户端可以访问一个znode,即使该客户端无权访问该节点的父节点,仍然可以访问该节点。
zk通过访问控制表来控制访问权限,一个ACK包含以下形式的记录:
scheme:auth-info,其中scheme对应了一组内置的鉴权模式,auth-info为对于特定模式所对应的方式进行编码的鉴权信息。
通过调研addAuthInfo来增加鉴权信息:
void addAuthInfo(String scheme, byte[] auth);
zk内置的acl鉴权模式:
1、OPEN_ACL_UNSAFE:使用anyone作为auth-info,表示任何人都可以访问
2、super:可以访问所有节点,不用鉴权
3、digest:命令格式:digest:name:secret, READ|WRITE|CREATE|DELETE|ADMIN
4、ip鉴权:ip:127.0.0.1/8080, READ,通过ip鉴权模式,不需要使用addAuthInfo
二、高级特性
1、zookeeper分布式锁的实现
并发访问时,可以先在zk上创建一个/lock的临时节点,若创建成功,表示获取锁成功,否则未获取成功,需要监听该节点,当该节点被删除后重新尝试创建;当使用完成后,主动释放或者断开与zk的连接,该锁就会被zk回收,这样可以保证该锁一定会被释放。其他等待线程监听该/lock节点,一旦该节点被释放,则可发起创建请求。
为防止大量的线程都在监听一个节点,当该节点发生变化时产生大量的通知,可能造成一定的影响,可以使用临时有序节点,以创建最小序列号的znode为锁的持有者,监听时,每个客户端监听前一个znode,如创建/lock-003的客户端监听/lock-002的znode,若/lock-002不存在,则/lock-003获得了锁
2、同步方法
为解决两个zk客户端在隐藏通道进行通讯,zk1客户端进行更新后,通知zk2,而zk2连接的zk服务器与zk1不同,且还未更新。此时,zk2就拿到不最新的更新数据。在getData调用sync方法就是为了解决这个问题。
zk.sync(path, voidCb, ctx);
服务端处理sync调用时,服务端会刷新群首与调用sync操作的客户端c所连接的服务端之间的通道,刷新的意思就是说在调用getData的返回数据的时候,服务端确保返回所有客户端c调用sync方法时所有可能的变化情况。上面的场景中,变化的情况通讯会先于sync操作的调用而发生,因此当zk2收到getData调用的响应,响应中必定包含zk1所通知的变化情况。注意,在此时,该节点也可能发生了其他变化,因此在调用getData时,zk服务器只保证所有变化情况能够返回。
sync还有个问题,就是在群首变更时,snyc操作不会进入执行管道中(zk的create,setData,delete会放入执行管道中,即使发生群首变更,也会被顺序执行),因此snyc操作有可能不会被传递执行。
三、zookeeper核心内容
1、zk架构:
zk服务器运行于两种模式下:独立模式(单点模式)和仲裁模式(zk集群)。其中集群模式下的zk服务器之间会进行状态复制。
zk集群模式下运行崩溃的服务器不得多于集群服务器的一半。
zk客户端通过TCP与zk服务器创建一个会话(长连接),zk服务端对一个会话中请求遵循FIFO的处理方式,当会话结束后,会话创建的临时节点就会失效。当一个客户端创建了多个会话,并同时发生请求时,就会破坏FIFO的模式。会话事件有:NOT_CONNECTED,CONNECTING,CONNECTED,CLOSED
2、zk会话:
客户端会话的生命周期:当未进行连接时,会话状态为NOT_CONNECTED,当zk客户端初始化后,转到CONNECTING状态。成功与zk服务器建立连接后,会话转到CONNECTED状态。当客户端与zk失去连接或者无法接受响应时,会转换回CONNECTING状态并尝试发现其他zk服务器。如果重连或发现其他服务器成功,会话会转回CONNECTED状态,否则,它会声明会话过期,然后转换到CLOSED状态。
在创建一个会话时,需要设置会话超时时间。当经过T时间,服务器收不到这个会话的任何消息,服务端就会声明会话过期。而在客户端,如果经过T/3的时间未收到任何消息,客户端将向服务端发送心跳消息。再经过2T/3时间后,zk客户端开始寻找其他服务器,而此时,它还有T/3的时间进行寻找。
当连接到一个新的服务器上时,该服务器的状态要与最后一次连接的zk服务器状态保持最新。zk通过在服务中排序更新操作来决定状态是否最新。zk确保每一个变化相对于其他已执行的更新是完全有序。如果一个客户端在位置i观察到一个更新,它就不能连接到只观察到j < i的服务器上。在zk实现中,系统根据每一个更新建立的顺序来分配给事物标识符(zkid)。
3、zk读写操作顺序:
写操作的顺序:zk集群中,zk的状态会在集群中进行复制。服务端对于状态变化的顺序达成一致,并会使用相同的顺序执行状态的更新。但是这个更新时间不是同时的。
读操作的顺序:若不采用隐藏通道,(即应用系统之间通过其他通讯方式进行通讯,不通过zk),读的顺序取决于通知的顺序
通知的顺序:在对znode进行更新操作前,zk会先将通知发送给客户端,这可以确保客户端不会读到任何无效配置。当进行批量更新时,为防止客户端读取部分更新数据,有两种解决办法:
(1)、下发更新通知后,客户端先统一读取一个znode,如果该znode存在,则说明更新未完成,不能进行读取操作;否则进行读取
(2)、使用Op操作进行更新,保证原子性。
4、请求、事物和标识符:
zk服务器在处理只读请求时,只会在本地运行,因此性能会很高;
对于更新操作,都会被转发给群首,由群首执行相应的操作,生成事物。对于一个setData操作,zk会将znode的数据信息和版本号包装成一个事物。当处理该事物时,服务端将会用事物中的数据信息来替换znode中的原数据信息,并会用事物中的版本号更新该节点,而不是增加版本号的值。
一个事物为一个单位,所有的变更处理需要以原子方式执行。zk集群以事物的方式运行,并确保所有的变更操作已原子方式被执行,同时不会被其他事物所干扰。在zk中并不存在回滚机制,而是确保事物的每一步操作都不互相干扰。每个zk服务器都会启动一个单独线程来处理事物,通过单线程来保障事物之间的顺序执行互不干扰。
同时,事物具有幂等性,多次执行同一个事物的是一样的。但是多个事物执行要考虑到顺序问题,否则无法实现一致性。
zk群首产生的事物标识符为zxid,通过zxid对事物进行标识,就可以按照群首所指定的顺序在各个服务器中按序执行。服务器之间进行新的群首选举时也会交换zxid信息,这样就可以知道哪个无障服务器接受了更多的事物,并可以同步他们之间的状态信息。
zxid一个long型整数(64位),分为两部分:时间戳和计数器,每部分32位。zk通过广播各个服务器之间的状态变更信息。
5、Zab协议:zookeeper atomic broadcast protocol原子广播协议
在接收到一个请求后,追随者会把该请求转发给群首,群首将探索性的执行该请求,并将执行结果以事物的方式对状态更新进行广播。当事物进行提交时,服务器就会将这些变更反馈到数据树上。
服务器如何确认一个事物已经提交(两阶段提交):
(1)、群首向所有追随者发送一个提案消息
(2)、当一个追随者接收到消息后,追随者会检查所发送的提案消息是否属于其所追随的群首,并确认群首所广播的提案消息和提交事物的顺序是否正确,然后响应群首一个ACK消息,通知群首已接受该提案。
(3)、当收到仲裁数量的服务器发送的确认消息后,群首就会发送消息通知追随者进行提交操作。
Zab协议保障了以下几个重要属性:
如果群首按顺序广播了事物T和事物T+,那么每个服务器在提交T+事物前保证事物T已经提交完成。(保证事物传递顺序的一致性)
如果某个服务器按照事物T、事物T+的顺序提交事物,所有其他服务器也必然会在提交事物T+前提交事物T。(保证服务器不会跳过任何事物)
一个被选举群首确保在提交完所有之前的时间戳内需要提交的事物,之后才开始广播新的事物。(保证及时存在多个群首,也不会导致事物被随意广播,导致服务器提交事物的顺序乱掉,群首不会立即处于活动状态,需要确保仲裁数量的服务器认可这个群首新的时间戳值,并且保证自时间戳开始值到时间戳e-1内的所有提案被提交)
在任何时间点,都不会出现两个被仲裁支持的群首。
群首是会变化的,一个群首活动期间发送的事物,以zxid来表示,zxid由时间戳和计数器组成,当时间戳一致,表示是某个群首,因为时间戳只会在选举新的群首时产生,计数器表示执行的第几个事物。基于此,若一个服务器在当任群首时,拥有不同的时间戳,那么就认为是不同的群首。若群首s持有时间戳为4,当前的群首l持有时间戳为6,那么追随者会追随l,执行它的提案,但是在此之前也会接受4-6之间的提案。记录已接受的提案消息非常关键,这样可以确保所有的服务器最终提交了被某个或多个服务已经提交完成的事物。
对于时间戳转换造成的追随者滞后,zk采用两种解决办法:
(1)、若滞后不多,则群首只需发送缺失的事物点,因为追随者按照严格的顺序接收事物点,这些缺失事物点永远是最近的。
(2)、若滞后很多,zk将发送名为SNAP的完整快照。发送完整的快照会增大系统恢复的延时。
6、观察者:
观察者不参与选举,但是接收群首的inform消息。群首发送给追随者有两种消息,一种是提议,包含了事物信息,一种是inform,只包含了zxid,不包含具体的数据信息。因此inform消息本质上是事物的提交消息。因此观察者不能实施提议。
引入观察者的一个主要原因是提高读请求的可扩展性。通过加入多个观察者,我们可以在不牺牲写操作的吞吐率的前提下服务更多的读操作。观察者可以观察到事物是否已经被提交,若被提交则是最新的版本,可以读取,否则需要等待写完成。以此来提高读操作。
7、群首选举:
zk采用仲裁模式进行群首选举,其中仲裁模式要求支持一个群首服务器数量必须至少存在一个服务器进程的交叉,仲裁服务器之间两两相交。
选举流程:
(1)、每个服务器启动后进入LOOKING状态,开始选举一个新的群首或查找已存在的群首。如果群首存在,其他服务器就会告诉新加入的服务器哪个是群首,同时,新服务器也会与群首联系,以确保自己的状态与群首一致。
(2)、如果集群中所有服务器均处于LOOKING状态,这些服务器就会进行通信来选举一个群首,通过信息交换对群首选举达成共识的选择。在选举过程中胜出的服务器将进入LEADING状态,而集群中的其他服务器将会进入FOLLOWING状态
(3)、当一个服务器进入LOOKING状态,就会向集群中的每个服务器发送一个通知消息,该消息包括该服务器的投票信息,投票中包含服务器标识id(sid)和最近执行的事物的zxid,如:一个服务器的投票信息为(1,5),则表示该服务器的id为1,最近执行的事物zxid为5。
(4)、当一个服务器接受到一个投票信息,它将会根据以下规则修改自己的投票信息:
将接收的voteid和votezxid作为一个标识符,并获取自己当前投票中的myxid和myzxid。
如果(votezxid > myzxid)或(votezxid = myzxid && voteid > myid),保留当前的投票信息,即下一次的自己的投票信息将会是接收到得投票信息
否则下次投票还是用自己的投票信息
总之,只有最新的服务器会赢得选举,因为其拥有最新一次的zxid。
(5)、当一个服务器收到的仲裁数量的服务器发来的投票都一样时,就表示选举成功。
注意:若在选举过程中,某一台服务器选举出错误的群首,最终导致出现两个群首,在这种情况下,两个群首又会组成一个新的仲裁服务器进行群首选举。而出错的服务器因无法接受到选举出来的群首信息而超时,因为它选的群首并没有认为自己就是群首。