高性能可扩展分布式RPC框架Dubbo-内核原理揭秘

1、前言

总体来讲,一个公司业务系统的演进流程基本都是从单体应用到多体应用。在单体应用时,不一样业务模块相互调用直接在本地 JVM 进程内就能够完成;而变为多个应用时,相互之间进行通讯的方式就不能简单的进行本地调用了,由于不一样业务模块部署到了不一样的 JVM 进程里面,更常见的是部署到了不一样的机器,这时候一个高效、稳定的 RPC 远程调用框架就变得很是重要。html

Dubbo做为阿里巴巴开发的一个开源的高性能的RPC调用框架,其致力于提供高性能和透明化的 RPC 远程调用服务解决方案。做为阿里巴巴 SOA 服务化治理方案的核心框架,目前它已进入 Apache 孵化器顶级项目,前景可谓无限光明。java

2、Dubbo-基础篇

2.1 Dubbo系统组成概述

使用Dubbo框架搭建的系统架构以下: ios

image.png

如上图是 Dubbo 的架构图,其中:算法

  • 服务提供方在启动时候会注册本身提供的服务到服务注册中心。sql

  • 服务消费方在启动时候会去服务注册中心订阅本身须要的服务的地址列表,而后服务注册中心异步把消费方须要的服务接口的提供者的地址列表返回给服务消费方,服务消费方根据路由规则和设置的负载均衡算法选择一个服务提供者 IP 进行调用。数据库

  • 监控平台主要用来统计服务的调用次数和调用耗时,服务消费者和提供者,在内存中累计调用次数和调用耗时,并定时每分钟发送一次统计数据到监控中心,监控中心则使用数据绘制图表来显示,监控平台不是分布式系统必须的,可是这些数据有助于系统运维和调优。服务提供者和消费者能够直接配置监控平台的地址,也能够经过服务注册中心来获取。apache

  • 服务注册中心则负责服务注册与发现,常见的服务注册中心有zookeeper、etcd。编程

2.2 Dubbo基础

本节主要简单的讲解Dubbo如何使用,以及本书中的demo实例,建议读者先阅读基础篇在进入后面的章节,由于后面章节基本是基于本章的demo进行讲解的。安全

demo中 Consumer 模块为服务消费者相关,本书中全部与消费端有关的demo都在该模块中,包含普通调用、各类异步调用、泛化调用、基于扩展接口实现的自定义负载均衡策略、集群容错策略等等。bash

其中 Provider 模块为服务提供者相关,本书中全部与服务提供端有关的demo都在该模块中,包含服务接口的实现类、服务提供方的同步处理请求、各类异步处理请求的实现等等。

其中 SDK 模块是一个二方包,用来存放服务接口,这是为了代码复用,在服务提供者和消费者(泛化调用除外)的模块里面都须要引入这个二方包。

3、Dubbo-高级篇

3.1 Dubbo分层架构

本节咱们从总体上来看看 Dubbo 的分层架构设计,架构分层是一个比较经典的模式,好比网络中的7层协议,每层执行固定的功能,上层依赖下层提供的功能,下层对上层的提供功能,下层的改变对上层不可见,而且每层都是一个可被替换的组件。

以下图是 Dubbo 官方提供的Dubbo的总体架构图:

image.png

Dubbo 官方提供的该架构图很复杂,一开始咱们不必深刻细节,下面咱们简单讲解下其中的主要模块:

  • 其中 Service 和 Config 层为 API接口层,是为了方便的让Dubbo使用方发布服务和引用服务;对于服务提供方来讲须要实现服务接口,而后使用 ServiceConfig API 来发布该服务;对于服务消费方来讲须要使用ReferenceConfig 对服务接口进行代理。Dubbo服务发布与引用方能够直接初始化配置类,也能够经过 Spring 配置自动生成配置类。

  • 其它各层均为 SPI层,SPI 意味着下面各层都是组件化能够被替换的,这也是 Dubbo 设计的比较好的一点。Dubbo 加强了 JDK 中提供的标准 SPI 功能,在 Dubbo 中除了 Service 和 Config 层外,其它各层都是经过实现扩展点接口来提供服务的;Dubbo 加强的 SPI 增长了对扩展点 IoC 和 AOP 的支持,一个扩展点能够直接 setter 注入其它扩展点;而且不会一次性实例化扩展点的全部实现类,这避免了当扩展点实现类初始化很耗时,但当前还没用上它的功能时仍进行加载实例化,浪费资源的状况;加强的 SPI 是在具体用某一个实现类的时候才对具体实现类进行实例化。后续会具体讲解 Dubbo 加强的 SPI 的实现原理。

  • Proxy 服务代理层:该层主要是对服务消费端使用的接口进行代理,把本地调用透明的转换为远程调用;另外对服务提供方的服务实现类进行代理,把服务实现类转换为 Wrapper 类,这是为了减小反射的调用,后面会具体讲解到。Proxy层的SPI扩展接口为 ProxyFactory,Dubbo 提供的实现主要有 JavassistProxyFactory(默认使用)和 JdkProxyFactory,用户能够实现ProxyFactory SPI接口,自定义代理服务层的实现。

  • Registry 服务注册中心层:服务提供者启动时候会把服务注册到服务注册中心,消费者启动时候会去服务注册中心获取服务提供者的地址列表,Registry层主要功能是封装服务地址的注册与发现逻辑,扩展接口 Registry 对应的扩展实现为 ZookeeperRegistry、RedisRegistry、MulticastRegistry、DubboRegistry等。扩展接口 RegistryFactory 对应的扩展接口实现为 DubboRegistryFactory、DubboRegistryFactory、RedisRegistryFactory、ZookeeperRegistryFactory。另外该层扩展接口Directory实现类有RegistryDirectory、StaticDirectory用来透明的把invoker列表转换为一个invoker;用户能够实现该层的一系列扩展接口,自定义该层的服务实现。

  • Cluster 路由层:封装多个服务提供者的路由规则、负载均衡、集群容错的实现,并桥接服务注册中心;扩展接口 Cluster 对应的实现类有 FailoverCluster(失败重试)、FailbackCluster(失败自动恢复)、FailfastCluster(快速失败)、FailsafeCluster(失败安全)、ForkingCluster(并行调用)等;负载均衡扩展接口 LoadBalance 对应的实现类为 RandomLoadBalance(随机)、RoundRobinLoadBalance(轮询)、LeastActiveLoadBalance(最小活跃数)、ConsistentHashLoadBalance(一致性hash)等。用户能够实现该层的一系列扩展接口,自定义集群容错和负载均衡策略。

  • Monitor 监控层:用来统计RPC 调用次数和调用耗时时间,扩展接口为 MonitorFactory,对应的实现类为 DubboMonitorFactroy。用户能够实现该层的MonitorFactory扩展接口,实现自定义监控统计策略。

  • Protocol 远程调用层:封装 RPC 调用逻辑,扩展接口为 Protocol, 对应实现有 RegistryProtocol、DubboProtocol、InjvmProtocol 等。

  • Exchange 信息交换层:封装请求响应模式,同步转异步,扩展接口 Exchanger,对应扩展实现有 HeaderExchanger 等。

  • Transport 网络传输层:抽象 mina 和 netty 为统一接口。扩展接口为 Channel,对应实现有 NettyChannel(默认)、MinaChannel 等;扩展接口Transporter对应的实现类有GrizzlyTransporter、MinaTransporter、NettyTransporter(默认实现);扩展接口Codec2对应实现类有DubboCodec、ThriftCodec等

  • Serialize 数据序列化层:提供能够复用的一些工具,扩展接口为 Serialization,对应扩展实现有 DubboSerialization、FastJsonSerialization、Hessian2Serialization、JavaSerialization等,扩展接口ThreadPool对应扩展实现有 FixedThreadPool、CachedThreadPool、LimitedThreadPool 等。

综上可知Dubbo的分层架构使得Dubbo的每层的功能都是可被替换的,这使得Dubbo的扩展性极强,上面说了那么多关于扩展点的东西,那么具体什么是扩展点呢,下面看下 Dubbo 扩展点一个简单例子。以扩展点 Protocol 为例:

@SPI("dubbo")
public interface Protocol {
...
}
复制代码

扩展点接口的类上面都含有@SPI注解,这里注解里面的"dubbo"说明Protocol扩展接口SPI的默认实现是DubboProtocol。

若是咱们想本身写一个 Protocol 扩展接口的实现类,那么咱们须要在实现类所在的 Jar 包内的 META-INF/dubbo/ 目录下建立一个名字为 org.apache.dubbo.rpc.Protocol 的文本文件,而后配置它的内容为:

myprotocol=com.alibaba.user.MyProtocol

复制代码

假设该实现类 MyProtocol 的内容以下:

package com.alibaba.user;
public class MyProtocol implemenets Protocol {
// ...
}

复制代码

那么如何使用咱们自定义的扩展实现呢?Dubbo 配置模块中,扩展点均有对应配置属性或标签,以下代码经过配置标签方式指定使用哪一个扩展实现:

<dubbo:protocol name="myprotocol" />

复制代码

注意这里的 name 必须与 jar 包内 META-INF/dubbo/ 目录下 org.apache.dubbo.rpc.Protocol 文件中的等号左侧的key的名字一致。

3.2 Dubbo内核原理

在Dubbo中框架的可扩展性是靠适配器原理结合加强SPI机制实现的,本书中首先会讲解Dubbo的适配器原理,什么是适配器模式?好比dubbo提供的扩展接口Protocol,Protocol的定义以下:

@SPI("dubbo")
public interface Protocol {
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
    ....
}
复制代码

Dubbo则会使用本书介绍的动态编译技术为接口Protocol生成一个适配器类Protocol$Adaptive的对象实例,Dubbo框架中须要使用Protocol的实例的时候实际就是使用的Protocol$Adaptive的对象实例来获取具体SPI实现类,其代码以下:

package org.apache.dubbo.rpc;
...
public class Protocol$Adaptive implements Protocol {   
 ...
public Exporter export(Invoker invoker) throws RpcException {
    String string;
    ...
    //(1)
    URL uRL = invoker.getUrl();
    String string2 = string = uRL.getProtocol() == null ? "dubbo" : uRL.getProtocol();
    if (string == null) {
        throw new IllegalStateException(new StringBuffer().append("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (").append(uRL.toString()).append(") use keys([protocol])").toString());
    }
    //(2)
    Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(string);
    //(3)
    return protocol.export(invoker);
}
    
复制代码

在dubbo框架中protocol的一个定义为: private static final Protocol protocol =ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();当调用protocol.export(wrapperInvoker)时候,实际是调用的Protocol$Adaptive的对象实例的export方法,而后后者根据wrapperInvoker中的url里面的协议类型参数执行代码(2)使用Dubbo加强SPI方法getExtension获取对应的SPI实现类,而后调用代码(3)执行具体SPI实现类的export方法。

而后本书会讲解Dubbo加强的SPI机制,本书中首先会借助 java.sql.Driver扩展接口讲解标准JDK中的SPI实现原理以及缺陷,而后讲解dubbo的加强SPI如何对其进行改进,如何实现的扩展接口之间自动IOC和扩展接口的功能加强AOP功能。

而后会讲解Dubbo使用JavaAssist减小反射调用开销:Dubbo会给每一个服务提供者的实现类生产一个Wrapper类,这个wrapper类里面最终调用服务提供者的接口实现类,wrapper类的存在是为了减小反射的调用。当服务提供方接受到消费方发来的请求后须要根据消费者传递过来的方法名和参数反射调用服务提供者的实现类,而反射自己是有性能开销的,因此dubbo把每一个服务提供者的实现类经过JavaAssist包装为一个Wrapper类,那么Wrapper类为什么能减小反射调用那?观看本书就能够找到答案

3.3 Dubbo功能实现原理

讲解完毕支撑Dubbo框架的内核原理后,本书会先从总体剖析Dubbo服务提供端如何发布服务的,这包含发布本地服务和发布远程服务的流程, Dubbo服务导出分 本地导出与远程导出,本地导出使用了 injvm 协议,是一个伪协议,它不开启端口,不发起远程调用,只在 JVM 内直接关联,但执行 Dubbo 的 Filter 链;默认下 Dubbo 同时支持本地导出与远程导出协议,能够经过ServiceConfig的setScope方式设置,其中配置为none表示不导出服务,为remote表示只导出远程服务,为local表示只导出本地服务。

你会知道Dubbo如何实现的服务延迟发布,如何把服务实现类转换为 Wrapper 类,以便减小反射的调用,何时构建的dubbo的Filter链,都有哪些Wrapper类对扩展接口的实现类进行了功能加强?如何启动的NettyServer对服务进行监听,同一个机器上的多个服务提供接口是启动多个NettyServer仍是一个?如何作到的?如何注册服务到服务注册中心的?服务注册到zookeeper后,其存储结构是怎么样的?

而后本书会讲解当服务提供方接受到请求后,如何进行处理的,这包含Filter链对请求的处理,以及如何找到对应的被wrapper类包装后的服务实现类,并对请求进行处理,如何实现的Dubbo的服务提供端异步执行。

而后会讲解Dubbo服务消费端的启动流程,这个过程,你会知道如何基于Proxy SPI扩展实现对服务接口进行代理。与服务提供端同样,消费端能够设置是否须要本地服务引用,你会知道在消费端若是没有指定scope类型,在启动时候会检查当前jvm内是否有导出的服务,若是有则自动开启本地引用(也就是协议类型修改成injvm),则具体调用时候会使用本地暴露的服务来提供服务,而不发起远程调用。

当具体发起远程调用时候,你会知道如何动态从服务注册中心动态订阅服务信息的,好比订阅服务提供者地址列表,服务降级信息,服务路由信息,以及Directory目录与Router路由服务,以及何时构建的路由规则链。

如何启动NettyClient具体发起远程调用的。而后你会知道同一个服务提供者机器能够提供多个服务,那么消费者机器须要与同一个服务提供者机器提供的多个共享链接仍是与每一个服务都创建一个?消费端是启动时候就与服务提供者机器创建好链接?

而后会讲解具体如何发起一次远程调用,这个过程你会知道当发起一次rpc调用时候会先通过MockInvoker进行处理,其会看是否设置了 force:return 降级策略,若是设置了则直接返回 mock 值,并不发起远程调用;否者发起远程调用,若是远程调用结果 OK,则直接返回远程调用返回的结果;若是远程调用失败了,则看当前是否设置了 fail:return 的降级策略,若是设置了,则直接返回 mock 值,否者返回调用远程服务失败的具体缘由。

若是没有设置服务降级策略或者mock服务,则会基于SPI机制选择具体的集群容错策略(本文会详细讲解常见的Failover、Failfast、Failsafe、Forking、Broadcast这几种集群容错实现原理,以及讲解如何本身基于SPI实现本身的容错策略),具体集群容错策略内有会根据SPI机制选择设置的服务负载均衡策略(本文会详细介绍常见的Random、RoundRobin、LeastActive、ConsistentHash),具体负载均衡策略内会基于SPI选择设置的服务目录实现,其内部维护了全部服务提供者的服务提供者列表与路由规则,负载均衡策略则会从符合路由规则的地址列表里面选择一个invoker返回,而后最终有该invoker执行。若是执行失败了,则根据具体集群容错策略从新选择一个invoker进行执行....

而后本书会讲解Dubbo线程模型与线程池策略,Dubbo 默认的底层网络通信使用的是 Netty ,服务提供方 NettyServer 使用两级线程池,其中 EventLoopGroup(boss) 主要用来接受客户端的连接请求,并把接受的请求分发给 EventLoopGroup(worker) 来处理,boss 和 worker 线程组咱们称之为 IO 线程。

若是服务提供方的逻辑能迅速完成,而且不会发起新的 IO 请求,那么直接在 IO 线程上处理会更快,由于这减小了线程池调度与上下文切换开销。但若是处理逻辑较慢,或者须要发起新的 IO 请求,好比须要查询数据库,则 IO 线程必须派发请求到新的线程池进行处理,不然 IO 线程会被阻塞,将致使不能接收其它请求。

Dubbo中在服务提供端与消费端的IO线程对请求处理时候默认是把请求转交给dubbo框架的内部线程池来进行处理的,以即可以及时释放IO线程。

根据IO线程把什么类型的消息或者请求交给内部线程池来处理,dubbo提供了不一样的线程模型,本书主要讲解Dubbo提供的线程模型AllDispatcher、DirectDispatcher、MessageOnlyDispatcher、ExecutionDispatcher、ConnectionOrderedDispatcher的实现原理,以及线程池策略FixedThreadPool、LimitedThreadPool、EagerThreadPool、CachedThreadPool的实现原理,以及如何基于SPI自定义本身的线程模型与线程池策略。

基础篇咱们讲解到,基于Dubbo APi搭建Dubbo服务时候,服务消费端引入了一个 SDK 二方包,里面存放着服务提供端提供的全部接口类,泛化接口调用方式主要在服务消费端没有 API 接口类及模型类元(好比入参和出参的 POJO 类)的状况下使用。其参数及返回值中没有对应的 POJO 类,因此全部 POJO 均转换为 Map 表示。使用泛化调用时候服务消费模块再也不须要引入 SDK 二方包,本书会详细介绍Dubbo中nativejava,true, bean三种泛化调用的实现。

基础篇咱们讲到Dubbo提供了隐式参数传递的功能,即服务调用方能够经过RpcContext.getContext().setAttachment()方法设置附加属性键值对,而后设置的值对能够在服务提供方服务方法内获取;本书咱们会详细介绍如何在在消费端设置参数,而且如何经过网络把参数传递到服务提供方,而后服务提供方如何进行获取。

正如Dubbo官网所说dubbo从2.7.0版本开始支持全部异步编程接口以CompletableFuture为基础,以便解决2.7.0以前版本异步调用的不便与功能缺失。

异步调用实现是基于 NIO 的非阻塞能力实现并行调用,服务消费端不须要启动多线程便可完成并行调用多个远程服务,相对多线程开销较小,以下图是Dubbo异步调用链路概要流程图图:

image.png

本书咱们首先讲解dubbo服务消费端的异步调用,首先讲解2.7.0版本前的异步调用实现原理,咱们会知道future调用get()方法方式实现异步缺点是当业务线程调用get()方法后业务线程会被阻塞,这不是咱们想要的,因此dubbo2.7.0版本提供了在CompletableFuture对象上设置回调函数的方式,让咱们实现真正的异步调用。

在Provider端非异步执行时候,其对调用方发来的请求的处理是在Dubbo内部线程模型的线程池中的线程来执行的,在dubbo中服务提供方提供的全部的服务接口都是使用这一个线程池来执行的,因此当一个服务执行比较耗时时候,可能会占用线程池中不少线程,这可能就会致使其余服务的处理收到影响。

Provider端异步执行则将服务的处理逻辑从Dubbo内部线程池切换到业务自定义线程,避免Dubbo线程池中线程被过分占用,有助于避免不一样服务间的互相影响。

可是须要注意provider端异步执行对节省资源和提高RPC响应性能是没有效果的,这时是由于若是服务处理比较耗时,虽然不是使用Dubbo框架内部线程处理,可是仍是须要业务本身的线程来处理,另外反作用还有会新增一次线程上下文切换(从dubbo内部线程池线程切换到业务线程),模型以下图11.2.0

image.png

本书首先会讲解基于定义CompletableFuture签名的接口实现异步执行的实现原理,而后讲解使用AsyncContext实现异步执行原理,最后讲解Dubbo的异步调用与执行引入的新问题以及如何解决的,这包含引入异步调用时候等结果返回后Filter链得不到执行的问题,以及异步执行时候上下文参数传递问题。

前面章节咱们介绍了服务消费端一次服务调用流程与服务提供端一次服务处理流程,可是仍是有一些东西是咱们没有提到的,好比服务消费端如何把服务请求信息序列化为二进制、服务提供方又是如何把消费端发送的二进制数据反序列化为可识别的POJO对象、好比Dubbo的应用层协议是怎么样的。本书咱们就来一一来看dubbo是如何作这些的。

本书会首先讲解Dubbo协议,在TCP协议栈中,每层协议都有本身的协议报文格式,好比TCP协议是网络七层模型中的传输层,有TCP协议报文格式;在TCP上层是应用层,应用层协议常见的有http协议等,Dubbo协议做为创建在TCP协议之上的一种应用层协议,天然也有本身的协议包格式,Dubbo协议也是参考TCP协议栈中的协议,协议内容由header和body两部分组成,本书会详细介绍协议header中每一个字段含义。而后讲解服务消费方编码原理,包含当服务消费端发送请求时候,如何把请求内容封装为Dubbo协议帧的。而后讲解服务提供方接受请求后如何对协议帧进行解码解决半包粘包问题的。

4、Dubbo-实践篇

实践篇咱们来探讨如何使用Arthas和一些demo来对研究Dubbo框架实现提供便捷,而且基于Netty与CompletableFuture模拟了RPC同步与纯异步调用。

首先本书会介绍如何安装Arthas,而后讲解如何使用Arthas查看查看扩展接口适配器类的源码,查看服务提供端Wrapper类的源码,如何查询Dubbo启动后都有哪些Filter,而后经过Demo验证RoundRobin LoadBalance负载均衡原理,而后探讨若是根据IP动态路由调用Dubbo服务。

Dubbo的服务消费端基于CompletableFuture实现了功能比较丰富的纯异步调用,其实还不仅仅是CompletableFuture的功劳,归根究竟是Netty的NIO非阻塞功能提供的底层实现,本文咱们就来基于CompletableFuture与Netty来模拟下如何异步发起远程调用,以及如何使用CompletableFuture自己的功能,让多个请求的异步结果进行运算,以便加深对dubbo异步调用实现原理的理解。

5、总结

如何你对上面内容感兴趣,想深刻研究,可是无从入手,那么机会来了,专栏

内容包含可是不限于上述内容,你们能够扫描订阅该专栏。

相关文章
相关标签/搜索