微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

【Dubbo】高级特性及原理

主要内容

  • dubbo 高级特性概述 ;
  • dubbo 高级特性原理 。

首先对 dubbo 支持的高级特性进行介绍 , 然后给出使用这些高级特性的示例 , 帮助读者更好地理解高级特性 , 最后对常用的高级特性的原理进行深入的分析 , 帮助读者更好地理解和掌握 dubbo 框架 。 当发现 dubbo 无法满足业务诉求时 , 也能进行深入的定制或扩展 。

1 dubbo 高级特性概述

dubbo 解决了分布式场景 RPC 通信调用的问题 , 但是要满足各种业务场景还是不够的 。 举个例子 , 支付业务需要自身迭代版本 , 比如 1.0 版本和 2.0 版本 , 在 2.0 版本做了大量性能改进 ,需要发布到性能测试环境与 1.0 版本做对比 , 这个时候需要框架提供服务隔离的能力 。 再举另外一个场景的例子 , 客户端消费远程服务时不希望阻塞 , 这个时候业务方可以在线程池中发起RPC 调用 , 但是这样不够优雅 , 需要框架支持异步调用和回调 。

目前 dubbo 框架在支持 RPC 通信的基础上 , 提供了大量的高级特性 , 比如服务端 Telnet 调用 、 Telnet 调用统计 、 服务版本和分组隔离 、 隐式参数 、 异步调用 、 泛化调用 、 上下文信息和结果缓存等特性 。 本章会对常用的高级特性原理做进一步分析 , 在表中展示了目前 dubbo支持的高级特性

在这里插入图片描述

当然 dubbo 提供的特性远远不止这些 , 比如并发控制和连接控制等 , 完整的特性请参考官方文档 , 我们这里主要对常用的特性进行分析 。

2 服务分组和版本

dubbo 中提供的服务分组和版本是强隔离的 , 如果服务指定了服务分组和版本 , 则消费方调用也必须传递相同的分组名称和版本名称

假设我们有订单查询接口 com.alibaba.pay.order.QueryService, 这个接口包含不同的版本实现 , 比如版本分别为 1.0.0-stable 和 2.0.0, 在服务端对应的实现名称分别为 com.alibaba .pay.order.StableQueryService 和 com.alibaba.pay.order .PerfomanceQueryService 。 我们可以在服务暴露时指定配置

服务暴露指定版本

在这里插入图片描述

服务暴露直接配置 version 属性即可 , 如果要为服务指定分组 , 则继续添加 group 属性即可 。 因为这个特性是强隔离的 , 消费方必须在配置文件中指定消费的版本 。 如果消费方式为泛化调用或注解引用 , 那么也需要指定对应的相同名称的版本号

消费方指定版本

在这里插入图片描述

在消费方 〈 dubbo:reference 〉 标签中指定要消费的版本号时 , 在服务拉取时会在客户端做一次过滤 。 如果要消费指定的分组 , 那么还需要指定 group 属性 。当服务提供方进行服务暴露时 , 服务端会根据 serviceGroup/serviceName:serviceversion:port组合成 key, 然后服务实现作为 value 保存在 dubboProtocol 类的 exporterMap 字段中 。 这个字段是一个 HashMap 对象 , 当服务消费调用时 , 根据消费方传递的服务分组 、 服务接口 、 版本号和服务暴露时的协议端口号重新构造这个 key, 然后从内存 Map 中查找对应实例进行调用

当客户端指定了分组和版本时 , 在 dubbolnvoker 构造函数中会将 URL 中包含的接口 、 分组 、 Token 和 timeout 加入 attachment, 同时将接口上的版本号存储在 version 字段 。 当发起 RPC请求时 , 通过 dubboCodec 把这些信息发送到服务器端 , 服务器端收到这些关键信息后重新组装成 key, 然后查找业务实现并调用

dubbo 客户端启动时是如何获取指定分组和服务版本对应的调用列表的呢 ? 当 dubbo 客户端启动时 , 实际上会把调用接口所有的协议节点都拉取下来 , 然后根据本地 URL 配置的接口 、category 、 分组和版本做过滤 , 具体过滤是在注册中心层面实现的 。 以 ZooKeeper 注册中心为例 ,当注册中心推送列表时 , 会调用 ZookeeperRegistry#toUrlsWithoutEmpty 方法 , 这个方法会把所有服务列表进行一次过滤 ,

过滤服务分组和版本

在这里插入图片描述

dubbo 中接收服务列表是在 RegistryDirectory 中完成的 , 它收到的列表是全量的列表 。RegistryDirectory 主要将 URL 转换成可以调用的 Invokers。 在获取列表前会经过①把服务列表解码 , 用于解码被转译的字符 。 消费指定分组和版本关键逻辑在②中 , 它会将特定接口的全量列表和消费方 URL 进行匹配 , 匹配规则是校验接口名 、 类别 、 版本和分组是否一致 。 消费方认的类别是 providers

3 参数回调

dubbo 支持异步参数回调 , 当消费方调用服务端方法时 , 允许服务端在某个时间点回调回客户端的方法 。 在服务端回调到客户端时 , 服务端不会重新开启 TCP 连接 , 会复用已经建立的从客户端到服务端的 TCP 连接 。 在讲解参数回调前 , 我们给出一个参数回调的例子然后对其实现原理进行分析 。

异步回调服务端实现

在这里插入图片描述


要实现异步参数回调 , 我们首先定义一个服务提供者接口 , 这里举例为 Callbackservice 注意其方法的第 2 个参数是接口 , 被回调的参数顺序不重要 。 第 2 个参数代表我们想回调的客户端 CallbackListener 接口 , 具体什么时候回调和调用哪个方法是由服务提供方决定的 。 对应到代码清单中 , ①是我们定义的服务提供方服务接口 , 定义了 addListener 方法 , 用于给客户端调用 。 ②定义了客户端回调接口 , 这个接口实现在客户端完成 。 ③对应普通 dubbo 服务实现 。 当客户端调用 addListener 方法时 , 会将客户端回调实例加入 listeners, 用于服务端定时回调客户端 。 服务提供者在初始化时会开启一个线程 , 它轮询检查是否有回调加入 , 如果有则每隔 5 秒回调客户端 。 在⑤中每隔 5 秒处理多个回调方法

当服务提供方完成后 , 我们需要编写消费方代码 , 用于调用服务提供者 addListener 方法 ,把客户端加入回调列表 ,

消费异步回调服务

在这里插入图片描述


客户端调用也是非常简单的 , 主要是调用服务提供者服务并把自己加入回调列表 , 同时指定 key 和对应的回调方法 。 ①会获取 Spring 的消费配置 <dubbo:reference …> 实例 , 调用CallbackService#addListener, 然后创建接口匿名类实现 。

在服务暴露和消费代码写完后 , 接下来我们需要做适当配置 , 告诉 dubbo 框架哪个参数是异步回调

异步参数回调配置

在这里插入图片描述


可以发现服务提供方要想实现回调 , 就需要指定回调方法参数是否为回调 , 对于客户端消费方来说没有任何区别 。

实现异步回调的原理比较容易理解 , 客户端在启动时 , 会拉取服务 Callbackservice 元数据 ,因为服务端配置了异步回调信息 , 这些信息会透传给客户端 。 客户端在编码请求时 , 会发现第2 个方法参数为回调对象 。 此时 , 客户端会暴露一个 dubbo 协议的服务 , 服务暴露的端口号是本地 TCP 连接自动生成的端口 。 在客户端暴露服务时 , 会将客户端回调参数对象内存 id 存储在 attachment 中 , 对应的 key 为 sys_callback_arg- 回调参数索引 。 这个 key 在调用普通服务addListener 时会传递给服务端 , 服务端回调客户端时 , 会把这个 key 对应的值再次放到attachment 中传给客户端 。 从服务端回调到客户端的 attachment 会用 keycallback.service,instid 保存回调参数实例 id, 用于查找客户端暴露的服务

客户端调用服务端方法时 , 并不会把第 2 个异步参数实例序列化并传给服务端 。 当服务端解码时 , 会先检查参数是不是异步回调参数 。 如果发现是异步参数回调 , 那么在服务端解码参数值时,会自动创建到消费方的代理 。 服务端创建回调代理实例 Invoker 类型是 ChannelWrappedlnvoker,比较特殊的是 , 构造函数的 service 值是客户端暴露对象 id, 当回调发生时 , 会把keycallback, service, instid 保存的对象 id 传给客户端 , 这样就能正确地找到客户端暴露的服务了 。

4 隐式参数

dubbo 服务提供者或消费者启动时 , 配置元数据会生成 URL, 一般是不可变的 。 在很多实际的使用场景中 , 在服务运行期需要动态改变属性值 , 在做动态路由和灰度发布场景中需要这个特性 。 dubbo 框架支持消费方在 RpcContext#setAttachment 方法中设置隐式参数 , 在服务端RpcContext#getAttachment 方法获取隐式传递 。

当客户端发起调用前 , 设置隐藏参数 , 框架会在拦截器中把当前线程隐藏参数传递到Rpclnvocation 的 attachment 中 , 服务端在拦截器中提取隐藏参数并设置到当前线程 RpcContext中 。 隐式传参的详细原理如图所示

在这里插入图片描述


通过 API 的方式设置参数和提取参数

隐式传参使用

在这里插入图片描述


在消费方调用服务方传递隐式参数时 , 会在 Abstractlnvoker#invoke 方法调用中合并RpcContext#getAttachments () 参数 。 用户的隐式参数会被合并到 Rpclnvocation 中的 attachment字段 , 这个字段发送给服务端 。 在服务提供方收到请求时 , 在 ContextFilter#invoke 中提取Rpclnvocation 中的 attachment 信息 , 并设置到当前线程上下文中 。 因为后端业务方法调用拦截器在同一个线程中执行 , 所以直接使用 RpcContext . getContext () . getAttachment 获取值即可 。 在图中会发现客户端在拦截器中 (ConsumercontextFilter) 执行 setAttachements 方法 , 这个主要支持服务端透传隐式参数给客户端 。

5 异步调用

本节主要聚焦 dubbo 在客户端支持异步调用方面的内容 ,2.7.0 + 版本在服务端支持异步调用 。 在客户端实现异步调用非常简单 ,在消费接口时配置异步标识 , 在调用时从上下文中获取 Future 对象 , 在期望结果返回时再调用阻塞方法 Future.get () 即可 。 我们给出了在客户端实现异步调用的实例

客户端异步调用

在这里插入图片描述


通过代码清单, 我们知道在客户端发起异步调用时 , 应该在保存当前调用的 Future 后 ,再发起其他远程调用 , 否则前一次异步调用的结果可能丢失(异步 Future 对象会被上下文覆盖) 。因为框架要明确知道用户意图 , 所以需要再明确开启使用异步特性 , 在 <dubbo:reference …>标签中指定 async 标记

消费方配置异步标识

在这里插入图片描述

dubbo 异步调用流程如图 9-2 所示

在这里插入图片描述


站在 dubbo 客户端角度来说 , 直接发起 RPC 调用端属于用户线程 。 用户线程①发起任意远程方法调用 , 最终会通过 I/O 线程发送网络报文 。 在真实发送报文前会在用户线程中设置当前异步请求 Future (③) 。 因此在用户线程发起下一个远程方法调用前 , 需要先保存异步 Future对象(④ )o dubbo 框架会把异步请求对象保存在 DefaultFuture 类中 , 当服务端响应或超时时 ,被挂起的用户线程将被唤醒(⑤) 。 用户线程设置异步 Future 对象的逻辑在dubbolnvoker#dolnvoke 方法中完成 , 感兴趣的读者可以参阅 dubbolnvoker 中对应的源码实现

6 泛化调用

dubbo 泛化调用特性可以在不依赖服务接口 API 包的场景中发起远程调用 。 这种特性特别适合框架集成和网关类应用开发 。 dubbo 在客户端发起泛化调用并不要求服务端是泛化暴露 。假设我们调用服务端 com . xxx . XxxService#sayHello 方法

泛化调用示例

在这里插入图片描述

目前泛化调用必需的参数主要包括应用名称注册中心(或者是直连调用地址) 、 真实接口名称和泛化标识 。 在发起远程服务调用时 , GenericService 方法参数类型分别为真实方法名 、真实方法参数类型签名和真实参数值 。 这里有一个注意事项 , 每次动态创建的 GenericService实例比较重 , 需要建立 TCP 连接 , 处理注册中心订阅和服务列表等计算 , 因此需要缓存ReferenceConfig 对象进行复用 。 但是往往很多业务开发时 , 忘记设置 ReferenceConfig 对象的Check 方法为 false, 导致在没有服务提供者时 , 触发框架抛出 No provider available 的异常 ,从而导致缓存命中失败 。

其实泛化的实现原理相对比较好理解 , 服务端在处理服务调用时 , 在 GenericFilter 拦截器中先把 Rpclnvocation 中传递过来的参数类型和参数值提取出来 , 然后根据传递过来的接口名 、方法名和参数类型查找服务端被调用方法获取真实方法后 , 主要提取真实方法参数类型(可能包含泛化类型) , 然后将参数值做 Java 类型转换 。 最后用解析后的参数值构造新的Rpclnvocation 对象发起调用

7 上下文信息

dubbo 上下文信息的获取和存储同样是基于 JDK 的 ThreadLocal 实现的 。 上下文中存放的是当前调用过程中所需的环境信息 。 RpcContext 是一个 ThreadLocal 的临时状态记录器 , 当收到或发送 RPC 时 , 当前线程关联的 RpcContext 状态都会变化 。 比如 : A 调用 B, B 再调用 C,则在 B 机器上 , 在 B 调用 C 之前 , RpcContext 记录的是 A 调用 B 的信息 , 在 B 调用 C 之后 ,RpcContext 记录的是 B 调用 C 的信息 。

假设在服务端有 DemoServicelmpl 实现 , 在代码清单中展示了上下文使用实例 :

服端上下文的获取和使用

在这里插入图片描述


在客户端和服务端分另 U 有一个拦截设置当前上下文信息 , 对应的分别为 ConsumerContextFilter和 ContextFilter。在客户端拦截器实现中 , 因为 Invoker 包含远程服务信息 , 因此直接设置远程 IP 等信息 。 在服务端拦截器中主要设置本地地址 , 这个时候无法获取远程调用地址 。 设置远程地址主要在 dubboProtocol#ExchangeHandlerAdapter.reply 方法中完成 , 可以直接通过channel.getRemoteAddress 方法获取

8 Telnet 操作

目前 dubbo 支持通过 Telnet 登录进行简单的运维 , 比如查看特定机器暴露了哪些服务 、 显示服务端口连接列表 、 跟踪服务调用情况 、 调用本地服务和服务健康状况等 。为了避免和前面章节讲解重复 , 本节我们主要讲解 Is 、 ps> trace 和 count 命令的实现和原理 。

当服务发布时 , 如果注册中心没有对应的服务 , 那么我们可以初步使用 Is 命令检查 dubbo服务是否正确暴露了 。 Is 主要提供了查询已经暴露的服务列表 、 查询服务详细信息和查询指定服务接口信息等功能 。 Is 命令的用法如下 :Is options [service]

命令说明 :

  • service 代表要查询的服务接口名称 , 可以是短名称或全名称
  • options 代表支持的命令参数 ;
  • -l 显示服务详细信息列表或服务方法的详细信息
    先启动服务提供应用程序 ( dubbo-samples-echo 子模块下任意 server), Is 命令示例如表 9-2
    所示

    在这里插入图片描述


    Is 命令的实现主要基于 ListTelnetHandler, dubbo 框架的 Telnet 调用只对 dubbo 协议提供支持 。 它的原理非常简单 , 当服务端收到 Is 命令和参数时 , 会加载 ListTelnetHandler 并执行 ,然后触发 dubboProtocol.getdubboProtocol () . getExporters () 方法获取所有已经暴露的服务 ,获取暴露的接口名和暴露服务别名 ( path 属性)进行匹配 , 将匹配的结果进行输出 。 如果是查看服务暴露的方法 , 则框架会获取暴露接口名 , 然后反射获取所有方法输出

ps 命令用于查看提供服务本地端口的连接情况 , ps 的命令用法如下 :ps options [port]
命令说明 :

  • port 代表要查询的服务暴露的端口 ;
  • options 代表支持的命令参数 ;
  • -l 显示服务暴露的所有端口或服务端端口建立连接的信息 。

ps 命令示例如表所示

在这里插入图片描述


ps 命令实现类对应 PortTelnetHandler 类 , 当 dubbo 服务暴露时 , 会把关联端口的服务端实例加入 dubboProtocol 类的 serverMap 字段 。 当执行 ps 命令时 , PortTelnetHandler 类会通过dubboProtocol . getdubboProtocol () . getServers () 提取暴露的 server 实例 。 它持有了端口号和所有客户端连接信息等 。 当无法确认命令对应的后端实现时 , 可以查找和扩展点名称相同的文件,它包含扩展点所有的实现定义 , 比如 com . alibaba.dubbo . remoting . telnet . TelnetHandler

trace 用于统计服务方法调用信息 , 比如跟踪服务调用方法返回值 、 连接信息和耗时等 。trace 命令示例如表 9-4 所示

在这里插入图片描述

trace service [method] [count]
命令说明 :

如果在使用 trace 命令跟踪方法调用时指定了最大次数 , 则不需要重复执行 trace 命令 , 当服务接口方法调用超过了最大次数后 , 不会把调用结果信息推送给 Telnet 客户端 。trace 命令对应的实现类是 TraceTelnetHandler, 它本身不会执行任何方法调用 , 首先根据传递的接口和方法查找对应的 Invoker, 然后把当前的 Telnet 连接 ( Channel ) 、 接口 、 方法和最大执行次数信息记录在 TraceFilter 中 , 当接口方法调用时 , TraceFilter 会取出对应的 Telnet连接 ( Channel ) , 并把调用结果信息发送的 Telnet 客户端 。

count 命令也用于统计服务信息 , 但它主要统计方法调用成功数 、 失败数 、 正在并发执行数 、平均耗时和最大耗时 。 如果在服务方暴露服务时配置了 executes 属性 , 那么使用 count 命令可以统计并发调用信息 。

count 命令示例如表所示

在这里插入图片描述


count service [method] [count]
命令说明 :

count 命令对应的实现类是 CountTelnetHandler, 每次执行 count 命令时在服务端会启动一个线程去循环统计当前调用次数 。 比如统计 10 次 , 在线程中每间隔 1 秒执行一次统计 , 直到达到统计次数退出线程 。 框架会使用 RpcStatus 类记录并发调用信息 , CountTelnetHandler 负责提取这些统计信息并输出给 Telnet 客户端 。

9 Mock 调用

dubbo 提供服务容错的能力 , 通常用于服务降级 , 比如验权服务 , 当服务提供方 “ 挂掉 ”后 , 客户端不抛出异常 , 而是通过 Mock 数据返回授权失败 。

目前 dubbo 提供以下几种方式来使用 Mock 能力 :

  • (1) <dubbo:reference mock=“true” …/>
  • (2) <dubbo:reference mock=“com.foo.BarServiceMock” ・・ ・/>
  • (3) <dubbo:reference mock=“return null” …/>
  • (4) <dubbo:reference mock=“throw com.alibaba.XXXException” ・・・/>
  • (5) <dubbo:reference mock=“force:return fake” ・・・/>
  • (6) <dubbo:reference mock="force:throw com.foo.MockException " ・・・・/>

dubbo 服务提供者调用抛出 RpcException 时 , 框架会降级到本地 Mock 伪装 。 以接口com.foo.BarService 例 , 第 1 种和第 2 种的使用方式是等价的 , 当直接指定 mock=true 时 ,客户端启动时会查找并加装 com . foo. BarServiceMock 类 。 查找规则根据接口名加 Mock 后缀组合成新的实现类 , 当然也可以使用自己的 Mock 实现类指定给 Mock 属性

当在 Mock 中指定 return null 时 , 允许调用失败返回空值 。 当在 Mock 中指定 throw 或 throw com.alibaba . XXXException 时 , 分别会抛出即 cException 和用户自定义异常 com.alibaba.XXXException 。2.6.5 版本以前 ( 包括当前版本 ) , 因为实现有缺陷 , 在使用方式 4 、 5 和 6 中需要更新后的版本支持 。 目前认场景都是在没有服务提供者或调用失败时 , 触发 Mock 调用 , 如果不想发起RPC 调用直接使用 Mock 数据 , 则需要在配置中指定 force: 语法 ( 同样需要版本高于 2 .6.5 ) 。

这些 Mock 关键逻辑是在哪里处理的呢 ? 处理 Mock 伪装对应的实现类是 MockClusterlnvoker,因为 MockClusterWrapper 是对扩展点 Cluster 的包装 , 当框架在加载 Cluster 扩展点时会自动使用MockClusterWrapper 类对 Cluster 实例进行包装 ( 认是 FailoverCluster ) 。

MockClusterlnvoker#invoke 对应的实现

在这里插入图片描述


代码清单中主要完成服务降级伪装 。 在①中如果没有配置 Mock, 则直接发起 RPC 调用 。 2.6.5 版本虽然支持 force 特性 , 但因为有 bug, ②中的这段代码实际上并不会执行 。 在2.6.5 版本以后 , 如果用户为 Mock 指定了 force, 则直接在本地伪装而不发起 RPC 调用 。 在③中先处理正常 RPC 调用 , 如果调用出错则会降级到 Mock 调用 。 在④中具体 Mock 数据是由开发者自己编码完成的 。 dubbo 框架对常用的返回值做了支持 , 比如接口返回布尔值 , 可以直接在 Mock 中指定 return true

10 结果缓存

dubbo 框架提供了对服务调用结果进行缓存的特性 , 用于加速热门数据的访问速度dubbo提供声明式缓存 , 以减少用户加缓存的工作量 。 因为每次调用都会使用 JSON.toJSONString 方法将请求参数转换成字符串 , 然后拼装唯一的 key, 用于缓存唯一键 。 如果不能接受缓存造成的开销 , 则谨慎使用这个特性 。

如果要使用缓存 , 则可以在消费方添加如下配置 :<dubbo:reference cache=“lru” …/>

lru 缓存策略是框架认使用的 , 因此我们会对它进行简单的说明 。 它的原理比较简单 , 缓存对应实现类是 LRUCache。 缓存实现类 LRUCache 继承了 JDK 的 LinkedHashMap 类 ,LinkedHashMap 是基于链表的实现 , 它提供了钩子方法 removeEldestEntry, 它的返回值用于判断每次向集合中添加元素时是否应该删除最少访问的元素 。 LRUCache 重写了这个方法 ,当缓存值达到 1000 时 , 这个方法会返回 true, 链表会把头部节点移除 。 链表每次添加数据时都会在队列尾部添加 , 因此队列头部就是最少访问的数据 ( LinkedHashMap 在更新数据时 , 会把更新数据更新到列表尾部 ) 。

11 小结

主要对 dubbo 中的高级特性进行讲解 , 比如服务分组和版本 、 参数回调 、 隐式参数 、异步调用 、 泛化调用 、 上下文信息 、 Telnet 操作 、 Mock 调用和结果缓存原理 。 虽然知识点比较独立 , 但这些特性点能够解决实际业务场景中的很多问题 。 比如版本和分组能够解决业务资源隔离 , 防止整体资源被个别调用方拖垮 , 可以将某些调用分配一个隔离的资源池中 , 单独为它们提供服务

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐