[享学Netflix] 四十、Ribbon核心API源码解析:ribbon-core(三)RetryHandler重试处理器

控制复杂性是计算机编程的本质。

–> 返回专栏总目录 <–
代码下载地址:https://github.com/f641385712/netflix-learning

前言

在分布式场景中,调用第三方接口会因为网络延迟、异常导致调用的服务出错,重试几次可能就会调用成功,是提高结果正确性的一种有效手段。重试机制最简单呢理解为try-catch-redo模式,但是优雅的重试也是有要求的,至少应该满足如下要求:

  • 无侵入:不改动当前的业务逻辑,对于需要重试的地方,可以很简单的实现
  • 可配置:包括重试次数,重试的间隔时间,是否使用异步方式等
  • 通用性:最好是无改动(或者很小改动)的支持绝大部分的场景

市面上也有单独的比较流行的重试框架如:spring-retry、guava-retry等,本文主要来看看Ribbon内部重试机制的实现:RetryHandler


正文

重试固然重要,但不是什么场景下都适合重试的,并且重试在生产环境中需要慎用。对于重试是有场景限制的,比如参数校验不合法、写操作等(要考虑写是否幂等)都不适合重试。远程调用超时、网络突然中断可以重试。在微服务治理框架中,通常都有自己的重试与超时配置,Ribbon自然也有重试的能力。


RetryHandler重试处理器

重试,是类似于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包内它仅有两个实现类:DefaultLoadBalancerRetryHandlerRequestSpecificRetryHandler。在Spring Cloud下的情况如下:

在这里插入图片描述
在这里插入图片描述


DefaultLoadBalancerRetryHandler

默认的重试实现。它只能识别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-httpclientribbon-transport包里有两个子类:HttpClientLoadBalancerErrorHandlerNettyHttpLoadBalancerErrorHandler,本处先不展开。


RequestSpecificRetryHandler

Specific:特征,细节,特殊的。也就是说它是和Request请求特征相关的重试处理器。

Ribbon会为允许请求的每个请求创建的RetryHandler的实例,每个请求可以带有自己的requestConfig,比如每个Client请求都可以有自己的retrySameServerretryNextServer参数。

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处,表示重试策略。


VipAddressResolver

VIP地址解析器,“VipAddress”是目标服务器场的逻辑名称,该处理器帮助解析并获取得到最终的地址。

public interface VipAddressResolver {
    public String resolve(String vipAddress, IClientConfig niwsClientConfig);
}

哪怕在Spring Cloud环境下,有且仅有唯一一个实现类:SimpleVipAddressResolver


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执行时的概念。
分隔线

声明

原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭。你也可【左边扫码/或加wx:fsx641385712】邀请你加入我的 Java高工、架构师 系列群大家庭学习和交流。
往期精选

发布了377 篇原创文章 · 获赞 572 · 访问量 51万+
App 阅读领勋章
微信扫码 下载APP
阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: Age of Ai 设计师: meimeiellie

分享到微信朋友圈

×

扫一扫,手机浏览