在微服务架构中,注册中心属于一种服务治理组件。服务治理的需求来自于服务的数量,也来自于服务实例的动态性。在服务相互调用的过程中,每个服务首先需要高效地找到目标服务才能执行远程调用,因此服务治理所承载的服务注册和发现机制就显得非常重要了。
而注册中心就是用来实现服务治理的工具,围绕注册中心涉及的角色包括以下三种:
- 注册中心:提供服务注册和发现机制。
- 服务提供者:将服务本身注册到注册中心,进而暴露服务。
- 服务消费者:从注册中心发现目标服务,进而消费服务。
微服务架构中的服务提供者和服务消费者可以认为是注册中心的客户端,在服务内部都嵌入了客户端组件。
图片
上面这张图所展示的注册中心基本模型看起来比较简单,但真正实现起来要考虑的点是非常多的。另一方面,如果想要实现这样一个注册中心,我们也需要选择合适的工具。在今天的课程中,我们选择的工具是功能、特性都非常适合构建注册中心的 ZooKeeper,让我们一起来看一下。
ZooKeeper 与注册中心
ZooKeeper 的物理结构本质上就是一个文件系统,包含了一系列被称为 ZNode 的节点。每一节点代表位于文件系统中的一个具体物理路径,用来存储数据。
图片
如上图,节点 count 位于/business/product/count 路径,节点 temp 可能存储着数据 100,而节点/shop/order/1 可能存储着类似{“id”:“1”,“itemName”:“Notebook”,“price”:“4000”}”这样的复杂数据结构和信息。ZooKeeper 中所有数据都是通过 ZNode 的路径被引用的。
介绍完 ZooKeeper 的基本结构,我们来分析为什么它适合实现注册中心。
事实上,在注册中心的实现过程中,最复杂的就是变更通知机制,因为它涉及到如何在服务提供者实例状态发生变更时,有效地通知到服务的消费者,从而避免远程调用发生失败。添加了通知机制的注册中心模型是这样的:
图片
我们知道状态变更管理可以采用发布 - 订阅模式,具体来说,服务提供者可以根据服务定义发布服务,而服务消费者则通过对自己感兴趣的服务进行订阅,并获取变更后的服务实例信息。
图片
上图展示的就是一种服务监听机制。有了这一机制,服务提供者实例的状态一旦发生变化,服务消费者就能第一时间获取变更通知,从而获取最新的服务状态。
那我们如何来实现这种监听机制呢?ZooKeeper 为我们提供了现成的解决方案,就是 Watcher 机制。这个 Watcher 机制就相当于上图中所展示的这种监听器。监听器通过对 ZNode 进行监听,确保了节点信息发生变化时能够实时捕捉到这种变化并把它传递到客户端,从而触发客户端的回调处理函数。
从实现上讲,ZNode 是开发人员通过代码操控的主要对象。对 ZNode 的基本操作包括创建节点、获取子节点以及获取和设置节点数据等。我们可以通过引入 ZooKeeper 的客户端组件来实现这些操作,常见的客户端包括自带的 ZooKeeper API 和第三方 Curator 等。
ZooKeeper 中涉及的主要操作包含了以下 6 种:
图片
各种工具框架中对 ZooKeeper 的控制基本都是对这些操作的封装和应用。基于 ZooKeeper 各项功能特性实现注册中心的基本思路如下图:
图片
可以看到,无论是服务的提供者还是消费者,在服务初始化时都会与 ZooKeeper 服务器建立连接。然后,图中所展示的发布服务定义、注册新服务、获取和监听服务地址等操作本质上都是对 ZNode 各种基本操作的封装。当然,作为 ZooKeeper 的客户端,服务提供者和消费者都需要与 ZooKeeper 保持心跳检测。
Dubbo 框架默认把 ZooKeeper 作为它的注册中心实现工具。接下来就让我们来看看 Dubbo 中的 ZooKeeper 注册中心。
Dubbo 中的 ZooKeeper 注册中心
对于 Dubbo 而言,我们首先需要明确保存在 ZNode 中的具体内容,这就是服务提供者和消费者的 URL 信息,它们会被分别保存在 ZooKeeper 的/providers 和/consumers 节点之下。
图片
同时,服务消费者还会对/providers 节点进行订阅。这样,消费者就能实时获取提供者的 URL 信息。显然,这个时候我们会对/providers 节点添加 Watcher 机制。
我们进一步分析这些注册信息,可以看到,Dubbo 对各个节点进行了合理编排,构成了 Root、Service、Type、URL 这样的服务地址分层结构。
图片
Dubbo 通过对不同层级节点进行注册和订阅,来实现服务地址的发布和推送。当然,这种分层结构对于我们如何组织 ZooKeeper 中的数据有很好的借鉴意义。
接下来,让我们来看一些 Dubbo 中的源码。我们来看一下代表注册中心的 ZookeeperRegistry 类,而 ZookeeperRegistry 中最重要的就是它的构造函数。这是对应的代码:
public ZookeeperRegistry(URL url, ZookeeperTransporter, zookeeperTransporter) {
...
//构建 Zookeeper 客户端
zkClient = zookeeperTransporter.connect(url);
//添加 Watcher
zkClient.addStateListener(new StateListener() {
public void stateChanged(int state) {
if (state == RECONNECTED) {
try {
recover();
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
});
}
不难看出,在 ZookeeperRegistry 的构造函数中,我们通过 ZookeeperClient 客户端工具创建了与服务器的连接,并且通过 addStateListener 方法添加了监听器。一旦连接发生重连,就会触发回复操作。
为了更好地理解这段代码,我们需要明确另外两个核心对象的创建过程。这两个核心对象分别是前面所展示的 ZookeeperTransporter 和 ZookeeperClient,其中 ZookeeperTransporter 根据传入的 URL,通过创建与 Zookeeper 服务器的连接获取一个 ZookeeperClient 对象,而 ZookeeperClient 则包含了注册中心运行过程中所有的数据操作。
图片
从功能定位上讲,我们可以把 ZookeeperTransporter 看做是一种通信层组件,只负责与 ZooKeeper 实现网络通信,而 ZookeeperClient 则封装了所有的注册中心操作方法,是一种业务层组件。Dubbo 在这里所采用的这种分层设计思想同样值得我们借鉴。
目前可以与 ZooKeeper 服务器进行交互的客户端有很多,Dubbo 提供了对 Zkclient 和 Curator 这两个客户端工具的集成,对应的 Transporter 和 ZookeeperClient 实现类如下所示:
图片
Dubbo 使用 Zkclient 作为其默认实现。
接下来,我们终于到了分析注册中心具体操作的时候了。ZookeeperRegistry 提供了 doRegister、doUnregister、doSubscribe 和 doUnsubscribe 方法,分别对应注册、取消注册、订阅和取消订阅这四个具体操作。其中 doRegister 和 doUnregister 这两个方法比较简单,只是直接调用 Zkclient 的 create 和 delete 方法。而 doSubscribe 方法完成服务订阅操作,代码比较长,我们提取其中的核心代码:
ChildListener zkListener = listeners.get(listener);
if (zkListener == null) {
listeners.putIfAbsent(listener, new ChildListener() {
public void childChanged(String parentPath, List<String>
currentChilds) {
for (String child : currentChilds) {
child = URL.decode(child);
if (!anyServices.contains(child)) {
anyServices.add(child);
subscribe(url.setPath(child).addParameters(Constants.INTERFACE_KEY, child,Constants.CHECK_KEY, String.valueOf(false)), listener);
}
}
}
});
zkListener = listeners.get(listener);
}
可以看到,Dubbo 会订阅父级目录,而当有子节点发生变化时就会触发 ChildListener 中的回调函数,这个回调函数会对这个路径下的所有子节点执行订阅操作。
掌握了服务订阅的实现过程,理解取消订阅的原理就很简单了,我们只要去掉 URL 上已经注册的监听器就可以了,doUnsubscribe 方法如下所示:
protected void doUnsubscribe(URL url, NotifyListener listener) {
ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
if (listeners != null) {
ChildListener zkListener = listeners.get(listener);
if (zkListener != null) {
zkClient.removeChildListener(toUrlPath(url), zkListener);
}
}
}
至此,在 Dubbo 中,如何基于 ZooKeeper 实现注册中心的实现过程就介绍完了。如果我们想要自己动手实现一个类似的注册中心,那这个 Dubbo 中的实现过程还是具备很多参考价值的。
总结
我们来总结回顾一下这次讲的内容。我们针对微服务架构中的一个核心技术组件,即注册中心的基本模型做了展开,并基于 ZooKeeper 这款分布式协调工具重点分析了 Dubbo 中注册中心的实现方式以及所具备的功能特性。