Skip to content

Dubbo ​

1. RPC ​

1.1 RPC 定义 ​

互联网公司的系统有成千上万个大大小小的服务组成,服务各自部署在不同的机器上,服务间的调用需要用到网络通信,服务消费方每调用一个服务都要写一坨网络通信相关的代码,不仅复杂而且极易出错。还要考虑新服务依赖老服务时如何调用老服务,别的服务依赖新服务的时候新服务如何发布方便他人调用。如何解决这个问题呢?业界一般采用RPC远程调用的方式来实现。

RPC:

Remote Procedure Call Protocol 既 远程过程调用,一种能让我们像调用本地服务一样调用远程服务,可以让调用者对网络通信这些细节无感知,比如服务消费方在执行 helloWorldService.sayHello("sowhat") 时,实质上调用的是远端的服务。这种方式其实就是RPC,RPC思想在各大互联网公司中被广泛使用,如阿里巴巴的dubbo、当当的Dubbox 、Facebook 的 thrift、Google 的grpc、Twitter的finagle等。

1.2 RPC demo ​

说了那么多,还是实现一个简易版的RPC demo吧。

1.2.1 公共接口 ​

plain
1. public interface SoWhatService {
2.     String sayHello(String name);  
3. }

1.2.2  服务提供者 ​

接口类实现

plain
1. public class SoWhatServiceImpl implements SoWhatService
2. {
3.  @Override
4.  public String sayHello(String name)
5.  {
6.   return "你好啊 " + name;
7.  }
8. }

服务注册对外提供者

plain
1. /**
2.  * 服务注册对外提供者
3.  */
4. 
5. public class ServiceFramework
6. {
7.  public static void export(Object service, int port) throws Exception
8.  {
9.   ServerSocket server = new ServerSocket(port);
10.   while (true)
11.   {
12.    Socket socket = server.accept();
13.    new Thread(() ->
14.    {
15.     try
16.     {
17.      //反序列化
18.      ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
19.      //读取方法名
20.      String methodName =(String) input.readObject();
21.      //参数类型
22.      Class<?>[] parameterTypes = (Class<?>[]) input.readObject();
23.      //参数
24.      Object[] arguments = (Object[]) input.readObject();
25.      //找到方法
26.      Method method = service.getClass().getMethod(methodName, parameterTypes);
27.      //调用方法
28.      Object result = method.invoke(service, arguments);
29.      // 返回结果
30.      ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
31.      output.writeObject(result);
32.     } catch (Exception e)
33.     {
34.      e.printStackTrace();
35.     }
36.    }).start();
37.   }
38.  }
39. }

服务运行

plain
1. public class ServerMain
2. {
3.  public static void main(String[] args)
4.  {
5.   //服务提供者 暴露出接口
6.   SoWhatService service = new SoWhatServiceImpl();
7.   try
8.   {
9.    ServiceFramework.export(service, 1412);
10.   } catch (Exception e)
11.   {
12.    e.printStackTrace();
13.   }
14.  }
15. }

1.2.3 服务调用者 ​

动态代理调用远程服务

plain
1. /**
2.  * @author sowhat
3.  * 动态代理调用远程服务
4.  */
5. public class RpcFunction
6. {
7.  public static <T> T refer(Class<T> interfaceClass, String host, int port) throws Exception
8.  {
9.   return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class<?>[]{interfaceClass},
10.     new InvocationHandler()
11.     {
12.      @Override
13.      public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable
14.      {
15.       //指定 provider 的 ip 和端口
16.       Socket socket = new Socket(host, port);
17.       ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
18.       //传方法名
19.       output.writeObject(method.getName());
20.       //传参数类型
21.       output.writeObject(method.getParameterTypes());
22.       //传参数值
23.       output.writeObject(arguments);
24.       ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
25.       //读取结果
26.       Object result = input.readObject();
27.       return result;
28.      }
29.     });
30.  }
31. }

调用方法

plain
1. public class RunMain
2. {
3.  public static void main(String[] args)
4.  {
5.   try
6.   {
7.    //服务调用者 需要设置依赖
8.    SoWhatService service = RpcFunction.refer(SoWhatService.class, "127.0.0.1", 1412);
9.    System.out.println(service.sayHello(" sowhat1412"));
10.   } catch (Exception e)
11.   {
12.    e.printStackTrace();
13.   }
14.  }
15. }

2. Dubbo 框架设计 ​

2.1  Dubbo 简介 ​

Dubbo 是阿里巴巴研发开源工具,主要分为2.6.x 跟 2.7.x 版本。是一款分布式、高性能、透明化的 RPC 服务框架,提供服务自动注册、自动发现等高效服务治理方案,可以和Spring 框架无缝集成,它提供了6大核心能力:

  1. 面向接口代理的高性能RPC调用

  2. 智能容错和负载均衡

  3. 服务自动注册和发现

  4. 高度可扩展能力

  5. 运行期流量调度

  6. 可视化的服务治理与运维

调用过程:

  1. 服务提供者 Provider 启动然后向 Registry 注册自己所能提供的服务。
  2. 服务消费者 Consumer 向Registry订阅所需服务,Consumer 解析Registry提供的元信息,从服务中通过负载均衡选择 Provider调用。
  3. 服务提供方 Provider 元数据变更的话Registry会把变更推送给Consumer,以此保证Consumer获得最新可用信息。

注意点:

  1. Provider 跟 Consumer 在内存中记录调用次数跟时间,定时发送统计数据到Monitor,发送的时候是短连接。
  2. Monitor 跟 Registry 是可选的,可直接在配置文件中写好,Provider 跟 Consumer进行直连。
  3. Monitor 跟 Registry 挂了也没事, Consumer 本地缓存了 Provider 信息。
  4. Consumer 直接调用 Provider 不会经过 Registry。Provider、Consumer这俩到 Registry之间是长连接。

2.2 Dubbo框架分层 ​

如上图,总的而言 Dubbo 分为三层。

  1. Busines层:由用户自己来提供接口和实现还有一些配置信息。
  2. RPC层:真正的RPC调用的核心层,封装整个RPC的调用过程、负载均衡、集群容错、代理。
  3. Remoting层:对网络传输协议和数据转换的封装。

如果每一层再细分下去,一共有十层。

  1. 接口服务层(Service):该层与业务逻辑相关,根据 provider 和 consumer 的业务设计对应的接口和实现。
  2. 配置层(Config):对外配置接口,以 ServiceConfig 和 ReferenceConfig 为中心初始化配置。
  3. 服务代理层(Proxy):服务接口透明代理,Provider跟Consumer都生成代理类,使得服务接口透明,代理层实现服务调用跟结果返回。
  4. 服务注册层(Registry):封装服务地址的注册和发现,以服务 URL 为中心。
  5. 路由层(Cluster):封装多个提供者的路由和负载均衡,并桥接注册中心,以Invoker 为中心,扩展接口为 Cluster、Directory、Router 和 LoadBlancce。
  6. 监控层(Monitor):RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory、Monitor 和 MonitorService。
  7. 远程调用层(Protocal):封装 RPC 调用,以 Invocation 和 Result 为中心,扩展接口为 Protocal、Invoker 和 Exporter。
  8. 信息交换层(Exchange):封装请求响应模式,同步转异步。以 Request 和Response 为中心,扩展接口为 Exchanger、ExchangeChannel、ExchangeClient 和 ExchangeServer。
  9. 网络传输层(Transport):抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel、Transporter、Client、Server 和 Codec。
  10. 数据序列化层(Serialize):可复用的一些工具,扩展接口为 Serialization、ObjectInput、ObjectOutput 和 ThreadPool。

他们之间的调用关系直接看下面官网图即可。

3. Dubbo SPI 机制 ​

Dubbo 采用 微内核设计 + SPI 扩展技术来搭好核心框架,同时满足用户定制化需求。这里重点说下SPI。

3.1 微内核 ​

操作系统层面的微内核跟宏内核:

  1. 微内核Microkernel:是一种内核的设计架构,由尽可能精简的程序所组成,以实现一个操作系统所需要的最基本功能,包括了底层的寻址空间管理、线程管理、与进程间通信。成功案例是QNX系统,比如黑莓手机跟车用市场。
  2. 宏内核Monolithic :把 进程管理、内存管理、文件系统、进程通信等功能全部作为内核来实现,而微内核则仅保留最基础的功能,Linux 就是宏内核架构设计。

Dubbo中的广义微内核:

  1. 思想是 核心系统 + 插件,说白了就是把不变的功能抽象出来称为核心,把变动的功能作为插件来扩展,符合开闭原则,更容易扩展、维护。比如小霸王游戏机中机体本身作为核心系统,游戏片就是插件。vscode、Idea、chrome等都是微内核的产物。
  2. 微内核架构其实是一直架构思想,可以是框架层面也可以是某个模块设计,它的本质就是将变化的部分抽象成插件,使得可以快速简便地满足各种需求又不影响整体的稳定性。

3.2 SPI 含义 ​

主流的数据库有MySQL、Oracle、DB2等,这些数据库是不同公司开发的,它们的底层协议不大一样,那怎么约束呢?一般就是定制统一接口,具体实现不管,反正面向相同的接口编程即可。等到真正使用的时候用具体的实现类就好,问题是哪里找用那个实现类呢?这时候就采用约定好的法则将实现类写到指定位置即可。

SPI 全称为 Service Provider Interface,是一种服务发现机制。它约定在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。

3.3 SPI demo ​

接口:

plain
1. package com.example.demo.spi;
2. 
3. public interface SPIService {
4.     void execute();
5. }

实现类1:

plain
1. public class SpiImpl1 implements SPIService{
2.  @Override
3.     public void execute() {
4.         System.out.println("SpiImpl1.execute()");
5.     }
6. }

实现类2:

plain
1. public class SpiImpl2 implements SPIService{
2.  @Override
3.     public void execute() {
4.   System.out.println("SpiImpl2.execute()");
5.     }
6. }

配置路径

调用加载类

plain
1. package com.example.demo.spi;
2. import sun.misc.Service;
3. import java.util.Iterator;
4. import java.util.ServiceLoader;
5. 
6. public class Test {
7.     public static void main(String[] args) {    
8.         Iterator<SPIService> providers = Service.providers(SPIService.class);
9.         ServiceLoader<SPIService> load = ServiceLoader.load(SPIService.class);
10. 
11.         while(providers.hasNext()) {
12.             SPIService ser = providers.next();
13.             ser.execute();
14.         }
15.         System.out.println("--------------------------------");
16.         Iterator<SPIService> iterator = load.iterator();
17.         while(iterator.hasNext()) {
18.             SPIService ser = iterator.next();
19.             ser.execute();
20.         }
21.     }
22. }

3.4 SPI源码追踪 ​

ServiceLoader.load(SPIService.class) 底层调用大致逻辑如下:

iterator.hasNext() 跟 iterator.next()底层调用大致如下:

3.5 Java SPI缺点 ​

  1. 不能按需加载,Java SPI在加载扩展点的时候,会一次性加载所有可用的扩展点,很多是不需要的,会浪费系统资源。
  2. 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
  3. 不支持AOP与依赖注入,JAVA SPI可能会丢失加载扩展点异常信息,导致追踪问题很困难。

3.6 Dubbo SPI ​

JDK自带的不好用Dubbo 就自己实现了一个 SPI,该SPI 可以通过名字实例化指定的实现类,并且实现了 IOC 、AOP 与 自适应扩展 SPI。

plain
key = com.sowhat.value

Dubbo 对配置文件目录的约定,不同于 Java SPI ,Dubbo 分为了三类目录。

  1. META-INF/services/ :该目录下 SPI 配置文件是为了用来兼容 Java SPI 。
  2. META-INF/dubbo/ :该目录存放用户自定义的 SPI 配置文件。
  3. META-INF/dubbo/internal/ :该目录存 Dubbo 内部使用的 SPI 配置文件。

使用的话很简单 引入依赖,然后百度教程即可。

java
1. @Test
2.  void sowhat()
3.  {
4.   ExtensionLoader<SPIService> spiService = ExtensionLoader.getExtensionLoader(SPIService.class);        //按需获取实现类对象
5.   SPIService demo1 = spiService.getExtension("SpiImpl1");
6.   demo1.execute();
7.  }

3.7 Dubbo SPI源码追踪 ​

**ExtensionLoader.getExtension **方法的整个思路是 查找缓存是否存在,不存在则读取SPI文件,通过反射创建类,然后设置依赖注入这些东西,有包装类就包装下,执行流程如下图所示:

说下重要的四个部分:

  1. injectExtension  IOC

查找 set 方法,根据参数找到依赖对象则注入。

  1. WrapperClass AOP

包装类,Dubbo 帮你自动包装,只需要某个扩展类的构造函数只有一个参数,并且是扩展接口类型,就会被判定为包装类。

  1. Activate

Active 有三个属性,group 表示修饰在哪个端,是 provider 还是 consumer,value 表示在 URL参数中出现才会被激活,order 表示实现类的顺序。

3.8 Adaptive  自适应扩展 ​

需求:根据配置来进行 SPI 扩展的加载后不想在启动的时候让扩展被加载,想根据请求时候的参数来动态选择对应的扩展。实现:Dubbo用代理机制实现了自适应扩展,为用户想扩展的接口 通过JDK 或者 Javassist 编译生成一个代理类,然后通过反射创建实例。实例会根据本来方法的请求参数得知需要的扩展类,然后通过 ExtensionLoader.getExtensionLoader(type.class).getExtension(name)来获取真正的实例来调用,看个官网样例。

plain
1. public interface WheelMaker {
2.     Wheel makeWheel(URL url);
3. }
4. // WheelMaker 接口的自适应实现类
5. public class AdaptiveWheelMaker implements WheelMaker {
6.     public Wheel makeWheel(URL url) {
7.         if (url == null) {
8.             throw new IllegalArgumentException("url == null");
9.         }
10.      // 1. 调用 url 的 getXXX 方法获取参数值
11.         String wheelMakerName = url.getParameter("Wheel.maker");
12.         if (wheelMakerName == null) {
13.             throw new IllegalArgumentException("wheelMakerName == null");
14.         }
15.         // 2. 调用 ExtensionLoader 的 getExtensionLoader 获取加载器
16.         // 3. 调用 ExtensionLoader 的 getExtension 根据从url获取的参数作为类名称加载实现类
17.         WheelMaker wheelMaker = ExtensionLoader.getExtensionLoader(WheelMaker.class).getExtension(wheelMakerName);
18.         // 4. 调用实现类的具体方法实现调用。
19.         return wheelMaker.makeWheel(URL url);
20.     }
21. }

查看Adaptive注解源码可知该注解可用在类或方法上,Adaptive 注解在类上或者方法上有不同的实现逻辑。

3.8.1 Adaptive 注解在类上 ​

Adaptive 注解在类上时,Dubbo 不会为该类生成代理类,Adaptive 注解在类上的情况很少,在 Dubbo 中,仅有两个类被 Adaptive 注解了,分别是 AdaptiveCompiler 和 AdaptiveExtensionFactory,表示拓展的加载逻辑由人工编码完成,这不是我们关注的重点。

3.8.2 Adaptive 注解在方法上 ​

Adaptive 注解在方法上时,Dubbo 则会为该方法生成代理逻辑,表示拓展的加载逻辑需由框架自动生成,大致的实现机制如下:

  1. 加载标注有 @Adaptive 注解的接口,如果不存在,则不支持 Adaptive 机制;
  2. 为目标接口按照一定的模板生成子类代码,并且编译生成的代码,然后通过反射生成该类的对象;
  3. 结合生成的对象实例,通过传入的URL对象,获取指定key的配置,然后加载该key对应的类对象,最终将调用委托给该类对象进行。
plain
1. @SPI("apple")
2. public interface FruitGranter {
3.   Fruit grant();
4.   @Adaptive
5.   String watering(URL url);
6. }
7. ---
8. // 苹果种植者
9. public class AppleGranter implements FruitGranter {
10.   @Override
11.   public Fruit grant() {
12.     return new Apple();
13.   }
14.   @Override
15.   public String watering(URL url) {
16.     System.out.println("watering apple");
17.     return "watering finished";
18.   }
19. }
20. ---
21. // 香蕉种植者
22. public class BananaGranter implements FruitGranter {
23.   @Override
24.   public Fruit grant() {
25.     return new Banana();
26.   }
27.   @Override
28.   public String watering(URL url) {
29.     System.out.println("watering banana");
30.     return "watering success";
31.   }
32. }

调用方法实现:

plain
1. public class ExtensionLoaderTest {
2.   @Test
3.   public void testGetExtensionLoader() {
4.     // 首先创建一个模拟用的URL对象
5.     URL url = URL.valueOf("dubbo://192.168.0.1:1412?fruit.granter=apple");
6.     // 通过ExtensionLoader获取一个FruitGranter对象
7.     FruitGranter granter = ExtensionLoader.getExtensionLoader(FruitGranter.class)
8.       .getAdaptiveExtension();
9.     // 使用该FruitGranter调用其"自适应标注的"方法,获取调用结果
10.     String result = granter.watering(url);
11.     System.out.println(result);
12.   }
13. }

通过如上方式生成一个内部类。大致调用流程如下:

4. Dubbo 服务暴露流程 ​

4.1 服务暴露总览 ​

Dubbo框架是以URL为总线的模式,运行过程中所有的状态数据信息都可以通过URL来获取,比如当前系统采用什么序列化,采用什么通信,采用什么负载均衡等信息,都是通过URL的参数来呈现的,所以在框架运行过程中,运行到某个阶段需要相应的数据,都可以通过对应的Key从URL的参数列表中获取。URL 具体的参数如下:

protocol:指的是 dubbo 中的各种协议,如:dubbo thrift http username/password:用户名/密码 host/port:主机/端口 path:接口的名称 parameters:参数键值对

plain
protocol://username:password@host:port/path?k=v

服务暴露从代码流程看分为三部分:

  1. 检查配置,最终组装成 URL。
  2. 暴露服务到到本地服务跟远程服务。
  3. 服务注册至注册中心。

服务暴露从对象构建转换看分为两步:

  1. 将服务封装成Invoker。
  2. 将Invoker通过协议转换为Exporter。

4.2  服务暴露源码追踪 ​

  1. 容器启动,Spring IOC 刷新完毕后调用 onApplicationEvent 开启服务暴露,ServiceBean 。
  2. export 跟 doExport 来进行拼接构建URL,为屏蔽调用的细节,统一暴露出一个可执行体,通过ProxyFactory 获取到 invoker。
  3. 调用具体 Protocol 将把包装后的 invoker 转换成 exporter,此处用到了SPI。
  4. 然后启动服务器server,监听端口,使用NettyServer创建监听服务器。
  5. 通过 RegistryProtocol 将URL注册到注册中心,使得consumer可获得provider信息。

5. Dubbo 服务引用流程 ​

Dubbo中一个可执行体就是一个invoker,所以 provider 跟 consumer 都要向 invoker 靠拢。通过上面demo可知为了无感调用远程接口,底层需要有个代理类包装 invoker。

服务的引入时机有两种:

  1. 饿汉式:

通过实现 Spring 的 InitializingBean 接口中的 afterPropertiesSet 方法,容器通过调用 ReferenceBean的 afterPropertiesSet 方法时引入服务。

  1. 懒汉式(默认):

懒汉式是只有当服务被注入到其他类中时启动引入流程。

服务引用的三种方式:

  1. 本地引入:服务暴露时本地暴露,避免网络调用开销。
  2. 直接连接引入远程服务:不启动注册中心,直接写死远程Provider地址 进行直连。
  3. 通过注册中心引入远程服务:通过注册中心抉择如何进行负载均衡调用远程服务。

服务引用流程:

  1. 检查配置构建map ,map 构建 URL ,通过URL上的协议利用自适应扩展机制调用对应的 protocol.refer 得到相应的 invoker ,此处
  2. 想注册中心注册自己,然后订阅注册中心相关信息,得到provider的 ip 等信息,再通过共享的netty客户端进行连接。
  3. 当有多个 URL 时,先遍历构建出 invoker 然后再由 StaticDirectory 封装一下,然后通过 cluster 进行合并,只暴露出一个 invoker 。
  4. 然后再构建代理,封装 invoker 返回服务引用,之后 Comsumer 调用的就是这个代理类。

调用方式:

  1. oneway:不关心请求是否发送成功。
  2. Async异步调用:Dubbo天然异步,客户端调用请求后将返回的 ResponseFuture 存到上下文中,用户可随时调用 future.get 获取结果。异步调用通过唯一ID 标识此次请求。
  3. Sync同步调用:在 Dubbo 源码中就调用了 future.get,用户感觉方法被阻塞了,必须等结果后才返回。

6. Dubbo 调用整体流程 ​

调用之前你可能需要考虑这些事:

  1. consumer 跟 provider 约定好通讯协议,dubbo支持多种协议,比如dubbo、rmi、hessian、http、webservice等。默认走dubbo协议,连接属于单一长连接,NIO异步通信。适用传输数据量很小(单次请求在100kb以内),但是并发量很高。
  2. 约定序列化模式,大致分为两大类,一种是字符型(XML或json 人可看懂 但传输效率低),一种是二进制流(数据紧凑,机器友好)。默认使用 hessian2作为序列化协议。
  3. consumer 调用 provider 时提供对应接口、方法名、参数类型、参数值、版本号。
  4. provider列表对外提供服务涉及到负载均衡选择一个provider提供服务。
  5. consumer 跟 provider 定时向monitor 发送信息。

调用大致流程:

  1. 客户端发起请求来调用接口,接口调用生成的代理类。代理类生成RpcInvocation 然后调用invoke方法。
  2. ClusterInvoker获得注册中心中服务列表,通过负载均衡给出一个可用的invoker。
  3. 序列化跟反序列化网络传输数据。通过NettyServer调用网络服务。
  4. 服务端业务线程池接受解析数据,从exportMap找到invoker进行invoke。
  5. 调用真正的Impl得到结果然后返回。

调用方式:

  1. oneway:不关心请求是否发送成功,消耗最小。
  2. sync同步调用:在 Dubbo 源码中就调用了 future.get,用户感觉方法被阻塞了,必须等结果后才返回。
  3. Async 异步调用:Dubbo天然异步,客户端调用请求后将返回的 ResponseFuture 存到上下文中,用户可以随时调用future.get获取结果。异步调用通过唯一ID标识此次请求。

7. Dubbo集群容错负载均衡 ​

Dubbo 引入了Cluster、Directory、Router、LoadBalance、Invoker模块来保证Dubbo系统的稳健性,它们的关系如下图:

  1. 服务发现时会将多个多个远程调用放入Directory,然后通过Cluster封装成一个Invoker,该invoker提供容错功能。
  2. 消费者代用的时候从Directory中通过负载均衡获得一个可用invoker,最后发起调用。
  3. 你可以认为Dubbo中的Cluster对上面进行了大的封装,自带各种鲁棒性功能。

7.1 集群容错 ​

集群容错是在消费者端通过Cluster子类实现的,Cluster接口有10个实现类,每个Cluster实现类都会创建一个对应的ClusterInvoker对象。核心思想是让用户选择性调用这个Cluster中间层,屏蔽后面具体实现细节。

ClusterCluster Invoker作用
FailoverClusterFailoverClusterInvoker失败自动切换功能,默认
FailfastClusterFailfastClusterInvoker一次调用,失败异常
FailsafeClusterFailsafeClusterInvoker调用出错则日志记录
FailbackClusterFailbackClusterInvoker失败返空,定时重试2次
ForkingClusterForkingClusterInvoker一个任务并发调用,一个OK则OK
BroadcastClusterBroadcastClusterInvoker逐个调用invoker,全可用才可用
AvailableClusterAvailableClusterInvoker哪个能用就用那个
MergeableClusterMergeableClusterInvoker按组合并返回结果

7.2 智能容错之负载均衡 ​

Dubbo中一般有4种负载均衡策略。

  1. RandomLoadBalance:加权随机,它的算法思想简单。假设有一组服务器 servers = [A, B, C],对应权重为 weights = [5, 3, 2],权重总和为10。现把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于服务器 B,[8, 10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在 [0, 10) 之间的随机数,然后计算这个随机数会落到哪个区间上。默认实现。
  2. LeastActiveLoadBalance:最少活跃数负载均衡,选择现在活跃调用数最少的提供者进行调用,活跃的调用数少说明它现在很轻松,而且活跃数都是从 0 加起来的,来一个请求活跃数+1,一个请求处理完成活跃数-1,所以活跃数少也能变相的体现处理的快。
  3. RoundRobinLoadBalance:加权轮询负载均衡,比如现在有两台服务器 A、B,轮询的调用顺序就是 A、B、A、B,如果加了权重,A 比B 的权重是2:1,那现在的调用顺序就是 A、A、B、A、A、B。
  4. ConsistentHashLoadBalance:一致性 Hash 负载均衡,将服务器的 IP 等信息生成一个 hash 值,将hash 值投射到圆环上作为一个节点,然后当 key 来查找的时候顺时针查找第一个大于等于这个 key 的 hash 值的节点。一般而言还会引入虚拟节点,使得数据更加的分散,避免数据倾斜压垮某个节点。如下图 Dubbo 默认搞了 160 个虚拟节点。

7.3 智能容错之服务目录 ​

关于 服务目录Directory 你可以理解为是相同服务Invoker的集合,核心是RegistryDirectory类。具有三个功能。

  1. 从注册中心获得invoker列表。
  2. 监控着注册中心invoker的变化,invoker的上下线。
  3. 刷新invokers列表到服务目录。

7.4 智能容错之服务路由 ​

服务路由其实就是路由规则,它规定了服务消费者可以调用哪些服务提供者。条件路由规则由两个条件组成,分别用于对服务消费者和提供者进行匹配。比如有这样一条规则:

plain
host = 10.20.153.14 => host = 10.20.153.12

该条规则表示 IP 为 10.20.153.14 的服务消费者只可调用 IP 为 10.20.153.12 机器上的服务,不可调用其他机器上的服务。条件路由规则的格式如下:

plain
[服务消费者匹配条件] => [服务提供者匹配条件]

如果服务消费者匹配条件为空,表示不对服务消费者进行限制。如果服务提供者匹配条件为空,表示对某些服务消费者禁用服务。

8. 设计RPC ​

通读下Dubbo的大致实现方式后其实就可以依葫芦画瓢了,一个RPC框架大致需要下面这些东西:

  1. 服务的注册跟发现的搞一个吧,你可以用ZooKeeper或者Redis来实现。
  2. 接下来consumer发起请求的时候你的面向接口编程啊,用到动态代理来实现调用。
  3. 多个provider提供相同服务你的用到LoadBalance啊。
  4. 最终选择一个机器后你的约定好通信协议啊,如何进行序列化跟反序列化呢?
  5. 底层就用现成的高性能Netty框架 NIO模式实现呗。
  6. 服务开启后的有monitor啊。

协议和序列化 ​

  1. dubbo 协议

默认就是走 dubbo 协议,单一长连接,进行的是 NIO 异步通信,基于 hessian 作为序列化协议。使用的场景是:传输数据量小(每次请求在 100kb 以内),但是并发量很高。

为了要支持高并发场景,一般是服务提供者就几台机器,但是服务消费者有上百台,可能每天调用量达到上亿次!此时用长连接是最合适的,就是跟每个服务消费者维持一个长连接就可以,可能总共就 100 个连接。然后后面直接基于长连接 NIO 异步通信,可以支撑高并发请求。

  1. rmi 协议

走 Java 二进制序列化,多个短连接,适合消费者和提供者数量差不多的情况,适用于文件的传输,一般较少用。

  1. hessian 协议

走 hessian 序列化协议,多个短连接,适用于提供者数量比消费者数量还多的情况,适用于文件的传输,一般较少用。

  1. http 协议

走 json 序列化。

  1. webservice

走 SOAP 文本

dubbo 支持的序列化协议?

dubbo 支持 hession、Java 二进制序列化、json、SOAP 文本序列化多种序列化协议。但是 hessian 是其默认的序列化协议。

Hessian 的对象序列化机制有 8 种原始类型:

  • 原始二进制数据
  • boolean
  • 64-bit date(64 位毫秒值的日期)
  • 64-bit double
  • 32-bit int
  • 64-bit long
  • null
  • UTF-8 编码的 string

负载均衡 ​

当同一个服务有多个提供者在提供服务时,客户端如何正确的选择提供者实 现负载均衡呢?dubbo也给我们提供了几种方案:

  • random 选提供者,并可以给提供者设置权重
  • roundrobin 选择提供者
  • leastactive ,相同活跃数的随机,活跃数:指调用前后计数差。使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
  • consistenthash ,相同参数的请求发到同一台机器上。

启动时检查 ​

Dubbo 缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常,阻止 Spring 初始化完成,以便上线时,能及早发现问题,默认 check=“true”。

可以通过 check=“false” 关闭检查,比如,测试时,有些服务不关心,或者出现了循环依赖,必须有一方先启动。

另外,如果你的 Spring 容器是懒加载的,或者通过 API 编程延迟引用服务,请关闭 check,否则服务临时不可用时,会抛出异常,拿到 null 引用,如果 check=“false”,总是会返回引用,当服务恢复时,能自动连上。

配置原则 ​

dubbo推荐在Provider上尽量多配置Consumer端属性

  1. 作服务的提供者,比服务使用方更清楚服务性能参数,如调用的超时时间,合理的重试次数,等等
  2. 在Provider配置后,Consumer不配置则会使用Provider的配置值,即Provider配置可以作为Consumer的缺省值。否则,Consumer会使用Consumer端的全局设置,这对于Provider不可控的,并且往往是不合理的

集群容错 ​

在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。

集群容错模式 ​

Failover Cluster ​

失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries=“2” 来设置重试次数(不含第一次)。

重试次数配置如下:

<dubbo:service retries=“2” />

或

<dubbo:reference retries=“2” />

或

dubbo:reference

<dubbo:method name=“findFoo” retries=“2” />

</dubbo:reference>

Failfast Cluster ​

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

Failsafe Cluster ​

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

Failback Cluster ​

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

Forking Cluster ​

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

Broadcast Cluster ​

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

集群模式配置 ​

按照以下示例在服务提供方和消费方配置集群模式

<dubbo:service cluster=“failsafe” />

或

<dubbo:reference cluster=“failsafe” />

服务注册原理 ​

  1. 起始于Spring IOC容器刷新完成后,注册入口为ServiceBean
java
//ServiceBean类继承了ApplicationListener<ContextRefreshedEvent>接口,IOC容器刷新完成后会通知
public void onApplicationEvent(ContextRefreshedEvent event) {
    // 是否有延迟导出 && 是否已导出 && 是不是已被取消导出
    if (isDelay() && !isExported() && !isUnexported()) {
        // 导出服务
        export();
    }
}
  1. 检测配置,补充默认配置,并且组装成 URL 。代码基本都集中在ServiceConfig
    • 检测 dubbo:service 标签的 interface 属性合法性,不合法则抛出异常
    • 检测 ProviderConfig、ApplicationConfig 等核心配置类对象是否为空,若为空,则尝试从其他配置类对象中获取相应的实例。
    • 检测并处理泛化服务和普通服务类
    • 检测本地存根配置,并进行相应的处理
    • 对 ApplicationConfig、RegistryConfig 等配置类进行检测,为空则尝试创建,若无法创建则抛出异常
  2. 通过javassist 动态代理封装 ref (你写的服务实现类),统一暴露出 Invoker
  3. 通过各协议的protocol类的export方法创建Exporter,比如DubboProtocol创建DubboExporter
  4. 将Exporter存入本地exporterMap中,exporterMap是一个ConcurrentHashMap,key是服务URL地址封装后的字符串,value就是当前Exporter
  5. 调用RegistryProtocol的export方法传入之前封装好的Invoker导出本地服务和远程服务
  6. 通过zkClient连接注册中,注册服务至注册中心,生成一个临时节点。

服务引用过程 ​

服务的引入时机有两种,第一种是饿汉式,第二种是懒汉式。

饿汉式是 Spring 容器调用 ReferenceBean 的 afterPropertiesSet 方法时引用服务。

懒汉式是ReferenceBean 对应的服务被注入到其他类中时引用。

默认情况下,Dubbo 使用懒汉式引入服务,如果需要使用饿汉式,可通过配置dubbo:reference 的 init 属性开启。

服务引用方式有三种

  1. 本地服务(JVM)
  2. 直连方式引用远程服务
  3. 通过注册中心引用远程服务

引用流程

  1. 服务引用的入口方法为 ReferenceBean 的 getObject 方法
  2. 调用ReferenceConfig的init方法 检测配置,补充默认配置
  3. 通过zkClient连接zk获取服务端的url等信息
  4. 封装远程url或直连url生成invoker
  5. 将封装好的invoker存入本地的CurrentHashMap中
  6. 通过javassist 生成动态代理类并使用netty进行远程调用

服务调用过程 ​

服务调用方式

Dubbo 支持同步和异步两种调用方式,其中异步调用还可细分为“有返回值”的异步调用和“无返回值”的异步调用。默认情况下,Dubbo 使用同步调用方式。

调用过程

消费端

  1. 从本地的CurrentHashMap中得到要调用的服务列表
  2. 使用负载均衡算法得到目标服务器invoker
  3. 通过javassist生成动态代理
  4. 通过动态代理调用InvokerInvocationHandler的Invoker方法
  5. 封装Request对象,设置消息头和消息体等信息
  6. 通过netty的send方法将request对象传入netty的channel

服务端

  1. 服务端接收到netty中的消费端request对象
  2. 针对request对象进行解码并传入线程池中
  3. 从本地CurrentHashMap中得到被调用的服务
  4. 调用用户线程进行业务逻辑处理
  5. 封装response对象,设置消息头和消息体等信息
  6. 通过netty的send方法将response对象传入netty的channel

消费端

  1. 消费端接收reponse消息的处理逻辑和服务端接收request的处理逻辑一致

dubbo全链路跟踪 ​

部署Zipkin+代码引入Brave

生成traceId+实现dubbo的Filter+日志记录下一章节:Java基础
返回目录:速通