同一dubbo服务暴露两次原理分析

文章目录

        • 1.背景
        • 2. 原理
        • 3. 引发的思考

1.背景

今天有测试开发反馈说某个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"/>

2. 原理

查明其原理首先想到的是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。

3. 引发的思考

暴露了两个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;
}

你可能感兴趣的:(dubbo源码分析)