zookeeper集群介绍与实践

       如果你查看zookeeper官方文档页面,你会看见一只黄色的大象和hadoop的字样,是的,zookeeper就是hadoop的一个组件,提供分布式协调服务。所谓协调就是协助别的系统完成某些功能,先举一个例子,我们知道一个分布式集群需要对外提供一个统一的数据视图,集群都需要选举出一个master/leader节点负责组织协调整个集群的所有节点达到数据的一致性,这个maste或leader在集群中是要唯一的,比如Hadoop的NamdeNode、HBase的HMaster、kafka的master节点等等,那么你很容易想到,在集群中运用算法选出一个leader\master不就行,比如大名鼎鼎的paxos算法,你说的没有错,但是你想想,既然选举一个leader\master是很多分布式集群共有的功能需求,何不把这个功能提取出来做成一个公共服务,其他的分布式系统的选举功能对接这个服务去完成不就好了,zookeeper就能提供这样的服务,当然zookeeper还能被利用做很多事情,还能做什么呢?一大推呢!比如:数据发布、订阅,负载均衡,命名服务,分布式协调\通知,集群管理,分布式锁,分布式队列等等,包括上面提到的master选举。听到这儿,你肯定在想这个zookeeper提供的功能这么强大啊,是不是望而却步了!别啊,才没有呢?其实zookeeper对外就提供了两个功能,1、读写客户端提交的数据。2、为客户端提供数据节点变化监听服务。就这两个功能跟上面那些很虎人的功能相比较,也太简单了吧。如果你是zeekoopeer,套用现在很流行的一句话,你肯定会说:“你不说我都不知道老子这么厉害呢”。

       是的,zookeeper很简单嘛,但是也不简单呀!简单是因为zookeeper功能简单,我上面说的那两个,你利用zookeeper提供的API很容易就用起来,你要做的就是写数据(常见的文件系统的路径名)和一个实现了指定接口的监听类,下面有我写的一个demo,看一遍你就马上能自己上手写,zookeeper集群也非常好搭建,装一个虚拟机软件,新建三个linux系统虚拟机器,搭建一个zk集群,我有篇zookeeper集群搭建的博客以供参考。这样你就可以利用zk写些简单的功能,比如服务注册与发现,分布式锁等等,然后连接虚拟机的zk集群,你就能把程序跑起来,这是你学习zk第一步要做的事情。这是zk简单的部分,而zk难就难在它的分布式,集群的选举、数据的一致性、事务管理、集群间通信、C\S间通信,工作流程等等这些都是你在会用zk的基础上进阶学习的。zk源码也是很难读的,特别是其中基于fast paxos算法的leader选举实现,集群中一大推的状态的定义和来回转换的实现,想要搞清楚这些,是很难的,但也没关系嘛,这个不影响对zk的学习,学习总有一个过程嘛,现在虽然看不懂,说不定过几年随着你能力的提升,你再回过头来看又看懂了,所以学习zk,包括学习很多中间件,你既不能浅尝则止,你需要看一部分重要源代码,也不能指望一步到位,如果你想彻彻底底的看懂每一行代码,当你的技术知识体系不够,技术栈不够深的时候,你这样做,可能适得其反,甚至散失兴趣。

       首先说说zookeeper存储的数据结构,也就是当你连接上zk集群后,向zk集群里写入的数据是什么格式,这个格式就是一个描述一个文件路径path的字符串,比如/root、/root/serverList,一个路径就是一个节点Znode,path就是这个节点的唯一标识,Znode里还可以包含数据,这个数据你可以理解为就是节点的value,这样这些节点就构成了一个由层次的目录结构。Znode有四种节点类型,新增节点的时候需要指定是哪种类型,分别为PERSISTENT、PERSISTENT_SEQUENTIAL、EPHEMERAL、EPHEMERAL_SEQUENTIAL,PERSISTENT代表这个新增节点是永久的,写入磁盘里的,EPHEMERAL代表这个节点是暂时,当客户端连接zk集群的会话session关闭时,这个节点就被删除,这两种节点后面加SEQUENTIAL,代表这个节点是有顺序号的,顺序号附加在节点名称后面,由父节点维护的一个单调递增的计数器生成。了解上面这些,zk你就可以用起来了,下面我给出一个样例,某个客户端通过zk获取另一个客户端机器的ip和端口,你只有先会用起来,你才能继续后面zk的深入学习,比如:工作原理、集群选举等等。

引入zk包

<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.9</version>
</dependency>

 

/**
 * <p>
 * </p>
 *
 * @author huqingniu
 * @version 1.0.0
 * @date 2019/3/16
 */
public class ZookeeperBeanFactory {

    private static final int SESSION_TIME = 2 * 1000;

    private String hosts;

    private Watcher watcher;

    public ZookeeperBeanFactory(){}

    public ZookeeperBeanFactory(String hosts, Watcher watcher){
        this.hosts = hosts;
        this.watcher = watcher;
    }

    public ZooKeeper getZooKeeper() throws IOException {
        return new ZooKeeper(hosts,SESSION_TIME,watcher);
    }
}

/**
 * <p>
 * </p>
 *
 * @author huqingniu
 * @version 1.0.0
 * @date 2019/3/16
 */
public class ZookeeperManager {

    private ZooKeeper zooKeeper;

    public ZookeeperManager(ZooKeeper zooKeeper){
        this.zooKeeper = zooKeeper;
    }

    public void createPath(String path) throws UnsupportedEncodingException, KeeperException, InterruptedException {
        createPath(path,path.getBytes("UTF-8"));
    }

    public void createPath(String path, byte[] data) throws KeeperException, InterruptedException {
        createPath(path,data,CreateMode.PERSISTENT);
    }

    public void createPath(String path, byte[] data, CreateMode createMode) throws KeeperException, InterruptedException {
        zooKeeper.create(path,data, ZooDefs.Ids.OPEN_ACL_UNSAFE,createMode);
    }

    public Stat existed(String path) throws KeeperException, InterruptedException {
        return zooKeeper.exists(path,false);
    }

    public byte[] getData(String path) throws KeeperException, InterruptedException {
        return zooKeeper.getData(path,false,null);
    }

    public List<String> getChild(String path, Watcher watcher) throws KeeperException, InterruptedException{
        try {
            return zooKeeper.getChildren(path,watcher);
        } catch (KeeperException e) {
            return new ArrayList<>();
        } catch (InterruptedException e) {
            throw e;
        }
    }

    public boolean getState(){
        ZooKeeper.States states = zooKeeper.getState();
        return states.isAlive();
    }

    public void close(){
        try {
            zooKeeper.close();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

/**
 * <p>
 * </p>
 *
 * @author huqingniu
 * @version 1.0.0
 * @date 2019/3/16
 */
public abstract class AbstractService implements Watcher{

    private static Logger logger = LoggerFactory.getLogger(AbstractService.class);

    protected String ROOT_PATH = "/root";

    protected String SERVER_PATH = ROOT_PATH + "/service";

    protected ZookeeperManager zookeeperManager;

    protected void init() throws IOException {
        String zookeeperAddress = "192.168.79.152:2181,192.168.79.153:2181,192.168.79.154:2181";
        ZookeeperBeanFactory zookeeperBeanFactory = new ZookeeperBeanFactory(zookeeperAddress,this);
        zookeeperManager = new ZookeeperManager(zookeeperBeanFactory.getZooKeeper());
        logger.info("zk 连接成功...");
    }
}

/**
 * <p>
 * </p>
 *
 * @author huqingniu
 * @version 1.0.0
 * @date 2019/3/16
 */
public class Provider extends AbstractService {

    private static Logger logger = LoggerFactory.getLogger(Provider.class);

    public void registry() throws Exception {
        init();
        Stat rootStat = zookeeperManager.existed(ROOT_PATH);
        if (null == rootStat) {
            zookeeperManager.createPath(ROOT_PATH);
            logger.info("zookeeper节点{}创建成功", ROOT_PATH);
        }
        Stat serverListStat = zookeeperManager.existed(SERVER_PATH);
        if (null == serverListStat) {
            zookeeperManager.createPath(SERVER_PATH);
            logger.info("zookeeper节点{}创建成功", SERVER_PATH);
        }
        //注册 server Node
        String serverNode = SERVER_PATH + "/server_" + IpUtils.getLocalIP().replaceAll("\\.", "_");
        String serverData = IpUtils.getLocalIP() + ":8080";
        Stat serverNodeStat = zookeeperManager.existed(serverNode);
        if (null == serverNodeStat) {
            zookeeperManager.createPath(serverNode, serverData.getBytes("UTF-8"), CreateMode.EPHEMERAL);
            logger.info("zookeeper节点{}创建成功", serverNode);
        }
        logger.info("服务地址注册完成:{}", serverData);
    }

    @Override
    public void process(WatchedEvent watchedEvent) {
    }
}

/**
 * <p>
 * </p>
 *
 * @author huqingniu
 * @version 1.0.0
 * @date 2019/3/16
 */
public class Consumer extends AbstractService {

    private static Logger logger = LoggerFactory.getLogger(Consumer.class);

    private static List<String> serverList = new ArrayList<>();

    public List<String> search() throws IOException {
        if (!serverList.isEmpty()){
            logger.info("从本地缓存中查询到provider服务器列表");
            return serverList;
        }
 init();
        return queryServerList();
    }

    public List<String> queryServerList() throws IOException {
        try {
            Stat rootStat = zookeeperManager.existed(ROOT_PATH);
            if (null == rootStat){
                logger.error("zookeeper节点{}不存在",ROOT_PATH);
                return new ArrayList<>();
            }
            Stat serverStat = zookeeperManager.existed(SERVER_PATH);
            if (null == serverStat){
                logger.error("zookeeper节点{}不存在",SERVER_PATH);
                return new ArrayList<>();
            }
            List<String> nodes = zookeeperManager.getChild(SERVER_PATH,this);
            logger.info("查询到共有{}服务节点:{}",nodes.size(),nodes);
            serverList.clear();
            for (String node : nodes){
                byte[] bytes = zookeeperManager.getData(SERVER_PATH + "/" + node);
                serverList.add(new String(bytes,"UTF-8"));
                logger.debug("服务节点:{},数据:{}",node,new String(bytes,"UTF-8"));
            }
        } catch (Exception e) {
            logger.error("获取服务异常:{}",e.getMessage());
        } finally {
            return serverList;
        }
    }

    @Override
    public void process(WatchedEvent watchedEvent) {
        logger.warn("监听到服务节点变换:{}",watchedEvent);
        if (watchedEvent.getType() == Event.EventType.NodeChildrenChanged
                && SERVER_PATH.equals(watchedEvent.getPath())){
            try {
                queryServerList();
            } catch (IOException e) {
                logger.error("IO Exception:{}",e.getMessage());
            }
        }
    }
}

/**
 * <p>
 * </p>
 *
 * @author huqingniu
 * @version 1.0.0
 * @date 2019/3/16
 */
public class Main {

    private static Logger logger = LoggerFactory.getLogger(Provider.class);

    @SuppressWarnings("Duplicates")
    public static void main(String[] args) {
        try {
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    Provider provider = new Provider();
                    try {
                        provider.registry();
                    } catch (Exception e) {
                        logger.error("注册异常:{}", e);
                    }
                }
            });
            thread1.start();
            thread1.join();
            logger.info("1------------------------------------------");
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    Consumer consumer = new Consumer();
                    try {
                        List<String> serverList = consumer.search();
                        logger.info("获取服务列表1:{}",serverList);
                    } catch (Exception e) {
                        logger.error("查询服务异常:{}",e);
                    }
                }
            });
            thread2.start();
            thread2.join();
            logger.info("2-----------------------------------------");
            Thread thread3 = new Thread(new Runnable() {
                @Override
                public void run() {
                    Consumer consumer = new Consumer();
                    try {
                        List<String> serverList = consumer.search();
                        logger.info("获取服务列表2:{}",serverList);
                    } catch (Exception e) {
                        logger.error("查询服务异常:{}",e);
                    }
                }
            });
            thread3.start();
            thread3.join();
            logger.info("3-----------------------------------------");
            Thread.sleep(600000L);
        } catch (Exception e) {
            logger.error("系统异常:{}", e);
        }
    }
}

       上面的代码再多介绍一下,初学zk的人可能有些地方有疑惑。客户端与zk集群建立连接,这是一个TCP长连接,也称为一个会话,通过这个连接客户端与zk集群互相发送请求和接受响应,同时客户端监听某个节点事件也是通过该连接,如果连接断开,那么这个会话也就结束了,如果有建立临时节点,临时节点也会被删除,有一点需要注意,建立会话时有设置一个会话超时时间,这个超时时间不是指这个会话的有效时间,因为这是个TCP长连接,它通过固有的心跳检测能一直使客户端和服务器端保持连接,只要不出现网络问题或你主动关闭。所以这个会话超时时间指的是,如果出现网络问题等各种原因,客户端与zk集群服务器断开了,那么只要在会话超时时间内,客户端能与zk集群内的任意一台恢复连接,那么之前的会话依旧有效,这个恢复连接就被认为还是之前的连接。数据节点Znode的概念也同样要理解清楚,/root是这个节点,/root/serverList也是一个节点。这些节点合在一起就形成了树结构的数据模型。/root、/root/serverList这些路径就是节点的唯一标识,也称节点名,当然每个Znode也保存着这个节点一系列的信息,这些信息客户端都可以获取到,客户端基于zk实现的很多功能都依赖这些节点信息,首先每个Znode节点都会存储节点数据,写入节点时就需要传入数据,上面的demo里,节点数据就是客户端的IP和端口。除了数据以外,Znode还存了一些状态信息,他们也同样重要,如Znode被创建时的事务ID,Znode被创建时的时间,Znode最后一次被更新时的事务ID,Znode最后一次被更新的时间,Znode的版本号,Znode的数据内容的长度,Znode节点类型,Znode的ACL版本号,Znode子节点个数,Znode子节点版本号,Znode子节点最后一次被修改时的事务ID,注意,只有子列表变更了,事务ID才会变更,数据内容变更没有影响。关于事务操作,每个节点的创建和删除,数据内容更新以及客户端会话创建与失效等操作,ZK集群都会为其分配一个全局唯一的事务ID。关于Watch这个事件监听器是ZK的重要功能,客户端监听的是某个Znode节点,当这个节点发生变换的时候,对这个节点感兴趣的客户端就会收到节点状态变换的通知。

接下来介绍基于ZK集群提供的两个主要功能Znode节点的读写和监听事件去实现的本文开头介绍的那些听上去很唬人的服务。

1、配置中心(数据发布与订阅)

       上面zk提供的功能你弄清楚了,你一听数据的发布与订阅,你就应该知道大概怎么基于ZK去做了,你就能自己去实现一个简化版的配置中心了。所谓配置中心,就是你把一些公共配置信息写到ZK集群的节点数据中,比如:数据库连接信息、固定文件路径等等信息。所有连接到ZK集群的机器都共享这些配置信息,而且可以利用watch监听事件去动态更新配置信息数据。一般订阅发布系统有推送、拉取两种模式,基于ZK实现的配置中心,推和拉的动作都有,客户端监听节点,节点数据变更,ZK集群会主动通知客户端,这算推,客户端监听到事件,需要主动去查节点数据,这个算拉。

2、命名服务

       所谓的命名服务就是,在分布式系统中,客户端应用能根据指定名字来获取资源或服务列表,所以被命名的实体通常都是服务的提供方,比如常见的就是一些分布式服务框架中(dubbo、RPC)中的服务地址列表。

3、心跳检测

       心跳检测用于不同机器或进程间彼此是否正常运行,常见的心跳检测有ping、TCP长连接等,利用ZK集群也可以实现心跳检测功能,ZK的临时节点有个特性就是客户端与ZK集群会话如果断开,临时节点就会被删除,利用这点,不同客户端或进程通过查询这个临时节点来判断另一客户端或进程是否存在,实现心跳检测功能。

4、master选举

       本文开头就提到了master选举,这里介绍具体实现,我们知道zk中存储的Znode的节点是唯一的,也就是如果有多个客户端同时向同一个zk集群写同一个Znode时,只有一个客户端会成功,其它客户端都会抛出节点已存在异常,这就是zk的强一致性,利用这个特性,就很容易在分布式环境中进行master选举了,成功写入的就是master节点。而且如果写入的Znode是临时节点,利用watch监听事件,master挂了,其它客户端收到通知后,可以再次重新进行master选举,就实现了动态选举的目的。

5、分布式锁

       分布式锁可能很多人用的最多的是redis锁,redis有十万级的高吞吐量,当然是一个非常好的选择,但是zk也提供了一种解决方法,就是利用Znode的唯一性、临时节点、watch监听事件,就不多说了,想必你也知道怎么做了。上面提供的zk各种应用是不是就是利用本文开头提到的zk底层的两个功能。

       到这儿,你就可以很好的把zk给灵活的运用起来了,如果你学习到别的引入了zk组件的技术,如大数据的学习,你就能很清楚的知道zk的作用。这时候你可以说自己熟悉zk,但是离精通还是差远了,因为真正zk难得部分也是分布式系统难得地方,是共性问题。 即需要你接触过不少分布式系统,也需要你有很好的基础知识,比如分布式的特点和各种在分布式下的问题、ACID到CAP到BASE、2pc原理,3pc原理以及它们之间的区别、paxos算法,ZAB协议以及它们的区别,在这些基础上你才能真正解析zk的内幕,它的序列化和通信协议、zk启动原理和过程、leader选举、数据同步等等。这些东西呢,浅尝则止没什么意义,无非就是记住了一个抽象过程和一堆其实不是很清楚的名词,然后在那强行解释。分布式的一致性原理我会写的,算是一个目标吧,但是要写也一定要写清楚,篇幅肯定很长,等我觉得自己搞透彻了,单独写吧,本文就到这儿了,最后分享一本书《从Paxos到Zookeeper分布式一致性原理与实践》,大家一起学习。

链接:https://pan.baidu.com/s/1mKVMlrhdomwVom_V4eV_7g

提取码:t8mn

手机二维码扫描如下

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值