Java重要面试名词整理(十五):Dubbo

文章目录

概念

远程方法调用本地方法调用是相对的两个概念,本地方法调用指的是进程内部的方法调用,而远程方法调用指的是两个进程内的方法相互调用。

Apache Dubbo 是一款微服务框架,为大规模微服务实践提供高性能 RPC 通信、流量治理、可观测性等解决方案,涵盖 Java、Golang 等多种语言 SDK 实现

注意:要把Spring中的@Service注解替换成Dubbo中的@DubboService注解。

Dubbo3.0新特性介绍

注册模型的改变

Spring Cloud在进行服务注册时,是把应用名以及应用所在服务器的IP地址和应用所绑定的端口注册到注册中心,相当于key是应用名,value是ip+port,而在Dubbo2.7中,是把接口名以及对应应用的IP地址和所绑定的端口注册到注册中心,相当于key是接口名,value是ip+port。

所以在Dubbo2.7中,一个应用如果提供了10个Dubbo服务,那么注册中心中就会存储10对keyvalue,而Spring Cloud就只会存一对keyvalue,所以以Spring Cloud为首的应用级注册是更加适合的。

Dubbo3.0中将注册模型也改为了应用级注册,提升效率节省资源的同时,通过统一注册模型,也为各个微服务框架的互通打下了基础。

新一代RPC协议-Triple协议(重点)

Triple 协议的设计参考了 gRPC、gRPC-Web、通用 HTTP 等多种协议模式,吸取每个协议各自的特性和优点,最终设计成为一个易于浏览器访问、完全兼容 gRPC 且支持 Streaming 通信的协议,Triple 支持同时运行在 HTTP/1、HTTP/2 协议之上。

Triple 协议的设计目标如下:

  • Triple 设计为对人类、开发调试友好的一款基于 HTTP 的协议,尤其是对 unary 类型的 RPC 请求。
  • 完全兼容基于 HTTP/2 的 gRPC 协议,因此 Dubbo Triple 协议实现可以 100% 与 gRPC 体系互调互通。
  • 仅依赖标准的、被广泛使用的 HTTP 特性,以便在实现层面可以直接依赖官方的标准 HTTP 网络库。

官网原话:Triple 协议设计理念与规范 | Apache Dubbo

triple、dubbo、rest这三个协议特点

  • triple协议基于的是HTTP2,rest协议目前基于的是HTTP1,都可以做到跨语言。

  • triple协议兼容了gPRC(Triple服务可以直接调用gRPC服务,反过来也可以),rest协议不行

  • triple协议支持流式调用,rest协议不行

  • rest协议更方便浏览器、客户端直接调用,triple协议不行(原理上支持,当得对triple协议的底层实现比较熟悉才行,得知道具体的请求头、请求体是怎么生成的)

  • dubbo协议是Dubbo3.0之前的默认协议,triple协议是Dubbo3.0之后的默认协议,优先用Triple协议

  • dubbo协议不是基于的HTTP,不够通用,triple协议底层基于HTTP所以更通用(比如跨语言、跨异构系统实现起来比较方便)

  • dubbo协议不支持流式调用

服务导出

服务导出

当我们在某个接口的实现类上加上**@DubboService**后,就表示定义了一个Dubbo服务,应用启动时Dubbo只要扫描到了@DubboService,就会解析对应的类,得到服务相关的配置信息,比如:

  1. 服务的类型,也就是接口,接口名就是服务名
  2. 服务的具体实现类,也就是当前类
  3. 服务的version、timeout等信息,就是@DubboService中所定义的各种配置

解析完服务的配置信息后,就会把这些配置信息封装成为一个ServiceConfig对象,并调用其export()进行服务导出,此时一个ServiceConfig对象就表示一个Dubbo服务。

而所谓的服务导出,主要就是完成三件事情:

  1. 确定服务的最终参数配置
  2. 按不同协议启动对应的Server(服务暴露
  3. 将服务注册到注册中心(服务注册

服务注册

当确定好了最终的服务配置后,Dubbo就会根据这些配置信息生成对应的服务URL

确定了服务URL之后,服务注册要做的事情就是把这个服务URL存到注册中心(比如Zookeeper)中去,说的再简单一点,就是把这个字符串存到Zookeeper中去,这个步骤其实是非常简单的,实现这个功能的源码在RegistryProtocol中的export()方法中,最终服务URL存在了Zookeeper的**/dubbo/接口名/providers**目录下。

但是服务注册并不仅仅就这么简单,既然上面的这个URL表示一个服务,并且还包括了服务的一些配置信息,那这些配置信息如果改变了呢?比如利用Dubbo管理台中的动态配置功能(注意,并不是配置中心)来修改服务配置,动态配置可以应用运行过程中动态的修改服务的配置,并实时生效。

应用级注册

在Dubbo3.0之前,Dubbo是接口级注册,服务注册就是把接口名以及服务配置信息注册到注册中心中,注册中心存储的数据格式大概为:

复制代码
接口名1:tri://192.168.65.221:20880/接口名1?application=应用名
接口名2:tri://192.168.65.221:20880/接口名2?application=应用名
接口名3:tri://192.168.65.221:20880/接口名3?application=应用名

key是接口名,value就是服务URL,上面的内容就表示现在有一个应用,该应用下有3个接口,应用实例部署在192.168.65.221,此时,如果给该应用增加一个实例,实例ip为192.168.65.222,那么新的实例也需要进行服务注册

如果一个应用有3个Dubbo服务,5个实例,那么注册中心就有15条记录,此时增加一个Dubbo服务,那么注册中心就会新增5条记录,注册中心的压力也会剧烈增加。

注册中心的数据越多,数据就变化的越频繁,比如修改服务的timeout,那么对于注册中心和应用都需要消耗资源用来处理数据变化。

所以为了降低注册中心的压力,Dubbo3.0支持了应用级注册,同时也兼容接口级注册,用户可以逐步迁移成应用级注册,而一旦采用应用级注册,最终注册中心的数据存储就变成为:

复制代码
应用名:192.168.65.221:20880
应用名:192.168.65.222:20880

表示在注册中心中,只记录应用所对应的实例信息(IP+绑定的端口),这样只有一个应用的实例增加了,那么注册中心的数据才会增加,而不关心一个应用中到底有多少个Dubbo服务。

服务暴露

服务暴露就是根据不同的协议启动不同的Server,比如dubbo和tri协议启动的都是Netty,像Dubbo2.7中的http协议启动的就是Tomcat

服务引入

服务引入

我们需要利用@DubboReference注解来引入某一个Dubbo服务,应用在启动过程中,进行完服务导出之后,就会进行服务引入,属性的类型就是一个Dubbo服务接口,而服务引入最终要做到的就是给这个属性赋值一个接口代理对象。

在Dubbo2.7中,只有接口级服务注册,服务消费者会利用接口名从注册中心找到该服务接口所有的服务URL,服务消费者会根据每个服务URL的protocol、ip、port生成对应的Invoker对象,比如生成TripleInvoker、DubboInvoker等,调用这些Invoker的invoke()方法就会发送数据到对应的ip、port,生成好所有的Invoker对象之后,就会把这些Invoker对象进行封装并生成一个服务接口的代理对象,代理对象调用某个方法时,会把所调用的方法信息生成一个Invocation对象,并最终通过某一个Invoker的invoke()方法把Invocation对象发送出去,所以代理对象中的Invoker对象是关键,服务引入最核心的就是要生成这些Invoker对象。

Invoker是非常核心的一个概念,也有非常多种类,比如:

  1. TripleInvoker:表示利用tri协议把Invocation对象发送出去
  2. DubboInvoker:表示利用dubbo协议把Invocation对象发送出去
  3. ClusterInvoker:有负载均衡功能
  4. MigrationInvoker:迁移功能,后面分析,Dubbo3.0新增的

接口级服务引入

在讲服务导出时,Dubbo3.0默认情况下即会进行接口级注册,也会进行应用级注册,目的就是为了兼容服务消费者应用,用的还是Dubbo2.7,用Dubbo2.7就只能老老实实的进行接口级服务引入

接口级服务引入 核心就是要找到当前所引入的服务有哪些服务URL,然后根据每个服务URL生成对应的Invoker,流程为:

  1. 首先,根据当前引入的服务接口生成一个RegistryDirectory对象 ,表示动态服务目录,用来查询并缓存服务提供者信息。
  2. RegistryDirectory对象会根据服务接口名去注册中心,比如Zookeeper中的**/dubbo/服务接口名/providers/**节点下查找所有的服务URL
  3. 根据每个服务URL生成对应的Invoker对象,并把Invoker对象存在RegistryDirectory对象的invokers属性中
  4. RegistryDirectory对象也会监听**/dubbo/服务接口名/providers/**节点的数据变化,一旦发生了变化就要进行相应的改变
  5. 最后将RegistryDirectory对象生成一个ClusterInvoker对象,到时候调用ClusterInvoker对象的invoke()方法就会进行负载均衡选出某一个Invoker进行调用

应用级服务引入

在Dubbo中,应用级服务引入,并不是指引入某个应用,这里和SpringCloud是有区别的,在SpringCloud中,服务消费者只要从注册中心找到要调用的应用的所有实例地址就可以了,但是在Dubbo中找到应用的实例地址还远远不够,因为在Dubbo中,我们是直接使用的接口,所以在Dubbo中就算是应用级服务引入,最终还是得找到服务接口有哪些服务提供者。

MigrationInvoker的生成

上面分析了接口级服务引入和应用级服务引入,最终都是得到某个服务对应的服务提供者Invoker,那最终进行服务调用时,到底该怎么选择呢?

事实上,在进行某个服务的服务引入时,会统一利用InterfaceCompatibleRegistryProtocol的refer来生成一个MigrationInvoker对象,在MigrationInvoker中有三个属性:

复制代码
private volatile ClusterInvoker<T> invoker;  // 用来记录接口级ClusterInvoker
private volatile ClusterInvoker<T> serviceDiscoveryInvoker; // 用来记录应用级的ClusterInvoker
private volatile ClusterInvoker<T> currentAvailableInvoker; // 用来记录当前使用的ClusterInvoker,要么是接口级,要么应用级

一开始构造出来的MigrationInvoker对象中三个属性都为空,接下来会利用MigrationRuleListener来处理MigrationInvoker对象,也就是给这三个属性赋值。

核心

HTTP2简单介绍

因为Triple协议是基于HTTP2协议的,所以我们得先大概了解一下HTTP2,我们比较熟悉的是HTTP1

HTTP1协议的这种格式,缺点也是很明显的:

  1. 额外占用了很多字节,比如众多的回车符、换行符,它们都是字符,都需要一个字节
  2. 大头儿子,通常一个HTTP1的请求,都会携带各种请求头,我们可以通过请求头来指定请求体的压缩方式,但是我们没有地方可以指定请求头的压缩方式,这样就导致了大头儿子的存在

为了解决这两个严重影响性能的问题,HTTP2出来了,你不就是要发送请求行、请求头、请求体吗,那HTTP2这么来设计,HTTP2设计了帧的概念,比如分为:

  1. 帧长度,用三个字节来存一个数字,这个数字表示当前帧的实际传输的数据的大小,3个字节表示的最大数字是2的24次方(16M),所以一个帧最大为9字节+16M。、
  2. 帧类型,占一个字节,可以分为数据帧和控制帧
    1. 数据帧又分为:HEADERS 帧和 DATA 帧,用来传输请求头、请求体的
    2. 控制帧又分为:SETTINGS、PING、PRIORITY,用来进行管理的
  3. 标志位,占一个字节,可以用来表示当前帧是整个请求里的最后一帧,方便服务端解析
  4. 流标识符,占4个字节,在Java中也就是一个int,不过最高位保留不用,表示Stream ID,这也是HTTP2的一个重要设计
  5. 实际传输的数据Payload,如果帧类型是HEADERS,那么这里存的就是请求头,如果帧类型是DATA ,那么这里存的就是请求体

高效利用HTTP2中的stream流

我们在利用HTTP2发送一个请求时,首先:

  1. 新建一个TCP连接(三次握手)
  2. 新建一个Stream,生成一个新的StreamID,生成一个控制帧,帧里记录了前面生成出来的StreamID,通过TCP连接发送出去
  3. 生成一个要发送的请求对应的HEADERS 帧,用来发送请求头,也是key:value的格式,先利用ascii进行编码,然后利用HPACK算法进行压缩,最终把压缩之后的字节存在帧中的Payload区域,记录好StreamID,最后通过TCP连接把这个HEADERS 帧发送出去
  4. 最后把要发送的请求体数据按指定的压缩算法(请求中所指定的压缩算法,比如gzip)进行压缩,把压缩之后的字节生成DATA 帧,记录好StreamID,通过TCP连接把DATA 帧发送出去。

对于服务端而言:

  1. 会不断的从TCP连接接收到某些帧
  2. 当接收到一个控制帧时,表示客户端要和服务端新建一个Stream,在服务端记录一下StreamID,比如在Dubbo3.0的源码中会生成一个ServerStreamObserver的对象
  3. 当接收到一个HEADERS 帧,取出StreamID,找到对应的ServerStreamObserver对象,并解压得到请求头,把请求头信息保存在ServerStreamObserver对象中
  4. 当接收到一个DATA 帧时,取出StreamID,找到对应的ServerStreamObserver对象,根据请求头的信息看如何解压请求体,解压之后就得到了原生了请求体数据,然后按业务逻辑处理请求体
  5. 处理完了之后,就把结果也生成HEADERS 帧和DATA 帧时发送客户端,客户端此时就变成了服务端,来处理响应结果。
  6. 客户端接收到响应结果的HEADERS 帧,是也先解压得到响应头,记录响应体的解压方式
  7. 然后继续接收到响应结果的DATA 帧,解压响应体,得到原生的响应体,处理响应体

Triple的底层原理分析

就是因为HTTP2中的数据帧机制,Triple协议才能支持UNARY、SERVER_STREAM、BI_STREAM三种模式。

  1. UNARY:就是最普通的,服务端只有在接收到完请求包括的所有的HEADERS帧和DATA帧之后(通过调用onCompleted()发送最后一个DATA帧),才会处理数据,客户端也只有接收完响应包括的所有的HEADERS帧和DATA帧之后,才会处理响应结果。
  2. SERVER_STREAM:服务端流,特殊的地方在于,服务端在接收完请求包括的所有的DATA帧之后,才会处理数据,不过在处理数据的过程中,可以多次发送响应DATA帧(第一个DATA帧发送之前会发送一个HEADERS帧),客户端每接收到一个响应DATA帧就可以直接处理该响应DATA 帧,这个模式下,客户端只能发一次数据,但能多次处理响应DATA帧。(目前有Bug,gRPC的效果是正确的,Dubbo3.0需要异步进行发送)
  3. BI_STREAM:双端流,或者客户端流,特殊的地方在于,客户端可以控制发送多个请求DATA帧 (第一个DATA帧发送之前会发送一个HEADERS帧),服务端会不断的接收到请求DATA帧并进行处理,并且及时的把处理结果作为响应DATA帧发送给客户端(第一个DATA帧发送之前会发送一个HEADERS帧),而客户端每接收到一个响应结果DATA帧也会直接处理,这种模式下,客户端和服务端都在不断的接收和发送DATA帧并进行处理,注意请求HEADER帧和响应HEADERS帧都只发了一个

总结

不管是Unary,还是ServerStream,还是BiStream,底层客户端和服务端之前都只有一个Stream,它们三者的区别在于:

  1. Unary:通过流,将方法入参值作为请求体发送出去,而且只发送一次,服务端这边接收到请求体之后,会执行服务方法,得到结果,把结果返回给客户端,也只响应一次。
  2. ServerStream:通过流,将方法入参值作为请求体发送出去,而且只发送一次,服务端这边接收到请求体之后,会执行服务方法,并且会把当前流对应的StreamObserver对象也传给服务方法,由服务方法自己控制如何响应,响应几次,响应什么数据,什么时候响应结束,都由服务方法自己控制。
  3. BiStream,通过流,客户端和服务端,都可以发送和响应多次。

其他

Dubbo3中的SPI机制

SPI的作用域

首先,新建一个Hello接口,和HelloImpl实现类:

java 复制代码
@SPI
public interface Hello {
}

public class HelloImpl implements Hello{
}

注意,在Hello接口上增加@SPI,表示该接口支持SPI,HelloImpl就是该接口的一种扩展实现。

然后在resources下新建META-INF文件夹,再在该文件夹下新建dubbo文件夹,最终路径为META-INF/dubbo。

注意,不要一次性直接建一个META-INF/dubbo文件,不然可能就是一个文件夹,名字叫"META-INF/dubbo",一定要分两次建。

Dubbo中的SPI机制有几种作用域:

  1. FrameworkModel
  2. ApplicationModel
  3. ModuleModel

它们是上下级关系,一个FrameworkModel可以包含多个ApplicationModel,一个ApplicationModel可以包含多个ModuleModel。

所以我们可以控制一个扩展点的作用域,上文中我们在Hello接口上定义了@SPI注解,该注解有一个属性scope,表示Hello接口这个扩展点的作用域,默认是ExtensionScope.APPLICATION,表示Hello接口这个扩展点的作用域为应用,表示Hello这个扩展点在当个应用内是单例的,既某个ApplicationModel可以通过名字重复得到同一扩展点。

所以,我们如果这么来写,是会报错的,那FrameworkModel来获取Hello扩展点是拿不到的,应用Hello的作用域是Application,而不是Framework。

依赖注入

Dubbo中的依赖注入并不像Spring中的依赖注入,有一些限制条件:

  1. 只支持setter注入,也就是某个属性得有对应的set方法,Dubbo中会调用set方法来给对应的属性赋值。
  2. 所赋的值是该属性对应接口的Adaptive对象
  3. 而要生成一个Adaptive对象,就需要该接口中有Adaptive的方法(有@Adaptive,并且有URL入参)

Adaptive表示自适应的意思,所以接口的Adaptive扩展点,会在真正执行某个方法的时候才会根据URL中的信息去确定扩展点。

相关推荐
tkevinjd几秒前
力扣146LRU缓存
面试
INFINI Labs2 分钟前
使用 Docker Compose 轻松实现 INFINI Console 离线部署与持久化管理
java·docker·eureka·devops·docker compose·console·easyserach
Cosolar3 分钟前
国产麒麟系统 aarch64 架构 PostgreSQL 15 源码编译安装完整教程
java·后端
GalaxyPokemon10 分钟前
PlayerFeedback 插件开发日志
java·服务器·前端
天天摸鱼的java工程师1 小时前
别再写那些重复代码了!8年Java老兵教你用 Hutool 提升开发效率
java·后端
南山安1 小时前
面试必考点: 深入理解CSS盒子模型
javascript·面试
喝杯绿茶1 小时前
springboot中的事务
java·spring boot·后端
TimelessHaze1 小时前
🧱 一文搞懂盒模型box-sizing:从标准盒到怪异盒的本质区别
前端·css·面试
麦兜*1 小时前
多阶段构建:打造最小化的 Spring Boot Docker 镜像
java·spring boot·后端·spring cloud·docker
oak隔壁找我1 小时前
Spring Boot Starter 入门教程
java·后端