[apache/dubbo]同一个服务的@Reference多种写法,导致netty不正常关闭,服务不正常

2024-06-24 103 views
8

项目A消费项目B的同一个dubbo类,如果存在不同的@Reference写法,可能会导致对应B服务下线后,对应内部连接不关闭,导致A调用调用B服务出现异常,此问题必现。

Environment
  • Dubbo version: 2.7.4.1 2.7.15,注册中心是 nacos ,本地客户端1.1.4和2.1.0都试过
  • Operating System version: Mac Linux
  • Java version: 1.8
Steps to reproduce this issue
  1. 项目B是个服务比如ProductInfoFacade 的服务提供者,项目A是消费项目B的ProductInfoFacade服务,服务中存在两种@Reference 写法。比如一个Service里面是 @Reference,另外个Service中是 @Reference(injvm = false)。

  2. 启动项目A和项目B服务,项目B多起几个服务(保证A能正常调用B服务)。关闭其中一台B服务的机器C,这个时候nacos会通知A服务去关闭这台C机器对应的所有dubbo消费者,正常会关闭C机器的netty连接,但是实际上由于有2个@Reference不同写法,会导致 org.apache.dubbo.rpc.protocol.dubbo.ReferenceCountExchangeClient 的 referenceCount 比正常时候多一个(每多一种这种同类的不同写法,就会比正常多一个),这样在关闭的时候,对应的netty连接不会关闭,因为ReferenceCountExchangeClient 的 close方法的 “referenceCount.decrementAndGet() <= 0” 不会小于等于0,导致最终不会关闭这个netty连接。后台有一个校验netty channel连接的任务“org.apache.dubbo.remoting.exchange.support.header.ReconnectTimerTask” (1分钟一次的)会一直重连这个netty连接,但是会失败走入到“logger.error("Fail to connect to " + channel, e);”(由于对应的C机器已经关闭了,肯定是连接不上,会有很多异常日志),同时 带@Reference(injvm = false)注解的ProductInfoFacade调用会报错,提示连接已断开。"org.apache.dubbo.rpc.RpcException: Failed to invoke remote method: getXXXX. provider: dubbo://10.132.138.241:20880/com.xxx.ProductInfoFacade?xxxxx. cause: message can not send. because channel is closed ",目前测试,带@Reference 注解的服务可以正常调度。

  3. 我们为什么加 @Reference(injvm = false)? 是由于我们这边并没有完全RPC化,存在 https://github.com/apache/dubbo/issues/6776 这个问题,这个问题也是2.7.8版本中解决 https://github.com/apache/dubbo/issues/6224在“org.apache.dubbo.config.spring.beans.factory.annotation.ReferenceAnnotationBeanPostProcessor.doGetInjectedBean”方法中添加了“prepareReferenceBean(referencedBeanName, referenceBean, localServiceBean);”导致的循环引用问题(“问题出现在 ServiceBean serviceBean = getServiceBean(referencedBeanName);”),代码如下

    @Override
    protected Object doGetInjectedBean(AnnotationAttributes attributes, Object bean, String beanName, Class<?> injectedType,
                                       InjectionMetadata.InjectedElement injectedElement) throws Exception {
        /**
         * The name of bean that annotated Dubbo's {@link Service @Service} in local Spring {@link ApplicationContext}
         */
        String referencedBeanName = buildReferencedBeanName(attributes, injectedType);
    
        /**
         * The name of bean that is declared by {@link Reference @Reference} annotation injection
         */
        String referenceBeanName = getReferenceBeanName(attributes, injectedType);
    
        referencedBeanNameIdx.computeIfAbsent(referencedBeanName, k -> new TreeSet<String>()).add(referenceBeanName);
    
        ReferenceBean referenceBean = buildReferenceBeanIfAbsent(referenceBeanName, attributes, injectedType);
    
        boolean localServiceBean = isLocalServiceBean(referencedBeanName, referenceBean, attributes);
       // 添加了这个逻辑导致的
        prepareReferenceBean(referencedBeanName, referenceBean, localServiceBean);
    
        registerReferenceBean(referencedBeanName, referenceBean, localServiceBean, referenceBeanName);
    
        cacheInjectedReferenceBean(referenceBean, injectedElement);
    
        return getBeanFactory().applyBeanPostProcessorsAfterInitialization(referenceBean.get(), referenceBeanName);
    }
    ....
    private void prepareReferenceBean(String referencedBeanName, ReferenceBean referenceBean, boolean localServiceBean) {
        //  Issue : https://github.com/apache/dubbo/issues/6224
        if (localServiceBean) { // If the local @Service Bean exists
            referenceBean.setInjvm(Boolean.TRUE);
            exportServiceBeanIfNecessary(referencedBeanName); // If the referenced ServiceBean exits, export it immediately
        }
    }
    
    private void exportServiceBeanIfNecessary(String referencedBeanName) {
        if (existsServiceBean(referencedBeanName)) {
            // 在初始化这个ServiceBean的时候,如果遇到 @Reference这个类的,还会走这个方法,第二次就是一个Spring的引用。
            ServiceBean serviceBean = getServiceBean(referencedBeanName);
            if (!serviceBean.isExported()) {
                serviceBean.export();
            }
        }
    }

    问题具体是 A服务的某个dubbo服务的实现类,如(ProductInfoFacadeImpl,实现的ProductInfoFacade),引入了B服务的一个Service,该Service中又有 @Reference这个 ProductInfoFacade,导致A服务在启动的时候,会去初始化 其他类的 @Reference这个 ProductInfoFacade服务时,会去加载A服务本地的ProductInfoFacadeImpl服务,Spring加载的,会去依赖B服务的Service,然后再是里面的 ProductInfoFacade时,又走了一次这个exportServiceBeanIfNecessary方法,这个时候获取的ServiceBean对象是一个SpringBean的引用,未完成初始化完,因为当前服务还在初始化这个ProductInfoFacade的ServiceBean,所以后面我们加了一个 @Reference(injvm = false),这样的话,就不会走 prepareReferenceBean 里面的 exportServiceBeanIfNecessary方法,完全走RPC了,不走本地服务,避免了这种循环依赖问题。

  4. 目前测试下来,如果是同一个dubbo服务的@Reference保持一样的写法,只留一种的话,不会存在这个问题。。如果存在2种,在关闭服务提供者的时候,消费者就会有问题。

回答

1
@RestController
@RequestMapping("/test")
public class TestController {

    @Reference
    private RoomFacade roomFacade;
    @Reference(injvm = false)
    private RoomFacade roomFacade2;
    @Reference
    private AnchorFacade anchorFacade;
    @Reference(injvm = false)
    private AnchorFacade anchorFacade2;

    @GetMapping("/test")
    public ReturnValue test(int type, Long id) {
       // TODO 根据type来调度不同的dubbo服务,实现
        return ReturnValue.renderSuccess();
    }
}

这种作为消费者的项目做测试就ok。。可以必现这种问题。。只需要启动后,把 “RoomFacade”和“AnchorFacade” 的服务提供者的机器随便关闭一台就能复现。。

1

升级到 3.0 或者 3.1 试下,这个我本地测试是没问题的

7

3.0.7版本也存在这个问题,有解决的吗?

5

可以提一个可以复现的 samples 吗

9

版本升级到 3.1.1 是可以通过测试的,应该是之前的版本在服务多订阅的时候推送不全导致的。

3

我这边要提供样例吗?

6

dubbo 版本升级到 3.1.1 看下还会不会,如果还有问题的话提供个 demo 样例

6

3.0.9及之后版本的已经修复了

8

具体步骤可以参考项目里面的 README.md

9

image

3.1.1 中继续重连是因为开启了推空保护,在 provider 地址变成 0 的时候会保留最后一个地址防止由于注册中心的 bug 导致地址为 0。

0

@lvliuzhong 注册中心地址上添加 enable-empty-protection=false 参数可以关闭推空保护

4

之前的版本也是这么解决的吗?

2

之前版本没有提供推空保护功能,这个功能是和 Nacos 一起合作设计的,目标是避免在 Nacos Server 挂了的时候对服务造成服务直接不可用的影响