zookeeper基础
摘抄自网上仅用于自己学习 https://www.jianshu.com/p/84ad63127cd1
1. Zookeeper 简介
Zookeeper 是一个开源的 分布式协调服务。设计目的是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。
zookeeper 是一个典型的分布式数据一致性的解决方案。分布式应用程序可以基于它实现诸如 数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、master 选举、分布式锁、分布式队列 等功能。
zookeeper 可以保证如下分布式一致性特性:
- 顺序一致性:从同一个客户端发起的事务请求,最终将会严格按照其发起顺序被应用到 zookeeper 中
- 原子性:所有事务请求的结果在集群中所有机器上的应用情况是一致的,也就是说要么整个集群所有机器都成功应用了某一个事务,要么都没有应用,一定不会出现集群中部分机器应用了该事务,而另外一部分没有应用的情况
- 单一视图:无论客户端连接的是哪个 zookeeper 服务器,其看到的服务端数据模型都是一致的
- 可靠性:一旦服务端成功地应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来,除非有另个事务又对其进行了改变
- 实时性:通常人们看到实时性的第一反应是,一旦一个事务被成功应用,那么客户端能够立即从服务端上读取到这个事务变更后的最新数据状态。这里需要注意的是,zookeeper 仅仅保证在一定的时间段内,客户端最终一定能够从服务端上读取到最新的数据状态。
2. zookeeper 基本概念
2.1 集群角色
在 zookeeper 中,有三种角色:
- Leader
- Follower
- Observer
一个 zookeeper 集群同一时刻只会有一个 Leader,其他都是 Follower 或者 Observer
zookeeper 配置简单,每个节点的配置文件一样,只有 myid 文件不一样。myid 文件的值必须是 zoo.cfg 中 server.{ 数值 } 的 { 数值 } 部分。myid 文件在 zoo.cfg 的 dataDir 文件夹下。
tickTime=2000 initLimit=10 syncLimit=5 dataDir=/opt/zkcluster/zookeeper3/dataDir dataLogDir=/opt/zkcluster/zookeeper3/dataLogDir clientPort=2193 server.1=10.10.130.151:2887:3887 server.2=10.10.130.151:2888:3888 server.3=10.10.130.151:2889:3889
集群中连接zookeeper客户端命令:./zookeeper1/bin/zkCli.sh -server 10.10.130.151:2191
zookeeper 默认只有 Leader 和 Follower 两种角色,没有 Observer 角色。
为了使用 Observer 模式,在任何想变成 Observer 的节点的配置文件中加入:peerType=observer 并在所有 server 的配置文件中,配置成 observer 模式的 server 一行追加 :observer,例如: server.3=10.10.130.151:2889:3889:observer
zookeeper 集群的所有机器通过一个 Leader 选举过程来选定一台被称为 Leader 的机器,Leader 服务器为客户端提供读和写服务。
Follower 和 Observer 都能提供读服务,不能提供写服务。两者唯一的区别在于,Observer 机器不参与 Leader 选举过程,也不参与写操作的 【过半写成功】策略,因此,Observer 可以在不影响写性能的情况下提升集群的读性能。
2.2 会话(Session)
Session 是指客户端会话。zookeeper 中,一个客户端连接是指客户端和 zookeeper 服务器之间的 TCP 长连接。zookeeper 对外的服务器端口默认是 2181,客户端启动时,会首先与服务器建立一个 TCP 连接,从第一次连接建立开始,客户端会话的生命周期也开始了,通过这个连接,客户端能够通过心跳检测和服务器保持有效的会话,也能够向 zookeeper 服务器发送请求并接受响应,同时还能通过该连接接收来自服务器的 watch 事件通知。Session 的 SessionTimeout 值用来设置一个客户端会话的超时时间。当由于服务器压力太大,网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,只要在 SessionTimeout 规定的时间内能够重新连接上集群中的任意一台服务器,那么值钱创建的会话仍然有效。
2.3 znode
zookeeper 会保存任务的分配、完成情况,等共享信息,怎么实现的呢?在 zookeeper 中,这些信息被保存在一个个的数据节点上,这些节点被称为 znode 。它采用了类似文件系统的层级数状结构进行管理。
节点上没有存储数据,也有着重要的含义。比如,在主从模式中,当 /master 节点没有数据时,代表分布式应用的主节点还没有被选举出来。
znode 节点存储的数据为字节数组。存储数据的格式 zookeeper 不做限制,也不提供解析,需要应用自己实现。
持久节点(persistent)和临时节点(ephemeral)
持久节点只能通过 delete 删除。临时节点在创建该节点的客户端崩溃或者关闭时,即客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。
有序节点
znode 可以被设置为有序(sequential)节点。有序 znode 节点被分配唯一一个单调递增的整数。如果创建了一个有序节点为 /workers/worker-,zookeeper 会自动分配一个序号 1,追加在名字后面,znode 名称为 /workers/worker-1。通过这种方式,可以创建唯一名称 znode,并且可以直观的看到创建的顺序。
znode 支持的操作及暴露的 API:(这里感觉有问题)
create /path data # 创建一个名为 /path 的 znode,数据为 data
delete /path # 删除名为 /path 的 znode
exists /path # 检查是否存在名为 /path 的 znode
setData /path data # 设置名为 /path 的 znode 的数据为 data
getData /path # 返回名为 /path 的 znode 的数据
getChildren /path # 返回所有 /path 节点的所有子节点列表
2.4 版本
zookeeper 的每一个 znode 上都会存储数据,对应于每个 znode,zookeeper 都会为其维护一个叫做 Stat 的数据结构,Stat 中记录了这个 znode 的三个数据版本,分别是 version(当前 znode 版本)、cversion(当前 znode 子节点版本) 和 aversion(当前 znode 的 ACL版本)。
每个 znode 都有版本号,随着每次数据变化自增。setData 和 delete,以版本号作为参数,当传入的版本号和服务器上不一致时,调用失败。当多个 zookeeper 客户端同时对一个 znode 操作时,版本将会起到作用,假设 c1,c2 同时往一个 znode 写数据,c1 先写完后,版本从 1 升为 2,但是 c2 写的时候携带版本号1,c2 会写入失败。
2.5 事务操作
在 zookeeper 中,能改变 zookeeper 服务器状态的操作称为事务操作。一般包括数据节点创建与删除、数据内容更新和客户端会话创建与失效等操作。对应于每一个事务请求,zookeeper 都会为其分配一个全局唯一的事务 ID,用 ZXID 表示,通常是一个 64 位的数字。每一个 ZXID 对应一次更新操作,从这些 ZXID 中可以间接地识别出 zookeeper 处理这些事务操作请求的全局顺序。
2.5 Watcher
Watcher(事件监听器),是 zookeeper 中一个很重要的特性。zookeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件被触发的时候,zookeeper 服务端会将事件通知到感兴趣的客户端上去。该机制是 zookeeper 实现分布式协调服务的重要特性。
2.6 ACL
zookeeper 采用 ACL(Access Control Lists)策略来进行权限控制。zookeeper 定义了如下 5 种权限
- CREATE:创建子节点的权限
- READ:获取节点数据和子节点列表的权限
- WRITE:更新节点数据的权限
- DELETE:删除子节点的权限
- ADMIN:设置节点 ACL 的权限
注意:CREATE 和 DELETE 都是针对子节点的权限控制
3. ZAB 协议
3.1 ZAB 协议概览
zookeeper 使用了一种称为 Zookeeper Atomic Broadcast(ZAB,Zookeeper 原子广播协议)的协议作为其数据一致性的核心算法。
基于 ZAB 协议,zookeeper 实现了一种主备模式(Leader、Follower)的系统架构来保持集群中各副本之间数据的一致性。
具体的,zookeeper 使用了一个单一的主进程(Leader)来接收并处理客户端的所有事务请求,并采用 ZAB 原子广播协议,将服务器数据的状态变更以事务 Proposal 的形式广播到所有的副本进程上去(Follower)。ZAB 协议的这个主备模型架构保证了同一时刻集群中只能有一个主进程来广播服务器的状态变更,因此能够很好地处理客户端大量的并发请求。另一方面,考虑到分布式环境中,顺序执行的一些状态变更,其前后会存在一定的依赖关系,有些状态变更必须依赖于比它早生成的那些状态变更。这样的依赖关系也对 ZAB 协议提出一个要求:ZAB 协议必须能够保证一个全局的变更序列被顺序应用。也就是说,ZAB 协议需要保证,如果一个状态变更已经被处理了,那么所有依赖的状态变更都应该已经被提前处理掉了。最后,考虑主进程在任何时候都有可能出现崩溃退出或者重启的现象,因此,ZAB 协议还需要做到在当前主进程出现上述异常情况的时候,依然能够正常工作。
ZAB 协议的核心是定义了对应那些会改变 zookeeper 服务器数据状态的事务请求的处理方式,即:
所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被称为 Leader 服务器,而剩下的其他服务器则称为 Follower 服务器。Leader 服务器负责将一个客户端事务请求转换成一个事务 Proposal(提案)并将该 Proposal 分发给集群中所有的 Follower 服务器。之后 Leader 服务器需要等待所有 Follower 服务器的反馈,一旦超过半数的 Follower 服务器进行了正确的反馈后,Leader 就会再次向所有的 Follower 服务器分发 Commit 消息,要求对刚才的 Proposal 进行提交。
崩溃恢复模式包括两个阶段:Leader 选举和数据同步。
当集群中有过半的 Follower 服务器和 Leader 服务器的状态同步,那么整个集群就可以进入消息广播模式了。
3.2 ZAB 协议介绍
ZAB 协议包括两种基本的模式,分别是崩溃恢复和消息广播。在整个 zookeeper 集群启动过程中,或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB 协议就会进入恢复模式,并选举产生新的 Leader 服务器。当选举产生了新的 Leader 服务器,同时集群中有过半的机器与该 Leader 服务器完成了状态同步后,ZAB 协议就会退出恢复模式。其中,状态同步是指数据同步,用来保证集群中存在过半的机器能够与 Leader 服务器的数据状态保持一致。
4. Zookeeper 典型应用场景
zookeeper 是一个高可用的分布式数据管理与协调框架。基于对 ZAB 算法的实现,该框架能够很好地保证分布式环境中数据的一致性。使得 zookeeper 成为了解决分布式一致性问题的利器。
4.1 数据发布与订阅(配置中心)
数据发布与订阅,即所谓的配置中心,顾名思义就是发布者将数据发布到 zookeeper 节点上,供订阅者进行数据订阅,进而达到动态获取数据的目的,实现配置信息的集中式管理和动态更新。
在我们平常的应用系统开发中,经常会碰到这样的需求,系统中需要使用一些通用的配置信息,例如机器列表信息、数据库配置信息等。这些全局配置信息通常具备以下 3 个特性。
- 数据量通常比较小
- 数据内容在运行时动态变化
- 集群中各机器共享,配置一致
对于这样的全局配置信息就可以发布到 zookeeper 上,让客户端(集群的机器)去订阅该消息。
发布/订阅系统一般有两种模式,分别是推(Push)和拉(Pull)模式。
- 推:服务端主动将数据更新发送给所有订阅的客户端
- 拉:客户端主动发起请求来获取最新数据,通常客户端都采用定时轮询拉取的方式
zookeeper 采用的是推拉相结合的方式。如下:
客户端先在服务端注册自己需要关注的节点,一旦该节点的数据发生变更,那么服务端就会向相应的客户端发送 watcher 事件通知,客户端接收到这个消息通知后,需要主动到服务端获取最新的数据(推拉结合)。
4.2 命名服务器(Naming Service)
命名服务也是分布式系统中比较常见的一类场景。在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址等信息。被命名的实体通常可以是集群中的机器,提供的服务,远程对象等等 —— 这些我们都可以统称他们为名字(Name)。
其中比较常见的就是一些分布式服务器框架(如 RPC、RMI)中的服务地址列表。通过在 zookeeper 里创建顺序节点,能够很容易创建一个全局唯一的路径,这个路径就可以作为一个名字。
zookeeper 的命名服务即生成全局唯一的 ID。
举个栗子。B 服务部署在六台服务器上,存在六个完全不同的 ip 地址,同时 B 服务本身提供一个 dubbo 接口对外,此时,有个 A 服务需要调用此接口, 如果提供某一台服务器的 ip,则存在该服务器宕机情况下接口不可用的情况,再切换 ip 就会影响服务的正常使用。此时,可以使用 zookeeper 作为注册中心,B 服务的六台服务器在指定 znode 下创建节点,而 A 服务调用之前,先通过指定 znode 的路径获取 B 服务的任意子节点中的 ip 信息,然后通过该 ip 访问。同时,zookeeper 动态维护这部分节点,定时利用心跳请求检查 B 服务的服务器状态,一旦发现某服务器无反馈,就删除节点,防止被 A 服务获取调用。
4.3 分布式协调/通知
zookeeper 中特有 Watcher 注册与异步通知机制,能够很好的实现分布式环境下不同机器,甚至不同系统之间的通知与协调,从而实现对数据变更的实时处理。使用方法通常是不同的客户端都对 ZK 上同一个 ZNode 进行注册,监听 ZNode 的变化(包括 ZNode 本身内容即子节点的),如果 ZNode 发生了变化,那么所有订阅的客户端都能够接收到相应的 Watcher 通知,并做出相应的处理。
zk 的分布式协调/通知,是一种通用的分布式系统机器间的通信方式。
4.4 心跳检测
机器间的心跳检测机制是指在分布式环境中,不同机器(或进程)之间需要检测到彼此是否在正常运行,例如 A 机器需要知道 B 机器是否正常运行,在传统的开发中,我们通常是通过主机之间是否可以互相 PING 通来判断,更复杂一点的话,则会通过在机器之间建立长连接,通过 TCP 连接固有的心跳检测机制来实现上层机器的心跳检测,这些都是非常常见的心跳检测方法。
ZK 实现分布式机器(进程)间的心跳检测是基于 ZK 的临时节点的特性,可以让不同的进程都在 ZK 的一个指定节点下创建临时子节点,不同的进程直接可以根据这个临时子节点来判断对应的进程是否存活。通过这种方式,检测和被检测系统并不需要直接相关联,大大减少了系统耦合。
4.5 工作进度汇报
在一个常见的任务分发系统中,通常任务被分发到不同的机器上执行后,需要实时地将自己的任务执行进度汇报给分发系统。这个时候就可以通过 ZK 来实现。在 ZK 上选择一个节点,每个任务客户端都在这个节点下面创建临时子节点,这样便可以实现两个功能:
- 通过判断临时节点是否存在来确定任务机器是否存活
- 各个任务机器会实时地将自己的任务进度写到这个临时节点上去,以便中心系统能够实时地获取到任务的执行进度
4.6 Master 选举
Master 选举可以说是 zookeeper 最典型的应用场景了。比如 HDFS 中 Active NameNode 的选举、YARN 中 Active ResourceManager 的选举 和 HBase 中 Active HMaster 的选举等。
针对 Master 选举的需求,通常情况下,我们可以选择常见的关系型数据库中的主键特性来实现:希望成为 Master 的机器都像数据库中插入一条相同主键 ID 的记录,数据库会帮我们进行主键冲突的检查,也就是说,只有一台机器能够插入成功 —— 那么,我们就认为向数据库中成功插入数据的客户端机器成为 Master。
依靠关系型数据库的主键特性确实能够很好地保证在集群中选举出唯一的一个 Master。但是,如果当前选举出的 Master 挂了,该怎么处理?怎么知道这个 Master 什么时候挂了?关系型数据库无法通知我们这个事件,但是 zookeeper 可以做到。
利用 zookeeper 的强一致性,能够很好地保证在分布式高并发情况下节点的创建一定能够保证全局唯一性,即 zookeeper 将会保证客户端无法创建一个已经存在的 ZNode。也就是说,如果同时有多个客户端请求创建同一临时节点,那么最终一定只有一个客户端请求能够创建成功。利用这个特性,就能很容易在分布式环境中进行 Master 选举了。
成功创建该节点的客户端所在的机器就成了 Master。同时,其他没有成功创建该节点的客户端,都会在该节点上注册一个子节点变更的 Watcher,用于监控当前 Master 机器是否存活,一旦发现当前的 Master 挂了,那么其他客户端将会重新进行 Master 选举。
这样就实现了 Master 的动态选举。
4.7 分布式锁
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。
分布式锁又分为排他锁和共享锁两种。
1. 排他锁
Exclusive Locks,简称 X 锁,又称写锁或独占锁。
如果事务 T1 对数据对象 O1 加上了排他锁,那么在整个加锁期间,只允许事务 T1 对 O1 进行读取和更新操作,其他任何事务都不能再对这个数据对象进行任何类型的操作(不能再对该对象加锁),直到 T1 释放了排他锁。
可以看出,排他锁的核心是如何保证当前只有一个事务获得锁,并且锁被释放后,所有正在等待获取锁的事务都能被通知到。
如何利用 zookeeper 实现排他锁?
定义锁:zookeeper 上的一个 ZNode 可以表示一个锁。例如 /exclusive_lock/lock 节点就可以被定义为一个锁。
获得锁:把 zookeeper 上的一个 ZNode 看作是一个锁,获得锁就通过创建 ZNode 的方式来实现。所有客户端都去 /exclusive_lock 节点下创建临时子节点 /exclusive_lock/lock。zookeeper 会保证在所有客户端中,最终只有一个客户端能够创建成功,那么就可以认为该客户端获得了锁。同时,所有没有获取到锁的客户端就需要到 /exclusive_lock 节点上注册一个子节点变更的 Watcher 监听,以便实时监听到 lock 节点的变更情况。
释放锁:因为 /exclusive_lock/lock 是一个临时节点,因此在以下两种情况下,都有可能释放锁。
- 当前获得锁的客户端机器发生宕机或重启,那么该临时节点就会被删除,释放锁。
- 正常执行完业务逻辑后,客户端就会主动将自己创建的临时节点删除,释放锁。
无论在什么情况下移除了 lock 节点,zookeeper 都会通知所有在 /exclusive_lock 节点上注册了节点变更 Watcher 监听的客户端。这些客户端在接收到通知后,再次重新发起分布式锁索取。
2. 共享锁
Shared Locks,简称 S 锁,又称为读锁。如果事务 T1 对数据对象 O1 加上了共享锁,那么 T1 只能对 O1 进行读操作,其他事务也能同时对 O1 加共享锁(不能是排他锁),直到 O1 上的所有共享锁都释放后 O1 才能被加排他锁。
即:可以多个事务同时获得一个对象的共享锁(同时读),有共享锁就不能加排他锁。
获取锁:
1)客户端创建一个已经存在的持久节点的临时顺序子节点,然后返回一个带序号节点名称;
2)客户端获取这个持久节点的子节点列表;
3)通过子节点列表判断自己在子节点中的顺序,如果是第一个子节点(序号最小),那么这个客户端获取到了锁;否则判断自己节点的类型,如果是读节点,那么判断是否有比自己小的写节点,若没有,则获取到了读锁,若有,则向比自己小的最后一个写节点注册一个 Watcher 监听;如果是写节点,那么向比自己小的最后一个节点注册一个 Watcher 监听;
4)等待 Watcher 通知,如果客户端创建的是读节点,则获取到了读锁;否则继续进行第二步。
释放锁:
- 因为客户端创建的节点是一个临时节点,所以当获取到锁的客户端发生了宕机,zookeeper 上的这个临时节点会被移除;
- 获取到锁的客户端,正常执行完业务逻辑后,客户端会主动将