ZooKeeper 是一个由Apache软件基金会开发的开源分布式协调和配置服务。它主要用于大型分布式系统中,用来解决诸如配置管理、服务发现、同步服务等分布式系统常见问题。允许分布式进程通过共享的层次名字空间,这是一组以目录树方式组织的数据注册引用,共同协调信息。
特点
最终一致性:客户端看到的数据最终是一致的。
可靠性:服务器保存了消息,那么它就一直都存在。
实时性:ZooKeeper 不能保证两个客户端同时得到刚更新的数据。
独立性(等待无关):不同客户端直接互不影响。
原子性:更新要不成功要不失败,没有第三个状态。
核心功能
1. 配置管理
允许分布式应用程序从中央存储库读取应用程序设置和配置信息,当配置信息更新时可以即时通知到各个应用实例。
2. 名称服务
提供了一个全局唯一的命名空间,用于解决名字到系统资源或服务的映射问题。
3. 分布式锁
可以提供跨多个节点的分布式锁服务,确保在分布式环境中只有一个进程能执行关键代码段。
4. 选举机制
对于需要选出一个主节点(Leader)的分布式系统,ZooKeeper 是一个理想的解决方案,它可以协助进行 Leader 选举。
5. 队列管理
可以实现分布式队列,确保任务的安全执行并保证任务不会被重复执行。
6. 节点和数据模型
架构是树形结构,每个节点称为 Znode,既可以用于存储数据,也可以用于监控子节点的变化。
7. 通知机制
提供了一个基于推送的通知机制,客户端可以在znode发生变化时获得通知,从而更新本地数据或执行相关操作。
8. 可靠性和顺序性
保证了一个客户端请求的串行执行,并为服务中的每个更新都分配一个全局唯一的递增编号,保证了顺序性和一致性。
9. 多客户端协调
支持从多个客户端并发访问,而且保证客户端间的逻辑一致性。
10. 持久性和临时性节点
zookeeper中的节点可以是持久化的也可以是临时的。持久化节点在创建后会一直存在,直到显式删除;临时节点在创建它的客户端会话结束时自动删除。
Znode
类型
持久 Znode(Persistent Znode)
它们在创建后会一直存在于 ZooKeeper 中,直到明确删除。
持久znode可以有子节点,子节点也可以是持久的。
临时 Znode(Ephemeral Znode)
它们的生命周期绑定到创建znode的客户端会话。当会话结束时,临时znode会被自动删除。
临时znode不可以有子节点。
顺序 Znode(Sequential Znode)
在创建时,ZooKeeper 会自动在znode名称后附加一个单调递增的计数器作为后缀,这保证了节点名称的唯一性。临时和持久节点都可以是顺序的。
特性
路径:每个znode都由一个路径标识,这个路径是绝对路径,类似于文件系统中的路径,以斜杠(/)为开始。
序列化节点:znode可以被设置为顺序性的,即当它们被创建时,ZooKeeper会自动在名称后附加一个递增的计数器。这对于实现一些复杂的协调机制,如分布式队列、锁等非常有用。
数据和版本号:每个znode保存的数据可以有大小限制,并可以更改。每次数据被更改,ZooKeeper都会增加该节点的数据版本号。
子节点:znode可以有子节点。与文件系统不同,znode存储数据的同时也可能拥有子节点。
临时节点:znode可以是临时的,这种znode的生命周期与创建它的会话绑定。如果会话关闭或会话超时,这些临时节点将被自动删除。
监视器(Watches):客户端可以对特定的znode设置监视器。这些监视器会在znode发生特定变化时通知客户端,例如,znode本身的创建、删除或znode数据的更改。
Watcher机制
工作原理
- 设置watcher
客户端在读取节点数据或获取节点子列表的过程中可以向这些节点注册Watcher。例如,使用getData()或getChildren()API方法时设置一个watch标志。
- watcher事件触发
当一个 Watcher 被注册后,如果被监听的节点发生了特定的事件变化,比如节点数据的更改、节点创建或删除,ZooKeeper 将向所有注册了相应节点 Watcher 的客户端发送通知。在 ZooKeeper 中,Watchers 是一次性的,这意味着一旦触发,它们将失效。如果客户端需要持续监听节点变化,需要在获取事件通知后重新注册 Watcher。
事件类型
- NodeCreated:指定节点被创建时触发。
- NodeDeleted:指定节点被删除时触发。
- NodeDataChanged:指定节点数据变化时触发。
- NodeChildrenChanged:指定节点的子节点列表变化时触发。
使用场景
- 配置管理:监听配置节点的变化,动态更新分布式应用中的配置。
- 服务发现:监听表示服务地址的节点,当服务实例变化时进行响应。
- 集群管理:检测节点添加或移除,进行分布式锁和领导选举等操作。
- 同步原语:实现分布式锁和队列等同步原语。
注意事项
- 慎重使用 Watcher 以避免大量并发的触发可能导致的性能问题。
- Watcher应该只用于对于应用很关键并需及时响应的变化。对于不太敏感的更改,可以考虑使用轮询等其他机制。
- 由于Watcher是一次性的,为了持续监听,需要在事件处理函数中重新注册Watcher,因此要设计好事件处理的逻辑,以免错过事件通知。
Server工作状态
LOOKING: 寻找Leader状态。当服务器处于该状态时,它会认为当前集群中没有Leader,因此需要进入Leader选举状态。
FOLLOWING: 跟随者状态。表明当前服务器角色是Follower。
LEADING: 领导者状态。表明当前服务器角色是Leader。
OBSERVING: 观察者状态。表明当前服务器角色是Observer。
持久化机制
Zookeeper的持久化机制涉及到数据存储和事务日志的持久化。
首先,Zookeeper的数据存储有两种方式:内存数据库和磁盘数据库。默认情况下,Zookeeper使用内存数据库,从磁盘加载先前保存在内存中的数据。一旦开始处理客户端请求,它会将所有更改记录在内存中,并异步将这些更改写入磁盘。这种机制确保了数据的实时性。另一方面,磁盘数据库作为可选的数据存储方式,将Zookeeper数据写入磁盘,并在启动时读取磁盘上的数据。在写入磁盘时,Zookeeper使用了一种称为"快照"的机制,该机制会将整个Zookeeper数据树保存到磁盘上的一个文件中。
其次,事务日志持久化是通过记录每一次事务操作到日志文件实现的。这些日志文件对于数据的恢复和事务的完整性非常重要。每当有一个新的事务操作时,会将这个操作记录到一个新的日志文件中,文件名通常包含一个特定的ZXID(表示该文件起始的事务ID)。这种日志文件格式有助于在需要时重放这些操作,从而恢复数据。
此外,为了加快数据恢复的速度,Zookeeper还提供了对树结构和session信息进行数据快照持久化的操作。数据快照会记录Zookeeper服务器上某一时刻的全量内存数据内容,并写入指定磁盘文件中。这些快照文件的格式通常包含一个特定的ZXID,用于确定数据恢复的起始点。
服务发现中的角色及其机制
1. 注册中心
ZooKeeper充当注册中心,服务实例在ZooKeeper中创建对应的Znode用于注册。Znode保存有关服务的元数据,包括服务的地址、端口号和其他配置信息。
2. 动态服务注册
当服务实例启动时,它会在ZooKeeper中创建一个临时节点(Ephemeral),通常还是个顺序节点(Sequential)。如果服务实例停止或连接断开,对应的临时节点将会自动从ZooKeeper中移除,从而实现服务实例的动态注册与注销。
3. 服务发现
服务消费者使用ZooKeeper客户端来查询服务实例的信息。消费者可以监听特定服务的节点目录,从而发现新注册的服务实例或已经下线的实例。
4. 负载均衡
服务消费者可以实现简单的负载均衡策略,通过选择一组服务实例中的任意一个进行通信。更复杂的负载均衡策略可能根据注册的元数据(如服务实例的负载、地理位置)来选择最合适的实例。
5. 故障检测和自我修复
由于服务实例的注册信息是通过临时节点维护的,一旦服务实例失败,与其相关的节点将会消失,这一点可以被其他服务实例或消费者监听到,启动故障恢复和重新选举流程(如果适用)。
6. 集群管理
在一些集群中,ZooKeeper也用于管理服务实例的配置,以及选举主服务实例(如 Master election)。
客户端与服务器端交互过程
1.建立连接
客户端启动时,会通过ZooKeeper类(或其他相应客户端库)创建一个连接到ZooKeeper服务器的会话。在这个过程中,客户端会提供一个服务端列表(可以是单节点或集群),以及一个会话超时值。
2. 会话的创建和确认
服务器收到客户端的连接请求后,将创建一个新的会话,并生成一个唯一的会话ID。服务器返回一个响应,包含会话ID和已协商会话超时的值。客户端在本地缓存会话ID,并根据超时值来判断何时认为该会话失效。
3. 请求处理
客户端可以发送请求来读取、修改数据或更改其状态。常见操作包括创建、删除znode,或设置对 znode的监听(Watch)。
所有更改状态的操作(如创建、删除、更新znode)都由选定的服务器(Leader)来协调,并确保这些更改在集群的所有服务器中复制和一致。
4. 响应和回调
对于读操作(如 getData),服务器将直接响应客户端的请求。修改状态的操作将首先全局协调一致性,完成后再响应客户端。如果客户端对某个znode设置了监听(Watch),那么在该znode发生改变时,服务器会向客户端发送一个通知。
5. 会话维持
客户端与服务器之间会定期交换心跳,以维持会话的活跃。会话超时是基于心跳的。如果超过了会话超时时间,客户端没有收到服务器的响应,它可能会尝试重新建立连接。
6. 断开连接
客户端完成操作后,可以显式关闭会话,或者由于超时而使会话结束。关闭会话时,所有由该会话创建的临时节点都会被自动删除。
ZAB协议
为分布式协调服务Zookeeper专门设计的一种支持崩溃恢复的原子广播协议。
包括两种基本的模式:崩溃恢复和消息广播。
当整个zookeeper集群刚刚启动或者Leader服务器宕机、重启或者网络故障导致不存在过半的服务器与Leader服务器保持正常通信时,所有进程(服务器)进入崩溃恢复模式,首先选举产生新的Leader服务器,然后集群中Follower服务器开始与新的Leader服务器进行数据同步,当集群中超过半数机器与该Leader服务器完成数据同步之后,退出恢复模式进入消息广播模式,Leader服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。
集群管理
集群工作原理
选举和领导者(Leader Election)
当ZooKeeper集群启动时,它会执行"领导者选举"(leader election)过程。在一个集群中,一台服务器作为领导者,其余服务器作为跟随者(followers)或观察者(observers):
领导者(Leader):负责处理所有的客户端修改请求(创建、删除、更新znodes),并负责将这些更改广播到其它服务器。还负责进行心跳检查并协调选举。
跟随者(Followers):可以处理来自客户端的读请求,并将客户端的写请求转发给Leader,同时参与事务日志持久化和领导者选举。
观察者(Observers):观察者与跟随者类似,但不参与领导者选举,也不在事务日志中投票,通常用于在不影响写吞吐的情况下增加读取速度。
数据的复制和一致性
一旦选举出领导者,领导者就会开始处理事务请求。为了保持数据的一致性,ZooKeeper使用一种原子广播协议(Zab)来同步所有节点上的更新。当领导者收到一个修改请求:
- 领导者分配一个单调递增的事务ID,并将该请求作为提案发送给所有的跟随者。
- 跟随者将提案记录到磁盘(事务日志),然后向领导者发送"接受"(ack)响应。
- 当领导者接收到超过半数服务器的响应时,就会提交该提案,并通知跟随者此提案已被提交。
这种协议确保了集群的每个节点最终都将拥有相同的数据拷贝。
容错机制
如果领导者服务器发生故障,剩余的节点会重新进行领导者选举。通过该机制,ZooKeeper集群能够在部分节点失效的情况下继续有效运行。对于集群中跟随者节点的绝大多数失败(例如,由于网络分区),领导者将丢弃所有更改直到超过半数的节点恢复正常,以确保集群数据的一致性不会遭到破坏。
观察和通知
客户端可以向ZooKeeper注册对特定znode的观察(watch),如果注册的znode发生更改(数据更改、节点创建或删除),ZooKeeper将通知所有对此znode有观察的客户端,允许客户端对变化作出响应。
选举流程
1. 自增选举轮次。Zookeeper规定所有有效的投票都必须在同一轮次中,在开始新一轮投票时,会首先对logicalclock进行自增操作。
2. 初始化选票。在开始进行新一轮投票之前,每个服务器都会初始化自身的选票,并且在初始化阶段,每台服务器都会将自己推举为Leader。
3. 发送初始化选票。完成选票的初始化后,服务器就会发起第一次投票。Zookeeper会将刚刚初始化好的选票放入sendqueue中,由发送器WorkerSender负责发送出去。
4. 接收外部投票。每台服务器会不断地从recvqueue队列中获取外部选票。如果服务器发现无法获取到任何外部投票,那么就会立即确认自己是否和集群中其他服务器保持着有效的连接,如果没有连接,则马上建立连接,如果已经建立了连接,则再次发送自己当前的内部投票。
5. 判断选举轮次。在发送完初始化选票之后,接着开始处理外部投票。在处理外部投票时,会根据选举轮次来进行不同的处理。
外部投票的选举轮次大于内部投票。若服务器自身的选举轮次落后于该外部投票对应服务器的选举轮次,那么就会立即更新自己的选举轮次(logicalclock),并且清空所有已经收到的投票,然后使用初始化的投票来进行PK以确定是否变更内部投票。最终再将内部投票发送出去。
外部投票的选举轮次小于内部投票。若服务器接收的外选票的选举轮次落后于自身的选举轮次,那么Zookeeper就会直接忽略该外部投票,不做任何处理,并返回步骤4。
外部投票的选举轮次等于内部投票。此时可以开始进行选票PK。
6. 选票PK。在进行选票PK时,符合任意一个条件就需要变更投票。
若外部投票中推举的Leader服务器的选举轮次大于内部投票,那么需要变更投票。
若选举轮次一致,那么就对比两者的ZXID,若外部投票的ZXID大,那么需要变更投票。
若两者的ZXID一致,那么就对比两者的SID,若外部投票的SID大,那么就需要变更投票。
7. 变更投票。经过PK后,若确定了外部投票优于内部投票,那么就变更投票,即使用外部投票的选票信息来覆盖内部投票,变更完成后,再次将这个变更后的内部投票发送出去。
8. 选票归档。无论是否变更了投票,都会将刚刚收到的那份外部投票放入选票集合recvset中进行归档。recvset用于记录当前服务器在本轮次的Leader选举中收到的所有外部投票(按照服务队的SID区别,如{(1, vote1), (2, vote2)…})。
9. 统计投票。完成选票归档后,就可以开始统计投票,统计投票是为了统计集群中是否已经有过半的服务器认可了当前的内部投票,如果确定已经有过半服务器认可了该投票,则终止投票。否则返回步骤4。
10. 更新服务器状态。若已经确定可以终止投票,那么就开始更新服务器状态,服务器首选判断当前被过半服务器认可的投票所对应的Leader服务器是否是自己,若是自己,则将自己的服务器状态更新为LEADING,若不是,则根据具体情况来确定自己是FOLLOWING或是OBSERVING。
以上10个步骤就是FastLeaderElection的核心,其中步骤4-9会经过几轮循环,直到有Leader选举产生。
脑裂
Zookeeper脑裂是指由于网络分区导致Zookeeper集群中的不同部分相互认为自己是主节点,从而产生数据不一致和可用性问题。
解决Zookeeper脑裂问题主要有以下几种方式:
Quorums机制(超过半数): 如果出现多个分区,则每个分区不满足超过总数的一半条件,所以要么只有一个leader,要么选举失败,这种方式是保持数据一致性,舍弃可用性的一种实现。
Weight机制(加权机制): 通过Quorums机制只有超过半数才能提供服务,这样如果集群很大,出现分区则无法使用,故可以给节点设置权重,一些节点权重较高,这样计算出来超过一定权重值则也可以选举leader。这种方式由于机器的权重不一样,故某些分区也是可以选举leader成功的,故可以继续提供服务,即保留可用性。所以可用性方面相对于Quorums机制是有提高的。
Fencing机制(共享资源加锁机制): 集群节点可以看到共享资源说明在集群中,可以获取共享资源的锁则成为leader。这种方式偏向于可用性,数据一致性较弱。
应用场景
- 配置管理:可以作为集中式配置服务器,应用程序可以将配置信息存储在ZooKeeper中,当配置发生变化时,ZooKeeper 会通知所有依赖于该配置的应用程序,使得它们能够实时地更新自己的配置。
- 命名服务:可以用于为分布式应用中的组件提供命名服务,类似于DNS在互联网上的作用,但更侧重于分布式环境下的服务发现和管理。
- 分布式协调与同步:提供了原子广播(Atomic Broadcast)机制,允许一个节点向其他所有节点发送消息,并确保所有节点按顺序接收到相同的消息,对于需要进行状态同步的分布式应用非常有用。
- 集群管理:能够帮助管理分布式集群的节点,包括监控节点状态变化、选举主节点、维护集群视图等。
- 分布式锁: 提供了一种创建临时有序节点的机制,可以用于实现分布式锁,保证多个节点之间的互斥访问。
- 队列管理:可以用来实现两种类型的队列:FIFO队列和固定队列。FIFO队列是先进先出的队列,而固定队列则是所有节点都必须到达后才开始处理的队列。
- 数据发布/订阅系统:可以作为一个发布/订阅系统的基础,数据发布者可以将数据存储在 ZooKeeper 中,而订阅者可以监听这些数据的变化并作出相应的反应。
- 负载均衡:可以用于实现基于客户端的负载均衡,客户端可以从ZooKeeper获取可用的服务列表,并根据这些信息选择合适的服务实例。
- 故障恢复:可以帮助检测和响应集群中的故障,如节点宕机或网络分区,从而自动进行服务恢复或重新路由请求。
- 事务协调: 在需要跨多个节点执行原子操作的情况下,可以作为协调者,确保所有节点上的操作要么全部成功,要么全部失败。