springboot+zuul(二)------智能负载
一、参考
参考资料: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(二)------智能负载的更多相关文章
- springboot+zuul(一)------实现自定义过滤器、动态路由、动态负载。
参考:https://blog.csdn.net/u014091123/article/details/75433656 https://blog.csdn.net/u013815546/articl ...
- 003.HAProxy ACL规则的智能负载均衡
一 简介 HAProxy可以工作在第七层模型,可通过ACL规则实现基于HAProxy的智能负载均衡系统,HAProxy通过ACL规则完成以下两种主要功能: 通过ACL规则检查客户端请求是否合法,如果符 ...
- spring cloud: zuul(二): zuul的serviceId/service-id配置(微网关)
spring cloud: zuul(二): zuul的serviceId/service-id配置(微网关) zuul: routes: #路由配置表示 myroute1: #路由名一 path: ...
- 《Spring Cloud》学习(二) 负载均衡!
第二章 负载均衡 负载均衡是对系统的高可用.网络压力的缓解和处理能力扩容的重要手段之一.Spring Cloud Ribbon是一个基于 HTTP 和 TCP 的客户端负载均衡工具,它基于Netfli ...
- haproxy 配置文件详解 之 ACL 智能负载均衡
由于HAProxy 可以工作在七层模型下, 因此,要实现 HAProxy 的强大功能,一定要使用强大灵活的ACL 规则,通过ACL 规则可以实现基于HAProxy 的智能负载均衡系统. HAProxy ...
- Spring Cloud负载均衡:使用zuul作服务器端负载均衡
1.目的: 本文简述Spring Cloud负载均衡之服务器负载均衡模式,使用组件为zuul. zuul作为Spring Cloud中的网关组件,负责路由转发.身份验证.请求过滤等等功能,那么我们可以 ...
- SpringBoot开发二十-Redis入门以及Spring整合Redis
安装 Redis,熟悉 Redis 的命令以及整合Redis,在Spring 中使用Redis. 代码实现 Redis 内置了 16 个库,索引是 0-15 ,默认选择第 0 个 Redis 的常用命 ...
- HAProxy(二):HAProxy的ACL规则实现智能负载均衡详解与示例
一.HAProxy的ACL的功能 ACL(Access Control List)访问控制列表,HAProxy中的ACL的匹配条件和控制条件有许多种,功能很强大,可以通过源地址.源端口.目标地址.目标 ...
- SpringBoot(三) - Ribbon客户端负载均衡,Zuul网关,Config配置中心
1.Ribbon客户端负载均衡 1.1 依赖 1.2 配置信息 # feign默认加载了ribbon负载均衡,默认负载均衡机制是:轮询 # 负载均衡机制是添加在消费端(客户端)的,如果改为随机,指定服 ...
随机推荐
- [leetcode] 9. Binary Tree Level Order Traversal
跟第七题一样,把最后的输出顺序换一下就行... Given a binary tree, return the level order traversal of its nodes' values. ...
- Java Web系列:Java Web 项目基础
1.Java Web 模块结构 JSP文件和AXPX文件类似,路径和URL一一对应,都会被动态编译为单独class.Java Web和ASP.NET的核心是分别是Servlet和IHttpHandle ...
- Spring学习(五)——集成MyBatis
本篇我们将在上一篇http://www.cnblogs.com/wenjingu/p/3829209.html的Demo程序的基础上将 MyBatis 代码无缝地整合到 Spring 中. 数据库仍然 ...
- pipeline构建时报错问题解决
问题: 1.No such field found: field java.lang.String sh. Administrators can decide whether to approve o ...
- VC6.0快捷键一览表
F1 显示帮助,如果光标停在代码的某个字符上,显示MSDN中相应的帮助内容 F2 书签功能: Ctrl+F2 –在某行设置一个书签(再按一次次是取消) F2 –跳到下一个书签位置 Shift+F2 – ...
- SQL Server—— 如何创建定时作业
在做SQL server 管理时,往往需要每日执行定时任务,但是如果每天都去人工执行,非常不方便,而且一般定时操作,都应该是在数据库压力不大时,一般是在夜间.所以我们需要创建定时作业来代替人工的执行定 ...
- 【QT】二进制读取图像文件并显示
打开对话框选择文件 二进制方式读取文件 转换成图像显示 void MainWindow::showImage() { //打开文件对话框 QString lastPath="D:/Engli ...
- python中的 小数据池 is 和 ==
1. 小数据池 一种数据缓存机制,也被称为驻留机制 小数据池针对的是:整数 , 字符 , 布尔值 .其他的数据类型不存在驻留机制 在python中对 -5 到256之间的整数会被驻留在内存中, 将一定 ...
- 446. Arithmetic Slices II - Subsequence
A sequence of numbers is called arithmetic if it consists of at least three elements and if the diff ...
- FastDFS 安装与使用
FastDFS 安装与使用 1. 什么是 FastDFS FastDFS是一个开源的高性能分布式文件系统(DFS). 它的主要功能包括:文件存储,文件同步和文件访问,以及高容量和负载平衡的设计. Fa ...