在第一篇读书笔记中,了解到了 Service Registry,它是整个“微服务架构”中的核心,不仅提供了 Service Registry(服务注册)功能,同时也为 Service Discovery(服务发现)提供了支持。
服务注册,指的是在服务启动后,将服务的相关配置信息注册到服务注册表中。
服务发现,可以理解为当客户调用这些服务时,将通过 Service GateWay(服务网关)从服务注册表中获取这些服务配置,然后通过反向代理的方式去调用具体的服务接口,从服务注册表中获取服务配置的过程。
同时,服务注册表会定期检查已经注册的服务,若发现某服务无法访问了,将其从服务注册表中移除掉,这种定期检测的过程叫作“心跳检测”。因此,服务注册表对“分布式数据一致性”的要求相当高,即,服务注册表中的服务配置一旦变更了,通知机制必须要做到高性能,且服务注册表本身还需要具备高可用。
那么,谁才能担当服务注册表的重任呢?我们认为 ZooKeeper 是服务注册表的最近解决方案之一。目标:认识 ZooKeeper,并学会使用 Zookeeper,最后基于 ZooKeeper 实现服务注册表的核心功能,同时,我们将使用 Node.js 搭建一个高可用的服务网关。
1. 什么是 ZooKeeper(What)
ZooKeeper 被用来提供分布式环境下的协调服务。Yahoo 公司使用 Java 语言开发的,它是 Hadoop 项目中的子项目,在 Hadoop、HBase、Kafka 等技术中充当了核心组件的角色。它的设计目标就是将那些复杂且容易出错的分布式一致性服务加以封装,构成一个高效且可靠的服务,并为用户提供一系列简单易用的接口。
ZooKeeper 是一个经典的分布式数据一致性解决方案,可以基于它实现数据发布与订阅、负载均衡、命名服务、分布式协调与通知、集群管理、领导选举、分布式锁、分布式队列等功能。
ZooKeeper 一般都以集群的方式对外提供服务,一个集群包含多个节点,每个节点都对应一台 ZooKeeper 服务器,所有的节点共同对外提供服务。包括五大特性。
- 顺序性(从同一个客户端发送的请求,将会严格按照其发送顺序进入 ZooKeeper ,类似于队列,拥有“先进先出”的特性)
- 原子性(整个集群中所有机器都成功地处理某一个请求,要么就都没有处理,类似事务的原子性)
- 单一性(无论客户端连接到哪台服务器节点,客户端看到的服务端数据模型都是一致的,不可能出现两种不同的数据状态。)
- 可靠性(一旦服务端数据状态发生了变化,就会立即存储起来)
- 实时性(当某个请求成功处理后,客户端能够立即获取服务端的最新数据状态,整个过程具备实时性)
1.1 ZooKeeper 树状模型
Zookeeper 内部拥有一个树状的内存模型,类似于文件系统,有若干个目录,每个目录中有若干个文件,但在 ZooKeeper 中将这些目录与文件统称为 ZNode,每个 ZNode 有对应的路径及其包含的数据。ZNode 由 ZooKeeper 客户端来创建,当客户端与服务端建立连接后,服务端将为客户端创建一个 Session,客户端对 ZNode 的所有操作均在这个会话中来完成。
ZNode 类型 | 说明 |
Persistent(持久节点) | 当会话结束后,该节点不会被删除。 |
Persistent Sequential(持久顺序节点) | 当会话结束后,该节点不会被删除,且节点名中带自增数后缀 |
Ephemeral(临时节点) | 当会话结束后,该节点会被删除 |
Ephemeral Sequential(临时顺序节点) | 当会话结束后,该节点会被删除,且节点后名中带自增数猴嘴 |
【注意】持久性节点才能有子节点,这是 ZooKeeper 所限制的。
1.2 Zookeeper 集群结构
ZooKeeper 参考了 Paxos 协议,设计了一款更加轻量级的协议,名为 Zab(ZooKeeper Atomic Broadcast)。Zab 协议分为两个阶段:Leader Election(领导选举)与 Atomic Broadcast(原子广播)。当 ZooKeeper 集群启动时,将会选举出一台节点为 Leader,而其他节点均为 Follower 。当 Leader 节点出现故障时,会自动选举出新的 Leader 节点,并让所有节点恢复到一个正常的状态。
当领导选举阶段结束后,进入原子广播阶段,该阶段将同步 Leader 与各个 Follower 节点之间的数据,确保 Leader 与 Follower 节点具有相同的状态。所有的写操作都会发送到 Leader 节点,并通过广播的方式将数据同步到其他 Follower 节点。
一个 ZooKeeper 集群通常由一组节点组成,一般情况下,3~5个节点就可以组成一个可用的集群。理论上,节点越多越好。
2. 如何使用 ZooKeeper(How)
由于 ZooKeeper 是基于 Java 语言开发的,因此使用它之前,需要安装 JDK 运行环境。
2.1 运行 ZooKeeper
具体步骤参考 Ubuntu 下 安装 ZooKeeper 这篇随笔。
2.2 使用命令行工具连接 ZooKeeper
命令:bin/zkCli.sh
成功连接后可以通过命令行来操作 ZooKeeper。
1) help 命令,查看相关 ZooKeeper 客户端命令。
2) 【ls】列出子节点、【ls2】列出子节点及当前节点基本信息、【stat】判断当前 path 是否存在、【create】创建节点、【get】获取节点数据、【set】更新节点数据、【delete】删除节点。
说明:【create】 命令中 -s 选项:用于指定该节点是否为顺序节点,-e 选项:用于指定该节点是否为临时节点,-acl 参数用于控制权限;【set】命令执行完,可以看见 dataVersion 自增,dataLength 属性从5变成了2。
2.3 使用 Java 客户端连接 ZooKeeper
在 Maven 依赖中获取 Zookeeper 的 Java 客户端的jar包。
<dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.9</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> <exclusion> <groupId>log4j</groupId> <artifactId>log4j</artifactId> </exclusion> </exclusions> </dependency>
下面这段代码用来连接 ZooKeeper 。
import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; import org.apache.zookeeper.ZooKeeper; import java.util.concurrent.CountDownLatch; public class ZookeeperDemo { private static final String CONNECTION_STRING = "127.0.0.1:2181"; private static final int SESSION_TIMEOUT = 5000; private static CountDownLatch latch = new CountDownLatch(1); public static void main(String[] args) throws Exception { ZooKeeper zooKeeper = new ZooKeeper(CONNECTION_STRING, SESSION_TIMEOUT, new Watcher() { @Override public void process(WatchedEvent watchedEvent) { if (watchedEvent.getState() == Event.KeeperState.SyncConnected) { latch.countDown(); } } }); latch.await(); System.out.println(zooKeeper); } }
建立 ZooKeeper 会话的过程是异步的,当构造完 zooKeeper 对象后,线程将继续执行后续代码,但此时会话可能尚未建立完成。因此,需要用 CountDownLatch 工具,当创建对象完毕后,立即调用 latch.await() 方法,是当前线程处于等待状态,等待 SyncConnected 事件到来,再执行 latch.countDown() 方法,此时会话已经建立,可以继续执行后续代码。
1) 列出子节点
// System.out.println(zooKeeper); // 同步方式列出根节点下所有子节点 // List<String> children = zooKeeper.getChildren("/",null); // for (String node : children){ // System.out.println(node); // } // 异步方式列出根节点下所有子节点 zooKeeper.getChildren("/", null, new AsyncCallback.Children2Callback() { @Override public void processResult(int i, String s, Object o, List<String> list, Stat stat) { for(String node: list){ System.out.println(node); } } },null);
2) 判断节点是否已经存在
// 同步方式查看是否存在节点 Stat stat = zooKeeper.exists("/", null); if (stat != null) { System.out.println("node exists"); } else { System.out.println("node does not exist"); } // 异步方式查看是否存在节点 zooKeeper.exists("/", null, new AsyncCallback.StatCallback() { @Override public void processResult(int i, String s, Object o, Stat stat) { if (stat != null) { System.out.println("node exists"); } else { System.out.println("node does not exist"); } } }, null);
3) 创建节点
// 同步方式创建子节点 String name = zooKeeper.create("/test", "hello".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); System.out.println(name); // 异步方式创建子节点 zooKeeper.create("/test1", "hello".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT, new AsyncCallback.StringCallback() { @Override public void processResult(int i, String s, Object o, String s1) { System.out.println(s1); } }, null);
4)获取节点数据
// 同步方式获取节点数据 byte[] data = zooKeeper.getData("/test", null, null); System.out.println(new String(data)); // 异步方式获取节点数据 zooKeeper.getData("/test1", null, new AsyncCallback.DataCallback() { @Override public void processResult(int i, String s, Object o, byte[] bytes, Stat stat) { System.out.println(new String(bytes)); } }, null);
5)更新节点数据
// 同步方式更新节点数据 Stat stat1 = zooKeeper.setData("/test", "hi".getBytes(), -1); System.out.println(stat1 != null); // 异步方式更新节点数据 zooKeeper.setData("test1", "hi".getBytes(), -1, new AsyncCallback.StatCallback() { @Override public void processResult(int i, String s, Object o, Stat stat) { System.out.println(stat != null); } }, null);
6)删除节点数据
// 同步方式删除节点数据 zooKeeper.delete("/test", -1); System.out.println(true); // 异步方式删除节点数据 zooKeeper.delete("/test1", -1, new AsyncCallback.VoidCallback() { @Override public void processResult(int i, String s, Object o) { System.out.println(i == 0); } }, null); Thread.sleep(Long.MAX_VALUE);
ZooKeeper 官方提供了两套 Java 客户端 API,即同步和异步。