控制复杂性是计算机编程的本质。
代码下载地址:https://github.com/f641385712/netflix-learning
在分布式场景中,调用第三方接口会因为网络延迟、异常导致调用的服务出错,重试几次可能就会调用成功,是提高结果正确性的一种有效手段。重试机制最简单呢理解为try-catch-redo
模式,但是优雅的重试也是有要求的,至少应该满足如下要求:
市面上也有单独的比较流行的重试框架如:spring-retry、guava-retry等,本文主要来看看Ribbon内部重试机制的实现:RetryHandler
。
重试固然重要,但不是什么场景下都适合重试的,并且重试在生产环境中需要慎用。对于重试是有场景限制的,比如参数校验不合法、写操作等(要考虑写是否幂等)都不适合重试。远程调用超时、网络突然中断可以重试。在微服务治理框架中,通常都有自己的重试与超时配置,Ribbon自然也有重试的能力。
重试,是类似于Ribbon这种组件里特别重要的概念,因此此接口特别的重要。它负责对执行时若发生异常时的一个处理接口:重试or让异常继续抛出。
说明:这和Feign的
feign.Retryer
功能是类似的
public interface RetryHandler {
public static final RetryHandler DEFAULT = new DefaultLoadBalancerRetryHandler();
// 该异常是否可处理(可重试)
// sameServer:true表示在同一台机器上重试。否则去其它机器重试
public boolean isRetriableException(Throwable e, boolean sameServer);
// 是否是Circuit熔断类型异常。比如java.net.ConnectException就属于这种故障
// 这种异常类型一般属于比较严重的,发生的次数多了就会把它熔断(下次不会再找它了)
public boolean isCircuitTrippingException(Throwable e);
// 要在一台服务器上执行的最大重试次数
public int getMaxRetriesOnSameServer();
// 要重试的最大不同服务器数。2表示最多去2台不同的服务器身上重试
public int getMaxRetriesOnNextServer();
}
core包内它仅有两个实现类:DefaultLoadBalancerRetryHandler
和RequestSpecificRetryHandler
。在Spring Cloud
下的情况如下:
默认的重试实现。它只能识别java.net
里的异常做出判断。若你有其它异常,你可以继承子类然后复写相关方法。
public class DefaultLoadBalancerRetryHandler implements RetryHandler {
// 这两个异常会进行重试。代表连接不上嘛,重试是很合理的
private List<Class<? extends Throwable>> retriable = Lists.newArrayList(ConnectException.class, SocketTimeoutException.class);
// 和电路circuit相关的异常类型
private List<Class<? extends Throwable>> circuitRelated = Lists.newArrayList(SocketException.class, SocketTimeoutException.class);
// 不解释。它哥三个都可以通过IClientConfig配置
// `MaxAutoRetries`,默认值是0。也就是说在同一机器上不重试(只会执行一次,失败就失败了)
protected final int retrySameServer;
// `MaxAutoRetriesNextServer`,默认值是1,也就是只会再试下面一台机器 不行就不行了
protected final int retryNextServer;
// 重试开关。true:开启重试 false:不开启重试
// `OkToRetryOnAllOperations`属性控制其值,默认也是false 也就是说默认并不重试
protected final boolean retryEnabled;
// 构造器赋值:值可以从IClientConfig里来(常用)
// 当然你也可以通过其他构造器传过来
public DefaultLoadBalancerRetryHandler(IClientConfig clientConfig) {
this.retrySameServer = clientConfig.get(CommonClientConfigKey.MaxAutoRetries, DefaultClientConfigImpl.DEFAULT_MAX_AUTO_RETRIES);
this.retryNextServer = clientConfig.get(CommonClientConfigKey.MaxAutoRetriesNextServer, DefaultClientConfigImpl.DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER);
this.retryEnabled = clientConfig.get(CommonClientConfigKey.OkToRetryOnAllOperations, false);
}
}
根据重试的概念,以上属性似乎已经能够解答重试所需的两个问题:
DefaultLoadBalancerRetryHandler:
// 是否是可进行重试的异常类型?
@Override
public boolean isRetriableException(Throwable e, boolean sameServer) {
// 1、若retryEnabled=false全局关闭了禁止重试,那就掉头就走,不用看了
// 2、若retryEnabled=true,就继续看看吧
if (retryEnabled) {
// 3、若是在同一台Server上(注意此Server上首次请求已经失败),所以需要看这次的异常类型是啥
if (sameServer) {
return Utils.isPresentAsCause(e, getRetriableExceptions());
} else { // 若是不同Server,那就直接告诉说可以重试呗
return true;
}
}
return false;
}
... // 省略相关get方法
该判断可以解释为:开启重试的情况下,若是同一台Server,因为它失败过了,所以需要判断这次的异常类型是啥是否需要重试;若是不同Server,你都不知道它是否ok,所以肯定让其重试给其机会。
DefaultLoadBalancerRetryHandler:
@Override
public int getMaxRetriesOnSameServer() {
return retrySameServer;
}
@Override
public int getMaxRetriesOnNextServer() {
return retryNextServer;
}
它在ribbon-httpclient
和ribbon-transport
包里有两个子类:HttpClientLoadBalancerErrorHandler
和NettyHttpLoadBalancerErrorHandler
,本处先不展开。
Specific
:特征,细节,特殊的。也就是说它是和Request请求特征相关的重试处理器。
Ribbon会为允许请求的每个请求创建的RetryHandler的实例,每个请求可以带有自己的requestConfig,比如每个Client请求都可以有自己的retrySameServer
和retryNextServer
参数。
public class RequestSpecificRetryHandler implements RetryHandler {
// fallback默认使用的是RetryHandler.DEFAULT
// 有点代理的意思
private final RetryHandler fallback;
private int retrySameServer = -1;
private int retryNextServer = -1;
// 只有是连接异常,也就是SocketException或者其子类异常才执行重试
private final boolean okToRetryOnConnectErrors;
// 若是true:只要异常了,任何错都执行重试
private final boolean okToRetryOnAllErrors;
protected List<Class<? extends Throwable>> connectionRelated = Lists.newArrayList(SocketException.class);
public boolean isConnectionException(Throwable e) {
return Utils.isPresentAsCause(e, connectionRelated);
}
// 构造器为属性赋值。requestConfig可以是单独的,若没指定就使用默认全局的
public RequestSpecificRetryHandler(boolean okToRetryOnConnectErrors, boolean okToRetryOnAllErrors, RetryHandler baseRetryHandler, @Nullable IClientConfig requestConfig) {
Preconditions.checkNotNull(baseRetryHandler);
this.okToRetryOnConnectErrors = okToRetryOnConnectErrors;
this.okToRetryOnAllErrors = okToRetryOnAllErrors;
this.fallback = baseRetryHandler;
if (requestConfig != null) {
if (requestConfig.containsProperty(CommonClientConfigKey.MaxAutoRetries)) {
retrySameServer = requestConfig.get(CommonClientConfigKey.MaxAutoRetries);
}
if (requestConfig.containsProperty(CommonClientConfigKey.MaxAutoRetriesNextServer)) {
retryNextServer = requestConfig.get(CommonClientConfigKey.MaxAutoRetriesNextServer);
}
}
}
}
相较于默认实现,它主要是针对Request,使得每个Request都能有一份独自的、自己的重试策略,通过传入requestConfig
来实现,若没有特别指定那便会使用RetryHandler fallback
策略进行兜底。下面是接口方法的实现:
RequestSpecificRetryHandler:
@Override
public boolean isRetriableException(Throwable e, boolean sameServer) {
// 若强制开启所有错误都重试,那就没啥好说的
// 此参数默认是false,只能通过构造器来指定其值
if (okToRetryOnAllErrors) {
return true;
}
// ClientException属于执行过程中会抛出的异常类型,所以需要加以判断
else if (e instanceof ClientException) {
ClientException ce = (ClientException) e;
// 若是服务端异常,那就同一台Server上不用重试了,没要求是同一台Server才允许其重试
if (ce.getErrorType() == ClientException.ErrorType.SERVER_THROTTLED) {
return !sameServer;
// 若不是服务端异常类型,那就换台Server都不用重试了
} else {
return false;
}
}
// 若不是ClientException,那就看看异常是否是Socket的链接异常喽
// okToRetryOnConnectErrors的值也是由构造的时候指定的
// 没有默认值....
else {
return okToRetryOnConnectErrors && isConnectionException(e);
}
}
// =========其它方法实现均为代理=========
@Override
public boolean isCircuitTrippingException(Throwable e) {
return fallback.isCircuitTrippingException(e);
}
@Override
public int getMaxRetriesOnSameServer() {
if (retrySameServer >= 0)
return retrySameServer;
return fallback.getMaxRetriesOnSameServer();
}
@Override
public int getMaxRetriesOnNextServer() {
if (retryNextServer >= 0)
return retryNextServer;
return fallback.getMaxRetriesOnNextServer();
}
该实现类对isRetriableException()
方法的实现稍显复杂,建议读者理解消化。该方法是最为重要的一个方法,唯一调用处是后面将要讲述的LoadBalancerCommand#retryPolicy
处,表示重试策略。
VIP地址解析器,“VipAddress”是目标服务器场的逻辑名称,该处理器帮助解析并获取得到最终的地址。
public interface VipAddressResolver {
public String resolve(String vipAddress, IClientConfig niwsClientConfig);
}
哪怕在Spring Cloud
环境下,有且仅有唯一一个实现类:SimpleVipAddressResolver
public class SimpleVipAddressResolver implements VipAddressResolver {
// 变量模版:包含在{}里面的算作变量
private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\{(.*?)\\}");
@Override
public String resolve(String vipAddressMacro, IClientConfig niwsClientConfig) {
if (vipAddressMacro == null || vipAddressMacro.length() == 0)
return vipAddressMacro;
return replaceMacrosFromConfig(vipAddressMacro);
}
// 私有方法:使用正则处理字符串
private static String replaceMacrosFromConfig(String macro) {
String result = macro;
Matcher matcher = VAR_PATTERN.matcher(result);
// 找到所有变量,然后替换掉真实值
while (matcher.find()) {
String key = matcher.group(1);
// 从全局配置里面找出真实值
String value = ConfigurationManager.getConfigInstance().getString(key);
if (value != null) {
result = result.replaceAll("\\$\\{" + key + "\\}", value);
// 给matcher重新赋值,继续下一个查找
matcher = VAR_PATTERN.matcher(result);
}
return result.trim();
}
}
}
实现略简单:使用正则表达式,替换字符串内的“变量”。
@Test
public void fun4() {
// 准备配置对象IClientConfig
// IClientConfig config = new DefaultClientConfigImpl();
// config.set(CommonClientConfigKey.valueOf("foo"), "YourBatman");
// config.set(CommonClientConfigKey.valueOf("port"), 80);
// config.set(CommonClientConfigKey.valueOf("foobar"), "Jay");
Configuration configuration = ConfigurationManager.getConfigInstance();
configuration.setProperty("foo","YourBatman");
// configuration.setProperty("port",80); // 这样报错,必须是字符串,尴尬
configuration.setProperty("port","80");
configuration.setProperty("foobar","Jay");
String vipArr = "${foo}.bar:${port},${foobar}:80,localhost:8080";
VipAddressResolver vipAddressResolver = new SimpleVipAddressResolver();
String vipAddredd = vipAddressResolver.resolve(vipArr, null);
System.out.println(vipAddredd);
}
控制台输出:
YourBatman.bar:80,Jay:80,localhost:8080
请注意:这里取值不是从IClientConfig
里取的,而是从全局Configuration配置里取值的,请勿弄错。
说明:
IClientConfig
里的值可以来自于配置Configuration,但是设置进IClientConfig
的值可不会跑到Configuration里面去~
关于Ribbon
的重试处理器RetryHandler
就介绍到这了,需要注意的是Ribbon把重试机制放在了ribbon-core
包下,而非ribbon-loadbalancer
下,是因为重试机制并不是负载均衡的内容,而是execute
执行时的概念。