6.7再谈 Invoker
在前面的服务注册与发现中,我们发现,服务在订阅过程中,把 notify 过来的 urls 都转成了 invoker,不知道大家是否还记得前面的 rpc 过程,protocol也是在服务端和消费端各连接子一个 invoker,如下图:
这张图主要展示 rpc 主流程,消费端想要远程调用时,他是调用 invoker.invoke方法;服务端收到网络请求时,也是直接触发 invoker.invoke 方法。
对的,你没有猜错,Dubbo 设计 invoker 实体的初衷,就是想要统一操作,无论你要做什么方法调用,都请使用 invoker 来包装后,使用 invoker.invoke来调用这个动作(你内部如何转我不管),简化来看,rpc 过程即是如此:
消费端 invoker.invoke ------>网络---->服务端 invoker.invoke--->ref 服务
上面的链路是个简化的路径,但在实际的 dubbo 调用中,此链条可能会有局部的多层嵌套,如:
消费端 invoker.invoke ------>容错策略--->网络---->服务端 invoker.invoke--->ref服务
那么此时要重新定义链条吗?那不是个好主意。
Dubbo 的做法是这样,将容错策略也包装成 invoker 对象:
FailfastClusterInvoker.invoke--->protocolInvoker.invoke-->网络---->服务端---->invoker.invoke--->ref 服务
依次类推,dubbo 内部有非常多的 invoker 包装类,它们层层嵌套,但 rpc流程不关心细节,只傻瓜式地调用其 invoke 方法,剩下的逻辑自会传递到最后一个 invoker 进行网络调用。
下面我们来研究一下 Cluster、Cluster Invoker、Directory、Router 和LoadBalance 组件是如何嵌入到 rpc 调用过程中来的。
6.7.1 集群容错
在消费端,当 protocol.refer 得到 invoker 对象后,按调用关系,会把 invoker对象作为目标 target,做一个动态代理生成 service 接口代理。
Dubbo 在这里玩了个心眼。真正的过程走得百绕千回,看这段代码,RegistryProtocol.doRefer 方法:
private
Invoker doRefer(Cluster cluster, Registry registry, Class type, URL url) { RegistryDirectory
directory = new RegistryDirectory (type, url); directory.setRegistry(registry);
directory.setProtocol(protocol);
//......(省略部分代码)
// 包装一个 invoker 集群返回
Invoker invoker = cluster.join(directory);
//......(省略部分代码)
return invoker;
}
原来的 protocol 被转进了 RegistryDirectory 类中去了,doRefer 返回的 invoker对象,是 cluster.join(directory)返回的 invoker。
而 cluster 是一个扩展接口,因此,这个接口方法最终执行的对象,是根据容错策略自适配出来的对象,如果 url 中不指定则默认是 failover。
再看 failover 实现类的逻辑,非常简单,只是返回一个FailoverClusterInvoker 对象:
public class FailoverCluster implements Cluster {
public final static String NAME = "failover";
@Override
publicInvokerjoin(Directorydirectory) throws RpcException {
return new FailoverClusterInvoker(directory);
}
}
再看下具体实现逻辑:
public class FailoverClusterInvoker
extends AbstractClusterInvoker { //......(省略部分代码)
public Result doInvoke(Invocation invocation, final List
> invokers, LoadBalance loadbalance) throws RpcException {
//......(省略部分代码)
//容错次数
int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIES_KEY,Constants.DEFAULT_RETRIES) + 1;
if (len <= 0) {
len = 1;
}
//......(省略部分代码)
for (int i = 0; i < len; i++) {
//......(省略部分代码)
Invoker
invoker = select(loadbalance, invocation, copyinvokers, invoked); //......(省略部分代码)
Result result = invoker.invoke(invocation);
return result;
}
}
}
此 invoker 的逻辑:
1、按重新次数 for 循环,只要不是正常返回,则再试一次
2、调用 select 方法,取得一个 loadbalance 策略的 invoker 对象,然后执行此 invoker 对象得到结果。要注意的是,此 select 方法,是从一组 invoker 中选择一个 invoker 出来:
Invoker
invoker = loadbalance.select(invokers, getUrl(), invocation);
3、继续往下追,在 FailoverClusterInvoker 对象的 invoke 方法中(父类)可看到,最终 invokers 来自这一句:
List
> invokers = directory.list(invocation);
此就是从这一句传入的:
Invoker invoker = cluster.join(directory);
最终追溯到 RegistryDirectory 对象的缓存 methodInvokerMap 上来。
如果你有印象,则知道,RegistryDirectory 正是我们上一步,注册/订阅逻辑的核心类,它将订阅得到的 urls 信息缓存在RegistryDirectory 中。
6.7.2 Dubbo 的过滤器链
Filter(过滤器)在很多框架中都有使用过这个概念,基本上的作用都是类似的,在请求处理前或者处理后做一些通用的逻辑,而且 Filter 可以有多个,支持层层嵌套。
Dubbo 的 Filter 实现入口是在 ProtocolFilterWrapper,因为ProtocolFilterWrapper 是 Protocol 的包装类,所以会在 SPI 加载的Extension 的时候被自动包装进来。当然 filter 要发挥作用,必定还是要在嵌入到 RPC 的调用线中(你马上应该反应过来,嵌入的办法就是包装成invoker)。
ProtocolFilterWrapper 作为包装类,会成为其它 protocol 的修饰加强外层。因此,protocol 的 export 和 refer 方法,首先是调用ProtocolFilterWrapper 类的。
暴露服务代码:
public
Exporter export(Invoker invoker) throws RpcException { //注册协议,并不是真的协议,所以用不上 filter
if (Constants.REGISTRY_PROTOCOL.equals(invoker.getUrl().getProtocol())) {
return protocol.export(invoker);
}
return protocol.export(buildInvokerChain(invoker, Constants.SERVICE_FILTER_KEY,
Constants.PROVIDER));
}
引入服务的代码:
public
Invoker refer(Class type, URL url) throws RpcException { if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
return protocol.refer(type, url);
}
return buildInvokerChain(protocol.refer(type, url), Constants.REFERENCE_FILTER_KEY,
Constants.CONSUMER);
}
可以看到,两者原来的 invoker 对象,都由 buildInvokerChain 做了一层包装,来看一下 filterChain 的逻辑:
private static
Invoker buildInvokerChain(final Invoker invoker, String key, String group){ Invoker
last = invoker; List
filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key,group);
if (!filters.isEmpty()) {
for (int i = filters.size() - 1; i >= 0; i--) {
final Filter filter = filters.get(i);
final Invoker
next = last; last = new Invoker
() { //......(省略部分代码)
@Override
public Result invoke(Invocation invocation) throws RpcException {
return filter.invoke(next, invocation);
}
//......(省略部分代码)
};
}
}
return last;
}
逻辑较为简单:
1、所有 filter 包装进 invoker 对象中,invoke 方法直接调对应的 filter.invoke
2、filter 对象首尾相联,前一个 filter.invoke 参数,传入后一个 filter 的 invoker对象
3、最后一个 filter.invoke 参数中,直接传原始的 invoker 对象
4、filter 的所有获取,按扩展点方式得到:
ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
6.8 Dubbo 的线程模型
首先明确一个基本概念:IO 线程和业务线程的区别
IO 线程:配置在 netty 连接点的用于处理网络数据的线程,主要处理编解码等直接与网络数据打交道的事件。
业务线程:用于处理具体业务逻辑的线程,可以理解为自己在 provider 上写的代码所执行的线程环境。
Dubbo 默认采用的是长链接的方式,即默认情况下一个 consumer 和一个 provider 之间只会建立一条链接,这种情况下 IO 线程的工作就是编码和解码数据,监听具体的数据请求,直接通过 Channel发布数据等等;
业务线程就是处理 IO 线程处理之后的数据,业务线程并不知道任何跟网络相关的内容,只是纯粹的处理业务逻辑,在业务处理逻辑的时候往往存在复杂的逻辑,所以业务线程池的配置往往都要比 IO 线程池的配置大很多。
IO 线程部分,Netty 服务提供方 NettyServer 又使用了两级线程池,master 主要用来接受客户端的链接请求,并把接受的请求分发给 worker 来处理。整个过程如下图:
IO 线程与业务线程的交互如下:
IO 线程的派发策略:
默认是 all:所有消息都派发到线程池,包括请求,响应,连接事件,断开事件,心跳等。 即 worker 线程接收到事件后,将该事件提交到业务线程池中,自己再去处理其他 IO 事件。
direct:worker 线程接收到事件后,由 worker 执行到底。
message:只有请求响应消息派发到线程池,其它连接断开事件,心跳等消息,直接在 IO 线程上执行。
execution:只有请求消息派发到线程池,不含响应(客户端线程池),响应和其它连接断开事件,心跳等消息,直接在 IO线程上执行。
connection:在 IO 线程上,将连接断开事件放入队列,有序逐个执行,其它消息派发到线程池。
业务线程池设置:
fixed:固定大小线程池,启动时建立线程,不关闭,一直持有。(缺省)
1. coresize:200
2. maxsize:200
3. 队列:SynchronousQueue o 回绝策略:AbortPolicyWithReport - 打印线程信息 jstack,之后抛出异常
cached:缓存线程池,空闲一分钟自动删除,需要时重建。
limited:可伸缩线程池,但池中的线程数只会增长不会收缩。只增长不收缩的目的是为了避免收缩时突然来了大流量引起的性能问题。 配置示例: