首先可以先了解一下市面上常见的注册中心
| eureka | consul | zookeeper | etcd | |
|---|---|---|---|---|
| 服务健康检查 | 可配支持 | 服务状态、内存、硬盘等 | 长链接 | 心跳链接 |
| 多数据中心 | - | 支持 | - | - |
| kv存储服务 | - | 支持 | 支持 | 支持 |
| 一致性算法 | - | raft | paxos | raft |
| CAP | ap | cp | cp | cp |
| 使用接口(多语言功能) | http | http、dns | 客户端 | http、grpc |
| watch支持 | 支持long polling大部分增量 | 全量支持long polling | 支持 | 支持long polling |
| 自身监控 | metrics | metrics | - | metrics |
| 安全 | - | acl、https | acl | https |
在Dubbo微服务体系中,注册中心是其核心的组件之一。Dubbo通过注册中心实现了分布式环境中各服务之间的注册与发现,是各个分布式节点之间的纽带。主要作用有:
- 动态加入:一个服务提供者通过注册中心可以动态的把自己暴露给其他消费者,无需消费者逐个去更新配置文件。
- 动态发现:一个消费者可以动态感知新的配置、路由规则和新的服务提供者,无需重启服务使之生效。
- 动态调整:注册中心支持参数的动态调整,新参数自动更新到所有相关服务节点。
- 统一配置:避免了本地配置导致每个服务的配置不一致问题。
dubbo主要包含四种注册中心的实现,分别是:Zookeeper、Redis、Simple、Multicast。
其中Zookeeper是官方推荐的注册中心实现,在生产环境中已经有大量实际使用,具体的实现在Dubbo的源码 dubbo-registry-zookeeper模块中。而Redis注册中心在稳定性方面相比ZK就差了一些,其稳定性主要是依赖Redis本身,阿里内部并没有使用Redis作为注册中心。Simple是一个基于内存的简单的注册中心实现,它本身就是一个标准的RPC服务,不支持集群,可能出现单点故障。Multicast模式则不需要启动任何注册中心,只要通过广播地址,就可以互相发现。服务提供者启动时,会广播自己的地址,消费者启动时,会广播订阅请求,服务提供者收到订阅请求,会根据配置广播或单播给订阅者。
注册中心的工作流程

Provider注册:Provider启动时,会向注册中心写入自己的元数据信息,同时会订阅配置元数据信息。Consumer订阅:这里的订阅指Consumer启动时也会向注册中心写入自己的元数据信息,并订阅服务提供者、路由和配置元数据信息。服务治理中心启动:dubbo-admin启动时,会同时订阅所有消费者、服务提供者、路由和配置元数据信息。动态注册、发现:当有新的Provider加入或者有离开时,注册中心服务提供者目录会发生变化,变化信息会动态通知给消费者、服务治理中心。监控中心采集:当Consumer发起调用时,会异步将调用、统计信息等上报给监控中心。
zookeeper的原理
zookeeper是树形节点的注册中心,每个节点的类型分为持久节点、持久顺序节点、临时节点、临时顺序节点。
- 持久节点:服务注册后保证节点不会丢失,注册中心重启也会存在。
- 持久顺序节点:在持久节点特性的基础上增加了节点先后顺序的能力。
- 临时节点:服务注册后连接丢失或者session超时,注册的节点会自动消失。
- 临时节点顺序:在临时节点特性的基础上增加了节点先后顺序的能力。
Dubbo使用ZK作为注册中心时,只会创建临时节点和持久节点两种,对创建顺序并没有要求。
/dubbo/com.foo.BarService/providers是服务提供者在Zookeeper注册中心的路径示例,是一种属性结构,该结构分为四层:root(根节点,对应示例中的dubbo)、service(接口名称,对应示例中的com.foo.BarService)、四种服务目录(对应示例中的providers,其他目录还有consumers、routers、configurators)。在服务分类节点下是具体的Dubbo服务URL。属性结构示例如下:
+ /dubbo
+-- service
+-- providers
+-- consumers
+-- routers
+-- configurators
树形结构的关系:
(1)树的根节点是注册中心分组,下面有多个服务接口,分组值来自用户配置<dubbo:registry>中的group属性,默认是/dubbo.
(2)服务接口下包含四类子目录,分别是providers、consumers、routers、configurators,这个路径时持久节点。
(3)服务提供者目录(/dubbo/service/providers)下面包含的接口有多个服务提供者URL元数据信息。
(4)服务消费者目录(/dubbo/service/consumers)下面包含的解耦有多个消费者URL元数据信息。
(5)路由配置目录(/dubbo/service/routers)下面包含多个用于消费者路由策略URL元数据信息。
(6)动态配置目录(/dubbo/service/configurators)下面包含多个用于服务者动态配置URL元数据信息。
树形示意图,如下:

配置实现:
<beans>
<!-- 适用于Zookeeper一个集群有多个节点,多个IP和端口逗号分割-->
<dubbo:registry protocol="zookeeper" address="ip:port,ip:port,ip:port" />
<!-- 适用于Zookeeper多个集群有多个节点,多个IP和端口用竖线分割-->
<dubbo:registry protocol="zookeeper" address="ip:port|ip:port|ip:port" />
</beans>
zookeeper发布的实现
provider和Consumer需要将自己注册到Zookeeper。服务提供者的注册是为了让消费者订阅(准确来说应该叫感知服务的存在),从而发起远程调用;也上服务治理中心感知有新的服务提供者上线。消费者的发布是为了让服务治理中心可以发现自己。Zookeeper发布订阅代码非常简单,只是调用Zookeeper 的 Client 库在注册中心创建一个目录而已,如下代码所示:
@Override
public void doRegister(URL url) {
try {
zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true));
} catch (Throwable e) {
throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
取消发布对应也很简单,只是把ZK注册中心上对应的路径删除,如下代码所示:
@Override
public void doUnregister(URL url) {
try {
zkClient.delete(toUrlPath(url));
} catch (Throwable e) {
throw new RpcException("Failed to unregister " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
zookeeper订阅的实现
订阅通常有pull和push两种方式,一种是客户端定时轮训注册中心拉取配置,另一种是注册中心主动推送数据给客户端。这两种方式各有利弊,目前Dubbo采用的是第一次启动拉取方式,后续接收事件重新拉取数据。
在服务暴露时,服务端会订阅configurators用于监听动态配置,在消费端启动时,消费端会订阅providers、routers和configurators这三个目录,分别对应服务提供者、路由和动态配置变更通知。
Dubbo提供了两种不同ZK开源客户端库的封装,分别对应接口:
- Apache Curator
- zkClient
我们可以在<dubbo:registry>的client属性中设置curator、zkClient来使用不同的客户端实现库,如果不设置默认使用Curator作为实现。
Zookeeper客户端采用的是“事件通知” + “客户端拉取”的方式,客户端在第一次连接上注册中心时,会获取对应目录下全量的数据。并在订阅的节点上注册一个watcher,客户端与注册中心之间保持TCP长连接,后续每个节点有任何数据变化的时候,注册中心会根据watcher的回调主动通知客户端(事件通知),客户端接到通知后,会把对应节点下的全量数据都拉取过来(客户端拉取),这一点在NotifyListener#notify(List<URL> urls)接口上就有说明。全量拉取有一个局限,当服务节点较多时会对网络造成很大的压力。
Zookeeper每个节点都有一个版本号,当某个节点的数据发生变化时,对应的版本号就会变化,并触发watcher事件,推送数据给订阅方。版本号强调的是变化次数,即使该节点的值没有变化,只要有更新操作,依然会使版本号变化。
Zookeeper实现服务订阅的核心代码在ZookeeperRegistry中:
@Override
public void doSubscribe(final URL url, final NotifyListener listener) {
try {
if (ANY_VALUE.equals(url.getServiceInterface())) {
/*** 服务治理中心订阅全部服务 ***/
// 订阅所有数据
String root = toRootPath();
// 获取Listeners
ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
if (listeners == null) {
// 为获取到监听器,这里创建一个监听器并放入缓存。
zkListeners.putIfAbsent(url, new ConcurrentHashMap<>());
listeners = zkListeners.get(url);
}
ChildListener zkListener = listeners.get(listener);
if (zkListener == null) {
// zkListener为空,说明是第一次,新建一个listener
listeners.putIfAbsent(listener, (parentPath, currentChilds) -> {
// 这是一个内部类实现,不会立即执行,只会在触发变更通知时执行
// 如果子节点有变化则会接收到通知,遍历所有子节点
for (String child : currentChilds) {
child = URL.decode(child);
// 如果存在子节点还未被订阅,说明是新增节点吗,则进行订阅
if (!anyServices.contains(child)) {
anyServices.add(child);
// 订阅新节点
subscribe(url.setPath(child).addParameters(INTERFACE_KEY, child,
Constants.CHECK_KEY, String.valueOf(false)), listener);
}
}
});
zkListener = listeners.get(listener);
}
// 创建持久节点,接下来订阅持久节点的直接子节点
zkClient.create(root, false);
List<String> services = zkClient.addChildListener(root, zkListener);
if (CollectionUtils.isNotEmpty(services)) {
// 遍历所有子节点进行订阅
for (String service : services) {
service = URL.decode(service);
anyServices.add(service);
subscribe(url.setPath(service).addParameters(INTERFACE_KEY, service,
Constants.CHECK_KEY, String.valueOf(false)), listener);
}
}
} else {
/*** 普通消费者服务订阅 ***/
List<URL> urls = new ArrayList<>();
//toCategoriesPath(url): 根据url类别,获取一组要订阅的路径
for (String path : toCategoriesPath(url)) {
ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
if (listeners == null) {
// 如果listeners缓存为空则创建缓存
zkListeners.putIfAbsent(url, new ConcurrentHashMap<>());
listeners = zkListeners.get(url);
}
ChildListener zkListener = listeners.get(listener);
// 如果zkListener缓存为空则创建缓存
if (zkListener == null) {
listeners.putIfAbsent(listener, (parentPath, currentChilds) -> ZookeeperRegistry.this.notify(url, listener, toUrlsWithEmpty(url, parentPath, currentChilds)));
zkListener = listeners.get(listener);
}
zkClient.create(path, false);
// 订阅,返回该节点下的子路径并缓存
List<String> children = zkClient.addChildListener(path, zkListener);
if (children != null) {
urls.addAll(toUrlsWithEmpty(url, path, children));
}
}
// 回调NotifyListener, 更新本地缓存信息
notify(url, listener, urls);
}
} catch (Throwable e) {
throw new RpcException("Failed to subscribe " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
zookeeper的缓存机制
缓存的存在就是用空间换取时间的一种机制。如果Consumer每次远程调用都要先去注册中心拉取一次可调用的服务列表,则会让注册中心承受巨大的流量压力。另外,每个额外的网络请求也会让整个系统的性能下降,同时服务列表变化的频率本身并不是很高,除非服务提供商对接口做了升级、或是服务节点新增或下线(从某各角度来看这并不是一个很高频的操作),所以每次都拉取也就显得并不那么必要。
因此针对这个问题,dubbo的注册中心实现了通用的缓存机制,在抽象类AbstractRegistry中实现。AbstractRegistry类结构关系图如下所示:

消费者或者服务治理中心获取注册信息后会做本地缓存。内存中会有一份,保存在Properties对象里,磁盘里也会有一份文件,通过file对象引用。在AbstractRegistry抽象类中有如下定义:
/** 本地缓存对象 **/
private final Properties properties = new Properties();
/** 磁盘文件服务缓存对象 **/
private File file;
/** 内存中的服务缓存对象 **/
private final ConcurrentMap<URL, Map<String, List<URL>>> notified = new ConcurrentHashMap<URL, Map<String, List<URL>>>();
其中内存中的缓存notified是ConcurrentHashMap里面又封装了一个Map,外层Map的key是消费者的URL,内层Map的key是分类,包含了providers、consumers、routers、configutators四种,value则对应的服务列表,对于没有服务提供者提供服务的URL,它会以特殊的empty://前缀了开头。
zookeeper缓存的加载
在服务初始化时候,AbstractRegistry构造器函数里会从本地磁盘文件中把持久化的注册数据到Properties对象里,并加载到内存缓存中,核心代码如下:
private void loadProperties() {
if (file != null && file.exists()) {
InputStream in = null;
try {
// 读取磁盘文件
in = new FileInputStream(file);
// 把数据写入到内存缓存中
properties.load(in);
……
} catch (Throwable e) {
……
} finally {
……
}
}
}
Properties保存了所有服务提供者的URL,使用URL#serviceKey()作为key,提供者列表、路由规则列表、配置规则列表等作为value。如果应用在启动过程中注册中心无法连接或岩机,则Dubbo框架会自动通过本地缓存加载Invokers。
zookeeper缓存的保存与更新
缓存的保存有同步和异步两种方式。异步会使用线程池异步保存,如果线程在执行过程中出现异常,则会再次调用线程池不断重试,代码如下:
if(syncSaveFile){
// 同步保存
doSaveProperties(version);
} else {
// 异步保存,放入线程池。会传入一个AtomicLong的版本号保证数据是最新的
registryCacheExecutor.execute(new SaveProperties(version));
}
AbstractRegistry#notify 方法中封装了更新内存和本地文件缓存的逻辑。当客户端第一次订阅获取全量数据的时候,或者后续由于订阅的数据发生变更时,都会调用该方法进行保存。
探讨Dubbo微服务框架中注册中心的作用及Zookeeper的实现细节,包括服务注册、发现、缓存机制及配置实现。
350

被折叠的 条评论
为什么被折叠?



