背景
产线上面很多外部接口的调用,希望指定超时时间。按时返回,但是发现竟然踩坑了。
最核心的原因的是 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®ister.ip=172.20.10.2&side=consumer&timeout=12000×tamp=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