Dubbo2 巩固教程之五

大纲

前言

学习资源

Dubbo 深入理解

Dubbo 设计原则

Dubbo 的整体架构

Dubbo 中存在四大组件:

  • Provider: 服务提供者。
  • Consumer: 服务消费者。
  • Registry: 服务注册与发现的中心,提供目录服务,也称为服务注册中心。
  • Monitor: 统计服务的调用次数、调用时间等信息的监控服务,并可以对服务设置权限、降级处理等,称为服务管控中心。

Dubbo 的功能性需求

Dubbo 的非功能性需求

  • Dubbo 发展之初遇到的问题

    • 随着 Dubbo 在阿里内部使用越来越火热,堆积的需求也越来越多,Dubbo 项目组人手不足。
    • Dubbo 项目外的人员希望也能加入进来丰富 Dubbo 生态,进行功能扩展开放,开闭原则(OCP)应声而出。
  • Dubbo 的非功能性需求:开放扩展

    • Dubbo 需要留一些扩展点,让项目参与者尽量黑盒扩展,而不是白盒的修改代码,否则分支、质量、合并、冲突都会很难管理。

Dubbo 的微核心插件式设计

  • Dubbo 是如何做到优雅的开放扩展
    • 微核心 + 插件式,平等对待第三方
      • 由一个插件生命周期管理容器构成微核心,核心不包括任何功能,这样可以确保所有功能都能被替换。
      • 框架作者能做到的功能,扩展者也一定要能做到,以保证平等对待第三方。
      • 因此,框架自身的功能也要用插件的方式实现,不能有任何硬编码。
    • 首先需要做的是统一扩展点的加载方式
    • 然后考虑采用什么样的扩展点加载方式
      • 采用微核心 + 插件式架构。通常微核心会采用 Factory、IoC、OSGi 等方式管理插件生命周期。
      • Dubbo 最初曾考虑使用 Spring IoC 或自己实现一个 IoC 容器,但最终选择了采用 Factory 方式管理插件,并曾经使用 JDK 的 SPI 机制来实现 Factory。
      • 因此,Dubbo 中曾有一个已被废弃的 @Extension 注解,最终被自己实现的 SPI 机制取代,也就是现在使用的 @SPI 注解。

Dubbo 的基本设计原则

  • 采用 Microkernel + Plugin(微核心 + 插件)模式,Microkernel 只负责组装 Plugin,而 Dubbo 自身的功能也是通过扩展点实现的,也就是 Dubbo 的所有功能点都可被用户自定义扩展所替换。
  • 采用 URL 作为配置信息的统一格式,所有扩展点都通过传递 URL 携带配置信息。

Dubbo 的组合式扩展设计

Dubbo 采用了组合式扩展:核心最小化(Protocol + Invoker + Exporter),其他能力通过分层与组合的方式添加。

扩充式扩展与增量式扩展

在对一个原有的功能设计扩展方案时,常见有两类扩展方式:

  • 扩充式扩展:

    • 在原有实现上做增强,让新旧功能共用同一套代码路径。
    • 新功能通过在原逻辑内 “兼容” 方式实现。
    • 旧功能也会受到新逻辑影响,即使不需要,也会额外承担开销。
  • 增量式扩展:

    • 原功能保持简单、保持原有代码路径不变。
    • 新功能以独立实现新增,不入侵旧逻辑。
    • 使用者按需选择使用旧实现或新实现,互不干扰。
  • 举例,扩展一个 OSGI 序列化功能,要求功能明确,把流转成对象,对象转成流。

    • 扩充式扩展的做法:
      • 在原有序列化逻辑内部增加对 OSGi 序列化的支持。
      • 无论是否需要 OSGi,都要先把 Stream 转成 byte[],才能满足新逻辑的处理流程。
      • 最终结果:
        • 新功能很容易实现,因为复用了旧逻辑。
        • 但旧场景本来不需要额外的 byte[] 中间转换,却被迫付出了额外成本(性能损耗、内存占用增加等)。
      • 存在问题:
        • 旧功能为新功能背负了不必要的成本。
    • 增量式扩展的做法:
      • 保留非 OSGi 的序列化代码,不改动、不增加额外中间步骤。
      • 单独新增一个 OSGi 序列化实现,内部按 OSGi 规范自行处理。
      • 若使用 OSGi 场景,则直接注入 / 依赖此实现即可。
      • 实现好处:
        • 旧功能不变,成本不变,新功能独立,互不影响。
扩展方式特点代价适用场景
扩充式扩展在原逻辑内兼容新功能,新旧走同一代码路径旧功能被连带影响,性能可能下降新功能非常轻量、对旧逻辑影响极小
增量式扩展新旧逻辑独立实现,互不影响需要维护多套实现新功能对旧功能影响较大、开销较大,或不想污染旧逻辑
泛化式扩展与组合式扩展

在设计一个可扩展系统时,常见有两类扩展思路:

  • 泛化式扩展

    • 定义:
      • 扩展点不断抽象化、泛化,将不同功能的要求都往同一个概念里塞。
      • 本质是取所有功能的并集:新功能加入时,需要扩充原有抽象,使其能容纳更多概念。
    • 缺点:
      • 容易让扩展点逐渐臃肿。
      • 新功能侵入原有核心模型,使系统越来越复杂。
      • 许多 “非核心功能” 会被迫纳入核心抽象。
  • 组合式扩展

    • 定义:
      • 将功能按职责正交拆分。
      • 本质是取所有功能的交集,扩展点保持最小交集(最小必要抽象)。
      • 新功能总是基于旧功能之上实现,通过组合、包装、链式调用等方式叠加,而不是修改核心抽象。
    • 优点:
      • 核心模型保持简洁稳定。
      • 扩展功能彼此独立、可插拔。
      • 系统不会因为扩展点膨胀而复杂化。
扩展方式特点问题 / 优点
泛化式扩展抽象不断膨胀,取所有功能并集模型臃肿,核心功能被污染
组合式扩展抽象保持极简,通过包装 / 组合扩展稳定、高内聚、可插拔、清晰分层
  • 泛化式扩展存在的问题(以 Dubbo 为例)
    • 在 Dubbo 中,如果选择泛化式扩展的做法,就会尝试把各种周边能力都塞进一个 “核心模型” 里,例如:
      • 路由器(Router)
      • 集群容错(Cluster)
      • 负载均衡(LoadBalance)
      • 服务发布者(Exporter)
      • 服务订阅者(Directory)
    • 一旦把它们都视为 “核心功能”,结果就是:
      • 核心抽象过度膨胀
      • 层次不清晰
      • 新功能必须入侵原核心框架
    • Dubbo 的做法是采用组合式扩展:核心极简 + 分层组合
      • Dubbo 的真正核心抽象并不包括上述能力
      • Dubbo 的核心包括三部分:
        • Invoker
          • 代表一个可调用体
          • 是 Dubbo 的最小执行单元
          • 类似 “函数指针”,统一抽象了调用逻辑
        • Protocol
          • 负责远程通信的协议栈
          • 决定如何导出服务、如何引用服务
        • Exporter
          • Protocol.export() 的结果
          • 表示一个已经发布出去的、可被消费的服务
      • 由此 Dubbo 形成两个主要层次:
        • 协议层(Protocol Layer)
          • 负责通信协议、序列化、网络传输等
          • 核心抽象非常稳定:Protocol + Invoker + Exporter
        • 路由层(Router Layer)
          • 在协议层之上进行组合式增强
          • 路由、负载均衡、容错等都属于这一层
          • 非核心能力都通过组合 Invoker 的方式增强,例如:
            • Router → 过滤或选择部分 Invoker
            • LoadBalance → 根据负载均衡策略选择一个 Invoker
            • Cluster → 封装多 Invoker 提供集群能力
            • Filter → 链式增强一个 Invoker
          • 因此,这些(路由、负载均衡、容错等)都不是 Dubbo 的核心,而是路由层的扩展能力

总结

Dubbo 的设计哲学是典型的组合式扩展:核心最小化(Protocol + Invoker + Exporter),其他能力通过分层与组合的方式添加。

Dubbo 的最大化复用设计

  • 每个扩展点只封装一个变化因子
  • 比如:想扩展 RPC 协议,可能只是想更换一种通信传输方式(比如,将 Netty 替换为 Mina),其他的都复用。需要将协议拆解如下:

Dubbo 的全管道式设计

Dubbo 采用了截面拦截(AOP 风格),而非模板方法(Template Method)设计模式。

  • 模板方法设计模式的局限

    • 模板方法通常用于代码复用:
      • 将公共逻辑抽取到父类的模板方法中;
      • 将可变逻辑抽象为钩子方法,由子类实现。
    • 但在 Framework 场景中,当 “公共逻辑” 本身非常复杂、包含大量功能点、并且新能力持续增加时:
      • 父类会变得臃肿;
      • 模板方法的结构会被扩展需求不断侵入;
      • 覆盖点过多、难以维护;
      • 扩展性差,一旦修改会影响所有子类。
    • 因此,模板方法不适合承担 Dubbo 这种需要大量、可插拔扩展点的系统。
  • Dubbo 采用截面拦截

    • Dubbo 将公共逻辑抽取出来,通过 Wrapper + Filter 的方式进行组合扩展:
      • 每个扩展点实现一个独立的 Filter;
      • Filter 在 Invoker 外层形成调用链(Pipeline);
      • 责任链模式保证扩展能力可插拔、可按需组合;
      • Wrapper(如 Protocol$Adaptive)负责将扩展链编织到 Invoker 调用中。
    • 这种架构类似 AOP:扩展功能作为 “切面” 附加在 Invoker 调用前后,而不需要修改核心逻辑。
  • Dubbo 中哪些功能是通过截面拦截实现的?

    • 在 Dubbo 中,许多功能都属于对 Invoker 调用的增强,而非核心逻辑,可以通过截面拦截实现:
      • local 本地调用
      • mock 服务降级
      • generic 泛化调用
      • echo 回声测试
      • token 令牌校验
      • accesslog 访问日志
      • monitor 调用统计
      • count 统计计数
      • limit 限流
    • 上面这些都不是由 Invoker 继承自父类获得的功能,而是通过独立的 Filter(例如限流的 LimitFilter),以调用链方式进行扩展。
    • 为什么 Dubbo 不把这些功能放在 Invoker 的父类中?原因很简单:
      • 这会让父类极度臃肿,维护困难;
      • 每加入一个新功能就要改父类,扩展困难;
      • 父类逻辑会被功能点挤满,失去核心职责;
      • 不同功能之间耦合严重,测试成本高;
      • 无法做到 “按需添加” 扩展。

Dubbo 的外部最少概念

  • 一致性概念模型:
    • 保持尽可能少的概念,有助于理解,对于开放的系统尤其重要。
    • 另外,各接口都使用一致的概念模型,能相互指引,并减少模型转换。

Dubbo 的十层整体设计

Dubbo 整体架构设计图(如上所示)的说明:

  • 图中左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口。
  • 图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。
  • 图中绿色小块的为扩展接口,蓝色小块为实现类,图中只显示用于关联各层的实现类。
  • 图中蓝色虚线为初始化过程,即启动时组装链,红色实线为方法调用过程,即运行时调时链,紫色三角箭头为继承,可以把子类看作父类的同一个节点,线上的文字为调用的方法。

Dubbo 的整体架构可以分为 10 层,从上到下依次是:

  • (1) Service 层:接口层,给服务提供者和服务消费者来实现的(留给开发人员来实现)。
  • (2) Config 配置层:对外配置接口,以 ServiceConfigReferenceConfig 为中心,可以直接初始化配置类,也可以通过 Spring 解析配置生成配置类。
  • (3) Proxy 服务代理层:服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton, 以 ServiceProxy 为中心,扩展接口为 ProxyFactory
  • (4) Registry 注册中心层:封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 RegistryFactoryRegistryRegistryService
  • (5) Cluster 路由层:封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口为 ClusterDirectoryRouterLoadBalance
  • (6) Monitor 监控层:RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactoryMonitorMonitorService
  • (7) Protocol 远程调用层:封装 RPC 调用,以 InvocationResult 为中心,扩展接口为 ProtocolInvokerExporter
  • (8) Exchange 信息交换层:封装请求响应模式,同步转异步,以 RequestResponse 为中心,扩展接口为 ExchangerExchangeChannelExchangeClientExchangeServer
  • (9) Transport 网络传输层:抽象 Mina 和 Netty 为统一接口,以 Message 为中心,扩展接口为 ChannelTransporterClientServerCodec
  • (10) Serialize 数据序列化层:可复用的一些工具,扩展接口为 SerializationObjectInputObjectOutputThreadPool

在 Dubbo 的十层整体设计中,各层之间的关系如下:

  • (1) 在 RPC 中,Protocol 是核心层,也就是只要有 Protocol + Invoker + Exporter 就可以完成非透明的 RPC 调用,然后在 Invoker 的主过程上 Filter 拦截点。
  • (2) 上图中的 Consumer 和 Provider 是抽象概念,只是想让看图者更直观的了解哪些类分属于客户端与服务器端,不用 Client 和 Server 的原因是:Dubbo 在很多场景下都使用 Provider、Consumer、Registry、Monitor 来划分逻辑拓普节点,保持统一概念。
  • (3) Cluster 是外围概念,所以 Cluster 的目的是将多个 Invoker 伪装成一个 Invoker,这样其它人只要关注 Protocol 层的 Invoker 即可,加上 Cluster 或者去掉 Cluster 对其它层都不会造成影响,因为只有一个提供者时,是不需要 Cluster 的。
  • (4) Proxy 层封装了所有接口的透明化代理,而在其它层都以 Invoker 为中心,只有到了暴露给用户使用时,才会使用 Proxy 将 Invoker 转成接口,或者将接口实现转成 Invoker,也就是去掉 Proxy 层 RPC 是可以 Run 的,只是不那么透明,不那么看起来像调本地服务一样调远程服务。
  • (5) Remoting 实现是 Dubbo 协议的实现,如果选择 RMI 协议,整个 Remoting 都不会用上的。Remoting 的内部划分为 Transport 传输层和 Exchange 信息交换层,Transport 层只负责单向消息传输,是对 Mina、Netty、Grizzly 的抽象,它也可以扩展为 UDP 传输,而 Exchange 层是在传输层之上封装了 Request 和 Response 语义。
  • (6) Registry 和 Monitor 实际上不算一层,而是一个独立的节点,只是为了全局概览,用层的方式画在一起。