转载请说明出处:https://blog.csdn.net/LiaoHongHB/article/details/84958209
代码下载地址:https://download.csdn.net/download/liaohonghb/10843702
ZooKeeper 是一个高可用的分布式数据管理与系统协调框架。基于对 Paxos 算法的实现,使该框架保证了分布式环境中数据的强一致性,也正是基于这样的特性,使得 ZooKeeper 可以解决很多分布式问题。
随着互联网系统规模的不断扩大,大数据时代飞速到来,越来越多的分布式系统将 ZooKeeper 作为核心组件使用,如 Hadoop、Hbase、Kafka、Storm等,因此,正确理解 ZooKeeper 的应用场景,对于 ZooKeeper 的使用者来说显得尤为重要。本节主要将重点围绕数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master选举、分布式锁和分布式队列等方面来讲解 ZooKeeper 的典型应用场景及实现。
1、数据发布/订阅
发布/订阅模式是一对多的关系,多个订阅者对象同时监听某一主题对象,这个主题对象在自身状态发生变化时会通知所有的订阅者对象。使它们能自动的更新自己的状态。发布/订阅可以使得发布方和订阅方独立封装、独立改变。当一个对象的改变需要同时改变其他对象,而且它不知道具体有多少对象需要改变时可以使用发布/订阅模式。发布/订阅模式在分布式系统中的典型应用有配置管理和服务发现、注册。
配置管理是指如果集群中的机器拥有某些相同的配置并且这些配置信息需要动态的改变,我们可以使用发布/订阅模式把配置做统一集中管理,让这些机器格子各自订阅配置信息的改变,当配置发生改变时,这些机器就可以得到通知并更新为最新的配置。
服务发现、注册是指对集群中的服务上下线做统一管理。每个工作服务器都可以作为数据的发布方向集群注册自己的基本信息,而让某些监控服务器作为订阅方,订阅工作服务器的基本信息,当工作服务器的基本信息发生改变如上下线、服务器角色或服务范围变更,监控服务器可以得到通知并响应这些变化。
1.1、配置管理
所谓的配置中心,顾名思义就是发布者将数据发布到 ZooKeeper 的一个或一系列节点上,供订阅者进行数据订阅,进而达到动态获取数据的目的,实现配置信息的集中式管理和数据的动态更新。
发布/订阅系统一般有两种设计模式,分别是推(Push)模式和拉(Pull)模式。
- 推模式
服务端主动将数据更新发送给所有订阅的客户端。
- 拉模式
客户端通过采用定时轮询拉取。
ZooKeeper采用的是推拉相结合的方式:客户端向服务端注册自己需要关注的节点,一旦该节点的数据发生变更,那么服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个消息通知之后,需要主动到服务端获取最新的数据。
如果将配置信息存放到ZK上进行集中管理,那么通常情况下,应用在启动的时候会主动到ZK服务器上进行一次配置信息的获取,同时,在指定上注册一个Watcher监听,这样一来,但凡配置信息发生变更,服务器都会实时通知所有订阅的客户端,从而达到实时获取最新配置信息的目的。
下面我们通过一个“配置管理”的实际案例来展示ZK在“数据发布/订阅”场景下的使用方式。
在我们平常的应用系统开发中,经常会碰到这样的需求:系统中需要使用一些通用的配置信息,例如机器列表信息、运行时的开关配置、数据库的配置信息等。这些全局配置信息通常具备以下特性:
1)、数据量通常比较小
2)、数据内容在运行时会发生变化
3)、集群中各机器共享、配置一致
对于这类配置信息,一般的做法通常可以选择将其存储的本地配置文件或是内存变量中。无论采取哪种配置都可以实现相应的操作。但是一旦遇到集群规模比较大的情况的话,两种方式就不再可取。而我们还需要能够快速的做到全部配置信息的变更,同时希望变更成本足够小,因此我们需要一种更为分布式的解决方案。
接下来我们以“数据库切换”的应用场景展开,看看如何使用ZK来实现配置管理。
配置存储
在进行配置管理之前,首先我们需要将初始化配置存储到ZK上去,一般情况下,我们可以在ZK上选取一个数据节点用于配置的存储,我们将需要集中管理的配置信息写入到该数据节点中去。
配置获取
集群中每台机器在启动初始化阶段,首先会从上面提到的ZK的配置节点上读取数据库信息,同时,客户端还需要在该配置节点上注册一个数据变更的Watcher监听,一旦发生节点数据变更,所有订阅的客户端都能够获取数据变更通知。
配置变更
在系统运行过程中,可能会出现需要进行书局切换的情况,这个时候就需要进行配置变更。借助ZK,我们只需要对ZK上配置节点的内容进行更新,ZK就能够帮我们将数据变更的通知发送到各个客户端,每个客户端在接收到这个变更通知后,就可以重新进行最新数据的获取。
架构图:
Manage Server工作流程:
Work Server工作流程:
代码:
1、新建zookeeper-publishsubscribe工程,并新建 publishsubscribe-config,publishsubscribe-command,publishsubscribe-server1,publishsubscribe-server2,publishsubscribe-server3等子模块。
2、publishsubscribe-command:
db.properties:以数据库配置为例,其中db.properties定义了数据库连接的相关变量;
jdbc.driverName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/db_project
jdbc.username=root
jdbc.password=root
CommandServer:读取db.properties文件中的信息,如果信息发生改变,则同步更新到zookeeper集群对应节点中。
public class CommandServer {
private Logger logger = LoggerFactory.getLogger(getClass());
private ZkClient zkClient = null;
private String commandPath;
private String commandData;
private int SESSIONTIMEOUT = 15000;
private int CONNECTIONTIMEOUT = 15000;
public CommandServer() {
}
public CommandServer(String ipAddress, String commandPath) {
this.zkClient = new ZkClient(ipAddress, SESSIONTIMEOUT, CONNECTIONTIMEOUT, new BytesPushThroughSerializer());
this.commandPath = commandPath;
}
public void start() {
logger.info("publishsubscribe-command 服务开始启动,准备初始化....");
init();
}
public void init() {
logger.info("publishsubscribe-command 开始初始化.......");
boolean exists = zkClient.exists(commandPath);
try {
if (!exists) {
commandData = readProperties();
zkClient.createPersistent(commandPath, commandData.getBytes());
} else {
commandData = readProperties();
String readData = zkClient.readData(commandPath).toString();
if (!commandData.equals(readData)) {
zkClient.writeData(commandPath, commandData.getBytes());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public String readProperties() throws IOException {
InputStream inputStream = CommandServer.class.getClassLoader()
.getResourceAsStream("db.properties");
Properties properties = new Properties();
properties.load(inputStream);
String driverName = properties.getProperty("jdbc.driverName");
String url = properties.getProperty("jdbc.url");
String username = properties.getProperty("jdbc.username");
String password = properties.getProperty("jdbc.password");
String commandData = driverName.concat("&").concat(url).concat("&").concat(username).concat("&").concat(password);
return commandData;
}
}
WebListener:项目已启动就调用Command.start方法;
@Component
public class WebListener implements ServletContextListener {
private Logger logger = LoggerFactory.getLogger(getClass());
private String ipAddress = "192.168.202.128:2181,192.168.202.129:2181,192.168.202.130:2181";
@Override
public void contextInitialized(ServletContextEvent sce) {
CommandServer commandServer = new CommandServer(ipAddress,"/command");
commandServer.start();
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
3、publishsubscribe-config:
ConfigServer:创建config根节点以及用来统一数据库配置信息的子节点和可用工作服务器的list列表子节点(服务注册)并监听这command节点和servers节点。
public class ConfigServer {
private Logger logger = LoggerFactory.getLogger(getClass());
private ZkClient zkClient = null;
private String configRootPath;
private String configServerPath;
private String configJdbcPath;
private String serverPath;
private String commandPath;
private int SESSIONTIMEOUT = 15000;
private int CONNECTIONTIMEOUT = 15000;
public ConfigServer() {
}
public ConfigServer(String ipAddress, String configRootPath, String configServerPath, String configJdbcPath,
String serverPath, String commandPath) {
this.zkClient = new ZkClient(ipAddress, SESSIONTIMEOUT, CONNECTIONTIMEOUT, new BytesPushThroughSerializer());
this.configRootPath = configRootPath;
this.configServerPath = configServerPath;
this.configJdbcPath = configJdbcPath;
this.serverPath = serverPath;
this.commandPath = commandPath;
}
public void start() {
logger.info("publishsubscribe-config启动,准备初始化.....");
init();
}
public void init() {
logger.info("publishsubscribe-config 开始初始化.....");
subscribeServer();
subscribeCommand();
}
/**
* 订阅工作服务器的节点变化
*/
public void subscribeServer() {
boolean exists = zkClient.exists(configRootPath);
if (!exists) {
zkClient.createPersistent(configRootPath);
}
boolean exists1 = zkClient.exists(configRootPath.concat(configServerPath));
if (!exists1) {
zkClient.createPersistent(configRootPath.concat(configServerPath));
}
zkClient.subscribeChildChanges(serverPath, new IZkChildListener() {
@Override
public void handleChildChange(String s, List<String> list) throws Exception {
logger.info("当前可以工作服务器节点信息是:{}", list.toString());
zkClient.writeData(configRootPath.concat(configServerPath), list.toString().getBytes());
}
});
}
/**
* 订阅配置服务器的节点变化
*/
public void subscribeCommand() {
boolean exists = zkClient.exists(configRootPath);
if (!exists) {
zkClient.createPersistent(configRootPath);
}
boolean exists1 = zkClient.exists(configRootPath.concat(configJdbcPath));
if (!exists1) {
zkClient.createPersistent(configRootPath.concat(configJdbcPath));
}
zkClient.subscribeDataChanges(commandPath, new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
logger.info("节点:{}内容发送变化", s);
zkClient.writeData(configRootPath.concat(configJdbcPath), o.toString().getBytes());
}
@Override
public void handleDataDeleted(String s) throws Exception {
logger.info("节点:{}被删除", s);
}
});
}
}
WebListener:同上理;
@Component
public class WebListener implements ServletContextListener {
private Logger logger = LoggerFactory.getLogger(getClass());
private String ipAddress = "192.168.202.128:2181,192.168.202.129:2181,192.168.202.130:2181";
@Override
public void contextInitialized(ServletContextEvent sce) {
ConfigServer configServer = new ConfigServer(ipAddress, "/config", "/workServer", "/jdbcInfo", "/servers",
"/command");
configServer.start();
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
4、publishsubscribe-server1:工作服务器,启动注册到zookeeper集群环境中;
WorkServer:服务注册到zookeeper集群中,并/servers/WorkServer1保存该服务器信息
public class WorkServer {
private Logger logger = LoggerFactory.getLogger(getClass());
private ZkClient zkClient = null;
private String serverPath;
private String serverData;
private int SESSIONTIMEOUT = 15000;
private int CONNECTIONTIMEOUT = 15000;
private String ownServerPath;
public WorkServer() {
}
public WorkServer(String ipAddress, String serverPath, String ownServerPath) {
this.zkClient = new ZkClient(ipAddress, SESSIONTIMEOUT, CONNECTIONTIMEOUT, new BytesPushThroughSerializer());
this.serverPath = serverPath;
this.ownServerPath = ownServerPath;
}
public void start() {
logger.info("publishsubscribe-server1启动,准备初始化....");
init();
}
public void init() {
boolean exists = zkClient.exists(serverPath);
if (!exists) {
zkClient.createPersistent(serverPath);
}
boolean exists1 = zkClient.exists(serverPath.concat(ownServerPath));
if (!exists1) {
zkClient.createPersistent(serverPath.concat(ownServerPath),"WorkServer1".getBytes());
}
zkClient.subscribeDataChanges("/config/jdbcInfo", new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
logger.info("数据库信息发生改变");
logger.info("当前数据库信息为:{}", zkClient.readData(s).toString());
}
@Override
public void handleDataDeleted(String s) throws Exception {
}
});
}
}
WebListener:同上理
@Component
public class WebListener implements ServletContextListener {
private Logger logger = LoggerFactory.getLogger(getClass());
private String ipAddress = "192.168.202.128:2181,192.168.202.129:2181,192.168.202.130:2181";
@Override
public void contextInitialized(ServletContextEvent sce) {
WorkServer workServer = new WorkServer(ipAddress, "/servers", "/WorkServer1");
workServer.start();
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
5、publishsubscribe-server2和publishsubscribe-server3同publishsubscribe-server1只是需要将节点名称换为/WorkServer2和/WorkServer3。
6、运行及运行结果:
先运行publishsubscribe-config模块,然后运行publishsubscribe-command以及publishsubscribe-server1-3模块。
可修改publishsubscribe-command中db.properities中的信息,然后重新运行publishsubscribe-command模块,查看publishsubscribe-config控制台打印的信息;
可删除/servers/WorkServer3节点,查看publishsubscribe-config控制台打印的信息;