一、概述
Zookeeper是一个开源的分布式的,为分布式应用提供协调服务的Apache项目。Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致服务的软件。
虽然可以用zk实现很多功能,但是实际上zk只提供了三个东西:文件系统、通知机制、集群管理机制。
zk的存储的数据的结构,类似于一个文件系统,结构如下:
图1.1 zk文件系统
每个节点称为znode,每个znode都是一个类似于KV的结构,每个节点名称相当于key,每个节点中都保存了对应的数据,类似于Key对应的value。每个znode下面都可以有多个子节点,就这样一直延续下去,构成了类似于Linux文件系统的架构。
当某个client监听某个节点时(watch机制,后面有讲),当该节点发生变化时(有可能是增加子节点,或者节点值变了等),zk就会通知监听该节点的客户端。后续该怎么处理就看客户端的处理逻辑了。
zk本身是一个集群结构,有一个leader节点,负责写请求,多个follower负责响应读请求。并且在leader节点故障时,会自动根据选举机制从剩下的follower中选出新的leader。
1)Zookeeper:一个领导者(leader),多个跟随者(follower)组成的集群。 2)Leader负责进行投票的发起和决议,更新系统状态。 3)Follower用于接收客户请求并向客户端返回结果,在选举Leader过程中参与投票。 4)集群中奇数台服务器只要有半数以上节点存活,Zookeeper集群就能正常服务。 5)全局数据一致:每个server保存一份相同的数据副本,client无论连接到哪个server,数据都是一致的。 6)更新请求顺序进行,来自同一个client的更新请求按其发送顺序依次执行。 7)数据更新原子姓,一次数据更新要么成功,要么失败。 8)实时姓,在一定时间范围内,client能读到最新数据。
leader: 1.恢复数据; 2.维持与Learner的心跳,接收Learner请求并判断Learner的请求消息类型; 3.Learner的消息类型主要有PING消息、REQUEST消息、ACK消息、REVALIDATE消息,根据不同的消息类型,进行不同的处理。 PING消息是指Learner的心跳信息;REQUEST消息是Follower发送的提议信息,包括写请求及同步请求;ACK消息是Follower的对提议的回复,超过半数的Follower通过,则commit该提议;REVALIDATE消息是用来延长SESSION有效时间。
follower: 1)向Leader发送请求(PING消息、REQUEST消息、ACK消息、REVALIDATE消息);| 2)接收Leader消息并进行处理; 3)接收Client的请求,如果为写请求,发送给Leader进行处理; 4)返回Client结果。
Follower的消息循环处理如下几种来自Leader的消息: 1)PING消息:心跳消息 2)PROPOSAL消息:Leader发起的提案,要求Follower投票 3)COMMIT消息:服务器端最新一次提案的信息 4)UPTODATE消息:表明同步完成 5)REVALIDATE消息:根据Leader的REVALIDATE结果,关闭待revalidate的session还是允许其接受消息 6)SYNC消息:返回SYNC结果到客户端,这个消息最初由客户端发起,用来强制得到最新的更新。
observer:和follower类似,但是不参与投票和选举 learner:follower和observer的统称
短暂节点--ephemeral: 客户端和服务器端断开连接后,创建的节点自己删除。并且在还未断开的过程中,这个临时的节点对其他客户端来说都是可见的。其中也可以分为普通的短暂节点和带序号的短暂节点。带序号的短暂节点称为ephemeral_sequential,就是会在节点的名称最后加上一串序号,标明顺序
持久节点--persistent: 创建的节点会永久存在,即使客户端和服务器端断开连接。也分为普通和带序号,区别和上面类似
首先先说几个相关概念: zxid: 每次修改zk中的数据以及zk的状态的变化(比如发生过选举等),就会有一个zxid数字串,每次随着事务的增加,会逐渐递增。所以zxid小的事务肯定最先发生的。
myid(sid): 每个zk节点服务器编号,在配置文件中指定的,必须保证全局唯一。
zk的几个状态: looking--表示正在搜寻leader leading--当前正在选举leader following--leader选举出来之后,正在新的leader和follower之间同步数据 observing:observer正在接受选举结果
下面正式讲选举过程: leader的选举基于paxos算法实现,这里不细究算法原理,就简单讲讲选举的过程。 (1)第一轮投票:所有活的zk节点都会将票投给自己,并将选票结果广播给其他节点。因为这个时候也不知道其他节点的情况,选票上有两个关键信息:当前节点最新的zxid以及sid。zxid越大,表示该节点所拥有的数据越新,所以zxid大的节点是会被首先选举成leader的;如果zxid相同,则比较sid,sid大的选举为leader。且因为sid是全局唯一的,所以根据这个原则,一定可以选出唯一的leader。 (2)收到其他节点的投票后,获取选票中的zxid以及sid,根据(1)的比较原则,选择最新的,然后更新选票,再把新的选票发送出去,并将选票标记为“第二轮选票”。我们要注意,因为每个节点收到其他节点的选票的时间点一般是不一致的,不同的节点会有延迟,所以会导致有不同选举轮次的选票出现。所以当节点发现自己的收到的选票比自己当前的投出的选票轮次大时,那么就直接更新当前选票,然后投出去。并且每个节点都会进行选票的归档统计(同一轮次的选票),如果发现没有节点选票过半数,节点就继续将最新的(zxix,sid)选票投出。 (3)在经历了多次(2)流程后,如果节点归档选票后,发现有节点选票过半,就会停止投票。而如果节点发现归档统计的结果,发现leader就是自己的话,就会广播告诉其他节点,我是leader。 (4)其他节点收到新的leader消息后,就会开始leader和follower的数据同步。
当我们使用stat path或者get path的方式查看节点元信息时,会有很多信息项目,那么每一项表示什么呢?
1)czxid- 引起这个znode创建的zxid,创建节点的事务的zxid 每次修改ZooKeeper状态都会收到一个zxid形式的时间戳,也就是ZooKeeper事务ID。 事务ID是ZooKeeper中所有修改总的次序。每个修改都有唯一的zxid,如果zxid1小于zxid2,那么zxid1在zxid2之前发生。 2)ctime - znode被创建的毫秒数(从1970年开始) 3)mzxid - znode最后更新的zxid 4)mtime - znode最后修改的毫秒数(从1970年开始) 5)pZxid-znode最后更新的子节点zxid 6)cversion - znode子节点变化号,znode子节点修改次数 7)dataversion - znode数据变化号 8)aclVersion - znode访问控制列表的变化号 9)ephemeralOwner- 如果是临时节点,这个是znode拥有者的session id。如果不是临时节点则是0。 10)dataLength- znode的数据长度 11)numChildren - znode子节点数量
图 2.1 zk写数据流程
读是局部姓的,即client只需要从与它相连的follower上读取数据即可; 写请求时,follower会将请求转发给leader,leader通过transaction(事务)的形式广播执行,这个过程是怎样的呢? (1)leader 会给所有follower发送一个PROPOSAL提案消息 (2)一个follower接收到这次PROPOSAL消息,写到磁盘,发送给leader一个ACK消息,告知已经收到。 (3)当Leader收到法定人数(quorum)的follower的ACK时候,发送commit消息执行。 注意:只有发送commit之后,做的修改才会提交,不然是会回退的;如果发现写入超时,是会回退这个更新操作的。
1) 首先要有一个main()线程 2) 在main线程中创建ZK客户端,这时会创建两个线程,一个负责网络连接通信(connect),一个负责监听(listener) 3) 通过connect线程将注册的监听事件发送给ZK 4) 在ZK的注册监听器列表中将注册的监听事件添加到列表中 5) ZK监听到有数据或路径发生变化时,就会将这个消息通过connect线程发送给listener线程 6) Listener线程内部调用process()方法
命令行下:客户端使用类似 ls path watch 的方式来监听一个节点的子节点的变化 当节点的子节点发生变化时,会通知相应的客户端,会显示如下的信息:
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/
WatchedEvent state:SyncConnected---事件状态:同步更新
type:NodeChildrenChanged---类型:节点的子节点改变
path:/--路径:/
图 3.1 zk统一命名服务
所谓命名服务就是对资源进行命名,用于更好地对资源进行定位。而zk本身的文件系统结构就可以创建以路径为名称的节点,用于存储服务器地址等信息。并且每个节点的名称都不会重复,严格按照文件系统的限制实现的。阿里巴巴开发的分布式服务框架DUBBO就是用zookeeper来作为其命名服务,维护全局的服务器列表,实现方式就是服务器启动时,在ZK的某个路径下创建一个代表自己的节点,节点的value存储的就是对应服务的服务地址(比如说URL地址)。
图 3.2 zk统一配置管理
在集群中环境的服务中,同一个程序会分布在多台机器上运行,便于横向扩展。个程序一般都会需要一些配置信息,如果程序分散部署在多台机器上,要逐个改变配置就变得很困难。同时,目前各种分布式系统以及微服务的流行,如何有效管理不同组件的配置也是一项重要的问题。现在可以在zk上创建一个节点,将配置信息作为节点的值。然后相关的应用程序都监听这个节点。当节点中的数据发生变化时,zk就会通知这些监听的程序说:“有东西改变了”。这些监听者就会自动到zk上获取更改后的配置信息,接着后面如何处理就看具体的业务逻辑了。 其实这是用到了zk的watcher机制,也就是发布/订阅机制,客户端可以向zookeeper服务器注册watcher,订阅自己感兴趣的节点,当相应的节点发生变化时,zookeeper服务器就会向客户端发布通知。 类似的场景还会用在服务高可用中,比如有两台数据库服务器,一主一备。
图3.3 zk统一配置管理--数据库主备
其中主备的数据中都运行一个zk的监控进程client,用于监控数据库服务的可用姓。当服务可用时,主备数据库的client会和zk建立持久会话,向zk发送心跳信息,表示自己正常运行。zk收到心跳信息,就会在节点上创建相应的节点,节点数据为当前可用的主数据的地址(实际上没有这么简单,这里只是简化了,便于理解)。如果任意一个数据库服务下线了,那么对应的在zk上的节点也会被删除。
图 3.4 zk服务动态上下线感知
对于一些复杂的分布式系统来说,系统中不同的组件非常多,即不同的服务很多。同一服务中还存在多台并发处理的服务器。那么如何知道这些服务哪些是可用的,哪些是不可用的呢?这就得用到zk了。 当服务上线时,会创建zkclient,并和zk保持持久会话,然后在zk的特定特定目录下创建一个临时znode(会话一断开,znode就消失)。而需要访问这些服务的客户端,就会监听在这个目录下(watcher机制),通过 getChildren() 这个api监听。当目录下的节点发生变化时,就意味着那些服务有下线或者上线了。这时候zk就会通知监听(订阅)该节点的客户端来获取最新的可用的服务列表。这样就可以动态上下线可用的服务。
zk可以用于保存其他业务集群中每个节点的状态信息,特别是一些主从结构的集群中。当发生故障转移时,可以从zk获取当前每个节点的状态信息。典型的比如 HBase的master节点的选举,就是通过zk协调状态实现的。 集群每个节点会在对应的一个znode下创建自己对应的子znode,用于保存自己的状态。然后每个节点都监听在这个znode下,也就是监听每个节点状态的变化。正常情况下,如果一个集群不发生故障,zk中保存的所有节点信息是不会变化的。如果zk中发生变化,意味着集群有故障发生。后续集群可借助zk中的节点状态数据来进行下一步的故障处理,比如选举新的master等操作。
图3.5 zk软负载均衡
每个到服务列表下注册的服务节点,相应的znode下的数据会保存服务地址,以及当前处理的访问数。当客户端来获取服务列表时,可以根据访问数选择比较少的一个服务节点进行请求的分发。达到一定的负载均衡的作用。
在一些资源的使用上,为了防止不同的请求的读写相互产生干扰,有了锁的概念。比如并发情况下,购买一件限量的商品。为了保证商品的数量是安全的,就需要用到锁机制。 锁分为共享锁(也叫读锁)和排他锁(也叫写锁)。获取共享锁时,所有事务都可以对数据进行读取,但是都不能修改数据。获取排他锁时,只有当前事务可以读取和修改数据,其他事务不可以。 在分布式系统中,对某些资源的限制就必须采用分布式锁,这种锁比起传统的非分布式锁复杂。可以使用zk来实现,实现方式也很多,下面讲一种。
首先,需要获取锁的人要在zk的特定目录下创建一个临时节点,并标明是读锁还是写锁。那么,这个目录下所有的节点都是想要获取锁的人。接下来就监听在这个目录下,有通知了,就表示有新的人要获取锁了。因为有很多人想获取锁,那么就要有个先后顺序,顺序由节点的顺序决定。 接着,就是节点的顺序问题了,如果都是读锁,那么顺序不影响;如果都是写锁,就得严格按照节点的顺序来决定获取写锁的顺序;如果两种锁混杂,节点的顺序是会影响到获取锁的顺序的。对于这样的场景,实现逻辑是这样的: (1)如果当前需要获取读锁,则判断该节点前面是否有比自己序号小的写节点,如果有,那么肯定得先等那个写节点获取写锁释放之后才轮到自己;如果没有,那么表示在自己前面的都是读锁,大家可以不用等待,可以一起获取读锁了。 (2)如果当前需要获取写锁,就需要看自己是不是序号最小的节点了,如果不是,那么意味着前面的写节点和读节点,无论是哪个,都不能让自己获取到写锁的。 最后,因为创建的节点都是临时节点,所以只要断开会话,就会自动删除,也就实现了锁的释放。
分布式系统中,非常重要的一个组件就是消息队列,可以实现应用解耦,异步消息,流量消减。首先传统的用户请求都是请求者直接发给服务器,服务器处理完成之后在将结果返回给请求者,这样会导致一个问题就是请求者在发出请求后会阻塞住,在收到服务器回复之前什么都干不了,而如果引入了消息队列,那么这个处理的流程就会变成请求者先把请求发给消息队列,然后服务器在从消息队列获得需要处理的消息,处理完成之后在将结果返回给请求者,这种方式下,请求者只要把请求发给消息队列就可以干其他事情了,这样就实现了请求的异步处理,也解除了请求者和服务器之间的耦合。 而在典型的大流量场景中,大量请求涌入到服务器时,会造成服务的瘫痪,所以需要有有个限流措施,但是又不至于丢失请求。所以需要一个组件将请求暂存,然后以一定的速度转发给服务器进行处理,这个组件就是消息队列。这种做法称为流量消减。 还有典型的抢购场景中,大量抢购请求会涌入到服务器中,为了防止服务器崩溃。可以使用消息队列,并将队列的大小设置为抢购数量的大小,超过则返回失败,也就抢不到东西了。成功的就会暂存在队列中,给服务器慢慢处理。 zk实现队列很简单,就是在特定目录下创建节点,存储请求。然后服务器依次从该目录下读取节点中的请求,处理并返回结果。处理完成后删除节点。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。