Dubbo是一个优秀的高性能RPC框架,也是一个经历过时间考验的优秀服务框架。优秀的框架就必然有很多值得学习和借鉴的设计。
Dubbo的内核中只包含了服务调用、集群管理、路由策略等最基础的核心功能。而除了核心之外的其他功能,全都通过扩展实现。包括:序列化、协议、负载均衡、过滤器等。
每个扩展都有一个对应的接口定义,具体实现可以自定动态加载和替换,这样插件式的设计,可以根据不同的业务选择不同的实现,灵活多变。
SPI机制:Service Provider Interface, 是一种Java平台提供的服务发现机制,允许在运行时按需加载和使用外部服务提供者。在Dubbo中,SPI机制广泛用于加载各种扩展点的具体实现。
实现原理
Dubbo会扫描指定路径下的META-INF/dubbo目录 或者 META-INF/dubbo/internal目录下的配置文件(如图),根据文件中的内容找到对应接口的所有实现类,并在需要的时候加载它们。
示例在Dubbo中定义了Protocol接口作为RPC通信层的一个扩展点,不同的协议(如dubbo、http等)都提供了具体的实现类,在启动时会dubbo会根据具体的协议配置,动态加载相应的实现,从而达到支持多种协议。
接口 Protocol#export 源码:
@Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
当使用注册中心(如zk)以及默认协议时,URL中的协议会首先为Registry,所以调用export接口时,会在META-INF的目录下找到key=registry的实现类:org.apache.dubbo.registry.integration.RegistryProtocol。而在RegistryRegistryProtocol#export方法中,URL的协议则会变为具体的dubbo,所以最后实现export发布逻辑的是key=dubbo的org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol#export方法。
- 动态加载:Java标准SPI会在类加载时一次性实例化所有服务提供者的实现。Dubbo仅在需要使用某个扩展点时才进行实例化,通过反射和工厂模式实现按需加载和初始化;
- 扩展点的发现获取:Dubbo通过ExtensionLoader类管理各个扩展点的生命周期,它可以根据接口类型查找并加载对应的服务提供者配置文件中的实现类。ExtensionLoader提供了根据名称精确获取指定扩展实现的功能,这是Java标准SPI不支持的;
- 依赖注入与IOC容器支持:Dubbo为扩展点的实现类提供了依赖注入功能,使得扩展可以方便地与其他扩展点或框架内的其他组件交互;
- 动态字节码生成优化:在一些场景下,Dubbo还利用了如Javassist这样的字节码操作库来动态生成代码,以提高运行时的性能或者简化扩展点的开发过程。这种机制可能被用来创建代理类或者策略选择逻辑等,使得扩展点的选择和调用更加灵活高效;
- 多级扩展点层次:Dubbo允许扩展点之间存在嵌套关系,例如过滤器链、路由规则等,这些复杂的关系可以通过SPI机制进行管理,构建出更复杂的业务逻辑。
Dubbo的微内核架构结合SPI机制实现了最大程度上的解耦和扩展性,可以通过编写自定义扩展点来满足特定需求,而无需修改Dubbo的核心代码,这极大地提高了框架的适应性和维护性。
在系统设计阶段首先定义清晰的接口规范(包括数据结构、函数签名等),然后再根据接口规范去实现相应的接口和组件。
Dubbo中服务提供者把服务的接口注册到注册中心,而消费者调用时,只需要知道要调用的接口即可,并不需要关心提供者的具体是如何实现的。而服务提供者可以根据业务需求的变化而更新,且不会影响到消费者。这种设计正是基于接口驱动的设计理念。
- 高层模块不应该依赖底层模块,两者都应该依赖于抽象(接口或者抽象类);
Dubbo中,服务提供者具体实现依赖于业务接口定义,消费者不直接依赖于服务提供者的具体实现,而是依赖服务的接口定义,也正符合了依赖倒置的原则。降低了模块之间的耦合度,提高了系统的可扩展性和维护性。
Dubbo不仅提供了远程服务调用功能,还在设计之初就考虑了与多种容器(如Spring、Jetty等)的深度集成,并支持面向切面编程(AOP)的能力。
- Spring集成:Dubbo通过org.apache.dubbo.container.spring.SpringContainer实现了与Spring框架的集成。可以直接在Spring配置文件中声明和管理Dubbo的相关组件,如服务提供者、消费者、协议、注册中心等。还支持Spring的依赖注入,允许服务实例通过Spring容器进行生命周期管理;
- 其他容器:除了Spring外,Dubbo还支持其他容器(如Jetty容器,用于嵌入式HTTP服务器场景)。此外,Dubbo还可以与日志容器(如Log4j Container)集成,统一日志管理。
- 扩展点机制与Wrapper:如第一部分所述,Dubbo通过的SPI机制实现了灵活的AOP功能。当服务提供者或消费者被实例化时,Dubbo可以通过Wrapper链的方式包装扩展点的实现类,在调用实际业务逻辑前后添加额外的横切逻辑,例如监控统计、权限验证、事务控制等。如:ProtocolFilterWrapper(过滤器链的包装类)、 ProtocolListenerWrapper (协议监听的包装类)、 QosProtocolWrapper(网络延迟和阻塞服务的包装类);
- 自定义扩展:根据Dubbo的SPI机制可以编写自定义的Wrapper类,这些Wrapper类必须实现特定的接口并遵循一定的命名规则(通常以Wrapper结尾)。在服务加载时,Dubbo会自动发现并应用这些Wrapper,从而实现AOP的效果;
- 依赖注入与拦截器:Dubbo内部也实现了类似于Spring IOC的功能,能够在实例化服务时完成依赖注入。同时,Dubbo还支持通过拦截器(Interceptor)的形式来实现AOP,允许开发者在服务调用的不同阶段(如before、after、around等)插入自定义逻辑。
- a. 服务接口层(Service Interface):该层是与实体业务逻辑相关的接口,定义了服务的边界;b. 配置层(Configuration):对外配置接口,以 ServiceConfig 和 ReferenceConfig 为中心,可以用来构造已注册的服务配置信息和消费者的引用配置信息;c. 服务代理层(Service Proxy):负责服务接口的代理实现,生成服务的客户端 Stub 和服务端的 Skeleton;d. 服务注册层(Registry):负责服务的注册与发现,以服务 URL 的形式;e. 集群层(Cluster):负责负载均衡、失败重试、高可用等,用来连接不同的服务提供者;f. 协议层(Protocol):负责服务到协议的映射,封装服务提供者的通信机制;g. 系统层(Remote):负责远程通讯,提供同步、异步调用机制,以及网络数据的传输;h. 服务端框架层(Server):服务提供者的框架支持,包括服务的导出和监听;i. 客户端框架层(Client):服务消费者的框架支持,包括服务的引用和调用。
- 子模块拆分:在一个大型项目中,可以根据业务逻辑将不同的服务拆分成多个独立的模块或子项目:
- dubbo-common -- 存放 Dubbo 常用的类,比如 URL 等;
- dubbo-remoting -- 存放 Dubbo 网络通信相关的类,比如 Exchange 等;
- dubbo-rpc -- 存放 Dubbo 远程调用相关的类,比如 Protocol 等;
- dubbo-cluster -- 存放 Dubbo 集群容错相关的类,比如 Cluster 等;
- dubbo-registry -- 存放 Dubbo 服务注册与发现相关的类,比如 RegistryFactory 等;
- dubbo-config -- 存放 Dubbo 配置相关的类,比如 ServiceConfig 等;
- 服务注册与发现:通过注册中心来负责服务接口的发布和调用,降低了服务提供者和消费者之间的耦合度;
- 远程调用:Dubbo通过RPC机制,使得服务调用对开发者来说就像是本地调用一样简单,隐藏了底层网络通信、序列化、反序列化等细节,降低了模块间的耦合度;
- 服务版本化与分组:Dubbo支持服务多版本并存和分组管理,允许服务升级过程中新旧版本并行,消费者可以选择绑定特定版本的服务,降低了不同版本服务的耦合度。
- 线程池模型:Dubbo 在服务提供方和服务消费方都支持通过配置线程池来控制并发执行。服务提供者可以通过设置executor配置项指定不同的线程模型(如固定大小线程池、缓存线程池等),以处理来自不同消费者的服务请求。消费方在调用远程服务时,也可以通过配置 consumer 端的线程池来控制并发调用的策略;
- 服务限流与熔断:Dubbo 提供了服务级别的流量控制能力,例如可以使用令牌桶算法或漏桶算法对并发请求数量进行限制,防止瞬时高峰流量压垮服务提供者。此外,还提供了熔断机制,在检测到服务提供者的错误率超过阈值时,自动开启熔断状态,拒绝部分请求或者快速失败,以保护服务的稳定性;
- 权重分配与负载均衡:Dubbo 内置了多种负载均衡策略,并可以根据服务提供者的权重动态分配请求,也支持扩展自定义的负载均衡策略:
- Random LoadBalance:随机 策略。可以动态调节提供者的权重
- RoundRobin LoadBalance:轮循 策略。加了权重的轮询
- LeastActive LoadBalance:最小活跃 策略。如果每个提供者的活跃数相同,则随机选择一个
- ConsistentHash LoadBalance:一致性Hash 策略。相同参数的请求总是发到同一提供者,如果提供者宕机,则会基于虚拟节点发送到其他提供者
- 异步调用:Dubbo 支持服务端同步返回和异步回调两种模式。在消费者端,只需要在接口方法上通过async="true"的方式配置为异步调用,这样调用该方法时将立即返回一个Future对象,当服务提供方处理完请求后,会通过回调机制通知消费者;
- 基于默认Future类:在异步调用场景下,Dubbo 使用DefaultFuture类来存储异步调用的结果,并通过事件监听机制保证结果数据能够正确地传递给消费者。消费者可以在需要用到结果时通过Future.get()方法获取实际的响应结果;
- 异步转同步:虽然Dubbo支持异步调用,但有时也需要在异步环境下模拟同步调用行为,这时可通过DefaultFuture.get()方法阻塞当前线程直到服务响应返回,这种情况下,Dubbo实际上是将异步调用转换为了同步调用。
- Failover Cluster:故障切换策略,当消费者调用某个提供者失败时,会自动切换到其他可用的提供者。可以通过配置retries参数来控制重试次数;
- Failfast Cluster:快速失败策略,一旦调用失败立即返回错误,不进行重试,适合对响应速度要求高且不允许失败重试的场景;
- Failsafe Cluster:失败安全策略,出现故障时不抛出异常,而是直接返回一个默认值或者空值,确保不影响主流程,通常用于非关键业务或可容忍失败的读操作;
- Failback Cluster:失败自动恢复策略,在调用失败后记录失败请求,并定时尝试重新发起调用,直到成功为止;
- Forking Cluster:并行调用多个提供者,所有调用都完成后合并结果集。
- 连接超时与重试机制:Dubbo 支持设置接口调用的超时时间(timeout),超过这个时间即为失败,然后根据容错策略决定是否进行重试;
- 负载均衡与隔离:通过多种负载均衡策略保证各个提供者的压力分布相对均匀,同时配合线程池和连接池等方式进行资源隔离,防止雪崩效应。
- 流量控制与熔断:Dubbo 具有基于令牌桶算法或漏斗算法的限流功能,可以限制服务提供者的并发调用量,防止过载。此外,还支持熔断机制,在一定时间内连续失败次数达到阈值时,触发熔断状态,暂时停止对该服务的调用,一段时间后再自动恢复调用,起到保护系统整体稳定的作用;
- 系统自适应保护:Dubbo 提供了一种自我保护机制,当系统检测到由于瞬时压力过大导致服务提供者响应变慢时,为了避免雪崩效应,会启动自我保护模式,临时拒绝部分请求,等待服务端恢复正常后再接受更多请求。
Dubbo 中主要运用的设计模式:
工厂模式(Factory Pattern): 例如:创建invoker对象时,使用工厂模式来创建不同的 Invoker 对象。在 Protocol 接口中,有多个实现类,使用工厂方法模式来创建这些实现类的实例;
代理模式(Proxy Pattern): 例如:使用代理模式来在客户端创建服务的代理,从而进行远程调用;
装饰器模式(Decorator Pattern): 例如:Dubbo 的 Invoker 体系中使用了装饰器模式,如 RpcInvocation 装饰 Invocation;
适配器模式(Adapter Pattern): 例如:Dubbo 的 Filter 链使用了适配器模式,将用户定义的 Filter 适配到 Dubbo 的 Filter 接口;
观察者模式(Observer Pattern): 例如:Dubbo 的 EventListener 接口使用了观察者模式,用于监听和处理事件;
组合模式(Composite Pattern): 例如:Dubbo 的 URL 模型使用了组合模式,用于表示层次化的 URL 结构;
策略模式(Strategy Pattern): 例如:Dubbo 的 Cluster 接口定义了多种负载均衡策略;
单例模式(Singleton Pattern): 例如:Dubbo 的 Config 类使用了单例模式,确保全局只有一个 Config 实例。
在我们考虑设计自己的软件系统时,也可以借鉴dubbo框架的这些优秀的设计:
- 模块化拆分,分清楚核心功能和非核心功能,对功能进行模块化的拆分;
- 最小可行性,MVP原则,先完成核心功能,再慢慢丰富;
- 可扩展,设计方便扩展的实现,对于非核心功能和个性化业务,都通过扩展的方式实现;
- 低耦合,模块与模块之间,以及系统与他系统之间,都要实现低耦合;
- 容错性,是否需要支持限流和熔断,以及调用失败之后的处理方案;
- 设计模式,合理的运用设计模式,可以使代码有更好的扩展性、可维护性。但不能乱用。