ZooKeeper3.6.3
星期六, 3月 1, 2025 | 43分钟阅读

关于 ZooKeeper3.6.3的学习。
简介
ZooKeeper是 Apache软件基金会的一个软件项目,它是一个为分布式应用提供一致性服务的软件,分布式应用程序可以基于ZooKeeper 实现数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master选举、分布式锁和分布式队列等功能。ZooKeeper现在是一个独立的顶级项目,曾经是Hadoop的一个子项目。
集群与分布式
集群:将一个任务部署在多个服务器,每个服务器都能独立完成该任务。例如:饭店后厨有三个厨师,他们每个人都会洗菜、切菜和炒菜,即使饭店同时来了很多客人也能轻松应对,这就是集群。
分布式:将一个任务拆分成若干个子任务,由若干个服务器分别完成这些子任务,每个服务器只能完成某个特定的子任务。例如:饭店后厨有三个厨师,洗菜、切菜和炒菜三个子任务分别由每个人独立完成,一个人洗菜,一个人切菜,一个人炒菜,这就是分布式。
从概念上就可以看出两者最主要的区别就是分布式是将一种业务拆分成多个子业务部署在多台服务器上,进而对外提供服务;而集群就是将多台服务器组合在一起提供同一种服务。
集群强调在多台服务器位置集中,并且容易统一管理;而分布式没有具体要求,不论放置在哪个位置,只要通过网络连接起来就行。集群是一种物理形态,即多台服务器在一起提供一种服务;而分布式是一种工作方式,即一个程序或业务分解到多台服务器分别完成。总结:集群是通过提高单位时间内执行的任务数来提升效率,分布式是以缩短单个任务的执行时间来提升效率。
技术架构演变
单一应用架构
通俗地讲,“单体应用(monolith application)”就是将应用程序的所有功能都打包成一个独立的单元。当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。
特点
所有的功能集成在一个项目工程中;
所有的功能打一个war包部署到服务器;
应用与数据库分开部署;
通过部署应用集群和数据库集群来提高系统的性能。
优点:
- 开发简单:一个IDE就可以快速构建单体应用;
- 便于共享:单个归档文件包含所有功能,便于在团队之间以及不同的部署阶段之间共享;
- 易于测试:单体应用一旦部署,所有的服务或特性就都可以使用了,这简化了测试过程,因为没有额外的依赖,每项测试都可以在部署完成后立刻开始;
- 容易部署:整个项目就一个war包,Tomcat安装好之后,应用扔上去就行了。群化部署也很容易,多个Tomcat+一个Nginx分分钟搞定。
缺点:
妨碍持续交付:随着时间的推移,单体应用可能会变得比较大,构建和部署时间也相应地延长,不利于频繁部署,阻碍持续交付。在移动应用开发中,这个问题会显得尤为严重;
不够灵活:随着项目的逐渐变大,整个开发流程的时间也会变得很长,即使在仅仅更改了一行代码的情况下,软件开发人员需要花费几十分钟甚至超过一个小时的时间对所有代码进行编译,并接下来花费大量的时间重新部署刚刚生成的产品,以验证自己的更改是否正确。如果多个开发人员共同开发一个应用程序,那么还要等待其他开发人员完成了各自的开发。这降低了团队的灵活性和功能交付频率;
受技术栈限制:项目变得越来越大的同时,我们的应用所使用的技术也会变得越来越多。这些技术有些是不兼容的,就比如在一个项目中大范围地混合使用C++和Java几乎是不可能的事情。在这种情况下,我们就需要抛弃对某些不兼容技术的使用,而选择一种不是那么适合的技术来实现特定的功能;
可靠性差:某个环节出现了死循环,导致内存溢出,会影响整个项目挂掉;
伸缩性差:系统的扩容只能针对应用进行扩容,不能做到对某个功能进行扩容,扩容后必然带来资源浪费的问题;
技术债务:假设我的代码库中有一个混乱的模块结构。此时,我需要添加一个新功能。如果这个模块结构清晰,可能我只需要2天时间就可以添加好这个功能,但是如今这个模块的结构很混乱,所以我需要4天时间。多出来的这两天就是债务利息。随着时间推移、人员变动,技术债务必然也会随之增多。
垂直应用架构
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。
特点
- 以单体结构规模的项目为单位进行垂直划分,就是将一个大项目拆分成一个一个单体结构项目;
- 项目与项目之间存在数据冗余,耦合性较大,比如上图中三个项目都存在用户信息;
- 项目之间的接口多为数据同步功能,如:数据库之间的数据库,通过网络接口进行数据库同步。
优点
开发成本低,架构简单;
避免单体应用的无限扩大;
系统拆分实现了流量分担,解决了并发问题;
可以针对不同系统进行扩容、优化;
方便水平扩展,负载均衡,容错率提高;
不同的项目可采用不同的技术;
系统间相互独立。
缺点
系统之间相互调用,如果某个系统的端口或者IP地址发生改变,调用系统需要手动变更;
垂直架构中相同逻辑代码需要不断的复制,不能复用;
系统性能扩展只能通过扩展集群结点,成本高、有瓶颈。
2.3.SOA面向服务架构
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心。当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。
P.S.从软件设计的角度上来说,ESB是一个抽象的间接层,提取了服务调用过程中调用与被调用动态交互中的一些共同的东西,减轻了服务调用者的负担。Java编程思想里提到:“所有的软件设计的问题都可以通过增加一个抽象的间接层而得到解决或者得到简化!”简单来说ESB就是一根管道,用来连接各个服务节点。为了集成不同系统,不同协议的服务,ESB做了消息的转化解释和路由工作,让不同的服务互联互通。
2.3.1.特点
基于SOA的架构思想将重复公用的功能抽取为组件,以服务的形式给各系统提供服务;
各项目(系统)与服务之间采用WebService、RPC等方式进行通信;
使用ESB企业服务总线作为项目与服务之间通信的桥梁。
优点
- 将重复的功能抽取为服务,提高开发效率,提高系统的可重用性、可维护性;
- 可以针对不同服务的特点制定集群及优化方案;
- 采用ESB减少系统中的接口耦合。
缺点
系统与服务的界限模糊,不利于开发及维护;
虽然使用了ESB,但是服务的接口协议不固定,种类繁多,不利于系统维护;
抽取的服务的粒度过大,系统与服务之间耦合性高;
涉及多种中间件,对开发人员技术栈要求高;
服务关系复杂,运维、测试部署困难。
微服务架构
特点
将系统服务层完全独立出来,并将服务层抽取为一个一个的微服务;
微服务中每一个服务都对应唯一的业务能力,遵循单一原则;
微服务之间采用RESTful等轻量协议传输。
优点
团队独立:每个服务都是一个独立的开发团队,这个小团队可以是2到5人的开发人员组成;
技术独立:采用去中心化思想,服务之间采用RESTful等轻量协议通信,使用什么技术什么语言开发,别人无需干涉;
前后端分离:采用前后端分离开发,提供统一Rest接口,后端不用再为PC、移动端开发不同接口;
数据库分离:每个微服务都有自己的存储能力,可以有自己的数据库。也可以有统一数据库;
服务拆分粒度更细,有利于资源重复利用,提高开发效率;
一个团队的新成员能够更快投入生产;
微服务易于被一个开发人员理解,修改和维护,这样小团队能够更关注自己的工作成果。无需通过合作才能体现价值;
可以更加精准的制定每个服务的优化方案(比如扩展),提高系统可维护性;
适用于互联网时代,产品迭代周期更短。
缺点
微服务过多,服务治理成本高,不利于系统维护;
分布式系统开发的技术成本高(网络问题、容错问题、调用关系、分布式事务等),对团队挑战大;
微服务将原来的函数式调用改为服务调用,不管是用rpc,还是http rest方式,都会增大系统整体延迟。这个是再所难免的,这个就需要我们将原来的串行编程改为并发编程甚至异步编程,增加了技术门槛;
多服务运维难度,随着服务的增加,运维的压力也在增大;
测试的难度提升。服务和服务之间通过接口来交互,当接口有改变的时候,对所有的调用方都是有影响的,这时自动化测试就显得非常重要了,如果要靠人工一个个接口去测试,那工作量就太大了,所以API文档的管理尤为重要。
微服务就是将一个单体架构的应用按业务划分为一个个的独立运行的程序即服务,它们之间通过HTTP协议进行通信(也可以采用消息队列来通信,如RabbitMQ,Kafaka等),可以采用不同的编程语言,使用不同的存储技术,自动化部署(如Jenkins)减少人为控制,降低出错概率。服务数量越多,管理起来越复杂,因此采用集中化管理。例如 Eureka,ZooKeeper等都是比较常见的服务集中化管理框架。
微服务是一种架构风格,架构就是为了解耦,实际使用的是分布式系统开发。一个大型的复杂软件应用,由一个或多个微服务组成。系统中的各个微服务可被独立部署,各个微服务之间是松耦合的。每个微服务仅关注于完成一件任务并很好的完成该任务。
一句话总结:微服务是SOA发展出来的产物,它是一种比较现代化的细粒度的SOA实现方式。
CAP原则
现如今,对于多数大型互联网应用,主机众多、部署分散,而且现在的集群规模越来越大,节点只会越来越多,所以节点故障、网络故障是常态,因此分区容错性也就成为了一个分布式系统必然要面对的问题。
解决了分区容错性,随之而来又产生了新的问题,那就是如何在保证了数据安全(一致,不易丢失)的同时,又让我们的分布式环境满足可用性呢?这就是著名的CAP原则。
CAP原则又称CAP定理,指的是在一个分布式系统中,Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。
CAP由 Eric Brewer 在2000年PODC会议上提出。该猜想在提出两年后被证明成立,成为我们熟知的CAP定理。CAP三者不可兼得。
取舍策略
CAP三个特性只能满足其中两个,那么取舍的策略就共有三种:
- CA without P:如果不要求P(不允许分区),则C(强一致性)和A(可用性)是可以保证的。但放弃P的同时也就意味着放弃了系统的扩展性,也就是分布式节点受限,没办法部署子节点,这是违背分布式系统设计的初衷的。
- CP without A:如果不要求A(可用),相当于每个请求都需要在服务器之间保持强一致,而P(分区)会导致同步时间无限延长(也就是等待数据同步完才能正常访问服务),一旦发生网络故障或者消息丢失等情况,就要牺牲用户的体验,等待所有数据全部一致了之后再让用户访问系统。设计成CP的系统其实不少,最典型的就是分布式数据库。对于分布式数据库来说,数据的一致性是最基本的要求,因为如果连这个标准都达不到,那么直接采用关系型数据库就好,没必要再浪费资源来部署分布式数据库。
- AP without C:要高可用并允许分区,则需放弃一致性。一旦产生分区,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。典型的应用就如某米的抢购手机场景,可能前几秒你浏览商品的时候页面提示是有库存的,当你选择完商品准备下单的时候,系统提示你下单失败,商品已售完。这其实就是先在A(可用性)方面保证系统可以正常的服务,然后在数据的一致性方面做了些牺牲,虽然多少会影响一些用户体验,但也不至于造成用户购物流程的严重阻塞。
总结
现如今,对于多数大型互联网应用,主机众多、部署分散,而且现在的集群规模越来越大,节点只会越来越多,所以节点故障、网络故障是常态,因此分区容错性也就成为了一个分布式系统必然要面对的问题。那么就只能在C和A之间进行取舍。但对于传统的项目就可能有所不同,拿银行的转账系统来说,涉及到金钱的对于数据一致性不能做出一丝的让步,C必须保证,出现网络故障的话,宁可停止服务。而互联网非金融项目普遍都是基于AP模式。
总而言之,没有最好的策略,好的系统应该是根据业务场景来进行架构设计的,只有适合的才是最好的。
BASE理论
CAP理论已经提出好多年了,难道真的没有办法解决这个问题吗?也许可以做些改变。比如C不必使用那么强的一致性,可以先将数据存起来,稍后再更新,实现所谓的“最终一致性”。
这个思路又是一个庞大的问题,同时也引出了第二个理论BASE理论。
BASE:全称Basically Available(基本可用),Soft state(软状态),和Eventually consistent(最终一致性)三个短语的缩写,来自ebay的架构师提出。
BASE 理论是对CAP中一致性和可用性权衡的结果,其来源于对大型互联网分布式实践的总结,是基于CAP定理逐步演化而来的。其核心思想是:
既然无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。
Basically Available(基本可用)
基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、功能上的可用性)。需要注意的是,基本可用绝不等价于系统不可用。
响应时间上的损失:正常情况下搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了1~2秒。
功能上的损失:购物网站在购物高峰(如双十一)时,为了保护系统的稳定性,部分消费者可能会被引导到一个降级页面。
Soft state(软状态)
什么是软状态呢?相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种“硬状态”。
软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据会有多个副本,允许不同副本数据同步的延时就是软状态的体现。
Eventually consistent(最终一致性)
系统不可能一直是软状态,必须有个时间期限。在期限过后,应当保证所有副本保持数据一致性。从而达到数据的最终一致性。这个时间期限取决于网络延时,系统负载,数据复制方案设计等等因素。
实际上,不只是分布式系统使用最终一致性,关系型数据库在某个功能上,也是使用最终一致性的,比如备份,数据库的复制都是需要时间的,这个复制过程中,业务读取到的值就是旧值。当然,最终还是达成了数据一致性。这也算是一个最终一致性的经典案例。
总结
总的来说,BASE理论面向的是大型高可用可扩展的分布式系统,和传统事务的ACID是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间是不一致的。
数据的一致性
定义
一些分布式系统通过复制数据来提高系统的可靠性和容错性,并且将数据的不同的副本存放在不同的机器
在数据有多份副本的情况下,如果网络、服务器或者软件出现故障,会导致部分副本写入成功,部分副本写入失败。这就造成各个副本之间的数据不一致,数据内容冲突。
模型
强一致性:要求无论更新操作实在哪一个副本执行,之后所有的读操作都要能获得最新的数据。
弱一致性:用户读到某一操作对系统特定数据的更新需要一段时间,我们称这段时间为“不一致性窗口”。
最终一致性:是弱一致性的一种特例,保证用户最终能够读取到某操作对系统特定数据的更新。
从客户端来看,有可能暂时获取的不是最新的数据,但是最终还是能访问到最新的
从服务端来看,数据存储并复制到整个系统超过半数的节点,以保证数据最终一致
Paxos算法
简介
Paxos算法是Leslie Lamport宗师提出的一种基于消息传递的分布式一致性算法,使其获得2013年图灵奖。
Paxos在1990年提出,被广泛应用于分布式计算中,Google的Chubby,Apache的Zookeeper都是基于它的理论来实现的
Paxos算法解决的问题是分布式一致性问题,即一个分布式系统中的各个进程如何就某个值(决议)达成一致。
传统节点间通信存在着两种通讯模型:共享内存(Shared memory)、消息传递(Messages passing),Paxos是一个基于消息传递的一致性算法。
算法描述
Paxos 描述了这样一个场景:
-有一个叫做 Paxos的小岛(Island)上面住了一批居民(Islander);
-岛上面所有的事情由一些特殊的人决定,他们叫做议员(Senator);
-议员的总数(Senator Count)是确定的,不能更改;
-岛上每次环境事务的变更都需要通过一个提议(Proposal),每个提议都有一个编号(PID),这个编号是一直增长的,不能倒退;
-每个提议都需要超过半数((Senator Count)/2+1)的议员同意才能生效(少数服从多数);
-每个议员只会同意大于当前编号的提议,包括已生效的和未生效的;
-如果议员收到小于等于当前编号的提议,他会拒绝,并告知对方:你的提议已经有人提过了。这里的当前编号是每个议员在自己记事本上记录的编号,他会不断更新这个编号;
-整个议会不能保证所有议员记事本上的编号总是相同的;
-现在议会有一个目标:保证所有的议员对于提议都能达成一致的看法。
现在议会开始运作,所有议员一开始记事本上面记录的编号都是0。有一个议员发了一个提议:将电费设定为1元/度。他首先看了一下记事本,嗯,当前提议编号是0,那么我的这个提议的编号就是1,于是他给所有议员发消息:1号提议,设定电费1元/度。其他议员收到消息以后查了一下记事本,哦,当前提议编号是0,这个提议可接受,于是他记录下这个提议并回复:我接受你的1号提议,同时他在记事本上记录:当前提议编号为1。发起提议的议员收到了超过半数的回复,立即给所有人发通知:1号提议生效!收到的议员会修改他的记事本,将1好提议由记录改成正式的法令,当有人问他电费为多少时,他会查看法令并告诉对方:1元/度。
再看冲突的解决:假设总共有三个议员S1-S3,S1和S2同时发起了一个提议:1号提议,设定电费。S1想设为1元/度,S2想设为2元/度。结果S3先收到了S1的提议,于是他做了和前面同样的操作。紧接着他又收到了S2的提议,结果他一查记事本,咦,这个提议的编号小于等于我的当前编号1,于是他拒绝了这个提议:对不起,这个提议先前提过了。于是S2的提议被拒绝,S1正式发布了提议:1号提议生效。S2向S1或者S3打听并更新了1号法令的内容,然后他可以选择继续发起2号提议。
Paxos推断
小岛(Island)服务器集群
议员(Senator)单台服务器
议员的总数(Senator Count)是确定的
提议(Proposal)每一次对集群中的数据进行修改
每个提议都有一个编号(PID),这个编号是一直增长的
每个提议都需要超过半数((Senator Count)/2+1)的议员同意才能生效
每个议员只会同意大于当前编号的提议
每个议员在自己记事本上面记录的编号,他不断更新这个编号
整个议会不能保证所有议员记事本上的编号总是相同的
议会有一个目标:保证所有的议员对于提议都能达成一致的看法。
前期投票(>1/2),后期广播(all)
Paxos算法
数据的全量备份
弱一致性=>最终一致性
算法模型延伸
如果Paxos岛上的议员人人平等,在某种情况下会由于提议的冲突而产生一个“活锁”(所谓活锁我的理解是大家都没有死,都在动,但是一直解决不了冲突问题)。Paxos的作者在所有议员中设立一个总统,只有总统有权发出提议,如果议员有自己的提议,必须发给总统并由总统来提出。
情况一:屁民甲(Client)到某个议员(ZK Server)那里询问(Get)某条法令的情况(ZNode的数据),议员毫不犹豫的拿出他的记事本(local storage),查阅法令并告诉他结果,同时声明:我的数据不一定是最新的。你想要最新的数据?没问题,等着,等我找总统Sync一下再告诉你。
情况二:屁民乙(Client)到某个议员(ZK Server)那里要求政府归还欠他的一万元钱,议员让他在办公室等着,自己将问题反映给了总统,总统询问所有议员的意见,多数议员表示欠屁民的钱一定要还,于是总统发表声明,从国库中拿出一万元还债,国库总资产由100万变成99万。屁民乙拿到钱回去了(Client函数返回)。
情况三:总统突然挂了,议员接二连三的发现联系不上总统,于是各自发表声明,推选新的总统,总统大选期间政府停业,拒绝屁民的请求。
无主集群模型
- 人人都会发送指令,投票
- 投票人数有可能导致分区(分不同阵营),
- 6个节点33对立
- 类似于以前党争
- 事务编号混乱,每个节点都有可能有自己的提议
- 提议的编号不能重复和小于
- 投票人数有可能导致分区(分不同阵营),
- 人人都会发送指令,投票
有主集群模型
- 只能有一个主发送指令,发送提议
- 单主会单点故障,肯定有备用的方案
- 重新选举,议员会把票投给数字编号和事务编号都大于自己的议员
- 数字编号(议员ID,ZooKeeper中叫 myid):为了快速选出总统
- 事务编号(会议ID,ZooKeeper中叫ZXID):为了确定谁的数据是最全的
- 选主过程:先比较ZXID,如果ZXID相同再比较myid
- 如果存在多个主就会脑裂
- 过半原则:选主过程中,如果某个议员获得了超过半数的选票,才可以成为主
- 议员同步数据只需要从主节点同步
- 节点越多业务能力越强,但是选举速度也会越慢
- 减少参与选举和投票的人数(例如 ZooKeeper的Observer)
主要集群中节点数目高于1/2+1,集群就可以正常运行
Raft算法
简介
Raft适用于一个管理日志一致性的协议,相比于Paxos协议Raft更易于理解和去实现它。
Raft将一致性算法分为了几个部分,包括领导选取(leader selection)、日志复制(logreplication)、安全(safety)
Raft算法的官网:https://raft.github.io/
中文版:https://acehi.github.io/thesecretlivesofdata-cn/raft/
英文版:http://thesecretlivesofdata.com/raft/
问题
分布式存储系统通过维护多个副本来提高系统的availability,难点在于分布式存储系统的核心问题:
- 维护多个副本的一致性。
Raft协议基于复制状态机(replicated state machine)
- 一组server从相同的初始状态起,按相同的顺序执行相同的命令,最终会达到一致的状态
- 一组server记录相同的操作日志,并以相同的顺序应用到状态机。
Raft有一个明确的场景,就是管理复制日志的一致性。
- 每台机器保存一份日志,日志来自于客户端的请求,包含一系列的命令,状态机会按顺序执行这些命令。
- 每台机器保存一份日志,日志来自于客户端的请求,包含一系列的命令,状态机会按顺序执行这些命令。
角色分配
Raft算法将Server划分为3种状态,或者也可以称作角色:
Leader
- 负责Client交互和log复制,同一时刻系统中最多存在1个。
Follower
- 被动响应请求RPC,从不主动发起请求RPC。
Candidate
- 一种临时的角色,只存在于leader的选举阶段,某个节点想要变成leader,那么就发起投票请求,同时自己变成candidate
- 一种临时的角色,只存在于leader的选举阶段,某个节点想要变成leader,那么就发起投票请求,同时自己变成candidate
算法流程
Term
- Term的概念类比中国历史上的朝代更替,Raft算法将时间划分成为任意不同长度的任期(term)。
- 任期用连续的数字进行表示。每一个任期的开始都是一次选举(election),一个或多个候选人会试图成为领导人。如果一个候选人赢得了选举,它就会在该任期的剩余时间担任领导人。在某些情况下,选票会被瓜分,有可能没有选出领导人,那么,将会开始另一个任期,并且立刻开始下一次选举。Raft算法保证在给定的一个任期最多只有一个领导人
RPC
Raft算法中服务器节点之间通信使用远程过程调用(RPCs)
基本的一致性算法只需要两种类型的RPCs,为了在服务器之间传输快照增加了第三种RPC。
RequestVote RPC:候选人在选举期间发起
AppendEntries RPC:领导人发起的一种心跳机制,复制日志也在该命令中完成
InstallSnapshot RPC:领导者使用该RPC来发送快照给太落后的追随者
日志复制(Log Replication)
主要用于保证节点的一致性,这阶段所做的操作也是为了保证一致性与高可用性。
当Leader选举出来后便开始负责客户端的请求,所有事务(更新操作)请求都必须先经过Leader处理
日志复制(Log Replication)就是为了保证执行相同的操作序列所做的工作。
在Raft中当接收到客户端的日志(事务请求)后先把该日志追加到本地的Log中
然后通过heartbeat把该Entry同步给其他Follower,Follower接收到日志后记录日志然后向Leader发送ACK
当Leader收到大多数(n/2+1)Follower的ACK信息后将该日志设置为已提交并追加到本地磁盘中
通知客户端并在下个heartbeat中Leader将通知所有的Follower将该日志存储在自己的本地磁盘中。
ZAB协议
背景
ZooKeeper 最早起源于雅虎研究院的一个研究小组。当时,研究人员发现,在雅虎的很多大型系统基本都需要依赖一个类似的系统来进行分布式协调,但是这些系统都存在分布式单点问题,所以雅虎的开发人员就试图开发出一个通用的无单点问题的分布式协调框架,以便让开发人员将精力集中在处理业务逻辑上。
于是,ZooKeeper 就诞生了!
ZooKeeper的出现不仅解决了分布式系统下数据一致性的问题,而且经历过线上验证,无论是从性能、易用性、稳定性上来说,都是工业级产品的标准。可以说在分布式系统中具有不可替代的核心地位,例如Hadoop、HBase、Kafka等大型分布式系统都已经将ZooKeeper作为其核心组件,用于分布式协调。
简介
ZAB协议,全称 ZooKeeper Atomic Broadcast(ZooKeeper 原子广播协议)。它是专门为分布式协调服务–ZooKeeper设计的一种支持崩溃恢复和原子广播的协议。
ZAB协议借鉴了Paxos算法,而ZooKeeper正是通过ZAB协议来保证分布式事务的最终一致性。基于该协议,ZooKeeper实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。
主备系统架构模型,就是指只有一个Leader节点负责处理外部的写事务请求,然后Leader节点将数据同步到其他 Follower 节点和Observer 节点。接下来我们详细介绍一下这三种角色。
三种角色
- Leader:负责整个ZooKeeper集群工作机制中的核心,主要工作有以下两个:事务请求的唯一调度和处理者,保证集群事务处理的顺序性集群内部各服务器的调度者。
- Follower:它是Leader的追随者,其主要工作有三个:处理客户端的非实物请求,转发事务请求给Leader服务器参与事务请求Proposal的投票参与Leader选举投票。
- Observer:是ZooKeeper自3.3.0开始引入的一个角色,它不参与事务请求Proposal的投票,也不参与Leader选举投票,只提供非事务的服务(查询),通常在不影响集群事务处理能力的前提下提升集群的非事务处理能力。
Follow 服务器和 Observer 服务器又统称为Learner服务器。
两种模式
ZAB协议的包括两种模式:崩溃恢复、原子广播。
崩溃恢复之数据恢复
当整个集群正在启动时,或者当Leader节点出现网络中断、崩溃等情况时,ZAB协议就会进入恢复模式并选举产生新的Leader,当Leader服务器选举出来后,并且集群中有过半的机器和该Leader节点完成数据同步后(同步指的是数据同步,用来保证集群中过半的机器能够和Leader服务器的数据状态保持一致),ZAB协议就会退出恢复模式。
当集群中已经有过半的Follower节点完成了和Leader状态同步以后,那么整个集群就进入了消息广播模式。这个时候,在Leader节点正常工作时,启动一台新的服务器加入到集群,那这个服务器会直接进入数据恢复模式,和Leader节点进行数据同步。同步完成后即可正常对外提供非事务请求的处理。
在整个消息广播中,Leader会将每一个事务请求转换成对应的Proposal来进行广播,并且在广播事务Proposal之前,Leader服务器会首先为这个事务Proposal分配一个全局单递增的唯一ID,称之为事务ID(即ZXID),由于ZAB协议需要保证每一个消息的严格的顺序关系,因此必须将每一个Proposal按照其ZXID的先后顺序进行排序和处理。
消息广播之原子广播
在ZooKeeper集群中,数据副本的传递策略就是采用消息广播模式。Leader服务器将客户端事务请求转化成一个Prososal(提议),并将该Proposal分发给集群中所有的Follower服务器。也就是向所有Follower节点发送数据广播请求(或数据复制)。
ZooKeeper中数据副本的同步方式与二段提交相似,但是却又不同。二段提交要求协调者必须等到所有的参与者全部反馈ACK确认消息后,再发送Commit消息。要求所有的参与者要么全部成功,要么全部失败。二段提交会产生严重的阻塞问题。
而在ZAB协议中Leader等待Follower的ACK反馈消息“只要半数以上的Follower 成功反馈即可,不需要收到全部Follower的反馈”。
总结
整个ZAB协议一共定义了三个阶段:
发现:要求ZooKeeper集群必须选举出一个Leader,同时Leader会维护一个Follower可用列表。将来客户端可以和这些 Follower节点进行通信。
同步:Leader要负责将本身的数据与Follower完成同步,做到多副本存储。这样便体现了CAP中的一致性和分区容错。Follower将队列中未处理完的请求消费完成后,写入本地事务日志中。
广播:Leader可以接受客户端新的事务Proposal 请求,将新的Proposal请求广播给所有的Follower。
三个阶段执行完为一个周期,在ZooKeeper集群的整个生命周期中,这三个阶段会不断进行,如果Leader崩溃或因其它原因导致 Leader 缺失,ZAB协议会再次进入阶段一。
角色分配
小岛–ZK Server Cluster
总统–ZK Server Leader
集群中所有修改数据的指令必须由总统发出
总统是由议员投票产生的(无主–>有主)\
选举条件
首先按照事务zxid进行排序
如果事务相同按照myid排序
议员(Senator)–ZK Server Learner
接受客户端请求
- 查询直接返回结果(有可能数据不一致)
- 写入数据,先将数据写入到当前server
- 发送消息给总统,总统将修改数据的命令发送给其他server
- 其他server接受命令后开始修改数据,修改完成后给总统返回成功的消息
- 当总统发现超过半数的人都修改成功,就认为修改成功了
- 并将信息传递给接受请求的zkServer,zkServer将消息返回给客户端,说明数据更新完成
分类Learner
Follower
- 拥有选举权,拥有投票权
- 接受客户端的访问
- 如果客户端执行写请求,只是将请求转发给Leader
Observer
只可以为客户端提供数据的查询和访问
如果客户端执行写请求,只是将请求转发给Leader
提议(Proposal)–ZNode Change
- 客户端的提议会被封装成一个节点挂载到一个Zookeeper维护的目录树上面
- 我们可以对数据进行访问(绝对路径)
- 数据量不能超过1M
提议编号(PID)–Zxid
- 会按照数字序列递增,不会减少不会重复
正式法令–所有ZNode及其数据
- 超过半数的服务器更新这个数据,就说明数据已经是正式的了
屁民–Client
- 发送请求(查询请求,修改请求)
ZooKeeper集群搭建
首先将三台虚拟机切换至相互免秘钥快照(keyfree),然后将准备好的ZooKeeper 安装包上传至服务器。
node01
先在node01机器上执行以下操作。
解压。
[root@node01 ~]# tar -zxvf apache-zookeeper-3.6.3-bin.tar.gz -C /opt/yjx/
[root@node01 ~]# rm apache-zookeeper-3.6.3-bin.tar.gz -rf
创建数据目录、日志目录。
[root@node01 ~]# mkdir -p /var/yjx/zookeeper/data
[root@node01 ~]# mkdir -p /opt/yjx/apache-zookeeper-3.6.3-bin/logs
修改配置文件,ZooKeeper 启动时默认加载的配置文件名为zoo.cfg
。
[root@node01 ~]# cd /opt/yjx/apache-zookeeper-3.6.3-bin/
[root@node01 apache-zookeeper-3.6.3-bin]# cp conf/zoo_sample.cfg conf/zoo.cfg
[root@node01 apache-zookeeper-3.6.3-bin]# vim conf/zoo.cfg
主要修改以下内容(数字表示行号):
12 dataDir=/var/yjx/zookeeper/data
13 dataLogDir=/opt/yjx/apache-zookeeper-3.6.3-bin/logs
15 clientPort=2181
37 server.1=node01:2888:3888
38 server.2=node02:2888:3888
39 server.3=node03:2888:3888
server.1中的1是myid文件中的内容,2888用于集群内部通信,3888用于选举Leader。
node02/03
接下来将node01的ZooKeeper所有文件拷贝至node02和node03。推荐从node02和node03拷贝。
[root@node02 ~]# scp -r root@node01:/opt/yjx/apache-zookeeper-3.6.3-bin /opt/yjx/
[root@node03 ~]# scp -r root@node01:/opt/yjx/apache-zookeeper-3.6.3-bin /opt/yjx/
# 或者使用分发脚本
[root@node01 ~]# yjxrsync /opt/yjx/apache-zookeeper-3.6.3-bin
node01/02/03
然后在三台机器的/var/yjx/zookeeper/data
目录下分别创建myid
文件,内容分别为1,2,3。
node01:
[root@node01 ~]# echo 1 > /var/yjx/zookeeper/data/myid
node02:
[root@node02 ~]# mkdir -p /var/yjx/zookeeper/data
[root@node02 ~]# echo 2 > /var/yjx/zookeeper/data/myid
node03:
[root@node03 ~]# mkdir -p /var/yjx/zookeeper/data
[root@node03 ~]# echo 3 > /var/yjx/zookeeper/data/myid
环境变量
最后 vim /etc/profile
配置环境变量,环境搭建结束。
export ZOOKEEPER_HOME=/opt/yjx/apache-zookeeper-3.6.3-bin
export PATH=$ZOOKEEPER_HOME/bin:$PATH
配完环境变量后 source /etc/profile
重新加载环境变量。
启动/查看状态/关闭
启动集群:
[root@node01 ~]# zkServer.sh start
[root@node02 ~]# zkServer.sh start
[root@node03 ~]# zkServer.sh start
查看状态:
[root@node01 ~]# zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/yjx/apache-zookeeper-3.6.3-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: follower
[root@node02 ~]# zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/yjx/apache-zookeeper-3.6.3-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: follower
[root@node03 ~]# zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/yjx/apache-zookeeper-3.6.3-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: leader
停止集群:
[root@node01 ~]# zkServer.sh stop
[root@node02 ~]# zkServer.sh stop
[root@node03 ~]# zkServer.sh stop
环境搭建成功后,删除ZooKeeper安装包,shutdown -h now
关机拍摄快照。
ZK集群一键启动脚本
在/usr/local/bin
目录下创建对应服务的脚本:
[root@node01 ~]# vim /usr/local/bin/zookeeper
zookeeper
脚本内容如下:
#!/bin/bash
user=$(whoami)
case $1 in
"start")
for i in node01 node02 node03
do
echo -e "\e[1;34m==================== $i ZooKeeper 启动 ====================\e[0m"
ssh $user@$i "/opt/yjx/apache-zookeeper-3.6.3-bin/bin/zkServer.sh start"
done
;;
"stop")
for i in node01 node02 node03
do
echo -e "\e[1;34m==================== $i ZooKeeper 停止 ====================\e[0m"
ssh $user@$i "/opt/yjx/apache-zookeeper-3.6.3-bin/bin/zkServer.sh stop"
done
;;
"status")
for i in node01 node02 node03
do
echo -e "\e[1;34m==================== $i ZooKeeper 状态 ====================\e[0m"
ssh $user@$i "/opt/yjx/apache-zookeeper-3.6.3-bin/bin/zkServer.sh status"
done
;;
"restart")
for i in node01 node02 node03
do
echo -e "\e[1;34m==================== $i ZooKeeper 重启 ====================\e[0m"
ssh $user@$i "/opt/yjx/apache-zookeeper-3.6.3-bin/bin/zkServer.sh restart"
done
;;
esac
修改脚本权限为用户读写执行rwx
,组读执行r-x
,其他用户无权限---
:
[root@node01 ~]# chmod 750 /usr/local/bin/zookeeper
ZooKeeper常见命令
一、zk服务命令
1. 启动ZK服务: bin/zkServer.sh start
2. 查看ZK服务状态: bin/zkServer.sh status
3. 停止ZK服务: bin/zkServer.sh stop
4. 重启ZK服务: bin/zkServer.sh restart
5. 连接服务器: zkCli.sh -server 127.0.0.1:2181
二、zk客户端命令
1.ls -- 查看某个目录包含的所有文件,例如:
[zk: 127.0.0.1:2181(CONNECTED) 1] ls /
ls /path
2.create -- 创建znode,并设置初始内容,例如:
[zk: 127.0.0.1:2181(CONNECTED) 1] create /test "test"
Created /test
创建一个新的 znode节点“ test ”以及与它关联的字符串
create /path data 默认创建持久节点
create -s /path data 创建顺序节点
create -e /path data 创建临时节点
create /parent/sub/path /data
3.get -- 获取znode的数据,如下:
[zk: 127.0.0.1:2181(CONNECTED) 1] get /test
get /path
get /path0000000018 访问顺序节点必须输入完整路径
4.set -- 修改znode内容,例如:
[zk: 127.0.0.1:2181(CONNECTED) 1] set /test "ricky"
set /path /data
5.delete -- 删除znode,例如:
[zk: 127.0.0.1:2181(CONNECTED) 1] delete /test
delete /path 删除没有子节点的节点
rmr /path 移除节点并且递归移除所有子节点
6.quit -- 退出客户端
7.help -- 帮助命令
ZooKeeper存储模型
存储结构
zookeeper是一个树状结构,维护一个小型的数据节点znode
数据以keyvalue的方式存在,目录是数据的key
所有的数据访问都必须以绝对路径的方式呈现
[zk: localhost:2181(CONNECTED) 10] stat /yjx
666 当前节点的值
cZxid = 0xf00000013
创建这个节点的事务id,ZXID是一个长度64位的数字,
低32位是按照数字递增,即每次客户端发起一个proposal,低32位的数字简单加1。
高32位是leader周期的epoch编号
ctime = Mon Dec 09 17:33:06 CST 2019 创建时间
mZxid = 0xf00000013 最后一次修改节点数据的事务ID
mtime = Mon Dec 09 17:33:06 CST 2019 修改时间
pZxid = 0xf00000014 子节点的最新事务ID
cversion = 1 对此znode的子节点进行的更改次数
dataVersion = 对此znode的数据所作的修改次数
aclVersion = 对此znode的acl更改次数
ephemeralOwner = 0x0 (持久化节点)0x16ee9fc0feb0001(临时节点)
dataLength = 3 数据的长度
numChildren = 1 子节点的数目
节点的分类
持久化节点(PERSISTENT)
- 默认创建的就是持久化节点
临时节点(Ephemral)
只要创建节点的会话有效,节点就不会失效
可以被所有的客户端所查看
事务编号和临时节点编号是一致的
create-e
一旦会话结束,临时节点也会被自动删除,一般这个功能用于判断节点和服务器是否保持连接
序列化节点(Sequential)
在名字的后面添加一个序列号(有序)
create-s
ZooKeeper监听机制
语法格式:addWatch [-m mode] path # optional mode is one of [PERSISTENT, PERSISTENT_RECURSIVE] - default is PERSISTENT_RECURSIVE
。
addWatch
的作用是针对指定节点添加事件监听,支持两种模式:
PERSISTENT
:持久化订阅,针对当前节点的修改和删除事件,以及当前节点的子节点的新增和删除事件。PERSISTENT_RECURSIVE
:持久化递归订阅,在PERSISTENT的基础上,增加了子节点修改的事件触发,以及子节点的子节点的数据变化都会触发相关事件(满足递归订阅特性)。默认模式。
PERSISTENT 模式案例演示:
# 创建节点
[zk: localhost:2181(CONNECTED) 0] create /bigdata 100
Created /bigdata
# 监听节点,使用 PERSISTENT 模式
[zk: localhost:2181(CONNECTED) 1] addWatch -m PERSISTENT /bigdata
# 修改当前节点会触发监听事件
[zk: localhost:2181(CONNECTED) 2] set /bigdata 200
WATCHER:: # 触发监听事件 NodeDataChanged
WatchedEvent state:SyncConnected type:NodeDataChanged path:/bigdata
# 创建子节点会触发监听事件
[zk: localhost:2181(CONNECTED) 3] create /bigdata/zk 300
WATCHER:: # 触发监听事件 NodeChildrenChanged
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/bigdata
Created /bigdata/zk
# 修改子节点不会触发监听事件
[zk: localhost:2181(CONNECTED) 4] set /bigdata/zk 301
# 删除子节点触发监听事件
[zk: localhost:2181(CONNECTED) 5] delete /bigdata/zk
WATCHER:: # 触发监听事件 NodeChildrenChanged
WatchedEvent state:SyncConnected type:NodeChildrenChanged
path:/bigdata
# 删除当前节点会触发监听事件
[zk: localhost:2181(CONNECTED) 6] delete /bigdata
WATCHER:: # 触发监听事件 NodeDeleted
WatchedEvent state:SyncConnected type:NodeDeleted path:/bigdata
PERSISTENT_RECURSIVE 模式案例演示:
# 创建节点
[zk: localhost:2181(CONNECTED) 0] create /bigdata 100
Created /bigdata
# 监听节点,不指定模式默认采用 PERSISTENT_RECURSIVE 模式
[zk: localhost:2181(CONNECTED) 1] addWatch /bigdata
# 修改当前节点会触发监听事件
[zk: localhost:2181(CONNECTED) 2] set /bigdata 200
WATCHER:: # 触发监听事件 NodeDataChanged
WatchedEvent state:SyncConnected type:NodeDataChanged path:/bigdata
# 创建子节点会触发监听事件
[zk: localhost:2181(CONNECTED) 3] create /bigdata/zk 30
WATCHER:: # 触发监听事件 NodeCreated
WatchedEvent state:SyncConnected type:NodeCreated path:/bigdata/zk
Created /bigdata/zk
# 创建子节点的子节点会触发监听事件
[zk: localhost:2181(CONNECTED) 4] create /bigdata/zk/test 400
WATCHER:: # 触发监听事件 NodeCreated
WatchedEvent state:SyncConnected type:NodeCreated
path:/bigdata/zk/test
Created /bigdata/zk/test
# 修改子节点会触发监听事件
[zk: localhost:2181(CONNECTED) 5] set /bigdata/zk 301
WATCHER:: # 触发监听事件 NodeDataChanged
WatchedEvent state:SyncConnected type:NodeDataChanged path:/bigdata/zk
# 修改子节点的子节点会触发监听事件
[zk: localhost:2181(CONNECTED) 6] set /bigdata/zk/test 401
WATCHER:: # 触发监听事件 NodeDataChanged
WatchedEvent state:SyncConnected type:NodeDataChanged path:/bigdata/zk/test
# 删除子节点的子节点触发监听事件
[zk: localhost:2181(CONNECTED) 7] delete /bigdata/zk/test
WATCHER:: # 触发监听事件 NodeDeleted
WatchedEvent state:SyncConnected type:NodeDeleted
path:/bigdata/zk/test
# 删除子节点和当前节点都会触发监听事件
[zk: localhost:2181(CONNECTED) 8] deleteall /bigdata
WATCHER:: # 触发监听事件 NodeDeleted
WatchedEvent state:SyncConnected type:NodeDeleted path:/bigdata/zk
WATCHER:: # 触发监听事件 NodeDeleted
WatchedEvent state:SyncConnected type:NodeDeleted path:/bigdata
仔细观察,两种模式的
WatchedEvent
稍微有些不同。
权限控制(了解)
ACL权限控制
ZK的节点有5种操作权限:CREATE、READ、WRITE、DELETE、ADMIN 也就是 增、删、改、查、管理权限,这5种权限简写为crwda,这5种权限中,delete是指对子节点的删除权限,其它4种权限指对自身节点的操作权限
身份的认证有4种方式:
- world:默认方式,相当于全世界都能访问
- auth:代表已经认证通过的用户(cli中可以通过addauth digest user:pwd 来添加当前上下文中的授权用户)
- digest:即用户名:密码这种方式认证,这也是业务系统中最常用的
- ip:使用Ip地址认证
schema
world:只有一个用户anyone,代表所有人(默认)
ip:使用IP地址认证
auth:使用已添加认证的用户认证
digest:使用用户名:密码 方式认证
id
world:只有一个id,anyone
ip:通常是一个ip地址或者地址段
auth:用户名
digest:自定义
权限
create 简写为c,可以创建子节点
delete 简写为d 可以删除子节点
read 简写为r 可以读取节点数据及显示子节点列表
write 简写为w 可以设置节点数据
admin 简写为a 可以设置管理权限
查看ACL
getAcl /parent
设置ACL
setAcl /parent world:anyone:r
添加用户
addauth digest zhangsan:123456
addauth digest lisi:123456
设置权限
setAcl /parent auth:zhangsan:123456:r
setAcl /parent auth:lisi:123456:rcwd
退出当前用户
quit
后续访问 /parent路径,需要先添加用户
addauth digest zhangsan:123456
四字命令(了解)
官方文档:https://zookeeper.apache.org/doc/r3.6.3/zookeeperAdmin.html#sc_4lw
可以通过
nc
(NetCat)命令,脱离ZK客户端和ZK服务交互。需预先安装
nc
命令,安装命令:yum -y install nc
。然后在
zoo.cfg
配置文件中配置启用四字命令41w.commands.whitelist=*
。命令格式:
echo [commond] | nc [ip] [port]
。安装nc
- yum install nc-y
- 如果出现下图的错误,请重新清空和重构cache
四字命令
ZooKeeper支持某些特定的四字命令(The Four Letter Words)与其进行交互。
使用方式,在shell终端输入:echo 四字命令|nc node01 2181
Java访问(了解)
pom
pom.xml 信息
<!-- ZooKeeper 核心依赖 -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.3</version>
</dependency>
<!-- JUnit 单元测试依赖 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
连接/关闭
ZooKeeperTest.java
package com.yjxxt;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
/*
@DisplayName:测试类在测试报告中的名称,可以加在类上,也可以加在方法上。
@BeforeAll和@AfterAll:它们定义了整个测试类在开始前以及结束时的操作,只能修饰静态方法,主要用于在测试过程中所需要的全局数据和外部资源的初始化和清理。
@BeforeEach和@AfterEach:它们所标注的方法会在每个测试用例方法开始前和结束时执行,主要是负责该测试用例所需要的运行环境的准备和销毁。
*/
@DisplayName("ZooKeeper 测试类")
public class ZooKeeperTest {
/**
* 客户端对象
*/
private ZooKeeper zooKeeper;
@BeforeEach
public void init() {
try {
/*
初始次数为 1,后面要在内部类中使用。
三种写法:
1、写成外部类成员变量,不用加 final;
2、作为函数局部变量,放在 try 外面,写成 final;
3、写在 try 中,不加 final。
*/
CountDownLatch countDownLatch = new CountDownLatch(1);
// zkServer 的 ip、port,如果是集群逗号分隔
String connectString = "192.168.100.101:2181,192.168.100.102:2181,192.168.100.103:2181";
// 超时时间
int sessionTimeout = 5000;
zooKeeper = new ZooKeeper(connectString, sessionTimeout,
// 连接成功后监听
watchedEvent -> {
// 如果状态变成已连接则次数 -1
if (Watcher.Event.KeeperState.SyncConnected.equals(watchedEvent.getState())) {
System.out.println("连接成功");
// 次数 -1
countDownLatch.countDown();
}
});
// 等待,次数为 0 时才会继续往下执行(等待监听器监听到连接成功,才能操作 zk)
countDownLatch.await();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
@AfterEach
public void close() {
// 关闭连接
try {
if (zooKeeper != null) {
zooKeeper.close();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
控制台打印结果:
连接成功
节点是否存在
/**
* 操作后,服务端会返回处理结果,返回 void、null也算处理结果。
* 同步指的是当前线程阻塞,等待服务端返回数据,收到返回的数据才继续往下执行;
* 异步回调指的是,把对结果(返回的数据)的处理写在回调函数中,当前线程不等待返回的数据,继续往下执行,收到返回的数据时自动调用回调函数来处
理。
*/
@DisplayName("检测节点是否存在")
@Test
public void testDataIsExists() {
// 同步方式
System.out.println("同步方式");
Stat exists = null;
String znode = "/bigdata";
try {
// 如果存在,返回节点状态 stat;如果不存在,返回 null。
// 第二个参数是 watch,是否监听。
exists = zooKeeper.exists(znode, false);
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
if (exists == null) {
System.out.println(znode + "节点不存在");
} else {
System.out.println(znode + "节点存在");
}
System.out.println("========== 华丽的分割线 ==========");
// 异步回调
System.out.println("异步回调");
zooKeeper.exists(znode, false,
// 第二个是 path znode 路径,第三个是 ctx 后面传入实参,第四个是 znode 的状态
(i, s, o, stat) -> {
// 如果节点不存在,返回的 stat 是 null
if (stat == null) {
System.out.println(znode + "节点不存在");
} else {
System.out.println(znode + "节点存在");
}
// ctx:Object 类型
}, "传给回调的参数");
}
控制台打印结果:
连接成功
同步方式
/bigdata节点不存在
========== 华丽的分割线 ==========
异步回调
/bigdata节点不存在
创建节点
@DisplayName("创建节点")
@Test
public void testCreateNode() {
// 同步方式
System.out.println("同步方式");
String znode1 = "/bigdata";
try {
// 数据要写成 byte[],不携带数据写成 null;
// 默认 acl 权限使用 ZooDefs.Ids.OPEN_ACL_UNSAFE;
// 最后一个是节点类型,P 是永久,E 是临时,S 是有序
zooKeeper.create(znode1, "abc".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println(znode1 + "节点创建成功");
// 如果节点已存在,会抛出异常
} catch (KeeperException | InterruptedException e) {
System.out.println("创建节点" + znode1 + "失败,请检查节点是否已存在");
e.printStackTrace();
}
System.out.println("========== 华丽的分割线 ==========");
// 异步回调
System.out.println("异步回调");
String znode2 = "/java";
zooKeeper.create(znode2, "abc".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT,
// 第二个 path,第三个 ctx,第四个节点状态
(i, s, o, s1, stat) -> {
// 回调方式不抛出异常,返回的 stat 是创建节点的状态,如果节点已存在,返回的 stat 是 null
if (stat == null) {
System.out.println("创建节点" + znode2 + "失败,请检查节点是否已存在");
} else {
System.out.println(znode2 + "节点创建成功");
}
}, null);
}
控制台打印结果:
连接成功
同步方式
/bigdata节点创建成功
========== 华丽的分割线 ==========
异步回调
/java节点创建成功
获取节点数据
@DisplayName("获取节点数据")
@Test
public void testGetNodeData() {
// 获取节点数据,返回 byte[]
// 同步方式
System.out.println("同步方式");
String znode1 = "/bigdata";
byte[] data = null;
try {
// 第二个参数是 watch,第三个是 stat
data = zooKeeper.getData(znode1, false, null);
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
// 调用 new String() 时要判断 data 是否为 null,如果是 null 会抛 NPE
if (data == null) {
System.out.println(znode1 + "节点没有数据");
} else {
System.out.println(znode1 + "节点数据:" + new String(data));
}
System.out.println("========== 华丽的分割线 ==========");
// 异步回调
System.out.println("异步回调");
String znode2 = "/java";
zooKeeper.getData(znode2, false,
// 第二个起依次是:path、ctx、返回的节点数据、节点状态
(i, s, o, bytes, stat) -> {
// 不必判断 bytes 是否是 null,如果节点没有数据,不会调用回调函数;
// 执行到此,说明 bytes 不是 null
System.out.println(znode2 + "节点数据:" + new String(bytes));
}, null);
}
控制台打印结果:
连接成功
同步方式
/bigdata节点数据:abc
========== 华丽的分割线 ==========
异步回调
/java节点数据:abc
更新节点数据
@DisplayName("更新节点数据")
@Test
public void testUpdateNodeData() {
// 同步方式
System.out.println("同步方式");
String znode1 = "/bigdata";
try {
// 最后一个参数是版本号,-1 表示可以是任何版本
zooKeeper.setData(znode1, "123".getBytes(), -1);
System.out.println(znode1 + "节点数据更新成功");
} catch (KeeperException | InterruptedException e) {
System.out.println(znode1 + "节点数据更新失败");
e.printStackTrace();
}
System.out.println("========== 华丽的分割线 ==========");
// 异步回调
System.out.println("异步回调");
String znode2 = "/java";
zooKeeper.setData("/java", "123".getBytes(), -1,
// 第二个是 path,第三个是 ctx
(i, s, o, stat) -> {
System.out.println(znode2 + "节点数据更新成功");
}, null);
}
控制台打印结果:
连接成功
同步方式
/bigdata节点数据更新成功
========== 华丽的分割线 ==========
异步回调
/java节点数据更新成功
获取子节点列表
/**
* 只获取子节点,不获取孙节点。
* watch:可以写 boolean,要添加监听就写 true,不监听写 false;
* 可以写 Watcher 对象,new 一个 Watcher 对象表示要监听,null 表示不监听。
*/
@DisplayName("获取子节点列表")
@Test
public void testGetNodeList() {
// 获取子节点列表,List<String>,比如/bigdata/zk,/bigdata/java,返回的是["zk"、"java"]
// 同步方式
System.out.println("同步方式");
String znode1 = "/";
List<String> children = null;
try {
// 第二个参数是 watch
children = zooKeeper.getChildren(znode1, false);
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
System.out.println(znode1 + "子节点列表:" + children);
System.out.println("========== 华丽的分割线 ==========");
// 异步回调
System.out.println("异步回调");
String znode2 = "/zookeeper";
zooKeeper.getChildren(znode2, false,
// 第二个起依次是:path、ctx、返回的子节点列表
(i, s, o, list) -> System.out.println(znode2 + "子节点列表:" + list), null);
}
控制台打印结果:
连接成功
同步方式
/子节点列表:[bigdata, java, zookeeper]
========== 华丽的分割线 ==========
异步回调
/zookeeper子节点列表:[config, quota]
删除节点
/**
* delete() 只能删除没有子节点的 znode,如果该 znode 有子节点会抛出异常。
* 没有提供递归删除子节点的方法,如果要删除带有子节点的 znode,需要自己实现递归删除。
* 可以先 getChildren() 获取子节点列表,遍历列表依次删除子节点,再删除父节点。
*/
@DisplayName("删除节点")
@Test
public void testDeleteNode() {
// 同步方式
System.out.println("同步方式");
String znode1 = "/bigdata";
try {
// 第二个参数是版本号,-1 表示可以是任何版本
zooKeeper.delete(znode1, -1);
System.out.println(znode1 + "节点删除成功");
} catch (InterruptedException | KeeperException e) {
System.out.println(znode1 + "节点删除失败");
e.printStackTrace();
}
System.out.println("========== 华丽的分割线 ==========");
// 异步回调
System.out.println("异步回调");
String znode2 = "/java";
zooKeeper.delete(znode2, -1,
// 第二个是 path,第三个是 ctx
(i, s, o) -> System.out.println(znode2 + "节点删除成功"), null);
}
控制台打印结果:
连接成功
同步方式
/bigdata节点删除成功
========== 华丽的分割线 ==========
异步回调
/java节点删除成功
复杂环境搭建(了解)
基于Observer的环境搭建
ZooKeeper的热部署
附录:
分布式协调框架应用
场景一:统一命名服务。
- 有一组服务器向客户端提供某种服务,我们希望客户端每次请求服务端都可以找到服务端集群中某一台服务器,这样服务端就可以向客户端提供客户端所需的服务。对于这种场景,我们的程序中一定有一份这组服务器的列表,每次客户端请求时候,都是从这份列表里读取这份服务器列表。那么这分列表显然不能存储在一台单节点的服务器上,否则这个节点挂掉了,整个集群都会发生故障,我们希望这份列表时高可用的。高可用的解决方案是:这份列表是分布式存储的,它是由存储这份列表的服务器共同管理的,如果存储列表里的某台服务器坏掉了,其他服务器马上可以替代坏掉的服务器,并且可以把坏掉的服务器从列表里删除掉,让故障服务器退出整个集群的运行,而这一切的操作又不会由故障的服务器来操作,而是集群里正常的服务器来完成。这是一种主动的分布式数据结构,能够在外部情况发生变化时候主动修改数据项状态的数据机构。,它和javaEE里的JNDI服务很像。
场景二:分布式锁服务。
- 当分布式系统操作数据,例如:读取数据、分析数据、最后修改数据。在分布式系统里这些操作可能会分散到集群里不同的节点上,那么这时候就存在数据操作过程中一致性的问题,如果不一致,我们将会得到一个错误的运算结果,在单一进程的程序里,一致性的问题很好解决,但是到了分布式系统就比较困难,因为分布式系统里不同服务器的运算都是在独立的进程里,运算的中间结果和过程还要通过网络进行传递,那么想做到数据操作一致性要困难的多。Zookeeper提供了一个锁服务解决了这样的问题,能让我们在做分布式数据运算时候,保证数据操作的一致性。
场景三:配置管理。
- 在分布式系统里,我们会把一个服务应用分别部署到n台服务器上,这些服务器的配置文件是相同的(例如:我设计的分布式网站框架里,服务端就有4台服务器,4台服务器上的程序都是一样,配置文件都是一样),如果配置文件的配置选项发生变化,那么我们就得一个个去改这些配置文件,如果我们需要改的服务器比较少,这些操作还不是太麻烦,如果我们分布式的服务器特别多,比如某些大型互联网公司的hadoop集群有数千台服务器,那么更改配置选项就是一件麻烦而且危险的事情。这时候zookeeper就可以派上用场了,我们可以把zookeeper当成一个高可用的配置存储器,把这样的事情交给zookeeper进行管理,我们将集群的配置文件拷贝到zookeeper的文件系统的某个节点上,然后用zookeeper监控所有分布式系统里配置文件的状态,一旦发现有配置文件发生了变化,每台服务器都会收到zookeeper的通知,让每台服务器同步zookeeper里的配置文件,zookeeper服务也会保证同步操作原子性,确保每个服务器的配置文件都能被正确的更新。
场景四:为分布式系统提供故障修复的功能。
- 集群管理是很困难的,在分布式系统里加入了zookeeper服务,能让我们很容易的对集群进行管理。集群管理最麻烦的事情就是节点故障管理,zookeeper可以让集群选出一个健康的节点作为master,master节点会知道当前集群的每台服务器的运行状况,一旦某个节点发生故障,master会把这个情况通知给集群其他服务器,从而重新分配不同节点的计算任务。Zookeeper不仅可以发现故障,也会对有故障的服务器进行甄别,看故障服务器是什么样的故障,如果该故障可以修复,zookeeper可以自动修复或者告诉系统管理员错误的原因让管理员迅速定位问题,修复节点的故障。大家也许还会有个疑问,master故障了,那怎么办了?zookeeper也考虑到了这点,zookeeper内部有一个“选举领导者的算法”,master可以动态选择,当master故障时候,zookeeper能马上选出新的master对集群进行管理。
最终一致性细节分类
因果一致性(Casual Consistency)
如果进程A通知进程B它已更新了一个数据项,那么进程B的后续访问将返回更新后的值,且一次写入将保证取代前一次写入。
与进程A无因果关系的进程C的访问,遵守一般的最终一致性规则。
查询微博和评论
读己之所写一致性(read-your-writes)
当进程A自己更新一个数据项之后,它总是访问到更新过的值,绝不会看到旧值。这是因果一致性模型的一个特例。
读自己的数据都从主服务器去读取,读其他人的数据再从从服务器去读取
发表微博与修改微博
会话(Session)一致性
这是上一个模型的实用版本,它把访问存储系统的进程放到会话的上下文中。只要会话还存在,系统就保证“读己之所写”一致性。如果由于某些失败情形令会话终止,就要建立新的会话,而且系统的保证不会延续到新的会话。
确保会话内访问的都是最新的
登录场景
单调(Monotonic)读一致性。
如果进程已经看到过数据对象的某个最新值,那么任何后续访问都不会返回在那个值之前的值。
不会读取最旧的数据
秒杀场景
单调写一致性。
- 系统保证来自同一个进程的写操作顺序执行。要是系统不能保证这种程度的一致性,就非常难以编程了。
- 按照顺序完成数据的书写
- 打游戏副本场景