zookeeper主要是文件系统和通知机制
基于观察者模式设计的分布式服务管理框架,开源的分布式框架
process(){重新再去获取服务器列表,并注册监听}
与 Unix 文件系统很类似,可看成树形结构,每个节点称做一个 ZNode。每一个 ZNode 默认能够存储 1MB 的数据。也就是只能存储小数据。每个ZNode都可以通过其路径唯一标识。
统一命名服务(域名服务)
在分布式环境下,经常需要对应用/服务进行统一命名,便于识别。
例如:IP不容易记住,而域名容易记住。
统一配置管理(一个集群中的所有配置都一致,且也要实时更新同步)
将配置信息写入ZooKeeper上的一个Znode,各个客户端服务器监听这个Znode。一旦Znode中的数据被修改,ZooKeeper将通知各个客户端服务器。
分布式环境下,配置文件同步非常常见。
统一集群管理(在分布式环境中,实时掌握每个节点的状态)
Zookeeper可以实现实时监控节点状态变化。
将节点信息写入ZooKeeper上的一个ZNode。监听ZNode获取实时状态变化
https://zookeeper.apache.org/
访问官网,点击下载
下拉选择版本
选择版本之后点击下载,以linux为例,选择.tar.gz文件
1.安装JDK
2.将zookeeper-3.4.9.tar.gz安装包拷贝到Linux系统下
在安装包路径下将zookeeper-3.4.9.tar.gz解压
之后进入zookeeper-3.4.9文件夹中,创建一个存储数据的文件夹并进入文件夹使用pwd获取文件路径
mkdir zkData
修改zookeeper-3.4.9目录下中的conf下的zoo_sample.cfg文件名,改为zoo.cfg
将zoo.cfg中的dataDir路径修改为zkData的路径
之后启动服务端在启动客户端
具体服务端的启动通过bin目录下的文件
通过zkServer.sh start
或者是./zkServer.sh start
查看zookeeper的状态
bin/zkServer.sh status
启动客户端
bin/zkCli.sh
退出客户端
quit
停止zookeeper
bin/zkServer.sh stop
zoo.cfg中参数的含义:
1.tickTime = 2000 :通信心跳时间,Zookeeper服务器与客户端心跳时间,单位毫秒
2.initLimit = 10 :LF初始通信时限
Leader和Follow初始连接时能容忍的最多心跳数(tickTime的数量) 即时间最多不能超过tickTime * initLimit的时间建立连接。
3.syncLimit = 5:LF同步通信时间
Leader和Follow之间通信时间如果超过syncLimit * tickTime,Leader认为Follow死掉,从服务器列表中删除Follow
4.dataDir:保存Zookeeper中的数据
注意:默认的tmp目录,容易被系统定期删除,所以一般不用默认的tmp目录。
5.clientPort = 2181,客户端连接端口,通常不做修改。
①
克隆两台虚拟机,分别配置不同的ip地址
之后在zookeeper/zkData目录下创建myid文件。
vim myid
在文件中添加与server对应的编号(注意:上下不要有空行,左右不要有空格)(每一台虚拟机的server对应编号都不一样)
2
注意:添加myid文件,一定要在Linux里面创建,在notepad++里面很可能乱码
②修改zoo.cfg配置文件
增加如下配置,关闭每台虚拟机的防火墙(systemctl stop firewalld)
################################cluster##############################
server.1=192.168.200.131:2888:3888
server.2=192.168.200.132:2888:3888
server.3=192.168.200.133:2888:3888
参数解读
当前主要配置编号的参数是server.A=B:C:D
A标识第几台服务器
B标识服务器地址
C标识服务器 Follower 与集群中的 Leader 服务器交换信息的端口
D主要是选举,如果Leader 服务器挂了。这个端口就是用来执行选举时服务器相互通信的端口,通过这个端口进行重新选举leader
配置文件以及服务器编号设置好后即可启动
③启动每台虚拟机中的zookeeper
zkServer.sh start
查看每台zookeeper的状态
区分好第一次启动与非第一次启动的步骤
Zookeeper第一次启动的选举机制
epoch在没有leader的时候,主要是逻辑时钟(与数字电路中的逻辑时钟相同)
Zookeeper非第一次启动的选举机制
通过ls /
查看当前znode包含的信息
查看当前数据节点详细信息
get /zookeeper
节点类型分为(两两进行组合)
创建节点不带序号的
通过create 进行创建
获取创建节点中命令的值
如果创建节点带序号的通过加上-s
参数
即便创建两个一样的,-s
自动会加上序号辨别两者不同
重启zkCli.sh之后,发现该结点仍然存在。
如果创建临时节点 加上参数 -e
临时节点不带序号-e
临时节点带序号-e -s
如果退出客户端,这些短暂节点将会被清除
断开客户端之后重启发现临时节点都被清除。
如果修改节点的值,通过set key value
即可
ZK客户端去ZK服务端中注册,说明要对哪个结点进行监听,注册完成之后,一旦监听的结点发生变化,ZK服务端就会去通知ZK客户端。
监听主要监听
3.5版本以下
监听数据值的变化
get /sanguo watch
3.5版本以上
监听数据值的变化
get -w /sanguo
在其中一台zookeeper客户端中监听/sanguo节点的变化,在另一台zookeeper客户端中修改/sanguo节点的值。
监听结点
另一台中修改结点值
此时如果另一台中再修改节点的值,监听处不会再显示监听结果,因为注册一次,就监听一次。
3.5版本以下
监听子节点的变化
ls /sanguo watch
3.5版本以上
监听数据值的变化
ls -w /sanguo
在其中一台zookeeper客户端中监听/sanguo节点的变化,在另一台zookeeper客户端中创建/sanguo的子节点。
监听处显示如下
删除某个结点
delete /sanguo/jin
删除全部节点
3.5版本以前
rmr /sanguo
3.5版本之后
deleteall /sanguo
查看结点状态
stat /sanguo
前提:启动zookeeper集群。
①创建maven工程
②添加pom文件
<dependencies>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>RELEASEversion>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-coreartifactId>
<version>2.8.2version>
dependency>
<dependency>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.4.9version>
dependency>
dependencies>
③拷贝log4j.properties文件到项目根目录
需要在项目的src/main/resources目录下,创建一个文件,命名为"log4j.properties",在文件中填入
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c]
- %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c]
- %m%n
④在客户端中生成具体代码
初始化并且负责监听节点
主要通过设置连接的服务器,以及超时参数,监听匿名函数new Watcher() {}
public class ZkClient {
//注意:逗号左右不能有空格
private String connectString ="192.168.200.131:2181,192.168.200.132,192.168.200.133";
private int sessionTimeout = 2000;
@Test
public void init() throws IOException {
ZooKeeper zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
public void process(WatchedEvent watchedEvent) {
}
});
}
}
创建一个新的节点
创建的节点必须在初始化节点这些之后,通过注解,在之前中加入before
getBytes()
Ids.OPEN_ACL_UNSAFE
允许所有人进行访问 @Test
public void create() throws KeeperException, InterruptedException {
String nodeCreated = zkClient.create("/zkt", "8811".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
执行之后可以看到zkt结点创建成功
获取子节点并且监听其变化
获取子节点通过getChildren()
关于获取子节点一共有两种
获取子节点第一个是路径,第二个是监听函数,如果使用true,则会使用初始化函数中重写的监听函数
@Test
public void getChildren() throws KeeperException,InterruptedException{
List<String> children = zkClient.getChildren("/",true);
for(String child : children){
System.out.println(child);
}
Thread.sleep(Long.MAX_VALUE);
}
注册一次生效一次,还需要在进行注册
希望通过延迟函数延迟程序的结束,继续监听Thread.sleep(Long.MAX_VALUE);
放在监听函数中就可以注册一次生效一次,之后在注册
public class ZkClient {
//注意:逗号左右不能有空格
private String connectString ="192.168.200.131:2181,192.168.200.132,192.168.200.133";
private int sessionTimeout = 2000;
private ZooKeeper zkClient;
@Before
public void init() throws IOException {
zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
public void process(WatchedEvent watchedEvent) {
System.out.println("--------------------------------------------");
List<String> children = null;
try {
children = zkClient.getChildren("/",true);
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
for(String child : children){
System.out.println(child);
}
System.out.println("------------------------------------");
}
});
}
@Test
public void create() throws KeeperException, InterruptedException {
String nodeCreated = zkClient.create("/zkt", "8811".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
@Test
public void getChildren() throws KeeperException,InterruptedException{
List<String> children = zkClient.getChildren("/",true);
for(String child : children){
System.out.println(child);
}
Thread.sleep(Long.MAX_VALUE);
}
}
创建zkt2结点之后可以立刻监听到。
发送给leader的时候
通俗解释:客户端给服务器的leader发送写请求,写完数据后给手下发送写请求,手下写完发送给leader,超过半票以上都写了则发回给客户端。之后leader在给其他手下让他们写,写完在发数据给leader
发送给follower的时候
通俗解释:客户端给手下发送写的请求,手下给leader发送写的请求,写完后,给手下发送写的请求,手下写完后给leader发送确认,超过半票,leader确认后,发给刻划断,之后leader在发送写请求给其他手下。
对于Zookeeper集群来说,除了Zookeeper是服务器之外,对于Zookeeper来说,无论是服务器还是客户端,都是客户端。即服务端是在zookeeper中注册服务,客户端在zookeeper中监听结点。
服务器代码
public class DistributeServer {
private String connectionString = "192.168.200.131:2181,192.168.200.132:2181,192.168.200.133:2181";
private int sessionTimeout = 2000;
private ZooKeeper zooKeeper;
public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
DistributeServer server = new DistributeServer();
//获取连接
server.getConnection();
//注册服务器到zk集群
server.register(args[0]);
//启动业务逻辑(睡觉)
server.business();
}
private void business() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
private void register(String hostname) throws KeeperException, InterruptedException {
zooKeeper.create("/servers/" + hostname,hostname.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(hostname + "已经上线了");
}
private void getConnection() throws IOException {
zooKeeper = new ZooKeeper(connectionString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
}
});
}
}
客户端代码
因为注册的时候记录一次
所以在初始化的时候,将其注册放在初始化内部getServerList();
public class DistributeClient {
private String connectionString = "192.168.200.131:2181,192.168.200.132:2181,192.168.200.133:2181";
private int sessionTimeout = 2000;
private ZooKeeper zk;
public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
DistributeClient client = new DistributeClient();
//1.获取zk连接
client.getConnection();
//2.监听/servers下面子节点的增加和删除
client.getServerList();
//3.业务逻辑(睡觉)
client.business();
}
private void business() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
private void getServerList() throws KeeperException, InterruptedException {
List<String> children = zk.getChildren("/servers", true);
ArrayList<String> servers = new ArrayList<>();
for(String child : children){
byte[] data = zk.getData("/servers/" + child, false, null);
servers.add(new String(data));
}
//打印
System.out.println(servers);
}
private void getConnection() throws IOException {
zk = new ZooKeeper(connectionString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
try {
getServerList();
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
具体测试的逻辑:
启动客户端,通过虚拟机进行create -e -s 节点信息(临时带序号的节点),或者delete进行更新
启动服务器,判定节点是否正常上下线
创建节点,判断是否是最小的节点
如果不是最小的节点,需要监听前一个的节点
健壮性可以通过CountDownLatch类
监听函数
如果集群状态是连接,则释放connectlatch
如果集群类型是删除,且前一个节点的位置等于该节点的文职,则释放该节点
判断节点是否存在不用一直监听
获取节点信息要一直监听getData
public class DistributedLock {
private final String connectionString = "192.168.200.131:2181,192.168.200.132:2181,192.168.200.133:2181";
private final int sessionTimeout = 2000;
private final ZooKeeper zk;
private CountDownLatch countDownLatch = new CountDownLatch(1);
private CountDownLatch waitLatch = new CountDownLatch(1);
private String waitPath;
private String currentMode;
public DistributedLock() throws IOException, InterruptedException, KeeperException {
//获取连接
zk = new ZooKeeper(connectionString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
//connectLatch 如果连接上zk 可以释放
if(watchedEvent.getState() == Event.KeeperState.SyncConnected){
countDownLatch.countDown();
}
//waitLatch 需要释放
if(watchedEvent.getType() == Event.EventType.NodeDeleted && watchedEvent.getPath().equals(waitPath)){
waitLatch.countDown();
}
}
});
//等待zk正常连接后,往下走程序
countDownLatch.await();
//判断根节点/locks是否存在
Stat stat = zk.exists("/locks", false);
if(stat == null){
//创建根节点
zk.create("/locks","locks".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
}
}
//对zk加锁
public void zkLock() {
//创建对应的临时带序号节点
try {
currentMode = zk.create("/locks/" + "seq-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
//判断创建的节点是否是序号最小的节点,如果是获取到锁,如果不是,监听他序号前一个节点
List<String> children = zk.getChildren("/locks",false);
//如果children只有一个值,那就直接获取锁;如果有多个节点,需要判断,谁最小。
if(children.size() == 1){
return;
}else{
Collections.sort(children);
//获取节点名称 seq-00000000
String thisNode = currentMode.substring("/locks/".length());
//通过seq-00000000获取该节点在children集合的位置
int index = children.indexOf(thisNode);
// System.out.println("index:"+index);
//判断
if(index == -1){
System.out.println("数据异常");
}else if(index == 0){
//就一个节点,可以获取锁了
return;
}else{
//需要监听 他前一个结点的变化
waitPath = "/locks/" + children.get(index - 1);
zk.getData(waitPath,true,null);
//等待监听
waitLatch.await();
return;
}
}
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//解锁
public void unZkLock(){
//删除节点
try {
zk.delete(currentMode,-1);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
}
具体锁的测试文件通过如下测试
public class DistributedLockTest {
public static void main(String[] args) throws InterruptedException, IOException, KeeperException {
final DistributedLock lock1 = new DistributedLock();
final DistributedLock lock2 =new DistributedLock();
new Thread(new Runnable() {
@Override
public void run() {
try{
lock1.zkLock();
System.out.println("线程1 启动,获取到锁");
Thread.sleep(5 * 1000);
lock1.unZkLock();
System.out.println("线程1 释放锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try{
lock2.zkLock();
System.out.println("线程2 启动,获取到锁");
Thread.sleep(5 * 1000);
lock2.unZkLock();
System.out.println("线程2 释放锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
原生的 Java API 开发存在的问题
(1)会话连接是异步的,需要自己去处理。比如使用CountDownLatch
(2)Watch 需要重复注册,不然就不能生效
(3)开发的复杂性还是比较高的
(4)不支持多节点删除和创建。需要自己去递归
curator是一个专门解决分布式锁的框架,解决了原生Java API开发分布式遇到的问题。
使用
添加依赖
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-frameworkartifactId>
<version>4.3.0version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-recipesartifactId>
<version>4.3.0version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-clientartifactId>
<version>4.3.0version>
dependency>
代码实现案例
public class CuratorLockTest {
public static void main(String[] args) {
//创建分布式锁1
InterProcessMutex lock1 = new InterProcessMutex(getCuratorFramework(), "/locks");
//创建分布式锁2
InterProcessMutex lock2 = new InterProcessMutex(getCuratorFramework(), "/locks");
new Thread(new Runnable() {
public void run() {
try {
lock1.acquire();
System.out.println("线程1 获取到锁");
lock1.acquire();
System.out.println("线程1 再次获取到锁");
Thread.sleep(5 * 1000);
lock1.release();
System.out.println("线程1 释放锁");
lock1.release();
System.out.println("线程1 再次释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
public void run() {
try {
lock2.acquire();
System.out.println("线程2 获取到锁");
lock2.acquire();
System.out.println("线程2 再次获取到锁");
Thread.sleep(5 * 1000);
lock2.release();
System.out.println("线程2 释放锁");
lock2.release();
System.out.println("线程2 再次释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
private static CuratorFramework getCuratorFramework() {
ExponentialBackoffRetry policy = new ExponentialBackoffRetry(3000, 3);
CuratorFramework client = CuratorFrameworkFactory.builder().connectString("192.168.200.131:2181,192.168.200.132:2181,192.168.200.133:2181")
.connectionTimeoutMs(2000)
.sessionTimeoutMs(2000)
.retryPolicy(policy)
.build();
//启动客户端
client.start();
System.out.println("zookeeper 启动成功");
return client;
}
}
企业中常考的面试有选举机制、集群安装以及常用命令
选举机制
半数机制,超过半数的投票通过,即通过。
(1)第一次启动选举规则:
投票过半数时,服务器 id 大的胜出
(2)第二次启动选举规则:
①EPOCH 大的直接胜出
②EPOCH 相同,事务 id 大的胜出
③事务 id 相同,服务器 id 大的胜出
集群安装
安装奇数台
服务器台数多:好处,提高可靠性;坏处:提高通信延时
常用命令
ls、get、create、delete