zookeeper
zookeeper的设计目标
- 高性能,因为zookeeper将数据都放到了内存里,适用于以读为主的应用场景
- 高可用,zookeeper一般以集群的方式对外提供服务,每台机器间保持通信,只要有半数以上机器存活,就能正常对外进行服务
- 严格顺序访问,对于来自客户端的每个更新请求,zookeeper都分配一个全局唯一的递增编号,这个编号反映了事务执行的先后顺序
zookeeper的特点
- 一个领导者Leader,多个跟随者Follower
- 集群中只要有半数以上的机器存活就能对外正常服务
- 全局数据一致性,每个Server保存一份相同的数据副本,Client无论连接到哪个Server,数据都是一致的
- 更新要求顺序进行,来自同一个Client的更新请求按其发送顺序依次执行
- 数据更新原子性,一次数据更新要么全部成功,要么失败。
- 实时性,在一定时间范围内,Client能读到最新数据。
zookeeper的数据结构
整体可以看做一棵树,每个节点叫做ZNode,每个ZNode默认能存储1MB的数据,每个ZNode都可以通过其路径唯一标识。ZNode大致分为三部分:
- 节点的数据,即znode data
- 节点的子节点children
- 节点的状态stat,用来描述当前节点的创建、修改记录,包括cZxid、ctime等
stat属性介绍:
- cZxid:数据节点创建时的事务id
- mZxid:数据节点最后一次更新的事务id
- pZxid:数据节点的子节点最后一次被修改时的事务ID
- ctime:数据节点创建时的时间
- mtime:数据节点最后一次更新的时间
- dataVersion:节点数据的更改次数
- cversion:子节点的更改次数
- aclVersion:节点的ACL(权限列表)的更改次数
- ephemeralOwner:如果znode是临时节点,则这是znode所有者的 session ID。 如果znode不是临时节点,则该字段设置为0。
- dataLength:这是znode数据字段的长度。
- numChildren:这表示znode的子节点的数量。
ZNode可以分为以下几类:
- 持久化节点
- 持久化有序节点
- 临时节点
- 临时有序节点
zookeeper的应用场景
- 配置管理
- 分布式锁:比如某个服务在操作时,其他服务都不能进行操作,对该操作进行加锁
- 集群管理
- 生成分布式唯一ID
zookeeper的安装
-
下载安装包,并上传到虚拟机
zookeeper的tar包下载 -
解压到
/usr/local
目录下,并打开 -
在zookeeper的根目录下创建data目录用来存储内存快照及事务文件日志:
mkdir data
-
打开conf目录,根据样例配置文件复制一份配置文件:
cp zoo_sample.cfg zoo.cfg
-
修改新配置文件的内容,主要指定data目录
-
在zookeeper的bin目录下启动zookeeper服务器:
./zkServer.sh start
,用命令查看是否启动./zkServer.sh status
-
使用客户端登录:
./zkCli.sh
-
退出客户端:
quit
-
关闭服务器:
./zkServer.sh stop
zookeeper的常用命令
登录客户端后使用以下命令:
- 新增节点:
create [-s] [-e] path data
。其中-s和-e为可选参数,-s表示有序节点,-e表示临时节点。 - 获取节点数据:
get path
- 修改节点数据:
set path data [version]
,修改之后dataVersion会加1,如果传入的version和node本身的dataNode不符合,那么会修改失败。 - 删除节点数据:
delete path [version]
,version是可选参数,如果传入的version和node本身的dataNode不符合,那么会删除失败;当删除的节点下面还有子节点是也会删除失败。 - 删除节点和子节点:
rmr path
- 查看节点状态:
stat path
,和get命令相比不返回数据信息 - 查看子节点列表:
ls path
- 客户端对节点数据改变事件监听:
get path watch
,如果path对应的节点数据发生变化,那么会在客户端给与提醒 - 客户端对节点状态改变事件监听:
stat path watch
,如果path对应的节点数据发生变化,那么会在客户端给与提醒 - 客户端对节点的子节点增加删除监听:
ls path watch
,如果path对应的节点数据发生变化,那么会在客户端给与提醒 - 远程登录zookeeper服务器:
./zkCli.sh -server ip
zookeeper的权限控制
zookeeper对节点权限的控制主要通过access control list访问控制列表来实现。主要使用schema:id:permission
来标识,其中schema表示授权的模式,id表示授权的对象,permission表示授予的权限。
授权相关命令
- 读取对节点的权限:
getAcl path
- 设置权限:
setAcl path acl
,例如setAcl /node1 ip:192.168.10.10:crwda
表示ip为192.168.10.10的机器对该节点有所有权限 - 添加认证用户:
addauth schema auth
,该操作一般对应第3、4种授权模式
授权模式如下:
(在zookeeper中可以多种模式相结合,只需要用逗号将其分开即可。)
- world:只有一个用户anyone,代表登录的所有人,也是默认的模式
命令:setAcl <path> ip:<ip>:<acl>
- ip:对客户端使用IP地址进行认证
命令:setAcl <path> world:anyone:<acl>
- auth:使用已添加认证的用户认证
创建认证用户命令:addauth digest <user>:<password>
授权命令:setAcl <path> auth:<user>:<acl>
- digest:使用“用户名:密码”方式认证
授权命令:setAcl <path> digest:<user>:<password>:<acl>
,这里的密码必须是加密后的密码,可以用命令echo -n <user>:<password> | openssl dgst -binary -sha1 | openssl base64
对指定用户名和密码进行加密然后作为密码输入 - super:超管配置,需要手动修改zkServer.sh脚本(略)
授予的对象:权限赋予的实体,即IP地址或者用户
授予的权限:
- create(c ):可以在当前节点下创建子节点
- delete(d ):可以删除子节点
- read(r ):可以读取节点数据和显示子节点的列表
- write(w ):可以创建、修改、删除节点数据
- admin(a ):可以设置节点访问控制列表权限
java操作zookeeper
zookeeper的连接是异步的,首先引入相关依赖
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.2</version>
</dependency>
- 连接zookeeper
public static void main(String[] args) {
try {
//创建计数器对象
CountDownLatch countDownLatch=new CountDownLatch(1);
//参数1:IP地址,参数2:客户端与服务器之间的会话超时毫秒时间,参数3:监视器对象
ZooKeeper zookeeper=new ZooKeeper("192.168.189.135:2181", 5000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
if (watchedEvent.getState()==Event.KeeperState.SyncConnected){
System.out.println("连接创建成功");
countDownLatch.countDown();
}
}
});
countDownLatch.await();
zookeeper.close();
} catch (Exception e) {
e.printStackTrace();
}
}
- 创建节点
- 同步方式:
//参数1:节点路径,参数2:节点数据,参数3:权限列表,参数4:节点类型
zookeeper.create("/node1","node1".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
其中权限也可以自定义,比如
List<ACL> acls=new ArrayList<>();
Id id=new Id("world","anyone");
//只读权限
acls.add(new ACL(ZooDefs.Perms.READ,id));
zookeeper.create("/node2","node2".getBytes(), acls, CreateMode.PERSISTENT);
- 异步方式:
zookeeper.create("/node3", "node3".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT, new AsyncCallback.StringCallback() {
@Override
public void processResult(int rc, String path, Object ctx, String name) {
System.out.println(rc); //0代表成功
System.out.println(path); //节点的路径
System.out.println(ctx); // 节点的上下文
System.out.println(name); //节点的名称
}
},"i am context");
- 更新节点
同步方式:stat setData(String path, byte[] data, int version)
异步方式:void setData(String path, byte[] data, int version, StatCallback cb, Object ctx)
- 删除节点
同步方式:void delete(String path,int version)
,version为-1表示不考虑版本
异步方式:void delete(String path, int version, VoidCallback cb, Object ctx)
- 读取节点数据
同步方式:byte[] getData(String path, boolean watch, Stat stat)
,第二个参数表示使用zookeeper对象创建时的监听器,此处也可以是自定义的监视器对象,stat可以获取节点的状态。
异步方式:void getData(String path, boolean watch, DataCallback cb, Object ctx)
- 读取子节点数据
同步方式:List<String> getChildren(String path, boolean watch)
,第二个参数也可以是监视器对象。
异步方式:void getChildren(String path, boolean watch, ChildrenCallback cb, Object ctx)
- 判断节点是否存在
同步方式:Stat exists(String path, boolean watch)
,当返回的对象为空时表示节点不存在。
异步方式:exists(String path, boolean watch, StatCallback cb, Object ctx)
事件监听机制
watcher概念:zookeeper提供了数据发布订阅功能,多个订阅者可能同时监听某一特定主题对象,当该主题对象的自身状态发生变化时,会实时通知所有订阅者。zookeeper采用watcher机制实现发布订阅功能。该机制在被订阅对象发生变化时会异步的通知客户端。watcher机制和观察者模式类似。
watcher的架构:
- zookeeper服务端
- zookeeper客户端
- 客户端的ZKWatchManager对象
客户端首先将watcher注册到服务端,同时将watcher保存在客户端的管理容器(ZKWatchManager)中。当节点数据发生变化,那么服务端会通知客户端,接着客户端的watch管理器会触发相关watcher来回调用处理流程。
watcher的特性:
- 一次性,一旦被触发就会被移除,再次使用就要重新注册
- watcher回调是顺序串行化执行的
- watchEvent是最小的通信单元,结构上只包含童稚状态、事件类型和节点路径,并不会告诉节点变化前后具体内容
- watcher只有在当前session彻底失效时才会无效,若在session有效期内重连成功,则watcher依然存在,仍可接受通知
watcher的通知状态(KeeperState):客户端与服务端连接状态发生变化时的通知类型
枚举属性 | 说明 |
---|---|
SyncConnected | 客户端与服务器正常连接时 |
Disconnected | 客户端与服务器断开时 |
Expired | 会话session失效时 |
AuthFailed | 身份认证失败时 |
watcher的事件类型(EventType):数据节点发生变化时对应的通知类型
枚举属性 | 说明 |
---|---|
None | 无 |
NodeCreated | 监听的节点被创建时 |
NodeDeleted | 监听的节点被删除时 |
NodeDataChanged | 监听的节点内容被修改时 |
NodeChildrenChanged | 监听节点的子节点列表发生变化时 |
捕获相应事件
注册方式 | Created | ChildrenChanged | Changed | Deleted |
---|---|---|---|---|
k.exists() | 可监控 | 可监控 | 可监控 | |
k.getData() | 可监控 | 可监控 | ||
k.getChildren() | 可监控 | 可监控 |
zookeeper作为配置中心
设计思路:
- 连接zookeeper服务器
- 读取zookeeper中的配置信息,注册watcher监听器,存入本地事件
- 当zookeeper中的配置信息发生变化时,通过watcher的回调方法捕获数据变化事件
- 重新获取配置信息
zookeeper生成分布式唯一ID
再过去的单库单表系统中,通常可以使用数据库字段自带的auto_increment属性来自动为每条记录生成一个唯一ID。但是分库分表之后,就无法依靠数据库的auto_increment属性来标识一条记录了。此时我们就可以用zookeeper生成分布式全局唯一ID。zookeeper通过创建临时有序节点来实现生成分布式唯一ID(主要是截取该临时有序节点path中的序号,该序号不会重复)
zookeeper设计分布式锁
设计思路:
- 每个客户端往/locks下创建临时有序节点/locks/lock_num,创建成功后/locks下面会有为每个客户端创建的对应的节点。如/locks/lock_0000001
- 客户端获取/locks下子节点,并进行排序,判断排在最前面的是否为自己,如果自己排在最前面,那么代表获取锁成功
- 如果自己的锁节点不在第一位,则监听自己前一位的锁节点
- 当前一位锁节点对应的客户端执行完成,释放了锁,将会触发监听的逻辑
- 监听客户端重新执行第二步逻辑,判断自己是否能获得锁。
实现分布式锁的创建
public class MyLock {
CountDownLatch countDownLatch=new CountDownLatch(1);
ZooKeeper zookeeper;
private static final String LOCK_ROOT_PATH="/locks";
private static final String LOCK_NODE_NAME="lock_";
private String lockPath;
public MyLock(){
try {
zookeeper=new ZooKeeper("192.168.189.135:2181", 5000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
if (watchedEvent.getType()==Event.EventType.None){
if (watchedEvent.getState()==Event.KeeperState.SyncConnected){
System.out.println("连接创建成功");
countDownLatch.countDown();
}
}
}
});
countDownLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
}
//用于监视上一个节点的监视器
Watcher watcher=new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getType()==Event.EventType.NodeDeleted){
synchronized (this){
notifyAll();
}
}
}
};
public void acquireLock() throws Exception{
createLock();
attemptLock();
}
private void createLock() throws Exception {
//判断 /locks 节点是否存在
Stat stat = zookeeper.exists("/locks", false);
if (stat==null){
zookeeper.create("/locks",new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
//在/locks下创建临时有序节点
lockPath = zookeeper.create("/locks/lock_", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
}
private void attemptLock() throws Exception {
List<String> children = zookeeper.getChildren("/locks", false);
Collections.sort(children);
int index=children.indexOf(lockPath.substring("/locks".length()+1));
if (index==0){
System.out.println("获取锁成功");
}else {
//获取上一个节点
String path = children.get(index - 1);
Stat stat = zookeeper.exists("/locks/" + path, watcher);
if (stat==null){ //如果上一个节点已经释放锁
attemptLock();
}else {
synchronized (watcher){
watcher.wait();
}
attemptLock();
}
}
}
private void releaseLock() throws Exception{
zookeeper.delete(this.lockPath,-1);
zookeeper.close();
System.out.println("锁已释放");
}
}
zookeeper集群的搭建
- 集群搭建:略
ZAB协议
zookeeper通过zab协议来保证分布式事务的最终一致性,基于zab协议,zookeeper集群中的角色主要有以下三类:
- 领导者:领导者负责进行投票的发起和决议,更新系统状态
- 追随者:follower用于接收客户端请求并向客户端返回结果,在选主过程中参与投票
- 观察者:可以接受客户端连接,将写请求转发给leader节点。但观察者不参与投票,只同步leader的状态
针对集群中的读请求,每一个节点都含有最新的数据,因此每个节点都能处理读请求;如果是写请求,那么只能有leader节点处理,即便是与follower相连的客户端发起的写请求,该请求也会被转发给leader节点处理,处理写请求分为以下步骤:
- leader从客户端收到一个写请求
- leader生成一个新的事务并为这个事务生成唯一的ZXID
- leader将这个事务提议(proposal)发送给所有的followers节点
- follower节点将收到的事务请求加入到历史队列(history queue)中,并发送ack给leader
- 当leader收到半数以上的ack消息,leader会发送commit请求
- 当follower收到commit请求后会将历史队列中的事务请求commit
zookeeper的leader选举
服务器状态:
- looking:寻找leader的状态。表示需要进入leader选举
- leading:领导者状态。表明当前服务器是leader
- follower:追随者状态。表示当前服务器是follower
- observing:观察者状态。表明当前服务器是observer
Leader选举是保证分布式数据一致性的关键。当Zookeeper集群中的一台服务器出现以下两种情况之一时,需要进入Leader选举。
- 服务器启动时的leader选举
若进行Leader选举,则至少需要两台机器,这里选取3台机器组成的服务器集群为例。在集群初始化阶段,当有一台服务器Server1启动时,其单独无法进行和完成Leader选举,当第二台服务器Server2启动时,此时两台机器可以相互通信,每台机器都试图找到Leader,于是进入Leader选举过程。选举过程如下。
- 每个Server发出一个投票。由于是初始情况,Server1和Server2都会将自己作为Leader服务器来进行投票,每次投票会包含所推举的服务器的myid和ZXID,使用(myid, ZXID)来表示,其中myid表示该节点的序号,序号越大权重越大。此时Server1的投票为(1, 0),Server2的投票为(2, 0),然后各自将这个投票发给集群中其他机器。
- 接受来自各个服务器的投票。集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票、是否来自LOOKING状态的服务器。
- 处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行PK,先检查ZXID,ZXID大的服务器优先成为leader,如果ZXID相同,那么就比较myid,myid大的优先作为leader服务器
- 统计投票。每次投票后,服务器会统计投票信息,判断是否有半数以上的机器接收到相同的投票信息,对于Server1、Server2而言,都统计出集群中已经有两台机器接受了(2,0)的投票信息,此时便认为已经选出了leader。
- 改变服务器状态。一旦确认了leader,每个服务器就会变更为相应的角色。
- 服务器运行时的leader选举
当leader挂掉,并且剩余的节点数大于半数时,zookeeper可以接着对外提供服务。选举过程如下
- 变更状态。Leader挂后,余下的非Observer服务器都会将自己的服务器状态变更为LOOKING,然后开始进入Leader选举过程。
- 每个Server会发出一个投票。在运行期间,每个服务器上的ZXID可能不同,此时假定Server1的ZXID为123,Server3的ZXID为122;在第一轮投票中,Server1和Server3都会投自己,产生投票(1, 122),(3, 122),然后各自将投票发送给集群中所有机器。
- 接收来自各个服务器的投票。与启动时过程相同。
- 处理投票。与启动时过程相同,此时,Server1将会成为Leader。
- 统计投票。与启动时过程相同。
- 改变服务器的状态。与启动时过程相同。
zookeeper中的观察者
观察者特点:
- 不参与leader选举
- 不参与集群中写数据时的ack反馈
观察者的配置:
- 首先在想成为observer角色的配置文件中修改peerType=observer
- 在所有server的配置文件中,配置observer模式的server那一行追加
:obersver
,例如
server.3=192.168.60.130:2289:3389:obersver
JAVA连接zookeeper集群
zookeeper=new ZooKeeper("192.168.189.135:2181,192.168.189.135:2182,192.168.189.135:2183", 5000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
if (watchedEvent.getType()==Event.EventType.None){
if (watchedEvent.getState()==Event.KeeperState.SyncConnected){
System.out.println("连接创建成功");
countDownLatch.countDown();
}
}
}
});
低级的欲望通过放纵就可以获得,高级的欲望通过自律方可获得,顶级的欲望通过煎熬才可获得