写点什么

Dubbo 核心技术

作者:苏格拉格拉
  • 2022-11-04
    浙江
  • 本文字数:5290 字

    阅读完需:约 17 分钟

Dubbo 是一款高性能、轻量级的开源 RPC 框架,提供服务自动注册、自动发现等高效服务治理方案, 可以和 Spring 框架无缝集成。

总体架构

  • Provider:暴露服务的服务提供方

  • Consumer:调用远程服务消费方

  • Registry:服务注册与发现注册中心

  • Monitor:监控中心和访问调用统计

  • Container:服务运行容器


Dubbo 服务器注册与发现的流程

  • 服务容器负责启动,加载,运行服务提供者。

  • 服务提供者在启动时,向注册中心注册自己提供的服务。

  • 服务消费者在启动时,向注册中心订阅自己所需的服务。

  • 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。

  • 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

  • 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

代码架构

各层说明

  • Config 配置层:对外配置接口,以 ServiceConfig , ReferenceConfig 为中心,可以直接初始化配置类,也可以通过 spring 解析配置生成配置类

  • Proxy 服务代理层:服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton, 以 ServiceProxy 为中心,扩展接口为 ProxyFactory

  • Registry 注册中心层:封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 RegistryFactory , Registry , RegistryService

  • Cluster 路由层:封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口为 Cluster , Directory , Router , LoadBalance

  • Monitor 监控层:RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory , Monitor , MonitorService

  • Protocol 远程调用层:封装 RPC 调用,以 Invocation , Result 为中心,扩展接口为 Protocol , Invoker , Exporter

  • Exchange 信息交换层:封装请求响应模式,同步转异步,以 Request , Response 为中心,扩展接口为 Exchanger , ExchangeChannel , ExchangeClient , ExchangeServer

  • Transport 网络传输层:抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel , Transporter , Client , Server , Codec

  • Serialize 数据序列化层:可复用的一些工具,扩展接口为 Serialization , ObjectInput , ObjectOutput , ThreadPool

注册发现

可选:Redis、Zookeeper、Consul 、Etcd。一般使用 ZooKeeper 提供服务注册与发现功能,解决单点故障以及分布式部署的问题(注册中心)。

通信协议

通信框架

默认使用 Netty 作为通讯框架。

协议

  • Dubbo: 单一长连接和 NIO 异步通讯,适合大并发小数据量的服务调用,以及消费者远大于提供者。传输协议 TCP,异步 Hessian 序列化。Dubbo 推荐使用 dubbo 协议。

  • RMI: 采用 JDK 标准的 RMI 协议实现,传输参数和返回参数对象需要实现 Serializable 接口,使用 Java 标准序列化机制,使用阻塞式短连接,传输数据包大小混合,消费者和提供者个数差不多,可传文件,传输协议 TCP。 多个短连接 TCP 协议传输,同步传输,适用常规的远程服务调用和 RMI 互操作。在依赖低版本的 Common-Collections 包,Java 序列化存在安全漏洞。

  • WebService:基于 WebService 的远程调用协议,集成 CXF 实现,提供和原生 WebService 的互操作。多个短连接,基于 HTTP 传输,同步传输,适用系统集成和跨语言调用。

  • HTTP: 基于 Http 表单提交的远程调用协议,使用 Spring 的 HttpInvoke 实现。多个短连接,传输协议 HTTP,传入参数大小混合,提供者个数多于消费者,需要给应用程序和浏览器 JS 调用。 Hessian:集成 Hessian 服务,基于 HTTP 通讯,采用 Servlet 暴露服务,Dubbo 内嵌 Jetty 作为服务器时默认实现,提供与 Hession 服务互操作。多个短连接,同步 HTTP 传输,Hessian 序列化,传入参数较大,提供者大于消费者,提供者压力较大,可传文件。

  • Memcache:基于 Memcache 实现的 RPC 协议。

  • Redis:基于 Redis 实现的 RPC 协议。

序列化框架

默认使用 Hessian 序列化,还有 Dubbo、FastJson、Java 自带序列化。

负载均衡

  • Random 随机选取提供者策略,有利于动态调整提供者权重。截面碰撞率高,调用次数越多,分布越均匀。

  • RoundRobin 轮循选取提供者策略,平均分布,但是存在请求累积的问题。

  • LeastActive 最少活跃调用策略,解决慢提供者接收更少的请求。

  • ConstantHash 一致性 Hash 策略,使相同参数请求总是发到同一提供者,一台机器宕机,可以基于虚拟节点,分摊至其他提供者,避免引起提供者的剧烈变动。

集群容错

  • Failover Cluster:失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。

  • Failfast Cluster:快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。

  • Failsafe Cluster:失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。

  • Failback Cluster:失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。

  • Forking Cluster:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2″ 来设置最大并行数。

  • Broadcast Cluster:广播调用所有提供者,逐个调用,任意一台报错则报错 。通常用于通知所有提供者更新缓存或日志等本地资源信息。

SPI

Java SPI VS Dubbo SPI

Dubbo 的扩展点加载从 JDK 标准的 SPI (Service Provider Interface) 扩展点发现机制加强而来。

Dubbo 改进了 JDK 标准的 SPI 的以下问题:

  • JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。

  • 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,会报不支持 ruby,而不是真正失败的原因。

  • 增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。

使用方法

首先在 META-INF/dubbo 目录下按接口全限定名建立一个文件,内容如下:

optimusPrime = org.apache.spi.OptimusPrimebumblebee = org.apache.spi.Bumblebee
复制代码

然后在接口上标注 @SPI 注解,以表明它要用 SPI 机制,类似下面这个图(我就是拿 Cluster 的图举个例子,和这个示例代码定义的接口不一样)。

接着通过下面的示例代码即可加载指定的实现类。

扩展点特性

扩展点自动包装(AOP)

自动包装扩展点的 Wrapper 类。 ExtensionLoader 在加载扩展点时,如果加载到的扩展点有拷贝构造函数,则判定为扩展点 Wrapper 类。

Wrapper 类内容:

package com.alibaba.xxx;import org.apache.dubbo.rpc.Protocol;
public class XxxProtocolWrapper implements Protocol { Protocol impl;
public XxxProtocolWrapper(Protocol protocol) { impl = protocol; }
// 接口方法做一个操作后,再调用extension的方法 public void refer() { //... 一些操作 impl.refer(); // ... 一些操作 }
// ... }
复制代码


Wrapper 类同样实现了扩展点接口,但是 Wrapper 不是扩展点的真正实现。它的用途主要是用于从 ExtensionLoader 返回扩展点时,包装在真正的扩展点实现外。即从 ExtensionLoader 中返回的实际上是 Wrapper 类的实例,Wrapper 持有了实际的扩展点实现类。


扩展点的 Wrapper 类可以有多个,也可以根据需要新增。


通过 Wrapper 类可以把所有扩展点公共逻辑移至 Wrapper 中。新加的 Wrapper 在所有的扩展点上添加了逻辑,有些类似 AOP,即 Wrapper 代理了扩展点。


扩展点自动装配(IOC)

加载扩展点时,自动注入依赖的扩展点。加载扩展点时,扩展点实现类的成员如果为其它扩展点类型, ExtensionLoader 在会自动注入依赖的扩展点。 ExtensionLoader 通过扫描扩展点实现类的所有 setter 方法来判定其成员。即 ExtensionLoader 会执行扩展点的拼装操作。

示例:有两个为扩展点 CarMaker (造车者)、 WheelMaker (造轮者) 接口类如下:


public interface CarMaker {            Car makeCar();          }
public interface WheelMaker { Wheel makeWheel(); }
复制代码


CarMaker 的一个实现类:

public class RaceCarMaker implements CarMaker {            WheelMaker wheelMaker;
public void setWheelMaker(WheelMaker wheelMaker) { this.wheelMaker = wheelMaker; }
public Car makeCar() { // ... Wheel wheel = wheelMaker.makeWheel(); // ... return new RaceCar(wheel, ...); } }
复制代码


ExtensionLoader 加载 CarMaker 的扩展点实现 RaceCarMaker 时, setWheelMaker 方法的 WheelMaker 也是扩展点则会注入 WheelMaker 的实现。

这里带来另一个问题, ExtensionLoader 要注入依赖扩展点时,如何决定要注入依赖扩展点的哪个实现。在这个示例中,即是在多个 WheelMaker 的实现中要注入哪个。

这个问题由扩展点自适应解决。

扩展点自适应

ExtensionLoader 注入的依赖扩展点是一个 Adaptive 实例,直到扩展点方法执行时才决定调用是哪一个扩展点实现。

Dubbo 使用 URL 对象(包含了 Key-Value)传递配置信息。

扩展点方法调用会有 URL 参数(或是参数有 URL 成员)

这样依赖的扩展点也可以从 URL 拿到配置信息,所有的扩展点自己定好配置的 Key 后,配置信息从 URL 上从最外层传入。URL 在配置传递上即是一条总线。

示例:有两个为扩展点 CarMaker 、 WheelMaker

接口类如下:

public interface CarMaker {            Car makeCar(URL url);          }
public interface WheelMaker { Wheel makeWheel(URL url); }
复制代码


CarMaker 的一个实现类:

public class RaceCarMaker implements CarMaker {            WheelMaker wheelMaker;
public void setWheelMaker(WheelMaker wheelMaker) { this.wheelMaker = wheelMaker; }
public Car makeCar(URL url) { // ... Wheel wheel = wheelMaker.makeWheel(url); // ... return new RaceCar(wheel, ...); } }
复制代码


当上面执行

// ...          Wheel wheel = wheelMaker.makeWheel(url);          // ...
复制代码

时,注入的 Adaptive 实例可以提取事先定义好的 Key 来决定使用哪个 WheelMaker 实现来调用对应实现的真正的 makeWheel 方法。如提取 wheel.type Key,即 url.get("wheel.type") 来决定 WheelMaker 实现。 Adaptive 实例的逻辑是固定的,从 URL 中提取事先定义好的 Key,动态生成真正的实现并执行它。

ExtensionLoader 里面的扩展点注入的 Adaptive 实现是在 dubbo 加载扩展点时动态生成的。Key 是从 URL 中获取的,而 URL 中 Key 的值是在扩展点接口的方法定义上通过 @Adaptive 注解提供的。 下面是 Dubbo 的 Transporter 扩展点的代码:

public interface Transporter {            @Adaptive({"server", "transport"})            Server bind(URL url, ChannelHandler handler) throws RemotingException;
@Adaptive({"client", "transport"}) Client connect(URL url, ChannelHandler handler) throws RemotingException; }
复制代码

对于 bind() 方法,Adaptive 实现先查找 server key,如果该 Key 没有值则找 transport key 值,来决定代理到哪个实际扩展点。

扩展点自动激活

对于集合类扩展点,比如: Filter , InvokerListener , ExportListener , TelnetHandler , StatusChecker 等,可以同时加载多个实现,此时,可以用自动激活来简化配置,如:

import org.apache.dubbo.common.extension.Activate;          import org.apache.dubbo.rpc.Filter;
@Activate // 无条件自动激活 public class XxxFilter implements Filter { // ... }
复制代码

或:

import org.apache.dubbo.common.extension.Activate;          import org.apache.dubbo.rpc.Filter;
@Activate("xxx") // 当配置了xxx参数,并且参数为有效值时激活,比如配了cache="lru",自动激活CacheFilter。 public class XxxFilter implements Filter { // ... }
复制代码


或:

import org.apache.dubbo.common.extension.Activate;          import org.apache.dubbo.rpc.Filter;
@Activate(group = "provider", value = "xxx") // 只对提供方激活,group可选"provider"或"consumer" public class XxxFilter implements Filter { // ... }
复制代码


参考

https://cloud.tencent.com/developer/article/1465448

https://dubbo.apache.org/zh/doc

发布于: 刚刚阅读数: 4
用户头像

还未添加个人签名 2018-08-22 加入

还未添加个人简介

评论

发布
暂无评论
Dubbo核心技术_分布式_苏格拉格拉_InfoQ写作社区