今天有测试开发反馈说某个api线上暴露了2个服务,登上一台机器连接dubbo端口,如下:
dubbo>ls com.yt.smc.api.redbagrain.app.SmcRedbagRainActivityAppApi
com.yt.smc.api.redbagrain.app.SmcRedbagRainActivityAppApi
com.yt.smc.api.redbagrain.app.SmcRedbagRainActivityAppApi
dubbo>ls com.yt.smc.api.redbagrain.app.SmcRedbagRainActivityAppApi2
giveOutSmcRedbagRainPrize
pullSmcRedbagRainActivityTime
dubbo>ls com.yt.smc.api.redbagrain.app.SmcRedbagRainActivityAppApi
giveOutSmcRedbagRainPrize
pullSmcRedbagRainActivityTime
再查一下监控平台发现该api确实暴露了两个,url如下:
dubbo://172.16.16.52:28887/com.yt.smc.api.redbagrain.app.SmcRedbagRainActivityAppApi?anyhost=true&application=smc&charset=UTF-8&default.delay=-1&default.executes=200&default.export=true&default.group=smc_prod&default.retries=0&default.service.filter=ytTraceFilter&default.timeout=5000&delay=-1&dubbo=2.8.4&export=true&generic=false&interface=com.yt.smc.api.redbagrain.app.SmcRedbagRainActivityAppApi&logger=log4j&methods=giveOutSmcRedbagRainPrize,pullSmcRedbagRainActivityTime&owner=turen&pid=7212&revision=2.5.6&side=provider&threads=300×tamp=1577268934419
dubbo://172.16.16.52:28887/com.yt.smc.api.redbagrain.app.SmcRedbagRainActivityAppApi2?anyhost=true&application=smc&charset=UTF-8&default.delay=-1&default.executes=200&default.export=true&default.group=smc_prod&default.retries=0&default.service.filter=ytTraceFilter&default.timeout=5000&delay=-1&dubbo=2.8.4&export=true&generic=false&interface=com.yt.smc.api.redbagrain.app.SmcRedbagRainActivityAppApi&logger=log4j&methods=giveOutSmcRedbagRainPrize,pullSmcRedbagRainActivityTime&owner=turen&pid=7212&revision=2.5.6&side=provider&threads=300×tamp=1577268934451
排查一下代码发现,确实同一个接口注册了两遍(两个开发处理git冲突引起),服务配置如下:
<dubbo:service interface="com.yt.smc.api.redbagrain.app.SmcRedbagRainActivityAppApi" ref="smcRedbagRainActivityAppApi"/>
<dubbo:service interface="com.yt.smc.api.redbagrain.app.SmcRedbagRainActivityAppApi" ref="smcRedbagRainActivityAppApi"/>
查明其原理首先想到的是dubbo对service标签的解析(这里主要看解析第二次注册的情况),这里可以定位到DubboBeanDefinitionParser类的parse方法,找到对interface属性的解析代码如下:
String id = element.getAttribute("id");
// id属性为空的情况,从上面配置上看并无配置id
if (StringUtils.isEmpty(id) && required) {
// 上面配置也没有配置name属性
String generatedBeanName = element.getAttribute("name");
if (StringUtils.isEmpty(generatedBeanName)) {
if (ProtocolConfig.class.equals(beanClass)) {
generatedBeanName = "dubbo";
} else {
// generatedBeanName为interface配置的值,即com.yt.smc.api.redbagrain.app.SmcRedbagRainActivityAppApi
generatedBeanName = element.getAttribute("interface");
}
}
if (StringUtils.isEmpty(generatedBeanName)) {
generatedBeanName = beanClass.getName();
}
id = generatedBeanName;
int counter = 2;
// spring容器中已经包含该id,则在后面加上计数器,出现了com.yt.smc.api.redbagrain.app.SmcRedbagRainActivityAppApi2
while (parserContext.getRegistry().containsBeanDefinition(id)) {
id = generatedBeanName + (counter++);
}
}
// id属性不为空的情况,从代码上看不允许配置重复的id,上面的2个配置若配置了同一个id,启动便失败
if (id != null && id.length() > 0) {
if (parserContext.getRegistry().containsBeanDefinition(id)) {
throw new IllegalStateException("Duplicate spring bean id " + id);
}
parserContext.getRegistry().registerBeanDefinition(id, beanDefinition);
beanDefinition.getPropertyValues().addPropertyValue("id", id);
}
简而言之,DubboBeanDefinitionParser解析service(ServiceBean)标签时发现spring容器中interface属性已经存在类对应的generatedBeanName值,就会使用计数器在原有上generatedBeanName上加1,这样解析到的ServiceBean的beanName属性即是:
com.yt.smc.api.redbagrain.app.SmcRedbagRainActivityAppApi2
在初始化ServiceBean时会调用其afterPropertiesSet方法,在该方法中会把beanName属性设置到ServiceBean父类ServiceConfig的path属性中,代码如下:
if (StringUtils.isEmpty(getPath())) {
if (StringUtils.isNotEmpty(beanName)
&& StringUtils.isNotEmpty(getInterface())
&& beanName.startsWith(getInterface())) {
setPath(beanName);
}
}
接着在暴露该api过程中,即调用ServiceConfig#doExportUrlsFor1Protocol方法时会使用ServiceConfig的path属性来拼接url暴露出来,这样就产生了两个不同的url。
暴露了两个api服务,使用起来不会出问题,从性能上考虑是否有影响呢?有了下面的思考:
若同一个接口暴露了2次,对服务消费者或者服务端是不是性能提高了?不是的,不论消费者怎么配置,服务端的处理能力是有限的,比如说服务端处理的并发线程数量(可通过service标签的executes属性来配置),如果服务端将com.yt.smc.api.redbagrain.app.SmcRedbagRainActivityAppApi注册了两遍,就生导出两个exporter,在执行调用的时候会触发ExecuteLimitFilter判断,而在ExecuteLimitFilter判断逻辑中,是以url做为key,处理数做为value来计算的,url不同就会被视为不同的服务,所以若服务端将一个api注册两次,且配置executes为20的话,相当于在注册一次情况下将executes配置为40,其中ExecuteLimitFilter源码如下:
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
URL url = invoker.getUrl();
String methodName = invocation.getMethodName();
int max = url.getMethodParameter(methodName, Constants.EXECUTES_KEY, 0);
if (!RpcStatus.beginCount(url, methodName, max)) {
throw new RpcException("Failed to invoke method " + invocation.getMethodName() + " in provider " + url + ", cause: The service using threads greater than + max + "\" /> limited.");
}
long begin = System.currentTimeMillis();
boolean isSuccess = true;
try {
return invoker.invoke(invocation);
} catch (Throwable t) {
isSuccess = false;
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new RpcException("unexpected exception when ExecuteLimitFilter", t);
}
} finally {
RpcStatus.endCount(url, methodName, System.currentTimeMillis() - begin, isSuccess);
}
}
public static boolean beginCount(URL url, String methodName, int max) {
max = (max <= 0) ? Integer.MAX_VALUE : max;
RpcStatus appStatus = getStatus(url);
RpcStatus methodStatus = getStatus(url, methodName);
if (methodStatus.active.incrementAndGet() > max) {
methodStatus.active.decrementAndGet();
return false;
} else {
appStatus.active.incrementAndGet();
return true;
}
}
public static RpcStatus getStatus(URL url, String methodName) {
String uri = url.toIdentityString();
ConcurrentMap<String, RpcStatus> map = METHOD_STATISTICS.get(uri);
if (map == null) {
METHOD_STATISTICS.putIfAbsent(uri, new ConcurrentHashMap<String, RpcStatus>());
map = METHOD_STATISTICS.get(uri);
}
RpcStatus status = map.get(methodName);
if (status == null) {
map.putIfAbsent(methodName, new RpcStatus());
status = map.get(methodName);
}
return status;
}