zookeeper 简单介绍及 API 使用
1.1 zookeeper 简介
zookeeper 是一个针对大型分布式系统的可靠的协调系统, 提供的功能包括配置维护名字服务分布式同步组服务等 zookeeper 可以集群复制, 集群间通过 zab 协议来保持数据的一致性该协议包括两个阶段: leader election 阶段和 Atomic broadcas 阶段
leader election 阶段: 集群间选举出一个 leader, 其他的机器则称为 follower, 所有的写操作都被传送给 leader, 并通过 broadcas 将所有的更新告诉 follower, 当 leader 崩溃或 leader 失去大多数的 follower 时, 需要重新选举出一个新的 leader, 让所有的服务器都恢复到一个正确的状态当 leader 被选举出来且大多数服务器完成了和 leader 的状态同步后, leader election 过程结束, 进入 Atomic broadcas 阶段
Atomic broadcas 阶段: Atomic broadcas 同步 leader 和 follower 之间的信息, 保证二者具有相同的系统状态
zookeeper 的协作过程简化了松散耦合系统之间的交互, 即使参与者彼此不知道对方的存在, 也能够相互发现并且完成交互
1.2 zookeeper API 简单使用
可以认为 zookeeper 是一个小型的精简的文件系统, 它的每个节点称为 znode,znode 除了本身能够包含一部分数据之外, 还能拥有子节点, 当节点或子节点数据发生变化时, 基于 watcher 机制, 会发出相应的通知给订阅其状态变化的客户端
1.2.1 zookeeper 节点创建
maven 项目中引入模块:
- <dependency>
- <groupId>org.apache.zookeeper</groupId>
- <artifactId>zookeeper</artifactId>
- <version>3.4.6</version>
- </dependency>
创建 zookeeper 对象和节点:
- public static void main(String[] args) throws Exception {
- /*
- * 127.0.0.1:2181: 服务器地址
- * 10: 超时时间
- * watcher: 若包含 boolean watch 的读方法中传入 true, 则将默认 watcher 注册为所关注事件的 watcher
- * 若传入 false, 则不注册任何 watcher 此处暂且定为空
- */
- ZooKeeper zookeeper = new ZooKeeper("127.0.0.1:2181", 10, null);
- {$80
}root: 节点路径 ; root data: 路径包含的字节数据
* Ids.OPEN_ACL_UNSAFE: 访问权限
* CreateMode.PERSISTENT: 节点类型
- */
- zookeeper.create("/root", "root data".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
- /*
- * 设置节点数据
- * -1: 版本号; 若匹配不到响应的节点则会抛出异常
- */
- zookeeper.setData("/root", "hello".getBytes(), -1);
- /*
- * 读取节点数据
- * stat 是节点状态参数, 读取时会传出该节点当前状态信息
- */
- Stat stat = new Stat();
- byte[] data = zookeeper.getData("/root", false, stat);
- System.out.println(new String(data));
- /*
- * 添加子节点, 若父节点不存在会抛出异常
- */
- zookeeper.create("/root/child", "child data".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
- /*
- * 判断节点是否存在, 不存在则返回的 stat 为 null
- */
- Stat existsStat = zookeeper.exists("/root/child", false);
- System.out.println(existsStat);
- {$85
}root/child: 删除节点路径
* -1: 节点的版本号; 若设置为 - 1, 则匹配所有版本, zookeeper 会比较删除的版本和服务器版本是否一致, 不一致会抛出异常
- */
- zookeeper.delete("/root/child", -1);
- }
实际运行中最常出现这个错误:
- Exception in thread "main" org.apache.zookeeper.KeeperException$ConnectionLossException: KeeperErrorCode = ConnectionLoss for /root
- at org.apache.zookeeper.KeeperException.create(KeeperException.java:90)
- at org.apache.zookeeper.KeeperException.create(KeeperException.java:42)
- at org.apache.zookeeper.ZooKeeper.create(ZooKeeper.java:643)
- at com.project.soa.zookeeper.ZookeeperDemo.main(ZookeeperDemo.java:12)
这是因为还未连接上 zookeeper 就开始添加删除节点等操作, 为避免这种情况发生, 可以在做操作时对连接状态做判断:
- ZooKeeper zookeeper = new ZooKeeper("127.0.0.1:2181", 10, null);
- if (zookeeper.getState() == States.CONNECTED) {
- }
1.2.2 watcher 的实现
节点的状态发生变化, 可以通过 zookeeper 的 watcher 机制让客户端取得通知 watcher 的实现较为简单, 只需实现 org.apache.ZooKeeper.Watcher 接口即可, 其中节点的状态变化包含以下几种状态:
注意: watcher 机制是一次性的, 每次处理完状态变化事件之后需重新注册 watcher 这也导致在处理事件和重新加上 watcher 这段时间发生的节点状态无法被感知
1.2.3 zkClient 的使用
zkClient 解决了 watcher 的一次性注册问题, 将 znode 的事件重新定义为子节点的变化数据的变化连接及状态的变化三类, watcher 执行后重新读取数据的同时再注册相同的 watcher 在异常发生时 zkClient 会自动创建新的 zookeeper 实例进行重连, 此时原来的 watcher 节点都将失效, 可在 zkClient 定义的连接状态变化的接口中进行相应处理同时 zkClient 还提供了序列化和反序列化接口 ZkSerializer, 简化了 znode 上对象的存储
maven 中引入 zkClient 模块:
- <dependency>
- <groupId>com.github.sgroschupf</groupId>
- <artifactId>zkclient</artifactId>
- <version>0.1</version>
- </dependency>
简单事例:
- public static void main(String[] args) {
- ZkClient zkClient = new ZkClient("192.168.146.132:2181");
- String path = "/root";
- zkClient.createPersistent(path);
- zkClient.create(path + "/child", "znode child", CreateMode.EPHEMERAL);
- List<String> children = zkClient.getChildren(path);
- System.out.println(children);
- int countChildren = zkClient.countChildren(path);
- System.out.println(countChildren);
- System.out.println(zkClient.exists(path));
- zkClient.writeData(path + "/child", "hello everyone");
- Object data = zkClient.readData(path + "/child");
- System.out.println(data);
- zkClient.delete(path + "/child");
- // 订阅数据的变化
- zkClient.subscribeDataChanges(path, new IZkDataListener() {
- public void handleDataDeleted(String arg0) throws Exception {
- }
- public void handleDataChange(String arg0, Object arg1) throws Exception {
- }
- });
- // 订阅子节点的变化
- zkClient.subscribeChildChanges(path, new IZkChildListener() {
- public void handleChildChange(String arg0, List<String> arg1) throws Exception {
- }
- });
- zkClient.subscribeStateChanges(new IZkStateListener() {
- public void handleStateChanged(KeeperState arg0) throws Exception {
- }
- public void handleNewSession() throws Exception {
- // 在这里可以进行异常发生时节点失效的容错处理
- }
- });
- }
1.2.4 路由和负载均衡
当服务规模变大时, 服务之间的依赖变得十分复杂, 这时我们不仅需要了解服务提供方, 还需要了解服务消费方以了解服务的调用情况, 可以以此作为服务扩容或下线的依据
服务消费者获取服务提供者地址列表的部分代码为:
- List<String> serverList;
- public List<String> getServerList() {
- serverList = new ArrayList<String>();
- String serviceName = "server - A";
- String serviceString = "127.0.0.1:2181";
- String path = "/config/" + serviceName;
- ZkClient zkClient = new ZkClient(serviceString);
- if (zkClient.exists(path)) {// 服务存在则取地址列表
- serverList = zkClient.getChildren(path);
- } else {
- throw new RuntimeException();
- }
- // 注册监听事件
- zkClient.subscribeChildChanges(path, new IZkChildListener() {
- public void handleChildChange(String s, List<String> list) throws Exception {
- serverList = list;
- }
- });
- return serverList;
- }
先取得服务上所注册的包含服务提供者地址的子节点, 取得服务器地址列表后便可根据负载均衡算法选取调用服务器, 服务器列表还存在本地以降低网络开销注册监听器来感知服务器上线下线和宕机事件, 若发生节点改动, 则将监听方法中取得的最新子节点赋给当前的 serverList
服务提供者向 zookeeper 注册服务:
- String path = "/config";
- String serverList = "127.0.0.1:2181";
- String serverName = "server";
- ZkClient zkClient = new ZkClient(serverList);
- if (!zkClient.exists(path)) {
- zkClient.createPersistent(path);// 创建根节点
- }
- if (zkClient.exists(path + "/" + serverName)) {
- zkClient.createPersistent(path + "/" + serverName);// 创建服务节点
- }
- // 注册当前服务器
- InetAddress addr = InetAddress.getLocalHost();
- // 取得本机 ip
- String ip = addr.getHostAddress().toString();
- // 创建当前服务器节点
- zkClient.createPersistent(path + "/" + serverName + "/" + ip);
这样只有当配置信息更新时服务消费者才会去获取最新的服务地址列表, 其他时候使用本地缓存即可, 这样能大大降低配置中心的压力
1.3 HTTP 服务网关
移动互联网的崛起出现了多平台的现状, 同样的功能厂商需根据不同平台开发不同的 APP, 使得开发成本增高而由于客户端 APP 第三方 ISV(独立软件开发商)应用都必须经过公共网络来发起客户端请求, 网关 (gateway) 作用得以凸显 gateway 接收外部各种 APP 的请求, 经过一系列权限与安全校验等, 根据服务名到对应配置中心选取服务器列表, 再由负载均衡算法选取一台服务器进行调用, 将结果返回给客户端
gateway 可以拦截一系列恶意请求, 而且能使不同的平台共用重复的逻辑, 降低开发和运维成本但由于 gateway 是整个网络的核心节点, 一旦失效, 依赖它的所有外部 APP 都将无法使用, 因此在设计之初应该考虑到系统流量的监控和容量的规划, 以便在达到峰值时能够快速进行系统扩容
上图是一种网关集群的架构方案, 一组对等的服务器组成网关集群接收外部 HTTP 请求, 当流量达到警戒值, 能方便地增加机器进行扩容网关前有两台负载均衡设备负责对网关集群进行负载均衡, 设备间进行心跳检测, 一旦其中一台宕机, 另一台则变更自己的地址接管宕机设备, 平时这两台机器均对外提供服务
来源: https://www.cnblogs.com/bearduncle/p/8602554.html