Dubbo超时生效原理以及服务端配置推送


Dubbo 客户端超时设置是Dubbo 客户端参数最常用设置参数。本文介绍在Dubbo超时参数配置生效原理,对应超时异常问题排查和Dubbo里两种特殊超时原理。

1. Dubbo超时参数设置方式

1.1 Dubbo 超时的配置方式

Dubbo的一般启动方式有三种,注解方式,xml方式,api方式。

<!--xml方式-->
 <dubbo:reference id="demoServiceRef"  timeout = "1000" interface="com.ctrip.framework.cdubbo.demo3.api.HelloBOMService" >
        <dubbo:parameter key="validateServiceId" value="false"/>
    </dubbo:reference>

注解的方式

@RestController
public class DubboController {

  @DubboReference(timeout = 10000)
  public HelloBOMService helloBOMService;

  @RequestMapping("/")

使用API的方式

public static void main(String[] args) throws IOException {
      ReferenceBuilder referenceBuilder = new ReferenceBuilder();
      referenceBuilder.<HelloBOMService>timeout(3000);
      ReferenceConfig<HelloBOMService> referenceConfig = referenceBuilder.build();
      HelloBOMService someClient = referenceConfig.get();

1.2 不同配置级别的超时

Dubbo的配置分为服务端和客户端,客户端的超时还有服务级别和操作级别。
优先级为服务端> 客户端服务级别>客户端方法级别

2. Dubbo超时是如何生效

超时相关的类DubboInvoker DefaultFuture
DubboInvoker决定了当前这个请求需要多久之后会超时。
DefaultFuter 决定了超时之后的异常怎么产生。

2.1 超时参数的设置

直接上代码 DubboInvoker Dubbo 有oneway机制只发送不收取于是这种请求不需要超时。
主要代码在 int timeout = calculateTimeout(invocation, methodName);

  @Override
    protected Result doInvoke(final Invocation invocation) throws Throwable {
        .... 
        try {
            boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
            int timeout = calculateTimeout(invocation, methodName);
            // 1. one way 表示只发不收 所以第一个分支实际不用超时
            if (isOneway) {
                boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
                currentClient.send(inv, isSent);
                return AsyncRpcResult.newDefaultAsyncResult(invocation);
            } else {
            	// 2.响应交给哪个线程池来执行
                ExecutorService executor = getCallbackExecutor(getUrl(), inv);
                // 具体的请求,相关调用链可以参考官方文档 最后会到HeaderExchangeChannel
                CompletableFuture<AppResponse> appResponseFuture =
                        currentClient.request(inv, timeout, executor).thenApply(obj -> (AppResponse) obj);
                // save for 2.6.x compatibility, for example, TraceFilter in Zipkin uses com.alibaba.xxx.FutureAdapter
                FutureContext.getContext().setCompatibleFuture(appResponseFuture);
                AsyncRpcResult result = new AsyncRpcResult(appResponseFuture, inv);
                result.setExecutor(executor);
                return result;
            }
        } catch (TimeoutException e) {
          .... 省略各种异常
        }
    }

计算超时时我们发现这里两种机制一种基于 countdownTimeout
这个方法里面计算出来的就是当前的超时时间。默认超时是静态变量1000ms。

  private int calculateTimeout(Invocation invocation, String methodName) {
        // 1. RpcContext.getContext()后续介绍
        Object countdown = RpcContext.getContext().get(TIME_COUNTDOWN_KEY);
        int timeout = DEFAULT_TIMEOUT;
        if (countdown == null) {
           // 参数1 url,dubbo 这个地方的url实际是整合了服务端配置原理会在下面讲到
           // 参数2 方法名,dubbo从url中拿参数的时候实际是按照先拿操作界别在按照服务级别,这个地方时获取方法界别的
           // 这里第三个参数就很有意思,RpcContext 在dubbo中是类似ThreadLocal并且单次生效的配置
            timeout = (int) RpcUtils.getTimeout(getUrl(), methodName, RpcContext.getContext(), DEFAULT_TIMEOUT); 
            if (getUrl().getParameter(ENABLE_TIMEOUT_COUNTDOWN_KEY, false)) {
                invocation.setObjectAttachment(TIMEOUT_ATTACHENT_KEY, timeout); // pass timeout to remote server
            }
        } else {
           ....
        }
        return timeout;
    }

进入RpcUtils看其中的计算,其中context.getObjectAttachment("timeout") 允许用户为每一次请求设立不同的超时。

  public static long getTimeout(URL url, String methodName, RpcContext context, long defaultTimeout) {
  		//1. 设置默认
        long timeout = defaultTimeout;
        //2. 取单次生效的超时时间
        Object genericTimeout = context.getObjectAttachment("timeout");
        if (genericTimeout != null) {
            timeout = convertToNumber(genericTimeout, defaultTimeout);
        } else if (url != null) {
        	// 3. 否则取配置的超时
            timeout = url.getMethodPositiveParameter(methodName, "timeout", defaultTimeout);
        }
        return timeout;
    }

Dubbo 会先查看用户有没有设置单次超时,有的话会使用单次超时,没有的话使用url中的操作界别超时,没有在寻找服务级别的超时配置,再没有使用默认的超时配置。

2.2 超时生效机制

DefaultFuter 类保存了Dubbo客户端所有发送但是未超时且未返回的请求。
Dubbo的请求和响应都带有messageId.
发送请求1的时候,服务端返回会带有1的序列号的响应,表示是请求1的响应。
DefaultFuture创建的代码
org.apache.dubbo.remoting.exchange.support.header.HeaderExchangeChannel#request(java.lang.Object, int, java.util.concurrent.ExecutorService)·

    @Override
    public CompletableFuture<Object> request(Object request, int timeout, ExecutorService executor) throws RemotingException {
        if (closed) {
            // 不能发送不发
        }
        // create request.
        // 有个自增的变量保证dubbo的消息的ID为自增
        Request req = new Request();
        req.setVersion(Version.getProtocolVersion());
        req.setTwoWay(true);
        req.setData(request);
        // 每发一个请求就创建一个 直到有返回才删除
        DefaultFuture future = DefaultFuture.newFuture(channel, req, timeout, executor);
        try {
            channel.send(req);
        } catch (RemotingException e) {
            future.cancel();
            throw e;
        }
        return future;
    }

DefaultFuture的静态 方法都会做两个操作,new操作会把请求放到一个Map中去
timeoutCheck操作会新建一个定时检查超时的线程。

// 1. 每次new都会put 这边id是传送出去的requestId而value是返回给前面的future.
private static final Map<Long, DefaultFuture> FUTURES = new ConcurrentHashMap<>();

   public static DefaultFuture newFuture(Channel channel, Request request, int timeout, ExecutorService executor) {
        final DefaultFuture future = new DefaultFuture(channel, request, timeout);
        // 2. 这个线程池的问题,还有一个threadLess的扩展以及线程池的仓库
        future.setExecutor(executor);
        // ThreadlessExecutor needs to hold the waiting future in case of circuit return.
        if (executor instanceof ThreadlessExecutor) {
            ((ThreadlessExecutor) executor).setWaitingFuture(future);
        }
        // 3. timeout check 生成定时检查超时的任务
        timeoutCheck(future);
        return future;
    }
    
    //这东西Dubbo里面用的比较多一个线程就可以处理多个加在上面的任务
    public static final Timer TIME_OUT_TIMER = new HashedWheelTimer(
            new NamedThreadFactory("dubbo-future-timeout", true),
            30,
            TimeUnit.MILLISECONDS);
pr
ivate static void timeoutCheck(DefaultFuture future) {
        TimeoutCheckTask task = new TimeoutCheckTask(future.getId());
        //创建 超时的健康监测,因为上面设置的30ms的ticktock时间,感觉小于30ms的超时似乎不会生效
        future.timeoutCheckTask = TIME_OUT_TIMER.newTimeout(task, future.getTimeout(), TimeUnit.MILLISECONDS);
    }

 private static class TimeoutCheckTask implements TimerTask {
        private final Long requestID;
        TimeoutCheckTask(Long requestID) {
            this.requestID = requestID;
        }

        @Override
        public void run(Timeout timeout) {
              ····
            // 1. 假如时间到了,利用利用requestId 找到对应的请求
            DefaultFuture future = DefaultFuture.getFuture(requestID);
             // 请求已经返回或者已经被处理过了 不需要超时
             if (future == null || future.isDone()) {
                return;
            }
            if (future.getExecutor() != null) {
                future.getExecutor().execute(() -> notifyTimeout(future));
            } else {
            	// 生成一个带超时错误的Response并返回 
                notifyTimeout(future);
            }
        }

        private void notifyTimeout(DefaultFuture future) {
            // 1. create exception response. 响应包含当时的信息,
            Response timeoutResponse = new Response(future.getId());
            // 2. set timeout status.
            timeoutResponse.setStatus(future.isSent() ? Response.SERVER_TIMEOUT : Response.CLIENT_TIMEOUT);
            timeoutResponse.setErrorMessage(future.getTimeoutMessage(true));
            // 3, handle response. 这一行假如是正常返回Response里面就是正常的响应
            DefaultFuture.received(future.getChannel(), timeoutResponse, true);
        }
    }

所以这边的逻辑是每次发送的时候保存请求的map并设置定时定期检查超时,假如有请求超时就直接给一个超时的异常响应。

3. 两种特殊的超时

3.1 单次请求超时

见上面2.1 超时设置的分析
配置代码

      ReferenceBuilder referenceBuilder = new ReferenceBuilder();
      referenceBuilder.<HelloBOMService>timeout(3000);
      ReferenceConfig<HelloBOMService> referenceConfig = referenceBuilder.build();
      HelloBOMService someClient = referenceConfig.get();
      //单次生效
      RpcContext.getContext().setObjectAttachment(TIMEOUT_KEY,1000);
      someClient.call(someRequest);

3.2 累加减少的超时分析

从上面计算超时的第二个分支进去

  private int calculateTimeout(Invocation invocation, String methodName) {
        Object countdown = RpcContext.getContext().get(TIME_COUNTDOWN_KEY);
        int timeout = DEFAULT_TIMEOUT;
        if (countdown == null) {
        } else {
            // 1. 发现上下文中有个定时器
            TimeoutCountDown timeoutCountDown = (TimeoutCountDown) countdown;
            // 2. 拿到剩余时间
            timeout = (int) timeoutCountDown.timeRemaining(TimeUnit.MILLISECONDS);
            // 3. 将剩余时间传递给服务端  TIMEOUT_ATTACHENT_KEY = "_TO"  这名字略不走心
            invocation.setObjectAttachment(TIMEOUT_ATTACHENT_KEY, timeout);// pass timeout to remote server
        }
        return timeout;
    }

TimeoutCountDown的原理比较简单就是给一个超时和起始时间,然后每次都计算出现在的时间点,还剩多少超时。比如我3面前设置超时为5,我当时拿到的超时就是(5-3)=2秒。
客户端把参数传到服务端之后又做了什么呢?
对应代码org.apache.dubbo.rpc.filter.ContextFilter#invoke

 @Override
   public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
       .....
       RpcContext context = RpcContext.getContext();
       // 1.  这个地方其实拿着的是 客端传递过来的 TIMEOUT_ATTACHENT_KEY = "_TO" 
       long timeout = RpcUtils.getTimeout(invocation, -1);
       if (timeout != -1) {
         // 2. 这边利用客户端传递过来的超时又新建一个超时出来
           context.set(TIME_COUNTDOWN_KEY, TimeoutCountDown.newCountDown(timeout, TimeUnit.MILLISECONDS));
       }
  			.....
       }
   }

考虑一种场景 A->B->C 假如A设置超时5s
但是B收到请求后,处理其他逻辑用了4s 然后开始调用C
这个时候C只要1s的超时就会使得A得不到正常的结果,所以没有必要hold线程到1s以上。
于是优化了性能

3. 超时异常问题排查

我们看下超时的message会给我们何种报错信息

    private String getTimeoutMessage(boolean scan) {
        long nowTimestamp = System.currentTimeMillis();
         // 首先是标志位sent,这个是netty的一个状态表示客户端是否把请求发送出去了。
         // 错误假如是 server-side response timeout 考虑是服务端的业务处理太慢导致 没在约定的时间内返回
         // Sending request timeout in client-side 考虑是客户端的请求没有发出去考虑是客户端的网络问题
        return (sent > 0 ? "Waiting server-side response timeout" : "Sending request timeout in client-side")
                + (scan ? " by scan timer" : "") + ". start time: "
                + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(start))) + ", end time: "
                + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(nowTimestamp))) + ","
                + (sent > 0 ? " client elapsed: " + (sent - start)
                + " ms, server elapsed: " + (nowTimestamp - sent)
                : " elapsed: " + (nowTimestamp - start)) + " ms, timeout: "
                + timeout + " ms, request: " + (logger.isDebugEnabled() ? request : getRequestWithoutData()) + ", channel: " + channel.getLocalAddress()
                + " -> " + channel.getRemoteAddress();
    }

4. 扩展:客户端怎么拿到服务端的超时配置

先省略大部分的注册发现状态。我们从第一次服务端推送服务实例的url到客户端。
相关的类是XXXRegistry extends FailbackRegistry 可能是ZK或者其他的。

// 推送消实例信息会走如下的方法其中consumerUrl 对应当前发现的客户端
// urls 这个场景下是对应的服务端URL。(一般还有配置的URL)
this.doNotify(consumerUrl, listener, urls);  

urls会根据协议进行分类然后传递给对应的listener。
listenerd的实现有如下几个

org.apache.dubbo.registry.integration.RegistryProtocol.OverrideListener 改写客户端url 下发动态配置
org.apache.dubbo.registry.integration.RegistryDirectory 获取服务端的urls 重新生成Invoker的列表
还有其他一些

明显我们需要找RegistryDirectory
调用链有点长直接跳一下
org.apache.dubbo.registry.integration.RegistryDirectory#toInvokers ->mergeUrl

private URL mergeUrl(URL providerUrl) {
 		// 1. query map 为客户端所带有的所有parameter
        providerUrl = ClusterUtils.mergeUrl(providerUrl, queryMap); // Merge the consumer side parameters
		
		// .....
		return url 
       }

org.apache.dubbo.rpc.cluster.support.ClusterUtils#mergeUrl

public static URL mergeUrl(URL remoteUrl, Map<String, String> localMap) {
        Map<String, String> map = new HashMap<String, String>();
        // 1. 获取远端的属性
        Map<String, String> remoteMap = remoteUrl.getParameters();

        if (remoteMap != null && remoteMap.size() > 0) {
            map.putAll(remoteMap);
			// 移除一些不下发的属性, 下面列表的属性服务端不会下发
			map.remove(THREAD_NAME_KEY);
			map.remove(DEFAULT_KEY_PREFIX + THREAD_NAME_KEY);
        }

        if (localMap != null && localMap.size() > 0) {
           //2. 本地属性 复制避免更改
            Map<String, String> copyOfLocalMap = new HashMap<>(localMap);
		  // 3. 特殊的key version 和group DubboInvoker 还需要靠他来实现version =*
            if(map.containsKey(GROUP_KEY)){
                copyOfLocalMap.remove(GROUP_KEY);
            }
            if(map.containsKey(VERSION_KEY)){
                copyOfLocalMap.remove(VERSION_KEY);
            }
			// 删除无用的key
            copyOfLocalMap.remove(RELEASE_KEY);
     		// 4. 重点 客户端后放 最终覆盖了服务端下发的配置
            map.putAll(copyOfLocalMap);

            map.put(REMOTE_APPLICATION_KEY, remoteMap.get(APPLICATION_KEY));
        }
        return remoteUrl.clearParameters().addParameters(map);
    }

结论先putAll服务端参数再putAll客户端参数,最终生效客户端参数。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值