Zookepper介绍
官方文档上这么解释zookeeper,它是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
配置管理: 我们可以将集群中的信息统一配置。它使用Zab这种一致性协议来提供配置在集群中的一致性。现在有很多开源项目使用Zookeeper来维护配置,比如在HBase中,客户端就是连接一个Zookeeper,获得必要的HBase集群的配置信息,然后才可以进一步操作。还有在开源的消息队列Kafka中,也使用Zookeeper来维护broker的信息。在Alibaba开源的SOA框架Dubbo中也广泛的使用Zookeeper管理一些配置来实现服务治理。
名字服务: 在微服务架构中,服务之间的调用都是通过服务名实现的,Zookeeper可以将服务名和ip做统一的管理。
分布式锁: Zookeeper可以实现分布式锁的功能。
集群管理: 在分布式的集群中,需要有一个中央控制节点负责存储的分配,当有新的存储进来的时候我们要根据现在集群目前的状态来分配存储节点。这个时候我们就需要动态感知到集群目前的状态。还有,比如一个分布式的SOA架构中,服务是一个集群提供的,当消费者访问某个服务时,就需要采用某种机制发现现在有哪些节点可以提供该服务(这也称之为服务发现,比如Alibaba开源的SOA框架Dubbo就采用了Zookeeper作为服务发现的底层机制)。还有开源的Kafka队列就采用了Zookeeper作为Cosnumer的上下线管理。
Zookeeper安装
1、docker pull zookeeper:3.4.11
2、docker run --privileged=true -id --name zookeeper --publish 2181:2181 -id zookeeper:latest
3、下载idea zookeeper插件,并配置 Quorum:localhost Port:2181
Zookeeper基本数据模型
Zookeeper维护一个类似文件系统的数据结构:
- zk的数据模型可以理解为linux/unix的文件目录结构:/usr/local/…
- 每一个节点都称之为znode,它可以有子节点,也可以有数据。
- 每个节点分为临时节点和永久节点,临时节点在客户端断开后消失,永久节点只能手动删除。
- 每个zk节点都有各自都版本号,可以通过命令来显示节点信息。
- 每当节点数据发生变化,那么节点的版本号会累加(类似于mysql中的版本号,想当于乐观锁)。
- 删除/修改过时节点,版本号不匹配会报错。
- 每个zk节点存储的数据不宜过大,几k即可。
- 节点可以设置权限acl(权限列表),可以通过权限来限制用户的访问。
Zookeeper简单命令
首先进入Zookeeper的docker容器内。
docker exec -it zookeeper /bin/bash
进入bin目录下,执行 ./zkCli.sh
-
ls 与 ls2
ls:查看目录下的节点。 例如:ls /zookeeper
ls2:除了能查看目录下的节点,还能返回节点的信息。例如:ls2 /zookeeper。另外 stat /zookeeper 只能返回节点信息,ls2相当于 ls + stat 命令。
-
create
create是创建节点的命令。默认创建:create /imooc imooc-data 。这样会创建一个 非顺序 持久化的节点。通过 get /imooc 能查看节点的数据和节点的信息。cversion 代表节点的版本。dataVersion 代表数据的版本。新增时它们都是0。
创建临时节点:create -e /imooc/tmp imooc-data。创建临时节点后,它的父节点的cversion就会+1。持久化节点和临时节点的区别是: ephemeralOwner。持久化节点是 ephemeralOwner=0x0,临时节点是 ephemeralOwner=0x1000e3cda420009,结构是不一样的。临时节点会在session失效时自动删除。
创建顺序节点:create -s /imooc/sec seq。执行之后会自动将节点名称后门增加一段递增都数字,多次执行后能看到一直在进行累加。
-
set
更新节点数据:set /imooc new-data。数据更新后,dataVersion会+1。
乐观锁:通过dataVersion能实现乐观锁。例如:imooc当前版本是 1。set /imooc 123 1。其中 1代表版本号,只有在版本号相同时才会更新成功。否则如果set时使用了错误的版本号,就会报错。
-
delete
delete 可以删除节点。如果不加版本号会直接删除。如果增加版本号会进行乐观锁判断,版本号相同才会删除,不同会报错。
zk特性 - watcher机制
watcher的特点
- 针对每个节点的操作,都会有一个监督者 -> watcher
- 当监控当某个对象(znode)发生了变化,则触发watcher事件
- zk中的watcher是一次性的,触发后立即销毁
- 父节点,子节点增删改都能够触发其watcher
- 针对不同类型的操作,触发watcher事件也不同,分成:1、(子)节点创建事件 2、(子)节点删除事件 3、(子)节点数据变化事件
watcher命令行
1、父节点 watcher事件
-
创建父节点触发:NodeCreated
通过 stat 可以创建watcher。例如:stat /imooc watcher。当/imooc被创建时,会触发NodeCreated事件。
-
修改父节点数据触发:NodeDataChanged
通过 get 也可以触发watcher事件。由于watcher事件是一次性的,所以我们需要再一次设置watcher。get /zookeeper/imooc watcher。当修改节点数据的时候,会触发NodeDataChanged事件。
-
删除父节点触发:NodeDeleted path:/imooc
删除节点也能触发watcher事件。
2、子节点watcher事件 -
创建子节点触发:NodeChildrenChanged
通过ls也可以创建watcher事件,当给父节点创建watcher事件后,创建子节点后,会触发NodeChildrenChanged。
-
删除子节点触发: NodeChildrenChanged
通过ls也可以创建watcher事件,当给父节点创建watcher事件后,删除子节点后,也会触发NodeChildrenChanged。
-
修改子节点不会触发任何事件
同样我们给父节点设置watcher事件,再修改子节点数据。并不会触发任何事件。
ACL(access control lists)权限控制
- 针对节点可以设置相关读写等权限,目的为了保障数据安全性。
- 权限permissions可以指定不同等权限范围以及角色。
ACL命令行
- getAcl:获取某个节点的acl权限信息。
- setAcl:设置某个节点的acl权限信息。
- addauth:输入认证授权信息,注册时输入明文密码,但在zk的系统里,密码是以加密的形式存在的。
ACL的构成
zk的acl通过[scheme: id:permissions]来构成权限列表。其中:
scheme: 代表采用的某种权限机制
id: 代表允许访问的用户
permissions:权限这字符串
scheme
- world:world下只有一个id,即只有一个用户,也就是anyone,那么组合的写法就是 world:anyone:[permissions]
- auth:代表认证登陆,需要注册用户有权限就可以,形式为 auth:user:password:[permissions]
- digest:需要对密码加密才能访问,组合形式为 digest:username:BASE64(SHA1(password)):[permissions]
- ip:当设置为ip指定的ip地址,此时限制ip进行访问,比如ip:192.168.1.1:[permissions]
- super:代表超级管理员,拥有所有的权限
permissions
权限字符串缩写 crdwa
- create:创建子节点
- read:获取节点/子节点
- write:设置节点数据
- delete:删除子节点
- admin:设置权限
world
当我们创建一个节点时,默认的权限就是 world:anyone:cdrwa,代表着所有人都可以对节点进行所有操作。
对于 world,我们只能修改permissions,例如:修改节点权限,所有人只拥有ra的权限。
auth和digest
这个两个的不同点只是digset的密码是需要加密的。
首先要注册用户: addauth digest imooc:imooc,然后给节点设置权限:setAcl /imooc auth:imooc:imooc:cdrwa
ip
设置权限ip:setAcl /imooc ip:192.168.1.1:cdrwa
super
super用户需 要修改zkServer.sh并且重启zkServer.sh。
这样我们再启动zk,就可以再命令行中,切换成super,addauth digest imooc:imooc。
四字命令:Four Letter Words
- zk可以通过它自身提供的简写命令来和服务器进行交互
- 需要使用到nc命令,安装:yum install nc
stat
查看zk的状态信息,以及是否mode(集群还是单机模式)
ruok
查看当前zkserver是否启动,返回imok。
dump
列出未经处理的会话和临时节点。
conf
查看服务器配置
cons
展示连接到服务器到客户端信息
envi
环境变量
mntr
监控zk健康信息·
wchs
展示watch的信息
zk集群搭建
-
首先docker中启动zk镜像,并进入zk容器。
-
创建zoo.cfg 文件
上面红色框住的内容即是我们修改的内容:①、tickTime:基本事件单元,这个时间是作为Zookeeper服务器之间或客户端与服务器之间维持心跳的时间间隔,每隔tickTime时间就会发送一个心跳;最小 的session过期时间为2倍tickTime
②、dataDir:存储内存中数据库快照的位置,除非另有说明,否则指向数据库更新的事务日志。注意:应该谨慎的选择日志存放的位置,使用专用的日志存储设备能够大大提高系统的性能,如果将日志存储在比较繁忙的存储设备上,那么将会很大程度上影像系统性能。
③、client:监听客户端连接的端口。
④、initLimit:允许follower连接并同步到Leader的初始化连接时间,以tickTime为单位。当初始化连接时间超过该值,则表示连接失败。
⑤、syncLimit:表示Leader与Follower之间发送消息时,请求和应答时间长度。如果follower在设置时间内不能与leader通信,那么此follower将会被丢弃。
⑥、server.A=B:C:D
A:其中 A 是一个数字,表示这个是服务器的编号;
B:是这个服务器的 ip 地址;
C:Leader选举的端口;
D:Zookeeper服务器之间的通信端口。我们需要修改的第一个是 dataDir ,在指定的位置处创建好目录。
第二个需要新增的是 server.A=B:C:D 配置,其中 A 对应下面我们即将介绍的myid 文件。B是集群的各个IP地址,C:D 是端口配置。 -
创建 myid 文件
在 上一步 dataDir 指定的目录下,创建 myid 文件。
然后在该文件添加上一步 server 配置的对应 A 数字。
比如我们上面的配置:
dataDir=/usr/local/software/zookeeper-3.3.6/data
然后下面配置是:server.0=192.168.146.200:2888:3888 server.1=192.168.146.201:2888:3888 server.2=192.168.146.202:2888:3888
那么就必须在 192.168.146.200 机器的的 /usr/local/software/zookeeper-3.3.6/data 目录下创建 myid 文件,然后在该文件中写上 0 即可。
后面的机器依次在相应目录创建myid文件,写上相应配置数字即可。
zk选举
ZooKeeper服务器角色
ZooKeeper集群是一个基于主从复制的高可用集群,每个服务器承担如下三种角色中的一种:
- Leader
一个ZooKeeper集群同一时间只会有一个实际工作的Leader,它会发起并维护与各Follwer及Observer间的心跳。所有的写操作必须要通过Leader完成再由Leader将写操作广播给其它服务器。 - Follower
一个ZooKeeper集群可能同时存在多个Follower,它会响应Leader的心跳。Follower可直接处理并返回客户端的读请求,同时会将写请求转发给Leader处理,并且负责在Leader处理写请求时对请求进行投票。 - Observer
角色与Follower类似,但是无投票权。
原子广播(ZAB)
为了保证写操作的一致性与可用性,ZooKeeper专门设计了一种名为原子广播(ZAB)的支持崩溃恢复的一致性协议。基于该协议,ZooKeeper实现了一种主从模式的系统架构来保持集群中各个副本之间的数据一致性。
根据ZAB协议,所有的写操作都必须通过Leader完成,Leader写入本地日志后再复制到所有的Follower节点。
一旦Leader节点无法工作,ZAB协议能够自动从Follower节点中重新选出一个合适的替代者,即新的Leader,该过程即为领导选举。该领导选举过程,是ZAB协议中最为重要和复杂的过程。
1. 写Leader
通过Leader进行写操作,主要分为五步:
1、客户端向Leader发起写请求。
2、Leader将写请求以Proposal的形式发给所有Follower并等待ACK。
3、Follower收到Leader的Proposal后返回ACK。
4、Leader得到过半数的ACK(Leader对自己默认有一个ACK)后向所有的Follower和Observer发送Commmit。
5、Leader将处理结果返回给客户端。
这里要注意:
1、Leader并不需要得到Observer的ACK,即Observer无投票权。
2、Leader不需要得到所有Follower的ACK,只要收到过半的ACK即可,同时Leader本身对自己有一个ACK。上图中有4个Follower,只需其中两个返回ACK即可,因为(2+1) / (4+1) > 1/2。
3、Observer虽然无投票权,但仍须同步Leader的数据从而在处理读请求时可以返回尽可能新的数据。
2.写Follower/Observer
Follower/Observer均可接受写请求,但不能直接处理,而需要将写请求转发给Leader处理。除了多了一步请求转发,其它流程与直接写Leader无任何区别。
3.读操作
Leader/Follower/Observer都可直接处理读请求,从本地内存中读取数据并返回给客户端即可。由于处理读请求不需要服务器之间的交互,Follower/Observer越多,整体可处理的读请求量越大,也即读性能越好。
选举算法
概念
-
myid
每个ZooKeeper服务器,都需要在数据文件夹下创建一个名为myid的文件,该文件包含整个ZooKeeper集群唯一的ID(整数)。例如,某ZooKeeper集群包含三台服务器,hostname分别为zoo1、zoo2和zoo3,其myid分别为1、2和3,则在配置文件中其ID与hostname必须一一对应,如下所示。在该配置文件中,server.后面的数据即为myid
server.1=zoo1:2888:3888 server.2=zoo2:2888:3888 server.3=zoo3:2888:3888
-
zxid
类似于RDBMS中的事务ID,用于标识一次更新操作的Proposal ID。为了保证顺序性,该zkid必须单调递增。因此ZooKeeper使用一个64位的数来表示,高32位是Leader的epoch,从1开始,每次选出新的Leader,epoch加一。低32位为该epoch内的序号,每 次epoch变化,都将低32位的序号重置。这样保证了zkid的全局递增性。
-
服务器状态
LOOKING 不确定Leader状态。该状态下的服务器认为当前集群中没有Leader,会发起Leader选举。
FOLLOWING 跟随者状态。表明当前服务器角色是Follower,并且它知道Leader是谁。
LEADING 领导者状态。表明当前服务器角色是Leader,它会维护与Follower间的心跳。
OBSERVING 观察者状态。表明当前服务器角色是Observer,与Folower唯一的不同在于不参与选举,也不参与集群写操作时的投票。
-
选票数据结构
每个服务器在进行领导选举时,会发送如下关键信息:
logicClock 每个服务器会维护一个自增的整数,名为logicClock,它表示这是该服务器发起的第多少轮投票
state 当前服务器的状态
self_id 当前服务器的myid
self_zxid 当前服务器上所保存的数据的最大zxid
vote_id 被推举的服务器的myid
vote_zxid 被推举的服务器上所保存的数据的最大zxid
投票流程
-
自增选举轮次
ZooKeeper规定所有有效的投票都必须在同一轮次中。每个服务器在开始新一轮投票时,会先对自己维护的logicClock进行自增操作。
-
初始化选票
每个服务器在广播自己的选票前,会将自己的投票箱清空。该投票箱记录了所收到的选票。例:服务器2投票给服务器3,服务器3投票给服务器1,则服务器1的投票箱为(2, 3), (3, 1), (1, 1)。票箱中只会记录每一投票者的最后一票,如投票者更新自己的选票,则其它服务器收到该新选票后会在自己票箱中更新该服务器的选票。
-
发送初始化选票
每个服务器最开始都是通过广播把票投给自己。
-
接收外部投票
服务器会尝试从其它服务器获取投票,并记入自己的投票箱内。如果无法获取任何外部投票,则会确认自己是否与集群中其它服务器保持着有效连接。如果是,则再次发送自己的投票;如果否,则马上与之建立连接。
-
判断选举轮次
收到外部投票后,首先会根据投票信息中所包含的logicClock来进行不同处理:
外部投票的logicClock大于自己的logicClock。说明该服务器的选举轮次落后于其它服务器的选举轮次,立即清空自己的投票箱并将自己的logicClock更新为收到的logicClock,然后再对比自己之前的投票与收到的投票以确定是否需要变更自己的投票,最终再次将自己的投票广播出去。
外部投票的logicClock小于自己的logicClock。当前服务器直接忽略该投票,继续处理下一个投票。
外部投票的logickClock与自己的相等。当时进行选票PK。
-
选票PK
选票PK是基于(self_id, self_zxid)与(vote_id, vote_zxid)的对比:
外部投票的logicClock大于自己的logicClock,则将自己的logicClock及自己的选票的logicClock变更为收到的logicClock
若logicClock一致,则对比二者的vote_zxid,若外部投票的vote_zxid比较大,则将自己的票中的vote_zxid与vote_myid更新为收到的票中的vote_zxid与vote_myid并广播出去,另外将收到的票及自己更新后的票放入自己的票箱。如果票箱内已存在(self_myid, self_zxid)相同的选票,则直接覆盖
若二者vote_zxid一致,则比较二者的vote_myid,若外部投票的vote_myid比较大,则将自己的票中的vote_myid更新为收到的票中的vote_myid并广播出去,另外将收到的票及自己更新后的票放入自己的票箱
-
统计选票
如果已经确定有过半服务器认可了自己的投票(可能是更新后的投票),则终止投票。否则继续接收其它服务器的投票。
-
更新服务器状态
投票终止后,服务器开始更新自身状态。若过半的票投给了自己,则将自己的服务器状态更新为LEADING,否则将自己的状态更新为FOLLOWING。
总结
通常那台服务器上的数据越新(ZXID会越大),其成为Leader的可能性越大,也就越能够保证数据的恢复。如果ZXID相同,则SID越大机会越大。
ZK API DEMO
会话创建与恢复
对一个单机节点进行连接。连接成功会进行watcher事件触发。
public class ZKConnect implements Watcher {
final static Logger log = LoggerFactory.getLogger(ZKConnect.class);
public static final String zkServerPath = "localhost:2181";
public static final Integer timeout = 5000;
public static void main(String[] args) throws IOException, InterruptedException {
/**
* 客户端和zk服务端链接是一个异步的过程
* 当连接成功后后,客户端会收的一个watch通知
*
* 参数:
* connectString:连接服务器的ip字符串,
* 比如: "192.168.1.1:2181,192.168.1.2:2181,192.168.1.3:2181"
* 可以是一个ip,也可以是多个ip,一个ip代表单机,多个ip代表集群
* 也可以在ip后加路径
* sessionTimeout:超时时间,心跳收不到了,那就超时
* watcher:通知事件,如果有对应的事件触发,则会收到一个通知;如果不需要,那就设置为null
* canBeReadOnly:可读,当这个物理机节点断开后,还是可以读到数据的,只是不能写,
* 此时数据被读取到的可能是旧数据,此处建议设置为false,不推荐使用
* sessionId:会话的id
* sessionPasswd:会话密码 当会话丢失后,可以依据 sessionId 和 sessionPasswd 重新获取会话
*/
ZooKeeper zk = new ZooKeeper(zkServerPath, timeout, new ZKConnect());
log.warn("客户端开始连接zookeeper服务器...");
log.warn("连接状态:{}", zk.getState());
new Thread().sleep(2000);
log.warn("连接状态:{}", zk.getState());
}
public void process(WatchedEvent event) {
log.warn("接受到watch通知:{}", event);
}
}
每次创建会话都会产生sessionId和sessionPassword,使用这两个参数,我们可以进行session的重连。
package connect;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @Title: ZKConnectDemo.java
* @Description: zookeeper 恢复之前的会话连接demo演示
*/
public class ZKConnectSessionWatcher implements Watcher {
final static Logger log = LoggerFactory.getLogger(ZKConnectSessionWatcher.class);
public static final String zkServerPath = "localhost:2181";
public static final Integer timeout = 5000;
public static void main(String[] args) throws Exception {
ZooKeeper zk = new ZooKeeper(zkServerPath, timeout, new ZKConnectSessionWatcher());
long sessionId = zk.getSessionId();
String ssid = "0x" + Long.toHexString(sessionId);
System.out.println(ssid);
byte[] sessionPassword = zk.getSessionPasswd();
log.warn("客户端开始连接zookeeper服务器...");
log.warn("连接状态:{}", zk.getState());
new Thread().sleep(1000);
log.warn("连接状态:{}", zk.getState());
new Thread().sleep(200);
// 开始会话重连
log.warn("开始会话重连...");
ZooKeeper zkSession = new ZooKeeper(zkServerPath,
timeout,
new ZKConnectSessionWatcher(),
sessionId,
sessionPassword);
log