基于Zookeeper实现服务器上下线通知

一、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]

注意到lsget有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连接不稳定可以稍微设置大一点

猜你喜欢

转载自blog.csdn.net/qq_41858402/article/details/106949654