摘要:大部分互联网公司都会使用dubbo作为微服务架构的中间件选择,这篇博客从一次故障说起,延伸到dubbo源码分析,以供他人借鉴。
在某次服务迁移中,需要把服务从服务器集群A迁移到服务器集群B,为了保证服务平滑迁移,所以存在一个时期,是服务器集群A和B同时存在的,我的服务provider要向集群A和B同时注册服务、提供服务。配置如下
<dubbo:registry address="A ip address" />
<dubbo:registry address="B ip address"/>
某日,集群A到期,已经彻底不可用,此时provider注册失败,产生大量warn日志
次日,日常发布时,dubbo服务发布失败,到致服务全部挂掉。
查看日志时,发现dubbo服务在提供服务时不断的向集群A注册服务,由于集群A已经不可用,所以注册全部失败。后来将dubbo配置中A的registry拿掉即解决问题。
但是,为了分析深层原因,我对dubbo注册服务相关的源码进行了分析。
在常规的使用中,dubbo往往和spring bean和spring context模块结合起来使用,下面是一个最简单的dubbo和spring结合的例子
public class Application {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/dubbo-provider.xml");
context.start();
System.in.read();
}
}
provider的xml配置文件如下
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<dubbo:application name="demo-provider"/>
<dubbo:registry address="multicast://224.5.6.7:1234" />
<dubbo:protocol name="dubbo"/>
<bean id="demoService" class="org.apache.dubbo.demo.provider.DemoServiceImpl"/>
<dubbo:service interface="org.apache.dubbo.demo.DemoService" ref="demoService"/>
beans>
其中的dubbo:service标签会被spring注册为ServiceBean对象,其中的原理就是spring提供了创建自定义标签的能力,dubbo的xsd文件实现了对标签的解析并且生成bean。
我们看一下ServiceBean源码
public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean,
ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, BeanNameAware,
ApplicationEventPublisherAware {
// ..省略了一些不重要的代码
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
SpringExtensionFactory.addApplicationContext(applicationContext);
supportedApplicationListener = addApplicationListener(applicationContext, this);
}
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (!isExported() && !isUnexported()) {
// 在spring初始化完成所有bean时,会发布事件。这个方法监听此事件,并进行export操作
export();
}
}
@Override
public void afterPropertiesSet() throws Exception {
// 在service bean的各个属性被赋值之后,这里会根据全局、或自定义的dubbo配置,将bean的一些空白属性给填补上。
}
@Override
public void export() {
super.export();// 这里进入ServiceConfig的export方法
// Publish ServiceBeanExportedEvent
publishExportEvent();
}
/**
* @since 2.6.5
*/
private void publishExportEvent() {
ServiceBeanExportedEvent exportEvent = new ServiceBeanExportedEvent(this);
applicationEventPublisher.publishEvent(exportEvent);
}
}
在export方法中进入了ServiceConfig的export方法
class ServiceConfig {
public synchronized void export() {
checkAndUpdateSubConfigs();
if (provider != null) {
if (export == null) {
export = provider.getExport();
}
if (delay == null) {
delay = provider.getDelay();
}
}
if (export != null && !export) {
return;
}
//在这里会判断如果设置了延迟注册,就会在delay时间后,进行注册
if (delay != null && delay > 0) {
delayExportExecutor.schedule(this::doExport, delay, TimeUnit.MILLISECONDS);
} else {
doExport();
}
}
}
进入doExport方法–>进入doExportUrls方法
private void doExportUrls() {
// 获取配置的多个注册中心
List<URL> registryURLs = loadRegistries(true);
for (ProtocolConfig protocolConfig : protocols) {
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
// export service
String contextPath = protocolConfig.getContextpath();
String host = this.findConfigedHosts(protocolConfig, registryURLs, map);
Integer port = this.findConfigedPorts(protocolConfig, name, map);
URL url = new URL(name, host, port, (StringUtils.isEmpty(contextPath) ? "" : contextPath + "/") + path, map);
String scope = url.getParameter(Constants.SCOPE_KEY);
// don't export when none is configured
if (!Constants.SCOPE_NONE.equalsIgnoreCase(scope)) {
// export to local if the config is not remote (export to remote only when config is remote)
if (!Constants.SCOPE_REMOTE.equalsIgnoreCase(scope)) {
exportLocal(url);
}
// export to remote if the config is not local (export to local only when config is local)
if (!Constants.SCOPE_LOCAL.equalsIgnoreCase(scope)) {
if (CollectionUtils.isNotEmpty(registryURLs)) {
for (URL registryURL : registryURLs) {
url = url.addParameterIfAbsent(Constants.DYNAMIC_KEY, registryURL.getParameter(Constants.DYNAMIC_KEY));
URL monitorUrl = loadMonitor(registryURL);
if (monitorUrl != null) {
url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());
}
// For providers, this is used to enable custom proxy to generate invoker
String proxy = url.getParameter(Constants.PROXY_KEY);
if (StringUtils.isNotEmpty(proxy)) {
registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy);
}
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
//这里是真正注册服务的地方
Exporter<?> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);
}
} else {
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
Exporter<?> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);
}
/**
* @since 2.7.0
* ServiceData Store
*/
MetadataReportService metadataReportService = null;
if ((metadataReportService = getMetadataReportService()) != null) {
metadataReportService.publishProvider(url);
}
}
}
this.urls.add(url);
}
Invoker> invoker = proxyFactory.getInvoker;
Exporter> exporter = protocol.export(wrapperInvoker);
这两句是最核心的代码了
Invoker是服务provider向外提供服务的一个代理类,一般由JavassistProxyFactory 实现,JavassistProxyFactory 很简单
@Override
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
// TODO Wrapper cannot handle this scenario correctly: the classname contains '$'
final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
return new AbstractProxyInvoker<T>(proxy, type, url) {
@Override
protected Object doInvoke(T proxy, String methodName,
Class<?>[] parameterTypes,
Object[] arguments) throws Throwable {
return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
}
};
}
他首先将你的类使用Wrapper类包装一次,然后调用wrapper类的invokeMethod方法。
Wrapper包装的过程也很简单,通过反射拿到你的类的属性,方法,根据这些,生成一模一样的方法属性,简单来说,可以认为Wrapper类是你的类的子类。
下一步便是 Exporter> exporter = protocol.export(wrapperInvoker);这里将使用你配置的协议暴露服务,如果你使用的是dubbo服务,将进入DubboProtocol的export方法
后面是一些连接注册中心,订阅消息的操作,服务注册到这里基本结束。
回到本文的最前面,如果有两个注册中心,一个注册中心不可用,服务注册时会发生什么?
在dubbo官方版本中,如果一个注册中心3秒内连不上,整个程序会停掉,并关闭所有注册中心的连接
[24/09/19 08:24:42:628 CST] main-SendThread(172.x.x.x:x) INFO zookeeper.ClientCnxn: Opening socket connection to server 172.x.x.x/172.x.x.x:x. Will not attempt to authenticate using SASL (unknown error)
Exception in thread "main" java.lang.IllegalStateException: Failed to connect to config center (zookeeper): 172.x.x.x:x in 30000ms.
at org.apache.dubbo.configcenter.support.zookeeper.ZookeeperDynamicConfiguration.<init>(ZookeeperDynamicConfiguration.java:75)
at org.apache.dubbo.configcenter.support.zookeeper.ZookeeperDynamicConfigurationFactory.createDynamicConfiguration(ZookeeperDynamicConfigurationFa
然而在我们使用的版本中,dubbo会不断重试,导致已经注册到正常注册中心的连接不会关掉,而后面要注册到zk的连接则不断重试。由于已经正常注册到zk的服务正是用来检查服务状态的服务,所以在服务正确性检查时未发现dubbo服务启动失败。