首先声明,本文并不是什么代码实战类型的文章,适合于想对dubbo有更加全面认识的读者阅读,文章不会过于深奥,只是将一系列的知识点串通起来,帮助读者温故而知新。

RPC服务的介绍

相信有过一些分布式开发经历的读者都有用过一些RPC框架,通过框架包装好之后提供的API接口调用远程服务,体验感觉起来就和调用本地服务一样轻松。这么方便好用的技术框架,在实际的开发过程中是如何包装的呢?

很早的时候,国外的工程师设计了一种能够通过A计算机调用B计算机上边应用程序的技术,这种技术不需要开发人员对于网络通讯了解过多,并且调用其他机器上边程序的时候和调用本地的程序一样方便好用。

A机器发起请求去调用B机器程序的时候会被挂起,B机器接收到A机器发起的请求参数之后会做一定的参数转换,最后将对应的程序结果返回给A,这就是最原始的RPC服务调用了。

RPC调用的优势

简单

不需要开发者对于网络通信做过多的设置,例如我们在使用http协议进行远程接口调用的时候,总是会需要编写较多的http协议参数(header,context,Accept-Language,Accept-Encode等等),这些处理对于开发人员来说,实际上都并不是特别友好。但是RPC服务调用框架通常都将这类解析进行了对应的封装,大大降低了开发人员的使用难度。

高效

在网络传输方面,RPC更多是处于应用层和传输层之间。这里我们需要先理清楚一个问题,网络分层。RPC是处于会话层的部分,相比处于应用层的HTTP而言,RPC要比Rest服务调用更加轻便。

常见的远程调用技术

rmi

利用java.rmi包实现,基于Java远程方法协议(Java Remote Method Protocol) 和java的原生序列化

Hessian

是一个轻量级的remoting onhttp工具,使用简单的方法提供了RMI的功能。基于HTTP协议,采用二进制编解码。

protobuf-rpc-pro

是一个Java类库,提供了基于 Google 的 Protocol Buffers 协议的远程方法调用的框架。基于 Netty 底层的 NIO 技术。支持 TCP 重用/ keep-alive、SSL加密、RPC 调用取消操作、嵌入式日志等功能。

Thrift

是一种可伸缩的跨语言服务的软件框架。它拥有功能强大的代码生成引擎,无缝地支持C + +,C#,Java,Python和PHP和Ruby。thrift允许你定义一个描述文件,描述数据类型和服务接口。依据该文件,编译器方便地生成RPC客户端和服务器通信代码。

最初由facebook开发用做系统内部语言之间的RPC通信,2007年由facebook贡献到apache基金 ,现在是apache下的opensource之一 。支持多种语言之间的RPC方式的通信:php语言client可以构造一个对象,调用相应的服务方法来调用java语言的服务,跨越语言的C/S RPC调用。底层通讯基于SOCKET。

Avro

出自Hadoop之父Doug Cutting, 在Thrift已经相当流行的情况下推出Avro的目标不仅是提供一套类似Thrift的通讯中间件,更是要建立一个新的,标准性的云计算的数据交换和存储的Protocol。支持HTTP,TCP两种协议。

Dubbo

Dubbo是 阿里巴巴公司开源的一个高性能优秀的服务框架**,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成。**

上边我们说到了RPC的远程调用发展历史,那么下边我们一起来深入探讨一下RPC的服务。

首先我们来看看OSI的网络协议内容。

OSI的七层网络模型

对于OSI的七层网络模型我绘制了下边的这么一张图:

大白话带你梳理一下Dubbo的那些事儿

下边是我个人对于这七层协议的理解:

  • 应用层 主要是对于服务接口的格式多定义,例如提供一定的终端接口暴露给外部应用调用。

  • 表示层 处理一些数据传输的格式转换,例如说编码的统一,加密和解密处理。

  • 会话层 管理用户的会话和对话,建立不同机器之间的会话连接。

  • 传输层 向网络层提供可靠有序的数据包信息。

  • 网络层 真正发送数据包信息的层面,提供流和拥塞控制,从而降低网络的资源损耗。

  • 数据链路层 封装对应的数据包,检测和纠正数据包传输信息。

  • 物理层 通过网络通讯设备发送数据

HTTP & RPC

HTTP主要是位于TCP/IP协议栈的应用层部分,首先需要构建三次握手的链接,接着才能进行数据信息的请求发送,最后进行四次挥手断开链接。

RPC在请求的过程中跨越了传输层和应用层,这是因为它本身是依赖于Socket的原因。(再深入的原因我也不知道)。减少了上边几层的封装,RPC的请求效率自然是要比HTTP高效很多。

那么一个完整的RPC调用应该包含哪些部分呢?

通常我们将一个完整的RPC架构分为了以下几个核心组件:

  • Server

  • Client

  • Server Stub

  • Client Stub

这四个模块中我稍微说下stub吧。这个单词翻译过来称之为存根。

*Client Stub *就是将客户端请求的参数,服务名称,服务地址进行打包,统一发送给server方。

*Server Stub *我用通俗易懂的语言来解释就是服务端接收到Client发送的数据之后进行消息解包,调用本地方法。(看过netty拆包机制应该会对这块比较了解)。

Dubbo的核心属性

其实Dubbo配置里面的核心内容就是 _服务暴露,服务发现,服务治理_。

什么是服务暴露,服务发现,服务治理?

下边我们用一段xml的配置来进行讲解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<dubbo:application name="dubbo-invoker-provider">
<dubbo:parameter key="qos.port" value="22222"/>
</dubbo:application>
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
<dubbo:protocol name="dubbo" port="20880"/>
<bean id="userService" class="com.sise.user.service.UserServiceImpl" />
<dubbo:service interface="com.sise.user.service.UserService" ref="userService" />
</beans>

在dubbo的配置文件里面,通常我们所说的dubbo:service 可以理解为服务暴露,dubbo:refernce 为服务发现,mock是服务治理,timeout属于服务治理的一种(性能调优).

假设dubbo里面希望将一些公共的配置抽取出来,我们可以通过properties文件进行配置,dubbo在加载配置文件的优先顺序如下:

  1. 优先会读取JVM -D启动参数后边的内容

  2. 读取xml配置文件

  3. 读取properties配置文件内容

dubbo默认会读取dubbo.properties配置文件的信息,例如下边这种配置:

1
2
dubbo.application.name=dubbo-user-service
dubbo.registry.address=zookeeper://127.0.0.1:2181

假设我们的dubbo配置文件不命名为dubbo.properties(假设命名为了my-dubbo.properties)的时候,可以在启动参数的后边加上这么一段指令:

1
-Ddubbo.properties.file=my-dubbo.properties

那么在应用程序启动之后,对应的工程就会读取指定的配置文件,这样就可以将一些共用的dubbo配置给抽取了出来。

XML和配置类的映射

在工作中,我们通常都会通过配置xml的方式来设定一个服务端暴露的服务接口和消费端需要调用的服务信息,这些配置的xml实际上在dubbo的源码中都会被解析为对应的实体类对象。

例如说我们常用到的reference配置类,下边我贴出一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.sise.user.config;
import com.sise.user.service.UserService;
import com.sise.user.service.UserServiceImpl;
import org.apache.dubbo.config.*;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
/**
* dubbo里面的自定义配置类
*
* @author idea
* @data 2019/12/29
*/
public class DubboSelfDefConfig {
/**
* dubbo的服务暴露
*/
public void server() {
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("dubbo-server-config");
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("zookeeper://127.0.0.1:2181");
ProtocolConfig protocolConfig = new ProtocolConfig();
protocolConfig.setName("dubbo");
protocolConfig.setPort(20880);
protocolConfig.setThreads(200);
UserService userService = new UserServiceImpl();
ServiceConfig<UserService> serviceConfig = new ServiceConfig<>();
serviceConfig.setApplication(applicationConfig);
serviceConfig.setRegistry(registryConfig);
serviceConfig.setProtocol(protocolConfig);
serviceConfig.setInterface(UserService.class);
serviceConfig.setRef(userService);
serviceConfig.export();
}

public void consumer() {
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("dubbo-client-config");
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("zookeeper://127.0.0.1:2181");
ReferenceConfig<UserService> referenceConfig = new ReferenceConfig<>();
referenceConfig.setApplication(applicationConfig);
referenceConfig.setRegistry(registryConfig);
referenceConfig.setInterface(UserService.class);
UserService localRef = referenceConfig.get();
localRef.echo("idea");
}
public static void main(String[] args) throws InterruptedException, IOException {
DubboSelfDefConfig d = new DubboSelfDefConfig();
d.consumer();
CountDownLatch countDownLatch = new CountDownLatch(1);
countDownLatch.await();
}
}

在这段代码里面,通过案例可以发现有这些信息内容:

1
2
UserService localRef = referenceConfig.get();
localRef.echo("idea");

这两行语句是获取具体服务的核心之处,由于我在别处定义了一个叫做UserService 的公共服务接口,因此在服务引用的过程中可以进行转换。

Dubbo2.7的三大新特新

Dubbo的github官方地址为 https://github.com/apache/dubbo

大白话带你梳理一下Dubbo的那些事儿在这里插入图片描述

Dubbo 目前有如图所示的 5 个分支,其中 2.7.1-release 只是一个临时分支,忽略不计,对其他 4 个分支而言,我归纳了一下,分别有如下信息:

  • 2.5.x 近期已经通过投票,Dubbo 社区即将停止对其的维护。

  • 2.6.x 为长期支持的版本,也是 Dubbo 贡献给 Apache 之前的版本,其包名前缀为:com.alibaba,JDK 版本对应 1.6。

  • 3.x-dev 是前瞻性的版本,对 Dubbo 进行一些高级特性的补充,如支持 rx 特性。

  • master 为长期支持的版本,版本号为 2.7.x,也是 Dubbo 贡献给 Apache 的开发版本,其包名前缀为:org.apache,JDK 版本对应 1.8。

Dubbo 2.7 新特性

Dubbo 2.7.x 作为 Apache 的孵化版本,除了代码优化之外,还新增了许多重磅的新特性,本文将会介绍其中最典型的2个新特性:

  • 异步化改造

  • 三大中心改造

异步化改造

1.异步化调用的方式,在Dubbo2.7版本里面提供了异步化调用的功能,相关案例代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping(value = "/test")
public class TestController {
@Reference(async = true)
private UserService userService;

@GetMapping("/testStr")
public String testStr(String param){
return userService.testEcho(param);
}
}

但是通过这种异步发送的方式我们通常都是获取不到响应值的,所以这里的return为null。

如果在低于2.7版本的dubbo框架中希望获取到异步返回的响应值还是需要通过RPC上下文来提取信息。

代码案例如下所示:

1
2
3
4
5
6
7
8
@GetMapping("/futureGet")
public String futureGet(String param) throws ExecutionException, InterruptedException {
userService.testEcho(param);
Future<String> future= RpcContext.getContext().getFuture();
String result = future.get();
System.out.println("this is :"+result);
return result;
}

通过RPC上下文的方式可以取到对应的响应值,但是这种方式需要有所等待,因此此时的效率会有所降低。假设我们将dubbo的版本提升到了2.7.1之后,通过使用CompletableFuture来进行接口优化的话,这部分的代码实现就会有所变化:

1
2
3
4
5
6
7
8
9
10
11
/**
* @author idea
* @date 2019/12/31
* @Version V1.0
*/
public interface DemoService {
String sayHello(String name) ;
default CompletableFuture<String> sayAsyncHello(String name){
return CompletableFuture.completedFuture(sayHello(name));
}
}

调用方代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.sise.consumer.controller;

import com.sise.dubbo.service.DemoService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author idea
* @date 2019/12/31
* @Version V1.0
*/
@RestController
@RequestMapping(value = "/demo")
public class DemoController {

@Reference
private DemoService demoService;

@RequestMapping(value = "/testDemo")
public String testDemo(String name){
System.out.println("【testDemo】 this is :"+name);
return demoService.sayHello(name);
}.

@RequestMapping(value = "/testAsyncDemo")
public String testAsyncDemo(String name){
System.out.println("【testAsyncDemo】 this is :"+name);
CompletableFuture<String> future = demoService.sayAsyncHello(name);
AtomicReference<String> result = null;
//通过一条callback线程来处理响应的数据信息
future.whenComplete((retValue,exception)->{
if(exception==null){
System.out.println(retValue);
result.set(retValue);
} else {
exception.printStackTrace();
}
});
return "通过一条callback线程来处理响应的数据信息,所以这个时候获取不到信息响应";
}
}

这样的调用是借助了callback线程来帮我们处理原先的数据内容,关于dubbo里面的异步化调用,我借用了官方的一张图来进行展示:

大白话带你梳理一下Dubbo的那些事儿

我们上边讲解的众多方法都只是针对于dubbo的客户端异步化,并没有讲解关于服务端的异步化处理,这是因为结合dubbo的业务线程池模型来思考,服务端的异步化处理比较鸡肋(因为dubbo内部服务端的线程池本身就是异步化调用的了)。

当然dubbo 2.6 里面对于接口异步化调用的配置到了2.7版本依旧有效。

三大中心的改造

注册中心

在dubbo2.7之前,dubbo主要还是由consumer,provider ,register组成,然而在2.7版本之后,dubbo的注册中心被拆解为了三个中心,分别是原先的注册中心元数据中心以及配置中心

元数据配置

在dubbo2.7版本中,将原先注册在zk上边的过多数据进行了注册拆分,这样能够保证减少对于zk端的压力。具体配置如下:

1
<dubbo:registry address=“zookeeper://127.0.0.1:2181” simplified="true"/>

简化了相应配置之后,dubbo也只会上传一些必要的服务治理数据了,简化版本的服务数据只剩下下边这些信息:

1
2
3
4
5
dubbo://30.5.120.185:20880/com.sise.TestService?
application=test-provider&
dubbo=2.0.2&
release=2.7.0&
timestamp=1554982201973

对于其他的元数据信息将会被存储到一些元数据中心里面,例如说redis,nacos,zk等

元数据配置改造主要解决的问题是:推送量大 -> 存储数据量大 -> 网络传输量大 -> 延迟严重

配置中心

dubbo2.7开始支持多种分布式配置中心的组件。例如说:zk,Spring Cloud Config, Apollo, Nacos,关于这部分的配置网上的资料也比较多,我就不在这里细说了。

END