Dubbo 核心组件
层次 | 作用 |
---|---|
Service | 该层与业务逻辑相关,根据 provider 和 consumer 的业务设计对应的接口和实现 |
Config | 对外配置接口,以 ServiceConfig 和 ReferenceConfig 为中心。可以理解为该层管理了整个 Dubbo 配置 |
Proxy | 使用动态代理的方式为接口创建代理类,Proxy 层最主要的接口就是 ProxyFactory。其默认的扩展点有:stub、jdk、javassist。jdk 使用反射的方式创建代理类,javassist 通过拼接字符串然后编译的方式创建代理类。对于服务提供者,代理的对象是接口的真实实现。 对于服务消费者,代理的对象是远程服务的 invoker 对象 |
Registry | 主要负责的就是服务的注册与发现。这层的主要接口就是 RegistryFactory,其接口方法有 @Adaptive 注解,会根据参数 protocol 来选择实现,默认的扩展实现有:zookeeper、redis、multicast(广播模式)、内存 |
Cluster | 集群容错层,主要负责:远程调用时的容错策略(如快速失败、失败重试);选择具体调用节点的负载均衡策略(如随机、一致性 hash 等);特殊调用路径的路由策略(如消费者只会调用某个 IP 的生产者) |
Monitor | 负责监控统计调用次数和调用时间 |
Protocal | 远程调用层,封装 RPC 调用的具体过程。Protocal 是 Invoker 暴露(发布一个任务让消费者调用)和引用(引用一个服务到本地)的主功能入口,它负责管理 Invoker 的整个生命周期。Invoker 是 Dubbo 的核心模型,它代表一个可执行体,允许向它发起 invoke 调用,它可能执行一个本地接口实现,也可能执行一个远程接口实现 |
Exchange | 封装请求响应模式,如如何将同步请求转化为异步请求。以 Request 和 Response 为中心,扩展接口为 Exchanger、ExchangeChannel、ExchangeClient 和 ExchangeServer |
Transport | 网络传输层,抽象 Mina 和 Netty 为统一接口。用户也可以扩展接口添加更多网络传输方式 |
Serialize | 序列化的作用是把对象转化为二进制流,然后在网络中传输。负责整个框架网络传输时的序列化和反序列化工作 |
Dubbo 总体调用流程
服务暴露过程
服务端在框架启动时,会初始化服务实例,通过 Proxy 组件调用具体协议(Protocol),把服务端要暴露的接口封装成 Invoker(真实是类型是 AbstractInvoker),然后转换成 Exporter,这时框架会打开服务端口并记录服务实例到内存中,最后通过 Registry 把服务元数据注册到注册中心。
- Proxy 组件:Dubbo 中只需要引用一个接口就可以调用远程服务。其实这是 Dubbo 为我们生成的代理类,调用的方法是 Proxy 组件生成的代理方法,会自动发起远程/本地调用,并返回结果。整个过程对用户全透明
- Protocol:可以将对接口的配置分居不同的协议转化成不同的 Invoker 对象。例如协议为 dubbo 时会将远程接口配置转换成一个 DubboInvoker
- Exporter:用于暴露到注册中心的对象,持有一个 Invoker 对象
- Registry:将 Exporter 注册到注册中心
以上就是服务暴露过程。消费者在启动时会通过 Registry 在注册中心订阅服务端的元数据(包括 IP 和 端口),并在第一个初始化时从注册中心**拉取全量服务端信息****
消费者调用流程
首先调用也是从一个 Proxy 开始,Proxy 持有一个 Invoker 对象并触发 invoke 调用。在 invoke 调用过程中,使用 Cluster 进行容错、路由以及负载均衡。Cluster 先通过 Directory 获取可用的远程服务 Invoker 列表,根据用户配置的路由规则(例如指定某些方法智能调用某个节点)将 Invoker 过滤一遍
然后存活下来的 Invoker 通过 LoadBalance 方法做负载均衡,选出一个可以调用的 Invoker。选中的 Invoker 会在调用 invoke 之前经过一个过滤器链(通常处理上下文、限流、计数等操作)
接着会使用 Network Client 做数据传输。传输之前通过 Codec 做私有协议构造,然后进行序列化传输至服务端。服务端接收到数据包后也会使用 Codec 处理协议头等,完成后对数据报文进行反序列化处理
随后远程调用请求(Request)被分配到 ThreadPool 中进行处理。Server 会处理这些 Request,根据请求查找对应的 Exporter(内部持有 Invoker),Invoker 用装饰器模式套了很多 Filter,因此在 Invoker 调用之前会经过服务端的过滤链
最终得到了服务端具体的接口实现并调用,然后将结果原路返回
Dubbo 注册中心
在 Dubbo 为服务体系中,注册中心是核心组件之一。Dubbo 通过注册中心实现了分布式环境中服务的注册和发现,是各个分布式节点之前的纽带。其主要作用如下:
- 动态加入:一个服务提供者通过注册中心可以动态地把自己暴露给其他消费者,无需消费者逐个更新配置文件
- 动态发现:一个消费者可以动态感知新的配置、路由规划和服务提供者,无需重启服务使之生效
- 动态调整:注册中心支持参数动态调整,新参数自动更新到所有相关服务点
- 统一配置:避免本地配置导致每个服务的配置不一致问题
1 工作流程
注册中心整体流程如下图所示:
- 服务提供者启动时,会向注册中心写入自己的元数据信息,同时会订阅配置元数据信息
- 消费者启动时,会向注册中心写入自己的元数据信息,同时订阅服务提供者、配置、路由元数据信息
- 服务治理中心(dubbo-admin)启动时,会同时订阅 服务提供者、消费者、配置、路由元数据信息
- 当有服务离开或新的服务加入时,注册中心服务提供者目录会发生变化,变化信息将动态通知给消费者和服务治理中心
- 当消费者发起服务调用时,会异步将调用、统计信息上报至监控中心(dubbo-monitor)
2 数据结构
注册中心的总体流程相同,但不同的注册中心有不同的实现方式,其数据结构也不同。常见的注册中心实现是 Zookeeper 和 Redis。下面重点介绍注册中心的 Zookeeper 的实现方式
Zookeeper 是树形结构的注册中心,每个节点分为持久节点、持久顺序节点、临时节点、临时顺序节点
- 持久节点:服务注册后保证节点不会丢失,注册中心重启仍然节点存在
- 持久顺序节点:在持久节点的基础上增加节点顺序功能
- 临时节点:服务注册后连接丢失或 session 超时,注册节点会被自动移除
- 临时顺序节点:在临时节点的基础上增加节点顺序功能
Dubbo 使用 Zookeeper 作为注册中心时,只使用持久节点和临时节点
假设 /dubbo/com.test.TestService/providers
是服务提供者在 Zookeeper 上的注册路径,该结构分为四层:root(根节点,默认是 dubbo)、service(接口名称,对应于 com.test.TestService)、四种服务目录(示例中的 providers 以及 consumers、routers、configurators)。在服务分类节点下是具体的 DUbbo 服务 URL,树形结构实例如下:
+ /dubbo
+-- com.test.TestService
+-- providers
+-- consumers
+-- routers
+-- configurators
- 树节点的根目录是注册中心分组,下面有多个服务接口,分组值来自用户配置 < dubbo:registry > 中的 group 属性
- 服务接口下包含 4 个子目录(providers、consumers、routers 以及 configurators),都是持久节点
- 服务提供者目录(
/dubbo/com.test.TestService/providers
)下面包含多个服务提供者 URL 元数据信息 - 服务消费者目录(
/dubbo/com.test.TestService/consumers
)下面包含多个消费者 URL 元数据信息 - 路由配置目录(
/dubbo/com.test.TestService/routers
)下面包含多个用于消费者路由策略 URL 元数据信息 - 动态配置目录(
/dubbo/com.test.TestService/configurators
)下面包含多个用于服务提供者动态配置 URL 元数据信息
3 订阅发布的实现
3.1 发布实现
服务提供者和服务消费者都需要将自己的元数据信息注册到注册中心。服务提供者的注册是为了让服务消费者感知服务的存在,从而发起远程调用;也让服务治理中心感知有新的服务提供者上线。消费者的注册是让了让服务治理中心可以发现自己
Zookeeper 实现发布的代码很简单,只是调用了 Zookeeper 客户端在注册中心上创建一个目录
zkClient.create(toUrlPath(url));
取消发布也很简单,只是把 ZooKeeper 注册中心上对应的路径删除即可
zkClient.delete(toUrlPath(url));
3.2 订阅实现
订阅通常有 pull 和 push 两种方式,Dubbo 目前采用 pull(拉取方式),后续接收事件并重新拉取数据
在服务暴露时,服务提供者会订阅 configurators 用于监听动态配置。消费者启动时会订阅 providers、routers 以及 configurators 这三个目录
Dubbo 中 Zookeeper 的客户端实现有两种:
- Apache Curator(默认)
- zKClient
用户可以通过 < dubbo:registry > 的 client 属性设置 curator 或 zkclient 作为 Zookeeper 客户端的实现方式
Zookeeper 注册中心采用的是 事件通知 + 客户端拉取 的方式,客户端第一次连接上注册中心时,会获取对应目录下的全量数据,并在订阅的节点上注册一个 watcher,客户端于注册中心保持 TCP 长连接,后续每个节点有数据变化的时候,注册中心会根据 watcher 的回调通知客户端数据有更新,客户端会将对应节点下的数据重新从注册中心拉取过来。
Dubbo 启停原理解析
1 优雅停机原理解析
优雅停机特性是所有 RPC 框架中非常重要的特性之一,因为核心业务在服务器正在执行时突然中断可能出现严重后果。Dubbo 的优雅停机原理如下图所示:
Dubbo 中实现优雅停机有几下几步:
- 收到 kill -9 进程退出信号,Spring 容器会触发容器销毁事件
- provider 端取消注册服务元数据信息
- consumer 端会收到最新的服务调用列表(不包含准备停机的地址)
- Dubbo 协议发送 readonly 事件报文通知 consumer 服务不可用
- 服务端等待已执行的任务结束并拒绝新任务执行
- provider 端断开与 consumer 端的 TCP 连接
既然第三步 consumer 端已经被通知最新的服务调用地址,provider 端为什么还要发送 readonly 事件报文给 consumer 端呢?这里主要考虑到注册中心推送服务有网络延迟,以及客户端计算服务列表可能占用一些时间。Dubbo 协议发送 readonly 事件报文时,consumer 端会设置响应的 provider 为不可用状态,下次负载均衡将不再调用下线的机器
Dubbo 远程调用
1 Dubbo 通信协议
Dubbo 支持 9 中通信协议
2.1 dubbo://(推荐)
描述:Dubbo 默认通信协议,单一长连接,进行的是 NIO 异步通信,基于 hessian 作为序列化协议
连接个数:单连接
连接方式:长链接
传输协议:TCP
传输方式:NIO 异步传输
序列化:Hessian 二进制序列化
适用范围:
- 传入传出参数数据包较小(建议小于100K)
- 消费者比提供者个数多,单一消费者无法压满提供者
- 尽量不要用 dubbo 协议传输大文件或超大字符串
适用场景 :
- 高并发场景,一般是服务提供者就几台机器,但是服务消费者有上百台,可能每天调用量达到上亿次!此时用长连接是最合适的,就是跟每个服务消费者维持一个长连接就可以,可能总共就100个连接。然后后面直接基于长连接 NIO 异步通信,可以支撑高并发请求 ,如果上亿次请求每次都是短连接的话,服务提供者会扛不住
- 因为走的是单一长连接,所以传输数据量太大的话,会导致并发能力降低。所以一般建议是传输数据量很小,支撑高并发访问
约束 :
- 参数及返回值需实现 Serializable 接口
- 参数及返回值不能自定义实现 List, Map, Number, Date, Calendar 等接口,只能用 JDK 自带的实现,因为 hessian 会做特殊处理,自定义实现类中的属性值都会丢失
- Hessian 序列化,只传成员属性值和值的类型,不传方法或静态变量
2.2 rmi://
描述:RMI 协议采用 JDK 标准的 java.rmi.* 实现,采用阻塞式短连接和 JDK 标准序列化方式
连接个数:多连接
连接方式:短链接
传输协议:TCP
传输方式:同步传输
序列化:Java 标准二进制序列化
适用范围:
- 传入传出参数数据包大小混合
- 消费者与提供者个数差不多
- 可传文件
适用场景 :
- 常规远程服务方法调用,与原生RMI服务互操作,一般较少用
约束 :
- 参数及返回值需实现 Serializable 接口
- dubbo 配置中的超时时间对 RMI 无效,需使用 java 启动参数设置
-Dsun.rmi.transport.tcp.responseTimeout=3000
2.3 hessian://
描述:Hessian 协议用于集成 Hessian 的服务,Hessian 底层采用 Http 通讯,采用 Servlet 暴露服务,Dubbo 缺省内嵌 Jetty 作为服务器实现
连接个数:多连接
连接方式:短链接
传输协议:HTTP
传输方式:同步传输
序列化:Hessian 二进制序列化
适用范围:
- 传入传出参数数据包较大
- 提供者比消费者个数多,提供者压力较大
- 可传文件
适用场景 :
- 页面传输
- 文件传输
- 原生 Hessian 服务互操作
约束 :
- 同 dubbo 协议
2.4 http://
描述:基于 HTTP 表单的远程调用协议,采用 Spring 的 HttpInvoker 实现
连接个数:多连接
连接方式:短链接
传输协议:HTTP
传输方式:同步传输
序列化:表单序列化
适用范围:
- 传入传出参数数据包大小混合,提供者比消费者个数多,可用浏览器查看,可用表单或 URL 传入参数,暂不支持传文件
适用场景 :
- 需同时给应用程序和浏览器 JS 使用的服务
约束 :
- 参数及返回值需符合 Bean 规范
2.5 webservice://
描述:基于 WebService 的远程调用协议,基于 Apache CXF 的 frontend-simple 和 transports-http 实现
连接个数:多连接
连接方式:短链接
传输协议:HTTP
传输方式:同步传输
序列化:同步传输 SOAP 文本序列化
适用场景 :
- 系统集成,跨语言调用
2.6 thrift://
描述:当前 dubbo 支持 的 thrift 协议是对 thrift 原生协议的扩展,在原生协议的基础上添加了一些额外的头信息,比如 service name,magic number 等
2.7 memcached://
描述:基于 memcached 实现的 RPC 协议
2.8 redis://
描述:基于 Redis 实现的 RPC 协议
2.9 rest://
描述:基于标准的 Java REST API——JAX-RS 2.0(Java API for RESTful Web Services的简写)实现的REST调用支持
2 dubbo:// 协议详解
Dubbo 数据包分为消息头和消息体,消息头用于存储一些元信息,比如魔法数(Magic),数据包类型(Request/Response),消息体长度(Data Length)等。消息体中用于存储具体的调用消息,比如方法名称,参数列表等。下面简单列举一下消息头的内容
偏移量(Bit) | 字段 | 取值 |
---|---|---|
0 ~ 7 | 魔数高位 | 0xda00 |
8 ~ 15 | 魔数低位 | 0xbb |
16 | 数据包类型 | 0 - Response, 1 - Request |
17 | 调用方式 | 仅在第16位被设为1的情况下有效,0 - 单向调用,1 - 双向调用 |
18 | 事件标识 | 0 - 当前数据包是请求或响应包,1 - 当前数据包是心跳包 |
19 ~ 23 | 序列化器编号 | 2 - Hessian2Serialization 3 - JavaSerialization 4 - CompactedJavaSerialization 6 - FastJsonSerialization 7 - NativeJavaSerialization 8 - KryoSerialization 9 - FstSerialization |
24 ~ 31 | 状态 | 20 - OK 30 - CLIENT_TIMEOUT 31 - SERVER_TIMEOUT 40 - BAD_REQUEST 50 - BAD_RESPONSE ...... |
32 ~ 95 | 请求编号 | 共 8 字节存储 RPC 请求的唯一 id,运行时生成,用来将请求和响应做关联 |
96 ~ 127 | 消息体长度 | 运行时计算 |
在网络通信中(基于 TCP)需要解决网络粘包/解包的问题,一些常用的方法是用回车、换行、固定长度和特殊分隔符进行处理。Dubbo 就是用魔法数(0xdabb)来分割处理粘包问题的
一般情况下,服务消费方会并发调用多个服务,每个用户线程发送请求后,会调用不同 DefaultFuture 对象的 get 方法进行等待。 一段时间后,服务消费方的线程池会收到多个响应对象。这个时候要考虑一个问题,如何将每个响应对象传递给相应的 DefaultFuture 对象,且不出错
答案是通过 请求编号。DefaultFuture 被创建时,会要求传入一个 Request 对象。此时 DefaultFuture 可从 Request 对象中获取请求编号,并将 <请求编号, DefaultFuture 对象> 映射关系存入到静态 HashMap 中,即 FUTURES。消费者端线程池中的线程收到 Response 对象后,会根据 Response 对象中的请求编号到 FUTURES 集合中取出相应的 DefaultFuture 对象,然后再将 Response 对象设置到 DefaultFuture 对象中。最后再唤醒用户线程,这样用户线程即可从 DefaultFuture 对象中获取调用结果了,整个过程大致如下图:
3 Dubbo 序列化协议
Dubbo序列化支持 java、compactedjava、nativejava、fastjson、dubbo、fst、hessian2、kryo,其中 dubbo 协议缺省为 hessian2 序列化,rmi 协议缺省为 java 序列化,http 协议缺省为 json 序列化
以下是 dubbo:2.7.7 扩展的序列化方式,可在
标签的 serialization
属性进行配置
fastjson=org.apache.dubbo.common.serialize.fastjson.FastJsonSerialization
fst=org.apache.dubbo.common.serialize.fst.FstSerialization
hessian2=org.apache.dubbo.common.serialize.hessian2.Hessian2Serialization
native-hessian=org.apache.dubbo.serialize.hessian.Hessian2Serialization
java=org.apache.dubbo.common.serialize.java.JavaSerialization
compactedjava=org.apache.dubbo.common.serialize.java.CompactedJavaSerialization
nativejava=org.apache.dubbo.common.serialize.nativejava.NativeJavaSerialization
kryo=org.apache.dubbo.common.serialize.kryo.KryoSerialization
kryo2=org.apache.dubbo.common.serialize.kryo.optimized.KryoSerialization2
avro=org.apache.dubbo.common.serialize.avro.AvroSerialization
protostuff=org.apache.dubbo.common.serialize.protostuff.ProtostuffSerialization
gson=org.apache.dubbo.common.serialize.gson.GsonSerialization
protobuf-json=org.apache.dubbo.common.serialize.protobuf.support.GenericProtobufJsonSerialization
protobuf=org.apache.dubbo.common.serialize.protobuf.support.GenericProtobufSerialization
Dubbo 序列化主要由 Serialization(序列化策略)、DataInput(反序列化,二进制->对象)、DataOutput(序列化,对象->二进制流) 来进行数据的序列化与反序列化
3.1 hessian 序列化
Hessian 数据结构
8 种原始类型 | 1. raw binary data 2. boolean 3. 64-bit millisecond date 4. 64-bit double 5. 32-bit int 6. 64-bit long 7. null 8. UTF8-encoded string |
3 种递归类型 | 1. list for lists and arrays 2. map for maps and dictionaries 3. object for objects |
1 中特殊类型 | 1. ref for shared and circular object references. |
hessian 序列化 VS Java 原生序列化
Java 序列化会把要序列化的对象类的元数据和业务数据全部序列化为字节流,而且是把整个继承关系上的东西全部序列化了。它序列化出来的字节流是一个对象从结构到内容的完整描述,包含所有的信息,因此效率较低而且字节流比较大。但是由于序列化了所有内容,因此也更可用和可靠
hession 序列化实现机制是着重于数据,附带简单的类型信息的方法。就像 Integer a = 1
,hessian会序列化成 I 1
这样的流,I 表示 int or Integer,1 就是数据内容。对于复杂对象,通过 Java 的反射机制,hessian 把对象所有的属性当成一个 Map 来序列化,产生类似 M className propertyName1 I 1 propertyName S stringValue
这样的流,包含了基本的类型描述和数据内容
hessian 的序列化在内容的序列化上做了一些优化,hessian 将需要序列化的多个相同的对象只会写入一次,其他用到该对象的只使用对象的引用,而不重新写入对象的描述信息和值信息
3.2 protobuf 序列化
Protocol Buffer 是 Google 出品的一种轻量并且高效的结构化数据存储格式,性能比 JSON、XML 要高很多,原因如下:
- 它使用 proto 编译器,自动进行序列化和反序列化,速度非常快,比 XML 和 JSON 快大概 20~100 倍
- 它的数据压缩效果好,就是说它序列化后的数据量体积小。因为体积小,传输起来带宽和速度上会有优化,详见下一节
- 使用 Varint 数字表示方法,减少用来表示数字的字节数
- 一个负数一般会被表示为一个很大的整数,因为计算机定义负数的符号位为数字的最高位。因此 Protocol Buffer 定义了 sint32 这种类型,采用 zigzag 编码,用无符号数来表示有符号数字,实现了用较少的 Bytes 来表示负数
- Protocol Buffer 的二进制流采用 Key-Value 结构存储信息,无需使用分隔符来分割不同的 Field。对于可选的 Field,如果消息中不存在该 field,那么在最终的 Message Buffer 中就没有该 field,这个特性也有助于节约消息本身的大小
数据压缩方法
Protobuf 序列化后所生成的二进制消息非常紧凑,这得益于 Protobuf 采用的非常巧妙的 Encoding 方法
考察消息结构之前,让我首先要介绍一个叫做 Varint 的术语
Varint 是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数
比如对于 int32 类型的数字,一般需要 4 个 Bytes 来表示。但是采用 Varint,对于很小的 int32 类型的数字,则可以用 1 个 Byte 来表示。当然凡事都有好的也有不好的一面,采用 Varint 表示法,大的数字则需要 5 个 Byte 来表示。从统计的角度来说,一般不会所有的消息中的数字都是大数,因此大多数情况下,采用 Varint 后,可以用更少的字节数来表示数字信息。下面就详细介绍一下 Varint
Varint 中的每个 Byte 的最高位 bit 有特殊的含义,如果该位为 1,表示后续的 Byte 也是该数字的一部分,如果该位为 0,则结束。其他的 7 个 bit 都用来表示数字。因此小于 128 的数字都可以用一个 Byte 表示。大于 128 的数字,比如 300,会用两个字节来表示:1010 1100 0000 0010
下图演示了 Google Protocol Buffer 如何解析两个 Bytes。注意到最终计算前将两个 Bytes 的位置相互交换过一次,这是因为 Google Protocol Buffer 字节序采用 little-endian 的方式
消息经过序列化后会成为一个二进制数据流,该流中的数据为一系列的 Key-Value 对。如下图所示:
采用这种 Key-Pair 结构无需使用分隔符来分割不同的 Field。对于可选的 Field,如果消息中不存在该 field,那么在最终的 Message Buffer 中就没有该 field,这些特性都有助于节约消息本身的大小
以代码清单 1 中的消息为例。假设我们生成如下的一个消息 Test1:
Test1.id = 10; Test1.str = "hello";
则最终的 Message Buffer 中有两个 Key-Value 对,一个对应消息中的 id;另一个对应 str
Key 用来标识具体的 field,在解包的时候,Protocol Buffer 根据 Key 就可以知道相应的 Value 应该对应于消息中的哪一个 field
Key 的定义如下:
(field_number << 3) | wire_type
可以看到 Key 由两部分组成。第一部分是 field_number,比如消息 Test1 中 field id 的 field_number 为 1。第二部分为 wire_type。表示 Value 的传输类型
Wire Type 可能的类型如下表所示:
Type | Meaning | Used For |
---|---|---|
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimi | string, bytes, embedded messages, packed repeated fields |
3 | Start group | Groups (deprecated) |
4 | End group | Groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
在我们的例子当中,field id 所采用的数据类型为 int32,因此对应的 wire type 为 0。细心的读者或许会看到在 Type 0 所能表示的数据类型中有 int32 和 sint32 这两个非常类似的数据类型。Google Protocol Buffer 区别它们的主要意图也是为了减少 encoding 后的字节数
在计算机内,一个负数一般会被表示为一个很大的整数,因为计算机定义负数的符号位为数字的最高位。如果采用 Varint 表示一个负数,那么一定需要 5 个 Bytes(为何是 5 bytes 呢?)。为此 Google Protocol Buffer 定义了 sint32 这种类型,采用 zigzag 编码
Zigzag 编码用无符号数来表示有符号数字,正数和负数交错,这就是 zigzag 这个词的含义了
如图所示:
使用 zigzag 编码,绝对值小的数字,无论正负都可以采用较少的 Bytes 来表示
其他的数据类型,比如字符串等则采用类似数据库中的 varchar 的表示方法,即用一个 varint 表示长度,然后将其余部分紧跟在这个长度部分之后即可
通过以上对 protobuf Encoding 方法的介绍,想必您也已经发现 protobuf 消息的内容小,适于网络传输。假如您对那些有关技术细节的描述缺乏耐心和兴趣,那么下面这个简单而直观的比较应该能给您更加深刻的印象
假设有一条 helloworld 消息 id=101 str="hello",用 Protobuf 序列化后的字节序列为:
08 65 12 06 48 65 6C 6C 6F 77
而如果用 XML,则类似这样:
31 30 31 3C 2F 69 64 3E 3C 6E 61 6D 65 3E 68 65
6C 6C 6F 3C 2F 6E 61 6D 65 3E 3C 2F 68 65 6C 6C
6F 77 6F 72 6C 64 3E
一共 55 个字节,这些奇怪的数字需要稍微解释一下,其含义用 ASCII 表示如下:
101
hello
4 线程模型
如果事件处理的逻辑能迅速完成,并且不会发起新的 IO 请求,比如只是在内存中记个标识,则直接在 IO 线程上处理更快,因为减少了线程池调度
但如果事件处理逻辑较慢,或者需要发起新的 IO 请求,比如需要查询数据库,则必须派发到线程池,否则 IO 线程阻塞,将导致不能接收其它请求
因此,需要通过不同的派发策略和不同的线程池配置的组合来应对不同的场景:
Dispatcher
-
all
所有消息都派发到线程池,包括请求,响应,连接事件,断开事件,心跳等(默认设置) -
direct
所有消息都不派发到线程池,全部在 IO 线程上直接执行 -
message
只有请求响应消息派发到线程池,其它连接断开事件,心跳等消息,直接在 IO 线程上执行 -
execution
只有请求消息派发到线程池,不含响应,响应和其它连接断开事件,心跳等消息,直接在 IO 线程上执行 -
connection
在 IO 线程上,将连接断开事件放入队列,有序逐个执行,其它消息派发到线程池
ThreadPool
-
fixed
固定大小线程池,启动时建立线程,不关闭,一直持有(默认设置) -
cached
缓存线程池,空闲一分钟自动删除,需要时重建 -
limited
可伸缩线程池,但池中的线程数只会增长不会收缩。只增长不收缩的目的是为了避免收缩时突然来了大流量引起的性能问题 -
eager
优先创建Worker
线程池。在任务数量大于corePoolSize
但是小于maximumPoolSize
时,优先创建Worker
来处理任务。当任务数量大于maximumPoolSize
时,将任务放入阻塞队列中。阻塞队列充满时抛出RejectedExecutionException
。(相比于cached
:cached
在任务数量超过maximumPoolSize
时直接抛出异常而不是将任务放入阻塞队列)
Dubbo 集群容错
1 集群容错概述
为了避免单点故障,现在的应用通常至少会部署在两台服务器上。对于一些负载比较高的服务,会部署更多的服务器。这样,在同一环境下的服务提供者数量会大于 1。对于服务消费者来说,同一环境下出现了多个服务提供者,这时会出现一个问题,服务消费者需要决定选择哪个服务提供者进行调用。另外服务调用失败时的处理措施也是需要考虑的,是重试呢,还是抛出异常,亦或是只打印异常。为了处理这些问题,Dubbo 定义了集群容错接口 Cluster
和 ClusterInvoker
集群 Cluster 用途是将多个服务提供者合并为一个 Cluster Invoker,并将这个 Invoker 暴露给服务消费者。这样一来,服务消费者只需通过这个 Invoker 进行远程调用即可,至于具体调用哪个服务提供者,以及调用失败后如何处理等问题,现在都交给集群模块去处理
集群模块是服务提供者和服务消费者的中间层,为服务消费者屏蔽了服务提供者的情况,这样服务消费者就可以专心处理远程调用相关事宜。比如发请求,接受服务提供者返回的数据等。这就是集群的作用
总体来说,Cluster 作为一个集群容错层,其核心接口为 Cluster
和 ClusterInvoker
。每一个不同的容错机制都继承 Cluster
接口进行不同的实现,每个实现类中都需要调用 join
方法创建一个对应的 ClusterInvoker
实现类。然后调用 ClusterInvoker
的 invoke
方法进行具体的调用流程。以 FailoverCluster
为例,其实现代码如下:
public class FailoverCluster implements Cluster {
public static final String NAME = "failover";
public Invoker join(Directory directory) throws RpcException {
return new FailoverClusterInvoker(directory);
}
}
整体过程如下图所示:
我们可以看到集群工作过程可分为两个阶段:
第一个阶段是在服务消费者初始化期间,集群 Cluster 实现类为服务消费者创建 Cluster Invoker 实例,即上图中的 merge 操作。
第二个阶段是在服务消费者进行远程调用时。以 FailoverClusterInvoker
为例:
-
FailoverClusterInvoker
首先会调用Directory#list
方法列举Invoker
列表(可将 Invoker 简单理解为服务提供者)。Directory 的用途是保存 Invoker,可简单类比为List
。其实现类RegistryDirectory
是一个动态服务目录,可感知注册中心配置的变化,它所持有的 Invoker 列表会随着注册中心内容的变化而变化。每次变化后,RegistryDirectory 会动态增删 Invoker -
FailoverClusterInvoker
调用Router
的route
方法进行路由,过滤掉不符合路由规则的Invoker
-
FailoverClusterInvoker
拿到Directory
返回的Invoker
列表后,它会通过LoadBalance
从Invoker
列表中选择一个Invoker
-
FailoverClusterInvoker
将调用参数传给LoadBalance
选择出的Invoker
实例的invoke
方法,进行真正的远程调用
2 容错机制实现
Dubbo 中的容错机制有以下几种,在 dubbo:service
、dubbo:reference
、dubbo:provider
、dubbo:consumer
的 cluster
属性中配置
机制 | 机制简介 |
---|---|
Failover | 当调用失败时会重试其他服务器。用户可以通过 retries="2" 设置重试次数。这是 Dubbo 的默认容错机制,会做负载均衡。通常使用在读操作或幂等操作上,但会导致接口延迟增大,容易加重下游服务的负载 |
Failfast | 当请求失败后,快速返回异常结果,不作任何重试。会对请求做负载均衡,通常使用在非幂等操作上 |
Failsafe | 请求出现异常时,直接忽略。会对请求做负载均衡,不关心调用是否成功,不抛出异常影响外层调用。适用于不重要的服务场景(如日志) |
Failback | 请求失败后,会将失败请求记录在失败队列中,并有一个定时线程池定时重试,适用于异步或最终一致性的服务场景,会做负载均衡 |
Forking | 同时调用多个相同的服务,只要其中一个返回就立即返回结果。用户可以设置 fork 属性确定最大并行调用数量。通常使用在对接口实时性要求极高的调用上,但会浪费更多资源 |
Broadcast | 广播调用所有服务,任意一个节点失败则报错,不用负载均衡 |
Available | 遍历所有服务列表,找到第一个可用节点,直接请求并返回结果,不做负载均衡 |
Mergeable | 自动将多个节点请求得到的结果进行合并 |
Mock |
2.1 Failover 策略
该策略的逻辑如下:
- 校验从
AbstractClusterInvoker
传入的Invoker
列表是否为空 - 获取配置参数,如
retries
- 初始化一些集合和对象,用于保存过程中出现的异常、记录调用哪些节点
- 使用
for
循环实现充实,循环次数就是重试次数。成功则返回,否则继续循环。如果循环结束仍没有一个成功的返回,则将(3)中的记录抛出- 校验
Invoker
列表是否为空 - 负载均衡
- 远程调用
- 校验
2.2 Failfast 策略
该策略的逻辑如下:
- 校验从
AbstractClusterInvoker
传入的Invoker
列表是否为空 - 负载均衡
- 远程调用,在
try
中调用Invoker#invoke
方法,若捕获到异常则封装成RpcException
抛出
public Result doInvoke(Invocation invocation, List> invokers, LoadBalance loadbalance) throws RpcException {
this.checkInvokers(invokers, invocation);
Invoker invoker = this.select(loadbalance, invocation, invokers, (List)null);
try {
return invoker.invoke(invocation);
} catch (Throwable var6) {
if (var6 instanceof RpcException && ((RpcException)var6).isBiz()) {
throw (RpcException)var6;
} else {
throw new RpcException...;
}
}
}
2.3 Failsafe 策略
该策略的逻辑如下:
- 校验从
AbstractClusterInvoker
传入的Invoker
列表是否为空 - 负载均衡
- 远程调用,在
try
中调用Invoker#invoke
方法,若捕获到异常则直接吞掉,返回一个结果集
public Result doInvoke(Invocation invocation, List> invokers, LoadBalance loadbalance) throws RpcException {
try {
this.checkInvokers(invokers, invocation);
Invoker invoker = this.select(loadbalance, invocation, invokers, (List)null);
return invoker.invoke(invocation);
} catch (Throwable var5) {
logger.error("Failsafe ignore exception: " + var5.getMessage(), var5);
return AsyncRpcResult.newDefaultAsyncResult((Object)null, (Throwable)null, invocation);
}
}
2.4 Failback 策略
若调用失败,则定期重试。FailbackClusterInvoker
中定义了一个 ConcurrentHashMap
用来保存失败调用请求。另外定义了一个定时线程池,默认每 5s 把所有的失败调用拿出来重试。若调用成功则将请求从 ConcurrentHashMap
中移除
该策略的逻辑如下:
- 校验从
AbstractClusterInvoker
传入的Invoker
列表是否为空 - 负载均衡
- 远程调用,在
try
中调用Invoker#invoke
方法,若捕获到异常则将调用请求保存到ConcurrentHashMap
,并返回一个空结果集 - 定时线程池每 5s 将
ConcurrentHashMap
中的失败请求进行重新请求,请求成功则从ConcurrentHashMap
中移除。若还是请求失败则异常也会被捕获,防止中断ConcurrentHashMap
后面的重试
2.5 Available 策略
该策略的逻辑如下:
- 遍历
AbstractClusterInvoker
传入的Invoker
列表,找到第一个可用的服务直接调用并返回 - 如果遍历后没有找到可用的
Invoker
,则抛出异常
2.6 Broadcast 策略
该策略的逻辑如下:
- 校验从
AbstractClusterInvoker
传入的Invoker
列表是否为空 - 初始化一些集合和对象,用于保存过程中出现的异常和结果信息
- 循环遍历
Invoker
列表,直接 RPC 调用。任何一个节点出错不中整个广播过程,会先记录异常,在最后广播完成后再抛出。最后一个节点的异常会覆盖前面节点的异常信息
2.7 Forking 策略
该策略的逻辑如下:
校验从
AbstractClusterInvoker
传入的Invoker
列表是否为空;初始化一个Invoker
集合,用于保存真正要调用的Invoker
列表;从 URL 中获取最大并行数、超时时间-
获取最终要调用的
Invoker
列表。假设用户设置的最大并行数是n
,实际可调用最大服务数为v
。若n < 0 or n > v
,说明可用服务少于用户设置,则最终调用的服务只能有v
个;若n < v
,则循环调用n
次负载均衡方法,获取Invoker
加入Invoker
列表中注意:
Invoker
加入Invoker
列表中会进行去重操作 初始化一个阻塞队列和一个异常计数器
-
执行调用。循环使用线程池并行调用,调用成功则将成功结果放入阻塞队列中;调用失败,则异常计数器 + 1。若所有线程的调动都失败了,即异常计数 ≥ 可调用
Invoker
数时,讲异常信息放入阻塞队列并行调用如何保证个别调用的失败不返回异常信息,只有全部调用失败财才返回异常信息呢?答案在于只有当异常计数 ≥ 可调用 Invoker 数时,才会将异常信息放入阻塞队列
-
主线程同步等待结果。主线程使用阻塞队列的
poll(timeout)
方法,同步等待阻塞队列中的一个结果,若是正常结果则返回,否则抛出异常从这点可知,Forking 的超时是通过在阻塞队列的
poll
方法中传入超时时间实现的
3 Directory 实现
todo
4 路由机制实现
todo
5 负载均衡实现
Dubbo 提供了4种负载均衡实现,分别是:
- 基于权重随机算法的
RandomLoadBalance
- 基于最少活跃调用数算法的
LeastActiveLoadBalance
- 基于 hash 一致性的
ConsistentHashLoadBalance
- 基于加权轮询算法的
RoundRobinLoadBalance
5.1 RandomLoadBalance
RandomLoadBalance
是加权随机算法的具体实现,它的算法思想很简单。假设我们有一组服务器 servers = [A, B, C],他们对应的权重为 weights = [5, 3, 2],权重总和为10。现在把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于服务器 B,[8, 10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在 [0, 10) 之间的随机数,然后计算这个随机数会落到哪个区间上。比如数字3会落到服务器 A 对应的区间上,此时返回服务器 A 即可。权重越大的机器,在坐标轴上对应的区间范围就越大,因此随机数生成器生成的数字就会有更大的概率落到此区间内。只要随机数生成器产生的随机数分布性很好,在经过多次选择后,每个服务器被选中的次数比例接近其权重比例。
生成随机数使用了
ThreadLocalRandom
,它的性能比Random
好一点。因为Random
生成随机数时,为了保证线程安全性,使用了CAS
操作保证每次只有一个线程可以获取并更新 seed。获取失败的线程将自旋重试,因此多线程下会因竞争同一个 seed 导致性能下降
ThreadLocalRandom
则为每个线程维护了一个 seed,这样就可以有效避免 seed 竞争
5.2 LeastActiveLoadBalance
LeastActiveLoadBalance
翻译过来是最小活跃数负载均衡 。活跃调用数越小,表明该服务提供者效率越高,单位时间内可处理更多的请求。此时应优先将请求分配给该服务提供者。在具体实现中,每个服务提供者对应一个活跃数 active。初始情况下,所有服务提供者活跃数均为0。每收到一个请求,活跃数加1,完成请求后则将活跃数减1。在服务运行一段时间后,性能好的服务提供者处理请求的速度更快,因此活跃数下降的也越快,此时这样的服务提供者能够优先获取到新的服务请求、这就是最小活跃数负载均衡算法的基本思想
5.3 ConsistentHashLoadBalance
一致性 hash 算法由麻省理工学院的 Karger 及其合作者于1997年提出的,算法提出之初是用于大规模缓存系统的负载均衡。它的工作过程是这样的,首先根据 ip 或者其他的信息为缓存节点生成一个 hash,并将这个 hash 投射到 [0, 232 - 1] 的圆环上。当有查询或写入请求时,则为缓存项的 key 生成一个 hash 值。然后查找第一个大于或等于该 hash 值的缓存节点,并到这个节点中查询或写入缓存项。如果当前节点挂了,则在下一次查询或写入缓存时,为缓存项查找另一个大于其 hash 值的缓存节点即可。
比如下面绿色点对应的缓存项将会被存储到 cache-2 节点中。由于 cache-3 挂了,原本应该存到该节点中的缓存项最终会存储到 cache-4 节点中
下面来看看一致性 hash 在 Dubbo 中的应用。我们把上图的缓存节点替换成 Dubbo 的服务提供者,于是得到了下图:
这里相同颜色的节点均属于同一个服务提供者,比如 Invoker1-1,Invoker1-2,……, Invoker1-160。这样做的目的是通过引入虚拟节点,让 Invoker 在圆环上分散开来,避免数据倾斜问题。所谓数据倾斜是指,由于节点不够分散,导致大量请求落到了同一个节点上,而其他节点只会接收到了少量请求的情况。比如:
如上,由于 Invoker-1 和 Invoker-2 在圆环上分布不均,导致系统中75%的请求都会落到 Invoker-1 上,只有 25% 的请求会落到 Invoker-2 上。解决这个问题办法是引入虚拟节点,通过虚拟节点均衡各个节点的请求量
5.4 RoundRobinLoadBalance
我们来看一下 Dubbo 中加权轮询负载均衡的实现 RoundRobinLoadBalance
。在详细分析源码前,我们先来了解一下什么是加权轮询。这里从最简单的轮询开始讲起
所谓轮询是指将请求轮流分配给每台服务器。举个例子,我们有三台服务器 A、B、C。我们将第一个请求分配给服务器 A,第二个请求分配给服务器 B,第三个请求分配给服务器 C,第四个请求再次分配给服务器 A。这个过程就叫做轮询。轮询是一种无状态负载均衡算法,实现简单,适用于每台服务器性能相近的场景下。
现实情况下,我们并不能保证每台服务器性能均相近。如果我们将等量的请求分配给性能较差的服务器,这显然是不合理的。因此,这个时候我们需要对轮询过程进行加权,以调控每台服务器的负载。经过加权后,每台服务器能够得到的请求数比例,接近或等于他们的权重比。比如服务器 A、B、C 权重比为 5:2:1。那么在8次请求中,服务器 A 将收到其中的5次请求,服务器 B 会收到其中的2次请求,服务器 C 则收到其中的1次请求
涉及到平滑轮询算法,有兴趣研究一下
Dubbo 扩展点加载机制
1 Java SPI
SPI 全称 service Provider Interface
Java SPI 使用了策略模式,一个接口多种实现。具体步骤如下:
- 定义一个接口和对应的方法
- 编写接口的实现类
- 在 META-INF/services/ 目录下创建一个以接口全路径名命名的文件,如 com.test.interface.Service
- 在文件中写入具体实现类的全路径名,若有多个则用分行符分离,如 com.test.interface.impl.ServiceImpl
- 在代码中通过
ServiceLoader
来加载具体的实现类
其缺点是:
- JDK 自带的 SPI 会一次性实例化所有扩展点实现,如果扩展点不使用,那么会浪费资源
- 在扩展点加载失败的情况下,JDK 扩展点加载机制无法提供扩展点加载失败的真正原因
- JDK 自带 SPI 不支持 IOC 和 AOP 的功能
2 Dubbo SPI
相比 Java SPI
,Dubbo SPI
做了一些改进和优化:
- 相对于 Java SPI 一次性加载所有实现,Dubbo SPI 是按需加载,只加载配置文件中的类,并分成不同的种类缓存在内存中,不会立即初始化,在性能上有更好的表现
- 更为详细的扩展加载失败信息
- 增加了对扩展 IOC 和 AOP的支持。一个扩展类可以直接 setter 注入其他扩展
Dubbo SPI 还兼容了 Java SPI 的配置路径和内容配置方式。在Dubbo启动的时候,会默认扫描这三个目录下的配置文件:META-INF/services
、META-INF/dubbo/
、META-INF/dubbo/internal
Dubbo SPI 配置规范如下表:
规范名 | 规范说明 |
---|---|
SPI配置文件路径 | META-INF/services、META-INF/dubbo/、META-INF/dubbo/internal |
SPI配置文件名称 | 全路径类名 |
文件内容格式 | key=value,多个用换行符分隔 |
2.1 分类与缓存
Dubbo SPI 可分为 Class 缓存 和 实例缓存。这两种缓存又根据扩展类的分类分为 普通扩展类、包装扩展类 和 自适应扩展类
- Class 缓存:Dubbo SPI 获取扩展类时,先从缓存中读取,若缓存中不存在则加载配置文件,根据配置将 Class 缓存到内存中,并不会全部初始化
- 实例缓存:基于性能考虑,Dubbo SPI 也将 Class 初始化后的实例进行缓存。每次获取实例会先在缓存中查找,若缓存中不存在则重新加载并进行缓存。这也是 Dubbo SPI 性能优于 Java SPI 的原因,即 Dubbo SPI 缓存的 Class 是按需实例化的
2.2 扩展点注解
@SPI
@Adaptive
3 ExtensionLoader 的工作原理
ExtensionLoader
的逻辑入口可以分为 getExtension
、getAdptiveExtension
、getActivateExtension
三个,分别是获取普通扩展类、获取自适应扩展类、获取自动激活的扩展类。总体逻辑都是从调用这三个方法开始的,每个方法可能会有不同的重载方法,根据不同的传入参数进行调整:
3.1 getExtension
3.2 getAdaptiveExtension
在 getAdaptiveExtension
方法中,会为扩展点接口自动生成实现类字符串,实现类主要包含以下逻辑:
- 为接口中每个有
@Adaptive
注解的方法生成默认实现(没有注解的方法则生成空实现) - 每个默认实现都会从 URL 中提取
Adaptive
参数值,并以此为依据动态加载扩展点 - 使用不同的编译器,把实现类字符串编译为自适应类并返回,详见第 4 节
4 扩展点动态编译实现
Dubbo SPI 使用了生成字符串代码再编译为 Class 的方式,支持字符串代码编译的有 JDK 编译器 和 Javassist 编译器
Dubbo 中有三种代码编译器,分别是 JDK 编译器
、Javassist 编译器
和 AdaptiveCompiler 编译器
。这几种编译器都实现了 Compiler 接口,编译器类之间的关系如下图:
Compiler
接口上含有一个 @SPI
注解,注解的默认值是 @SPI("javassist")
,很明显,Javassist 编译器将作为默认编译器。如果用户想改变默认编译器,则可以通过
标签进行设置
AdaptiveCompiler
上面有 @Adaptive
注解,说明 AdaptiveCompiler
会固定为默认实现,这个 Compiler 的主要作用和 AdaptiveExtensionFactory
相似,就是为了管理其他 compiler
@Adaptive
public class AdaptiveCompiler implements Compiler {
...
public static void setDefaultCompiler(String compiler) {
DEFAULT_COMPILER = compiler;
}
public Class> compile(String code, ClassLoader classLoader) {
...
return compiler.compile(code, classLoader);
}
}
AdaptiveCompiler#setDefaultCompiler
方法会在 ApplicationConfig
中被调用,也就是 Dubbo 在启动时,会解析
标签,获取设置的值,初始化对应的编译器。如果没有标签设置,则使用 @SPI("javassist")
中的设置,即 javassistCompiler
4.1 Javassist 动态代码编译
Javassist 动态代码编译示例:
@Test
public void test_javassist() throws CannotCompileException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
//初始化Javassist类池
ClassPool classPool = ClassPool.getDefault();
//创建一个Hello World类
CtClass ctClass = classPool.makeClass("Hello World");
//添加一个test方法,会打印Hello World,直接传入方法的字符串
CtMethod method = CtMethod.make("" +
"public static void test(){" +
"System.out.println(\"Hello World\");" +
"}", ctClass);
ctClass.addMethod(method);
//生成类
Class aClass = ctClass.toClass();
//通过反射调用这个类实例
Object object = aClass.newInstance();
Method m = aClass.getDeclaredMethod("test", null);
m.invoke(object, null);
}
由于之前已经生成了代码字符串,因此在 JavassistCompiler 中,就是不断通过正则表达式匹配不同部位的代码,然后调用 Javassist 库中的 API 生成不同部位的代码,最后得到一个完整的 Class 对象
具体步骤如下:
- 初始化 Javassist,设置默认参数,如设置当前的classpath
- 通过正则匹配出所有 import 的包,并使用 Javassist 添加 import
- 通过正则匹配出所有 extends 的包,创建 Class 对象,并使用 Javassist 添加 extends
- 通过正则匹配出所有 implements 包,并使用 Javassist 添加 implements
- 通过正则匹配出类里面所有内容,即得到 {} 中的内容,再通过正则匹配出所有方法,并使用 Javassist 添加类方法
- 生成 Class 对象
4.2 JDK 动态代码编译
JDKCompiler 是 Dubbo 编译器的另一种实现,使用了 JDK 自带的编译器,原生JDK编译器包位于 java.tools
下。主要使用了三个东西:JavaFileObject 接口
、ForwardingJavaFileManager 接口
、JavaCompiler.CompilationTask 方法
整个动态编译过程可以简单地总结为:首先初始化一个 JavaFileObject 对象,并把字符串作为参数传入构造方法,然后调用 JavaCompiler.CompilationTask 方法编译出具体的类。JavaFileManager 负责管理类文件输入/输出的位置
-
JavaFileObject接口:
字符串代码会被包装成一个文件对象,并提供获取二进制流的接口。Dubbo框架中的JavaFileObjectImpl类可以看做该接口的一种扩展实现,构造方法中需要传入生成好的字符串代码,此文件对象的输入和输入都是ByteArray流。 -
JavaFileManager接口:
主要管理文件的读取和输出位置。JDK中没有可以直接使用的实现类,唯一的实现鳄梨ForwardingJavaFileManager构造器又是protect类型。因此Dubbo中定制化实现了一个JavaFileManagerImpl类,并通过一个自定义类加载器ClassLoaderImpl完成资源加载。 -
JavaCompiler.CompilationTask:
把JavaFileObject对象编译层具体的类
Dubbo 配置覆盖优先级
以 timeout 为例,下图显示了配置的查找顺序,其它 retries, loadbalance, actives 等类似:
- 方法级优先,接口级次之,全局配置再次之
- 如果级别一样,则消费方优先,提供方次之
其中,服务提供方配置,通过 URL 经由注册中心传递给消费方