一、zookeeper简介
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,它包含一个简单的原语集,分布式应用程序可以基于它实现同步服务,配置维护和命名服务等。
目前zookeeper被广泛应用于hadoop生态体系中各种框架的分布式协调,我们也可以利用zookeeper来简化分布式应用开发。
二、Zookeeper的核心功能:
- 替客户存取数据
- 为客户提供对数据的监听服务
三、Zookeeper可以实现的分布式协调服务包括:
1、统一名称服务
2、配置管理
3.分布式锁
4.集群节点状态协调(负载均衡/主从协调)
四、zookeeper中的数据结构
1、zookeeper中为用保管数据时,使用的是树状结构(类似于目录树)
2、zookeeper的“目录”树中,每一个节点都叫做一个znode
- 每一个znode都有自己的path
- 每一个znode都有自己携带的一份数据(用户的)
- 每一个znode都有自己的类型:
- PERSISTENT :永久的(只要客户端不删除,则永远存在)
- PERSISTENT_SEQUENTIAL :永久且有序号的
- EMPEMERAL:短暂的(只要客户端掉线,则会被自动删除)
- EMPEMERAL_SEQUENTIAL:短暂且有序号的
znode维护的数据主要是用于存储协调的数据,如状态、配置、位置等信息,每个节点存储的数据量很小,KB级别,最大不要超过1M。
五、zookeeper集群搭建
1、zookeeper集群组件:
Zookeeper集群中的服务器角色有三种:
leader server,
follower server,
observer server;
注意:leader特殊之处在于它有决定权,具有Request Processor(observer server 与follower server的区别就在于不参与leader选举)
2、部署模式:
单节点
分布式(伪分布式)
3、配置文件:
(1)、添加一个zoo.cfg配置文件
$ZOOKEEPER/conf
mv zoo_sample.cfg zoo.cfg
(2)、修改配置文件(zoo.cfg)
dataDir=/itcast/zookeeper-3.4.5/data
server.1=itcast05:2888:3888
server.2=itcast06:2888:3888
server.3=itcast07:2888:3888
(3)、在(dataDir=/itcast/zookeeper-3.4.5/data)创建一个myid文件,里面内容是server.N中的N(server.2里面内容为2)
echo "1" > myid
(4)、将配置好的zk拷贝到其他节点
scp -r /itcast/zookeeper-3.4.5/ itcast06:/itcast/
scp -r /itcast/zookeeper-3.4.5/ itcast07:/itcast/
(5)、注意:在其他节点上一定要修改myid的内容
在itcast06应该讲myid的内容改为2 (echo "6" > myid)
在itcast07应该讲myid的内容改为3 (echo "7" > myid)
4.启动集群
分别启动zk
./zkServer.sh start
六、zookeeper演示测试
服务启动
bin/zkServer.sh status 获取节点角色状态
Zookeeper支持一下四字节命令来进行交互,查询状态信息等;可以用telnet/nc来发送命令,如:
echo ruok | nc server01 2181
echo conf | nc server01 2181
服务状态详细信息查看(四字命令):四字命令可以获取更多信息
conf |
输出相关服务配置的详细信息。 |
cons |
列出所有连接到服务器的客户端的完全的连接 / 会话的详细信息。包括“接受 / 发送”的包数量、会话 id 、操作延迟、最后的操作执行等等信息。 |
dump |
列出未经处理的会话和临时节点。 |
envi |
输出关于服务环境的详细信息(区别于 conf 命令)。 |
reqs |
列出未经处理的请求 |
ruok |
测试服务是否处于正确状态。如果确实如此,那么服务返回“imok ”,否则不做任何相应。 |
stat |
输出关于性能和连接的客户端的列表。 |
wchs |
列出服务器 watch 的详细信息。 |
wchc |
通过 session 列出服务器 watch 的详细信息,它的输出是一个与watch 相关的会话的列表。 |
wchp |
通过路径列出服务器 watch 的详细信息。它输出一个与 session相关的路径。 |
shell客户端操作
bin/zkCli.sh -server server01 2181
操作命令:
connect host:port
get path [watch]
ls path [watch]
set path data [version]
rmr path
delquota [-n|-b] path
quit
printwatches on|off
create [-s] [-e] path data acl
stat path [watch]
close
ls2 path [watch]
history
listquota path
setAcl path acl
getAcl path
sync path
redo cmdno
addauth scheme auth
delete path [version]
setquota -n|-b val path
七、ZooKeeper数据模型和层次命名空间
提供的命名空间与标准的文件系统非常相似。一个名称是由通过斜线分隔开的路径名序列所组成的。ZooKeeper中的每一个节点是都通过路径来识别。如图:
八、ZooKeeper中的数据节点:
1、 每一个节点称为znode,通过路径来访问;
2、每一个znode维护着:数据、stat数据结构(ACL、时间戳及版本号);
3、znode维护的数据主要是用于存储协调的数据,如状态、配置、位置等信息,每个节点存储的数据量很小,KB级别;
4、znode的数据更新后,版本号等控制信息也会更新(增加);
5、znode还具有原子性操作的特点:写--全部替换,读--全部;
6、znode有永久节点和临时节点之分:临时节点指创建它的session一结束,该节点即被zookeeper删除。
九、zk性能:
Zookeeper 的读写速度非常快(基于内存数据库),并且读的速度要比写的速度更快。
1、顺序一致性:客户端的更新顺序与它们被发送的顺序相一致。
2、原子性:更新操作要么成功要么失败,没有第三种结果。
3、单系统镜像:无论客户端连接到哪一个服务器,客户端将看到相同的 ZooKeeper 视图。
4、可靠性:一旦一个更新操作被应用,那么在客户端再次更新它之前,它的值将不会改变。这个保证将会产生下面两种结果:
(1)、如果客户端成功地获得了正确的返回代码,那么说明更新已经成功。如果不能够获得返回代码(由于通信错误、超时等等),那么客户端将不知道更新操作是否生效。
(2)、当从故障恢复的时候,任何客户端能够看到的执行成功的更新操作将不会被回滚。
5、实时性:在特定的一段时间内,客户端看到的系统需要被保证是实时的。在此时间段内,任何系统的改变将被客户端看到,或者被客户端侦测到。
注意:给予这些一致性保证, ZooKeeper 更高级功能的设计与实现将会变得非常容易,例如: leader 选举、队列以及可撤销锁等机制的实现。
十、zookeeper-api应用
基本使用,Zookeeper中znode的增删改查以及监听器的使用方法
public class ZKDemo {
private ZooKeeper zk = null;
@Before
public void init() throws Exception {
zk = new ZooKeeper("127.0.0.1:2181", 2000, new Watcher() {
/**
* 监听事件发生时的回调方法
*/
@Override
public void process(WatchedEvent event) {
if (event.getType() == EventType.None){
return;
}
System.out.println(event.getType());
System.out.println(event.getPath());
try {
zk.getData("/aaa", true, null);
zk.getChildren("/aaa", true);
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}
});
}
/**
* 向zookeeper服务集群中注册数据,添加znode
*
* @throws InterruptedException
* @throws KeeperException
* @throws UnsupportedEncodingException
*/
@Test
public void testCreateZnode() throws UnsupportedEncodingException, KeeperException, InterruptedException {
zk.create("/aaa", "这是我的处女作".getBytes("utf-8"), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
// sequential的顺序维护是在一个父节点的范围之内
zk.create("/aaa/bbb", "草草".getBytes("utf-8"), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
zk.create("/aaa/ccc", "花花".getBytes("utf-8"), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
// 换一个父节点,序号的递增顺序重新开始
zk.create("/aab/ddd", "树树".getBytes("utf-8"), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
zk.close();
}
/**
* 从zkeeper中删除znode
*
* @throws Exception
*/
@Test
public void testDeleteZnode() throws Exception {
// 参数1:要删除的节点的路径 参数2:要删除的节点的版本,-1匹配所有版本
zk.delete("/aab", -1);
Stat exists = zk.exists("/aab", false);
System.out.println(exists);
}
@Test
public void testUpdateZnode() throws Exception {
byte[] data = zk.getData("/aaa", false, null);
System.out.println(new String(data, "utf-8"));
zk.setData("/aaa", "我有一头小毛驴我从来也不骑".getBytes("utf-8"), -1);
data = zk.getData("/aaa", false, null);
System.out.println(new String(data, "utf-8"));
}
/**
* 获取子节点信息
*
* @throws Exception
*/
@Test
public void testGetChildren() throws Exception {
List<String> children = zk.getChildren("/aaa", false);
for (String child : children) {
System.out.println(child);
}
}
/**
* zk的监听机制: 1、事先定义好监听的回调函数 2、在对znode进行各种访问操作时可以注册监听
* 3、监听的znode上发生相应事件时,客户端zk会接收到zookeeper集群的事件通知 4、客户端zk根据
* 事件调用我们事先定义好的回调函数
*
* @throws InterruptedException
* @throws KeeperException
*
*/
@Test
public void testWatch() throws KeeperException, InterruptedException {
// 在获取znode数据时注册了监听
// 监听器是一次性,只要监听到一次事件,就失效了
// getData监听的事件是数据的更改
byte[] data = zk.getData("/aaa", true, null);
// 在做查询子节点操作时注册监听
// 监听的事件就是监听节点下的子节点变化事件
List<String> children = zk.getChildren("/aaa", true);
Thread.sleep(Long.MAX_VALUE);
}
}
十一、zookeeper应用案例(分布式应用HA||分布式锁)
1、实现分布式应用的主节点HA及客户端动态更新主节点状态
A、客户端实现
public class AppClient {
private String groupNode = "sgroup";
private ZooKeeper zk;
private Stat stat = new Stat();
private volatile List<String> serverList;
/**
* 连接zookeeper
*/
public void connectZookeeper() throws Exception {
zk = new ZooKeeper("localhost:4180,localhost:4181,localhost:4182", 5000, new Watcher() {
public void process(WatchedEvent event) {
// 如果发生了"/sgroup"节点下的子节点变化事件, 更新server列表, 并重新注册监听
if (event.getType() == EventType.NodeChildrenChanged && ("/" + groupNode).equals(event.getPath())) {
try {
updateServerList();
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
updateServerList();
}
/**
* 更新server列表
*/
private void updateServerList() throws Exception {
List<String> newServerList = new ArrayList<String>();
// 获取并监听groupNode的子节点变化
// watch参数为true, 表示监听子节点变化事件.
// 每次都需要重新注册监听, 因为一次注册, 只能监听一次事件, 如果还想继续保持监听, 必须重注册
List<String> subList = zk.getChildren("/" + groupNode, true);
for (String subNode : subList) {
// 获取每个子节点下关联的server地址
byte[] data = zk.getData("/" + groupNode + "/" + subNode, false, stat);
newServerList.add(new String(data, "utf-8"));
}
// 替换server列表
serverList = newServerList;
System.out.println("server list updated: " + serverList);
}
/**
* client的工作逻辑写在这个方法中
* 此处不做任何处理, 只让client sleep
*/
public void handle() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
public static void main(String[] args) throws Exception {
AppClient ac = new AppClient();
ac.connectZookeeper();
ac.handle();
}
}
B、服务器端实现
public class AppServer {
private String groupNode = "sgroup";
private String subNode = "sub";
/**
* 连接zookeeper
* @param address server的地址
*/
public void connectZookeeper(String address) throws Exception {
ZooKeeper zk = new ZooKeeper("localhost:4180,localhost:4181,localhost:4182",5000, new Watcher() {
public void process(WatchedEvent event) {
// 不做处理
}
});
// 在"/sgroup"下创建子节点
// 子节点的类型设置为EPHEMERAL_SEQUENTIAL, 表明这是一个临时节点, 且在子节点的名称后面加上一串数字后缀
// 将server的地址数据关联到新创建的子节点上
String createdPath = zk.create("/" + groupNode + "/" + subNode,address.getBytes("utf-8"),
Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("create: " + createdPath);
}
/**
* server的工作逻辑写在这个方法中
* 此处不做任何处理, 只让server sleep
*/
public void handle() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
public static void main(String[] args) throws Exception {
// 在参数中指定server的地址
if (args.length == 0) {
System.err.println("The first argument must be server address");
System.exit(1);
}
AppServer as = new AppServer();
as.connectZookeeper(args[0]);
as.handle();
}
}
2、分布式共享锁的简单实现
客户端A
public class DistributedClient {
// 超时时间
private static final int SESSION_TIMEOUT = 5000;
// zookeeper server列表
private String hosts = "localhost:4180,localhost:4181,localhost:4182";
private String groupNode = "locks";
private String subNode = "sub";
private ZooKeeper zk;
// 当前client创建的子节点
private String thisPath;
// 当前client等待的子节点
private String waitPath;
private CountDownLatch latch = new CountDownLatch(1);
/**
* 连接zookeeper
*/
public void connectZookeeper() throws Exception {
zk = new ZooKeeper(hosts, SESSION_TIMEOUT, new Watcher() {
public void process(WatchedEvent event) {
try {
// 连接建立时, 打开latch, 唤醒wait在该latch上的线程
if (event.getState() == KeeperState.SyncConnected) {
latch.countDown();
}
// 发生了waitPath的删除事件
if (event.getType() == EventType.NodeDeleted && event.getPath().equals(waitPath)) {
doSomething();
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
// 等待连接建立
latch.await();
// 创建子节点
thisPath = zk.create("/" + groupNode + "/" + subNode, null, Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);
// wait一小会, 让结果更清晰一些
Thread.sleep(10);
// 注意, 没有必要监听"/locks"的子节点的变化情况
List<String> childrenNodes = zk.getChildren("/" + groupNode, false);
// 列表中只有一个子节点, 那肯定就是thisPath, 说明client获得锁
if (childrenNodes.size() == 1) {
doSomething();
} else {
String thisNode = thisPath.substring(("/" + groupNode + "/").length());
// 排序
Collections.sort(childrenNodes);
int index = childrenNodes.indexOf(thisNode);
if (index == -1) {
// never happened
} else if (index == 0) {
// inddx == 0, 说明thisNode在列表中最小, 当前client获得锁
doSomething();
} else {
// 获得排名比thisPath前1位的节点
this.waitPath = "/" + groupNode + "/" + childrenNodes.get(index - 1);
// 在waitPath上注册监听器, 当waitPath被删除时, zookeeper会回调监听器的process方法
zk.getData(waitPath, true, new Stat());
}
}
}
private void doSomething() throws Exception {
try {
System.out.println("gain lock: " + thisPath);
Thread.sleep(2000);
// do something
} finally {
System.out.println("finished: " + thisPath);
// 将thisPath删除, 监听thisPath的client将获得通知
// 相当于释放锁
zk.delete(this.thisPath, -1);
}
}
public static void main(String[] args) throws Exception {
for (int i = 0; i < 10; i++) {
new Thread() {
public void run() {
try {
DistributedClient dl = new DistributedClient();
dl.connectZookeeper();
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
}
Thread.sleep(Long.MAX_VALUE);
}
}
客户端B
public class DistributedClient2 {
// 超时时间
private static final int SESSION_TIMEOUT = 5000;
// zookeeper server列表
private String hosts = "localhost:4180,localhost:4181,localhost:4182";
private String groupNode = "locks";
private String subNode = "sub";
private ZooKeeper zk;
// 当前client创建的子节点
private volatile String thisPath;
private CountDownLatch latch = new CountDownLatch(1);
/**
* 连接zookeeper
*/
public void connectZookeeper() throws Exception {
zk = new ZooKeeper(hosts, SESSION_TIMEOUT, new Watcher() {
public void process(WatchedEvent event) {
try {
// 连接建立时, 打开latch, 唤醒wait在该latch上的线程
if (event.getState() == KeeperState.SyncConnected) {
latch.countDown();
}
// 子节点发生变化
if (event.getType() == EventType.NodeChildrenChanged && event.getPath().equals("/" + groupNode)) {
// thisPath是否是列表中的最小节点
List<String> childrenNodes = zk.getChildren("/" + groupNode, true);
String thisNode = thisPath.substring(("/" + groupNode + "/").length());
// 排序
Collections.sort(childrenNodes);
if (childrenNodes.indexOf(thisNode) == 0) {
doSomething();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
// 等待连接建立
latch.await();
// 创建子节点
thisPath = zk.create("/" + groupNode + "/" + subNode, null, Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);
// wait一小会, 让结果更清晰一些
Thread.sleep(10);
// 监听子节点的变化
List<String> childrenNodes = zk.getChildren("/" + groupNode, true);
// 列表中只有一个子节点, 那肯定就是thisPath, 说明client获得锁
if (childrenNodes.size() == 1) {
doSomething();
}
}
/**
* 共享资源的访问逻辑写在这个方法中
*/
private void doSomething() throws Exception {
try {
System.out.println("gain lock: " + thisPath);
Thread.sleep(2000);
// do something
} finally {
System.out.println("finished: " + thisPath);
// 将thisPath删除, 监听thisPath的client将获得通知
// 相当于释放锁
zk.delete(this.thisPath, -1);
}
}
public static void main(String[] args) throws Exception {
for (int i = 0; i < 10; i++) {
new Thread() {
public void run() {
try {
DistributedClient2 dl = new DistributedClient2();
dl.connectZookeeper();
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
}
Thread.sleep(Long.MAX_VALUE);
}
}