Zookeeper
一.前言
-
redis课程:
-
之前讲过redis是单实例的,内存快,但是有单点故障,访问压力,存储容量问题
所以推出了复制集群,HA sentinel哨兵集群(不是绝对的实时同步,可能连最终一致性都谈不上),分片集群模式
也可以用它来实现分布式锁,但是会有节点单机导致死锁,和锁超时问题,需要额外创建一个线程来监控续时间
这种分布式锁方案不优,下面就介绍一下zookeeper,来完成分布式协调,分布式锁的讲解使用
二.简介
-
官网: https://zookeeper.apache.org
官网介绍:
ZooKeeper是一个分布式的、开源的分布式应用程序协调服务。它公开了一组简单的原语,分布式应用程序可以在此基础上实现更高级别的同步(临时节点)、配置维护(1M数据存储)、分组管理(path结构)和命名服务(sequence)。它的设计易于编程,并使用了一个数据模型样式后熟悉的目录树结构的文件系统。它在Java中运行,并且有Java和C的绑定 (这些高级功能需要Client代码实现) 。
众所周知,协调服务很难做好。它们特别容易出错,例如竞争条件和死锁。ZooKeeper背后的动机是从零开始减轻分布式应用程序实现协调服务的责任。
三.安装,搭建集群
-
安装
-
先安装jdk
linux jdk 安装
1.下载安装包
2.tar -zxvf jdk-8u251-linux-i586.tar.gz -C /home/soft
3.vim /etc/profile #配置环境变量
export JAVA_HOME=/home/soft/jdk1.8.0_251
export JRE_HOME= J A V A H O M E / j r e e x p o r t C L A S S P A T H = . : {JAVA_HOME}/jre export CLASSPATH=.: JAVAHOME/jreexportCLASSPATH=.:{JAVA_HOME}/lib: J R E H O M E / l i b e x p o r t P A T H = {JRE_HOME}/lib export PATH= JREHOME/libexportPATH={JAVA_HOME}/bin:$PATH
4.source /etc/profile #执行profile
5.sudo yum install glibc.i686解决版本不匹配问题
验证
java -version -
在安装zookeeper
准备4个节点 node1~node4
1.wget xxxxx
2.tar xf zookeeper.tar.gz
3.mkdir /opt/mashibing
5.mv zookeeper/opt/mashibing
6.vi /etc/profile
//添加配置环境变量
export ZOOKEEPER_HOME=/opt/mashibing/zookeeper-3.4.6
export PATH= P A T H : PATH: PATH:ZOOKEEPER_HOME/bin
7.cd zookeeper/conf
8.cp zoo.zem.cfg zoo.cfg
9 ci zoo.cfg
//修改配置文件配置项
dataDir=自己知道存放日志快照路径
//添加节点选举和同步端口节点
server.1=node01:2888:3888
server.2=node02:2888:3888
server.3=node01:2888:3888
server.4=node01:2888:3888
10.mkdir -p /var/mashibing/zk
11.echo 1 > /var/mashibing/zk/myid
12.cd /opt && scp -r ./mashibing/ node02:·pwd·
#先配置ip互通
#vi /etc/hosts
#192.168.125.60 node01
#192.168.125.61 node02
13.node02~node04 创建myid 1,2,3,4启动
14.zkServer.sh start-foreground
#前台打印日志方式启动
#默认是后台启动验证:
zkCli.sh #客户端启动
help
ls/
create /ooxx “”
create -s /abc/aaa
create -e /ooxx/xxoo
create -s -e /ooxx/xoxoget /ooxx
//查看节点的连接信息
netstat -natp | egrep ‘(2888|3888)’
-
-
选举节点连接信息
2888端口会被follower的随机端口连接,一但follower发生写操作就被通过2888端口转发到当前zookeeper进行处理,写入成功后通过2888端口分发给所有follower。后启动的zkserver会开启对3888端口的监听,并随机申请端口号连接已启动的zkserver的3888端口,这样达到的效果就是每台zk都有和其他三台zk有建立socket连接,这样每台机器就可以双向通信了。
-
zookeeper有2种运行状态
主从集群,一个主leader肯定会宕机,会导致服务不可用/数据不一致,这种情况是属于不可靠集群,但是对zookeeper的集群,是高可用的,它可以快速的恢复出一个leader
组成ZooKeeper服务的服务器必须相互了解。它们维护一个内存中的状态映像,以及一个事务日志和持久性存储中的快照。只要大多数服务器可用,ZooKeeper服务就可以使用。
客户端连接到单个ZooKeeper服务器。客户端维护一个TCP连接,通过它发送请求,获取响应,获取监视事件并发送心跳。如果与服务器的TCP连接断开,则客户端将连接到其他服务器。
主从复制:follower是leader的数据全量副本,读写操作可以发生在leader身上,读操作可以发生在任何node上(读写分离)。连接到 zookeeper 的 follower 节点的 Clien t发送 write 请求时,会被转发到 leader 上。
.
但是对于主从复制集群的第一个反应就是 leader 是一个单点,可能会发生单点故障。单点故障就会带来服务不可用的问题,服务不可用就说明 集群 不可靠,但是事实是 zookeeper 是一个 高可用集群。
.
zookeeper集群的两种状态: 1. 可用状态(leader在工作,有主状态)。2. 不可用状态(leader挂掉了,无主状态)。
两种状态的区别就是 leader 是否挂掉了。相较于不可用状态,越快恢复 可用状态 越好。
官方给出的数据:ZooKeeper只需要不到200毫秒就可以选出一个新的leader。
四.数据结构
-
zookeeper是一个目录树结构,数据是存在内存中的
- 统一配置节点
- 分组管理<- path结构
- 统一命名 <- sqquential
- 同步<- 临时节点
- 要保证对外提供协调服务的时候很快
- node可以存数据最大1mb
- 序列节点
- 每次启动或者创建数据都会消耗一次序列节点
- 持久节点
- 临时节点
- 每一个客户端连接到zookeeper一定产生一个seesion来代表这个客户端
- 如果客户端在,session就一直在,客户端挂,session就会消失
- 没有连接池的概念
- 临时节点也会消耗序列节点
- 每一个客户端连接到zookeeper一定产生一个seesion来代表这个客户端
- 序列节点
-
通过以上4点可以实现:
1. 分布式锁(client代码实现)
- 锁依托一个父节点,且具备持久s代表了父节点可以有多把锁
- 队列式事物锁,可重入锁
2. HA,选主
五.特征
- zookeeper集群有三种角色
- leader
- 领导,对外提供读写服务
- follower
- 副本,对外提供读服务,可以参与选举
- observer
- 副本,比follower级别低,对外提供读服务,不能参与选举
- leader
这样提高了选举主的效率,就像中国选主席,不需要每个人都去投票,有人大代表,当选主的速度快了,代表着从不可用切换到可用状态更快了
这样做的好处(扩展性):
就是随意增添新的zkserver,除了让从 非可用状态 快速 切换到可用状态,还能极限的放大 查询能力。
- 可靠性和快速恢复
- 其中 “攘其外” 表示对外提供数据的可靠性(数据的可靠 可用 一致性),“安其内” 则表示 服务的可靠性 (快速选leader)。简单的讲,就是服务能够在不可用状态 快速恢复 可用状态的前提下 ,并且对外提供可靠的数据。
- Paxos 分布式数据一致算法
可看这篇文件了解:
https://www.douban.com/note/208430424/?_i=0317593WV7tBkz
- ZAB
- zk对paxos算法做了一个更简单的实现:ZAB协议(原子广播协议),ZAB协议作用在zk集群可用状态。
- Client1 对着 follower1 发起写操作, create /ooxx 节点
- follower把 create /ooxx 转发给leader
- leader 生成 Zxid(事务ID),leader里会维护着对应follower数量的FIFO队列。
- (a): leader开启事务,通过所有FIFO队列里广播log给所有follower,follower1收到log后会回复给leader一个ok。follower2如果因为网络延迟没有及时给leader回复ok也没关系。因为follower1的ok + leader自身的ok 已经在集群数目中 过半。
(b): leader通过FIFO队列通知所有followers(包含follower2) create /ooxx 生效。虽然follower2没有对之前的log回复ok,但是只要follower2没有挂掉,最终能消费掉FIFO的消息,那么最终follower2的数据和其他zkserver的数据是一致的(最终一致性) - leader返回给follower1一个写入成功ok
- follower1再返回给Client1一个写入成功ok
以上涉及的要点:原子广播(原子 + 广播),FIFO队列。
原子:要么全部成功,要么要不失败,没有中间态(基于队列实现)。
广播:分布式多节点,不一定所有人都能接收到广播,超过一半就可以生效。
FIFO队列:push和pop的顺序性。如果follower收到的Zxid小于自身的Zxid,该操作会被拒绝。 - 选举过程
-
快速选举leader分两种场景:
- 集群第一次启动(没有数据、版本、历史状态)。
- 重启集群,或者leader挂掉重选leader(之前运行时产生过数据,可能有的szkser数据多,有的数据少)。
-
并且每台zkserver都有自己的myid,和Zxid(事务ID)。
那么根据上述条件,要选取出leader要满足什么条件呢?
数据最全的(Zxid最高的,一定是过半通过的)。
myid最大的。
比较规则:收到其他zkserver的数据后,都是先比较zxid,如果zxid相同,再比较myid。集群第一次启动:启动zkserver数量的达到 最大数的一半 + 1 就可以根据myid最大来选出 leader,后启动的zkserver只能追随已经选举出的leader。
重启集群或者leader挂掉了:无论谁先发现了leader挂掉了触发了投票包,投票包一定会发送到node02(zxid最大 的zkserver && myid最大) ,node02收到投票后就一定会触发自身发起投票 ,只要node02发起了投票,那么其他zkserver一定会选择投它。
-
选举算法
参考:http://t.csdn.cn/Sh41U
-
- watch监控
-
ZooKeeper支持watch的概念。客户端可以在znode上设置watch。当znode改变时,watch将被触发并移除。当一个监视被触发时,客户端会收到一个数据包,表示znode已经改变。如果客户机和ZooKeeper服务器之一之间的连接中断,客户机将收到一个本地通知。
新版本:客户端还可以在znode上设置永久的、递归的监视,这些监视在触发时不会被删除,并且会递归地触发对已注册的znode以及任何子znode的更改。
zookeeper集群可以做到统一视图,Client访问zk集群中的任何节点,都可以使用sync进行同步,所取回的数据都是一样的。
同时zookeeper还是一个目录树结构,有层次结构节点的概念。 -
有Client1和Client2两个客户端,Client2想要动态的发现Client1的服务。
就可以在zookeeper统一视图和目录树的模型的条件下,有一个/ooxx的节点,如果Client1在/ooxx节点下创建一个节点/a数据为自身IP来代表自己(节点结构为/ooxx/a),Client2连接这个zookeeper集群,一定能通过统一视图拿到/a节点的信息,就可以动态的发现Client2服务了。
但是如果Client1挂掉,Client2想要依赖Client1挂掉的事件关闭自身的服务,就需要Client1和Client2手动开启一个连接的socket发送心跳,同样也有更便捷高效的方式:向zookeeper注册watch。
发送心跳和zookeeper的watch相比区别在于方向性和时效性。
方向性:
自己手动实现心跳需要其中一个Client按一定频率发送心跳。
而watch只需要Client1在zookeeper上创建/a节点时设定为临时节点和session绑定即可,Client2在获取/a节点时注册watch删除事件,当Client1挂了/a节点就会被清理而产生事件,zookeeper就会触发回调Client2的watch的方法。
时效性:
zookeeper的实效性必然高于手动建立心跳的实效性,因为一旦Client1挂了,Client的watch方法会立马被回调。
心跳方式 最长需要要等一个心跳的间隔才能发现Client挂了。
六.API
- 简单API
- 导入maven坐标
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.6</version>
</dependency>
- API代码
public class App
{
public static void main( String[] args ) throws Exception {
System.out.println( "Hello World!" );
//zk有session的概念,但是没有线程池的概念。因为每一个连接会得到一个独立的session,监控watch时就会出现混乱。
/**
* 在new zk的时候注册session级别的watch是异步的,
* 如果想要成功连接后再查看zk状态,需要先阻塞。
*/
CountDownLatch latch=new CountDownLatch(1);
/**
* 参数1:zookeeper集群的所有IP地址
* 参数2:Client程序停止运行后,session的保留时间。
* 参数3:在zookeeper里面watch分为两类(观察和回调):
* 第一类:new zk的时候,传入的watch,这个watch是session级别的,和path没有关系。(当前参数)
* 第二类:path级别的,并且watch的注册 只发生在读类型,调用get,exites
*
* 另外watch事件的注册,只发生在读类型的调用:get、exeits.
*/
ZooKeeper zk = new ZooKeeper("192.168.116.135:2181,192.168.116.131:2181,192.168.116.132:2181,192.168.116.133:2181",
3000, new Watcher() {
/**
* 回调方法
* @param event
*/
@Override
public void process(WatchedEvent event) {
//事件状态
Event.KeeperState state = event.getState();
//事件类型
Event.EventType type = event.getType();
String path = event.getPath();
System.out.println("new zk watch::"+event.toString());
switch (state) {
case Unknown:
break;
case Disconnected:
break;
case NoSyncConnected:
break;
case SyncConnected:
System.out.println("connected...");
latch.countDown();
break;
case AuthFailed:
break;
case ConnectedReadOnly:
break;
case SaslAuthenticated:
break;
case Expired:
break;
}
switch (type) {
case None:
break;
case NodeCreated:
break;
case NodeDeleted:
break;
case NodeDataChanged:
break;
case NodeChildrenChanged:
break;
}
}
});
latch.await();
ZooKeeper.States state = zk.getState();
//根据zk状态不同,打印不同的字符串
switch (state) {
case CONNECTING:
System.out.println("ing....");
break;
case ASSOCIATING:
break;
case CONNECTED:
System.out.println("ed....");
break;
case CONNECTEDREADONLY:
break;
case CLOSED:
break;
case AUTH_FAILED:
break;
case NOT_CONNECTED:
break;
}
/**
* 增加节点
* create创建有两种形式,一种是阻塞,一种是非阻塞回调。(这里使用传统阻塞)
* 参数1:节点名称。 参数2:节点数据(二进制) 参数3:权限
* 参数4:节点类型(临时节点,因为在new zk的时候设定3000毫秒,所以程序运行结束后3秒消失)
*
* 返回的pathName(nodeName)还是很有必要的,因为有可能是顺序节点
*/
String nodeName = zk.create("/ooxx", "olddata".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
Stat stat = new Stat();
/**
* 查询节点
* getDate方法分为两大类(同步异步),四种方式。
* 参数1:查询数据的节点。
* 参数2:get方法注册的watch(第二类型,监控级别是path,是一次性的)
* 参数3:全量数据。
*/
byte[] data = zk.getData("/ooxx", new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println("getdata event ::"+event.toString());
try {
/**
* 回调处理完业务逻辑,可以直接在逻辑后添加再次注册
* 注意:第二个参数如果是true,则代表修改的时候调用的是default watch(new zk的watch)。
* false表示不注册。
* 写this代表path级别的watch
*/
zk.getData("/ooxx",this,stat);
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},stat);
System.out.println("stat::"+stat.getVersion()+"::"+stat.getCzxid());
System.out.println("查询节点获得结果 ::"+ new String(data));
/**
* 修改节点,
* 一旦修改节点,就会触发之前getData方法中注册的回调。
*/
Stat newdata = zk.setData("/ooxx", "newdata".getBytes(), 0);
//第二次修改不会触发getData的watch回调,因为注册watch回调是一次性的,需要从新注册才能再次触发
Stat newdata01 = zk.setData("/ooxx", "newdata01".getBytes(), newdata.getVersion());
/**
* 在zookeeper安装目录的conf目录下有log4j的文件,可以拿出来放到项目的resources目录下打印日志
* 观察:
* 连接的节点
* 生成sessionID和临时节点归属匹配
*
* 在连接过程中如果zkserver如果挂掉了,程序会从新连接其他可用的zkserver,并且sessionID不会断。
*/
Thread.sleep(7777777);
}
}
- 异步方式获取
/**
* 异步的方式是没有返回值的,获取值之后会回调processResult方法
*/
System.out.println("-------async start--------");
zk.getData("/ooxx", false, new AsyncCallback.DataCallback() {
/**
*
* @param rc 状态码
* @param path 路径
* @param ctx 上下文,其实就是getData时自己定义的abc
* @param data 数据
* @param stat 元数据
*/
@Override
public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
System.out.println("-------async call back--------");
System.out.println("async getdata::"+new String(data));
}
},"abc");
System.out.println("-------async over--------");
输出结果:
-------async start--------
-------async over--------
-------async call back--------
async getdata::newdata01
- 注册配置
在不同的主机里都启动了service,但是启动的时候需要配置需要从哪里来呢?我们可以在本地写配置文件,但是随着集群的规模越来越大,如果有某一个配置项需要修改的话,那么运维人员需要登录每一台主机修改每一个配置文件。
如果有一个地方,是所有service都能去独立访问的,那么只需要修改这一个位置其他人就都可以读取到,这个独立的位置可以是数据库、redis等任何一个可以共享的存储位置。但是为什么选择zookeeper呢? 就是因为zookeeper的回调机制,客户端除了能get到zookeeper中的节点数据,还能watch节点。只要有人更新zookeeper的节点数据了,并且这个客户端 watch 这个节点了,节点的修改一定会回调这个客户端的方法,这样客户端就会第一时间知道节点被修改了。任何使用zookeeper的客户端都不需要去轮询它是否被修改,成本低。
- 分布式锁
分布式做 镜像 负载均衡service的时候,要求service集群不能同时执行相同的业务逻辑,只要有一台service正在处理业务,其他service就必须处于阻塞状态。或者要求多个service不能同时访问同一个资源。
像JVM提供的锁只能解决service内的线程同步问题,确不能解决分布式service之间的同步问题。这个时候就需要使用到分布式锁了。分布式锁是把锁的概念抽离到service服务外边,放到外边就可以用很多别的技术来实现,比如redis,数据库等能被多service同时访问即可,但是这些方式的实现方式都很繁琐并且准确性不高。
目前来说最方便的实现方式是使用zookeeper,zookeeper是高可用的并且视图统一。那么zookeeper实现分布式锁之前 需要注意的问题有哪些?
- 争抢所,只有一台service能获得锁。
- 获得锁的service突然挂掉了形成死锁。
- 获得锁的service成功处理完请求,释放锁。
- 锁被释放,如何让其他service知道。如果没有watch和call back怎么实现?
(a) service主动轮询,发送心跳。但是如果service有1000台,同一时间只能有一台获得锁,则代表每个心跳都要有999台service主动轮询锁服务。 弊端:延迟,压力大。
(b)使用zookeeper的wacth支持。watch监控也分为两种使用:
· watch ParentNode。虽然能解决延迟问题,但是也有弊端,锁释放会回调 999 个service,999和节点会发生新一轮争抢锁,通信上会有一些压力。
· sequence(序列化节点) + temporary节点,在parentNode下创建临时顺序节点,watch的时候watch跟当前service相关联的节点的前一个节点。一旦最小的节点释放了,只会给第二个节点发送事件。无延迟,压力小。
代码:
实现思路:主要方法通过watch和事件回调,参数回调机制(需要搭配CountDownLatch来实现),当服务创建节点设置锁时,会返回对于的序列化节点,把节点排序,做判断,节点所在集合里面的索引是不是第一个(注意集合里面的值不带/符号):
-
是第一个节点
- 实现业务逻辑
- CountDownLatch减一
- 在CountDownLatch.await之间把对应节点删除,方便触发事件
-
不是
- 通过exsits监听自己的上一个节点
- 如果触发了事件,在调用获取方法进入判断是否为第一个节点