一、参考

参考资料:https://www.cnblogs.com/flying607/p/8330551.html

ribbon+spring retry重试策略源码分析:https://blog.csdn.net/xiao_jun_0820/article/details/79320352

二、背景

这几天在做服务的高可用。

为了确保提供服务的某一台机器出现故障导致客户的请求不可用,我们需要对这台服务器做故障重试或者智能路由到下一个可用服务器。

为此,特地上网查了些资料,最后选用了ribbon+spring retry的重试策略。

从参考的技术文章中可以看出,故障重试的核心

1是引入spring retry的依赖

        <dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.2.RELEASE</version>
</dependency>

2是开启zuul和ribbon重试配置

zuul:
retryable: true #重试必配
ribbon:
MaxAutoRetriesNextServer: 2 #更换服务实例次数
MaxAutoRetries: 0 #当前服务重试次数
OkToRetryOnAllOperations: true #设置成false,只处理get请求故障

当然本文的目的并不止于此。

添加完了这些配置后,我们发现依然存在一些局限性。

1、当提供服务的集群机器实例小于MaxAutoRetriesNextServer时,只有采用轮询策略的负载可以正常使用。

2、当提供服务的集群机器实例大于MaxAutoRetriesNextServer时,采取轮询或者随机策略的负载偶尔可以正常使用。

而采用最小并发策略,或者单一负载(一般是为了解决session丢失问题,即同一个客户端发的请求固定访问某个服务器)则

完全不能正常工作。

为什么这么说呢?比如我们有5台机器提供服务,第一台机器可以正常提供服务,第二台并发量最小。

当第二到第五台服务器挂掉以后,采用轮询方式且MaxAutoRetriesNextServer=2。那么,ribbon会尝试访问第三台、第四台服务器。

结果不言而喻。当然如果运气好,第三台或第四台服务器是可以用的,那就能正常提供服务。

采用随机策略,同样要依靠运气。

最小并发或单一策略的,则是不论重试几次则因为总是选择挂掉的第二个节点而完全失效。
那么,有什么解决办法呢?

三、动态设置MaxAutoRetriesNextServer

出现这些问题,一个关键是MaxAutoRetriesNextServer被写死了,而我们的提供server的数量又可能随着集群的负载情况增加(减少并不影响)。

总不能因为每次增加服务器数量就改一次MaxAutoRetriesNextServer配置吧?既然不想改配置,那当然就是动态设置MaxAutoRetriesNextServer的值啊。

翻看重试的源码 RibbonLoadBalancedRetryPolicy.java

    @Override
public boolean canRetryNextServer(LoadBalancedRetryContext context) {
//this will be called after a failure occurs and we increment the counter
//so we check that the count is less than or equals to too make sure
//we try the next server the right number of times
return nextServerCount <= lbContext.getRetryHandler().getMaxRetriesOnNextServer() && canRetry(context);
}

可以看出MaxAutoRetriesNextServer的值是从DefaultLoadBalancerRetryHandler里面获取的。但是DefaultLoadBalancerRetryHandler又不提供设置MaxAutoRetriesNextServer的接口。

往上追溯DefaultLoadBalancerRetryHandler实例化的源码

    @Bean
@ConditionalOnMissingBean
public RibbonLoadBalancerContext ribbonLoadBalancerContext(ILoadBalancer loadBalancer,
IClientConfig config, RetryHandler retryHandler) {
return new RibbonLoadBalancerContext(loadBalancer, config, retryHandler);
} @Bean
@ConditionalOnMissingBean
public RetryHandler retryHandler(IClientConfig config) {
return new DefaultLoadBalancerRetryHandler(config);
}

发现DefaultLoadBalancerRetryHandler对象可以从RibbonLoadBalancerContext实例中获取, 而RibbonLoadBalancerContext却可以从SpringClientFactory获取,那么我们只要新建retryHandler并重新赋值给RibbonLoadBalancerContext就可以了。

代码:

1、将IClientConfig托管到spring上

    @Bean
public IClientConfig ribbonClientConfig() {
DefaultClientConfigImpl config = new DefaultClientConfigImpl();
config.loadProperties(this.name);
config.set(CommonClientConfigKey.ConnectTimeout, DEFAULT_CONNECT_TIMEOUT);
config.set(CommonClientConfigKey.ReadTimeout, DEFAULT_READ_TIMEOUT);
return config;
}

2、新建retryHandler并更新到RibbonLoadBalancerContext

private void setMaxAutoRetiresNextServer(int size) {  //size: 提供服务的集群数量
SpringClientFactory factory = SpringContext.getBean(SpringClientFactory.class); //获取spring托管的单例对象
IClientConfig clientConfig = SpringContext.getBean(IClientConfig.class);
int retrySameServer = clientConfig.get(CommonClientConfigKey.MaxAutoRetries, 0);//获取配置文件中的值, 默认0
boolean retryEnable = clientConfig.get(CommonClientConfigKey.OkToRetryOnAllOperations, false);//默认false。
RetryHandler retryHandler = new DefaultLoadBalancerRetryHandler(retrySameServer, size, retryEnable);//新建retryHandler
factory.getLoadBalancerContext(name).setRetryHandler(retryHandler);
}

MaxAutoRetriesNextServer动态设置的问题就解决了。

四、剔除不可用的服务。

Eureka好像有提供服务的剔除和恢复功能,所以如果有用Eureka注册中心,就不用往下看了。具体配置我也不太清楚。

因为我们没用到eureka,所以在故障重试的时候,获取到的服务列表里依然包含了挂掉的服务器。

这样会导致最小并发策略和单一策略的负载出现问题。

跟踪源码,我们发现服务器故障后会调用canRetryNextServer方法,那么不如就在这个方法里面做文章吧。

自定义RetryPolicy 继承RibbonLoadBalancedRetryPolicy并且重写canRetryNextServer

public class ServerRibbonLoadBalancedRetryPolicy extends RibbonLoadBalancedRetryPolicy {

    private RetryTrigger trigger;
public ServerRibbonLoadBalancedRetryPolicy(String serviceId, RibbonLoadBalancerContext context, ServiceInstanceChooser loadBalanceChooser, IClientConfig clientConfig) {
super(serviceId, context, loadBalanceChooser, clientConfig);
} public void setTrigger(RetryTrigger trigger) {
this.trigger = trigger;
} @Override
public boolean canRetryNextServer(LoadBalancedRetryContext context) {
boolean retryEnable = super.canRetryNextServer(context);
if (retryEnable && trigger != null) {
//回调触发
trigger.exec(context);
}
return retryEnable;
} @FunctionalInterface
public interface RetryTrigger {
void exec(LoadBalancedRetryContext context);
}
}

自定义RetryPolicyFactory继承RibbonLoadBalancedRetryPolicyFactory并重写create方法

public class ServerRibbonLoadBalancedRetryPolicyFactory extends RibbonLoadBalancedRetryPolicyFactory {
private SpringClientFactory clientFactory;
private ServerRibbonLoadBalancedRetryPolicy policy;
private ServerRibbonLoadBalancedRetryPolicy.RetryTrigger trigger; public ServerRibbonLoadBalancedRetryPolicyFactory(SpringClientFactory clientFactory) {
super(clientFactory);
this.clientFactory = clientFactory;
} @Override
public LoadBalancedRetryPolicy create(String serviceId, ServiceInstanceChooser loadBalanceChooser) {
RibbonLoadBalancerContext lbContext = this.clientFactory
.getLoadBalancerContext(serviceId);
policy = new ServerRibbonLoadBalancedRetryPolicy(serviceId, lbContext, loadBalanceChooser, clientFactory.getClientConfig(serviceId));
policy.setTrigger(trigger);
return policy;
} public void setTrigger(ServerRibbonLoadBalancedRetryPolicy.RetryTrigger trigger) {
policy.setTrigger(trigger);//跟上面是setTrigger不知道谁会先触发,所以两边都设置了。
this.trigger = trigger;
}
}

把LoadBalancedRetryPolicyFactory托管到spring

    @Bean
@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
public LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory(SpringClientFactory clientFactory) {
return new ServerRibbonLoadBalancedRetryPolicyFactory(clientFactory);
}

然后我们就可以在我们rule类上面实现RetryTrigger方法。

public class ServerLoadBalancerRule extends AbstractLoadBalancerRule implements ServerRibbonLoadBalancedRetryPolicy.RetryTrigger {

    private static final Logger LOGGER = LoggerFactory.getLogger(ServerLoadBalancerRule.class);
/**
* 不可用的服务器
*/
private Map<String, List<String>> unreachableServer = new HashMap<>(256);
/**
* 上一次请求标记
*/
private String lastRequest; @Autowired
LoadBalancedRetryPolicyFactory policyFactory; @Override
public Server choose(Object key) {
//初始化重试触发器
retryTrigger();
return getServer(getLoadBalancer(), key);
} private Server getServer(ILoadBalancer loadBalancer, Object key) {
    //从数据库获取服务列表
    List<ServerAddress> addressList = getServerAddress();
    setMaxAutoRetriesNextServer(addressList.size()); //过滤不可用服务
} private void retryTrigger() {
RequestContext ctx = RequestContext.getCurrentContext();
String batchNo = (String) ctx.get(Constant.REQUEST_BATCH_NO);
if (!isLastRequest(batchNo)) {
//不是同一次请求,清理所有缓存的不可用服务
unreachableServer.clear();
} if (policyFactory instanceof ServerRibbonLoadBalancedRetryPolicyFactory) {
((ServerRibbonLoadBalancedRetryPolicyFactory) policyFactory).setTrigger(this);
}
} private boolean isLastRequest(String batchNo) {
return batchNo != null && batchNo.equals(lastRequest);
} @Override
public void exec(LoadBalancedRetryContext context) {
RequestContext ctx = RequestContext.getCurrentContext();
     //UUID,故障重试不会发生变化。客户每次请求时会产生新的batchNo,可以在preFilter中生成。 
String batchNo = (String) ctx.get(Constant.REQUEST_BATCH_NO);
lastRequest = batchNo; List<String> hostAndPorts = unreachableServer.get((String) ctx.get(Constant.REQUEST_BATCH_NO));
if (hostAndPorts == null) {
hostAndPorts = new ArrayList<>();
}
if (context != null && context.getServiceInstance() != null) {
String host = context.getServiceInstance().getHost();
int port = context.getServiceInstance().getPort();
if (!hostAndPorts.contains(host + Constant.COLON + port))
hostAndPorts.add(host + Constant.COLON + port);
unreachableServer.put((String) ctx.get(Constant.REQUEST_BATCH_NO), hostAndPorts);
}
}
}

这样,我们就拿到了不可用的服务了,然后在重试的时候过滤掉unreachableServer中的服务就可以了。

这里有一点要注意的是,MaxAutoRetriesNextServer的值必须是没有过滤的服务列表的大小。

当然,有人会有疑问,如果服务器数量过多,重试时间超过ReadTimeout怎么办?我这里也没关于超时的设置,因为本身让客户等待过久就不是很合理的需求

所以配置文件里面设置一个合理的ReadTimeout就好了,在这个时间段里面如果重试没取到可用的服务就直接抛超时的信息给客户。

源码地址: https://github.com/rxiu/study-on-road/tree/master/trickle-gateway

springboot+zuul(二)------智能负载的更多相关文章

  1. springboot+zuul(一)------实现自定义过滤器、动态路由、动态负载。

    参考:https://blog.csdn.net/u014091123/article/details/75433656 https://blog.csdn.net/u013815546/articl ...

  2. 003.HAProxy ACL规则的智能负载均衡

    一 简介 HAProxy可以工作在第七层模型,可通过ACL规则实现基于HAProxy的智能负载均衡系统,HAProxy通过ACL规则完成以下两种主要功能: 通过ACL规则检查客户端请求是否合法,如果符 ...

  3. spring cloud: zuul(二): zuul的serviceId/service-id配置(微网关)

    spring cloud: zuul(二): zuul的serviceId/service-id配置(微网关) zuul: routes: #路由配置表示 myroute1: #路由名一 path: ...

  4. 《Spring Cloud》学习(二) 负载均衡!

    第二章 负载均衡 负载均衡是对系统的高可用.网络压力的缓解和处理能力扩容的重要手段之一.Spring Cloud Ribbon是一个基于 HTTP 和 TCP 的客户端负载均衡工具,它基于Netfli ...

  5. haproxy 配置文件详解 之 ACL 智能负载均衡

    由于HAProxy 可以工作在七层模型下, 因此,要实现 HAProxy 的强大功能,一定要使用强大灵活的ACL 规则,通过ACL 规则可以实现基于HAProxy 的智能负载均衡系统. HAProxy ...

  6. Spring Cloud负载均衡:使用zuul作服务器端负载均衡

    1.目的: 本文简述Spring Cloud负载均衡之服务器负载均衡模式,使用组件为zuul. zuul作为Spring Cloud中的网关组件,负责路由转发.身份验证.请求过滤等等功能,那么我们可以 ...

  7. SpringBoot开发二十-Redis入门以及Spring整合Redis

    安装 Redis,熟悉 Redis 的命令以及整合Redis,在Spring 中使用Redis. 代码实现 Redis 内置了 16 个库,索引是 0-15 ,默认选择第 0 个 Redis 的常用命 ...

  8. HAProxy(二):HAProxy的ACL规则实现智能负载均衡详解与示例

    一.HAProxy的ACL的功能 ACL(Access Control List)访问控制列表,HAProxy中的ACL的匹配条件和控制条件有许多种,功能很强大,可以通过源地址.源端口.目标地址.目标 ...

  9. SpringBoot(三) - Ribbon客户端负载均衡,Zuul网关,Config配置中心

    1.Ribbon客户端负载均衡 1.1 依赖 1.2 配置信息 # feign默认加载了ribbon负载均衡,默认负载均衡机制是:轮询 # 负载均衡机制是添加在消费端(客户端)的,如果改为随机,指定服 ...

随机推荐

  1. Oracle EBS FND User Info API

    1. 与用户信息相关API PKG. --和用户处理有关的API FND_USER_PKG;     --和用户密码处理有关的API FND_WEB_SEC;     --和用户职责处理有关的API ...

  2. Android-解析JSON数据(JSON对象/JSON数组)

    在上一篇博客中,Android-封装JSON数据(JSON对象/JSON数组),讲解到Android真实开发中更多的是去解析JSON数据(JSON对象/JSON数组) 封装JSON的数据是在服务器端进 ...

  3. 【转】不用软件,解压Win8/Win8.1的install.wim文件

    今天用好压解压Windows 8.1的install.wim文件,居然提示文件损坏,换了7Z仍然如此:其实文件是好的.只不过这些软件暂时不支持罢了,还好可以用dism命令来手动完成. 一.检查镜像版本 ...

  4. Centos 安装 erlang 环境

    系统 Centos 6.5 64位 Erlang 18.3.4 安装依赖组件 yum install -y gcc gcc-g++ unixODBC unixODBC-devel wxBase wxG ...

  5. .net List方法属性及作用

    List的方法和属性 方法或属性 作用 Capacity 用于获取或设置List可容纳元素的数量.当数量超过容量时,这个值会自动增长.您可以设置这个值以减少容量,也可以调用trin()方法来减少容量以 ...

  6. 基于ASP.NET几十万数据几秒钟就可以导入到数据库中

    /// <summary> /// 一.构建模拟数据存放于DataTable /// </summary> /// <returns>DataTable</r ...

  7. iOS错误 - too many open files (error = 24)

    碰到这个错误是在用 UIImageView 显示图片的时候.UIImage 用的是 imageNamed 方法.错误原因是打开了太多的文件.应该是太多文件的打开导致了 UIImage 的 cache ...

  8. 在myeclipse中有的项目上有个红色感叹号

    之前做项目的时候遇到过这个问题,最后确定原因是项目引用了很多放在D盘或E盘上的jar包,但是我们不小心把这些jar包删除或移动路径了,因此myeclipse识别不了出现红色的感叹号,解决方式是在mye ...

  9. HTML Strip Char Filter

    The html_strip character filter strips HTML elements from the text and replaces HTML entities with t ...

  10. java使用Redis5--分布式存储

    Redis实现分布式存储的方法主要是采用一致性哈稀分片(Shard),将不同的key分配到不同的redis server上,达到横向扩展的目的. package redis; import java. ...