Zookeeper基本原理
ZooKeeper重要概念解读
Data model(数据模型)
ZooKeeper数据模型采用层次化的多叉树形结构,每个节点上都可以存储数据,这些数据可以是数字、字符串或者是二级制序列。并且。每个节点还可以拥有N个子节点,最上层是根节点以“/”来代表。每个数据节点在ZooKeeper中被称为znode,它是ZooKeeper中数据的最小单元。并且,每个znode都一个唯一的路径标识。强调一句:ZooKeeper主要是用来协调服务的,而不是用来存储业务数据的,所以不要放比较大的数据在znode上,ZooKeeper给出的上限是每个结点的数据大小最大是1M。ZooKeeper节点路径标识方式和Unix文件系统路径非常相似,都是由一系列使用斜杠”/“进行分割的路径表示,开发人员可以向这个节点中写入数据,也可以在节点下面创建子节点。
znode(数据节点)
介绍了ZooKeeper树形数据模型之后,我们知道每个数据节点在ZooKeeper中被称为znode,它是ZooKeeper中数据的最小单元。你要存放的数据就放在上面,是你使用ZooKeeper过程中经常需要接触到的一个概念。
znode4种类型
我们通常是将znode分为4大类:
- 持久(PERSISTENT)节点:一旦创建就一直存在即使ZooKeeper集群宕机,直到将其删除。
- 临时(EPHEMERAL)节点:临时节点的生命周期是与客户端会话(session)绑定的,会话消失则节点消失。并且,临时节点只能做叶子节点,不能创建子节点。
- 持久顺序(PERSISTENT_SEQUENTIAL)节点:除了具有持久(PERSISTENT)节点的特性之外,子节点的名称还具有顺序性。比如
/node1/app0000000001
、/node1/app0000000002
。 - 临时顺序(EPHEMERAL_SEQUENTIAL)节点:除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性。
znode数据结构
每个znode由2部分组成:
- stat:状态信息
- data:节点存放的数据的具体内容
如下所示,我通过get命令来获取根目录下的dubbo节点的内容。(get命令在下面会介绍到)。
[zk: 127.0.0.1:2181(CONNECTED) 6] get /dubbo
# 该数据节点关联的数据内容为空
null
# 下面是该数据节点的一些状态信息,其实就是Stat对象的格式化输出
cZxid = 0x2
ctime = Tue Nov 27 11:05:34 CST 2018
mZxid = 0x2
mtime = Tue Nov 27 11:05:34 CST 2018
pZxid = 0x3
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1
Stat类中包含了一个数据节点的所有状态信息的字段,包括事务ID-cZxid、节点创建时间-ctime和子节点个数-numChildren等等。
znode 状态信息 | 解释 |
---|---|
cZxid | create ZXID,即该数据节点被创建时的事务id |
ctime | create time,即该节点的创建时间 |
mZxid | modified ZXID,即该节点最终一次更新时的事务id |
mtime | modified time,即该节点最后一次的更新时间 |
pZxid | 该节点的子节点列表最后一次修改时的事务id,只有子节点列表变更才会更新pZxid,子节点内容变更不会更新 |
cversion | 子节点版本号,当前节点的子节点每次变化时值增加1 |
dataVersion | 数据节点内容版本号,节点创建时为0,每更新一次节点内容(不管内容有无变化)该版本号的值增加1 |
aclVersion | 节点的ACL版本号,表示该节点ACL信息变更次数 |
ephemeralOwner | 创建该临时节点的会话的sessionId;如果当前节点为持久节点,则ephemeralOwner=0 |
dataLength | 数据节点内容长度 |
numChildren | 当前节点的子节点个数 |
版本(version)
在前面我们已经提到,对应于每个znode,ZooKeeper都会为其维护一个叫作Stat的数据结构,Stat中记录了这个znode的三个相关的版本:
- dataVersion:当前znode节点的版本号
- cversion:当前znode子节点的版本
- aclVersion:当前znode的ACL的版本。
ACL(权限控制)
ZooKeeper采用ACL(AccessControlLists)策略来进行权限控制,类似于UNIX文件系统的权限控制。对于znode操作的权限,ZooKeeper提供了以下5种:
- CREATE:能创建子节点
- READ:能获取节点数据和列出其子节点
- WRITE:能设置/更新节点数据
- DELETE:能删除子节点
- ADMIN:能设置节点ACL的权限
其中尤其需要注意的是,CREATE和DELETE这两种权限都是针对子节点的权限控制。对于身份认证,提供了以下几种方式:
- world:默认方式,所有用户都可无条件访问。
- auth:不使用任何id,代表任何已认证的用户。
- digest:用户名:密码认证方式:username:password。
- ip:对指定ip进行限制。
Watcher(事件监听器)
Watcher(事件监听器),是ZooKeeper中的一个很重要的特性。ZooKeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper服务端会将事件通知到感兴趣的客户端上去,该机制是ZooKeeper实现分布式协调服务的重要特性。
会话(Session)
Session可以看作是ZooKeeper服务器与客户端的之间的一个TCP长连接,通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向ZooKeeper服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的Watcher事件通知。Session有一个属性叫做:sessionTimeout,sessionTimeout代表会话的超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,只要在sessionTimeout规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。另外,在为客户端创建会话之前,服务端首先会为每个客户端都分配一个sessionID。由于sessionID是ZooKeeper会话的一个重要标识,许多与会话相关的运行机制都是基于这个sessionID的,因此,无论是哪台服务器为客户端分配的sessionID,都务必保证全局唯一。
ZooKeeper集群
为了保证高可用,最好是以集群形态来部署ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么ZooKeeper本身仍然是可用的。通常3台服务器就可以构成一个ZooKeeper集群了。ZooKeeper官方提供的架构图就是一个ZooKeeper集群整体对外提供服务。每一个Server代表一个安装ZooKeeper服务的服务器。组成ZooKeeper服务的服务器都会在内存中维护当前的服务器状态,并且每台服务器之间都互相保持着通信。集群间通过ZAB协议(ZooKeeperAtomicBroadcast)来保持数据的一致性。最典型集群模式:Master/Slave模式(主备模式)。在这种模式中,通常Master服务器作为主服务器提供写服务,其他的Slave服务器从服务器通过异步复制的方式获取Master服务器最新的数据提供读服务。
ZooKeeper集群角色
但是,在ZooKeeper中没有选择传统的Master/Slave概念,而是引入了Leader、Follower和Observer三种角色。ZooKeeper集群中的所有机器通过一个Leader选举过程来选定一台称为“Leader”的机器,Leader既可以为客户端提供写服务又能提供读服务。除了Leader外,Follower和Observer都只能提供读服务。Follower和Observer唯一的区别在于Observer机器不参与Leader的选举过程,也不参与写操作的“过半写成功”策略,因此Observer机器可以在不影响写性能的情况下提升集群的读性能。
角色 | 说明 |
---|---|
Leader | 为客户端提供读和写的服务,负责投票的发起和决议,更新系统状态。 |
Follower | 为客户端提供读服务,如果是写服务则转发给Leader。参与选举过程中的投票。 |
Observer | 为客户端提供读服务,如果是写服务则转发给Leader。不参与选举过程中的投票,也不参与“过半写成功”策略。在不影响写性能的情况下提升集群的读性能。此角色于ZooKeeper3.3系列新增的角色。 |
ZooKeeper集群Leader选举过程
当Leader服务器出现网络中断、崩溃退出与重启等异常情况时,就会进入Leader选举过程,这个过程会选举产生新的Leader服务器。
这个过程大致是这样的:
- Leaderelection(选举阶段):节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准leader。
- Discovery(发现阶段):在这个阶段,followers跟准leader进行通信,同步followers最近接收的事务提议。
- Synchronization(同步阶段):同步阶段主要是利用leader前一阶段获得的最新提议历史,同步集群中所有的副本。同步完成之后准leader才会成为真正的leader。
- Broadcast(广播阶段):到了这个阶段,ZooKeeper集群才能正式对外提供事务服务,并且leader可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。
ZooKeeper集群中的服务器状态
- LOOKING:寻找Leader。
- LEADING:Leader状态,对应的节点为Leader。
- FOLLOWING:Follower状态,对应的节点为Follower。
- OBSERVING:Observer状态,对应节点为Observer,该节点不参与Leader选举。
ZooKeeper集群为啥最好奇数台?
ZooKeeper集群在宕掉几个ZooKeeper服务器之后,如果剩下的ZooKeeper服务器个数大于宕掉的个数的话整个ZooKeeper才依然可用。假如我们的集群中有n台ZooKeeper服务器,那么也就是剩下的服务数必须大于n/2。先说一下结论,2n和2n-1的容忍度是一样的,都是n-1,大家可以先自己仔细想一想,这应该是一个很简单的数学问题了。比如假如我们有3台,那么最大允许宕掉1台ZooKeeper服务器,如果我们有4台的的时候也同样只允许宕掉1台。假如我们有5台,那么最大允许宕掉2台ZooKeeper服务器,如果我们有6台的的时候也同样只允许宕掉2台。综上,何必增加那一个不必要的ZooKeeper呢?
ZooKeeper选举的过半机制防止脑裂
何为集群脑裂?
对于一个集群,通常多台机器会部署在不同机房,来提高这个集群的可用性。保证可用性的同时,会发生一种机房间网络线路故障,导致机房间网络不通,而集群被割裂成几个小集群。这时候子集群各自选主导致“脑裂”的情况。举例说明:比如现在有一个由6台服务器所组成的一个集群,部署在了2个机房,每个机房3台。正常情况下只有1个leader,但是当两个机房中间网络断开的时候,每个机房的3台服务器都会认为另一个机房的3台服务器下线,而选出自己的leader并对外提供服务。若没有过半机制,当网络恢复的时候会发现有2个leader。仿佛是1个大脑(leader)分散成了2个大脑,这就发生了脑裂现象。脑裂期间2个大脑都可能对外提供了服务,这将会带来数据一致性等问题。
过半机制是如何防止脑裂现象产生的?
ZooKeeper的过半机制导致不可能产生2个leader,因为少于等于一半是不可能产生leader的,这就使得不论机房的机器如何分配都不可能发生脑裂。
ZAB协议和Paxos算法
Paxos算法应该可以说是ZooKeeper的灵魂了。但是,ZooKeeper并没有完全采用Paxos算法,而是使用ZAB协议作为其保证数据一致性的核心算法。另外,在ZooKeeper的官方文档中也指出,ZAB协议并不像Paxos算法那样,是一种通用的分布式一致性算法,它是一种特别为Zookeeper设计的崩溃可恢复的原子消息广播算法。
ZAB协议介绍
ZAB(ZooKeeperAtomicBroadcast原子广播)协议是为分布式协调服务ZooKeeper专门设计的一种支持崩溃恢复的原子广播协议。在ZooKeeper中,主要依赖ZAB协议来实现分布式数据一致性,基于该协议,ZooKeeper实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。
ZAB协议两种基本的模式:崩溃恢复和消息广播
ZAB协议包括两种基本的模式,分别是
- 崩溃恢复:当整个服务框架在启动过程中,或是当Leader服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB协议就会进入恢复模式并选举产生新的Leader服务器。当选举产生了新的Leader服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步之后,ZAB协议就会退出恢复模式。其中,所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和Leader服务器的数据状态保持一致。
- 消息广播:当集群中已经有过半的Follower服务器完成了和Leader服务器的状态同步,那么整个服务框架就可以进入消息广播模式了。当一台同样遵守ZAB协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个Leader服务器在负责进行消息广播,那么新加入的服务器就会自觉地进入数据恢复模式:找到Leader所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。
关于ZAB协议&Paxos算法需要讲和理解的东西太多了,具体可以看下面这两篇文章:
图解Paxos一致性协议
ZookeeperZAB协议分析
Zookeeper基本原理
- ZooKeeper分为服务器端(Server)和客户端(Client),客户端可以连接到整个ZooKeeper服务的任意服务器上(除非leaderServes参数被显式设置,leader不允许接受客户端连接)。
- 客户端使用并维护一个TCP连接,通过这个连接发送请求、接受响应、获取观察的事件以及发送心跳。如果这个TCP连接中断,客户端将自动尝试连接到另外的ZooKeeper服务器。客户端第一次连接到ZooKeeper服务时,接受这个连接的ZooKeeper服务器会为这个客户端建立一个会话。当这个客户端连接到另外的服务器时,这个会话会被新的服务器重新建立。
- 上图中每一个Server代表一个安装Zookeeper服务的机器,即是整个提供Zookeeper服务的集群(或者是由伪集群组成);
- 组成ZooKeeper服务的服务器必须彼此了解。它们维护一个内存中的状态图像,以及持久存储中的事务日志和快照,只要大多数服务器可用,ZooKeeper服务就可用;
- ZooKeeper启动时,将从实例中选举一个leader,Leader负责处理数据更新等操作,一个更新操作成功的标志是当且仅当大多数Server在内存中成功修改数据。每个Server在内存中存储了一份数据。
- Zookeeper是可以集群复制的,集群间通过Zab协议(Zookeeper Atomic Broadcast)来保持数据的一致性;
- Zab协议包含两个阶段:leader election阶段和Atomic Brodcast阶段。
- 集群中将选举出一个leader,其他的机器则称为follower,所有的写操作都被传送给leader,并通过brodcast将所有的更新告诉给follower。
- 当leader崩溃或者leader失去大多数的follower时,需要重新选举出一个新的leader,让所有的服务器都恢复到一个正确的状态。
- 当leader被选举出来,且大多数服务器完成了和leader的状态同步后,leadder election的过程就结束了,就将会进入到Atomic brodcast的过程。
- Atomic Brodcast同步leader和follower之间的信息,保证leader和follower具有形同的系统状态。
Zookeeper角色
启动Zookeeper服务器集群环境后,多个Zookeeper服务器在工作前会选举出一个Leader。选举出leader前,所有server不区分角色,都需要平等参与投票(obServer除外,不参与投票);选主过程完成后,存在以下几种角色
- 领导者(leader):领导者负责进行投票的发起和决议,更新系统状态。
- 学习者(Learner)或跟随者(Follower):Follower用于接收客户请求并向客户端返回结果,在选主过程中参与投票。Follower可以接收client请求,如果是写请求将转发给leader来更新系统状态。
- 观察者(ObServer):ObServer可以接收客户端连接,将写请求转发给leader节点。但Observer不参加投票过程,只同步leader的状态,ObServer的目的是为了扩展系统,提高德取谏度。
为什么需要server?
- ZooKeeper需保证高可用和强一致性;
- 为了支持更多的客户端,需要增加更多的Server;
- Follower增多会导致投票阶段延迟增大,影响性能
在Zookeeper中ObServer起到什么作用?
- ObServer不参与投票过程,只同步leader的状态
- Observers接受客户端的连接,并将写请求转发给leader节点
- 加入更多ObServer节点,提高伸缩性,同时还不影响吞吐率
为什么在Zookeeper中Server数目一般为奇数?
我们知道在Zookeeper中Leader选举算法采用了Zab协议。Zab核心思想是当多数Server写成功,则任务数据写成功。
①如果有3个Server,则最多允许1个Server挂掉。
②如果有4个Server,则同样最多允许1个Server挂掉。既然3个或者4个Server,同样最多允许1个Server挂掉,那么它们的可靠性是一样的,所以选择奇数个ZooKeeper Server即可,这里选择3个Server。
ZooKeeper的写数据流程
- Client向ZooKeeper的Server1上写数据,发送一个写请求。
- 如果Server1不是Leader,那么Server1会把接受到的请求进一步转发给Leader,因为每个ZooKeeper的Server里面有一个是Leader。这个Leader会将写请求广播给各个Server,比如Server1和Server2,各个Server写成功后就会通知Leader。
- 当Leader收到大多数Server数据写成功了,那么就说明数据写成功了。如果这里三个节点的话,只要有两个节点数据写成功了,那么就认为数据写成功了。写成功之后,Leader会告诉Server1数据写成功了。
- Server1会进一步通知Client数据写成功了,这时就认为整个写操作成功。
总结
- ZooKeeper本身就是一个分布式程序(只要半数以上节点存活,ZooKeeper就能正常服务)。
- 为了保证高可用,最好是以集群形态来部署ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么ZooKeeper本身仍然是可用的。
- ZooKeeper将数据保存在内存中,这也就保证了高吞吐量和低延迟(但是内存限制了能够存储的容量不太大,此限制也是保持znode中存储的数据量较小的进一步原因)。
- ZooKeeper是高性能的。在“读”多于“写”的应用程序中尤其地明显,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景。)
- ZooKeeper有临时节点的概念。当创建临时节点的客户端会话一直保持活动,瞬时节点就一直存在。而当会话终结时,瞬时节点被删除。持久节点是指一旦这个znode被创建了,除非主动进行znode的移除操作,否则这个znode将一直保存在ZooKeeper上。
- ZooKeeper底层其实只提供了两个功能:①管理(存储、读取)用户程序提交的数据;②为用户程序提供数据节点监听服务。
Zookeeper相关概念进阶
一致性问题
设计一个分布式系统必定会遇到一个问题,因为分区容忍性(partition tolerance)的存在,就必定要求我们需要在系统可用性(availability)和数据一致性(consistency)中做出权衡。这就是著名的CAP定理。理解起来其实很简单,比如说把一个班级作为整个系统,而学生是系统中的一个个独立的子系统。这个时候班里的小红小明偷偷谈恋爱被班里的大嘴巴小花发现了,小花欣喜若狂告诉了周围的人,然后小红小明谈恋爱的消息在班级里传播起来了。当在消息的传播(散布)过程中,你抓到一个同学问他们的情况,如果回答你不知道,那么说明整个班级系统出现了数据不一致的问题(因为小花已经知道这个消息了)。而如果他直接不回答你,因为整个班级有消息在进行传播(为了保证一致性,需要所有人都知道才可提供服务),这个时候就出现了系统的可用性问题。而上述前者就是Eureka的处理方式,它保证了AP(可用性),后者就是我们今天所要讲的ZooKeeper的处理方式,它保证了CP(数据一致性)。
一致性协议和算法
而为了解决数据一致性问题,在科学家和程序员的不断探索中,就出现了很多的一致性协议和算法。比如2PC(两阶段提交),3PC(三阶段提交),Paxos算法等等。这时候请你思考一个问题,同学之间如果采用传纸条的方式去传播消息,那么就会出现一个问题——我咋知道我的小纸条有没有传到我想要传递的那个人手中呢?万一被哪个小家伙给劫持篡改了呢?这个时候就引申出一个概念——拜占庭将军问题。它意指在不可靠信道上试图通过消息传递的方式达到一致性是不可能的,所以所有的一致性算法的必要前提就是安全可靠的消息通道。而为什么要去解决数据一致性的问题?你想想,如果一个秒杀系统将服务拆分成了下订单和加积分服务,这两个服务部署在不同的机器上了,万一在消息的传播过程中积分系统宕机了,总不能你这边下了订单却没加积分吧?你总得保证两边的数据需要一致吧?
2PC(两阶段提交)
两阶段提交是一种保证分布式系统数据一致性的协议,现在很多数据库都是采用的两阶段提交协议来完成分布式事务的处理。在介绍2PC之前,我们先来想想分布式事务到底有什么问题呢?还拿秒杀系统的下订单和加积分两个系统来举例吧,我们此时下完订单会发个消息给积分系统告诉它下面该增加积分了。如果我们仅仅是发送一个消息也不收回复,那么我们的订单系统怎么能知道积分系统的收到消息的情况呢?如果我们增加一个收回复的过程,那么当积分系统收到消息后返回给订单系统一个Response,但在中间出现了网络波动,那个回复消息没有发送成功,订单系统是不是以为积分系统消息接收失败了?它是不是会回滚事务?但此时积分系统是成功收到消息的,它就会去处理消息然后给用户增加积分,这个时候就会出现积分加了但是订单没下成功。所以我们所需要解决的是在分布式系统中,整个调用链中,我们所有服务的数据处理要么都成功要么都失败,即所有服务的原子性问题。
在两阶段提交中,主要涉及到两个角色,分别是协调者和参与者。
第一阶段:当要执行一个分布式事务的时候,事务发起者首先向协调者发起事务请求,然后协调者会给所有参与者发送prepare请求(其中包括事务内容)告诉参与者你们需要执行事务了,如果能执行我发的事务内容那么就先执行但不提交,执行后请给我回复。然后参与者收到prepare消息后,他们会开始执行事务(但不提交),并将Undo和Redo信息记入事务日志中,之后参与者就向协调者反馈是否准备好了。
第二阶段:第二阶段主要是协调者根据参与者反馈的情况来决定接下来是否可以进行事务的提交操作,即提交事务或者回滚事务。比如这个时候所有的参与者都返回了准备好了的消息,这个时候就进行事务的提交,协调者此时会给所有的参与者发送Commit请求,当参与者收到Commit请求的时候会执行前面执行的事务的提交操作,提交完毕之后将给协调者发送提交成功的响应。而如果在第一阶段并不是所有参与者都返回了准备好了的消息,那么此时协调者将会给所有参与者发送回滚事务的rollback请求,参与者收到之后将会回滚它在第一阶段所做的事务处理,然后再将处理情况返回给协调者,最终协调者收到响应后便给事务发起者返回处理失败的结果。
个人觉得2PC实现得还是比较鸡肋的,因为事实上它只解决了各个事务的原子性问题,随之也带来了很多的问题。
- 单点故障问题,如果协调者挂了那么整个系统都处于不可用的状态了。
- 阻塞问题,即当协调者发送prepare请求,参与者收到之后如果能处理那么它将会进行事务的处理但并不提交,这个时候会一直占用着资源不释放,如果此时协调者挂了,那么这些资源都不会再释放了,这会极大影响性能。
- 数据不一致问题,比如当第二阶段,协调者只发送了一部分的commit请求就挂了,那么也就意味着,收到消息的参与者会进行事务的提交,而后面没收到的则不会进行事务提交,那么这时候就会产生数据不一致性问题。
3PC(三阶段提交)
因为2PC存在的一系列问题,比如单点,容错机制缺陷等等,从而产生了3PC(三阶段提交)。那么这三阶段又分别是什么呢?
- CanCommit阶段:协调者向所有参与者发送CanCommit请求,参与者收到请求后会根据自身情况查看是否能执行事务,如果可以则返回YES响应并进入预备状态,否则返回NO。
- PreCommit阶段:协调者根据参与者返回的响应来决定是否可以进行下面的PreCommit操作。如果上面参与者返回的都是YES,那么协调者将向所有参与者发送PreCommit预提交请求,参与者收到预提交请求后,会进行事务的执行操作,并将Undo和Redo信息写入事务日志中,最后如果参与者顺利执行了事务则给协调者返回成功的响应。如果在第一阶段协调者收到了任何一个NO的信息,或者在一定时间内并没有收到全部的参与者的响应,那么就会中断事务,它会向所有参与者发送中断请求(abort),参与者收到中断请求之后会立即中断事务,或者在一定时间内没有收到协调者的请求,它也会中断事务。
- DoCommit阶段:这个阶段其实和2PC的第二阶段差不多,如果协调者收到了所有参与者在PreCommit阶段的YES响应,那么协调者将会给所有参与者发送DoCommit请求,参与者收到DoCommit请求后则会进行事务的提交工作,完成后则会给协调者返回响应,协调者收到所有参与者返回的事务提交成功的响应之后则完成事务。若协调者在PreCommit阶段收到了任何一个NO或者在一定时间内没有收到所有参与者的响应,那么就会进行中断请求的发送,参与者收到中断请求后则会通过上面记录的回滚日志来进行事务的回滚操作,并向协调者反馈回滚状况,协调者收到参与者返回的消息后,中断事务。
这里是3PC在成功的环境下的流程图,你可以看到3PC在很多地方进行了超时中断的处理,比如协调者在指定时间内为收到全部的确认消息则进行事务中断的处理,这样能减少同步阻塞的时间。还有需要注意的是,3PC在DoCommit阶段参与者如未收到协调者发送的提交事务的请求,它会在一定时间内进行事务的提交。为什么这么做呢?是因为这个时候我们肯定保证了在第一阶段所有的协调者全部返回了可以执行事务的响应,这个时候我们有理由相信其他系统都能进行事务的执行和提交,所以不管协调者有没有发消息给参与者,进入第三阶段参与者都会进行事务的提交操作。
总之,3PC通过一系列的超时机制很好的缓解了阻塞问题,但是最重要的一致性并没有得到根本的解决,比如在PreCommit阶段,当一个参与者收到了请求之后其他参与者和协调者挂了或者出现了网络分区,这个时候收到消息的参与者都会进行事务提交,这就会出现数据不一致性问题。所以,要解决一致性问题还需要靠Paxos算法⭐️⭐️⭐️。
Paxos算法
Paxos算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一,其解决的问题就是在分布式系统中如何就某个值(决议)达成一致。在Paxos中主要有三个角色,分别为Proposer提案者、Acceptor表决者、Learner学习者。Paxos算法和2PC一样,也有两个阶段,分别为Prepare和accept阶段。
prepare阶段
- Proposer提案者:负责提出proposal,每个提案者在提出提案时都会首先获取到一个具有全局唯一性的、递增的提案编号N,即在整个集群中是唯一的编号N,然后将该编号赋予其要提出的提案,在第一阶段是只将提案编号发送给所有的表决者。
- Acceptor表决者:每个表决者在accept某提案后,会将该提案编号N记录在本地,这样每个表决者中保存的已经被accept的提案中会存在一个编号最大的提案,其编号假设为maxN。每个表决者仅会accept编号大于自己本地maxN的提案,在批准提案时表决者会将以前接受过的最大编号的提案作为响应反馈给Proposer。
accept阶段
当一个提案被Proposer提出后,如果Proposer收到了超过半数的Acceptor的批准(Proposer本身同意),那么此时Proposer会给所有的Acceptor发送真正的提案(你可以理解为第一阶段为试探),这个时候Proposer就会发送提案的内容和提案编号。表决者收到提案请求后会再次比较本身已经批准过的最大提案编号和该提案编号,如果该提案编号大于等于已经批准过的最大提案编号,那么就accept该提案(此时执行提案内容但不提交),随后将情况返回给Proposer。如果不满足则不回应或者返回NO。
当Proposer收到超过半数的accept,那么它这个时候会向所有的acceptor发送提案的提交请求。需要注意的是,因为上述仅仅是超过半数的acceptor批准执行了该提案内容,其他没有批准的并没有执行该提案内容,所以这个时候需要向未批准的acceptor发送提案内容和提案编号并让它无条件执行和提交,而对于前面已经批准过该提案的acceptor来说仅仅需要发送该提案的编号,让acceptor执行提交就行了。
而如果Proposer如果没有收到超过半数的accept那么它将会将递增该Proposal的编号,然后重新进入Prepare阶段。
paxos算法的死循环问题
其实就有点类似于两个人吵架,小明说我是对的,小红说我才是对的,两个人据理力争的谁也不让谁🤬🤬。比如说,此时提案者P1提出一个方案M1,完成了Prepare阶段的工作,这个时候acceptor则批准了M1,但是此时提案者P2同时也提出了一个方案M2,它也完成了Prepare阶段的工作。然后P1的方案已经不能在第二阶段被批准了(因为acceptor已经批准了比M1更大的M2),所以P1自增方案变为M3重新进入Prepare阶段,然后acceptor,又批准了新的M3方案,它又不能批准M2了,这个时候M2又自增进入Prepare阶段。。。就这样无休无止的永远提案下去,这就是paxos算法的死循环问题。那么如何解决呢?很简单,人多了容易吵架,我现在就允许一个能提案就行了。
引出ZAB
Zookeeper架构
作为一个优秀高效且可靠的分布式协调框架,ZooKeeper在解决分布式数据一致性问题时并没有直接使用Paxos,而是专门定制了一致性协议叫做ZAB(ZooKeeperAtomicBroadcast)原子广播协议,该协议能够很好地支持崩溃恢复。
ZAB中的三个角色
和介绍Paxos一样,在介绍ZAB协议之前,我们首先来了解一下在ZAB中三个主要的角色,Leader领导者、Follower跟随者、Observer观察者。
- Leader:集群中唯一的写请求处理者,能够发起投票(投票也是为了进行写请求)。
- Follower:能够接收客户端的请求,如果是读请求则可以自己处理,如果是写请求则要转发给Leader。在选举过程中会参与投票,有选举权和被选举权。
- Observer:就是没有选举权和被选举权的Follower。
在ZAB协议中对zkServer(即上面我们说的三个角色的总称)还有两种模式的定义,分别是消息广播和崩溃恢复。
消息广播模式
说白了就是ZAB协议是如何处理写请求的,上面我们不是说只有Leader能处理写请求嘛?那么我们的Follower和Observer是不是也需要同步更新数据呢?总不能数据只在Leader中更新了,其他角色都没有得到更新吧?不就是在整个集群中保持数据的一致性嘛?如果是你,你会怎么做呢?废话,第一步肯定需要Leader将写请求广播出去呀,让Leader问问Followers是否同意更新,如果超过半数以上的同意那么就进行Follower和Observer的更新(和Paxos一样)。当然这么说有点虚,画张图理解一下。
嗯。。。看起来很简单,貌似懂了🤥🤥🤥。这两个Queue哪冒出来的?答案是ZAB需要让Follower和Observer保证顺序性。何为顺序性,比如我现在有一个写请求A,此时Leader将请求A广播出去,因为只需要半数同意就行,所以可能这个时候有一个FollowerF1因为网络原因没有收到,而Leader又广播了一个请求B,因为网络原因,F1竟然先收到了请求B然后才收到了请求A,这个时候请求处理的顺序不同就会导致数据的不同,从而产生数据不一致问题。所以在Leader这端,它为每个其他的zkServer准备了一个队列,采用先进先出的方式发送消息。由于协议是通过TCP来进行网络通信的,保证了消息的发送顺序性,接受顺序性也得到了保证。除此之外,在ZAB中还定义了一个全局单调递增的事务IDZXID,它是一个64位long型,其中高32位表示epoch年代,低32位表示事务id。epoch是会根据Leader的变化而变化的,当一个Leader挂了,新的Leader上位的时候,年代(epoch)就变了。而低32位可以简单理解为递增的事务id。定义这个的原因也是为了顺序性,每个proposal在Leader中生成后需要通过其ZXID来进行排序,才能得到处理。
崩溃恢复模式
说到崩溃恢复我们首先要提到ZAB中的Leader选举算法,当系统出现崩溃影响最大应该是Leader的崩溃,因为我们只有一个Leader,所以当Leader出现问题的时候我们势必需要重新选举Leader。Leader选举可以分为两个不同的阶段,第一个是我们提到的Leader宕机需要重新选举,第二则是当Zookeeper启动时需要进行系统的Leader初始化选举。下面我先来介绍一下ZAB是如何进行初始化选举的。假设我们集群中有3台机器,那也就意味着我们需要两台以上同意(超过半数)。比如这个时候我们启动了server1,它会首先投票给自己,投票内容为服务器的myid和ZXID,因为初始化所以ZXID都为0,此时server1发出的投票为(1,0)。但此时server1的投票仅为1,所以不能作为Leader,此时还在选举阶段所以整个集群处于Looking状态。接着server2启动了,它首先也会将投票选给自己(2,0),并将投票信息广播出去(server1也会,只是它那时没有其他的服务器了),server1在收到server2的投票信息后会将投票信息与自己的作比较。首先它会比较ZXID,ZXID大的优先为Leader,如果相同则比较myid,myid大的优先作为Leader。所以此时server1发现server2更适合做Leader,它就会将自己的投票信息更改为(2,0)然后再广播出去,之后server2收到之后发现和自己的一样无需做更改,并且自己的投票已经超过半数,则确定server2为Leader,server1也会将自己服务器设置为Following变为Follower。整个服务器就从Looking变为了正常状态。当server3启动发现集群没有处于Looking状态时,它会直接以Follower的身份加入集群。还是前面三个server的例子,如果在整个集群运行的过程中server2挂了,那么整个集群会如何重新选举Leader呢?其实和初始化选举差不多。首先毫无疑问的是剩下的两个Follower会将自己的状态从Following变为Looking状态,然后每个server会向初始化投票一样首先给自己投票(这不过这里的zxid可能不是0了,这里为了方便随便取个数字)。假设server1给自己投票为(1,99),然后广播给其他server,server3首先也会给自己投票(3,95),然后也广播给其他server。server1和server3此时会收到彼此的投票信息,和一开始选举一样,他们也会比较自己的投票和收到的投票(zxid大的优先,如果相同那么就myid大的优先)。这个时候server1收到了server3的投票发现没自己的合适故不变,server3收到server1的投票结果后发现比自己的合适于是更改投票为(1,99)然后广播出去,最后server1收到了发现自己的投票已经超过半数就把自己设为Leader,server3也随之变为Follower。
请注意ZooKeeper为什么要设置奇数个结点?比如这里我们是三个,挂了一个我们还能正常工作,挂了两个我们就不能正常工作了(已经没有超过半数的节点数了,所以无法进行投票等操作了)。而假设我们现在有四个,挂了一个也能工作,但是挂了两个也不能正常工作了,这是和三个一样的,而三个比四个还少一个,带来的效益是一样的,所以Zookeeper推荐奇数个server。
那么说完了ZAB中的Leader选举方式之后我们再来了解一下崩溃恢复是什么玩意?其实主要就是当集群中有机器挂了,我们整个集群如何保证数据一致性?如果只是Follower挂了,而且挂的没超过半数的时候,因为我们一开始讲了在Leader中会维护队列,所以不用担心后面的数据没接收到导致数据不一致性。如果Leader挂了那就麻烦了,我们肯定需要先暂停服务变为Looking状态然后进行Leader的重新选举(上面我讲过了),但这个就要分为两种情况了,分别是确保已经被Leader提交的提案最终能够被所有的Follower提交和跳过那些已经被丢弃的提案。确保已经被Leader提交的提案最终能够被所有的Follower提交是什么意思呢?假设Leader(server2)发送commit请求(忘了请看上面的消息广播模式),他发送给了server3,然后要发给server1的时候突然挂了。这个时候重新选举的时候我们如果把server1作为Leader的话,那么肯定会产生数据不一致性,因为server3肯定会提交刚刚server2发送的commit请求的提案,而server1根本没收到所以会丢弃。
那怎么解决呢?聪明的同学肯定会质疑,这个时候server1已经不可能成为Leader了,因为server1和server3进行投票选举的时候会比较ZXID,而此时server3的ZXID肯定比server1的大了。(不理解可以看前面的选举算法)。那么跳过那些已经被丢弃的提案又是什么意思呢?假设Leader(server2)此时同意了提案N1,自身提交了这个事务并且要发送给所有Follower要commit的请求,却在这个时候挂了,此时肯定要重新进行Leader的选举,比如说此时选server1为Leader(这无所谓)。但是过了一会,这个挂掉的Leader又重新恢复了,此时它肯定会作为Follower的身份进入集群中,需要注意的是刚刚server2已经同意提交了提案N1,但其他server并没有收到它的commit信息,所以其他server不可能再提交这个提案N1了,这样就会出现数据不一致性问题了,所以该提案N1最终需要被抛弃掉。
Zookeeper的几个理论知识
了解了ZAB协议还不够,它仅仅是Zookeeper内部实现的一种方式,而我们如何通过Zookeeper去做一些典型的应用场景呢?比如说集群管理,分布式锁,Master选举等等。这就涉及到如何使用Zookeeper了,但在使用之前我们还需要掌握几个概念。比如Zookeeper的数据模型、会话机制、ACL、Watcher机制等等。
数据模型
zookeeper数据存储结构与标准的Unix文件系统非常相似,都是在根节点下挂很多子节点(树型)。但是zookeeper中没有文件系统中目录与文件的概念,而是使用了znode作为数据节点。znode是zookeeper中的最小数据单元,每个znode上都可以保存数据,同时还可以挂载子节点,形成一个树形化命名空间。
每个znode都有自己所属的节点类型和节点状态。其中节点类型可以分为持久节点、持久顺序节点、临时节点和临时顺序节点。
- 持久节点:一旦创建就一直存在,直到将其删除。
- 持久顺序节点:一个父节点可以为其子节点维护一个创建的先后顺序,这个顺序体现在节点名称上,是节点名称后自动添加一个由10位数字组成的数字串,从0开始计数。
- 临时节点:临时节点的生命周期是与客户端会话绑定的,会话消失则节点消失。临时节点只能做叶子节点,不能创建子节点。
- 临时顺序节点:父节点可以创建一个维持了顺序的临时节点(和前面的持久顺序性节点一样)。
节点状态中包含了很多节点的属性比如czxid、mzxid等等,在zookeeper中是使用Stat这个类来维护的。下面我列举一些属性解释。
- czxid:CreatedZXID,该数据节点被创建时的事务ID。
- mzxid:ModifiedZXID,节点最后一次被更新时的事务ID。
- ctime:CreatedTime,该节点被创建的时间。
- mtime:ModifiedTime,该节点最后一次被修改的时间。
- version:节点的版本号。
- cversion:子节点的版本号。
- aversion:节点的ACL版本号。
- ephemeralOwner:创建该节点的会话的sessionID,如果该节点为持久节点,该值为0。
- dataLength:节点数据内容的长度。
- numChildre:该节点的子节点个数,如果为临时节点为0。
- pzxid:该节点子节点列表最后一次被修改时的事务ID,注意是子节点的列表,不是内容。
会话
我想这个对于后端开发的朋友肯定不陌生,不就是session吗?只不过zk客户端和服务端是通过TCP长连接维持的会话机制,其实对于会话来说你可以理解为保持连接状态。在zookeeper中,会话还有对应的事件,比如CONNECTION_LOSS连接丢失事件、SESSION_MOVED会话转移事件、SESSION_EXPIRED会话超时失效事件。
ACL
ACL为AccessControlLists,它是一种权限控制。在zookeeper中定义了5种权限,它们分别为:
- CREATE:创建子节点的权限。
- READ:获取节点数据和子节点列表的权限。
- WRITE:更新节点数据的权限。
- DELETE:删除子节点的权限。
- ADMIN:设置节点ACL的权限。
Watcher机制
Watcher为事件监听器,是zk非常重要的一个特性,很多功能都依赖于它,它有点类似于订阅的方式,即客户端向服务端注册指定的watcher,当服务端符合了watcher的某些事件或要求则会向客户端发送事件通知,客户端收到通知后找到自己定义的Watcher然后执行相应的回调方法。
Zookeeper的几个典型应用场景
选主
还记得上面我们的所说的临时节点吗?因为Zookeeper的强一致性,能够很好地在保证在高并发的情况下保证节点创建的全局唯一性(即无法重复创建同样的节点)。利用这个特性,我们可以让多个客户端创建一个指定的节点,创建成功的就是master。但是,如果这个master挂了怎么办?你想想为什么我们要创建临时节点?还记得临时节点的生命周期吗?master挂了是不是代表会话断了?会话断了是不是意味着这个节点没了?还记得watcher吗?我们是不是可以让其他不是master的节点监听节点的状态,比如说我们监听这个临时节点的父节点,如果子节点个数变了就代表master挂了,这个时候我们触发回调函数进行重新选举,或者我们直接监听节点的状态,我们可以通过节点是否已经失去连接来判断master是否挂了等等。
总的来说,我们可以完全利用临时节点、节点状态和watcher来实现选主的功能,临时节点主要用来选举,节点状态和watcher可以用来判断master的活性和进行重新选举。
分布式锁
分布式锁的实现方式有很多种,比如Redis、数据库、zookeeper等。个人认为zookeeper在实现分布式锁这方面是非常非常简单的。上面我们已经提到过了zk在高并发的情况下保证节点创建的全局唯一性,这玩意一看就知道能干啥了。实现互斥锁呗,又因为能在分布式的情况下,所以能实现分布式锁呗。如何实现呢?这玩意其实跟选主基本一样,我们也可以利用临时节点的创建来实现。首先肯定是如何获取锁,因为创建节点的唯一性,我们可以让多个客户端同时创建一个临时节点,创建成功的就说明获取到了锁。然后没有获取到锁的客户端也像上面选主的非主节点创建一个watcher进行节点状态的监听,如果这个互斥锁被释放了(可能获取锁的客户端宕机了,或者那个客户端主动释放了锁)可以调用回调函数重新获得锁。
zk中不需要向redis那样考虑锁得不到释放的问题了,因为当客户端挂了,节点也挂了,锁也释放了。是不是很简单?
那能不能使用zookeeper同时实现共享锁和独占锁呢?答案是可以的,不过稍微有点复杂而已。还记得有序的节点吗?这个时候我规定所有创建节点必须有序,当你是读请求(要获取共享锁)的话,如果没有比自己更小的节点,或比自己小的节点都是读请求,则可以获取到读锁,然后就可以开始读了。若比自己小的节点中有写请求,则当前客户端无法获取到读锁,只能等待前面的写请求完成。如果你是写请求(获取独占锁),若没有比自己更小的节点,则表示当前客户端可以直接获取到写锁,对数据进行修改。若发现有比自己更小的节点,无论是读操作还是写操作,当前客户端都无法获取到写锁,等待所有前面的操作完成。这就很好地同时实现了共享锁和独占锁,当然还有优化的地方,比如当一个锁得到释放它会通知所有等待的客户端从而造成羊群效应。此时你可以通过让等待的节点只监听他们前面的节点。具体怎么做呢?其实也很简单,你可以让读请求监听比自己小的最后一个写请求节点,写请求只监听比自己小的最后一个节点,感兴趣的小伙伴可以自己去研究一下。
命名服务
如何给一个对象设置ID,大家可能都会想到UUID,但是UUID最大的问题就在于它太长了。那么在条件允许的情况下,我们能不能使用zookeeper来实现呢?我们之前提到过zookeeper是通过树形结构来存储数据节点的,那也就是说,对于每个节点的全路径,它必定是唯一的,我们可以使用节点的全路径作为命名方式了。而且更重要的是,路径是我们可以自己定义的,这对于我们对有些有语意的对象的ID设置可以更加便于理解。
集群管理和注册中心
看到这里是不是觉得zookeeper实在是太强大了,它能干的事情还很多呢。可能我们会有这样的需求,我们需要了解整个集群中有多少机器在工作,我们想对集群中的每台机器的运行时状态进行数据采集,对集群中机器进行上下线操作等等。而zookeeper天然支持的watcher和临时节点能很好的实现这些需求。我们可以为每条机器创建临时节点,并监控其父节点,如果子节点列表有变动(我们可能创建删除了临时节点),那么我们可以使用在其父节点绑定的watcher进行状态监控和回调。
至于注册中心也很简单,我们同样也是让服务提供者在zookeeper中创建一个临时节点并且将自己的ip、port、调用方式写入节点,当服务消费者需要进行调用的时候会通过注册中心找到相应的服务的地址列表(IP端口什么的),并缓存到本地(方便以后调用),当消费者调用服务时,不会再去请求注册中心,而是直接通过负载均衡算法从地址列表中取一个服务提供者的服务器调用服务。当服务提供者的某台服务器宕机或下线时,相应的地址会从服务提供者地址列表中移除。同时,注册中心会将新的服务地址列表发送给服务消费者的机器并缓存在消费者本机(当然你可以让消费者进行节点监听,我记得Eureka会先试错,然后再更新)。
ZooKeeper应用场景总结
统一命名服务
在分布式环境下,经常需要对应用/服务进行统一命名,便于识别不同服务。
- 类似于域名与ip之间对应关系,ip不容易记住,而域名容易记住。
- 通过名称来获取资源或服务的地址,提供者等信息。
按照层次结构组织服务/应用名称。
- 可将服务名称以及地址信息写到ZooKeeper上,客户端通过ZooKeeper获取可用服务列表类
配置管理
分布式环境下,配置文件管理和同步是一个常见问题。
- 一个集群中,所有节点的配置信息是一致的,比如Hadoop集群。
- 对配置文件修改后,希望能够快速同步到各个节点上。
配置管理可交由ZooKeeper实现。
- 可将配置信息写入ZooKeeper上的一个Znode。
- 各个节点监听这个Znode。
- 一旦Znode中的数据被修改,ZooKeeper将通知各个节点。
集群管理
分布式环境中,实时掌握每个节点的状态是必要的。
- 可根据节点实时状态做出一些调整。
可交由ZooKeeper实现。
- 可将节点信息写入ZooKeeper上的一个Znode。
- 监听这个Znode可获取它的实时状态变化。
典型应用
- Hbase中Master状态监控与选举。
分布式通知与协调
分布式环境中,经常存在一个服务需要知道它所管理的子服务的状态。
- NameNode需知道各个Datanode的状态。
- JobTracker需知道各个TaskTracker的状态。
心跳检测机制可通过ZooKeeper来实现。
信息推送可由ZooKeeper来实现,ZooKeeper相当于一个发布/订阅系统。
分布式锁
处于不同节点上不同的服务,它们可能需要顺序的访问一些资源,这里需要一把分布式的锁。
分布式锁具有以下特性:
- ZooKeeper是强一致的。比如各个节点上运行一个ZooKeeper客户端,它们同时创建相同的Znode,但是只有一个客户端创建成功。
- 实现锁的独占性。创建Znode成功的那个客户端才能得到锁,其它客户端只能等待。当前客户端用完这个锁后,会删除这个Znode,其它客户端再尝试创建Znode,获取分布式锁。
- 控制锁的时序。各个客户端在某个Znode下创建临时Znode,这个类型必须为CreateMode.EPHEMERAL_SEQUENTIAL,这样该Znode可掌握全局访问时序。
分布式队列
分布式队列分为两种:
当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达,这种是同步队列。
- 一个job由多个task组成,只有所有任务完成后,job才运行完成。
- 可为job创建一个/job目录,然后在该目录下,为每个完成的task创建一个临时的Znode,一旦临时节点数目达到task总数,则表明job运行完成。
队列按照FIFO方式进行入队和出队操作,例如实现生产者和消费者模型。
安装部署
zookeeper的安装模式有三种:
- 单机模式(stand-alone):单机单server
- 集群模式:多机多server,形成集群
- 伪集群模式:单机多个server,形成伪集群
#服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个tickTime时间就会发送一个心跳。单位为毫秒
tickTime=2000
#所有跟随者与领导者进行连接并同步的时间,如果在设定的时间内,半数以上的跟随者未能完成同步,领导者便会宣布放弃领导地位,进行另一次的领导选举,#单位为tick值的倍数
initLimit=10
#对于主节点与从节点进行同步操作时的超时时间,单位为tick值的倍数。
syncLimit=5
clientPort=2181
dataDir=/usr/local/zookeeper/data
dataLogDir=/usr/local/zookeeper/dataLog
#server.A=B:C:D:其中A是一个数字,表示这个是第几号服务器;B是这个服务器的ip地址;C表示的是这个服务器与集群中的Leader服务器交换信息的端口;D表示的是万一集群中的Leader服务器挂了,需要一个端口来重新进行选举,选出一个新的Leader,而这个端口就是用来执行选举时服务器相互通信的端口。如果是伪集群的配置方式,由于B都是一样,所以不同的Zookeeper实例通信端口号不能一样,所以要给它们分配不同的端口号。
server.1=192.168.236.128:2888:3888
server.2=192.168.236.129:2888:3888
server.3=192.168.236.130:2888:3888
启动命令
./zkServer.sh start
./zkCli.sh -server 127.0.0.1:2181
参数 | 描述 |
---|---|
dataDir | 用于存放内存数据库快照的文件夹,同时用于集群的myid文件也存在这个文件夹里 |
dataLogDir | 用于单独设置transaction log的目录,transaction log分离可以避免和普通log还有快照的竞争。 |
tickTime | 心跳时间,为了确保client—server连接存在的,以毫秒为单位,最小超时时间为两个心跳时间 |
clientPort | 客户端监听端口 |
globalOutstandingLimit | client请求队列的最大长度,防止内存溢出,默认值为1000。 |
preAllocSize | 预分配的Transaction log空间block为proAllocSize KB,默认block为64M,一般不需要更改,除非snapshot过于频繁 |
snapCount | 在snapCount个snapshot后写一次transaction log,默认值是100000 |
traceFile | 用于记录请求的log,打开会影响性能,用于debug,最好不要定义。 |
maxClientCnxns | 最大并发客户端数,用于防止DOS的,默认值是10,设置为0是不加限 |
clientPortBindAddress | 可以设置指定的client ip以及端口,不设置的话等于ANY:clientPort |
minSessionTimeout | 最小的客户端session超时时间,默认值为2个tickTime,单位是毫秒 |
maxSessionTimeout | 最大的客户端session超时时间,默认值为20个tickTime,单位是毫秒 |
electionAlg | 用于选举的实现的参数,0为以原始的基于UDP的方式协作,1为不进行用户验证的基于UDP的快速选举,2为进行用户验证的基于UDP的快速选举,3为基于TCP的快速选举,默认值为3。 |
initLimit | 多少个tickTime内,允许其他server连接并初始化数据,如果zooKeeper管理的数据较大,则应相应增大这个值 |
syncLimit | 多少个tickTime内,允许其他server连接并初始化数据,如果zooKeeper管理的数据较大,则应相应增大这个值 |
leaderServes | leader是否接受客户端连接。默认值为yes。leader负责协调更新。当更新吞吐量远高于读取吞吐量时,可以设置为不接受客户端连接,以便leader可以专注于同步协调工作。 |
server.x=ip:xxxx:xxxx | 配置集群里面的主机信息,其中server.x的x要写在myid文件中,决定当前机器的id,server.x=第一个port用于连接leader,第二个用于leader选举。如果 electionAlg为0,则不需要第二个port。hostname也可以填ip。 |
group.x=nnnnn[:nnnnn] | 分组信息,表明哪个组有哪些节点,例如group.1=1:2:3 group.2=4:5:6group.3=7:8:9。 |
weight.x=nnnnn | 权重信息,表明哪个结点的权重是多少,例如weight.1=1weight.2=1weight.3=1 |
ZooKeeper安装和使用
使用Docker安装zookeeper
a.使用Docker下载ZooKeeper
docker pull zookeeper:3.5.8
b.运行ZooKeeper
docker run -d --name zookeeper -p 2181:2181 zookeeper:3.5.8
连接ZooKeeper服务
a.进入ZooKeeper容器中
先使用docker ps
查看ZooKeeper的ContainerID,然后使用docker exec -it ContainerID /bin/bash
命令进入容器中。
b.先进入bin目录,然后通过./zkCli.sh -server 127.0.0.1:2181
命令连接ZooKeeper服务
root@eaf70fc620cb:/apache-zookeeper-3.5.8-bin# cd bin
常用命令演示
查看常用命令(help命令)
通过help
命令查看ZooKeeper常用命令
创建节点(create命令)
通过create
命令在根目录创建了node1节点,与它关联的字符串是”node1”
[zk:127.0.0.1:2181(CONNECTED) 34] create /node1 “node1”
通过create
命令在根目录创建了node1节点,与它关联的内容是数字123
[zk: 127.0.0.1:2181(CONNECTED) 1] create /node1/node1.1 123
Created /node1/node1.1
更新节点数据内容(set命令)
[zk: 127.0.0.1:2181(CONNECTED) 11] set /node1 "set node1"
获取节点的数据(get命令)
get
命令可以获取指定节点的数据内容和节点的状态,可以看出我们通过set
命令已经将节点数据内容改为”set node1”。
[zk: zookeeper(CONNECTED) 12] get -s /node1
set node1
cZxid = 0x47
ctime = Sun Jan 20 10:22:59 CST 2019
mZxid = 0x4b
mtime = Sun Jan 20 10:41:10 CST 2019
pZxid = 0x4a
cversion = 1
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 9
numChildren = 1
查看某个目录下的子节点(ls命令)
通过ls
命令查看根目录下的节点
[zk: 127.0.0.1:2181(CONNECTED) 37] ls /
[dubbo, ZooKeeper, node1]
通过ls
命令查看node1目录下的节点
[zk: 127.0.0.1:2181(CONNECTED) 5] ls /node1
[node1.1]
ZooKeeper中的ls命令和linux命令中的ls类似,这个命令将列出绝对路径path下的所有子节点信息(列出1级,并不递归)
查看节点状态(stat命令)
通过stat
命令查看节点状态
[zk: 127.0.0.1:2181(CONNECTED) 10] stat /node1
cZxid = 0x47
ctime = Sun Jan 20 10:22:59 CST 2019
mZxid = 0x47
mtime = Sun Jan 20 10:22:59 CST 2019
pZxid = 0x4a
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 11
numChildren = 1
查看节点信息和状态(ls2命令)
ls2
命令更像是ls
命令和stat
命令的结合。ls2
命令返回的信息包括2部分:
- 子节点列表
- 当前节点的stat信息。
[zk: 127.0.0.1:2181(CONNECTED) 7] ls2 /node1
[node1.1]
cZxid = 0x47
ctime = Sun Jan 20 10:22:59 CST 2019
mZxid = 0x47
mtime = Sun Jan 20 10:22:59 CST 2019
pZxid = 0x4a
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 11
numChildren = 1
删除节点(delete命令)
这个命令很简单,但是需要注意的一点是如果你要删除某一个节点,那么这个节点必须无子节点才行。
[zk: 127.0.0.1:2181(CONNECTED) 3] delete /node1/node1.1
ZooKeeperJava客户端Curator简单使用
Curator是Netflix公司开源的一套ZooKeeper Java客户端框架,相比于Zookeeper自带的客户端zookeeper来说,Curator的封装更加完善,各种API都可以比较方便地使用。Curator4.0+版本对ZooKeeper3.5.x支持比较好。开始之前,请先将下面的依赖添加进你的项目。
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
连接ZooKeeper客户端
通过CuratorFrameworkFactory创建CuratorFramework对象,然后再调用CuratorFramework对象的start()
方法即可!
private static final int BASE_SLEEP_TIME = 1000;
private static final int MAX_RETRIES = 3;
// Retry strategy. Retry 3 times, and will increase the sleep time between retries.
RetryPolicy retryPolicy = new ExponentialBackoffRetry(BASE_SLEEP_TIME, MAX_RETRIES);
CuratorFramework zkClient = CuratorFrameworkFactory.builder()
// the server to connect to (can be a server list)
.connectString("127.0.0.1:2181")
.retryPolicy(retryPolicy)
.build();
zkClient.start();
对于一些基本参数的说明:
- baseSleepTimeMs:重试之间等待的初始时间
- maxRetries:最大重试次数
- connectString:要连接的服务器列表
- retryPolicy:重试策略
数据节点的增删改查
创建节点
我们在ZooKeeper常见概念解读中介绍到,我们通常是将znode分为4大类:
- 持久(PERSISTENT)节点:一旦创建就一直存在即使ZooKeeper集群宕机,直到将其删除。
- 临时(EPHEMERAL)节点:临时节点的生命周期是与客户端会话(session)绑定的,会话消失则节点消失。并且,临时节点只能做叶子节点,不能创建子节点。
- 持久顺序(PERSISTENT_SEQUENTIAL)节点:除了具有持久(PERSISTENT)节点的特性之外,子节点的名称还具有顺序性。比如
/node1/app0000000001
、/node1/app0000000002
。 - 临时顺序(EPHEMERAL_SEQUENTIAL)节点:除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性。
你在使用的ZooKeeper的时候,会发现CreateMode
类中实际有7种znode类型,但是用的最多的还是上面介绍的4种。
a.创建持久化节点
你可以通过下面两种方式创建持久化的节点。
//注意:下面的代码会报错,下文说了具体原因
zkClient.create().forPath("/node1/00001");
zkClient.create().withMode(CreateMode.PERSISTENT).forPath("/node1/00002");
但是,你运行上面的代码会报错,这是因为的父节点node1
还未创建。你可以先创建父节点node1
,然后再执行上面的代码就不会报错了。
zkClient.create().forPath("/node1");
更推荐的方式是通过下面这行代码,creatingParentsIfNeeded方法可以保证父节点不存在的时候自动创建父节点,这是非常有用的。
zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath("/node1/00001");
b.创建临时节点
zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath("/node1/00001");
c.创建节点并指定数据内容
zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath("/node1/00001","java".getBytes());
zkClient.getData().forPath("/node1/00001");//获取节点的数据内容,获取到的是byte数组
d.检测节点是否创建成功
zkClient.checkExists().forPath("/node1/00001");//不为null的话,说明节点创建成功
删除节点
a.删除一个子节点
zkClient.delete().forPath("/node1/00001");
b.删除一个节点以及其下的所有子节点
zkClient.delete().deletingChildrenIfNeeded().forPath("/node1");
获取/更新节点数据内容
zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath("/node1/00001","java".getBytes());
zkClient.getData().forPath("/node1/00001");//获取节点的数据内容
zkClient.setData().forPath("/node1/00001","c++".getBytes());//更新节点数据内容
获取某个节点的所有子节点路径
List<String> childrenPaths = zkClient.getChildren().forPath("/node1");