本文基于dubbo 2.7.5版本代码
文章目录
- 一、总述
- 二、ConfigCenterBean
- 1、includeSpringEnv属性
- 三、读取配置中心的配置
- 四、监听配置中心
- 五、ServiceNameMappingListener
- 1、如何创建ServiceNameMappingListener
- 2、发布事件ServiceConfigExportedEvent
- 六、Failed to receive INITIALIZED event from zookeeper, pls. check if url
一、总述
在dubbo里面,可以通过配置文件设置dubbo参数,或者在启动参数中设置,除了这两个之外,还可以使用配置中心,dubbo启动时可以自动从配置中心读取并更改自身配置。
dubbo支持多种形式的配置中心,包括zk,consul,apollo等。dubbo提供了访问各种配置中心的实现类,实现类如下:
这些实现类的均实现自接口DynamicConfiguration,可以将dubbo需要的几乎所有的配置都放在配置中心,同时dubbo还提供了一些监听器,当配置中心的数据发生变化,这些监听器会监听到数据变化进而修改本地配置。
本文将以zk作为配置中心进行介绍。
二、ConfigCenterBean
dubbo访问配置中心时,需要知道配置中心的地址、端口号、连接超时时间等,那么dubbo从哪里获得这些数据呢?答案是ConfigCenterBean对象。dubbo启动时候读取配置文件,将与配置中心相关的配置设置到该对象中。
ConfigCenterBean可以设置配置中心地址、连接超时时间、是否优先级最高等。还可以设置连接配置中心的用户名、密码。
在spring配置文件中以dubbo.config-centers或者dubbo.config-center为前缀可以设置ConfigCenterBean对象的值。
1、includeSpringEnv属性
includeSpringEnv属性需要注意:
includeSpringEnv用于设置是否需要从spring的Environment对象(注意dubbo也有一个Environment对象,两个对象不是一个类)中获取配置信息。默认为false。ConfigCenterBean实现了EnvironmentAware接口,在启动的时候会调用setEnvironment()方法:
public void setEnvironment(Environment environment) {
if (includeSpringEnv) {
// Get PropertySource mapped to 'dubbo.properties' in Spring Environment.
setExternalConfig(getConfigurations(getConfigFile(), environment));
// Get PropertySource mapped to 'application.dubbo.properties' in Spring Environment.
setAppExternalConfig(getConfigurations(StringUtils.isNotEmpty(getAppConfigFile()) ? getAppConfigFile() : ("application." + getConfigFile()), environment));
}
}
如果includeSpringEnv设置为true,那么将从spring的Environment对象中读取key为“dubbo.properties”和“application.dubbo.properties”的配置值,并分别设置给属性externalConfiguration和appExternalConfiguration。
那么这里设置属性externalConfiguration和appExternalConfiguration有什么用呐?
前面文章《dubbo解析-DubboBootstrap在dubbo中的作用》提到DubboBootstrap会对Environment(这里的Environment是dubbo的,不是spring的)初始化,初始化的时候将ConfigCenterBean的externalConfiguration和appExternalConfiguration的值设置到Environment对象的externalConfigurationMap和appExternalConfigurationMap。代码如下:
public void initialize() throws IllegalStateException {
ConfigManager configManager = ApplicationModel.getConfigManager();
Optional<Collection<ConfigCenterConfig>> defaultConfigs = configManager.getDefaultConfigCenter();
defaultConfigs.ifPresent(configs -> {
for (ConfigCenterConfig config : configs) {
//externalConfigurationMap和appExternalConfigurationMap是Map对象
this.setExternalConfigMap(config.getExternalConfiguration());
this.setAppExternalConfigMap(config.getAppExternalConfiguration());
}
});
}
从代码中可以看出,dubbo的Environment对象只保存了最后一个ConfigCenterBean的externalConfiguration和appExternalConfiguration的值。之后代码也会修改externalConfigurationMap和appExternalConfigurationMap,比如从配置中心拉取了配置,但是只是在此基础上更新,比如新增一个key和value,修改value值。这样便在Environment和ConfigCenterBean中各保存了一份完整的配置信息。
三、读取配置中心的配置
dubbo在启动的时候读取配置中心的配置数据。下面的分析以zk作为配置中心为例。同时zk也是默认的配置中心。
DubboBootstrap初始化(initialize方法)时要调用startConfigCenter方法,该方法代码如下:
private void startConfigCenter() {
//获取所有ConfigCenterConfig对象,ConfigCenterBean是其子类,其实这个位置获得是ConfigCenterBean对象
Collection<ConfigCenterConfig> configCenters = configManager.getConfigCenters();
if (CollectionUtils.isNotEmpty(configCenters)) {
CompositeDynamicConfiguration compositeDynamicConfiguration = new CompositeDynamicConfiguration();
//遍历每个ConfigCenterBean对象
for (ConfigCenterConfig configCenter : configCenters) {
//使用java系统属性等设置ConfigCenterBean对象的属性
configCenter.refresh();
//校验ConfigCenterBean对象的parameters是否合法
ConfigValidationUtils.validateConfigCenterConfig(configCenter);
//prepareEnvironment方法建立与配置中心的连接,拉取配置数据,并保存到本地
compositeDynamicConfiguration.addConfiguration(prepareEnvironment(configCenter));
}
environment.setDynamicConfiguration(compositeDynamicConfiguration);
}
//使用配置中心的配置更新如下对象的属性:
//ApplicationConfig、MonitorConfig、ModuleConfig、ProtocolConfig、RegistryConfig、
//ProviderConfig、ConsumerConfig。
//更新对象属性时,使用如下规则搜索配置:dubbo.类名去掉Config.id值.属性名,
//比如更新id为“test”的ProviderConfig的threads属性时,
//从配置中心搜索key=dubbo.provider.test.threads,如果能找到就更新threads属性。
configManager.refreshAll();
}
private DynamicConfiguration prepareEnvironment(ConfigCenterConfig configCenter) {
//ConfigCenterConfig必须配置address,否则为无效
if (configCenter.isValid()) {
if (!configCenter.checkOrUpdateInited()) {
return null;
}
//构建连接配置中心的url,url中包含了ip、端口、协议等
//getDynamicConfiguration根据url的协议使用SPI创建DynamicConfiguration对象
//DynamicConfiguration对象建立与配置中心的连接
DynamicConfiguration dynamicConfiguration = getDynamicConfiguration(configCenter.toUrl());
//从配置中心拉取key=dubbo.properties,group=dubbo的值(这两个值都是默认值,我们可以通过修改属性configFile来修改key)
String configContent = dynamicConfiguration.getProperties(configCenter.getConfigFile(), configCenter.getGroup());
String appGroup = getApplication().getName();
String appConfigContent = null;
if (isNotEmpty(appGroup)) {
//从配置中心拉取应用配置,group是应用名,key是appConfigFile的值,
//如果appConfigFile=null,则使用configFile,
//默认是使用configFile的值,也就是dubbo.properties
appConfigContent = dynamicConfiguration.getProperties
(isNotEmpty(configCenter.getAppConfigFile()) ? configCenter.getAppConfigFile() : configCenter.getConfigFile(),
appGroup
);
}
try {
environment.setConfigCenterFirst(configCenter.isHighestPriority())
//将配置信息保存到Environment的Map属性中,后面的配置会覆盖之前的
environment.updateExternalConfigurationMap(parseProperties(configContent));
environment.updateAppExternalConfigurationMap(parseProperties(appConfigContent));
} catch (IOException e) {
throw new IllegalStateException("Failed to parse configurations from Config Center.", e);
}
return dynamicConfiguration;
}
return null;
}
上面的代码主要目的是连接配置中心,然后从配置中心拉取指定key和group的配置数据,并保存到本地的Map对象中,最后使用这些配置数据更新dubbo的XXXConfig对象属性。
上面代码会拉取两个配置,一个是属性configFile指定的,另一个是appConfigFile指定的,拉取的第一个配置数据,可以认为是全局配置,第二个配置数据可以认为是指定应用的独特配置,而且会覆盖第一个配置数据。但是两个配置优先级都低于java应用配置(java应用配置是指通过System.getProperty获取的)。默认情况下,两个配置数据在配置中心的路径如下图(下图来源于官网):
- namespace,用于隔离不同环境的配置,可以通过配置“config.namespace”指定名字,默认是dubbo;
- config,Dubbo约定的固定节点,不可更改,所有配置和服务治理规则都存储在此节点下;
- dubbo/application,分别用来隔离全局配置、应用级别配置:dubbo是默认group值,application对应应用名,这一层对应的是上面提到的group
- dubbo.properties,此节点的node value存储具体配置内容,该节点的名字由上面提到的key指定。
prepareEnvironment方法使用如下代码建立与配置中心的连接,下面我们详细分析一下连接是如何建立的:
DynamicConfiguration dynamicConfiguration = getDynamicConfiguration(configCenter.toUrl());
我们来看方法getDynamicConfiguration:
static DynamicConfiguration getDynamicConfiguration(URL connectionURL) {
//获取连接配置中心使用的协议,下面分析以zk为例
String protocol = connectionURL.getProtocol();
//使用SPI加载DynamicConfigurationFactory对象,其支持的协议以及对应的类可以参见文件
//org.apache.dubbo.common.config.configcenter.DynamicConfigurationFactory。
DynamicConfigurationFactory factory = getDynamicConfigurationFactory(protocol);
//使用DynamicConfigurationFactory创建DynamicConfiguration
return factory.getDynamicConfiguration(connectionURL);
}
static DynamicConfigurationFactory getDynamicConfigurationFactory(String name) {
Class<DynamicConfigurationFactory> factoryClass = DynamicConfigurationFactory.class;
ExtensionLoader<DynamicConfigurationFactory> loader = getExtensionLoader(factoryClass);
return loader.getOrDefaultExtension(name);
}
如果使用的配置中心是zk,那么DynamicConfigurationFactory的实现类是ZookeeperDynamicConfigurationFactory,代码如下:
public class ZookeeperDynamicConfigurationFactory extends AbstractDynamicConfigurationFactory {
private ZookeeperTransporter zookeeperTransporter;
//使用SPI创建ZookeeperDynamicConfigurationFactory对象时调用的该方法
public void setZookeeperTransporter(ZookeeperTransporter zookeeperTransporter) {
this.zookeeperTransporter = zookeeperTransporter;
}
@Override
protected DynamicConfiguration createDynamicConfiguration(URL url) {
return new ZookeeperDynamicConfiguration(url, zookeeperTransporter);
}
}
ZookeeperDynamicConfiguration的构造方法如下:
ZookeeperDynamicConfiguration(URL url, ZookeeperTransporter zookeeperTransporter) {
this.url = url;
//构建访问配置中心的根路径,默认是:/dubbo/config/
rootPath = PATH_SEPARATOR + url.getParameter(CONFIG_NAMESPACE_KEY, DEFAULT_GROUP) + "/config";
initializedLatch = new CountDownLatch(1);
//创建监听器,监听器后面分析
this.cacheListener = new CacheListener(rootPath, initializedLatch);
//创建单个线程,用于处理被监听的事件
this.executor = Executors.newFixedThreadPool(1, new NamedThreadFactory(this.getClass().getSimpleName(), true));
//建立与配置中心的连接
zkClient = zookeeperTransporter.connect(url);
//设置监听器和监听目录
zkClient.addDataListener(rootPath, cacheListener, executor);
try {
//可以通过ConfigCenterBean的parameters设置init.timeout的值,init.timeout表示建立链接的超时时间
long timeout = url.getParameter("init.timeout", 5000);
boolean isCountDown = this.initializedLatch.await(timeout, TimeUnit.MILLISECONDS);
if (!isCountDown) {
throw new IllegalStateException("Failed to receive INITIALIZED event from zookeeper, pls. check if url "
+ url + " is correct");
}
} catch (InterruptedException e) {
logger.warn("Failed to build local cache for config center (zookeeper)." + url);
}
}
建立链接后,便可以使用下面的代码获取配置了:
String configContent = dynamicConfiguration.getProperties(configCenter.getConfigFile(), configCenter.getGroup());
通过key和group得到配置,对于zk来说,访问的目录是rootPath/group/key,其中rootPath的默认值是/dubbo/config/,group如果没有设置值的话,使用默认值:dubbo。
四、监听配置中心
建立与配置中心链接时,在ZookeeperDynamicConfiguration的构造方法中设置监听器CacheListener。代码如下:
zkClient.addDataListener(rootPath, cacheListener, executor);
cacheListener将监听rootPath路径。我们来看一下zkClient.addDataListener方法:
//该方法用于设置监听zk的指定目录
public void addDataListener(String path, DataListener listener, Executor executor) {
//listeners是一个两层map对象,类型如下:
//ConcurrentMap<String, ConcurrentMap<DataListener, TargetDataListener>>
//最外层的map,key是被监听的路径,内层的map,key和value都是监听器,
//其区别是value可以认为是对key的封装,在本代码中key是CacheListener,value是CuratorWatcherImpl。
ConcurrentMap<DataListener, TargetDataListener> dataListenerMap = listeners.get(path);
if (dataListenerMap == null) {
listeners.putIfAbsent(path, new ConcurrentHashMap<DataListener, TargetDataListener>());
dataListenerMap = listeners.get(path);
}
TargetDataListener targetListener = dataListenerMap.get(listener);
if (targetListener == null) {
//createTargetDataListener创建目标监听器,方法见下面[1]
dataListenerMap.putIfAbsent(listener, createTargetDataListener(path, listener));
targetListener = dataListenerMap.get(listener);
}
//访问zk将targetListener注册为监听器,方法代码见[2]
addTargetDataListener(path, targetListener, executor);
}
//[1] 以zk为配置中心为例
protected CuratorZookeeperClient.CuratorWatcherImpl createTargetDataListener(String path, DataListener listener) {
return new CuratorWatcherImpl(client, listener);
}
//[2]
public List<String> addTargetChildListener(String path, CuratorWatcherImpl listener) {
try {
return client.getChildren().usingWatcher(listener).forPath(path);
} catch (NoNodeException e) {
return null;
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
zkClient.addDataListener方法的监听器其实是CuratorWatcherImpl ,CuratorWatcherImpl的代码如下:
static class CuratorWatcherImpl implements CuratorWatcher, TreeCacheListener {
private CuratorFramework client;
private volatile ChildListener childListener;
private volatile DataListener dataListener;
private String path;
public CuratorWatcherImpl(CuratorFramework client, ChildListener listener, String path) {
//代码删减
}
//createTargetDataListener方法调用下面的方法创建CuratorWatcherImpl对象,
//从方法入参DataListener可以看出,只监听zk节点数据的变化
public CuratorWatcherImpl(CuratorFramework client, DataListener dataListener) {
this.dataListener = dataListener;
}
//...
//代码删减
//当数据有变化时,通知调用下面的方法
//本方法主要是做类型的转换,然后调用CacheListener通知数据变化
@Override
public void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception {
if (dataListener != null) {
if (logger.isDebugEnabled()) {
logger.debug("listen the zookeeper changed. The changed data:" + event.getData());
}
TreeCacheEvent.Type type = event.getType();
EventType eventType = null;
String content = null;
String path = null;
switch (type) {
case NODE_ADDED:
eventType = EventType.NodeCreated;
path = event.getData().getPath();
content = event.getData().getData() == null ? "" : new String(event.getData().getData(), CHARSET);
break;
case NODE_UPDATED:
eventType = EventType.NodeDataChanged;
path = event.getData().getPath();
content = event.getData().getData() == null ? "" : new String(event.getData().getData(), CHARSET);
break;
case NODE_REMOVED:
path = event.getData().getPath();
eventType = EventType.NodeDeleted;
break;
case INITIALIZED:
eventType = EventType.INITIALIZED;
break;
case CONNECTION_LOST:
eventType = EventType.CONNECTION_LOST;
break;
case CONNECTION_RECONNECTED:
eventType = EventType.CONNECTION_RECONNECTED;
break;
case CONNECTION_SUSPENDED:
eventType = EventType.CONNECTION_SUSPENDED;
break;
}
//调用CacheListener,下面介绍该方法
dataListener.dataChanged(path, content, eventType);
}
}
}
CacheListener的dataChanged方法如下:
public void dataChanged(String path, Object value, EventType eventType) {
if (eventType == null) {
return;
}
//当发生INITIALIZED类型的事件时,表示客户端缓存数据同步成功之后可以与zk服务端正常交互,
//在ZookeeperDynamicConfiguration构造方法中,调用initializedLatch.await方法等待该事件,
//默认等待5s,超时抛出异常
if (eventType == EventType.INITIALIZED) {
initializedLatch.countDown();
return;
}
if (path == null || (value == null && eventType != EventType.NodeDeleted)) {
return;
}
// TODO We only care the changes happened on a specific path level, for example
// /dubbo/config/dubbo/configurators, other config changes not in this level will be ignored
//本监听器对路径深度有检查,深度至少是四级
if (path.split("/").length >= MIN_PATH_DEPTH) {
//获取key值,也就是路径中最后一个“/”后面的内容
String key = pathToKey(path);
ConfigChangeType changeType;
//本监听器只处理下面三种事件:增加、删除、修改
switch (eventType) {
case NodeCreated:
changeType = ConfigChangeType.ADDED;
break;
case NodeDeleted:
changeType = ConfigChangeType.DELETED;
break;
case NodeDataChanged:
changeType = ConfigChangeType.MODIFIED;
break;
default:
return;
}
//创建事件ConfigChangedEvent
ConfigChangedEvent configChangeEvent = new ConfigChangedEvent(key, getGroup(path), (String) value, changeType);
//通知各个监听器
//CacheListener其实是一个复合监听器,包含了多个子监听器
//CacheListener根据被监听路径将ConfigChangedEvent事件发送给对应的监听器
Set<ConfigurationListener> listeners = keyListeners.get(path);
if (CollectionUtils.isNotEmpty(listeners)) {
listeners.forEach(listener -> listener.process(configChangeEvent));
}
}
}
dataChanged方法可以看出,CacheListener是一个复合监听器,其持有一个监听器集合,当指定目录下的数据发生变化时,通知集合中的监听器,这个集合可以包含的监听器如下:
- ServiceConfigurationListener监听目录:/dubbo/config/dubbo/接口名:version:goup+.configurators
- ProviderConfigurationListener监听目录:/dubbo/config/dubbo/ApplicationConfig的name值+.configurators
- ConsumerConfigurationListener监听目录:/dubbo/config/dubbo/ApplicationConfig的name值+.configurators
- ReferenceConfigurationListener监听目录:/dubbo/config/dubbo/接口名:version:goup+.configurators
- TagRouter监听目录:/dubbo/config/dubbo/remote.application属性值+.tag-router
- ServiceRouter监听目录:/dubbo/config/dubbo/接口名:version:goup+.condition-router
- AppRouter监听目录:/dubbo/config/dubbo/应用名+.condition-router
上面这些监听器都是在其构造方法中将自身作为监听器添加到CacheListener的listeners中。
下面简单介绍前四个监听器的作用:
- ServiceConfigurationListener:根据修改后的配置,重新发布服务;
- ProviderConfigurationListener:根据修改后的配置,重新发布服务;
- ReferenceConfigurationListener:根据修改后的配置,重新建立对远程服务的引用;
- ConsumerConfigurationListener:根据修改后的配置,重新建立对远程服务的引用。
因为ProviderConfigurationListener和ConsumerConfigurationListener监听应用目录,如果dubbo应用发布的服务或者引用的服务比较多,则会造成dubbo修改配置有延时。
如果需要修改配置,可以在配置中心修改相应目录下的数据,这样上述监听器便监听到数据变化,进而修改本地配置。dubbo不会自动向这些目录下存储配置数据。
五、ServiceNameMappingListener
该监听器监听事件ServiceConfigExportedEvent,当发生该事件后,将接口名, 应用名等信息发布到配置中心的/dubbo/config/mapping,这些信息在服务自省里面使用。
首先看一下这个监听器如何被创建的。
1、如何创建ServiceNameMappingListener
DubboBootstrap对象有一个属性是EventDispatcher类,代码如下:
private final EventDispatcher eventDispatcher = EventDispatcher.getDefaultExtension();
该属性是一个事件分发器,默认是DirectEventDispatcher,在该类的构造方法中调用下面的方法加载所有的监听器:
protected void loadEventListenerInstances() {
//通过SPI加载EventListener的实现类,
//实现类及其名字参见下方文件org.apache.dubbo.event.EventListener
ExtensionLoader<EventListener> loader = ExtensionLoader.getExtensionLoader(EventListener.class);
//将监听器注册到本事件分发器,之后便可以监听本分发器发布的事件
loader.getSupportedExtensionInstances().forEach(this::addEventListener);
}
文件org.apache.dubbo.event.EventListener内容如下:
service-mapping=org.apache.dubbo.config.event.listener.ServiceNameMappingListener
config-logging=org.apache.dubbo.config.event.listener.LoggingEventListener
service-instance=org.apache.dubbo.registry.client.event.listener.CustomizableServiceInstanceListener
registry-logging=org.apache.dubbo.registry.client.event.listener.LoggingEventListener
加载完ServiceNameMappingListener之后,便可以监听ServiceConfigExportedEvent事件。
2、发布事件ServiceConfigExportedEvent
ServiceConfig类在方法doExport中将服务暴露,暴露完毕后,发布事件ServiceConfigExportedEvent。之后ServiceNameMappingListener监听到该事件,代码如下:
public class ServiceNameMappingListener implements EventListener<ServiceConfigExportedEvent> {
//ServiceNameMapping的实现类是DynamicConfigurationServiceNameMapping
private final ServiceNameMapping serviceNameMapping = getDefaultExtension();
@Override
public void onEvent(ServiceConfigExportedEvent event) {
ServiceConfig serviceConfig = event.getServiceConfig();
//获取被暴露服务的URL,一般URL只有一个,如果服务以多种协议暴露,那么会有多个
List<URL> exportedURLs = serviceConfig.getExportedUrls();
exportedURLs.forEach(url -> {
String serviceInterface = url.getServiceInterface();//服务的全限定接口名
//gourp、version、protocol未发布到配置中心,没有使用
String group = url.getParameter(GROUP_KEY);
String version = url.getParameter(VERSION_KEY);
String protocol = url.getProtocol();//被暴露服务使用的协议
//调用DynamicConfigurationServiceNameMapping的map方法
serviceNameMapping.map(serviceInterface, group, version, protocol);
});
}
}
DynamicConfigurationServiceNameMapping的map方法代码如下:
public void map(String serviceInterface, String group, String version, String protocol) {
//MetadataService服务信息不发布到配置中心
if (IGNORED_SERVICE_INTERFACES.contains(serviceInterface)) {
return;
}
DynamicConfiguration dynamicConfiguration = DynamicConfiguration.getDynamicConfiguration();
String key = getName();//key是应用名
//content是zk节点的数据值
String content = valueOf(System.currentTimeMillis());
execute(() -> {
//将服务信息发布到配置中心,配置中心可能有多个,在publishConfig方法会遍历每个配置中心发布数据
//以zk为例,在zk上的路径是/dobbo/config/mapping/接口名/应用名,节点值是content
dynamicConfiguration.publishConfig(key, buildGroup(serviceInterface, group, version, protocol), content);
if (logger.isInfoEnabled()) {
logger.info(String.format("Dubbo service[%s] mapped to interface name[%s].",
group, serviceInterface, group));
}
});
}
监听器收到事件ServiceConfigExportedEvent后,将服务的接口名、应用名以及服务发布时间注册到配中心。以zk为例,在配置中心的路径组成是rootPath/mapping/接口名/应用名,rootPath也就是ZookeeperDynamicConfiguration类的rootPath属性值,默认是/dobbo/config,接口名是接口的全限定名,包括了包名在内。
在服务发现场景中,消费端访问配置中心,通过接口名可以找到可用的应用,具体原理见后一篇文章。
六、Failed to receive INITIALIZED event from zookeeper, pls. check if url
在启动dubbo时,有时候会报上面标题中的错误,这个错误是在ZookeeperDynamicConfiguration的构造方法中抛出的。
curator在建立与zk的连接后,会同步zk的数据,当同步完成后发布INITIALIZED事件,默认dubbo等待该事件5s,如果超过5s没有收到事件,就抛出标题中的异常。下面是对curator对INITIALIZED事件的说明:
Posted after the initial cache has been fully populated.
On startup, the cache synchronizes its internal state with the server, publishing a series of {@link #NODE_ADDED} events as new nodes are discovered. Once the cachehas been fully synchronized, this {@link #INITIALIZED} this event is published. All events published after this event represent actual server-side mutations.
On reconnection, the cache will resynchronize its internal state with the server, and fire this event again once its internal state is completely refreshed.
Note: because the initial population is inherently asynchronous, so it’s possible to observe server-side changes (such as a {@link #NODE_UPDATED}) prior to this event being published.
意思就是启动时,缓存将与服务器同步,在发现新节点时发布一系列NODE_ADDED事件。缓存完全同步后,将发布INITIALIZED事件。在此事件之后发布的所有事件都表示实际的服务器端变化。重新连接时,缓存将与服务器重新同步其内部状态,并在其内部状态完全刷新后再次触发此事件。
解决办法是将“init.timeout”的时间设置长一些,比如设置超时时间为10s:
dubbo.config-center.parameters[init.timeout]=10000