当前位置: 首页 > 科技观察

深入理解Zookeeper核心原理

时间:2023-03-20 20:14:49 科技观察

上一篇文章Zookeeper基本原理&详细应用场景详细介绍了Zookeeper的基本原理及其应用场景。虽然介绍了它的底层存储原理以及如何使用Zookeeper来实现分布式锁。但是我认为这只是对Zookeeper的一点肤浅的认识。所以这篇文章就给大家详细讲讲Zookeeper的核心底层原理。对Zookeeper不熟悉的可以回去看看。ZNode应该算是Zookeeper的基础,数据存储的最小单位。在Zookeeper中,类似文件系统的存储结构被Zookeeper抽象成一棵树,树中的每个节点(Node)称为一个ZNode。ZNode中维护了一个数据结构,用于记录ZNode中数据变化的版本号和ACL(AccessControlList)的变化。有了这些数据的版本号及其更新后的Timestamp,Zookeeper就可以验证客户端请求的缓存是否合法,并协调更新。而且Zookeeper客户端在进行更新或删除操作时,必须带上对应数据的版本号进行修改。如果Zookeeper检测到对应的版本号不存在,则不会进行更新。如果合法,ZNode中的数据更新后,其对应的版本号也会一起更新。这个版本号的逻辑其实很多框架都会用到。比如在RocketMQ中,Broker向NameServer注册时,也会带上这样一个版本号,叫做DateVersion。接下来,我们详细看一下维护版本号相关数据的数据结构。它叫做StatStructure,它的字段有:比如通过stat命令,我们可以查看一个ZNode中StatStructure的具体值。这里的epoch和zxid是和Zookeeper集群相关的,后面会详细介绍。ACLACL(AccessControlList)用于控制ZNode的相关权限,其权限控制与Linux中类似。Linux中有三种权限,分别是read、write、execute,对应的字母分别是r、w、x。权限粒度也分为所有者权限、组权限、其他组权限三种。例如:drwxr-xr-x3USERNAMEGROUP1.0K31518:19dir_name什么是粒度?粒度是权限作用的对象的分类也就是说,以上三个粒度的描述是对用户(Owner)、用户所属组(Group)、其他组(Other)的权限划分.这应该算是权限控制的一个标准。典型的三级。Zookeeper虽然也是三阶段的,但是两者在粒度的划分上是有区别的。Zookeeper中的三阶段格式为Scheme、ID、Permissions,分别表示权限机制、允许访问的用户、具体的权限。Scheme代表一种权限模式,有以下5种:world在Scheme下,ID只能是anyone,表示所有人都可以访问auth,代表经过认证的用户。测试。ip只允许某些特定的IP访问ZNodeX509通过客户端的证书进行认证同时有五种权限:CREATE创建一个节点READ获取一个节点或者列出其子节点WRITE可以设置节点数据DELETE可以删除子节点ADMIN可以设置权限和linux一样,这个权限也有缩写。例如:getAcl方法可以让用户查看对应ZNode的权限。如图所示,我们可以分三段输出结果。它们是:该方案使用worldid值作为anyone,这意味着所有用户都有权限。具体权限为cdrwa,分别是CREATE、DELETE、READ、WRITE、ADMIN的缩写。Session机制了解了Zookeeper的Version机制后,我们可以继续探索Zookeeper的Session机制。我们知道Zookeeper中有四种节点,分别是持久节点、持久时序节点、临时节点和临时时序节点。我们在上一篇文章中讲到,如果客户端创建了一个临时节点,然后断开连接,那么所有的临时节点都会被删除。事实上,断开连接这个词并不十分准确。应该是指客户端建立连接时session过期后,其创建的所有临时节点都会被删除。那么Zookeeper是如何知道当前客户端创建了哪些临时节点的呢?答案就是上面提到的StatStructure中的**ephemeralOwner(临时节点的Owner)**字段,如果当前是临时顺序节点,那么ephemeralOwner会存储你需要知道创建的Owner的SessionID节点。有了SessionID,自然就可以匹配到对应的client了。当Session过期后,可以删除客户端创建的所有临时节点。例如,当相应的服务创建连接时,它必须提供一个字符串,其中所有服务器和端口以逗号分隔。127.0.0.1:3000:2181,127.0.0.1:2888,127.0.0.1:3888Zookeeper客户端收到这个字符串后会随机选择一个服务和端口建立连接。如果之后连接断开,客户端会从字符串中选择下一个服务器,继续尝试连接,直到连接成功。除了这个最基本的IP+端口,Zookeeper3.2.0以后的版本还支持连接字符串中的路径,例如。127.0.0.1:3000:2181,127.0.0.1:2888,127.0.0.1:3888/app/a这样/app/a会被认为是当前服务的根目录,在其下创建的所有节点路径将以/app/a为前缀。例如,如果我创建一个节点/node_name,它的完整路径将是/app/a/node_name。此功能特别适用于多租户环境。对于每个租户来说,它认为自己是顶级根目录/。当Zookeeper的客户端和服务器连接时,客户端会得到一个64位的SessionID和密码。这个密码是做什么用的?我们知道Zookeeper可以部署多个实例。如果客户端断开连接并与另一个Zookeeper服务器建立连接,则在建立连接时会带上密码。密码是Zookeeper的一种安全措施,所有Zookeeper节点都可以验证它。这样即使连接到其他Zookeeper节点,Session也是有效的。Session过期有两种情况,即:客户端在指定的过期时间后没有在指定的时间内发送心跳。第一种情况,过期时间会在Zookeeper客户端建立连接时发送给服务器。过期时间范围目前只能在2倍tickTime到20倍tickTime之间。Ticktime是Zookeeper服务端的一个配置项,用于指定客户端向服务端发送心跳的时间间隔。默认值为tickTime=2000,单位为毫秒。这套Session的过期逻辑由Zookeeper服务器维护。一旦Session过期,服务器将立即删除所有由客户端创建的临时节点,然后将更改通知所有监听这些节点的客户端。对于第二种情况,Zookeeper中的心跳是通过PING请求实现的。每隔一段时间,客户端就会向服务器发送PING请求,这就是心跳的本质。心跳让服务器感知到客户端还活着,客户端也感知到与服务器的连接仍然有效。这个间隔是**tickTime**,默认是2秒。Watch机制了解了ZNode和Session之后,我们终于可以进入下一个关键函数Watch了。**Monitor(手表)**这个词在上面的内容中不止一次被提及。首先,用一句话总结一下它的作用。为某个节点注册监听器。一旦节点发生变化(如更新或删除),监听器将收到一个WatchEvent。就像ZNode有很多种一样,Watch也有很多种。类型有一次性手表和永久手表。一次性Watch触发后,Watch会被移除。permanentWatch被触发后,仍然会被保留,可以继续监听ZNode上的变化。它是Zookeeper3.6.0中的一个新功能。一次性Watch可以调用GetData()、getChildren()和exists()等方法在参数中设置,永久Watch需要调用addWatch()来实现。而且一次性的Watch也会有问题,因为Watch触发的事件到达客户端,然后在客户端再建立一个新的Watch,是有一个时间间隔的。但是,如果在此时间间隔内发生变化,则客户端无法察觉。Zookeeper集群架构ZAB协议为前面的铺垫完毕后,大家可以从整体架构的角度进一步了解Zookeeper。为了保证其高可用性,Zookeeper采用了基于主从的读写分离架构。我们知道在类似Redis的主从架构中,节点使用Gossip协议进行通信,那么Zookeeper中的通信协议是什么?答案是**ZAB(ZookeeperAtomicBroadcast)**协议。ZAB协议是一种支持崩溃恢复的原子广播协议,用于在Zookeeper之间传递消息以保持所有节点同步。ZAB还具有高性能、高可用、易用、易维护等特点,支持故障自动恢复。ZAB协议将Zookeeper集群中的节点分为三个角色,分别是Leader、Follower和Observer,如下图所示:总的来说,这种架构类似于Redis主从或者MySQL主从的架构(有兴趣也可以看看之前写的文章,里面都有讲到)Redis主从和MySQL主从的区别在于通常的主从架构中有两个角色,分别是Leader,Follower(或者Master,Slave),但是Zookeeper多了一个Observer。问题是,Observer和Follower有什么区别?本质上两者的功能是一样的,都为Zookeeper提供了横向扩展的能力,使其能够处理更多的并发。但不同的是,在Leader选举过程中,Observer不参与投票。顺序一致性上面说了Zookeeper集群是读写分离的。只有Leader节点可以处理写请求。如果Follower节点收到写入请求,会将请求转发给Leader节点进行处理。Follower节点本身不会处理写请求。要求。Leader节点收到消息后,会按照请求的严格顺序,一一处理。这是Zookeeper的一大特点,它保证了消息的顺序一致性。比如消息A先于消息B到达,那么在所有Zookeeper节点中,消息A都会先于消息B到达,Zookeeper会保证消息的全局顺序。Zookeeper如何保证zxid的消息顺序?答案是通过zxid。zxid可以简单理解为一条消息在Zookeeper中的唯一ID。节点之间会通过发送**Proposal(交易提案)**来进行通信和同步数据,提案中会包含zxid和具体的数据(Message)。zxid由两部分组成:epoch可以理解为一个王朝,或者Leader的迭代版本。每个Leader的时代都不一样。每个Leader使用的epoch是唯一的,不同的消息在同一个epoch有不同的counter值,这样所有的proposal在Zookeeper集群中都有唯一的zxid。在恢复模式下正常运行的Zookeeper集群将处于广播模式。相反,如果超过一半的节点宕机,它就会进入恢复模式。恢复模式是什么?在Zookeeper集群中,有两种模式,分别是:恢复模式和广播模式。当Zookeeper集群出现故障时,会进入恢复模式,也叫LeaderActivation。顾名思义,就是在这个阶段选举出Leader。节点会生成zxid和Proposal,然后互相投票。Therearetwomainprinciplesforvoting:ThezxidoftheelectedLeadermustbethelargestamongallFollowersandmorethanhalfoftheFollowershavereturnedACK,whichmeansthattheelectedLeaderisrecognized.不正常,Zookeeper会直接进行新一轮的选举。如果一切顺利,Leader选举成功,但是此时集群无法正常对外提供服务,因为新的Leader和Follower之间的关键数据同步还没有进行。之后,Leader会等待其余的Follower连接上,然后将丢失的数据通过Proposal发送给所有的Follower。至于怎么知道丢失了哪些数据,Proposal本身需要记录日志。通过Proposal中zxid的低32位Counter中的值,可以进行Diff。当然,这里有一个优化。如果缺失数据太多,那么一个一个发送Proposal的效率就太低了。因此,如果Leader发现缺失数据过多,就会对当前数据进行快照,并直接发送给Follower。TheEpochofthenewlyelectedLeaderwillbe+1totheoriginalvalue,andtheCounterwillberesetto0.你认为它就在这里吗?事实上,仍然无法正常提供服务。数据同步完成后,Leader会向Follower发送一个NEW_LEADERProposal。当且仅当提案被半数以上的FollowersAcked时,Leader才会提交NEW_LEADER提案,集群才能正常工作。至此,恢复模式结束,集群进入广播模式。广播模式广播模式下,Leader收到消息后,会向所有其他Follower发送一个Proposal(事务提议),Follower收到Proposal后会返回一个ACK给Leader。当Leader收到quorumsACK时,当前的Proposal将被提交并应用到节点的内存中。法定人数是多少?Zookeeper官方建议每两个Zookeeper节点至少有一个需要返回ACK。假设有N个Zookeeper节点,计算公式应该是n/2+1,这样可能不是很直观。通俗地说,如果超过半数的Follower返回ACK,就可以提交Proposal并应用到内存中的ZNode。Zookeeper使用2PC来保证节点间的数据一致性(如上图所示),但是由于Leader需要和所有Follower进行交互,所以通信开销会变大,Zookeeper的性能也会下降。因此,为了提高Zookeeper的性能,只需要将所有Follower节点的ACK都返回给一半以上的Follower返回ACK即可。