一、参考

参考资料: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. 終于解決调用wordpress 4.3 xmlrpc api 发布包含分类的文章时返回“抱歉,文章类型不支持您的分类法”错误的問題

    這個問題我找了很多資料都沒有明說是如何解決,后來突發奇想得出我的解決方案如下,所以特此記錄一下: object postId = blogService.NewPost(0,"admin&q ...

  2. [Elixir004]通过环境变量(Environment Variables)来管理config

    在elixir的config中我们有时会使用的到一些不想暴露出来的配置项,常用的作法是如Phoenix #config/prod.exs use Mix.Config ... # Finally im ...

  3. django:multivaluedictkeyerror错误

    查了一下,是因为获取前台数据时,用了request.POST[],改用request.POST.get()之后没有这个报错了 细节: request.POST是用来接受从前端表单中传过来的数据,比如用 ...

  4. AGC007C Pushing Balls

    题目链接 题意:\(N\)个坑,\(N+1\)个球,相间分布,距离为以\(d_1\)为首项,\(x\)为公差的等差数列.对于每次操作,随机选择一个未入坑的球,随机选择向左或向右,掉入第一个没有球的坑, ...

  5. java学习笔记DOM4J解析(7)

    DOM4J即Document Object Model for Java使用java技术以文档方式解析XML数据的模型. DOM4J是开源组织提供的一个免费的.强大的XML解析工具,如果开发者需要在项 ...

  6. linux环境搭建前期配置

    一.永久修改主机名 1.修改network文件 # vim /etc/sysconfig/network 加入 HOSTNAME=主机名 保存退出 2.修改hosts文件 # vim /etc/hos ...

  7. 工作中遇到的两个问题-正则以及console

    一.今天做点击按钮验证邮箱时,遇到以下几个问题: (1)点击按钮后,执行if(regExp.test(str)),出现一种奇怪的现象:第一次输入正确邮箱验证通过,第二次输入正确邮箱就返回false,第 ...

  8. 哈工大ComingX-创新工场俱乐部正式成立

    当我把这两个Logo放在一起的时候,我有一种感觉,这种感觉同样存在于ComingX队员的心中.大学我们走到了一起,非你我所预料,却又如此自然.在感恩节的零点,我迫不及待地告诉各位ComingX队员和关 ...

  9. 关于iframe切换的问题

    定位不到元素的另一种问题是有这种iframe,所有我们需要切换到该页面中去 1.首先找到这个iframe的位置,像上图有id属性我们直接  iframe = driver.find_element_b ...

  10. 【JavaScript】动态原型模式创建对象 ||为何不能用字面量创建原型对象?

    var proto = ""; function Person(name, age, job) { this.name = name; this.age = age; this.j ...