本文基于dubbo 2.7.5版本代码
下面这篇文章介绍了dubbo为什么要采用元数据中心,对元数据中心和注册中心存储的数据做了对比,最后介绍了如何配置使用,我将在本文从代码上分析元数据中心。
岛风:一文聊透 Dubbo 元数据中心
元数据中心是dubbo2.7版本之后新增的功能,主要是为了减轻注册中心的压力,将部分存储在注册中心的内容放到元数据中心。元数据中心的数据只是给自己使用的,改动不需要告知对端,比如服务端修改了元数据,不需要通知消费端。这样注册中心存储的数据减少,同时大大降低了因为配置修改导致注册中心频繁通知监听者,从而大大减轻注册中心的压力。
要使用元数据中心的功能,需要增加三项配置:
例如:
dubbo.registry.address=zookeeper://localhost:2181
dubbo.registry.simplified=true
dubbo.metadata-report.address=zookeeper://127.0.0.1:2182(不配置address,启动报错)
这个例子注册中心和元数据中心都使用了zookeeper。
元数据中心具体的配置项可以参见类MetadataReportConfig。
如果要减少注册中心的配置信息,需要增加配置simplified=true。下面介绍一下该参数如何发挥作用的。
下图是客户端启动时创建服务提供者代理对象的流程。RegistryProtocol的getRegisteredConsumerUrl里面处理simplified参数。
getRegisteredConsumerUrl的代码如下。
//simplified参数在入参registryUrl里面
//可以简单认为入参registryUrl包含了几乎所有在客户端配置的参数
//入参consumerUrl只包含将要注册到注册中心的数据
public URL getRegisteredConsumerUrl(final URL consumerUrl, URL registryUrl) {
//如果simplified=false,则在consumerUrl基础上添加另外两个配置信息
if (!registryUrl.getParameter(SIMPLIFIED_KEY, false)) {
return consumerUrl.addParameters(CATEGORY_KEY, CONSUMERS_CATEGORY,
CHECK_KEY, String.valueOf(false));
} else {
如果simplified=true,则从consumerUrl中筛选出5个值,常量定义见下方
return URL.valueOf(consumerUrl, DEFAULT_REGISTER_CONSUMER_KEYS, null).addParameters(
CATEGORY_KEY, CONSUMERS_CATEGORY, CHECK_KEY, String.valueOf(false));
}
}
//DEFAULT_REGISTER_CONSUMER_KEYS定义了简化后注册中心必须有的数据,包括:
//1、应用名,key为application
//2、应用版本,key为version
//3、分组信息,key为group
//4、dubbo rpc协议版本,key为dubbo
//5、dubbo版本,key为release
public static final String[] DEFAULT_REGISTER_CONSUMER_KEYS = {
APPLICATION_KEY, VERSION_KEY, GROUP_KEY, DUBBO_VERSION_KEY, RELEASE_KEY
};
上面方法的返回值就是要注册到注册中心的URL。例如,当simplified=true时,方法返回值如下:
consumer://192.168.56.1/com.dubbo.WebsiteProcessor?
application=consumer&category=consumers&check=false&dubbo=2.0.2&release=2.7.5
服务端的原理类似,服务端在服务暴露时,调用RegistryProtocol的getUrlToRegistry方法处理simplified。
if (!registryUrl.getParameter(SIMPLIFIED_KEY, false)) {
return providerUrl.removeParameters(getFilteredKeys(providerUrl)).removeParameters(
MONITOR_KEY, BIND_IP_KEY, BIND_PORT_KEY, QOS_ENABLE, QOS_HOST, QOS_PORT, ACCEPT_FOREIGN_IP, VALIDATION_KEY,
INTERFACES);
} else {
//下面处理simplified=false
//extraKeys为配置dubbo.registry.extra-keys的值
String extraKeys = registryUrl.getParameter(EXTRA_KEYS_KEY, "");
if (!providerUrl.getPath().equals(providerUrl.getParameter(INTERFACE_KEY))) {
if (StringUtils.isNotEmpty(extraKeys)) {
extraKeys += ",";
}
extraKeys += INTERFACE_KEY;
}
//DEFAULT_REGISTER_PROVIDER_KEYS为要注册到注册中心的数据的key,值见下方
String[] paramsToRegistry = getParamsToRegistry(DEFAULT_REGISTER_PROVIDER_KEYS
, COMMA_SPLIT_PATTERN.split(extraKeys));
//组装URL
return URL.valueOf(providerUrl, paramsToRegistry, providerUrl.getParameter(METHODS_KEY, (String[]) null));
}
public static final String[] DEFAULT_REGISTER_PROVIDER_KEYS = {
APPLICATION_KEY, CODEC_KEY, EXCHANGER_KEY, SERIALIZATION_KEY, CLUSTER_KEY, CONNECTIONS_KEY, DEPRECATED_KEY,
GROUP_KEY, LOADBALANCE_KEY, MOCK_KEY, PATH_KEY, TIMEOUT_KEY, TOKEN_KEY, VERSION_KEY, WARMUP_KEY,
WEIGHT_KEY, TIMESTAMP_KEY, DUBBO_VERSION_KEY, RELEASE_KEY
};
对服务端来说,注册中心需要注册的数据比较多,不再一一介绍,大家看DEFAULT_REGISTER_PROVIDER_KEYS即可。
元数据中心的配置类是MetadataReportConfig,可以在配置文件中使用前缀“dubbo.metadata-report”设置MetadataReportConfig的属性。也可以配置多个元数据中心,但是在dubbo 2.7.5版本里面只能其中一个生效。
元数据中心的启动流程图,元数据中心以zookeeper为例:
MetadataReportInstance的metadataReport属性是一个静态属性。MetadataService下一篇文章介绍。
客户端和服务端都是在DubboBootstrap中启动的元数据中心,元数据中心使用接口MetadataReport表示,实现类有:
本文将以zookeeper为例介绍元数据中心。如果使用redis或者其他作为元数据中心,在dubbo.metadata-report.address中修改协议即可。
该类是所有元数据中心的顶层抽象类。先看一下其构造方法,构造方法主要做了两件事:
public AbstractMetadataReport(URL reportServerURL) {
setUrl(reportServerURL);
//构建本地文件名,APPLICATION_KEY默认不存在,需要在
//MetadataReportConfig.parameters中配置
String defaultFilename = System.getProperty("user.home") + "/.dubbo/dubbo-metadata-" + reportServerURL.getParameter(APPLICATION_KEY) + "-" + reportServerURL.getAddress().replaceAll(":", "-") + ".cache";
//如果在MetadataReportConfig.parameters中配置了file参数,则使用它作为文件名
String filename = reportServerURL.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 service store file " + file + ", cause: Failed to create directory " + file.getParentFile() + "!");
}
}
if (!initialized.getAndSet(true) && file.exists()) {
file.delete();//第一次启动的时候将文件删除
}
}
this.file = file;
loadProperties();//这里调用该方法没有用,因为文件不存在
//设置是否异步将数据传输到元数据中心
syncReport = reportServerURL.getParameter(SYNC_REPORT_KEY, false);
//当访问元数据中心失败时,MetadataReportRetry设置了两个与重试相关的参数:
//1、重试次数;2、多长时间重试一次
metadataReportRetry = new MetadataReportRetry(reportServerURL.getParameter(RETRY_TIMES_KEY, DEFAULT_METADATA_REPORT_RETRY_TIMES),
reportServerURL.getParameter(RETRY_PERIOD_KEY, DEFAULT_METADATA_REPORT_RETRY_PERIOD));
//参数cycle.report表示是否按照一定的频率将元数据更新到元数据中心,默认是true
if (reportServerURL.getParameter(CYCLE_REPORT_KEY, DEFAULT_METADATA_REPORT_CYCLE_REPORT)) {
//使用定时器按照一定的频率将元数据更新到元数据中心
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("DubboMetadataReportTimer", true));
//定时任务是每天运行一次,在每天的2:00至6:00一个随机时间
scheduler.scheduleAtFixedRate(this::publishAll, calculateStartTime(), ONE_DAY_IN_MIll, TimeUnit.MILLISECONDS);
}
}
下面介绍该类的主要方法。
先看一下构造方法中定时任务调用的publishAll方法。
void publishAll() {
logger.info("start to publish all metadata.");
//allMetadataReports是一个Map类型,
//存储了客户端和服务端的所有与服务相关的元数据
this.doHandleMetadataCollection(allMetadataReports);
}
//遍历allMetadataReports中的值,
//如果是服务端的元数据,调用storeProviderMetadata;
//如果是客户端的元数据,调用storeConsumerMetadata
private boolean doHandleMetadataCollection(Map<MetadataIdentifier, Object> metadataMap) {
if (metadataMap.isEmpty()) {
return true;
}
Iterator<Map.Entry<MetadataIdentifier, Object>> iterable = metadataMap.entrySet().iterator();
while (iterable.hasNext()) {
Map.Entry<MetadataIdentifier, Object> item = iterable.next();
if (PROVIDER_SIDE.equals(item.getKey().getSide())) {
this.storeProviderMetadata(item.getKey(), (FullServiceDefinition) item.getValue());
} else if (CONSUMER_SIDE.equals(item.getKey().getSide())) {
this.storeConsumerMetadata(item.getKey(), (Map) item.getValue());
}
}
return false;
}
publishAll的作用是调用doHandleMetadataCollection方法遍历allMetadataReports,然后将服务端和客户端的数据分别存储到元数据中心。
allMetadataReports是AbstractMetadataReport的一个属性,类型是Map
MetadataIdentifier对象记录了服务接口的名字、版本、分组、是服务端还是消费端标示、应用名。
该方法用于保存服务接口元数据信息。
服务端发布服务、客户端创建代理时,都会调用storeProviderMetadata方法。该方法的入参可以参考allMetadataReports的key和value值的介绍。
客户端也会调用该方法,但是客户端调用该方法保存的信息与服务端保存的内容是一样的,客户端以引用的服务接口为基础,构建MetadataIdentifier和ServiceDefinition对象。在一些情况下可能会存在客户端和服务端存储的数据相互覆盖的情况。
public void storeProviderMetadata(MetadataIdentifier providerMetadataIdentifier, ServiceDefinition serviceDefinition) {
//是同步保存,还是异步保存,如果异步的话,使用线程池,
//reportCacheExecutor是单线程的线程池,
//reportCacheExecutor=Executors.newFixedThreadPool(1, new NamedThreadFactory("DubboSaveMetadataReport", true));
if (syncReport) {
storeProviderMetadataTask(providerMetadataIdentifier, serviceDefinition);
} else {
reportCacheExecutor.execute(() -> storeProviderMetadataTask(providerMetadataIdentifier, serviceDefinition));
}
}
private void storeProviderMetadataTask(MetadataIdentifier providerMetadataIdentifier, ServiceDefinition serviceDefinition) {
try {//代码有删减
allMetadataReports.put(providerMetadataIdentifier, serviceDefinition);
//failedReports记录保存失败的数据,如果保存成功,则删除
failedReports.remove(providerMetadataIdentifier);
Gson gson = new Gson();
String data = gson.toJson(serviceDefinition);
//doStoreProviderMetadata由子类实现,
//例如,ZookeeperMetadataReport是将数据直接保存到zookeeper
//providerMetadataIdentifier用于构造zk的路径,data是zk节点上的数据
doStoreProviderMetadata(providerMetadataIdentifier, data);
//将数据保存到本地文件,本地文件名在构造方法中已经创建
saveProperties(providerMetadataIdentifier, data, true, !syncReport);
} catch (Exception e) {
//记录保存失败的元数据
failedReports.put(providerMetadataIdentifier, serviceDefinition);
//重试,下面小节分析
metadataReportRetry.startRetryTask();
}
}
保存到元数据中心和文件中的元数据是以json格式存储的。
saveProperties方法是将元数据存储到本地文件中,这个方法里面使用了一个自增的数字作为版本,每修改一次文件数字加1,如果下次更新时发现版本比当前版本小,则不修改文件。
与storeProviderMetadata相对,但是该方法没有原始调用点,可能会在后续版本更新中使用,该方法原理与storeProviderMetadata类似。
saveServiceMetadata方法将本dubbo实例所有暴露的服务存储到元数据中心。与saveServiceMetadata相对,getExportedURLs方法是查询暴露的服务。
saveSubscribedData将客户端引用的服务存储到元数据中心。getSubscribedURLs从元数据中心查询客户端引用的服务。
该类是一个内部类。方法storeConsumerMetadata和storeProviderMetadata,如果出现访问元数据中心失败,那么会调用MetadataReportRetry的startRetryTask进行重试。
在AbstractMetadataReport构造方法中创建MetadataReportRetry对象,其入参有两个:1、重试次数;2、多长时间重试一次。MetadataReportRetry的startRetryTask方法使用异步线程池中的线程按照要求重试。每次重试调用retry方法,尝试将数据保存至元数据中心:
public boolean retry() {
return doHandleMetadataCollection(failedReports);
}
当全局重试次数超过了retry.times的设定值,则调用cancelRetryTask方法:
void cancelRetryTask() {
retryScheduledFuture.cancel(false);//取消正在运行的任务
//关闭线程池,之后重试机制无法使用,
//当超过重试次数后,dubbo认为元数据中心出现问题,可能宕机或者网络断了
//这有一个缺点,即使元数据中心可以使用了或者网络连通了,重试功能也无法使用了,只能重启应用程序
retryExecutor.shutdown();
}
该类以zookeeper作为元数据中心,该类在构造方法中创建与zookeeper的连接,之后如果元数据有变更,直接将数据存储到zookeeper。