RPC本质

抛开Dubbo不讲,最简单的一个RPC其实就是:

  1. consumer端根据IPport用Netty客户端发出请求,请求数据中带有调用的接口名
  2. provider端收到请求后,根据接口名,找到接口实现类,执行业务处理;
  3. provider处理完之后,把结果响应回consumer,这样就完成了RPC的一次调用。

但是只是这样肯定不行,存在很多问题,比如怎么感知provider?调用超时、失败怎么办?代码侵入太大怎么办?所以Dubbo的话,我认为在这个的基础上,有了代理、服务注册发现、容错处理、负载均衡等等。

服务注册发现机制

provider

provider启动之后会进行spring配置的解析,然后发起服务暴露,这时候provider主要做两件事:

  1. 启动Netty服务端,监听客户端(consumer)的请求;
  2. 向注册中心注册自己的服务,在zk上创建节点,并和注册中心维持心跳连接。

provider服务注册实现方式,就是在zk上创建节点,节点中包含了服务的元信息,包括IP、端口、接口名、版本号等等。

consumer

consumer启动之后也会进行Spring配置的解析,然后发起服务引用,这时候consumer主要做两件事:

  1. 启动Netty客户端,后面发起RPC调用的时候用;
  2. 向注册中心注册自己的服务,并订阅节点信息,当节点发生变化时,会通知consumer,consumer再拉取最新当节点信息;
  3. consumer会在本地对节点信息做一个副本,调用时走副本,并有一个定时任务去获取最新的节点信息。

    容错机制

  4. Dubbo默认的容错机制是fail-over 失败重试,因为有重试所以我们需要考虑处理幂等的情况,所以fail-over适合读的场景或provider方实现了幂等。

  5. fail-fast 快速失败,失败后就返回异常,适合写的场景。
  6. fail-safe 失败安全,就算调用失败也不会抛异常,适合记日志一些不重要的业务。

此外还有像fork(调用多个有一个成功就可以),广播(都成功才可以)等。

协议

Dubbo (默认)、HTTP、rmi、hessian。

序列化

Hessian2、JDK序列化、fastjson。

线程派发策略

策略 用途
all 所有消息都派发到线程池,包括请求,响应,连接事件,断开事件等,默认
direct 所有消息都不派发到线程池,全部在 IO 线程上直接执行
message 只有请求响应消息派发到线程池,其它消息均在 IO 线程上执行
execution 只有请求消息派发到线程池,不含响应。其它消息均在 IO 线程上执行
connection 在 IO 线程上,将连接断开事件放入队列,有序逐个执行,其它消息派发到线程池

线程池

线程池类型 说明
fixed 固定大小线程池,默认线程数200,启动时建立,不会关闭,一直存在
cached 缓存线程池,空闲一分钟自动删除,需要时重建
limited 可伸缩线程池,但只会扩大不会缩小。这么做的目的是避免收缩的时候来大流量带来性能问题
eager 优先创建 worker 线程池。任务数大于 corePoolSize 小于 maxPoolSize,创建 worker 处理,线程数大于 maxPoolSize ,任务放入阻塞队列,阻塞队列满了走拒绝策略。

负载均衡

  1. 随机加权负载均衡
  2. 加权轮询负载均衡
  3. 最小活跃数负载均衡

对于调用的活跃数的检测统计是基于过滤器实现的,每次调用(不管调用是否成功)都会调用次数自增。

  1. 一致性hash负载均衡

把provider的IP等信息hash计算之后放在一个环(TreeMap)上,然后对于consumer的请求,对请求的信息做hash之后在环上顺时针的找最近的provider,找到的就是要调用的provider。这种方式可以让同一个consumer的请求一直调用在同一台provider上。
此外为了防止环上分配不均的问题,dubbo采用了虚拟节点的方式来解决。

SPI

SPI的作用

通过策略模式来提高扩展性。大概使用姿势是这样的:

  1. 在配置文件中META-INF,用接口的全限定名作为文件名,文件内容写接口实现类的全限定名;
  2. 调用ServiceLoader.load(接口.class)方法就会去加载配置文件,然后用反射的方式(Class.forName)实例化实现类。

大概梳理一下,SPI也就是提供了功能的接口,但具体实现自己扩展,而如何扩展呢?就是通过策略模式,在接口名的配置文件中配置实现类,通过反射的方式加载实现类。

Dubbo SPI的实现原理

@SPI

注解在接口上,表明这个接口是可以用SPI机制扩展的。

@adaptive

注解在接口方法上,会用javasist根据参数自动生成一个类,然后在使用的时候,生成的类会根据参数URL选择不用的实现类去处理(实现类会在配置文件中配置)。
注解在实现类上,不会生成类,调用loader.getAdaptiveExtension()会返回这个类。

利用@adaptive注解,可以在程序运行时,根据传入参数的不同,动态选择不用的实现类处理

@activate

通常获取扩展实现只能获取到一个实现类,但例如拦截器等场景通常需要我们可以扩展一系列的功能,而 @activate 注解就是为了这种场景而生的。@activate 可以注解在很多实现类上,然后通过 group 或 value 在URL参数中自动激活不同的多个实现类。

和JDK SPI的区别

Dubbo SPI的原理的话,我觉得跟JDK SPI也没什么区别,但是Dubbo SPI功能会更强大一点,具体来说,以下几点:

  1. Java SPI 每次都会把所有实现类都加载并实例化(是在迭代器迭代的时候创建实例),而 Dubbo SPI 是分两段创建实例,先进行类加载,然后在使用到具体实现的时候才实例化,并且 Dubbo SPI 大量使用缓存,会把 Class 对象和实例对象都缓存起来,性能更好;
  2. Java SPI 在类加载失败的时候难以定位异常;
  3. Dubbo SPI 还支持 IOC 和 AOP 。

    IOC SPI实现原理

    遍历方法 setXXX(class),如果有这种方法的就去进行注入,注入有两种方式:IOC和Spring,IOC从配置文件中找,Spring从Bean容器中找。

    AOP SPI实现原理

    Dubbo在解析配置文件中的每行配置信息时,会判断这行配置对应类的构造方法的传入参数类型,是不是扩展接口,如果是的话就把这行配置对应的类放入缓存,然后后面创建实例的时候,会遍历缓存,通过构造方法传参的方式,对扩展类进行了包装
    这样后面在调用这个扩展类的时候,先进行包装类的处理,然后在进行扩展类的处理,实现了AOP。

    十层架构

    Dubbo - 图1