面向服务的体系架构 SOA(三) --- Zookeeper API、zkClient API的使用


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项目中引入模块:


org.apache.zookeeper
zookeeper
3.4.6

创建zookeeper对象和节点:

 1     public static void main(String[] args) throws Exception {
 2         /*
 3          * 127.0.0.1:2181:服务器地址
 4          * 10:超时时间
 5          * watcher:若包含boolean watch的读方法中传入true,则将默认watcher注册为所关注事件的watcher
 6          * 若传入false,则不注册任何watcher。此处暂且定为空
 7          */
 8         ZooKeeper zookeeper = new ZooKeeper("127.0.0.1:2181", 10, null);
 9         /*
10          * 若创建的节点已经存在,则会抛出异常
11          * /root:节点路径 ; root data:路径包含的字节数据
12          * Ids.OPEN_ACL_UNSAFE:访问权限
13          * CreateMode.PERSISTENT:节点类型
14          */
15         zookeeper.create("/root", "root data".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
16         /*
17          * 设置节点数据
18          * -1:版本号;若匹配不到响应的节点则会抛出异常
19          */
20         zookeeper.setData("/root", "hello".getBytes(), -1);
21         /*
22          * 读取节点数据
23          * stat是节点状态参数,读取时会传出该节点当前状态信息
24          */
25         Stat stat = new Stat();
26         byte[] data = zookeeper.getData("/root", false, stat);
27         System.out.println(new String(data));
28         /*
29          * 添加子节点,若父节点不存在会抛出异常
30          */
31         zookeeper.create("/root/child", "child data".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
32         /*
33          * 判断节点是否存在,不存在则返回的stat为null
34          */
35         Stat existsStat = zookeeper.exists("/root/child", false);
36         System.out.println(existsStat);
37         /*
38          * /root/child:删除节点路径
39          * -1:节点的版本号;若设置为-1,则匹配所有版本,zookeeper会比较删除的版本和服务器版本是否一致,不一致会抛出异常
40          */
41         zookeeper.delete("/root/child", -1);
42     }

 实际运行中最常出现这个错误:

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就开始添加、删除节点等操作,为避免这种情况发生,可以在做操作时对连接状态做判断:

1 ZooKeeper zookeeper = new ZooKeeper("127.0.0.1:2181", 10, null);
2 if (zookeeper.getState() == States.CONNECTED) {
3 
4 }

 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模块:


com.github.sgroschupf
zkclient
0.1


简单事例:
 1     public static void main(String[] args) {
 2         ZkClient zkClient = new ZkClient("192.168.146.132:2181");
 3         String path = "/root";
 4         zkClient.createPersistent(path);
 5         zkClient.create(path + "/child", "znode child", CreateMode.EPHEMERAL);
 6         List children = zkClient.getChildren(path);
 7         System.out.println(children);
 8         int countChildren = zkClient.countChildren(path);
 9         System.out.println(countChildren);
10         System.out.println(zkClient.exists(path));
11         zkClient.writeData(path + "/child", "hello everyone");
12         Object data = zkClient.readData(path + "/child");
13         System.out.println(data);
14         zkClient.delete(path + "/child");
15         
16         //订阅数据的变化
17         zkClient.subscribeDataChanges(path, new IZkDataListener() {
18             
19             public void handleDataDeleted(String arg0) throws Exception {
20                 
21             }
22             
23             public void handleDataChange(String arg0, Object arg1) throws Exception {
24                 
25             }
26         });
27 
28         //订阅子节点的变化
29         zkClient.subscribeChildChanges(path, new IZkChildListener() {
30             
31             public void handleChildChange(String arg0, List arg1) throws Exception {
32                 
33             }
34         });
35         
36         zkClient.subscribeStateChanges(new IZkStateListener() {
37             
38             public void handleStateChanged(KeeperState arg0) throws Exception {
39                 
40             }
41             
42             public void handleNewSession() throws Exception {
43                 // 在这里可以进行异常发生时节点失效的容错处理
44                 
45             }
46         });
47     }

 1.2.4 路由和负载均衡

当服务规模变大时,服务之间的依赖变得十分复杂,这时我们不仅需要了解服务提供方,还需要了解服务消费方以了解服务的调用情况,可以以此作为服务扩容或下线的依据。

服务消费者获取服务提供者地址列表的部分代码为:

 1     List serverList;
 2 
 3     public List getServerList() {
 4         serverList = new ArrayList();
 5         String serviceName = "server - A";
 6         String serviceString = "127.0.0.1:2181";
 7         String path = "/config/" + serviceName;
 8         ZkClient zkClient = new ZkClient(serviceString);
 9         if (zkClient.exists(path)) {//服务存在则取地址列表
10             serverList = zkClient.getChildren(path);
11         } else {
12             throw new RuntimeException();
13         }
14         // 注册监听事件
15         zkClient.subscribeChildChanges(path, new IZkChildListener() {
16 
17             public void handleChildChange(String s, List list) throws Exception {
18                 serverList = list;
19             }
20         });
21         return serverList;
22     }

先取得服务上所注册的包含服务提供者地址的子节点,取得服务器地址列表后便可根据负载均衡算法选取调用服务器,服务器列表还存在本地以降低网络开销。注册监听器来感知服务器上线、下线和宕机事件,若发生节点改动,则将监听方法中取得的最新子节点赋给当前的serverList。

服务提供者向zookeeper注册服务:

 1         String path = "/config";
 2         String serverList = "127.0.0.1:2181";
 3         String serverName = "server";
 4         ZkClient zkClient = new ZkClient(serverList);
 5         if (!zkClient.exists(path)) {
 6             zkClient.createPersistent(path);//创建根节点
 7         }
 8         if (zkClient.exists(path + "/" + serverName)) {
 9             zkClient.createPersistent(path + "/" + serverName);//创建服务节点
10         }
11         //注册当前服务器
12         InetAddress addr = InetAddress.getLocalHost();
13         //取得本机ip
14         String ip = addr.getHostAddress().toString();
15         //创建当前服务器节点
16         zkClient.createPersistent(path + "/" + serverName + "/" + ip);

这样只有当配置信息更新时服务消费者才会去获取最新的服务地址列表,其他时候使用本地缓存即可,这样能大大降低配置中心的压力。

1.3 HTTP服务网关

移动互联网的崛起出现了多平台的现状,同样的功能厂商需根据不同平台开发不同的APP,使得开发成本增高。而由于客户端APP、第三方ISV(独立软件开发商)应用都必须经过公共网络来发起客户端请求,网关(gateway)作用得以凸显。gateway接收外部各种APP的请求,经过一系列权限与安全校验等,根据服务名到对应配置中心选取服务器列表,再由负载均衡算法选取一台服务器进行调用,将结果返回给客户端。

gateway可以拦截一系列恶意请求,而且能使不同的平台共用重复的逻辑,降低开发和运维成本。但由于gateway是整个网络的核心节点,一旦失效,依赖它的所有外部APP都将无法使用,因此在设计之初应该考虑到系统流量的监控和容量的规划,以便在达到峰值时能够快速进行系统扩容。

上图是一种网关集群的架构方案,一组对等的服务器组成网关集群接收外部HTTP请求,当流量达到警戒值,能方便地增加机器进行扩容。网关前有两台负载均衡设备负责对网关集群进行负载均衡,设备间进行心跳检测,一旦其中一台宕机,另一台则变更自己的地址接管宕机设备,平时这两台机器均对外提供服务。