Dubbo 开发随笔

Dubbo 版本说明

Dubbo 3 对比 Dubbo 2

Dubbo 3 版本对比 Dubbo 2 版本:

  • 服务发现资源利用率显著提升
  • 对比接口级服务发现,单机常驻内存下降 50%,地址变更期 GC 消耗下降一个数量级(百次 -> 十次)
  • 对比应用级服务发现,单机常驻内存下降 75%,GC 次数趋零
  • Dubbo 协议性能基本持平,Triple 协议在网关、Stream 吞吐量方面更具优势
  • Dubbo 协议(3.0 vs 2.x),3.0 实现较 2.x 总体 QPS、RT 持平,略有提升
  • Triple 协议 vs Dubbo 协议,直连调用场景 Triple 性能并无优势,但其优势在于网关、Stream 调用场景

Dubbo 2.7.1 版本存在的 Bug

Dubbo 2.7.1 版本存在无法正确获取到网卡 IP 地址和动态注册服务的 Bug,详细介绍可以看 这里

Dubbo 基础介绍

Dubbo 支持的通信协议

Dubbo 支持的序列化协议

Dubbo 整合框架

Spring 与 SpringBoot 整合

  • 使用 Dubbo 的三种方式

    • 三种使用方式分别为:XML 配置、Annotation 配置、原生 API
    • Spring 与 SpringBoot 都支持以 XML 配置、Annotation 配置使用 Dubbo
  • Spring 与 SpringBoot 整合 Dubbo 的简单总结

    • SpringBoot + XML: @ImportResource
    • SpringBoot + Annotation: @EnableDubbo
    • Spring + Annotation: AnnotationConfigApplicationContext
    • Spring + XML: ClassPathXmlApplicationContextFileSystemXmlApplicationContext

特别注意

在 Dubbo 2.6.x 版本及以前,由于使用基于注解(Annotation)的方式整合 Dubbo,无法实现 Dubbo 方法级的配置(即 <dubbo:method> 标签的功能),如果 Spring、SpringBoot 需要用到 Dubbo 方法级的配置,那么就需要使用 XML 的方式整合 Dubbo。但在 Dubbo 2.7.x 版本及以后,Dubbo 已经支持注解方式实现方法级配置,不再必须依赖 XML 配置。

Dubbo 服务高可用

Dubbo 服务降级

当服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理,从而释放服务器资源以保证核心交易正常运作或高效运作。Dubbo 可以通过服务降级功能临时屏蔽某个出错的非关键服务,并定义降级后的返回策略。


基于 Dubbo Mock 实现服务降级 - 返回空结果

  • 在服务消费者中配置 <dubbo:reference> 时,将 mock 属性设置为 return null
1
2
3
4
5
<dubbo:reference id="fooService" 
interface="com.test.service.FooService"
timeout="10000"
check="false"
mock="return null" />
  • Dubbo 会在调用服务提供者失败时,直接返回 null 结果,实现最简单的降级逻辑。

基于 Dubbo Mock 实现服务降级 - 自定义降级逻辑

  • 在服务消费者中配置 <dubbo:reference> 时,将 mock 属性设置为 true
1
2
3
4
5
<dubbo:reference id="fooService" 
interface="com.test.service.FooService"
timeout="10000"
check="false"
mock="true" />
  • 在服务消费者中,往接口同一个包路径下创建一个 Mock 类,命名规则:接口名 + Mock,实现接口和降级逻辑:
1
2
3
4
5
6
7
8
9
public class FooServiceMock implements FooService {

@Override
public void sayHello() {
// 降级逻辑,例如返回默认提示或调用备用服务
System.out.println("服务不可用,执行降级逻辑");
}

}
  • Dubbo 会在调用服务提供者失败时,自动使用该 Mock 类来替代真实服务,实现自定义的降级逻辑。

Dubbo 向注册中心写入动态配置覆盖规则

1
2
3
RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181"));
registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null"));
  • 配置参数说明:
    • mock=force:return+null 表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。
    • 还可以改为 mock=fail:return+null 表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。
    • 利用 Dubbo-Admin 的 UI 界面(旧版),可以方便地对服务进行屏蔽 / 恢复(mock=force:return+null)、容错 / 恢复(mock=fail:return+null)处理,即上面提到的两种服务降级方式

Dubbo 集群容错策略

在集群调用失败时,Dubbo 提供了以下多种集群容错策略,默认策略为 Failover(故障转移)。在线上生产环境中,一般使用 Hystrix、Resilience4j 等框架进行容错处理。

  • Failover:故障转移,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 参数来配置重试次数(不含第一次)。
  • Failfast:快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
  • Failsafe:失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
  • Failback:失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
  • Forking:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。
  • Broadcast:广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
  • ZoneAwareCluster:区域感知集群,当部署了多个机房或区域(Zone)时,优先调用同一机房的服务提供者,以降低跨区调用延迟和网络风险。
  • MergeableCluster:结果合并集群,会调用多个服务提供者,并将多个服务提供者的调用结果进行合并,最终返回合并后的结果。
1
2
3
4
<!-- 使用 Failover 集群容错策略时,重试次数的几种配置方式如下 -->
<dubbo:service retries="2" />
<dubbo:reference retries="2" />
<dubbo:reference> <dubbo:method name="findFoo" retries="2" /> </dubbo:reference>
1
2
3
<!-- 集群模式配置,按照以下几种配置方式在服务提供方和消费方配置集群模式 -->
<dubbo:service cluster="failsafe" />
<dubbo:reference cluster="failsafe" />

Dubbo 失败重试和超时重试

在分布式系统中,网络抖动、节点故障等情况时有发生,Dubbo 提供了灵活的失败重试与超时重试配置来提高服务调用成功率。

  • 基本原理

    • 失败重试:服务消费者调用服务提供者失败(抛出异常)时,Dubbo 会自动按配置的 retries 参数进行重试。
    • 超时重试:服务调用时间超过 timeout 限制时,Dubbo 会中断等待,并按 retries 配置进行重试。
  • 配置示例

    • 在服务消费者中配置 <dubbo:reference> 时,设置 retriestimeout 属性
      1
      2
      3
      4
      5
      6
      <dubbo:reference id="userService" 
      interface="com.test.service.UserService"
      check="true"
      async="false"
      retries="3"
      timeout="2000" />
    • timeout="2000":表示单次调用超时时间为 2 秒,超时即中断调用,不会无限等待。
    • retries="3":表示调用失败或超时后,最多重试 3 次(即总共调用 4 次:1 次原始调用 + 3 次重试),默认重试 2 次。
  • 参数建议

    • 建议结合实际业务特点配置参数:
      • timeout:建议根据业务 SLA 设置。例如:公司部分读请求要求在 200ms 内返回结果,那么可以设置 timeout="200",超时即放弃等待。
      • retries:适用于读操作(幂等操作,如查询数据)。例如设置 retries="3",表示原始调用失败后再尝试调用 3 次,提升服务调用的成功率。
  • 注意事项

    • 对于写操作(非幂等操作),由于可能会引发重复提交等问题,一般需要显式设置 retries="0"
    • 重试并不是万能的,过高的 retries 会放大系统压力,建议与限流、降级等策略结合使用。
    • 对高延迟接口,应优先优化接口性能,而不是依赖长超时配置。

Dubbo 宕机环境下的高可用性

  • 监控中心宕掉不影响使用,只是丢失部分采样数据
  • 数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
  • 注册中心对等集群,任意一台宕掉后,将自动切换到另一台
  • 注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
  • 服务提供者无状态,任意一台宕掉后,不影响使用
  • 服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复

Dubbo 源码剖析

Dubbo 的服务暴露

dubbo-export

Dubbo 的服务引用

dubbo-reference

Dubbo 配置类之间的关系

dubbo-config

Dubbo 代码调试

打印详细日志信息

为了更好观察 Dubbo 的运行行为,通常需要打印 DEBUG 级别的日志信息,比如:

  • 基于 Properties 的日志配置内容(比如 Log4jlog4j.properties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# ===============================
# 基础日志配置
# ===============================
log4j.rootLogger=INFO, stdout

# 控制台输出
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss} [%t] %-5p %c - %m%n

# ===============================
# Dubbo 日志级别设置
# ===============================
# 打印 Dubbo 框架相关相关的 Debug 日志
log4j.logger.org.apache.dubbo=DEBUG

# 如果需要更详细的 RPC 调用、网络通信、注册中心等日志,可进一步细化日志打印(可选)
log4j.logger.org.apache.dubbo.rpc=DEBUG
log4j.logger.org.apache.dubbo.remoting=DEBUG
log4j.logger.org.apache.dubbo.registry=DEBUG
log4j.logger.org.apache.dubbo.config=DEBUG
log4j.logger.org.apache.dubbo.monitor=DEBUG
log4j.logger.org.apache.dubbo.metadata=DEBUG
  • 基于 XML 的日志配置内容(比如 Lockbacklogback.xml 或者 SpringBoot 的 logback-spring.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>

<!-- 根日志级别 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>

<!-- =============================
Dubbo 日志配置
============================= -->

<!-- Dubbo 框架整体日志 -->
<logger name="org.apache.dubbo" level="DEBUG"/>

<!-- 如果需要更详细的 RPC 调用、网络通信、注册中心等日志,可进一步细化日志打印(可选) -->
<logger name="org.apache.dubbo.rpc" level="DEBUG"/>
<logger name="org.apache.dubbo.remoting" level="DEBUG"/>
<logger name="org.apache.dubbo.registry" level="DEBUG"/>
<logger name="org.apache.dubbo.config" level="DEBUG"/>
<logger name="org.apache.dubbo.monitor" level="DEBUG"/>
<logger name="org.apache.dubbo.metadata" level="DEBUG"/>

</configuration>

Dubbo 性能调优

线程池状态导出

概念介绍

  • Dubbo 可以通过 jstack 自动导出线程堆栈来保留现场,方便排查问题。
  • Dubbo 线程池状态的默认导出策略:
    • 导出开关:默认打开
    • 导出路径:user.home 标识的用户主目录
    • 导出间隔:最短间隔允许每隔 10 分钟导出一次
  • 当业务线程池满时,往往需要知道线程都在等待哪些资源、条件,以找到系统的瓶颈点或异常点。

配置方式

  • 导出开关
1
2
# Properties 配置(比如 dubbo.properties)
dubbo.application.dump.enable=true
1
2
<!-- Spring XML 配置 -->
<dubbo:application name="demo-provider" dump-enable="false"/>
1
2
3
4
5
# YAML 配置(比如 application.yml)
dubbo:
application:
name: dubbo-springboot-demo-provider
dump-enable: false
  • 导出路径
1
2
# Properties 配置(比如 dubbo.properties)
dubbo.application.dump.directory=/tmp
1
2
<!-- Spring XML 配置 -->
<dubbo:application name="demo-provider" dump-directory="/tmp"/>
1
2
3
4
5
# YAML 配置(比如 application.yml)
dubbo:
application:
name: dubbo-springboot-demo-provider
dump-directory: /tmp

Dubbo 动态配置

配置规则(也叫 “动态配置” 或者 “动态配置规则”)是 Dubbo 设计的在无需重启应用的情况下,动态调整 RPC 调用行为的一种能力。Dubbo 从 2.7.0 版本开始,支持从应用和服务两个粒度来调整动态配置。

Dubbo-Admin 服务治理

  • Dubbo 支持通过 Dubbo-Admin 实现服务治理,支持以下功能(如下图所示):
    • 支持动态配置服务降级(Mock),使用介绍请看 这里
    • 支持动态配置条件路由、标签路由、黑白名单、动态配置、负载均衡、权重调整,使用说明请看 这里

Dubbo 常见使用问题

多网卡环境指定 IP 的问题

服务器有多个网卡接口(比如 eth1eth0),希望 Provider(服务提供者)使用指定的 IP 地址往册中心(比如 ZooKeeper、Nacos)注册服务

  • 方法一:在配置文件中明确指定 Provider 要注册的 IP 地址
1
2
# 使用 Properties 配置方式,如 application.properties
dubbo.protocol.host=<your_ip>
1
2
3
4
5
6
# 使用 YAML 配置方式,如 application.yml
dubbo:
protocol:
name: dubbo
port: 20880
host: <your_ip>
1
2
# 使用 XML 配置方式,如 dubbo.xml
<dubbo:protocol name="dubbo" port="20880" host="<your_ip>"/>
  • 方式二:通过 JVM 启动参数指定 Provider 要注册的 IP 地址
1
java -Ddubbo.protocol.host=<your_ip> -jar your-application.jar
  • 方法三:通过系统环境变量指定 Provider 要注册的 IP 地址
1
export DUBBO_IP_TO_BIND=<your_ip>
  • 方式四:通过 JVM 启动参数指定 Provider 要使用的网卡
1
2
3
4
java -Ddubbo.network.interface.preferred=eth1 -jar your-application.jar

# 多个网卡可以使用英文逗号隔开(写在左边的优先级最高)
java -Ddubbo.network.interface.preferred=eth1,eth0 -jar your-application.jar

服务器有多个网卡接口(比如 eth1eth0),希望 Consumer(服务消费者)使用指定的 IP 地址去调用 Provider(服务提供者)的接口

  • 方式一:通过 JVM 启动参数指定 Consumer 要使用的网卡
1
2
3
4
java -Ddubbo.network.interface.preferred=eth1 -jar your-application.jar

# 多个网卡可以使用英文逗号隔开(写在左边的优先级最高)
java -Ddubbo.network.interface.preferred=eth1,eth0 -jar your-application.jar

Dubbo 关于网卡的配置

  • 通过 JVM 启动参数 -Ddubbo.network.interface.ignored=eth1,eth0,排除一个或多个网卡,适用于服务消费者(Consumer)和服务提供者(Provider)。
  • 通过 JVM 启动参数 -Ddubbo.network.interface.preferred=eth1,eth0,指定一个或多个网卡,适用于服务消费者(Consumer)和服务提供者(Provider)。

SpringBoot 整合 Dubbo 出错

错误信息:

1
SpringBoot-2.1.0+ 整合 Apache Dubbo-2.7.0,启动应用后提示需要添加SpringBoot配置 “spring.main.allow-bean-definition-overriding=true”

异常日志:

1
2
3
4
5
6
7
8
9
10
11
***************************
APPLICATION FAILED TO START
***************************

Description:

The bean 'dubboConfigConfiguration.Single', defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.

Action:

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

异常分析:

1
2
3
问题是由注解 @EnableDubbo、@EnableDubboConfig 的使用所导致,具体可参考以下资料:
https://github.com/apache/dubbo/issues/3193
https://github.com/apache/dubbo-spring-boot-project/issues/476

解决方法:

1
2
3
方法一: 往 SpringBoot 的配置文件(application.properties)中添加对应配置,允许在 Spring 容器内可以覆盖 Bean 的定义: spring.main.allow-bean-definition-overriding=true

方法二: 将 Apache Dubbo-2.7.0 升级到 Apache Dubbo-2.7.2 版本,具体可参考:https://github.com/apache/dubbo-spring-boot-project/issues/467

@DubboReference 注解无法实现注入

错误描述:

1
在 Spring + SpringMVC + Dubbo 的项目中,发现在服务消费者中的 Controller 里面的 @DubboReference 注解会失效,即无法正常注入 Bean,导致程序运行时抛出空指针异常。

错误分析:

1
2
(1) Spring 的扫描根本无法识别 Dubbo 的 @DubboReference 注解;同一方面,Dubbo 的扫描也无法识别 Spring 的 @Controller 注解,所以这两个扫描的顺序要安排好。
(2) 如果先扫描 Spring 的 @Controller 注解,这时候把控制器都实例化好后,再扫描 Dubbo 的 @DubboReference 注解,这样就会无法实现 Bean 的注入,导致程序运行时抛出空指针异常。

解决方法:

第一步:在服务提供者的模块中,先扫描 Spring 的注解(如 @Controller),然后再扫描 Dubbo 的注解(如 @DubboService,为了暴露服务到注册中心),配置示例如下所示:

  • application-context.xml 配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 扫描并注册 Spring 组件 -->
<context:component-scan base-package="com.clay.dubbo.producer"/>

<!-- 配置服务应用名 -->
<dubbo:application name="dubbo-provider-application">
<dubbo:parameter key="qos.enable" value="true"/>
<dubbo:parameter key="qos.port" value="22222"/>
<dubbo:parameter key="qos.accept.foreign.ip" value="false"/>
</dubbo:application>

<!-- 注册中心配置 -->
<dubbo:registry address="zookeeper://192.168.2.235:2181" timeout="5000"/>

<!-- 配置服务协议 -->
<dubbo:protocol name="dubbo" port="20880"/>

<!-- 开启 Dubbo 的注解扫描 -->
<dubbo:annotation package="com.clay.dubbo.producer"/>
  • application-mvc.xml 配置文件
1
2
3
4
5
<!-- 启用 SpringMVC 的注解驱动功能 -->
<mvc:annotation-driven/>

<!-- 扫描 Web 控制器 -->
<context:component-scan base-package="com.clay.dubbo.producer.controller"/>

第二步:在服务消费者的模块中,先扫描 Dubbo 的注解(如 @DubboReference,为了引用远程服务),然后再扫描 Spring 的注解(如 @Controller),配置示例如下所示:

  • application-context.xml 配置文件
1
2
<!-- 扫描并注册 Spring 组件 -->
<context:component-scan base-package="com.clay.dubbo.consumer"/>
  • application-mvc.xml 配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 启用 SpringMVC 的注解驱动功能 -->
<mvc:annotation-driven/>

<!-- 配置服务应用名 -->
<dubbo:application name="dubbo-consumer-application">
<dubbo:parameter key="qos.enable" value="true"/>
<dubbo:parameter key="qos.port" value="22222"/>
<dubbo:parameter key="qos.accept.foreign.ip" value="false"/>
</dubbo:application>

<!-- 注册中心配置 -->
<dubbo:registry address="zookeeper://192.168.2.235:2181" timeout="5000"/>

<!-- 开启 Dubbo 注解扫描(先扫描)-->
<dubbo:annotation package="com.clay.dubbo.consumer"/>

<!-- 扫描 Web 控制器(后扫描) -->
<context:component-scan base-package="com.clay.dubbo.consumer.controller"/>