Spring Boot 3 + Apache Dubbo 3: Triple Protocol, Service Governance & High-Performance RPC in Practice

This article details migrating from Spring Cloud/OpenFeign to Apache Dubbo 3 with Spring Boot 3, covering Triple protocol over HTTP/2, binary serialization, service governance (timeouts, retries, load balancing), streaming, observability, and production pitfalls with benchmark comparisons showing 3-5x throughput gains.

Xiaolin Talks Programming
Xiaolin Talks Programming
Xiaolin Talks Programming
Spring Boot 3 + Apache Dubbo 3: Triple Protocol, Service Governance & High-Performance RPC in Practice

1. Why Move Away from REST/OpenFeign?

Spring Cloud with OpenFeign is the default starter for Java microservices, but as systems grow from dozens to hundreds of services, several problems emerge that cannot be solved by simply adding machines.

Interface contracts drift easily. Feign clients are defined per interface method; the same downstream service may be redefined in multiple upstream XxxClient interfaces. Field changes rely on verbal agreements; renaming a DTO requires hunting compilation errors across repositories. Worse, some changes pass compilation but fail at runtime during deserialization.

Protocol and serialization overhead is significant. REST defaults to JSON over HTTP/1.1. Text serialization is inherently larger than binary, and HTTP/1.1 suffers from head-of-line blocking, inefficient connection reuse, and Keep-Alive management. At QPS above 10,000, these become real costs. A single internal call taking 5 ms with 2 ms spent on serialization and network stack is common.

Governance capabilities are scattered. Timeouts, retries, circuit breaking, and load balancing are spread across LoadBalancer, Resilience4j, Sentinel, and Gateway. Configuration hierarchies are inconsistent, and cross-language support is even harder. Implementing canary releases by tag or zone-aware routing often requires custom filters.

Streaming capability is missing. AI inference, log pushing, and real-time risk control need server push or bidirectional streaming. REST can only patch this with SSE/WebSocket, which is disconnected from the RPC call system.

Migrating to Dubbo 3 brings direct benefits: binary serialization plus HTTP/2 multiplexing yields 3-10x single-node throughput improvement depending on payload shape; interface-as-contract with the api module as an anti-corruption layer; governance consolidated into the framework layer with timeout, retries, loadbalance, cluster declared via config or annotations; Triple protocol compatible with both gRPC and Dubbo semantics, enabling cross-language and streaming; application-level service discovery reduces registry data from "interfaces × instances" to "applications × instances", lowering Nacos pressure by an order of magnitude in large clusters.

2. Technology Selection: Dubbo 3 Architecture, Triple Protocol & Registry

2.1 Dubbo 3 Layered Architecture

Dubbo 3 retains classic layered design but with two key kernel refactors.

┌─────────────────────────────────────────────┐
│  Service Layer: @DubboService / @DubboReference │
├─────────────────────────────────────────────┤
│  Config Layer: Config Center, Dynamic Governance Rules │
├─────────────────────────────────────────────┤
│  Proxy Layer: Dynamic Proxy (JDK / ByteBuddy) │
├─────────────────────────────────────────────┤
│  Registry Layer: Service Discovery (Interface / Application Level) │
├─────────────────────────────────────────────┤
│  Cluster Layer: Routing, Load Balance, Fault Tolerance, Mock │
├─────────────────────────────────────────────┤
│  Protocol Layer: tri / dubbo / rest │
├─────────────────────────────────────────────┤
│  Exchange / Transport / Serialize │
│  (HTTP/2, Netty, Hessian2/Protobuf/JSON) │
└─────────────────────────────────────────────┘

Refactor 1: Application-Level Service Discovery. Dubbo 2.7 and earlier used interface-level registration: a provider exposing 20 interfaces generated 20 metadata entries, further multiplied by group and version. Dubbo 3 defaults to register-mode=all then migrates to instance mode: registry stores only "application → instance IP:Port", while interface-to-application mapping moves to the metadata center. Data volume drops dramatically.

Refactor 2: Triple Protocol. This is the protagonist of Dubbo 3's protocol layer and the focus of this article.

2.2 Triple Protocol: One Protocol, Two Semantics

Triple is Dubbo 3's proprietary protocol built on HTTP/2, designed to be fully gRPC-compatible while retaining Dubbo's native service governance semantics.

Compared to the old Dubbo protocol (TCP private, no multiplexing, no streaming) and REST/JSON over HTTP/1.1 (text serialization, gateway-friendly but governance via external components), Triple runs on HTTP/2, supports three streaming modes, allows Protobuf/Hessian2/Fastjson2 serialization, and is gateway/Service Mesh friendly. Governance capabilities remain the full Dubbo suite.

Triple supports three interface definition styles:

Protobuf IDL : protoc generates stubs, standard cross-language approach.

Java Interface (non-Protobuf) : Reuses legacy Dubbo interfaces, default Hessian2 serialization, lowest migration cost.

Java Interface + Streaming Methods : Uses StreamObserver to declare streaming semantics.

For brownfield migration, start with Java Interface style to move interfaces as-is, only changing the protocol. Once stable, switch performance-critical core paths to Protobuf IDL.

2.3 Registry: Nacos vs ZooKeeper

Both Nacos 2.x and ZooKeeper 3.8 work, but differences are significant.

Nacos supports CP+AP switching; ZooKeeper is CP (ZAB). Push model: Nacos 2.x uses long-polling + UDP push; ZooKeeper uses one-time Watch triggers. Health checks: Nacos uses heartbeat + active probing; ZooKeeper uses session timeout. Metadata center built into Nacos; ZooKeeper needs separate deployment. Config center built into Nacos; ZooKeeper has none. At scale, Nacos handles 10k+ instances stably; ZooKeeper suffers Watch storms with many instances. Operational cost: Nacos medium, ZooKeeper medium-high.

Conclusion: Choose Nacos for new projects. Three reasons: Dubbo 3's metadata center reuses Nacos directly; Nacos 2.x long-connection push is smoother during frequent instance churn; unified config and registry shortens dynamic governance push path. ZooKeeper suits teams already using it with mature ops.

3. Engineering Integration: Dependencies, Versions & Layering

3.1 Version Compatibility Matrix (Critical)

Spring Boot 3 imposes two hard constraints: JDK 17+ and Jakarta EE 9+ (i.e., javax.* → jakarta.*). Dubbo officially supports Spring Boot 3 from 3.2.0; current stable recommendation is 3.3.x.

JDK 17 or 21 (both LTS). Spring Boot 3.2.x/3.3.x. Dubbo 3.3.x. Nacos Server 2.3.x+, Nacos Client 2.3.x (match client/server versions). dubbo-registry-nacos same version as Dubbo.

Maven dependencies (key excerpts):

<properties>
  <java.version>17</java.version>
  <dubbo.version>3.3.4</dubbo.version>
  <nacos-client.version>2.3.2</nacos-client.version>
</properties>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-bom</artifactId>
      <version>${dubbo.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
  </dependency>
  <!-- Registry -->
  <dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-registry-nacos</artifactId>
  </dependency>
  <!-- Metadata Center (required for application-level discovery) -->
  <dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-metadata-report-nacos</artifactId>
  </dependency>
  <!-- Triple protocol extra (recommended for non-Protobuf mode) -->
  <dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-serialization-hessian2</artifactId>
  </dependency>
</dependencies>

Note: If spring-cloud-starter-alibaba-nacos-discovery is also present, align Nacos Client version to avoid NacosException: Client not connected.

3.2 Interface Layering & API Module Governance

Common structural mistake: mixing api and provider in one module, forcing consumers to depend on all provider implementation dependencies. Recommended three-layer structure:

user-service-api          # Pure interfaces + DTOs, zero business deps
  └── UserQueryService.java
  └── dto/UserDTO.java
user-service-provider     # Implementation + Spring Boot bootstrap
  └── UserQueryServiceImpl.java
user-service-consumer     # Caller

The api module may only depend on utilities like lombok, jakarta.validation — absolutely no Spring Context, MyBatis, database drivers . This constraint is the foundation of the anti-corruption layer; once broken, refactoring becomes irreversible.

DTO design rules:

Must implement Serializable and declare explicit serialVersionUID . Hessian2 tolerates field add/remove but major class structure changes still break.

Do not use Map&lt;String, Object&gt; for parameters. Fields scatter, no type constraints, governance becomes difficult later.

Do not pass Entities across services , especially JPA/Hibernate objects with lazy loading — serialization will explode.

3.3 Provider Configuration

server:
  port: 8081

dubbo:
  application:
    name: user-service
  # Application-level discovery: write interface→application mapping to metadata center
  register-mode: instance
  qos-enable: true
  qos-port: 22222
  # Graceful shutdown wait time
  shutdown-wait: 10000
  protocol:
    name: tri
    port: 50051
    # Triple non-Protobuf serialization
    serialization: hessian2
  # Thread pool
  threadpool:
    fixed
    threads: 200
    queues: 0
  registry:
    address: nacos://127.0.0.1:8848
    parameters:
      namespace: prod
      group: DEFAULT_GROUP
  metadata-report:
    address: nacos://127.0.0.1:8848
    parameters:
      namespace: prod

Service implementation:

@DubboService(
  version = "1.0.0",
  group = "default",
  timeout = 3000,
  retries = 0
)
public class UserQueryServiceImpl implements UserQueryService {
  @Override
  public UserDTO getById(Long id) {
    if (id == null || id <= 0) {
      throw new IllegalArgumentException("invalid id");
    }
    return UserDTO.builder()
      .id(id)
      .name("user-" + id)
      .build();
  }
}

Load balancing is usually configured on consumer side; provider-side config is easily overridden, so don't worry about it here.

3.4 Consumer Configuration

dubbo:
  application:
    name: order-service
  register-mode: instance
  registry:
    address: nacos://127.0.0.1:8848
    parameters:
      namespace: prod
  consumer:
    timeout: 2000
    retries: 1
    check: false  # Don't verify provider existence at startup
    lazy: true    # Lazy load, faster startup
    validation: true
@DubboReference(
  version = "1.0.0",
  group = "default",
  timeout = 1000,
  retries = 1,
  loadbalance = "random",
  cluster = "failover"
)
private UserQueryService userQueryService;
check=false

helps during consumer startup to avoid failure if downstream isn't ready. However, for production, enable it to surface issues at startup rather than on first request.

4. Core Capabilities: Invocation Models & Streaming

4.1 Synchronous Invocation

Default synchronous, blocking current thread until return or timeout. Simple but unsuitable for slow downstream under high concurrency.

4.2 Asynchronous Invocation

Dubbo 3 recommends CompletableFuture over legacy RpcContext.getFuture():

// Provider interface declares CompletableFuture return
public interface UserQueryService {
  CompletableFuture<UserDTO> getByIdAsync(Long id);
}

// Consumer
CompletableFuture<UserDTO> future = userQueryService.getByIdAsync(1L);
future.whenComplete((user, ex) -> {
  if (ex != null) {
    log.error("async call failed", ex);
  } else {
    log.info("got user: {}", user);
  }
});

Can also use async=true on consumer to turn synchronous interface into async:

@DubboReference(async = true)
private UserQueryService userQueryService;

// Call returns null immediately, fetch Future from Context
userQueryService.getById(1L);
CompletableFuture<Object> f = RpcContext.getServiceContext().getCompletableFuture();

Note: In Dubbo 3, RpcContext.getContext() is deprecated; use RpcContext.getServiceContext() (server context) or RpcContext.getClientAttachment() / getServerAttachment() (attachments). This is a frequent upgrade pitfall.

4.3 Generic Invocation

Generic invocation lets consumers call without interface JAR, suitable for gateways, test platforms, open platforms:

@DubboReference(
  interfaceName = "com.example.UserQueryService",
  version = "1.0.0",
  generic = "true"
)
private GenericService genericService;

public Object invoke(String method, String[] types, Object[] args) {
  return genericService.$invoke(method, types, args);
}

Dubbo 3.2+ recommends new GenericService API with protobuf generic support ( generic = "protobuf"). Generic invocation performance is 20-40% lower than strong typing; only use for edge scenarios, never in core paths.

4.4 Streaming Invocation

Triple's killer feature. Declared via Java Interface:

public interface StreamService {
  // Server streaming: one request, multiple responses
  void serverStream(String req, StreamObserver<String> responseObserver);
  // Client streaming: multiple requests, one response
  StreamObserver<String> clientStream(StreamObserver<String> responseObserver);
  // Bidirectional streaming
  StreamObserver<String> biStream(StreamObserver<String> responseObserver);
}

Server streaming implementation:

@DubboService
public class StreamServiceImpl implements StreamService {
  @Override
  public void serverStream(String req, StreamObserver<String> observer) {
    for (int i = 0; i < 10; i++) {
      observer.onNext(req + " -> chunk " + i);
    }
    observer.onCompleted();
  }
  @Override
  public StreamObserver<String> biStream(StreamObserver<String> response) {
    return new StreamObserver<>() {
      @Override
      public void onNext(String data) {
        response.onNext("echo: " + data);
      }
      @Override
      public void onError(Throwable t) {
        response.onError(t);
      }
      @Override
      public void onCompleted() {
        response.onCompleted();
      }
    };
  }
}

Backpressure is critical for streaming. When consumer cannot keep up, it must signal via StreamObserver flow control to slow down provider, otherwise OOM risk. Triple's HTTP/2 flow control provides natural support, but application layer must avoid piling unbounded tasks in onNext.

4.5 Timeouts & Retries

Priority high to low: Method-level > Interface-level > Global config > Default (1000ms) .

@DubboService(
  version = "1.0.0",
  methods = {
    @Method(name = "getById", timeout = 500, retries = 0),
    @Method(name = "listByIds", timeout = 2000, retries = 1)
  }
)

Three retry pitfalls:

Never retry write operations. retries=0 is default recommendation for write interfaces unless idempotency guaranteed.

Retries amplify traffic. A→B→C each retrying 2 times means C may face 8x traffic — cascade failure source.

failover defaults to retry on 3 different providers. If only one instance exists, retries hammer the same machine. Combine with cluster=available for safety.

5. Service Governance: Load Balancing, Fault Tolerance & Canary

5.1 Load Balancing

random

(weighted random) is default, general-purpose. roundrobin (weighted round-robin) for similar-performance instances. leastactive (least active calls) better for slow requests with high latency variance. consistenthash for session stickiness, cache affinity. shortestresponse (new in 3.3) picks by shortest response time, good for tail-latency sensitivity. p2c (power of two choices) is an extension, recommended for large clusters.

Production selection by interface semantics: stateless queries use random or p2c; local-cache-dependent use consistenthash; long-running tasks use leastactive.

5.2 Cluster Fault Tolerance

failover

(default) — failover to another node, risky for writes. failfast — fail immediately, for non-idempotent writes. failsafe — return empty result, for logging/metrics. failback — background scheduled retry, for async notifications. forking — parallel call N nodes, take first success, high resource cost. broadcast — call all providers, for config refresh. available — only call available instances, works with circuit breaking.

5.3 Circuit Breaking & Rate Limiting

Dubbo 3 has basic traffic protection, but production-grade circuit breaking/rate limiting still recommends Sentinel. Integration:

<dependency>
  <groupId>com.alibaba.csp</groupId>
  <artifactId>sentinel-apache-dubbo3-adapter</artifactId>
  <version>1.8.8</version>
</dependency>
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

After integration, each Dubbo service method becomes a Sentinel resource named like com.example.UserQueryService:1.0.0:getById(java.lang.Long). Configure flow control, circuit breaking, hot-spot in Sentinel console:

Flow control : QPS threshold, thread count, associate flow.

Circuit breaking : slow call ratio, exception ratio, exception count.

Hot-spot : rate limit by parameter value.

@PostConstruct
public void initFlowRules() {
  List<FlowRule> rules = new ArrayList<>();
  FlowRule rule = new FlowRule();
  rule.setResource("com.example.UserQueryService:1.0.0:getById(java.lang.Long)");
  rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
  rule.setCount(500);
  rules.add(rule);
  FlowRuleManager.loadRules(rules);
}

Dubbo 3.3 includes dubbo-governance and dubbo-tolerance (e.g., adaptive circuit breaking) but maturity lags Sentinel; use as supplement, not replacement.

5.4 Tag Routing & Canary

Tag routing is Dubbo 3's standard for full-chain canary.

Provider side tagging:

dubbo:
  provider:
    tag: gray

Consumer side specifying:

RpcContext.getClientAttachment().setAttachment(Constants.TAG_KEY, "gray");
// Or static config
@DubboReference(parameters = {"tag", "gray"})

Better: dynamic push via config center:

# Nacos DataId: dubbo-governance-tagrouter
force: false
enabled: true
key: order-service
tags:
  - name: gray
    addresses:
      - 10.0.0.11:50051
  - name: ""
    addresses:
      - 10.0.0.12:50051
force: false

means fallback to normal instances if gray tag finds no match, avoiding traffic blackhole.

Condition routing for stronger rules like zone affinity:

# Nacos DataId: dubbo-governance-conditionrouter
- from:
  - host=10.0.0.*
  to:
  - host=10.0.1.*
  priority: 1
  enabled: true
  force: false

5.5 Graceful Start/Stop

High accident zone during instance shutdown. Registry hasn't notified all consumers yet, traffic still arrives, resulting in No provider available.

Complete graceful shutdown chain:

Receive stop signal (SIGTERM / K8s preStop).

Deregister from registry first, so consumers stop discovering.

Reject new requests: QoS command offline or set dubbo.registry.register=false.

Wait for in-flight requests: shutdown-wait (default 10s).

Close thread pools and connections, process exits.

K8s config example:

spec:
  template:
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: user-service
        lifecycle:
          preStop:
            exec:
              command: ["sh","-c","sleep 10"]

The

preStop
sleep 10

buffers the chain: first let Endpoints be removed from Service, then enter Dubbo deregistration. Spring Boot side must ensure spring.lifecycle.timeout-per-shutdown-phase is long enough:

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s
server:
  shutdown: graceful

Startup also needs anti-flapping. Dubbo 3 provides delayed exposure ( dubbo.provider.delay) and service pre-check. K8s should configure readinessProbe pointing to health endpoint, only allowing traffic after Dubbo service is fully exposed:

readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8081
  initialDelaySeconds: 15
  periodSeconds: 5

6. Observability: Metrics, Tracing & Admin

6.1 Metrics

Dubbo 3 has built-in Metrics abstraction with Micrometer bridge ( dubbo-metrics-micrometer) and direct Prometheus output ( dubbo-metrics-prometheus).

<dependency>
  <groupId>org.apache.dubbo</groupId>
  <artifactId>dubbo-metrics-prometheus</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
dubbo:
  metrics:
    enable: true
    protocol: prometheus
    port: 20888
    aggregation:
      enabled: true
      bucket-num: 10
    jvm:
      enable: true
    threadpool:
      enable: true

management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  metrics:
    tags:
      application: ${spring.application.name}

Core metrics ( dubbo_ prefix): dubbo_provider_requests_total /

dubbo_consumer_requests_total
dubbo_provider_rt_milliseconds_bucket

(P50/P90/P99 latency histogram)

dubbo_provider_qps_total
dubbo_provider_requests_failed_total
dubbo_threadpool_threads

(thread pool active count, queue depth)

Alerting recommendations: P99 latency, failure rate, thread pool queue depth must have independent thresholds. Queue saturation often precedes business error rate rise by ~3 minutes — key early signal.

6.2 Tracing

In Spring Boot 3, OpenTelemetry is mainstream. Dubbo 3.3 provides dubbo-tracing-otel, but early versions experimental; verify on non-critical paths first.

<dependency>
  <groupId>org.apache.dubbo</groupId>
  <artifactId>dubbo-tracing-otel</artifactId>
</dependency>
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
  <groupId>io.opentelemetry</groupId>
  <artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
dubbo:
  tracing:
    enabled: true
    sampling:
      probability: 0.1  # Production 0.01~0.1
    propagation:
      type: w3c

management:
  tracing:
    sampling:
      probability: 0.1
  otlp:
    tracing:
      endpoint: http://otel-collector:4318/v1/traces

Trace propagation relies on Dubbo Attachment (HTTP Header / gRPC Metadata). When passing custom context across processes, always use Attachment, never ThreadLocal.

6.3 Log Correlation

Most overlooked but highest troubleshooting efficiency: embed TraceId in logs.

@Slf4j
@Component
public class TraceIdFilter implements Filter {
  @Override
  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
    String traceId = MDC.get("traceId");
    if (traceId == null) {
      traceId = UUID.randomUUID().toString().replace("-", "");
      MDC.put("traceId", traceId);
    }
    try {
      chain.doFilter(req, res);
    } finally {
      MDC.clear();
    }
  }
}

Logback pattern:

<pattern>%d{HH:mm:ss.SSS} [%thread] [%X{traceId:-}] %-5level %logger{36} - %msg%n</pattern>

Combine with Dubbo's RpcContext: on consumer side put traceId into Attachment, on provider side extract and put back into MDC — cross-service log chain connected.

6.4 Dubbo Admin

Dubbo Admin 0.6.x series, compatible with Dubbo 3.x, provides:

Service query & metadata view

Instance up/down, weight adjustment

Dynamic config (timeout, retry, load balance, routing rules)

Service testing (direct Triple invocation)

Service stats & dependency graph

Deployment:

docker run -d --name dubbo-admin \
  -p 8080:8080 \
  -e admin.registry.address=nacos://nacos:8848 \
  -e admin.config-center=nacos://nacos:8848 \
  -e admin.metadata-report.address=nacos://nacos:8848 \
  apache/dubbo-admin:0.6.0

Production must enable auth ( admin.check.auth.enable=true), otherwise anyone with console access can modify routing rules.

7. Production Pitfalls: Eight High-Frequency Issues

7.1 Thread Pool Isolation

Dubbo defaults to single shared thread pool ( all), all services share one pool. A slow interface occupying all threads blocks every Dubbo service in the app.

Dubbo 3 supports per-protocol-port isolation ( dubbo.protocol.threadpool), but finer isolation requires multi-port exposure:

dubbo:
  protocols:
  - id: fast
    name: tri
    port: 50051
    threadpool: fixed
    threads: 200
    queues: 0
  - id: slow
    name: tri
    port: 50052
    threadpool: fixed
    threads: 50
    queues: 200
@DubboService(protocol = "slow", timeout = 30000)
public class ReportExportServiceImpl implements ReportExportService {}
queues: 0

means no queuing; excess requests rejected immediately. Better than unbounded queue because queuing guarantees timeout while consuming memory and connections.

7.2 Connection Count & HTTP/2 Concurrency

Triple on HTTP/2 multiplexes over single connection, theoretically no need for per-request connections. Two limits: SETTINGS_MAX_CONCURRENT_STREAMS: HTTP/2 default concurrent stream limit, typically 100-1000, excess queues.

Single connection failure: all streams on one TCP connection affected simultaneously.

Production recommendations:

dubbo:
  consumer:
    connections: 2  # 2 connections per provider
    lazy: true
  provider:
    accepts: 1000  # Server connection accept limit, concurrent streams still bound by HTTP/2 settings

7.3 Serialization Security

Hessian2 deserialization vulnerabilities (CVE-2021-43297 etc.) are known. Dubbo 3 enables serialization whitelist by default:

dubbo:
  application:
    serialize-check-status: STRICT  # DISABLE / WARN / STRICT
    check-serializable: true

Production must use STRICT and configure whitelist in API module:

# Resource file META-INF/dubbo/serialize-allow-list.txt
com.example.api.dto.UserDTO
com.example.api.dto.OrderDTO

Serialization switch advice: new projects use fastjson2 or protobuf; existing projects keep Hessian2 with strict whitelist, evaluate migration after interfaces stabilize.

7.4 Timeout Propagation

Dubbo timeouts don't auto-propagate downstream. A calls B with 3s timeout, B calls C with its own 5s config. Result: A times out and returns, B still waits on C.

Solution: manually pass remaining time via Attachment. Dubbo 3.3 provides TimeoutCountDownFilter prototype, but production prefers business-layer wrapper: downstream timeout = min(own remaining timeout, downstream configured timeout) .

7.5 Version Compatibility & Canary

version

field is Dubbo's interface version, but don't use it for canary. Reasons: version is interface-level, granularity too coarse.

Consumer must explicitly change code/config to switch, not dynamic.

Version mismatch yields No provider available with no fallback.

Correct approach:

Breaking change → bump version (e.g., 1.0.0 → 2.0.0), run dual versions in parallel.

Compatible change → keep version, use tag routing for canary.

DTO field add/remove → ensure backward compatibility (no field deletion, no type change), Hessian2 naturally tolerates.

7.6 Generic Invocation Abuse

Generic invocation convenient but costs: no compile-time type checks; serialization via Map path, 20-40% performance drop; cannot use method-level governance. Only for gateway, open platform, test platform; core paths must be strongly typed.

7.7 Missing Metadata Center

Application-level discovery ( register-mode=instance) relies on metadata center storing "interface → application" mapping. If only registry configured without metadata center, Dubbo falls back to interface-level mode or consumers can't find services.

dubbo:
  metadata-report:
    address: nacos://127.0.0.1:8848
    parameters:
      namespace: prod

Correct upgrade sequence to application-level discovery:

All apps first configure register-mode=all (register both interface and application level).

Then configure metadata center.

Observe, confirm no issues, then switch all to instance.

7.8 Graceful Shutdown Order

Spring Boot's shutdown and Dubbo's shutdown have ordering requirements. If Spring destroys DataSource first while Dubbo still processes requests, Connection closed exceptions occur.

Correct approach:

@Component
public class GracefulShutdownListener implements ApplicationListener<ContextClosedEvent> {
  @Override
  public void onApplicationEvent(ContextClosedEvent event) {
    // 1. First let Dubbo go offline
    try {
      DubboBootstrap.getInstance().stop();
    } catch (Exception e) {
      log.warn("dubbo shutdown error", e);
    }
    // 2. Then let Spring destroy beans
  }
}

Also ensure in application.yml:

dubbo:
  application:
    shutdown-wait: 10000
    register-mode: instance

8. Benchmark & Summary

8.1 Benchmark Method & Comparative Data

Test env: two 4C8G machines, same AZ 10GbE, JDK 17, Spring Boot 3.2.5, Dubbo 3.3.4. Tools: wrk2 for REST, dubbo-benchmark for Dubbo, 200 concurrency, 5 minutes, stable period data.

Two scenarios: small payload request {"id":1}, response 8-field DTO ~300B; large payload same request, response ~5KB with list.

Results magnitude:

REST+JSON (Tomcat): small ~8,500 QPS, P99 22ms, CPU 75%; large ~2,100 QPS, P99 68ms, CPU 90%.

Dubbo3 Triple Hessian2: small ~32,000 QPS, P99 9ms, CPU 62%; large ~7,800 QPS, P99 26ms, CPU 78%.

Dubbo3 Triple Protobuf: small ~41,000 QPS, P99 6ms, CPU 55%; large ~11,200 QPS, P99 18ms, CPU 70%.

Triple vs native gRPC interop: small ~38,000 QPS, P99 7ms, CPU 58%.

Numbers are indicative; actual varies by machine, payload, serializer — test in your own environment.

Key conclusions:

Small payload: Triple vs REST throughput up 3.5-4.8x, P99 latency down to 1/2-1/3.

Large payload: improvement narrows to 3-5x as serialization absolute time dominates.

Protobuf vs Hessian2 adds 20-40% but high migration cost for existing Java interfaces.

Triple vs native gRPC performance parity (<10% diff), meaning Triple doesn't sacrifice cross-language capability.

8.2 Migration Checklist

Architecture Layer

[ ] api module zero business deps, all DTOs Serializable [ ] Interface version strategy clear (breaking change → bump version, compatible → tag routing)

[ ] Application-level discovery ( register-mode=instance) + metadata center configured

Configuration Layer

[ ] Timeout layered: global default → interface → method

[ ] Write interfaces retries=0 [ ] Slow interfaces separate protocol port + dedicated thread pool

[ ] queues not unbounded, reject over queue

[ ] serialize-check-status=STRICT + whitelist file

Governance Layer

[ ] Sentinel integrated, core interfaces have flow control & circuit breaking rules

[ ] Canary tag routing configured with force=false [ ] K8s preStop + terminationGracePeriodSeconds + shutdown-wait aligned

[ ] readinessProbe covers Dubbo service exposure completion

Observability Layer

[ ] Prometheus metrics collection, alerts cover P99 / failure rate / thread pool queue

[ ] Tracing sampling rate reasonable (production 1%-10%)

[ ] TraceId in logs, cross-service traceable

[ ] Dubbo Admin deployed with auth enabled

Release Layer

[ ] Interface-level → application-level registration smooth switch (first all then instance)

[ ] Canary release process validated (tag routing + separate instance group)

[ ] Rollback plan: protocol/version quickly switchable

8.3 Summary

Migrating from REST/OpenFeign to Dubbo 3 + Triple yields gains in three areas: performance, contract, governance.

Performance: HTTP/2 multiplexing + binary serialization → 3-5x throughput, latency halved. Contract: api module becomes strong-typed contract carrier, incompatible changes caught at compile time. Governance: timeout, retry, load balance, routing, circuit breaking consolidated into framework, unified config, cross-language usable.

But migration isn't free. Interface definitions must be modularized, serialization whitelist maintained, graceful start/stop chain wired, metadata center deployed. These costs buy an RPC infrastructure capable of supporting years of scale growth.

If team still hesitates, my advice in one sentence: new services directly on Dubbo 3 + Triple; existing services prioritized by business value, migrate high-QPS, deep-call-chain paths first. Full migration isn't the goal — making core paths faster and more stable is.

References

Apache Dubbo Official Docs: https://dubbo.apache.org/

Dubbo 3 Triple Protocol Design & Implementation

Spring Boot 3 Official Docs: https://spring.io/projects/spring-boot

Nacos 2.x Official Docs: https://nacos.io/

Sentinel Official Docs: https://sentinelguard.io/

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

MicroservicesRPCperformance benchmarkHTTP/2Service GovernanceTriple ProtocolSpring Boot 3Apache Dubbo 3
Xiaolin Talks Programming
Written by

Xiaolin Talks Programming

Focuses on sharing original technical insights. Senior architect at a top tech company with years of experience in technical architecture and management, and extensive interview experience. Offers one-on-one technical coaching, guiding you from beginner to architecture design to technical management. Follow for free learning resources. Free one-on-one interview coaching to help you land offers quickly.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.