ZooKeeper是源代码开放的分布式协调服务,由雅虎创建,是Google Chubby的开源实现。ZooKeeper是一个高性能的分布式数据一致性解决方案,它将那些复杂的、容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并提供一系列简单易用的接口给用户使用。
知识要点:
1、源代码开放
2、是分布式协调服务,它解决分布式数据一致性问题
A:顺序一致性
从一个客户端发起的事务请求,最终会严格的按照其发起的顺序被应用到zookper中。
B:原子性 PAXOS 算法+ZAB原子广播协议[P1]
所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么集群中所有的机器都成功地应用了某一事务,要么都没有应用,一定不会出现部分机器的应用。
[P1]底层算法,PAXOS 复制算法保证了zk集群中数据的一致性。
C:单一视图[P2]
无论客户端连接的是哪个zookper服务端,看到的服务端数据都是一致的。
[P2]几个节点的数据时保持一致的
D:可靠性
一旦一个服务器成功应用了某个事务,并完成对客户端的响应,那么该事务引起的服务端状态将会被一致保留下来,除非有另一个事务对其更改。
E:实时性
通常所说的实时性是指一旦事务被成功应用,那么服务端就能立刻从服务端获取变更后的新数据。Zookper仅能保证在一定的时间内客户端一定能从服务器端读取最新的数据状态。
3、高性能
4、 我们可以通过调用ZooKeeper提供的接口来解决一些分布式应用中的实际问题
1 简单的数据结构
简单的数据结构,zk是以简单的树形结构来进行相互协调的(也叫树形名字空间)
2 可以构建集群
一般一个zk集群是由一组机器构成,一般3到5台机器就可以组成zk集群了,只要集群中有超过半数以上的机器可以正常运行,那么zk集群就可以正常对外提供服务。
3 顺序访问
对于来自每个客户端的每一个请求,zk都会分配一个全局唯一的递增编号,这个编号反映乐事务操作的先后顺序,应用程序可以利用这个特性来实现更高层次的同步。
4 高性能
由于zk将全量数据存储在内存中,并直接服务于所有的非事务请求,因为尤其是在读操作为主的场景下性能非常突出,,在jemeter压力测试下(100%读场景下),其结果大约是12~13w的qps。
Leader服务器是整个Zookeeper集群工作机制中的核心
Follower服务器是Zookeeper集群状态的跟随者
Observer服务器充当一个观察者的角色
---Leader,Follower 设计模式
---Observer 观察者设计模式
会话是指客户端和ZooKeeper服务器的连接,ZooKeeper中的会话叫Session,客户端靠与服务器建立一个TCP的长连接来维持一个Session,客户端在启动的时候首先会与服务器建立一个TCP连接,通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能向ZK服务器发送请求并获得响应。
Zookeeper中的节点有两类
1.集群中的一台机器称为一个节点
2.数据模型中的数据单元Znode,分为持久节点和临时节点 [P3]
[P3]1 本次会话有效,
2 可以实现分布式锁:当A和B两个线程去修改DB同一份数据,我们希望先来先修改,修改后再由另外线程修改,就可以通过由线程A或B在Zk节点创建临时节点的方式抢占锁,因为Zk集群中能操作同一节点的只能是一个会话,只有释放当前会话,临时节点才会删除掉,才能由另外会话去创建。所以当一会话抢占到锁后,就可以去操作DB,操作完成后关闭zk会话。让其他线程继续执行。(可能zk的创建和删除临时节点性能比较高,才用临时节点做分布式锁)。
另外,一般先get,再create。只有当内存中不存在临时节点才创建。这样提高性能。
Zookeeper的数据模型是一棵树,树的节点就是Znode,Znode中可以保存信息
我们看下图
如图:
版本类型 |
说明 |
version |
当前数据节点数据内容的版本号 |
cversion |
Children Version 当前数据节点子节点的版本号 |
aversion |
当前数据节点ACL变更版本号 |
悲观锁和乐观锁
悲观锁又叫悲观并发锁,是数据库中一种非常严格的锁策略,具有强烈的排他性,能够避免不同事务对同一数据并发更新造成的数据不一致性,在上一个事务没有完成之前,下一个事务不能访问相同的资源,适合数据更新竞争非常激烈的场景
相比悲观锁,乐观锁使用的场景会更多,悲观锁认为事务访问相同数据的时候一定会出现相互的干扰,所以简单粗暴的使用排他访问的方式,而乐观锁认为不同事务访问相同资源是很少出现相互干扰的情况,因此在事务处理期间不需要进行并发控制,当然乐观锁也是锁,它还是会有并发的控制!
对于数据库我们通常的做法是在每个表中增加一个version版本字段,事务修改数据之前先读出数据,当然版号也顺势读取出来,然后把这个读取出来的版本号加入到更新语句的条件中,比如,读取出来的版本号是1,我们修改数据的语句可以这样写,update 某某表 set 字段一=某某值 where id=1 and version=1,那如果更新失败了说明以后其他事务已经修改过数据了,那系统需要抛出异常给客户端,让客户端自行处理,客户端可以选择重试
ZooKeeper允许用户在指定节点上注册一些Watcher,当数据节点发生变化的时候,ZooKeeper服务器会把这个变化的通知发送给感兴趣的客户端
ACL是Access Control Lists的简写, ZooKeeper采用ACL策略来进行权限控制,有以下权限:
CREATE:创建子节点的权限
READ:获取节点数据和子节点列表的权限
WRITE:更新节点数据的权限
DELETE:删除子节点的权限
ADMIN:设置节点ACL的权限
集群环境分:集群、伪集群、单机三类环境。
我们准备了三台linux环境centos7系统,ip为:
168.168.168.86、168.168.168.88、168.168.168.89
1. 准备Java运行环境,JDK
2.下载ZooKeeper安装包
3.配置文件zoo.cfg
4.创建myid
5.配置其他机器
6.启动服务器
下载地址:http://zookeeper.apache.org
上传zookeeper-3.4.9.tar.gz文件到192.168.98.98、99/100的/opt路径下.
三台机器做同样的操作(配置不同)
#cd /opt/
# tar -zxvf zookeeper-3.4.9.tar.gz
#mv zookeeper-3.4.9 zookeeper
配置zk环境变量:
vi /etc/profile
exportZOOKEEPER_HOME=/opt/zookeeper
export PATH=$ZOOKEEPER_HOME/bin:$PATH
刷新:
source /etc/profile
# cd /opt/zookeeper/conf
# cp zoo_sample.cfg zoo.cfg ----- zoo_sample.cfg是zk的样例配置
# vi zoo.cfg
以上,
dataDir=/opt/zookeeper/data # (要先创建data文件夹)是zk存储快照文件的存放路径
在最下方输入服务器的配置:
格式为:server.id=ip:port:port
其中id为服务器的编号,整数;第一个port为zk中leader与follower服务器的通信端口,第二个port专门用于leader选举中的投票通信。
保存退出!
下一步,拷贝zoo.cfg到88和89的conf目录下:
#scp zoo.cfg [email protected]:/opt/zookeeper/conf
#scp zoo.cfg [email protected]:/opt/zookeeper/conf
# cd /opt/zookeeper/data
# vim myid
写入服务器id,98就写0,99就写1,100就写2.
配置完成.
# cd /opt/zookeeper/bin
# ./zkServer.sh start
Ps:zkServer.sh start-foreground 可以看到启动日志
测试是否启动成功:zkServer.sh status
注意:若出现当前服务器无法对外提供服务的提示,是因为其他zk服务器还没有启动,zk集群是不能正常工作的。当集群中超过半数的zk服务器启动成功,zk集群才可以正常工作。
可以用zkServer.shstart-foreground命令启动来查看日志,发现:
推测是防火墙没有关闭,关闭防火墙:
systemctl stopfirewalld.service
systemctl disablefirewalld.service
systemctl statusfirewalld.service
再次启动正常!
我们可以看到当前服务器的集群模式:leader/follower
使用zkclient.sh链接zk服务器:
#cd /opt/zookeeper/bin
#./zkCli.sh -timeout 5000 –r –server192.168.98.98:2181
参数解释:
Timeout:表示在timeout指定的时间内(毫秒)内服务器没有收到客户端的心跳包,表示当前会话失效。
-r: 表示某台服务器如果跟半数以上的服务器失去链接后不再对外提供服务。
命令:
输入h并回车查看命令:
# ls / ---列出根节点下的子节点
#statnode_1 ---查看node_1的节点状态信息
注意: 修改子节点的内容不算一次子节点列表发生改变。
#get /node_1/node_1_1 ---查看节点的状态信息,包含内容
#create /node_3 123 ---创建节点node_3,内容为123
#create –e /node_3/node_3_1 234 ---创建临时节点node_3_1内容为234
临时节点:客户端与服务器会话断开连接后临时节点自动删除。
#create –s /node_3/node_3_1 234 ---创建顺序节点node_3_1内容为234
顺序节点:每创建一次就会使得序列号加1
#set node_3 999
再次修改node_3的数据:
# set /node_3 999 ----会发现dataversion版本号加1
再来一次修改且指定版本号2:
#set /node_3 999 2 ---发现版本号加1变为3 ,虽然指定的是2
当查询到版本号为3时,再指定版本号为2时会发现报错!
#delete /node_3 ---只能删除不含子节点的节点
如果想删除含有子节点的节点循环删除,可以:
#rmr /node_3
#quit ---退出客户端跟服务器之前的会话链接
#connect host:port 命令连接其他zk服务器
#close 关闭与其他zk服务器的连接
#history ---查看执行过的命令历史
#redo 14 ---重新执行14编号命令
#setquota –n|-b val path ---n限制的是子节点个数,b限制的是数据长度
#限制node_1节点的子节点个数为2(但是却可以超过2不会报错,zk只是记录了warn信息,可以查看bin下的zookeeper.out)
#setquota –n 2 /node_1
#listquota /node_1 ----查看node_1节点的配额限制
#delquota –n|-b path ---删除子节点配额
#delquota –n /node_1
#create /node_20ip:192.168.1.105:crwda ---表示创建节点的同时指定权限为允许ip为105进行crwda操作。
#create /node_21degest:jike:xxzsdsadasas:crwda ---表示创建节点的同时制定权限为允许用户名密码为xxx进行crwda操作。
步骤:
1 下载zookeeper相关jar包
2 创建java工程并且导入jar及依赖jar
import java.io.IOException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
importorg.apache.zookeeper.Watcher.Event.EventType;
importorg.apache.zookeeper.Watcher.Event.KeeperState;
public class CreateSession implementsWatcher {
privatestatic ZooKeeper zookeeper;
publicstatic void main(String[] args) throws IOException, InterruptedException {
zookeeper= new ZooKeeper("192.168.1.105:2181",5000,new CreateSession());
System.out.println(zookeeper.getState());
Thread.sleep(Integer.MAX_VALUE);
}
privatevoid doSomething(){
System.out.println("dosomething");
}
@Override
publicvoid process(WatchedEvent event) {
//TODO Auto-generated method stub
System.out.println("收到事件:"+event);
if(event.getState()==KeeperState.SyncConnected){
if(event.getType()==EventType.None && null==event.getPath()){
doSomething();
}
}
}
}
import java.io.IOException;
import org.apache.zookeeper.CreateMode;
importorg.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.Watcher.Event.EventType;
importorg.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooDefs.Ids;
public class CreateNodeSync implementsWatcher {
privatestatic ZooKeeper zookeeper;
publicstatic void main(String[] args) throws IOException, InterruptedException {
zookeeper= new ZooKeeper("192.168.1.105:2181",5000,new CreateNodeSync());
System.out.println(zookeeper.getState());
Thread.sleep(Integer.MAX_VALUE);
}
privatevoid doSomething(){
try{
//创建节点返回路径
//节点创建以后任何人可以对节点做任何操作
//创建模式:持久节点
String path =
zookeeper.create("/node_4", "123".getBytes(), Ids.OPEN_ACL_UNSAFE , CreateMode.PERSISTENT );
System.out.println("returnpath:"+path);
}catch (KeeperException e) {
e.printStackTrace();
}catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("dosomething");
}
@Override
publicvoid process(WatchedEvent event) {
System.out.println("收到事件:"+event);
//事件组成1:事件的状态是已连接或断开连接
if(event.getState()==KeeperState.SyncConnected){
/**
事件组成2:
这一行判断触发的场景是:
当客户端跟服务端建立会话链接后,事件监听器会发送客户端一个通知,
此时的事件类型为none,此时的节点路径为null。这段逻辑
dosomething只会执行一次,用于刚刚建立连接后的业务处理。*/
if(event.getType()==EventType.None && null==event.getPath()){
doSomething();
}else{
。。。。。。。。。。。。。//事件组成3 其他情况
}
}
}
}
import java.io.IOException;
import org.apache.zookeeper.AsyncCallback;
import org.apache.zookeeper.CreateMode;
importorg.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
importorg.apache.zookeeper.Watcher.Event.EventType;
importorg.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooDefs.Ids;
public class CreateNodeASync implementsWatcher {
privatestatic ZooKeeper zookeeper;
publicstatic void main(String[] args) throws IOException, InterruptedException {
zookeeper= new ZooKeeper("192.168.1.105:2181",5000,new CreateNodeASync());
System.out.println(zookeeper.getState());
Thread.sleep(Integer.MAX_VALUE);
}
privatevoid doSomething(){
//与同步创建不同的地方,传入异步回调接口实例
zookeeper.create("/node_5","123".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT,new IStringCallback(),"创建");
}
@Override
publicvoid process(WatchedEvent event) {
//TODO Auto-generated method stub
System.out.println("收到事件:"+event);
if(event.getState()==KeeperState.SyncConnected){
if(event.getType()==EventType.None && null==event.getPath()){
doSomething();
}
}
}
staticclass IStringCallback implementsAsyncCallback.StringCallback{
//Rc表示异步调用的返回值:0 表示创建成功
@Override
publicvoid processResult(int rc , String path, Object ctx, String name) {
//TODO Auto-generated method stub
StringBuildersb = new StringBuilder();
sb.append("rc="+rc).append("\n");
sb.append("path="+path).append("\n");
sb.append("ctx="+ctx).append("\n");
sb.append("name="+name);
System.out.println(sb.toString());
}
}
}
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs.Perms;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Id;
importorg.apache.zookeeper.server.auth.DigestAuthenticationProvider;
public class CreateNodeSyncAuth implements Watcher {
private staticZooKeeper zookeeper;
private static booleansomethingDone = false;
public static voidmain(String[] args) throws IOException, InterruptedException {
zookeeper =new ZooKeeper("192.168.1.105:2181",5000,new CreateNodeSyncAuth());
System.out.println(zookeeper.getState());
Thread.sleep(Integer.MAX_VALUE);
}
/*
* 权限模式(scheme): ip,digest
* 授权对象(ID)
* ip权限模式: 具体的ip地址
* digest权限模式:username:Base64(SHA-1(username:password))
* 权限(permission):create(C), DELETE(D),READ(R), WRITE(W), ADMIN(A)
* 注:单个权限,完全权限,复合权限
*
* 权限组合: scheme + ID +permission
* */
private void doSomething(){
try {
//基于ip的权限组合:表示允许ip来源为192.168.1.105的zk客户端读取节点信息。
ACL aclIp =new ACL(Perms.READ,new Id("ip","192.168.1.105"));
//基于用户名和密码的权限组合:表示允许用户名和密码为xxx的用户来读和写节点数据。
ACL aclDigest= new ACL(Perms.READ|Perms.WRITE,
new Id("digest",DigestAuthenticationProvider.generateDigest("jike:123456")));[P2]
ArrayListacls = new ArrayList();
acls.add(aclDigest);
acls.add(aclIp);
//zookeeper.addAuthInfo("digest","jike:123456".getBytes());
//创建节点时将权限集合acls传入
Stringpath = zookeeper.create("/node_4","123".getBytes(), acls, CreateMode.PERSISTENT);
System.out.println("returnpath:"+path);
somethingDone= true;
} catch(KeeperException e) {
e.printStackTrace();
} catch(InterruptedException e) {
e.printStackTrace();
} catch(NoSuchAlgorithmException e) {
//TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
public voidprocess(WatchedEvent event) {
// TODOAuto-generated method stub
System.out.println("收到事件:"+event);
if(event.getState()==KeeperState.SyncConnected){
if(!somethingDone && event.getType()==EventType.None &&null==event.getPath()){
doSomething();
}
}
}
}
Zkclient和Curator是第三方对原生zookeeperJava ApI的封装,简化代码编写复杂度。对连接重试和事件监听反复注册做了优化。
说明下:
1 ZkClient不再像原生API那样每次需要监听之前都要设置watch为true。而是使用subScribeChildChanges(子节点的新增、删除、自身的新增和删除)和subScribeDataChanges(监听节点删除和数据变化)两套监听方法,来分别对子节点变化和节点数据变化进行监听。
2 Curator的监听必须依赖一个jar,curator-recipes.jar,curator有两种实现监听的方式:NodeCacheListener(监听节点新增、修改)和PathChildrenCacheListener(监听子节点的增删改操作)
Watcher的特性:
zk的Watcher是一次性触发,当watcher监视的数据发生变化时,通知设置了该Watch的client,即watcher(客户端)。由于zk的wacther都是一次性的,所以必须设置该监控。
数据变化对应的事件类型和状态类型:
EventType.NodeCreated
EventType.NodeDataChanged
EventType.NodeChildrenChanged
EventType.NodeDeleted
EventType.NONE
KeeperState.Disconnected
KeeperState.SyncConnected
KeeperState.AuthFailed
KeeperState.Expired