在Dubbo微服务体系中,注册中心是核心组件之一。Dubbo通过注册中心实现分布式环境中各个服务之间的注册和发现,是各个节点间的纽带。
在微服务体系中,注册中心的作用如下:
- 服务动态加入。一个服务提供者可以通过注册中心动态的将自己暴露给各个服务消费方。
- 服务动态订阅。服务消费方可以通过注册中心,实时的感知新旧服务的上线与下线。
- 动态调整。注册中心支持参数的动态调整,新参数将自动更新到所有相关的节点中。
- 统一配置。提供统一的配置服务。
在Dubbo中,ZooKeeper是官方推介的注册中心。通过源码,我们可以看到,Dubbo支持的注册中心如下:Redis、Etcd、Nacos、Sofa、ZooKeeper等。
阿里内部并没有采用 Zookeeper 做为注册中心,而是使用自己实现的基于数据库的注册中心,即:Zookeeper 注册中心并没有在阿里内部长时间运行的可靠性保障,此 Zookeeper 桥接实现只为开源版本提供,其可靠性依赖于 Zookeeper 本身的可靠性。
http://dubbo.apache.org/zh-cn/docs/user/references/registry/zookeeper.html
注册中心的工作流程
注册中心的工作流程比较简单,官方也有详细的说明,总体流程如下:
- 服务提供者启动时,会向注册中心写入自己的元数据信息。
- 消费者启动时,会向注册中心写入自己的元数据信息,并且订阅服务提供者、路由、配置等信息。
- 服务治理中心启动时,会同时订阅所有消费者、服务提供者、路由和配置数据。
- 当有服务提供者离开或者有新的服务提供者加入时,会自动通知相关的服务消费者。
ZooKeeper注册中心
Zookeeper 是 Apacahe Hadoop 的子项目,是一个树型的目录服务,支持变更推送,适合作为 Dubbo 服务的注册中心,工业强度较高,可用于生产环境,并推荐使用。节点类型可以分为持久节点、持久顺序节点、临时节点、临时顺序节点。Dubbo使用ZooKeeper作为注册中心时,会创建持久节点和临时节点,对创建顺序没有太大要求。Dubbo启动时会在ZooKeeper中创建如下目录:
树形结构如下:
- 树的根节点是注册中心分组,默认是/dubbo。
- Service接口下包含4类子目录,分别是providers、consumers、routers、configurators。
- /dubbo/service/providers下有多个服务提供者url等元数据信息。
- /dubbo/service/consumers下有多个服务消费者url元数据信息。
- /dubbo/service/routers下包含服务消费者路由信息。
- /dubbo/service/configurators下包含服务提供者配置信息。
基于ZooKeeper注册中心的源码分析
ZooKeeper注册中心采用的是“事件通知”+“客户端拉取“的方式,客户端在第一次连接上注册中心时会获取目录下的全量配置信息,并在订阅的节点上注册一个watcher,客户端与ZooKeeper保持一个TCP长链接,之后每个节点有数据发生变化时,注册中心会以事件通知的形式,触发watcher事件,把数据推送给订阅方。
ZooKeeper全量订阅服务
org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#doSubscribe方法
//订阅所有类型,ANY_VALUE的值为*
if (ANY_VALUE.equals(url.getServiceInterface())) {
String root = toRootPath();
ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.computeIfAbsent(url, k -> new ConcurrentHashMap<>());
ChildListener zkListener = listeners.computeIfAbsent(listener, k -> (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)), k);
}
}
});
//创建持久节点
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);
}
}
}
由上述代码可知,此处代码主要支持Dubbo服务治理平台(dubbo-admin),平台在启动时拉取全量数据。
ZooKeeper订阅类别服务
org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#doSubscribe方法
List<URL> urls = new ArrayList<>();
//根据url的类别,获取一组要订阅的数据
for (String path : toCategoriesPath(url)) {
ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.computeIfAbsent(url, k -> new ConcurrentHashMap<>());
ChildListener zkListener = listeners.computeIfAbsent(listener, k -> (parentPath, currentChilds) -> ZookeeperRegistry.this.notify(url, k, toUrlsWithEmpty(url, parentPath, currentChilds)));
//创建持久节点
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);
首先根据url获取一组需要订阅的路径,如果url的类别是*,则会订阅provider、consumer、routers、configurators,否则默认订阅provider。代码最后一行,回调notifyListen,更新本地缓存信息。
缓存机制
缓存的存在是为了提高性能,如果每次每次调用都远程拉取可调用服务的列表,会对配置中心造成巨大的压力,所以Dubbo注册中心提供了缓存的机制。服务消费者和服务治理中心获取配置后会缓存在本地,保存在内存中的Properties对象里,磁盘上也会持久化一份,通过File类引用。
org.apache.dubbo.registry.support.AbstractRegistry
public abstract class AbstractRegistry implements Registry {
// Local disk cache file
// 磁盘文件服务缓存对象
private File file;
// Local disk cache, where the special key value.registries records the list of registry centers, and the others are the list of notified service providers
// 内存缓存对象
private final Properties properties = new Properties();
}
为了更容易理解缓存机制,我们先来看一下ZookeeperRegistry配置中心的类继承关系,我们可以清楚的看到,ZookeeperRegistry继承于抽象类AbstractRegistry。
缓存的加载
在服务初始化时候,AbstractRegistry构造函数里会从本地磁盘文件中把持久化注册中心数据加载到内存的Properties对象里,代码如下
org.apache.dubbo.registry.support.AbstractRegistry#AbstractRegistry
public AbstractRegistry(URL url) {
setUrl(url);
if (url.getParameter(REGISTRY__LOCAL_FILE_CACHE_ENABLED, true)) {
// Start file save timer
syncSaveFile = url.getParameter(REGISTRY_FILESAVE_SYNC_KEY, false);
String defaultFilename = System.getProperty("user.home") + "/.dubbo/dubbo-registry-" + url.getParameter(APPLICATION_KEY) + "-" + url.getAddress().replaceAll(":", "-") + ".cache";
String filename = url.getParameter(FILE_KEY, defaultFilename);
File file = null;
if (ConfigUtils.isNotEmpty(filename)) {
file = new File(filename);
if (!file.exists() && file.getParentFile() != null && !file.getParentFile().exists()) {
if (!file.getParentFile().mkdirs()) {
throw new IllegalArgumentException("Invalid registry cache file " + file + ", cause: Failed to create directory " + file.getParentFile() + "!");
}
}
}
this.file = file;
//加载本地磁盘磁盘数据到内存到Properties对象里
loadProperties();
notify(url.getBackupUrls());
}
}
org.apache.dubbo.registry.support.AbstractRegistry#loadProperties
private void loadProperties() {
if (file != null && file.exists()) {
InputStream in = null;
try {
//读取磁盘文件缓存数据
in = new FileInputStream(file);
//加载到内存里
properties.load(in);
if (logger.isInfoEnabled()) {
logger.info("Load registry cache file " + file + ", data: " + properties);
}
} catch (Throwable e) {
logger.warn("Failed to load registry cache file " + file, e);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
logger.warn(e.getMessage(), e);
}
}
}
}
}
设计模式——模版模式
Dubbo整个注册中心采用了模版模式,类继承关系如下
AbstractRegister实现了Register接口中的注册、订阅、查询、通知等方法,FailbackRegistery又继承了AbstractRegister抽象类,重写了父类的注册、订阅、查询、通知方法,还添加了4个未实现的抽象模版方法,代码如下
org.apache.dubbo.registry.support.FailbackRegistry
// ==== Template method ====
public abstract void doRegister(URL url);
public abstract void doUnregister(URL url);
public abstract void doSubscribe(URL url, NotifyListener listener);
public abstract void doUnsubscribe(URL url, NotifyListener listener);
以订阅为例,FailbackRegistry重写了subscribe方法,但只实现了订阅的大体通用逻辑和异常处理等,具体实现逻辑交给子类实现(子类:EtcdRegistry、NacosRegistry、RedisRegistry、ZookeeperRegistry等)。
org.apache.dubbo.registry.support.FailbackRegistry#subscribe
@Override
public void subscribe(URL url, NotifyListener listener) {
super.subscribe(url, listener);
removeFailedSubscribed(url, listener);
try {
// 订阅逻辑,交给子类实现
doSubscribe(url, listener);
} catch (Exception e) {
Throwable t = e;
List<URL> urls = getCacheUrls(url);
if (CollectionUtils.isNotEmpty(urls)) {
notify(url, listener, urls);
logger.error("Failed to subscribe " + url + ", Using cached list: " + urls + " from cache file: " + getUrl().getParameter(FILE_KEY, System.getProperty("user.home") + "/dubbo-registry-" + url.getHost() + ".cache") + ", cause: " + t.getMessage(), t);
} else {
// If the startup detection is opened, the Exception is thrown directly.
boolean check = getUrl().getParameter(Constants.CHECK_KEY, true)
&& url.getParameter(Constants.CHECK_KEY, true);
boolean skipFailback = t instanceof SkipFailbackWrapperException;
if (check || skipFailback) {
if (skipFailback) {
t = t.getCause();
}
throw new IllegalStateException("Failed to subscribe " + url + ", cause: " + t.getMessage(), t);
} else {
logger.error("Failed to subscribe " + url + ", waiting for retry, cause: " + t.getMessage(), t);
}
}
// Record a failed registration request to a failed list, retry regularly
addFailedSubscribed(url, listener);
}
}
参考文献:
http://dubbo.apache.org/zh-cn/docs/user/references/registry/zookeeper.html