ZooKeeper是怎么工作的,原理是什么?
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,它是集群的必备元素,可以提供分布式锁、配置管理、分布式通知等服务。
ZooKeeper的工作原理主要基于以下几个方面:
-
数据模型:ZooKeeper的数据模型类似于一个分层的文件系统,类似于一个多叉树,也被称为ZNode树。每个节点称为一个znode,每一个ZNode都可以有子节点,每个节点都可以存储数据,。每个znode在ZooKeeper中都可以通过全路径名进行唯一标识。
-
会话:客户端和ZooKeeper之间的交互是基于会话的。客户端启动时,会与ZooKeeper建立一个TCP连接,这个连接就是一个会话。会话有超时机制,如果在超时时间内,ZooKeeper没有收到客户端的心跳,那么会话就会被关闭。
-
Watcher机制:客户端可以在一个znode上注册一个watcher,当这个znode的数据发生变化时,ZooKeeper会通知到客户端。这种机制可以让客户端及时地获取到数据的变化。
-
一致性:ZooKeeper保证了在整个集群中,客户端无论连接到哪个ZooKeeper服务器,看到的数据都是一致的。这是通过ZooKeeper的Zab协议来实现的,Zab协议是一个基于主从模式的一致性协议,主要用于在主节点崩溃后,从从节点中选举出新的主节点,并且在主节点崩溃前后,保证所有已经提交的请求都能被正确处理。
-
原子性:ZooKeeper的所有操作都是原子性的,要么全部成功,要么全部失败。这保证了即使在面临网络分区、服务器崩溃等故障时,ZooKeeper的数据也能保持一致性。
-
顺序性:ZooKeeper保证了来自同一个客户端的请求,会按照请求的顺序来执行。这对于需要依赖请求顺序的应用来说,是非常重要的。
总的来说,ZooKeeper通过数据模型、会话、Watcher机制、一致性、原子性和顺序性等机制,为分布式应用提供了一个高可用、高性能、可靠的协调服务。
Zab协议
Zab协议全称为ZooKeeper Atomic Broadcast协议,是ZooKeeper为了保证分布式数据一致性而设计的一种协议。Zab协议主要用于在所有ZooKeeper服务器之间复制内存中的数据树和事务日志,保证了在整个ZooKeeper集群中,所有的写操作都是线性一致的。
Zab协议主要包括两种模式:崩溃恢复模式和消息广播模式。
崩溃恢复模式
当ZooKeeper集群启动,或者Leader服务器崩溃后,Zab协议会进入崩溃恢复模式。在这个模式下,Zab协议会从Follower服务器中选举出新的Leader服务器,并且在Leader服务器崩溃前后,保证所有已经提交的请求都能被正确处理。
在Zab协议的崩溃恢复模式下,为了选举出新的Leader,同时保证新的Leader拥有最新的数据,Zab协议采用了一个叫做"Leader选举+日志同步"的机制。
- Leader选举: Zab协议的Leader选举过程是基于投票的("过半机制")。每个ZooKeeper服务器都会发起一轮投票,投票包含自己的服务器ID和最后一条日志的ZXID(ZooKeeper事务ID)。每个服务器都会选择ZXID最大的服务器作为Leader,如果ZXID相同,则选择服务器ID最大的。这样可以保证新的Leader至少拥有和其他服务器一样新的数据。
谁可以监控到leader崩溃了,谁触发了选举动作?
在ZooKeeper集群中,所有的Follower服务器都会定期向Leader服务器发送心跳,以确认Leader服务器是否还在运行。如果在一定时间内,Follower服务器没有收到Leader服务器的心跳回应,那么Follower服务器就会认为Leader服务器已经崩溃。 当Follower服务器检测到Leader服务器崩溃后,它会触发新一轮的Leader选举。在这个选举过程中,所有的服务器都会参与,每个服务器都会发起一轮投票,投票包含自己的服务器ID和最后一条日志的ZXID(ZooKeeper事务ID)。每个服务器都会选择ZXID最大的服务器作为Leader,如果ZXID相同,则选择服务器ID最大的。 这个过程会进行多轮,直到所有的服务器的投票都一致,选举出一个新的Leader。新的Leader在被选举出来后,会向所有的Follower服务器发送新的心跳,以确认自己已经成为新的Leader。 所以,是Follower服务器监控到Leader服务器崩溃,并触发了新一轮的Leader选举。
leader选举过程是谁主导执行的?在ZooKeeper的Zab协议中,Leader选举过程是由集群中的所有服务器共同参与的,而不是由某一个特定的服务器主导执行的。 当ZooKeeper集群启动,或者当前的Leader服务器崩溃后,所有的服务器都会进入到Leader选举过程。每个服务器都会发起一轮投票,投票包含自己的服务器ID和最后一条日志的ZXID(ZooKeeper事务ID)。每个服务器都会选择ZXID最大的服务器作为Leader,如果ZXID相同,则选择服务器ID最大的。 在投票过程中,每个服务器既是投票者,也是被投票者。每个服务器在收到其他服务器的投票后,会将自己的投票和收到的投票进行比较,如果收到的投票更优(即ZXID更大,或者ZXID相同但服务器ID更大),那么服务器会更新自己的投票。这个过程会进行多轮,直到所有的服务器的投票都一致,选举出一个新的Leader。 所以,ZooKeeper的Leader选举过程是一个分布式的过程,由集群中的所有服务器共同参与,而不是由某一个特定的服务器主导执行的。
过半机制具体来说,一个ZooKeeper服务器要成为Leader,需要得到超过半数的服务器(包括自己)的投票。这个机制可以保证在整个ZooKeeper集群中,只能选举出一个Leader。 这个"过半机制"是为了保证ZooKeeper集群的数据一致性和可用性。因为只有当超过半数的服务器同意时,一个请求才能被提交,这样可以保证即使有部分服务器崩溃或者网络分区,只要有超过半数的服务器还在运行,ZooKeeper集群就能继续提供服务。同时,这个机制也保证了在整个集群中,只能有一个Leader,避免了数据的不一致。 需要注意的是,这个"过半机制"也决定了ZooKeeper集群的容错能力。一个有N个服务器的ZooKeeper集群,最多可以容忍N/2(向下取整)个服务器崩溃。所以在实际部署时,通常会选择奇数个服务器,例如3个、5个、7个等,以提高集群的容错能力。
- 日志同步: Leader选举结束后,新的Leader会和其他的Follower进行日志同步。Leader会将自己的日志中比Follower新的部分发送给Follower,Follower在接收到这些日志后,会将这些日志追加到自己的日志中。这样可以保证所有的Follower都拥有和Leader一样新的数据。
通过这个"Leader选举+日志同步"的机制,Zab协议在崩溃恢复模式下,可以保证新的Leader拥有最新的数据,同时也可以保证所有的Follower都能更新到最新的数据。
消息广播模式
当ZooKeeper集群中的Leader服务器被选举出来后,Zab协议会进入消息广播模式。在这个模式下,Leader服务器会将所有的写操作(即状态变更请求)以事务提案的形式发送给所有的Follower服务器。Follower服务器在接收到事务提案后,会将其写入本地日志,并向Leader服务器发送ACK。当Leader服务器收到了超过半数的Follower服务器的ACK后,就会认为这个事务提案已经被接受,然后向所有的Follower服务器发送COMMIT命令,通知它们提交这个事务。
在Zab协议中,Leader服务器在收到超过半数的Follower服务器的ACK后,会认为事务提案已经被接受,然后向所有的Follower服务器发送COMMIT命令。
对于没有发送ACK的Follower服务器,也就是说它们还没有接收到事务提案,或者还没有来得及处理事务提案,它们在接收到COMMIT命令后,会直接忽略这个命令,因为它们没有对应的事务提案可以提交。
然后,这些Follower服务器会继续从Leader服务器接收新的事务提案。如果它们接收到的新的事务提案的ZXID(ZooKeeper事务ID)比它们期望的ZXID大,那么它们就知道它们错过了一些事务提案。这时,它们会向Leader服务器发送一个SNAPSHOT请求,请求Leader服务器发送一个数据快照,以及从这个数据快照之后的所有事务提案。这样,这些Follower服务器就可以通过这个数据快照和事务提案,来更新它们的状态,使之与Leader服务器保持一致。
所以,即使有一部分Follower服务器没有发送ACK,也没有影响,因为它们最终还是会通过SNAPSHOT请求,来获取到错过的事务提案,并更新它们的状态。这就保证了所有的Follower服务器最终都能提交成功。
Zab协议通过这两种模式,保证了ZooKeeper集群中的数据一致性和高可用性。
ZooKeeper应用
分布式锁
ZooKeeper可以用于实现分布式锁,主要是利用了ZooKeeper的数据模型和一致性保证。具体来说,当一个客户端想要获取锁时,它会在ZooKeeper的一个指定节点(例如/lock)下创建一个临时顺序节点,然后查看自己创建的节点是否是当前所有子节点中序号最小的,如果是,则获取锁成功;如果不是,则监听比自己序号小的那个节点,当那个节点被删除时,再次尝试获取锁。这样就保证了同一时刻只有一个客户端能获取到锁。
在ZooKeeper中,每个节点被称为一个znode,znode有几种类型,包括持久节点、临时节点、持久顺序节点和临时顺序节点。
- 持久节点:创建后在客户端断开连接后依然存在。
- 临时节点:创建后如果客户端断开连接,那么这个节点就会被自动删除。
- 持久顺序节点:创建后会在节点后面自动添加一个递增的数字,即使客户端断开连接,节点依然存在。
- 临时顺序节点:创建后会在节点后面自动添加一个递增的数字,如果客户端断开连接,那么这个节点就会被自动删除。
在实现分布式锁时,客户端会在一个指定的节点(例如/lock)下创建一个临时顺序节点。例如,第一个客户端创建的节点可能是/lock/0000000001,第二个客户端创建的节点可能是/lock/0000000002,以此类推。这个节点是临时的,也就是说,如果客户端断开连接,那么这个节点就会被自动删除。同时,这个节点是顺序的,也就是说,每个客户端创建的节点的序号都是递增的,这样就可以通过序号来确定哪个客户端应该获得锁。
在ZooKeeper中,客户端与服务器之间的连接可能因为以下几种情况断开:
客户端主动关闭连接:客户端在完成所有操作后,可以主动调用API来关闭与ZooKeeper服务器的连接。
网络问题:如果客户端与ZooKeeper服务器之间的网络出现问题,例如网络断开、延迟过高等,那么连接可能会被断开。
ZooKeeper服务器故障:如果ZooKeeper服务器出现故障,例如服务器崩溃、重启等,那么与客户端的连接也会被断开。
Session超时:ZooKeeper客户端在与服务器建立连接时,会设置一个Session超时时间。如果在这个时间内,客户端没有发送任何请求到服务器,那么服务器就会认为这个Session已经超时,然后关闭与客户端的连接。
在使用ZooKeeper实现分布式锁时,如果客户端在获取到锁,完成操作后,可以主动关闭与ZooKeeper的连接,这样对应的临时节点就会被删除,其他等待获取锁的客户端就可以获取到锁。如果客户端在还没有完成操作时,就与ZooKeeper服务器的连接被断开(例如因为网络问题或者服务器故障),那么对应的临时节点也会被删除,这样可以避免因为客户端故障导致的锁无法被释放的问题。
通过监听(Watcher)机制来实现重试: 具体来说,当一个客户端尝试获取锁(即在ZooKeeper的指定节点下创建临时顺序节点)失败时(因为已经有其他客户端获取到了锁),它会找到比自己创建的节点序号小的那个节点,然后对那个节点注册一个Watcher。 当持有锁的客户端完成操作,释放锁(即关闭与ZooKeeper的连接,删除对应的临时节点)时,ZooKeeper会通知到注册了Watcher的客户端,这个客户端就知道可以再次尝试获取锁了。 这个监听机制是ZooKeeper的一个重要特性,它可以避免客户端通过轮询来检查锁状态,从而减少了网络流量和服务器的负载。
InterProcessMutex
的acquire
方法有两个版本,一个是可以设置超时的,一个是无限等待直到获取到锁。
acquire(long time, TimeUnit unit)
:这个版本的acquire
方法会尝试获取锁,如果在指定的时间内没有获取到锁,那么就会返回false。这个方法是可以设置超时的,也就是说,它会在尝试获取锁时阻塞,但是最多阻塞指定的时间。acquire()
:这个版本的acquire
方法会尝试获取锁,如果没有获取到锁,那么就会一直等待,直到获取到锁。这个方法是无限等待的,也就是说,它会在尝试获取锁时阻塞,直到获取到锁。所以,如果你不希望
acquire
方法阻塞,你可以使用可以设置超时的版本,然后设置一个合适的超时时间。例如:
javaInterProcessMutex lock = new InterProcessMutex(client, "/lock_path"); if (!lock.acquire(10, TimeUnit.SECONDS)) { // 在10秒内没有获取到锁,返回失败或者执行其他操作 return; }
在这个示例中,如果在10秒内没有获取到锁,
acquire
方法就会返回false,然后你可以选择返回失败,或者执行其他的操作。
配置中心
ZooKeeper可以用于实现分布式配置中心,主要是利用了ZooKeeper的数据模型和Watcher机制。具体来说,可以将配置信息存储在ZooKeeper的节点中,当需要修改配置时,只需要更新对应的节点。客户端在启动时,会读取这些配置信息,并注册Watcher,当配置信息发生变化时,ZooKeeper会通知到客户端,客户端再重新读取配置信息。这样就可以实现配置的动态更新。
Kafka中的Controller选举
Kafka中的Controller选举也是利用了ZooKeeper的数据模型和一致性保证。具体来说,当Kafka集群启动时,所有的Broker都会尝试去创建一个临时节点(例如/controller),由于ZooKeeper保证了创建节点的原子性,所以只有一个Broker能创建成功,创建成功的Broker就成为Controller。当Controller崩溃时,对应的临时节点会被删除,其他的Broker检测到节点被删除后,会再次尝试创建节点,从而选举出新的Controller。
zk的分布式锁和redis的分布式锁有什么区别?
ZooKeeper和Redis都可以用来实现分布式锁,但是它们的实现方式和特性有一些不同:
-
实现方式:
- ZooKeeper的分布式锁是基于其临时顺序节点和Watcher机制实现的。客户端在尝试获取锁时,会在ZooKeeper的指定节点下创建一个临时顺序节点,然后检查自己创建的节点是否是所有子节点中序号最小的,如果是,那么就获取到了锁;如果不是,那么就找到比自己小的那个节点,对其注册Watcher,然后等待锁被释放。
- Redis的分布式锁是基于其SETNX(Set if Not eXists)命令和过期时间实现的。客户端在尝试获取锁时,会使用SETNX命令尝试设置一个键,如果设置成功,那么就获取到了锁;如果设置失败(因为键已经存在),那么就等待一段时间后再次尝试。
-
容错性:
- ZooKeeper的分布式锁具有较好的容错性。因为锁是基于ZooKeeper的临时节点实现的,所以当持有锁的客户端发生故障时,其创建的临时节点会被自动删除,锁就会被释放,其他等待获取锁的客户端就有机会获取到锁。
- Redis的分布式锁的容错性较差。如果持有锁的客户端发生故障,没有来得及删除键,那么其他客户端需要等待到键过期后才能获取到锁。如果设置的过期时间过长,那么就可能导致锁被长时间占用,影响系统的性能。
-
性能:
- ZooKeeper的性能较低,因为它需要在磁盘上持久化所有的数据,而且其所有的写操作(包括创建、删除节点等)都需要在集群中的所有服务器上达成一致,这会消耗较多的网络和磁盘IO。
- Redis的性能较高,因为它是基于内存的,其所有的操作都是在内存中完成的,所以速度非常快。但是,如果Redis服务器发生故障,那么可能会导致锁的信息丢失。
-
适用场景:
- ZooKeeper更适合用于需要严格的顺序一致性和容错性的场景,例如Leader选举、配置管理等。
- Redis更适合用于需要高性能和简单的场景,例如缓存、消息队列等。