一、Zookeeper简介
Zookeeper是一个开源的分布式的,为分布式应用提供协调服务的Apache项目。
1.1 Zookeeper工作机制
Zookeeper是基于观察者设计模式设计的分布式服务管理框架,它负责存储和管理大家关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper就将负责通知已经在Zookeeper上注册的那些观察者做出相应的反应。
1.2 Zookeeper特点
- Zookeeper集群有一个Leader和多个Follower组成
- 集群中要有半数以上节点存活,Zookeeper集群就能正常工作
- 全局数据一致:每个节点保存相同的数据,Client无论连接哪个节点数据都一样
- 更新请求顺序进行:来自同一个Client更新请求按照其发送顺序依次进行
- 数据更新原子性:一次数据更新要么成功,要么失败
- 实时性:在一定时间范围内,Client能读到最新的数据
1.3 应用场景
- 统一命名服务
- 统一配置管理
- 统一集群管理
- 服务器动态上下线[本文实现目标]
- 软负载均衡
二、Zookeeper简单Shell命令
启动Zookeeper服务
zkServer.sh start
关闭Zookeeper服务
zkServer.sh stop
查看Zookeeper状态
zkServer.sh status
当搭建的是完全分布式时,集群的Zookeeper服务必须启动半数以上才能正常工作,且myid最大的为leader[即当启动个数超过半数的那一刻myid最大的为leader,此后及时更大的myid节点加入集群为不会更改leader]
启动Zookeeper客户端
zkCli.sh
以下是客户端命令[类似Linux]
查看帮助
help
查看指定路径的节点
ls path [watch]
ls2 path //查看详细信息
在指定路径创建节点
create [-s] [-e] path data
-e 创建的节点是临时的 客户端退出该节点自动删除
-s 创建带序号的节点,原节点下没有节点从0开始,若已经有n个节点则从n+1开始
data 为该节点存储的信息,一定要有否则无法创建
这个是实现服务器上下线功能的基础
获取指定节点上的信息
get path [watch]
注意到ls
和get
有watch可选项,这是实现服务器上下线功能的核心,其中ls
为监视指定路径的节点个数变化,get
为监视指定节点的信息变化
ls /file watch //监视file节点的节点变化
delete /file/idea //删除file节点下的idea节点
//客户端发出节点变化信息
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/file
这时候我们已经具备实现此功能的所有知识
三、基本思路
服务器上线向Zookeeper集群的servers节点下注册信息,即在servers节点下创建临时的带序号的存有服务器ip,hostname等信息的节点,当servers节点下有新增节点客户端通过ls /servers watch
即可实现监听功能,并且通过get /servers/server000000x
获取节点的信息便可得知是哪台服务器上线,服务器下线临时节点自动删除客户端监听原理相同。因此大致流程如下:
- 服务端
- 获取Zookeeper连接
- 注册信息
- 业务逻辑
- 客户端
- 获取Zookeeper连接
- 注册监听
- 业务逻辑
四、代码实现
4.1 相关依赖
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.10</version>
</dependency>
</dependencies>
关于IDEA2020.1的Maven找不到包的问题,可以再pom.xml文件目录下进入cmd执行mvn idea:idea
实测可以解决。
解决log4j打印日志问题,可以再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
4.2 服务端实现
1. 获取Zookeeper集群连接
private void getConnection() throws IOException {
zkClient = new ZooKeeper(
ResourceBundle.getBundle("zookeeper").getString("zookeeperName"),
2000,
watch -> {
}
);
}
Zookeeper构造器如下
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)
//connectionString 连接对象
//sessionTimeout 最大连接时间
//watcher 监听逻辑
我的zoo.cfg内容如下
# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just
# example sakes.
dataDir=/usr/local/soft/zookeeper-3.4.10/zkData
# the port at which the clients will connect
clientPort=2181
# the maximum number of client connections.
# increase this if you need to handle more clients
#maxClientCnxns=60
#
# Be sure to read the maintenance section of the
# administrator guide before turning on autopurge.
#
# http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
#
# The number of snapshots to retain in dataDir
#autopurge.snapRetainCount=3
# Purge task interval in hours
# Set to "0" to disable auto purge feature
#autopurge.purgeInterval=1
server.1=master:2888:3888
server.2=slave01:2888:3888
server.3=slave02:2888:3888
配置文件规定了端口号即2181
,因此连接对象为ip:2181
,通过配置文件读取
2. 注册信息
private void registered() throws KeeperException, InterruptedException {
zkClient.create(
"/servers/server", //创建节点的路径
dealHostName().getBytes(), //存储到节点上的信息
ZooDefs.Ids.OPEN_ACL_UNSAFE, //类似一种权限
CreateMode.EPHEMERAL_SEQUENTIAL //即-e -s
);
}
3. 业务逻辑
private void business() throws InterruptedException {
System.out.println(hostname + " is online");
Thread.sleep(Long.MAX_VALUE);
}
为了让服务器注册完信息后保持存活,方便控制服务器上下线实时监测客户端是否发生变化。
4. 服务端完整代码
package zookeeper.project.driver;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.util.ResourceBundle;
/*
* Server端,维护一些服务器的上下线通知
* 当服务器上线时,向zookeeper集群servers节点上注册短暂的server节点并存储服务器名
* */
public class Server {
private String hostname;
private ZooKeeper zkClient;
//获取连接
private void getConnection() throws IOException {
zkClient = new ZooKeeper(
ResourceBundle.getBundle("zookeeper").getString("zookeeperName"),
2000,
watch -> {
}
);
}
//注册信息
private void registered() throws KeeperException, InterruptedException {
zkClient.create(
"/servers/server",
dealHostName().getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL
);
}
//业务逻辑
private void business() throws InterruptedException {
System.out.println(hostname + " is online");
Thread.sleep(Long.MAX_VALUE);
}
//处理hostname
private String dealHostName() {
try {
hostname = Inet4Address.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
hostname = "0.0.0.0";
}
return hostname;
}
//服务器上线
public void start() {
try {
getConnection();
registered();
business();
} catch (IOException | InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
}
4.2 客户端实现
1. 获取连接
private void getConnection() throws IOException {
zkClient = new ZooKeeper(
ResourceBundle.getBundle("zookeeper").getString("zookeeperName"),
2000,
watch -> {
try {
//回滚,持续监听
String format = simpleDateFormat.format(new Date());
System.out.println("\n=========== " + format + " ===========");
registered();
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}
);
}
2. 注册监听
private void registered() throws KeeperException, InterruptedException {
List<String> children = zkClient.getChildren("/servers", true);
ArrayList<String> list = new ArrayList<>();
for (String path : children) {
list.add(new String(zkClient.getData("/servers/" + path, false, null)));
}
for (String ip : list) {
try {
ip += (" | " + ResourceBundle.getBundle("ip").getString(ip));
} catch (MissingResourceException e) {
ip += " | 未知IP";
}
System.out.println(" \t " + ip);
}
}
3. 业务逻辑
private void business() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
4. 客户端完整代码
package zookeeper.project.driver;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooKeeper;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;
/*
* Client需要实时监测zookeeper集群servers下的节点变化,并获取节点存储的信息,即可得知服务器的上下线通知
* */
public class Client {
private ZooKeeper zkClient;
private final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//获取连接
private void getConnection() throws IOException {
zkClient = new ZooKeeper(
ResourceBundle.getBundle("zookeeper").getString("zookeeperName"),
2000,
watch -> {
try {
//回滚,持续监听
String format = simpleDateFormat.format(new Date());
System.out.println("\n=========== " + format + " ===========");
registered();
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}
);
}
//注册监听
private void registered() throws KeeperException, InterruptedException {
List<String> children = zkClient.getChildren("/servers", true);
ArrayList<String> list = new ArrayList<>();
for (String path : children) {
list.add(new String(zkClient.getData("/servers/" + path, false, null)));
}
for (String ip : list) {
try {
ip += (" | " + ResourceBundle.getBundle("ip").getString(ip));
} catch (MissingResourceException e) {
ip += " | 未知IP";
}
System.out.println(" \t " + ip);
}
}
//业务逻辑
private void business() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
//启动客户端监听
public void start() {
try {
getConnection();
//registered();
business();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
4.3 测试
package zookeeper.project.client;
import zookeeper.project.driver.Client;
public class Demo {
public static void main(String[] args) {
new Client().start();
}
}
package zookeeper.project.server;
import zookeeper.project.driver.Server;
public class ServerDemo1 {
public static void main(String[] args) {
new Server().start();
}
}
控制台输出
=========== 2020-06-24 18:21:28 ===========
192.168.42.201 | 未知IP
192.168.2.128 | WangJun
192.168.2.129 | LiYuHang
ip后面的备注通过配置文件读取,相当于ip白名单,将合法的ip写入配置文件中即可
- 注:sessionTimeout设置的是2000,即连接超时2s视为连接失败,亲测局域网下设置2000连接不稳定可以稍微设置大一点