前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[享学Netflix] 四十、Ribbon核心API源码解析:ribbon-core(三)RetryHandler重试处理器

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

作者头像
YourBatman
发布2020-03-19 10:24:04
1.2K0
发布2020-03-19 10:24:04
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦

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

代码下载地址:https://github.com/f641385712/netflix-learning

前言

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

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

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


正文

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


RetryHandler重试处理器

重试,是类似于Ribbon这种组件里特别重要的概念,因此此接口特别的重要。它负责对执行时若发生异常时的一个处理接口:重试or让异常继续抛出。

说明:这和Feign的feign.Retryer功能是类似的

代码语言:javascript
复制
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里的异常做出判断。若你有其它异常,你可以继承子类然后复写相关方法。

代码语言:javascript
复制
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);
    }
}

根据重试的概念,以上属性似乎已经能够解答重试所需的两个问题:

  • 什么时候需要重试?重试肯定是抛出了异常,所以需要检查该异常需不需要重试:
代码语言:javascript
复制
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,所以肯定让其重试给其机会。

  • 重试策略/重试几次呢?
代码语言:javascript
复制
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参数。

代码语言:javascript
复制
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策略进行兜底。下面是接口方法的实现:

代码语言:javascript
复制
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”是目标服务器场的逻辑名称,该处理器帮助解析并获取得到最终的地址。

代码语言:javascript
复制
public interface VipAddressResolver {
    public String resolve(String vipAddress, IClientConfig niwsClientConfig);
}

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


SimpleVipAddressResolver
代码语言:javascript
复制
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();        
		}
	}
}

实现略简单:使用正则表达式,替换字符串内的“变量”。


示例
代码语言:javascript
复制
@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);
}

控制台输出:

代码语言:javascript
复制
YourBatman.bar:80,Jay:80,localhost:8080

请注意:这里取值不是从IClientConfig里取的,而是从全局Configuration配置里取值的,请勿弄错。

说明:IClientConfig里的值可以来自于配置Configuration,但是设置进IClientConfig的值可不会跑到Configuration里面去~


总结

关于Ribbon的重试处理器RetryHandler就介绍到这了,需要注意的是Ribbon把重试机制放在了ribbon-core包下,而非ribbon-loadbalancer下,是因为重试机制并不是负载均衡的内容,而是execute执行时的概念。

分隔线
分隔线
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 正文
    • RetryHandler重试处理器
      • DefaultLoadBalancerRetryHandler
      • RequestSpecificRetryHandler
    • VipAddressResolver
      • SimpleVipAddressResolver
      • 示例
  • 总结
相关产品与服务
负载均衡
负载均衡(Cloud Load Balancer,CLB)提供安全快捷的流量分发服务,访问流量经由 CLB 可以自动分配到云中的多台后端服务器上,扩展系统的服务能力并消除单点故障。负载均衡支持亿级连接和千万级并发,可轻松应对大流量访问,满足业务需求。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档