文章目录
服务化必然离不开集群的概念。同一服务会部署在多台机器上,于是有了一系列集群的内容。服务调用失败时的处理措施也是需要考虑的,是重试呢,还是抛出异常,亦或是只打印异常等;还有如何获取服务提供者的服务列表,以及如何自定义路由规则,比如指定ip的消费者调用指定ip的提供者;服务消费者需要决定选择哪个服务提供者进行调用。
1. 集群模块的内容
由上述几个问题引出dubbo集群容错相关内容:
- 集群容错策略:对应接口:Cluster,核心方法:join
- 服务字典:对应接口:Directory,核心方法:list
- 服务路由:对应接口:Router,核心方法:route
- 负载均衡:对应接口:LoadBalance,核心方法:select
以及三个Invoker接口的抽象类或实现类:AbstractClusterInvoker、MergeableClusterInvoker、MockClusterInvoker。后两个是特殊类型。
再详细研究各模块内容之前,先看看dubbo集群内容的工作原理和触发时机。
2. 集群内容总体流程
dubbo集群模块的工作主要包含两个阶段。
第一阶段,①,发生在服务消费端启动时,服务引用过程的创建Invoker阶段。此时RegistryProtocol
的doRefer方法调用Cluster
接口的join方法,得到一个ClusterInvoker。
第二阶段,②③④⑤,发生在服务调用阶段,ClusterInvoker的invoke方法被调用,触发这几个动作:
- 调用
Directory
的list方法,得到服务列表List<Invoker<T>>
,也叫服务字典。如图,服务列表有两种,静态列表和实时从注册中心获取变化的动态列表。默认动态列表。 - 在list方法内部,获取到服务列表后,再调用
Router
的route方法,进一步筛选,得到路由后的服务列表。如图,路由也有两种,脚本路由和条件路由。默认为条件路由。 - 对于大部分集群策略,还会有负载均衡的过程,即调用
LoadBalance
的select方法,获取到真实的Invoker,比如DubboInvoker。负载均衡策略有加权随机、加权轮询、最小活跃数、一致性哈希四种,默认为加权随机算法。 - 得到真实的Invoker后,Cluster层的内容也算完了,之后就是调用真实Invoker的invoke方法,它会调用服务目标方法,完成RPC调用。
介绍完集群层的总体内容,本文下面详细研究集群容错机制。服务字典,服务路由和负载均衡在后续文章介绍。
3. 集群容错机制
3.1 代码结构图
集群容错机制增强了整个应用的鲁棒性。Cluster接口一共有九种实现,分别对应九种ClusterInvoker,支持SPI扩展。其中有七种通用容错机制,ClusterInvoker继承AbstractClusterInvoker,和两种特殊容错机制,直接implements Invoker接口。
下面是ClusterInvoker的类图:
3.2 九种容错机制概览
dubbo实现了七种集群容错机制可供选择,当某个服务远程调用出现异常时,如网络抖动、服务暂时不可用,可以根据不同策略自动容错。另外还有两种机制用于本地测试和服务降级。下面介绍通用得其中容错机制。
机制名 | 简介 | 优缺点 | 适用场景 |
---|---|---|---|
Failover | 失败重试其他节点 ,可通过retris 属性设置重试次数,dubbo默认容错机制 | 导致接口延迟增大,并加重下游服务的负载 | 读操作和幂等的写操作 |
Failfast | 失败快速返回异常 | 受网络抖动影响较大 | 非幂等接口的调用 |
Failsafe | 失败直接忽略异常 | \ | 如某些不重要的日志同步 |
Failback | 失败自动记录在失败队列里,并由一个定时线程池定时重试 | \ | 异步或要求最终一致性的请求 |
Forking | 并行调用多个服务,只要有一个返回,则立即返回结果。可通过forks 属性设置最大并行调用数 | 浪费服务器资源 | 适用于对实时性要求极高的调用 |
Broadcast | 广播所有服务,任意节点报错则报错 | \ | 状态更新后的广播 |
Avaliable | 遍历所有列表,找到一个可用节点,则请求并返回结果。若没有可用节点则抛异常 | \ | 最简单的方式 |
Mock | 广播调用所有服务,任意节点报错则报错 | 特殊机制 | 服务降级 |
Mergeable | 可以自动把多个请求节点得到的请求合并 | 特殊机制 | 本地测试 |
4. 集群容错源码解析
前面说过,集群内容从第一阶段,服务引用过程开始,RegistryProtocol
的doRefer方法调用Cluster
接口的join方法,得到一个ClusterInvoker。下面先看看Cluster接口
4.1 Cluster
Cluster接口支持Dubbo SPI扩展,定义了join方法,默认实现类是FailoverCluster,join方法的实现也很简单,就是new一个对应的ClusterInvoker返回。
public class FailoverCluster implements Cluster {
public final static String NAME = "failover";
@Override
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
return new FailoverClusterInvoker<T>(directory);
}
}
后面看看第二阶段,服务引用时Cluster接口的invoke方法被调用。
4.2 AbstractClusterInvoker#invoke()
先看看各种 Cluster Invoker 的父类 AbstractClusterInvoker的invoke()方法,
@Override
public Result invoke(final Invocation invocation) throws RpcException {
checkWhetherDestroyed();
LoadBalance loadbalance = null;
// binding attachments into invocation.
Map<String, String> contextAttachments = RpcContext.getContext().getAttachments();
if (contextAttachments != null && contextAttachments.size() != 0) {
((RpcInvocation) invocation).addAttachments(contextAttachments);
}
// 调用directory的list方法,获取路由后服务列表
List<Invoker<T>> invokers = list(invocation);
if (invokers != null && !invokers.isEmpty()) {
// 获取负载均衡策略
loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl()
.getMethodParameter(RpcUtils.getMethodName(invocation), Constants.LOADBALANCE_KEY, Constants.DEFAULT_LOADBALANCE));
}
RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
// doInvoke抽象方法,由各子类实现
return doInvoke(invocation, invokers, loadbalance);
}
AbstractClusterInvoker的invoke()方法,首先做了服务列表,服务路由,获取负载均衡策略等工作,然后后面的集群容错逻辑由各个子类取实现。下面看看各个子类的代码
4.2.1 FailoverClusterInvoker
FailoverClusterInvoker 在调用失败时,会自动切换 Invoker 进行重试。这个是dubbo默认使用的实现类。
public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
List<Invoker<T>> copyinvokers = invokers;
checkInvokers(copyinvokers, invocation);
// 获取重试次数配置
int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;
if (len <= 0) {
len = 1;
}
// retry loop.
RpcException le = null; // last exception.
List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyinvokers.size()); // invoked invokers.
Set<String> providers = new HashSet<String>(len);
// 循环调用,失败重试
for (int i = 0; i < len; i++) {
if (i > 0) {
checkWhetherDestroyed();
// 重新调用之前,重新获取最新的服务列表
copyinvokers = list(invocation);
// check again
checkInvokers(copyinvokers, invocation);
}
// 调用select方法负载均衡,拿到真实的Invoker
Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked);
invoked.add(invoker);
RpcContext.getContext().setInvokers((List) invoked);
try {
// 调用真实invoker的invoke方法
Result result = invoker.invoke(invocation);
if (le != null && logger.isWarnEnabled()) {
logger.warn("...");
}
return result;
} catch (RpcException e) {
if (e.isBiz()) { // biz exception.
throw e;
}
le = e;
} catch (Throwable e) {
le = new RpcException(e.getMessage(), e);
} finally {
providers.add(invoker.getUrl().getAddress());
}
}
throw new RpcException("...");
}
如上,FailoverClusterInvoker 的 doInvoke 方法首先是获取重试次数,然后根据重试次数进行循环调用,失败后进行重试。在 for 循环内,首先是通过负载均衡组件选择一个 Invoker,然后再通过这个 Invoker 的 invoke 方法进行远程调用。如果失败了,记录下异常,并进行重试。重试时会再次调用父类的 list 方法列举 Invoker。
select方法在调用loadbalance.selct方法前后都做了一些逻辑处理,比如读取粘滞连接配置(sticky ,尽可能获取同一个服务器的服务)、失败重选等,这里不再细说。
4.2.2 FailbackClusterInvoker
FailbackClusterInvoker 会在调用失败后,返回一个空结果给服务消费者。并通过定时任务对失败的调用进行重传,适合执行消息通知等操作。
@Override
protected Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
try {
checkInvokers(invokers, invocation);
Invoker<T> invoker = select(loadbalance, invocation, invokers, null);
return invoker.invoke(invocation);
} catch (Throwable e) {
logger.error("Failback to invoke method " + invocation.getMethodName() + ", wait for retry in background. Ignored exception: "
+ e.getMessage() + ", ", e);
// 失败执行addFailed,返回空RpcResult
addFailed(invocation, this);
return new RpcResult(); // ignore
}
}
private void addFailed(Invocation invocation, AbstractClusterInvoker<?> router) {
// double check 保证线程安全
if (retryFuture == null) {
synchronized (this) {
if (retryFuture == null) {
// 定时任务执行重传,每隔5秒执行一次
retryFuture = scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
// collect retry statistics
try {
retryFailed();
} catch (Throwable t) { // Defensive fault tolerance
logger.error("Unexpected error occur at collect statistic", t);
}
}
}, RETRY_FAILED_PERIOD, RETRY_FAILED_PERIOD, TimeUnit.MILLISECONDS); // RETRY_FAILED_PERIOD = 5 * 1000
}
}
}
failed.put(invocation, router);
}
void retryFailed() {
if (failed.size() == 0) {
return;
}
// 遍历 failed,对失败的调用进行重试
for (Map.Entry<Invocation, AbstractClusterInvoker<?>> entry : new HashMap<Invocation, AbstractClusterInvoker<?>>(
failed).entrySet()) {
Invocation invocation = entry.getKey();
Invoker<?> invoker = entry.getValue();
try {
invoker.invoke(invocation);
failed.remove(invocation);
} catch (Throwable e) {
logger.error("Failed retry to invoke method " + invocation.getMethodName() + ", waiting again.", e);
}
}
}
上面代码也很简单,首先是 doInvoker,该方法负责初次的远程调用。若远程调用失败,则通过 addFailed 方法将调用信息存入到 failed 中,等待定时重试。addFailed 在开始阶段会根据 retryFuture 为空与否,来决定是否开启定时任务。retryFailed 方法则是包含了失败重试的逻辑,该方法会对 failed 进行遍历,然后依次对 Invoker 进行调用。调用成功则将 Invoker 从 failed 中移除,调用失败则忽略失败原因。
4.2.3 FailfastClusterInvoker
FailfastClusterInvoker 只会进行一次调用,失败后立即抛出异常。适用于幂等操作,比如新增记录。
这个代码就更简单了,失败直接抛异常。
@Override
public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
checkInvokers(invokers, invocation);
// 选择 Invoker
Invoker<T> invoker = select(loadbalance, invocation, invokers, null);
try {
// 调用 Invoker
return invoker.invoke(invocation);
} catch (Throwable e) {
if (e instanceof RpcException && ((RpcException) e).isBiz()) {
// 抛出异常
throw (RpcException) e;
}
// 抛出异常
throw new RpcException(..., "Failfast invoke providers ...");
}
}
4.2.4 FailsafeClusterInvoker
FailsafeClusterInvoker 是一种失败安全的 Cluster Invoker。所谓的失败安全是指,当调用过程中出现异常时,FailsafeClusterInvoker 仅会打印异常,而不会抛出异常。适用于写入审计日志等操作。
@Override
public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
try {
checkInvokers(invokers, invocation);
// 选择 Invoker
Invoker<T> invoker = select(loadbalance, invocation, invokers, null);
// 进行远程调用
return invoker.invoke(invocation);
} catch (Throwable e) {
// 打印错误日志,但不抛出
logger.error("Failsafe ignore exception: " + e.getMessage(), e);
// 返回空结果忽略错误
return new RpcResult();
}
}
4.2.5 ForkingClusterInvoker
ForkingClusterInvoker 会在运行时通过线程池创建多个线程,并发调用多个服务提供者。只要有一个服务提供者成功返回了结果,doInvoke 方法就会立即结束运行。ForkingClusterInvoker 的应用场景是在一些对实时性要求比较高读操作(注意是读操作,并行写操作可能不安全)下使用,但这将会耗费更多的资源。
public Result doInvoke(final Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
try {
checkInvokers(invokers, invocation);
final List<Invoker<T>> selected;
// 获取 forks 配置
final int forks = getUrl().getParameter(Constants.FORKS_KEY, Constants.DEFAULT_FORKS);
// 获取超时配置
final int timeout = getUrl().getParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
// 如果 forks 配置不合理,则直接将 invokers 赋值给 selected
if (forks <= 0 || forks >= invokers.size()) {
selected = invokers;
} else {
selected = new ArrayList<Invoker<T>>();
// 循环选出forks个invoker,放入selected
for (int i = 0; i < forks; i++) {
Invoker<T> invoker = select(loadbalance, invocation, invokers, selected);
if (!selected.contains(invoker)) {//Avoid add the same invoker several times.
selected.add(invoker);
}
}
}
RpcContext.getContext().setInvokers((List) selected);
final AtomicInteger count = new AtomicInteger();
final BlockingQueue<Object> ref = new LinkedBlockingQueue<Object>();
// 遍历 selected 列表
for (final Invoker<T> invoker : selected) {
// 为每个 Invoker 创建一个执行线程
executor.execute(new Runnable() {
@Override
public void run() {
try {
Result result = invoker.invoke(invocation);
// 将结果放到阻塞队列中
ref.offer(result);
} catch (Throwable e) {
int value = count.incrementAndGet();
// 仅在value大于等于selected.size() 时,也就是全部调用失败,才将异常对象放到阻塞队列中
if (value >= selected.size()) {
ref.offer(e);
}
}
}
});
}
try {
Object ret = ref.poll(timeout, TimeUnit.MILLISECONDS);
if (ret instanceof Throwable) {
Throwable e = (Throwable) ret;
throw new RpcException(e instanceof RpcException ? ((RpcException) e).getCode() : 0, "Failed to forking invoke provider " + selected + ", but no luck to perform the invocation. Last error is: " + e.getMessage(), e.getCause() != null ? e.getCause() : e);
}
return (Result) ret;
} catch (InterruptedException e) {
throw new RpcException("Failed to forking invoke provider " + selected + ", but no luck to perform the invocation. Last error is: " + e.getMessage(), e);
}
} finally {
// clear attachments which is binding to current thread.
RpcContext.getContext().clearAttachments();
}
}
如上,ForkingClusterInvoker 的流程是,首先获取forks参数,也就是同时调用invoker的数量,默认为invoker总数。然后选出forks个invoker,并各自用独立的线程同时调用,得到结果RpcResult或Throwable都放入阻塞队列,后面再去poll一个结果,如果是RpcResult,则正常返回。如果是Throwable,说明全部调用失败,抛出异常。
ForkingClusterInvoker 的特性是,只要有一个服务提供者成功返回了结果,doInvoke 方法就会立即结束运行。这里逻辑的关键代码就是value >= selected.size()
这里的判断。这个判断成立说明invoker全部调用失败,这是才在阻塞队列里放异常结果。
4.2.6 BroadcastClusterInvoker
BroadcastClusterInvoker 会逐个调用每个服务提供者,如果其中一台报错,在循环调用结束后,BroadcastClusterInvoker 会抛出异常。该类通常用于通知所有提供者更新缓存或日志等本地资源信息。
实现方式很简单,这里就不贴代码了。
5. 总结
本节介绍集群容错部分总体内容,包括服务字典(Directory)、集群容错策略(Cluster)、服务路由(Router)、负载均衡(LoadBalance),以及这几部分内容的关键方法和调用时机。然后详细解读了集群容错策略部分的源码,其他三个部分的源码细节,后续博客继续详细解读。