中间件-分布式之Zookeeper

896 阅读5分钟

4. 分布式

4.1 Zookeeper

Zookeeper提供两个功能;

  • 原子广播
  • 崩溃恢复

4.1.1 Zookeeper部署

  • 单机模式

单机模式的Zookeeper服务启动相对简单,切换到安装目录下的bin文件夹,执行zkServer脚本,Zookeeper就会根据默认的配置文件启动:

如果提示:错误: 找不到或无法加载主类 org.apache.zookeeper.server.quorum.QuorumPeerMain,则需要下载apache-zookeeper-x.y,z-bin这种形式的安装包,注意文件夹名称里面的bin;

可以通过访问**http://localhost:8080/commands**来访问Zookeeper的AdminServer,AdminServer是嵌入式Jetty服务器,为Zookeeper指令提供Http接口;

  • 伪集群

配置参考:[A Guide to Deployment and Administration][zookeeper.apache.org/doc/r3.5.0-…]

在单台电脑上构建一个3个节点的Zookeeper伪集群,节点配置的区别在于data目录及端口;

将下载的Zookeeper安装包解压到目标文件夹,这里以本机的C:\Zookeeper为例,首先将其拷贝到另外三个文件夹zookeeper1~3下:

cp -r .\apache-zookeeper-3.6.1-bin\ .\zookeeper1

文件夹与文件操作,以zookeeper1为例:

  • 为每个节点创建data与log目录;
mkdir .\zookeeper1\data
mkdir .\zookeeper1\logs
  • 为每个节点zookeeper的id记录文件myid,写入id,id由用户定义,数字类型;
touch .\zookeeper1\data\myid
echo 1>.\zookeeper1\data\myid

zoo.cfg的详细配置字段可参考末尾的思维导图,配置信息:

  • 三个必选项
    • tickTime,时间单位
    • dataDir,数据目录,主要存储内存数据库的快照,事务日志,以及myid文件
    • clientPort,Socket端口,用于监听请求
  • 集群quorum

server.x=host:port1:port2,其中x为zookeeper服务器的id,在dataDir路径下的myid文件中标识,port1为通信端口,port2为选举端口

注意事项:

每个zookeeper服务器会启动不同端口来进行通信或服务,这种情况下Socket地址(host:port)需保证唯一,具体到本例中:

  • server.x后面的任意host:port组合均需为唯一,在伪集群环境下,host都一样,所以三个zookeeper服务器的六个端口均需设置为不同端口
  • zookeeper服务器默认在8080端口启动adminServer,可通过admin.serverPort来配置端口,避免端口冲突

修改每个节点的配置文件信息:

以zookeeper1的zoo.cfg为例:

tickTime=2000
dataDir=data
dataLogDir=logs
clientPort=2181
initLimit=5
syncLimit=2
admin.serverPort=8200
server.1=localhost:7770:3880
server.2=localhost:7771:3881
server.3=localhost:7772:3882

zookeeper2的zoo.cfg大部分与zookeeper1一致,不同部分如下:

clientPort=3181
admin.serverPort=8201

zookeeper3与zookeeper1的zoo.cfg不同的部分:

clientPort=4181
admin.serverPort=8202

在控制台通过zkServer脚本启动,可以看到入口为QuorumPeerMain类,它会读取zoo.cfg的配置,启动zookeeper:

如果出现找不到myid的提示,首先要看dataDir对应目录是否配置正确,其次要保证dataDir下有myid文件,其内容为整数;

启动本地伪集群的三个zookeeper服务后,三个节点采用Fast Leader Election算法选举主节点,然后完成集群配置;

注意:节点的启动顺序会影响leader节点的选举,因为sid(server id)的关系为:sid3>sid2>sid1,所以sid3与sid2的启动顺序会影响达成quorum的顺序,一旦sid2先于sid3启动,则sid2先达成quorum条件,形成法定人数,sid3只能成为following状态

伪集群搭建起来之后就可以通过在控制台关闭服务的方式模拟如下情景,通过跟踪日志可以获得Zookeeper运行机制的详细信息:

  • follower节点崩溃恢复
  • leader节点崩溃重新选举

4.1.2 代码示例

Zookeeper官网,[ZooKeeper Java Example][zookeeper.apache.org/doc/r3.6.1/…zookeeper-docs

org.apache.zookeeper包中的核心类是Zookeeper,通过指定clientPortAddress来访问Zookeeper服务器来来获取会话,会话由SessionID标识;

zookeeper中的数据以树的形式组织,其中每个节点为一个znode,路径下面数据的变化以事件监听机制进行处理;

以下的实例代码分为两个类:

  • DataWatcher,除了必要的znode节点初始化之外,实现Watcher接口,重写**process(WatchedEvent var1)**方法,针对NodeDataChanged事件进行处理;
  • DataUpdater,定时更新**/MyConfig下的数据,通过UUID.randomUUID()**来生成数据,保证数据变更;
public class DataWatcher implements Watcher, Runnable {
    private static String hostPort = "localhost:2181";
    private static String zooDataPath = "/MyConfig";
    byte zoo_data[] = null;
    ZooKeeper zk;

    public DataWatcher() {
        try {
            zk = new ZooKeeper(hostPort, 2000, this);
            System.out.printf("zookeeper session ID:%s\n",zk.getSessionId());
            if (zk != null) {
                try {
                    //Create the znode if it doesn't exist, with the following code:
                    if (zk.exists(zooDataPath, this) == null) {
                        zk.create(zooDataPath, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
                    }
                } catch (KeeperException | InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public void printData() throws InterruptedException, KeeperException {
        zoo_data = zk.getData(zooDataPath, this, null);
        String zString = new String(zoo_data);
        // The following code prints the current content of the znode to the console:
        System.out.printf("\nCurrent Data @ ZK Path %s: [%s]\n", zooDataPath, zString);
    }
    @Override
    public void process(WatchedEvent event) {
        System.out.printf("\nEvent Received: %s\n", event.toString());
        //We will process only events of type NodeDataChanged
        if (event.getType() == Event.EventType.NodeDataChanged) {
            try {
                printData();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (KeeperException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException, KeeperException {
        DataWatcher dataWatcher = new DataWatcher();
        dataWatcher.printData();
        dataWatcher.run();
    }
    public void run() {
        try {
            synchronized (this) {
                while (true) {
                    wait();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        }
    }
}

DataUpdater:

public class DataUpdater implements Watcher {
    private static String hostPort = "localhost:2181";
    private static String zooDataPath = "/MyConfig";
    ZooKeeper zk;

    public DataUpdater() throws IOException {
        try {
            zk = new ZooKeeper(hostPort, 2000, this);
            System.out.printf("zookeeper session ID:%s\n",zk.getSessionId());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    // updates the znode path /MyConfig every 5 seconds with a new UUID string.
    public void run() throws InterruptedException, KeeperException {
        while (true) {
            String uuid = UUID.randomUUID().toString();
            byte zoo_data[] = uuid.getBytes();
            zk.setData(zooDataPath, zoo_data, -1);
            try {
                Thread.sleep(5000); // Sleep for 5 secs
            } catch(InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        DataUpdater dataUpdater = new DataUpdater();
        dataUpdater.run();
    }
    @Override
    public void process(WatchedEvent event) {
        System.out.printf("\nEvent Received: %s\n", event.toString());
    }
}

启动后,两个类分别的输出:

# DataMonitor
zookeeper session ID:0
Event Received: WatchedEvent state:SyncConnected type:None path:null
Event Received: WatchedEvent state:SyncConnected type:NodeCreated path:/MyConfig
Current Data @ ZK Path /MyConfig: [] 
Event Received: WatchedEvent state:SyncConnected type:NodeDataChanged path:/MyConfig
Current Data @ ZK Path /MyConfig: [b3ac09d9-49ce-470e-ae29-f0d441b9e6b1]
Event Received: WatchedEvent state:SyncConnected type:NodeDataChanged path:/MyConfig
Current Data @ ZK Path /MyConfig: [8d7b7b5c-f69a-445f-9bb0-96b2abe8b1be]
...
# DataUpdater
zookeeper session ID:0
Event Received: WatchedEvent state:SyncConnected type:None path:null

4.1.3 可视化工具

Zookeeper节点中的数据可以在ZooInspector中查看,ZooInspector命令行启动:

java -jar zookeeper-dev-ZooInspector.jar

image-20200818181551256

4.1.x 思维导图总结

  • zookeeper 单机配置:

  • 集群设置:

  • Zookeeper Developer

参考资料: