- zookeeper是一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master选举、分布式锁和分布式队列等功能。
zookeeper可以保证如下分布式一致性特性:
① 顺序一致性:
从同一个客户端发起的事务请求,最终将会严格的按照其发起顺序被应用到zookeeper中去
② 原子性:
所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的
③ 单一视图:
无论客户端连接的是哪个zookeeper服务器,其看到的服务端数据模型都是一致的
④ 可靠性:
一旦服务端成功的应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来,除非有另一个事务又对其进行了变更
⑤ 实时性:
zookeeper保证在一定的时间段内,客户端最终一定能够从服务端上读取到最新的数据状态 - zookeeper的设计目标
zookeeper致力于提供一个高性能、高可用、且具有严格的顺序访问控制能力的分布式协调服务。
目标一:简单的数据模型
zookeeper使得分布式程序能够通过一个共享的、树型结构的名字空间来进行相互协调。zookeeper将全量数据存储在内存中,以此来实现服务器吞吐、减少延迟的目的。
目标二:可以构建集群
组成zookeeper集群的每台机器都会在内存中维护当前的服务器状态,并且每台机器之间都互相保持着通信。zookeeper的客户端程序会选择和集群中任意一台机器共同来创建一个TCP连接,而一旦客户端和某台zookeeper服务器之间的连接断开后,客户端会自动连接到集群中的其他机器。
目标三:顺序访问
对于来自客户端的每个更新请求,zookeeper都会分配一个全局唯一的递增编号,这个编号反应了所有事务操作的先后顺序,应用程序可以使用zookeeper的这个特性来实现更高层次的同步原语
目标四:高性能
由于zookeeper将全量数据存储在内存中,并直接服务于客户端的所有非事务请求,因此它尤其适用于以读操作为主的应用场景。
- zookeeper基本概念
① 集群角色
在zookeeper中,引入了leader、follower、observer三种角色。leader为客户端提供读和写服务,follower和observer都能提供读服务,唯一的区别在于observer不参与leader选举过程,也不参与写操作的“过半写成功”策略,因此observer可以在不影响写性能的情况下提升集群的读性能。
② 会话(session)
客户端启动时,首先会与服务器建立一个TCP连接,客户端能够通过心跳检测与服务器保持有效的会话,session的sessionTimeout值用来设置一个客户端会话的超时时间。
③ 数据节点(znode)
数据模型中的数据单元,称之为znode。zookeeper数据模型是一棵树(znode tree)。每个znode上都会保存自己的数据内容,同时还会保存一系列属性信息。zookeeper中,znode分为持久节点和临时节点,持久节点一旦被创建将一直保存在zookeeper上;临时节点的生命周期和客户端会话绑定,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。
④ 版本
⑤ watcher
事件监听器,zookeeper允许用户在指定节点上注册一些watcher,并且在一些特定事件触发的时候,zookeeper服务端会将事件通知到感兴趣的客户端上去。
⑥ ACL
zookeeper采用ACL(Access Control Lists)策略来进行权限控制,定义了5种权限:create、read、write、delete、admin - zookeeper数据模型
zookeeper使用了“数据节点”概念,称为ZNode。ZNode是zookeeper中数据的最小单元,每个ZNode上都可以保存数据,同时还可以挂载子节点。
事务ID:
在zookeeper中,事务是指能够改变zookeeper服务器状态的操作,一般包括数据节点创建与删除、数据节点内容更新和客户端会话创建与失效等操作。对于每一个事务请求,zookeeper都会为其分配一个全局唯一的事务ID,用ZXID表示。 - 节点特性
节点类型:持久节点、持久顺序节点(zookeeper自动为给定节点名后加上一个数字后缀)、临时节点(不能再有子节点)、临时顺序节点
状态信息:使用get命令即可获取该节点的状态信息。 - 版本
悲观锁:又称悲观并发控制,具有强烈的独占和排他性。
乐观锁:在更新请求提交之前,每个事务都会首先检查当前事务读取数据后,是否有其他事务对该数据进行了修改。如果有其他事务更新的话,那么正在提交的事务就需要回滚。
可以把一个乐观锁控制的事务分为三个阶段:数据读取、写入校验、数据写入。
在zookeeper中,version属性正是用来实现乐观锁机制中的“写入校验”的。
version = setDataRequest.getVersion();
int currentVersion = nodeRecord.stat.getVersion();
if (version != -1 && version != currentVersion){
throw new KeeperException.BadVersionException(path);
}
version = currentVersion + 1 ;
如果version为”-1”,说明客户端并不要求使用乐观锁,可以忽略版本比对。否则,比较version和currentVersion,如果不匹配则抛异常。
- Watcher——数据变更的通知
watcher具有以下特性:
·一次性
无论是服务端还是客户端,一旦一个Watcher被触发,zookeeper都会将其从相应的存储中移除。因此,开发人员在watcher的使用上要记住的一点是需要反复注册。
·客户端串行执行
·轻量 - ACL——保障数据安全
zookeeper的ACL权限控制,可以从三个方面来理解,分别是:权限模式(Scheme)、授权对象(ID)和权限(Permission),通常使用“scheme:id:permission”来标识一个有效的ACL信息。
权限模式:scheme
·IP:通过IP地址粒度来进行权限控制
·Digest:以类似“username:password”形式的权限标识来进行权限配置。
·World:数据节点的访问权限对所有用户开放
·Super:超级用户,可以对任意zookeeper上的数据节点进行任何操作。
授权对象:ID
授权对象指的是权限赋予的用户或一个指定实体。
权限:Permission
zookeeper中,所有对数据的操作权限分为:
CREATE(C):子节点的创建权限
DELETE(D):子节点的删除权限
READ(R):数据节点的读取权限
WRITE(W):数据节点的更新权限
ADMIN(A):数据节点的管理权限 - zookeeper客户端与服务端一次会话的创建过程:
·初始化阶段:初始化zookeeper对象、设置会话默认watcher、构造zookeeper服务器地址列表管理器:HProvider、创建并初始化客户端网络连接器:ClientCnxn、初始化SendThread和EventThread
·会话创建阶段:启动SendThread和EventThread、获取一个服务器地址、创建TCP连接、构造ConnectRequest请求、发送请求
·响应处理阶段:接收服务器响应、处理Response、连接成功、生成事件:SyncConnected-None、查询Watcher、处理事件 - Chroot:客户端隔离命名空间
Chroot特性允许每个客户端为自己创建一个命名空间,使得之后对于服务器的任何操作,都将会被限制在其自己的命名空间下。客户端在connectString中添加后缀的方式设置Chroot:
192.168.6.187:2181,192.168.6.229:2181/hbase/regan
- Leader选举:
每个Server生成自己的投票信息(myid,ZXID),ZXID代表了本机处理信息的数量及数据的新旧程度,每个服务器都将自己的投票信息中ZXID与其他server的ZXID进行比较,ZXID数大的胜出,如果相同,再比较myid,myid大的胜出。 - 各服务器角色介绍
·Leader
事务请求的唯一调度者,保证集群事务处理的顺序性。集群内部各服务器的调度者
·Follower
处理客户端非事务请求,转发事务请求给Leader服务器。参与事务请求Proposal的投票。参与Leader选举投票
·Observer
观察zookeeper集群的最新状态并将这些状态变更同步过来。对于非事务请求,都可以进行独立的处理,而对于事务请求,则会转发给Leader服务器进行处理。Observer不参与任务形式的投票。通常用于在不影响集群事务处理能力的前提下提升集群的非事务处理能力。 - 数据与存储
在zookeeper中,数据存储分为两部分:内存数据存储与磁盘数据存储
① 内存数据
DataTree是zookeeper内存数据存储的核心,是一个“树”的数据结构,代表了内存中的一份完整数据。
DataNode是数据存储的最小单元。其内部除了保存节点的数据内容、ACL列表和节点状态之外,还记录了父节点的引用和子节点列表两个属性。
ZKDatabase是zookeeper的内存数据库,负责管理zookeeper的所有会话、DataTree存储和事务日志。ZKDatabase会定时向磁盘dump快照数据,同时在zookeeper服务器启动的时候,会通过磁盘上的事务日志和快照数据文件恢复成一个完整的内存数据库。
② 事务日志
运行一段时间后在data/version-2目录下会生成zookeeper的事务日志文件:
文件大小都是64M,文件名后缀是一个事务ID:ZXID,并且是写入该事务日志文件第一条事务记录的ZXID。使用ZXID作为后缀,可以帮助我们迅速定位到某一个事务操作所在的事务日志。
事务日志都是二进制格式,通过命令:Java LogFormatter 事务日志文件,即可可视化事务日志。
③ snapshot——数据快照
数据快照用来记录zookeeper服务器上某一个时刻的全量内存数据内容,并将其写入到指定的磁盘文件中。数据快照同样存在于data/version-2目录下,使用ZXID的十六进制来作为文件名后缀,该后缀标识了本次数据快照开始时刻的服务器最新ZXID。在数据恢复阶段,zookeeper会根据该ZXID来确定数据恢复的起始点。通过命令:
Java SnapshotFormatter 快照数据文件,可以可视化快照文件。
④ 初始化
在zookeeper服务器启动期间,首先会进行数据初始化工作,用于将存储在磁盘上的数据文件加载到zookeeper服务器内存中。 - ZAB协议
zookeeper并没有完全采用paxos算法,而是使用了一种称为zookeeper atomic broadcast(zookeeper原子消息广播协议)的协议作为其数据一致性的核心算法
ZAB协议的核心是定义了对于那些会改变zookeeper服务器数据状态的事务请求的处理方式:
所有事务请求必须由一个全局唯一的服务器(leader)来协调处理,leader服务器负责将客户端事务请求转换为一个事务proposal(提议),并将该proposal分发给集群中所有的follower服务器。之后leader服务器需要等待所有follower服务器的反馈,一旦超过半数follower服务器进行了正确的反馈后,leader就会再次向所有follower服务器分发commit消息,要求其将前一个proposal进行提交 - 协议介绍
ZAB协议包括两种基本的模式:崩溃恢复模式和消息广播模式
① 消息广播
类似于一个二阶段提交过程,不同的是在过半follower服务器已经反馈Ack之后就开始提交事务proposal了。
② 崩溃恢复模式 - zookeeper可执行脚本
- 客户端脚本
连接特定的zookeeper服务器:
$ bin/zkCli.sh -server ip:port
创建zookeeper节点:
create [-s] [-e] path data [acl]
其中,-s或-e分别指定节点特性:顺序或临时节点。不加时,创建的是持久节点
不带acl时,不做任何权限控制
create -e /regan hello!
列出节点:
ls /path
读取节点:
get /path
更新节点数据:
set /path data
删除节点:
delete /path
- java客户端API
创建zookeeper连接:
ZooKeeper zooKeeper = new ZooKeeper("192.168.6.187:2181,192.168.6.188:2181,192.168.6.229:2181", 1000*60, null, false);
复用连接:
ZooKeeper zooKeeper2 = new ZooKeeper("192.168.6.187:2181,192.168.6.188:2181,192.168.6.229:2181", 1000*60, null,zooKeeper.getSessionId(),zooKeeper.getSessionPasswd(), false);
创建节点:
zookeeper.create()
删除节点:
zookeeper.delete()
获取子节点:
zookeeper.getChildren()
获取节点数据:
zookeeper.getData()
更新节点数据:
zookeeper.setData()
检查节点是否存在:
zookeeper.exists()
权限控制:
zookeeper.addAuthInfo(scheme, auth)
public class Exist_API_Sync_Usage implements Watcher{
private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
//CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。创建时需要指定一个初始化数值,用于在程序执行中做countdown()操作
private static ZooKeeper zk ;
public static void main(String argv[]) throws IOException, InterruptedException, KeeperException {
String path = "/regan_test";
zk = new ZooKeeper("192.168.6.188:2181", 5000, new Exist_API_Sync_Usage());
//创建zookeeper客户端连接时注册一个watcher
zk.exists(path, true);
//查看节点是否存在,并进行监听
connectedSemaphore.await();
//线程阻塞
zk.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
//创建节点
zk.setData(path, "hello1".getBytes(), -1);
//为节点设置data
zk.create(path+"/regan_son", "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
//创建子节点
zk.delete(path+"/regan_son", -1);
//删除子节点
Thread.sleep(Integer.MAX_VALUE);
//线程休眠
}
//该方法实现了Watcher接口的process方法,定制具体要监听的内容
public void process(WatchedEvent watchedEvent){
try {
//如果连接被建立起来了,则进行以下内容的监听
if(KeeperState.SyncConnected == watchedEvent.getState()){
//监听节点如果不存在,则connectedSemaphore执行减1操作,因为connectedSemaphore值初始化设置为1,因此,减1操作执行后其值为0,将释放锁。
if(EventType.None == watchedEvent.getType() && null == watchedEvent.getPath()){
connectedSemaphore.countDown();
//监听节点如果存在,则向客户端打印存在信息,同时继续监听
}else if (EventType.NodeCreated == watchedEvent.getType()) {
System.out.println("Node ("+watchedEvent.getPath()+") Created");
zk.exists(watchedEvent.getPath(), true);
//监听节点如果被删除,则向客户端打印删除信息,同时继续监听
}else if (EventType.NodeDeleted == watchedEvent.getType()) {
System.out.println("Node ("+watchedEvent.getPath()+") Deleted");
zk.exists(watchedEvent.getPath(), true);
//监听节点如果被改变,则向客户端打印改变信息,同时继续监听
}else if (EventType.NodeDataChanged == watchedEvent.getType()) {
System.out.println("Node ("+watchedEvent.getPath()+") DataChanged");
zk.exists(watchedEvent.getPath(), true);
}
}
} catch (Exception e) { }
}
}