2023-08-12
原文作者:Ressmix 原文地址:https://www.tpvlog.com/article/187

从本章开始,我将介绍分布式系统中与 可扩展 这一特性息息相关的一些开源框架的基本架构和核心原理。本章,我们先来看下Dubbo这个分布式RPC框架。

在分布式理论系列中,我已经讲过了服务化拆分,读者可以先去回顾一下。目前,业界最常用的开源RPC通信框架有Dubbo和Thrift。读者在阅读本章的过程中可以参考Dubbo官方文档:http://dubbo.apache.org/zh-cn/docs/user/quick-start.html。

一、基本架构

我们先来看看Dubbo的基本架构:

202308122221551231.png

在使用dubbo的过程中,与客户端直接交互的有以下五类节点:

节点 角色说明
Provider 服务提供方
Consumer 服务消费方
Registry 服务注册与发现的注册中心
Monitor 统计服务的调用次数和调用时间的监控中心
Container 服务运行容器

1.1 框架使用流程

Dubbo框架的使用流程大致如下:

  1. 服务容器负责启动、加载、运行服务提供者;
  2. 服务提供者在启动时,向注册中心注册自己提供的服务;
  3. 服务消费者在启动时,向注册中心订阅自己所需的服务;
  4. 注册中心将服务提供者的地址列表返回给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者;
  5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一个服务提供者进行调用,如果调用失败,再选另一个调用;
  6. 服务消费者和提供者,在内存中累计调用次数和调用时间,每分钟发送一次统计数据到监控中心。

1.2 服务调用流程

下图是Dubbo服务调用过程的抽象,我们以一次Dubbo请求来看下基本的流程:

202308122221565282.png

  1. 首先,服务消费者通过代理对象 Proxy 发起远程调用;
  2. 接着通过网络客户端 Client 将编码后的请求发送给服务提供方的网络层上,也就是 Server;
  3. Server 在收到请求后,首先要做的事情是对数据包进行解码,然后将解码后的请求发送至分发器 Dispatcher;
  4. 分发器将请求派发到指定的线程池上,由线程池调用具体的服务。

读者可以先对一次服务调用的流程有个初步印象,后面我将细化分析底层的原理。

二、底层原理

在开始分析dubbo的底层原理之前,先看下dubbo官网的这张图:

202308122221580283.png

  • 图中左边淡蓝背景的,为服务消费方使用的核心接口;右边淡绿色背景的,为服务提供方使用的核心接口;位于中轴线上的,为双方都用到的接口。
  • 图中从下至上分为十层,各层均为单向依赖;右边的黑色箭头,代表层之间的依赖关系,每一层都可以剥离上层被复用;其中Service 和 Config 层为 API,其它各层均为 SPI(什么是SPI?参考下一章)。
  • 图中的绿色小块,表示接口,蓝色小块表示实现类,图中只显示用于关联各层的实现类。
  • 图中的蓝色虚线,为初始化过程,即启动时组装链;红色实线,为方法调用过程,即运行时调时链;紫色三角箭头为继承,可以把子类看作父类的同一个节点,线上的文字为调用的方法。

可以看到,dubbo是一种典型的分层架构,各层的主要功能如下:

  • config 配置层 :对外配置接口,以 ServiceConfigReferenceConfig 为中心,可以直接初始化配置类,也可以通过 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

我知道,很多读者看到这里基本已经晕了,没关系,我会通过一次dubbo请求调用,来分析下dubbo的整个工作流程,逐一剖析各层的作用:

202308122222000434.png

2.1 服务注册/发现

首先,dubbo要正常运行,需要包含三个最基本的组件:

  • 服务提供者
  • 服务消费者
  • 注册中心

服务提供者,向注册中心注册自身的服务地址和端口,这一过程叫做服务注册
服务消费者,从注册中心拉取一份服务提供者的列表,保存在本地,这一个过程叫做服务发现

202308122222069615.png

服务提供者和服务消费者就是service层,注册中心就是registry层。

2.2 接口代理

服务消费者并不是直接去调用服务提供者的接口,proxy服务代理层会为接口自动生成代理对象。该代理对象会通过cluster路由层根据软负载均衡算法,选择合适的服务提供者。

202308122222078686.png

集群

Dubbo 在Cluster路由层定义了集群接口 Cluster 以及 Cluster Invoker。集群 Cluster 是将多个服务提供者合并为一个 Cluster Invoker,并将这个 Invoker 暴露给服务消费者。这样一来,消费者只需通过这个 Invoker 进行远程调用即可,至于具体调用哪个服务提供者,以及调用失败后如何处理等问题,都交给集群模块去处理。

所以,集群模块可以看成服务提供者(Provider)和服务消费者(Consumer)的中间层,为消费者屏蔽了服务提供者的情况,这样消费者就可以专心处理远程调用相关事宜,比如发请求,接受服务提供者返回的数据等,这就是集群模块的作用。

202308122222091307.png

Cluster路由层的所有组件见上图,包含 Cluster、Cluster Invoker、Directory、Router 和 LoadBalance 等。 其工作过程可分为两个阶段:

  1. 在服务消费者初始化期间,集群 Cluster 实现类为消费者创建 Cluster Invoker 实例,即上图中的 merge 操作;
  2. 在服务消费者进行远程调用时,以 FailoverClusterInvoker 为例,该类型 Cluster Invoker 首先会调用 Directory(Directory 的用途是保存 Invoker) 的 list 方法列举 Invoker 列表(可将 Invoker 简单理解为服务提供者),当 FailoverClusterInvoker 拿到 Directory 返回的 Invoker 列表后,通过 LoadBalance 从列表中选择一个 Invoker。最后 FailoverClusterInvoker 会将参数传给选出的 Invoker 实例的 invoker 方法,进行真正的远程调用。

集群容错策略

对于服务消费者来说,同一环境下可能有多个服务提供者,消费者需要决定当某个服务调用失败时该如何处理,是重试,还是抛出异常,亦或是只打印异常等。这种处理措施,就是集群容错策略。

Dubbo 根据容错策略提供了多种集群实现:

  • Failover Cluster:失败自动切换,自动重试其他机器,默认就是这个,常见于读操作
  • Failfast Cluster:快速失败,一次调用失败就立即失败,常见于写操作
  • Failsafe Cluster:失败安全,出现异常时忽略掉,常用于不重要的接口调用,比如记录日志
  • Failback Cluster:失败自动恢复,失败了后台自动记录请求,然后定时重发,比较适合于写消息队列这种
  • Forking Cluster:并行调用多个服务提供者,并行调用多个provider,只要一个成功就立即返回
  • Broadcacst Cluster:逐个调用所有的provider

负载均衡

Cluster路由层包括一个LoadBalance组件,主要用作出现多个服务提供者时的客户端负载均衡,具体的负载均衡算法,后续小节专门讲解。

2.3 协议选择

代理对象创建完成后,还需要进行请求协议的选择,Dubbo支持dubbo、rmi、hessian、http等协议,protocol层会根据配置完成协议的选择。

2.4 封装请求

接着,Exchange信息交换层会对请求进行封装,最终会将请求封装成Dubbo的Request对象。

2.5 网络通信

真正发起请求是在 Transport 网络传输层,该层其实就是一个基于netty/mina实现的Server:

202308122222097618.png

以Netty为例,Netty底层采用了I/O 多路复用模型,基本通信原理如下:

  1. 首先,服务提供者端,Netty会有一个Acceptor线程,通过selector这个多路复用函数去轮询监听Server Socket——ServerSocketChannel 的各种网络事件;
  2. 服务消费者端,如果发起建立连接请求,被Acceptor线程监听到后,会在Server端自动生成对应的SocketChannel;
  3. 服务端的Processor线程会去轮询SocketChannel,查看对应的消费者有没有发生请求过来,如果有请求,则解析出来交给服务提供者处理;
  4. 服务消费者端,对于响应的处理也一样,会有一个工作线程去轮询客户端的SocketChannel,解析后交给消费者处理。

2.6 序列化

网络数据均以二进制进行传输,所以需要对请求的数据进行序列化,序列化的工作在Serialize数据序列化层完成。

2.7 接受请求

服务提供者,接收请求时,也是靠的transport 网络传输层,首先进行反序列化,然后解析出Request,按照协议格式再次解析,交给动态代理对象处理。

三、通信协议

上一节中,我们提到Protocol 协议层负责通信协议的选择,本节来看下Dubbo提供了哪些协议。

3.1 dubbo

dubbo缺省协议,采用单一长连接和NIO异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。

思考一下为什么 dubbo:// 适合高并发小数据量的场景?

在大多数场景下,服务的现状都是服务提供者少、服务消费者多,一个服务提供者通常可以满足20个左右服务消费者的并发调用 。

如果每天调用量为亿量级,那长连接是最合适的,因为此时每个服务消费者维持一个长连接就可以,可能总共就100个连接,然后直接基于长连接NIO异步通信,就可以支撑高并发请求。否则,如果上亿次请求每次都是短连接的话,相当于每个请求都要建立一次连接,系统开销会非常大。

另一方面,由于是单一长连接,所以传输数据量太大的话,会导致并发能力降低(数据量大可能导致网络阻塞)。

    缺省协议,使用基于mina1.1.7+hessian3.2.1的tbremoting交互。
    连接个数:单连接
    连接方式:长连接
    传输协议:TCP
    传输方式:NIO异步传输
    序列化:Hessian二进制序列化
    适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用dubbo协议传输大文件或超大字符串
    适用场景:常规远程服务方法调用

3.2 hessian

Hessian协议用于集成Hessian的服务,Hessian底层采用Http通讯,采用Servlet暴露服务,Dubbo缺省内嵌Jetty作为服务器实现基于Hessian的远程调用协议。

    连接个数:多连接
    连接方式:短连接
    传输协议:HTTP
    传输方式:同步传输
    序列化:Hessian二进制序列化
    适用范围:传入传出参数数据包较大,提供者比消费者个数多,提供者压力较大,可传文件
    适用场景:页面传输,文件传输,或与原生hessian服务互操作

3.3 http

采用Spring的HttpInvoker实现。

    连接个数:多连接
    连接方式:短连接
    传输协议:HTTP
    传输方式:同步传输
    序列化:表单序列化(JSON)
    适用范围:传入传出参数数据包大小混合,提供者比消费者个数多,可用浏览器查看,可用表单或URL传入参数,暂不支持传文件
    适用场景:需同时给应用程序和浏览器JS使用的服务

3.4 rmi

RMI协议采用 JDK 标准的java.rmi.*实现,采用阻塞式短连接和JDK标准序列化方式。

    连接个数:多连接
    连接方式:短连接
    传输协议:TCP
    传输方式:同步传输
    序列化:Java标准二进制序列化
    适用范围:传入传出参数数据包大小混合,消费者与提供者个数差不多,可传文件
    适用场景:常规远程服务方法调用,与原生RMI服务互操作

3.5 webservice

基于WebService的远程调用协议 。

    连接个数:多连接
    连接方式:短连接
    传输协议:HTTP
    传输方式:同步传输
    序列化:SOAP文本序列化
    适用场景:系统集成,跨语言调用

3.6 thrift

Thrift是一个轻量级、跨语言的远程服务调用框架,最初由Facebook开发,后面进入Apache开源项目。它通过自身的IDL中间语言, 并借助代码生成引擎生成各种主流语言的RPC服务端/客户端模板代码。

当前 dubbo 支持的 thrift 协议是对 thrift 原生协议的扩展,在原生协议的基础上添加了一些额外的头信息,比如service name,magic number等。

四、负载均衡

关于负载均衡,我已经在基础篇里介绍过了,本节主要来看下Dubbo中的负载均衡。Dubbo的负载均衡是Cluster 路由层提供的功能之一。

Dubbo 需要对Consumer的调用请求进行分配,避免少数Provider负载过大。 Dubbo采用的负载均衡属于软负载均衡中的 进程内负载均衡

所谓进程内负载均衡,指服务消费者自己根据服务提供者的地址列表,选择其中一个发起调用,如果调用失败,再选另一台调用。

Dubbo 提供了4种负载均衡算法实现:

  • RandomLoadBalance:基于权重的随机算法;
  • LeastActiveLoadBalance:基于最少活跃调用数算法;
  • ConsistentHashLoadBalance:基于一致性 hash 算法;
  • RoundRobinLoadBalance:基于加权轮询算法。

4.1 Random Load Balance

默认情况下,dubbo是使用random load balance算法,可以对provider不同实例设置不同的权重,会按照权重来负载均衡,权重越大分配流量越高,一般就用这个默认的就可以了。

4.2 Roundrobin Load Balance

该算法就是简单的轮询,但是如果各个机器的性能不一样,容易导致性能差的机器负载过高。所以此时需要调整权重,让性能差的机器承载权重小一些,流量少一些。

4.3 Leastactive Load Balance

这个就是自动感知算法,如果某台机器性能很差,那么接收的请求就越少,越不活跃,此时就会给该机器分配更少的调用请求。

4.4 Consistant Hash Load Balance

一致性Hash算法,相同key的请求一定分发到同一个provider上去,provider挂掉的时候,会基于虚拟节点均匀分配剩余的流量,抖动不会太大。如果你需要将一类请求映射到同一个Provider,那就用这个算法。

五、总结

本章,我介绍了Dubbo这一分布式RPC框架的基本架构和底层原理。

阅读全文