ZooKeeper 典型应用场景-数据发布与订阅

ZooKeeper 典型应用场景-数据发布与订阅

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就能够帮我们将数据变更的通知发送到各个客户端,每个客户端在接收到这个变更通知后,就可以重新进行最新数据的获取。

1.2、服务发现、注册

1.3、综合例子

架构图:

[图片上传失败...(image-8f75c4-1587351379309)]

Manage Server 程序主体流程:

[图片上传失败...(image-cbb9bd-1587351379309)]

Work Server 程序主体流程:

[图片上传失败...(image-a7b1d7-1587351379309)]

系统的核心类:

[图片上传失败...(image-29066e-1587351379309)]

1.4、程序代码实现

[
复制代码

](javascript:void(0); "复制代码")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">public class ServerConfig { private String dbUrl; private String dbPwd; private String dbUser; public String getDbUrl() { return dbUrl;
} public void setDbUrl(String dbUrl) { this.dbUrl = dbUrl;
} public String getDbPwd() { return dbPwd;
} public void setDbPwd(String dbPwd) { this.dbPwd = dbPwd;
} public String getDbUser() { return dbUser;
} public void setDbUser(String dbUser) { this.dbUser = dbUser;
}

@Override public String toString() { return "ServerConfig [dbUrl=" + dbUrl + ", dbPwd=" + dbPwd + ", dbUser=" + dbUser + "]";
}

}</pre>

[
复制代码

](javascript:void(0); "复制代码")

[
复制代码

](javascript:void(0); "复制代码")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">public class ServerData { private String address; private Integer id; private String name; public String getAddress() { return address;
} public void setAddress(String address) { this.address = address;
} public Integer getId() { return id;
} public void setId(Integer id) { this.id = id;
} public String getName() { return name;
} public void setName(String name) { this.name = name;
}

@Override public String toString() { return "ServerData [address=" + address + ", id=" + id + ", name=" + name + "]";
}

}</pre>

[
复制代码

](javascript:void(0); "复制代码")

[
复制代码

](javascript:void(0); "复制代码")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">import org.I0Itec.zkclient.IZkDataListener; import org.I0Itec.zkclient.ZkClient; import org.I0Itec.zkclient.exception.ZkNoNodeException; import com.alibaba.fastjson.JSON; public class WorkServer { private ZkClient zkClient; private String configPath; private String serversPath; private ServerData serverData; private ServerConfig serverConfig; private IZkDataListener dataListener; public WorkServer(String configPath, String serversPath, ServerData serverData, ZkClient zkClient, ServerConfig initConfig) { this.zkClient = zkClient; this.configPath = configPath; this.serversPath = serversPath; this.serverData = serverData; this.serverConfig = initConfig; this.dataListener = new IZkDataListener() { public void handleDataDeleted(String dataPath) throws Exception {

        } public void handleDataChange(String dataPath, Object data) throws Exception {
            String retJson = new String((byte[]) data);
            ServerConfig serverConfigLocal = (ServerConfig) JSON.parseObject(retJson, ServerConfig.class);
            updateConfig(serverConfigLocal);
            System.out.println("new Work server config is:" + serverConfig.toString());
        }
    };
} public void start() {
    System.out.println("work server start...");
    initRunning();
} public void stop() {
    System.out.println("work server stop...");
    zkClient.unsubscribeDataChanges(configPath, dataListener);
} private void initRunning() {
    registMe();
    zkClient.subscribeDataChanges(configPath, dataListener);
} private void registMe() {
    String mePath = serversPath.concat("/").concat(serverData.getAddress()); try {
        zkClient.createEphemeral(mePath, JSON.toJSONString(serverData)
                .getBytes());
    } catch (ZkNoNodeException e) {
        zkClient.createPersistent(serversPath, true);
        registMe();
    }
} private void updateConfig(ServerConfig serverConfig) { this.serverConfig = serverConfig;
}

}</pre>

[
复制代码

](javascript:void(0); "复制代码")

[
复制代码

](javascript:void(0); "复制代码")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">import java.util.List; import org.I0Itec.zkclient.IZkChildListener; import org.I0Itec.zkclient.IZkDataListener; import org.I0Itec.zkclient.ZkClient; import org.I0Itec.zkclient.exception.ZkNoNodeException; import org.I0Itec.zkclient.exception.ZkNodeExistsException; import com.alibaba.fastjson.JSON; public class ManageServer { private String serversPath; private String commandPath; private String configPath; private ZkClient zkClient; private ServerConfig config; private IZkChildListener childListener; private IZkDataListener dataListener; private List<String> workServerList; public ManageServer(String serversPath, String commandPath,
String configPath, ZkClient zkClient, ServerConfig config) { this.serversPath = serversPath; this.commandPath = commandPath; this.zkClient = zkClient; this.config = config; this.configPath = configPath; this.childListener = new IZkChildListener() { public void handleChildChange(String parentPath,
List<String> currentChilds) throws Exception {
workServerList = currentChilds;

            System.out.println("work server list changed, new list is ");
            execList();
        }
    }; this.dataListener = new IZkDataListener() { public void handleDataDeleted(String dataPath) throws Exception { // ignore;

} public void handleDataChange(String dataPath, Object data) throws Exception {
String cmd = new String((byte[]) data);
System.out.println("cmd:" + cmd);
exeCmd(cmd);
}
};
} private void initRunning() {
zkClient.subscribeDataChanges(commandPath, dataListener);
zkClient.subscribeChildChanges(serversPath, childListener);
} /* * 1: list 2: create 3: modify */
private void exeCmd(String cmdType) { if ("list".equals(cmdType)) {
execList();

    } else if ("create".equals(cmdType)) {
        execCreate();
    } else if ("modify".equals(cmdType)) {
        execModify();
    } else {
        System.out.println("error command!" + cmdType);
    }
} private void execList() {
    System.out.println(workServerList.toString());
} private void execCreate() { if (!zkClient.exists(configPath)) { try {
            zkClient.createPersistent(configPath, JSON.toJSONString(config)
                    .getBytes());
        } catch (ZkNodeExistsException e) {
            zkClient.writeData(configPath, JSON.toJSONString(config)
                    .getBytes());
        } catch (ZkNoNodeException e) {
            String parentDir = configPath.substring(0,
                    configPath.lastIndexOf('/'));
            zkClient.createPersistent(parentDir, true);
            execCreate();
        }
    }
} private void execModify() {
    config.setDbUser(config.getDbUser() + "_modify"); try {
        zkClient.writeData(configPath, JSON.toJSONString(config).getBytes());
    } catch (ZkNoNodeException e) {
        execCreate();
    }
} public void start() {
    initRunning();
} public void stop() {
    zkClient.unsubscribeChildChanges(serversPath, childListener);
    zkClient.unsubscribeDataChanges(commandPath, dataListener);
}

}</pre>

[
复制代码

](javascript:void(0); "复制代码")

[
复制代码

](javascript:void(0); "复制代码")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; import com.sql.zookeeper.common.ZookeeperConstant; import org.I0Itec.zkclient.ZkClient; import org.I0Itec.zkclient.serialize.BytesPushThroughSerializer; public class SubscribeZkClient { private static final int CLIENT_QTY = 5; private static final String CONFIG_PATH = "/config"; private static final String COMMAND_PATH = "/command"; private static final String SERVERS_PATH = "/servers"; public static void main(String[] args) throws Exception {

    List<ZkClient> clients = new ArrayList<ZkClient>();
    List<WorkServer> workServers = new ArrayList<WorkServer>();
    ManageServer manageServer = null; try {
        ServerConfig initConfig = new ServerConfig();
        initConfig.setDbPwd("123456");
        initConfig.setDbUrl("jdbc:mysql://localhost:3306/mydb");
        initConfig.setDbUser("root");

        ZkClient clientManage = new ZkClient(ZookeeperConstant.ZK_CONNECTION_STRING, 5000, 5000, new BytesPushThroughSerializer());
        manageServer = new ManageServer(SERVERS_PATH, COMMAND_PATH, CONFIG_PATH, clientManage, initConfig);
        manageServer.start(); for (int i = 0; i < CLIENT_QTY; ++i) {
            ZkClient client = new ZkClient(ZookeeperConstant.ZK_CONNECTION_STRING, 5000, 5000, new BytesPushThroughSerializer());
            clients.add(client);
            ServerData serverData = new ServerData();
            serverData.setId(i);
            serverData.setName("WorkServer#" + i);
            serverData.setAddress("192.168.1." + i);

            WorkServer workServer = new WorkServer(CONFIG_PATH, SERVERS_PATH, serverData, client, initConfig);
            workServers.add(workServer);
            workServer.start();
        }
        System.out.println("敲回车键退出!\n"); new BufferedReader(new InputStreamReader(System.in)).readLine();

    } finally {
        System.out.println("Shutting down..."); for (WorkServer workServer : workServers) { try {
                workServer.stop();
            } catch (Exception e) {
                e.printStackTrace();
            }
        } for (ZkClient client : clients) { try {
                client.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

}</pre>

[
复制代码

](javascript:void(0); "复制代码")

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容