一、参考

参考资料: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 FND API–Create User

    --API - fnd_user_pkg.createuser----Example -- -- ---------------------------------------- API to CRE ...

  2. 在github创建用户

    在Github注册账户 第一个是创建用户名,第二个是填写邮箱,第三个是设置密码 进入给github会让你选择账户类型 第二步完成后会让你完成邮箱的验证 验证完邮箱以后此时就验证成功了点击绿色的 let ...

  3. task4: 结对编程-词频统计[修改版]

    问题描述: 读取一个文件,统计其中单词出现次数,并按从高到低的顺序显示,相同顺序的字典序排列. 思路: 基于上次的程序用正则提取出文本里的单词,然后利用字典计数(先get,为null则置1,不为nul ...

  4. postgresql 修改配置生效方法

    对于配置服务器,,太多时候我们在Linux中做的操作是,配置*.conf文件,然后重启服务.而很多服务都具有reload功能,而但是具体到某个配置,有时候直接说出需不需要重启服务而使得配置生效,这并不 ...

  5. ASP.NET 邮件发送

    ASP.NET 邮件发送用NET的MAIL类即可实现. 邮件发时,为不影响进程,所以采用多线程实现比较好. /// <summary> /// 多线程邮件发送 多线程需注意不要引用到外部方 ...

  6. pageadmin CMS网站建设教程:站点添加自定义字段

    首先看看pagedmin默认的站点设置都有什么,如下图: 这里只有一些最基本的参数设置,用过3.0版本或用过其他公司开发的cms的用户应该有这种体验,在站点设置中可以设置logo图片,备案号,底部内容 ...

  7. pageadmin网站制作 怎么验证sql用户名和密码的正确性

    使用pageadmin建站系统的时候,不懂可以参考官网教程. 1.打开SQL Server Management Studio会弹出如下界面. 第一个箭头指向的就是服务器名称,如果用ip无法连接sql ...

  8. 自注意力机制(Self-attention Mechanism)——自然语言处理(NLP)

    近年来,注意力(Attention)机制被广泛应用到基于深度学习的自然语言处理(NLP)各个任务中.随着注意力机制的深入研究,各式各样的attention被研究者们提出.在2017年6月google机 ...

  9. BZOJ 3813--奇数国(线段树&欧拉函数&乘法逆元&状态压缩)

    3813: 奇数国 Time Limit: 10 Sec  Memory Limit: 256 MBSubmit: 755  Solved: 432[Submit][Status][Discuss] ...

  10. Django 项目拆分配置文件settings.py

    使用Django命令生成一个项目的基本结构时, 配置信息默认保存在和项目目录同名的目录下的settings.py文件里, 对于一个项目而言, 这样往往是不合适的, 在实际的开发中,需要将配置文件拆分为 ...