背景

产线上面很多外部接口的调用,希望指定超时时间。按时返回,但是发现竟然踩坑了。

最核心的原因的是 dubbo 的底层 bug,但是最大的问题还是在于测试用例覆盖不够完全。

我们来简单复现一下这个问题

基础版本

maven 依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>dubbo</artifactId>
    <version>2.5.7</version>
</dependency>

接口定义

最简单那的实现即可

api

package org.example.api;

public interface HelloService {

    String sayHello(String name);

}

provider

public class HelloServiceImpl implements HelloService {

    @Override
    public String sayHello(String name) {
        System.out.println("Sleep start...");
        try {
            // 模拟耗时
            TimeUnit.SECONDS.sleep(3);
            System.out.println("Sleep end...");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        return "Hello " + name + " from Dubbo Provider";
    }

}

服务端启动,简单起见,不依赖 zk。直连验证

package org.example;

import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ProtocolConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.config.ServiceConfig;
import org.example.api.HelloService;
import org.example.provider.HelloServiceImpl;

public class ProviderApp {

    public static void main(String[] args) throws Exception {
        ServiceConfig<HelloService> service = new ServiceConfig<>();
        service.setApplication(new ApplicationConfig("dubbo-provider"));
        service.setProtocol(new ProtocolConfig("dubbo", 20880));
        service.setInterface(HelloService.class);
        service.setRef(new HelloServiceImpl());
        // 指定无注册中心
        service.setRegistry(new RegistryConfig("N/A"));

        // 暴露服务
        service.export();

        System.out.println("Provider started...");
        System.in.read(); // 阻塞
    }

}

consumer

package org.example.consumer;

import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ReferenceConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import org.example.api.HelloService;

public class ConsumerApp {

    public static void main(String[] args) {
        ReferenceConfig<HelloService> reference = new ReferenceConfig<>();
        reference.setApplication(new ApplicationConfig("dubbo-consumer"));
        reference.setInterface(HelloService.class);
        reference.setUrl("dubbo://127.0.0.1:20880"); // 点对点直连
        // 指定无注册中心
        reference.setRegistry(new RegistryConfig("N/A"));
        // 超时12s
        reference.setTimeout(12000);

        HelloService helloService = reference.get();
        System.out.println(helloService.sayHello("World"));
    }

}

启动测试

我们启动 provider

信息:  [DUBBO] Start NettyServer bind /0.0.0.0:20880, export /172.20.10.2:20880, dubbo version: 2.5.7, current host: 127.0.0.1
Provider started...

启动 consumer,整长请求结束。

信息:  [DUBBO] Refer dubbo service org.example.api.HelloService from url dubbo://127.0.0.1:20880/org.example.api.HelloService?application=dubbo-consumer&dubbo=2.5.7&interface=org.example.api.HelloService&methods=sayHello&pid=13236&register.ip=172.20.10.2&side=consumer&timeout=12000&timestamp=1758881973712, dubbo version: 2.5.7, current host: 172.20.10.2
Hello World from Dubbo Provider
九月 26, 2025 6:19:38 下午 com.alibaba.dubbo.config.AbstractConfig info

此时服务端:

Sleep start...
Sleep end...

异步调用

consumer 调整

如果我们不想同步等待这个接口太久,而是改为异步调用获取结果。

dubbo 当然是支持的。

核心点思路:

1) 你还是调用 helloService.sayHello(“World”),Dubbo 会立即返回 null(因为是异步)。

2) 结果会放到 RpcContext 里,可以通过 Future<T> 拿到。

异步的好处是,如果我们同时调用接口多次,可以让其并发执行。

最后通过 future 获取执行结果即可,从而大大降低耗时。

异步调用的例子

package org.example.consumer;

import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ReferenceConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.rpc.RpcContext;
import org.example.api.HelloService;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class ConsumerFutureApp {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ReferenceConfig<HelloService> reference = new ReferenceConfig<>();
        reference.setApplication(new ApplicationConfig("dubbo-consumer"));
        reference.setInterface(HelloService.class);
        reference.setUrl("dubbo://127.0.0.1:20880"); // 点对点直连
        // 指定无注册中心
        reference.setRegistry(new RegistryConfig("N/A"));
        // 超时12s
        reference.setTimeout(12000);

        // 配置异步调用
        reference.setAsync(true);

        HelloService helloService = reference.get();

        // 发起异步调用(立即返回 null)
        String immediate = helloService.sayHello("World");
        System.out.println(TimeUtil.getTimeStr()+" 调用立即返回: " + immediate);

        // 从 RpcContext 里获取 Future
        Future<String> future = RpcContext.getContext().getFuture();

        // 等待结果
        String result = future.get();
        System.out.println(TimeUtil.getTimeStr()+" 异步结果: " + result);
    }

}

执行日志:

2025-09-26 18:24:59.456 调用立即返回: null
2025-09-26 18:25:02.468 异步结果: Hello World from Dubbo Provider

超时中断

业务上,这种接口可能是不稳定的。

于是会有产品要求,当耗时超过指定时间,我们就丢弃这个结果,不做等待。

很自然的,我们可以通过配置中心加一个耗时配置,针对不同的接口,配置不同的超时。

这里简单起见,我们演示一下指定为 10s 超时丢弃结果。

修改方式

我们只修改 future.get 的耗时,其他不变。

// 等待结果(指定超时)
String result = future.get(10, TimeUnit.SECONDS);
System.out.println(TimeUtil.getTimeStr()+" 异步结果: " + result);

我们的 provider 耗时为 5s。

大家觉得结果是什么?

实际测试

但是结果是超时,为什么呢?我们不是指定 future 10S 才超时吗?

2025-09-26 18:30:23.642 调用立即返回: null
Exception in thread "main" java.util.concurrent.TimeoutException: com.alibaba.dubbo.remoting.TimeoutException: Waiting server-side response timeout. start time: 2025-09-26 18:30:23.604, end time: 2025-09-26 18:30:24.645, client elapsed: 37 ms, server elapsed: 1004 ms, timeout: 12000 ms, request: Request [id=0, version=2.0.0, twoway=true, event=false, broken=false, data=RpcInvocation [methodName=sayHello, parameterTypes=[class java.lang.String], arguments=[World], attachments={async=true, path=org.example.api.HelloService, id=0, interface=org.example.api.HelloService, version=0.0.0, timeout=12000}]], channel: /172.20.10.2:37434 -> /172.20.10.2:20880
com.alibaba.dubbo.remoting.TimeoutException: Waiting server-side response timeout. start time: 2025-09-26 18:30:23.604, end time: 2025-09-26 18:30:24.645, client elapsed: 37 ms, server elapsed: 1004 ms, timeout: 12000 ms, request: Request [id=0, version=2.0.0, twoway=true, event=false, broken=false, data=RpcInvocation [methodName=sayHello, parameterTypes=[class java.lang.String], arguments=[World], attachments={async=true, path=org.example.api.HelloService, id=0, interface=org.example.api.HelloService, version=0.0.0, timeout=12000}]], channel: /172.20.10.2:37434 -> /172.20.10.2:20880
	at com.alibaba.dubbo.remoting.exchange.support.DefaultFuture.get(DefaultFuture.java:136)
	at com.alibaba.dubbo.rpc.protocol.dubbo.FutureAdapter.get(FutureAdapter.java:73)
	at org.example.consumer.ConsumerFutureTimeoutApp.main(ConsumerFutureTimeoutApp.java:39)

	at com.alibaba.dubbo.rpc.protocol.dubbo.FutureAdapter.get(FutureAdapter.java:75)
	at org.example.consumer.ConsumerFutureTimeoutApp.main(ConsumerFutureTimeoutApp.java:39)

bug 跟踪

实际上会发现问题出在

at com.alibaba.dubbo.rpc.protocol.dubbo.FutureAdapter.get(FutureAdapter.java:73)

看一下这个类的源码

    public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
        int timeoutInMillis = (int)unit.convert(timeout, TimeUnit.MILLISECONDS);

        try {
            return ((Result)this.future.get(timeoutInMillis)).recreate();
        } catch (com.alibaba.dubbo.remoting.TimeoutException var6) {
            com.alibaba.dubbo.remoting.TimeoutException e = var6;
            throw new TimeoutException(StringUtils.toString(e));
        } catch (RemotingException var7) {
            RemotingException e = var7;
            throw new ExecutionException(e.getMessage(), e);
        } catch (Throwable var8) {
            Throwable e = var8;
            throw new RpcException(e);
        }
    }

可以看到将我们指定的时间转换为 mills,然后判断超时,看起来没问题对吧?

实际上这里写反了,会把我们的结果转换为 0。

正确写法应该是:

int timeoutInMillis = (int)TimeUnit.MILLISECONDS.convert(timeout, unit);

才是将数据转换为毫秒。

临时修正

当然,代码封板之后不太好解决。

可以将错就错,临时把配置改大一些。也就是要改为对应毫秒的 1000 倍。

String result = future.get(10000000, TimeUnit.SECONDS);

此时调用正常

2025-09-26 18:42:19.100 调用立即返回: null
2025-09-26 18:42:22.103 异步结果: Hello World from Dubbo Provider

如果我们改为 2s 呢

String result = future.get(2000000, TimeUnit.SECONDS);

此时依然会超时

2025-09-26 18:43:10.585 调用立即返回: null
Exception in thread "main" java.util.concurrent.TimeoutException: com.alibaba.dubbo.remoting.TimeoutException: Waiting server-side response timeout. start time: 2025-09-26 18:43:10.546, end time: 2025-09-26 18:43:12.600, client elapsed: 38 ms, server elapsed: 2016 ms, timeout: 12000 ms, request: Request [id=0, version=2.0.0, twoway=true, event=false, broken=false, data=RpcInvocation [methodName=sayHello, parameterTypes=[class java.lang.String], arguments=[World], attachments={async=true, path=org.example.api.HelloService, id=0, interface=org.example.api.HelloService, version=0.0.0, timeout=12000}]], channel: /172.20.10.2:3131 -> /172.20.10.2:20880
com.alibaba.dubbo.remoting.TimeoutException: Waiting server-side response timeout. start time: 2025-09-26 18:43:10.546, end time: 2025-09-26 18:43:12.600, client elapsed: 38 ms, server elapsed: 2016 ms, timeout: 12000 ms, request: Request [id=0, version=2.0.0, twoway=true, event=false, broken=false, data=RpcInvocation [methodName=sayHello, parameterTypes=[class java.lang.String], arguments=[World], attachments={async=true, path=org.example.api.HelloService, id=0, interface=org.example.api.HelloService, version=0.0.0, timeout=12000}]], channel: /172.20.10.2:3131 -> /172.20.10.2:20880
	at com.alibaba.dubbo.remoting.exchange.support.DefaultFuture.get(DefaultFuture.java:136)
	at com.alibaba.dubbo.rpc.protocol.dubbo.FutureAdapter.get(FutureAdapter.java:73)
	at org.example.consumer.ConsumerFutureTimeoutApp.main(ConsumerFutureTimeoutApp.java:40)

	at com.alibaba.dubbo.rpc.protocol.dubbo.FutureAdapter.get(FutureAdapter.java:75)
	at org.example.consumer.ConsumerFutureTimeoutApp.main(ConsumerFutureTimeoutApp.java:40)

测试用例覆盖问题

最可怕的点在于,测试环境的外部渠道全部是挡板 mock 掉了,1S 内的请求实际上不认为是超时。

也让测试误以为配置是正确的。

当实际发布到产线,真实的调用超过1S全部失败。

让发布者以为有问题,所以临时回顾。

小结

这里的反思点有两处:

1)对于看起来很自然的地方也不能信任。一定要充分验证。

2)测试用例要尽可能的贴合真实,不然上生产代价更大。

dubbo 接口动态设置超时时间

filter

package org.example.consumer.filter;

import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.URL;
import com.alibaba.dubbo.common.extension.Activate;
import com.alibaba.dubbo.rpc.*;
import com.alibaba.dubbo.rpc.support.RpcUtils;

import java.lang.reflect.Method;
import java.util.Map;

@Activate(group = {"consumer"}, order = -5000)
public class DynamicTimeoutUrlFilter implements Filter {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        // 举例:根据方法名动态设置不同超时时间
        if ("sayHello".equals(invocation.getMethodName())) {
            setTimeout(invoker, invocation, 1000);
        } else {
            setTimeout(invoker, invocation, 5000);
        }

        return invoker.invoke(invocation);
    }

    private void setTimeout(Invoker<?> invoker, Invocation invocation, int timeout)  {
        try {
            URL url = invoker.getUrl();

            Method method = URL.class.getDeclaredMethod("getNumbers");
            method.setAccessible(true);

            // 拿到原来的配置
            Map<String, Number> map = (Map<String, Number>) method.invoke(url);
            String methodName = RpcUtils.getMethodName(invocation);
            map.put(methodName+"."+Constants.TIMEOUT_KEY, timeout);
        } catch (Exception e) {
            e.printStackTrace();
        } 
    }
}

这里的设置超时写的很丑,应该可以有更加简单的方法。

但是下面的方式测试了没有用:

package org.example.consumer.filter;

import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.extension.Activate;
import com.alibaba.dubbo.rpc.*;

@Activate(group = {"consumer"}, order = -5000)
public class DynamicTimeoutFilter implements Filter {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        // 举例:根据方法名动态设置不同超时时间
        if ("sayHello".equals(invocation.getMethodName())) {
            setTimeout(invocation, "1000");
        } else {
            setTimeout(invocation, "5000");
        }

        return invoker.invoke(invocation);
    }

    private void setTimeout(Invocation invocation, String timeout) {
        // 1) 设置到 Invocation(大多数实现都会读这个)
        invocation.getAttachments().put(Constants.TIMEOUT_KEY, timeout);

        // 2) 同步设置到 RpcContext(有些路径会先读 RpcContext)
        RpcContext.getContext().setAttachment(Constants.TIMEOUT_KEY, timeout);
    }

}

使用

package org.example.consumer;

import com.alibaba.dubbo.common.extension.ExtensionLoader;
import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ReferenceConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.rpc.Filter;
import com.alibaba.dubbo.rpc.RpcContext;
import org.example.api.HelloService;
import org.example.consumer.filter.DynamicTimeoutFilter;
import org.example.consumer.filter.DynamicTimeoutUrlFilter;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class ConsumerFutureFilterApp {

    public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
        ReferenceConfig<HelloService> reference = new ReferenceConfig<>();
        reference.setApplication(new ApplicationConfig("dubbo-consumer"));
        reference.setInterface(HelloService.class);
        reference.setUrl("dubbo://127.0.0.1:20880"); // 点对点直连
        // 指定无注册中心
        reference.setRegistry(new RegistryConfig("N/A"));
        // 超时12s
        reference.setTimeout(12000);

        // 配置异步调用
        reference.setAsync(true);

        // 手动注册 Filter
        ExtensionLoader<Filter> loader = ExtensionLoader.getExtensionLoader(Filter.class);
        loader.addExtension("dynamicTimeout", DynamicTimeoutUrlFilter.class);
        reference.setFilter("dynamicTimeout");

        HelloService helloService = reference.get();

        // 发起异步调用(立即返回 null)
        String immediate = helloService.sayHello("World");
        System.out.println(TimeUtil.getTimeStr()+" 调用立即返回: " + immediate);

        // 从 RpcContext 里获取 Future
        Future<String> future = RpcContext.getContext().getFuture();

        // 等待结果(指定超时)
        String result = future.get();
        System.out.println(TimeUtil.getTimeStr()+" 异步结果: " + result);
    }

}

测试

2025-09-26 19:15:02.411 调用立即返回: null
Exception in thread "main" java.util.concurrent.ExecutionException: Waiting server-side response timeout by scan timer. start time: 2025-09-26 19:15:02.372, end time: 2025-09-26 19:15:03.388, client elapsed: 38 ms, server elapsed: 978 ms, timeout: 1000 ms, request: Request [id=0, version=2.0.0, twoway=true, event=false, broken=false, data=RpcInvocation [methodName=sayHello, parameterTypes=[class java.lang.String], arguments=[World], attachments={async=true, path=org.example.api.HelloService, id=0, interface=org.example.api.HelloService, version=0.0.0, timeout=12000}]], channel: /172.20.10.2:56625 -> /172.20.10.2:20880
	at com.alibaba.dubbo.rpc.protocol.dubbo.FutureAdapter.get(FutureAdapter.java:63)
	at org.example.consumer.ConsumerFutureFilterApp.main(ConsumerFutureFilterApp.java:48)
Caused by: com.alibaba.dubbo.remoting.TimeoutException: Waiting server-side response timeout by scan timer. start time: 2025-09-26 19:15:02.372, end time: 2025-09-26 19:15:03.388, client elapsed: 38 ms, server elapsed: 978 ms, timeout: 1000 ms, request: Request [id=0, version=2.0.0, twoway=true, event=false, broken=false, data=RpcInvocation [methodName=sayHello, parameterTypes=[class java.lang.String], arguments=[World], attachments={async=true, path=org.example.api.HelloService, id=0, interface=org.example.api.HelloService, version=0.0.0, timeout=12000}]], channel: /172.20.10.2:56625 -> /172.20.10.2:20880
	at com.alibaba.dubbo.remoting.exchange.support.DefaultFuture.returnFromResponse(DefaultFuture.java:218)
	at com.alibaba.dubbo.remoting.exchange.support.DefaultFuture.get(DefaultFuture.java:139)
	at com.alibaba.dubbo.remoting.exchange.support.DefaultFuture.get(DefaultFuture.java:113)
	at com.alibaba.dubbo.rpc.protocol.dubbo.FutureAdapter.get(FutureAdapter.java:61)
	... 1 more
九月 26, 2025 7:15:03 下午 com.alibaba.dubbo.config.AbstractConfig info
信息:  [DUBBO] Run shutdown hook now., dubbo version: 2.5.7, current host: 172.20.10.2

小结

这种方式直接指定超时,比 future 中指定超时的好处在于不会因为 future.get 导致的累加问题。

反思

把所有的问题,变成机遇,并且追求卓越。

比如发现页面比较慢,那么就从多个维度优化:

1)技术上,添加超时。提前热门跑批缓存预热+智能化推荐+用户缓存。

2)业务上,可以和下游对接者沟通改进

把一个本来是平平无奇的问题,延伸到一个技术+业务+思考的挑战,并且以此追求体验的提升+技术创新的奖项等等。

当然这背后需要业务埋点+指标数据支撑+对业务的深入理解+对技术应用的思考

视野的宽广:

从代码=》jvm==>部署容器=》服务器==》业务侧===》用户侧

不要只拘泥于代码这一个小点,比如 GC 导致性能下降,升降调 jdk21 性能也许会好很多。

很多性能问题,都在技术之外。

永远要多想2步。

北海啊,在那之前,要多想。

参考资料

https://blog.csdn.net/yuanshangshenghuo/article/details/107741892