Implementing Dubbo Asynchronous Calls with CompletableFuture: Practices and Performance Gains
By refactoring Dubbo RPC interfaces to return CompletableFuture, applying thenApply/thenCombine/thenCompose patterns, isolating a custom business thread pool, and handling errors and tracing, the team achieved up to 50% latency reduction, 25% response‑time improvement, a one‑third server cut and CPU utilization rise, demonstrating substantial performance and cost benefits.
Background
Since Dubbo 2.7.0, all asynchronous programming interfaces are based on CompletableFuture . Asynchronous Dubbo can greatly improve interface performance and reduce blocking between dependent calls. In a cost‑reduction project, the customer‑service robot team applied Dubbo async to several high‑QPS services and observed a 50% performance boost and a one‑third reduction in application servers.
Dubbo Async Implementation
By moving complex business logic from the default Dubbo thread pool (size 200) to a custom business thread pool, the request handling capacity of the Dubbo pool is increased while resource utilization improves.
2.1 Interface Refactor
The original synchronous method Result<RecommendAtConnectRes> getRecommendContent(RecommendAtConnectReq request) is kept for compatibility, and a new asynchronous method returning CompletableFuture<Result<RecommendAtConnectRes>> is added.
public interface RecommendAtConnectApi {
Result<RecommendAtConnectRes> getRecommendContent(RecommendAtConnectReq request);
CompletableFuture<Result<RecommendAtConnectRes>> asyncGetRecommendContent(RecommendAtConnectReq request);
}2.2 Future Usage Patterns
Common patterns include:
Result transformation with thenApply
Combining multiple futures with thenCombine (or allOf for more than two)
Sequential dependency with thenCompose
Example of thenApply :
CompletableFuture<String> cFuture = cAsyncService.asyncSayHello(name);
CompletableFuture<DataDTO> finalFuture = cFuture.thenApply(c -> new DataDTO());
return finalFuture;Example of thenCombine :
CompletableFuture<String> cFuture = cAsyncService.asyncSayHello(name);
CompletableFuture<String> dFuture = dAsyncService.asyncSayHello(name);
CompletableFuture<DataDTO> allFuture = cFuture.thenCombine(dFuture, (c, d) -> new DataDTO());
return allFuture;Example of thenCompose (with optional handling):
CompletableFuture<Optional<RecommendAtConnectDto>> taskEngineFuture = pushGsTaskEngineHandler.asyncPushHandler(connectRequest);
CompletableFuture<Optional<RecommendAtConnectDto>> refundFuture = getNextFuture(taskEngineFuture, connectRequest, unused -> pushLogisticsRefundHandler.asyncPushHandler(connectRequest));
return refundFuture;2.3 CompletableFuture Internals
The core fields are result (the computed value) and stack (a Treiber stack of dependent actions). Methods such as thenApply , thenCombine , and thenCompose create specific Completion objects, push them onto the stack via CAS, and fire them when the result becomes available.
Key source snippets:
// thenApply implementation
private
CompletableFuture
uniApplyStage(Executor e, Function
f) {
if (f == null) throw new NullPointerException();
CompletableFuture
d = new CompletableFuture<>();
if (e != null || !d.uniApply(this, f, null)) {
UniApply
c = new UniApply<>(e, d, this, f);
push(c);
c.tryFire(SYNC);
}
return d;
}3.1 Practical Experience – Scenario Selection
Three robot scenarios were chosen: order detail page, chat “you may ask” page, and input suggestion. They have high QPS, are IO‑intensive, and do not affect core dialogue interfaces, making them ideal for async conversion.
3.2 Best Practices
3.2.1 Dependency Mapping
Before refactoring, draw a dependency graph to identify parallel and serial relationships. Parallel CF nodes can be executed concurrently, while dependent nodes must be chained.
3.2.2 Code Example
public CompletableFuture
getResult(){
// Parallel three chains
CompletableFuture
cf1 = cf1Service.getResult();
CompletableFuture
cf2Combine = getCf2Combine();
CompletableFuture
cf3Combine = getCf3Combine();
// Combine and transform
CompletableFuture
finalFuture = CompletableFuture.allOf(cf1, cf2Combine, cf3Combine);
return finalFuture.thenApply((unused) ->
new CFResponse(cf1.get().getCf1Value() + cf2Combine.get().getCf2CombineValue() + cf3Combine.get().getCf3CombineValue()));
}
private CompletableFuture
getCf2Combine() {
CompletableFuture
cf2 = cf2Service.getResult();
return cf2.thenCompose(cf2Response -> {
CompletableFuture
cf4 = cf4Service.getResult(cf2Response.getCf2Value());
return cf4.thenApply(cf4Response -> new CF2CombineResponse(cf4Response.getCf4Value()));
});
}
private CompletableFuture
getCf3Combine() {
CompletableFuture
cf3 = cf3Service.getResult();
return cf3.thenCompose(cf3Response -> {
CompletableFuture
cf5 = cf5Service.getResult(cf3Response.getCf3Value());
CompletableFuture
cf6 = cf6Service.getResult(cf3Response.getCf3Value());
return CompletableFuture.allOf(cf5, cf6)
.thenCompose(unused -> cf7Service.getResult(cf5.get().getCf5Value(), cf6.get().getCf6Value()));
});
}3.2.3 Thread‑Pool Isolation
Define a custom business thread pool to avoid blocking Dubbo’s internal pool. Example Spring bean:
@Bean(name = "dubboAsyncBizExecutor")
public ThreadPoolTaskExecutor dubboAsyncBizExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(200);
executor.setMaxPoolSize(200);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("dubboAsyncBizExecutor-");
executor.setRejectedExecutionHandler((r, e) -> log.error("dubbo async biz task exceed limit"));
return executor;
}Use it with CompletableFuture.supplyAsync(..., dubboAsyncBizExecutor) .
3.2.4 Exception Handling
Handle errors with exceptionally and unwrap CompletionException via a utility method.
CompletableFuture
asyncPushContent(RecommendAtConnectRequest request) {
CompletableFuture
future = orderSourcePredictHandlerChain.asyncHandleOfPredict(request);
return future.thenApply(body -> {
if (StrUtil.isBlank(body)) return null;
return RecommendAtConnectDtoUtil.getDto(body, ...);
}).exceptionally(err -> {
log.error("async error", ExceptionUtils.extractRealException(err));
return null;
});
}
public class ExceptionUtils {
public static Throwable extractRealException(Throwable t){
if (t instanceof CompletionException || t instanceof ExecutionException) {
if (t.getCause() != null) return t.getCause();
}
return t;
}
}3.2.5 Stability Guarantees
Keep original synchronous methods unchanged.
Introduce async methods alongside them.
Control traffic via AB testing to switch between sync and async.
Provide a one‑click rollback to the original logic.
3.3 Issues Encountered
Trace‑ID loss in async callbacks, requiring monitoring support.
Thread‑pool isolation only available from Dubbo 3.2.0.
thenCompose cannot return null ; wrap with Optional .
Logging must be placed inside callbacks to capture real results.
Monitoring platforms may not include callback latency, making performance diagnosis harder.
Asynchronous Benefits
Load test shows 50% interface latency reduction.
Production RT decreased by ~25% (e.g., input‑suggestion from 173 ms to 119 ms).
Server count reduced by one‑third, CPU utilization increased from ~18% to ~50%.
Conclusion
The practice demonstrates that converting Dubbo RPC to asynchronous CompletableFuture calls removes blocking between services, expands thread‑pool capacity, and improves CPU utilization, enabling higher throughput with fewer hardware resources. This contributes to cost‑reduction and efficiency gains for the organization.
DeWu Technology
A platform for sharing and discussing tech knowledge, guiding you toward the cloud of technology.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.