【Spring Cloud】OpenFeign和Spring Cloud Loadbalancer调用失败后的重试机制比较
比较OpenFeign和Spring Cloud Loadbalancer在调用失败后的重试机制,发现OpenFeign的重试机制更优。
1 概述
搭建一个微服务系统,有两个服务,Client和Server,Server有三个实例A、B、C,我让Client调用Server,Loadbalancer负载分担默认采用轮询机制,当Server-A/B/C响应都正常时,会轮流负载分担到三个实例上。而当我把其中的两个实例Server-A和Server-B设置为处理超时后,问题出现了。
当使用spring cloud loadbalancer的重试策略时,调用会遇到失败的情况。
当使用feign的重试策略时,调用不会失败。
下面就详细介绍这两种情况。
2 环境配置
我用的是Spring Cloud框架,以下组合:Nacos + OpenFeign + Loadbalancer + Hystrix,Spring Cloud版本号是:2021.0.4,Spring Boot版本号:2.6.11,Nacos版本号:2021.0.1.0,Hystrix版本号:2.2.10.RELEASE。
1、Client 的 pom.yml 文件部分配置如下:
<properties>
<java.version>1.8</java.version>
<spring-boot.version>2.6.11</spring-boot.version>
<spring-cloud.version>2021.0.4</spring-cloud.version>
<com.alibaba.cloud.version>2021.0.1.0</com.alibaba.cloud.version>
<spring-cloud-openfeign.version>3.1.4</spring-cloud-openfeign.version>
<openfeign.feign-httpclient>11.8</openfeign.feign-httpclient>
<spring-cloud-loadbalancer.version>3.1.4</spring-cloud-loadbalancer.version>
<spring-cloud-hystrix.version>2.2.10.RELEASE</spring-cloud-hystrix.version>
</properties>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<!-- 不使用Ribbon进行客户端负载均衡,而使用loadbalancer -->
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
<version>${com.alibaba.cloud.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>${spring-cloud-hystrix.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>${openfeign.feign-httpclient}</version>
</dependency>
<!--> 注意,当使用Spring Cloud Loadbalancer的重试策略时,必须增加对spring-retry的依赖 <-->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.3.3</version>
</dependency>
注意:
在包含nacos时,需要排除ribbon,采用loadbalancer。
同时,当需要使用Spring Cloud Loadbalancer的重试策略时,必须增加对spring-retry的依赖,否则在调用失败后不会重试。
2、Server 的 pom.xml 文件,不需要包含feign、loadbalancer、hystrix,只需要包含nacos。
<properties>
<java.version>1.8</java.version>
<spring-boot.version>2.6.11</spring-boot.version>
<spring-cloud.version>2021.0.4</spring-cloud.version>
<com.alibaba.cloud.version>2021.0.1.0</com.alibaba.cloud.version>
</properties>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<!-- 不使用Ribbon进行客户端负载均衡,而使用loadbalancer -->
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
<version>${com.alibaba.cloud.version}</version>
</dependency>
3 重试策略选择
我们分别选择Loadbalancer和Feign的重试策略,进行试验。
3.1 Loadbalancer重试策略
3.1.1 yml配置
1、Client yml配置:
############### 服务端口号 ###############
server:
port: 60002
spring:
application:
######### 服务名称 #########
name: Client-1
cloud:
inetutils:
# 优先选择这个前缀的IP进行注册
preferred-networks: 172.26.57
######### nacos注册中心 #########
nacos:
discovery:
# nacos注册中心的地址
server-addr: 172.26.57.84:8848
heart-beat-interval: 2000 # 该实例在客户端上报心跳的间隔时间(毫秒)
heart-beat-timeout: 7000 # 该实例在不发送心跳后,从健康到不健康的时间(毫秒)
ip-delete-timeout: 15000 # 该实例在不发送心跳后,被 nacos下掉该实例的时间(毫秒)
######### 负载分担 #########
loadbalancer:
enabled: true
health-check:
refetch-instances: true
refetch-instances-interval: 5s
repeat-health-check: false
retry:
# 该参数用来开启或关闭重试机制,默认是开启
enabled: true
# 对当前实例重试的次数,默认值: 0
max-retries-on-same-service-instance: 0
# 切换实例进行重试的次数,默认值: 1
max-retries-on-next-service-instance: 2
# 对所有的操作请求都进行重试
retry-on-all-operations: true
circuitbreaker:
hystrix:
enabled: true
####################### Feign配置 ##########################
feign:
client:
config:
default:
# 两端建立连接的请求超时时间,默认10000ms
connectTimeout: 2000
# 读取超时时间,默认60000ms,建立连接后从服务端读取到可用资源所用的时间
readTimeout: 3000
# 调用日志打印等级,需要同步将Feign调用类的日志等级设置为Debug才生效
loggerLevel: BASIC
httpclient:
# 为true时表示程序使用Apache的httpclient作为HTTP请求框架。默认值: true
enabled: true
# 默认值: 2000
#connectionTimeout: 2000
circuitbreaker:
enabled: true
####################### Hystrix配置 ##########################
hystrix:
command:
default:
execution:
timeout:
enabled: true
isolation:
thread:
# 熔断超时时间
# Hystrix的超时时间需要大于Ribbon的超时时间,否则 Hystrix命令超时后,该命令直接熔断,重试机制就没有意义了
# hystrix超时 >= (MaxAutoRetries + 1) * (ribbon ConnectTimeout + ribbon ReadTimeout)
timeoutInMilliseconds: 10000
重要配置说明:
spring.cloud.loadbalancer.retry.enabled=true 表示使能Loadbalancer的重试策略
max-retries-on-next-service-instance=2 表示调用第一个实例失败后,切换实例重试2次
feign.client:.config.default.readTimeout=3000 表示Feign调用其它服务如果超过3秒钟未返回,则视为调用超时
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=10000 表示调用其它服务超过10秒未返回则打开熔断开关
2、Server yml配置:
############### 服务端口号 ###############
server:
port: 60005
spring:
application:
######### 服务名称 #########
name: Server-1
main:
######### nacos注册中心 #########
cloud:
inetutils:
# 优先选择这个前缀的IP进行注册
preferred-networks: 172.26.57
nacos:
discovery:
# nacos注册中心的地址
server-addr: 172.26.57.84:8848
heart-beat-interval: 2000 # 该实例在客户端上报心跳的间隔时间(毫秒)
heart-beat-timeout: 7000 # 该实例在不发送心跳后,从健康到不健康的时间(毫秒)
ip-delete-timeout: 15000 # 该实例在不发送心跳后,被 nacos下掉该实例的时间(毫秒)
3.1.2 代码
1、Client代码:
/**
Client1Application启动类
*/
@SpringBootApplication
@EnableDiscoveryClient /** 向注册中心注册 */
@EnableFeignClients /** 使能Feign调用功能 */
@EnableScheduling /** 使能Schedule功能 */
public class Client1Application {
public static void main(String[] args) {
SpringApplication.run(Client1Application.class, args);
}
}
/**
Feign调用类,调用Server服务
*/
@Primary
@FeignClient(name = "Server-1", fallback = HelloRpcHystrix.class)
public interface HelloRPC {
@RequestMapping(value = "/server/hello")
ResponseEntity<String> hello();
}
/**
熔断类,当调用其它服务超出Hystrix配置的超时时间(timeoutInMilliseconds)后,调用该方法进行返回。
*/
@Component
public class HelloRpcHystrix implements HelloRPC {
@Override
public ResponseEntity<String> hello() {
return new ResponseEntity<>("调用失败,短路处理!!!", HttpStatus.REQUEST_TIMEOUT);
}
}
/**
应用类,循环调用Server服务
*/
@Component
@Slf4j
public class Requester {
@Autowired
private HelloRPC helloRPC;
@Scheduled(initialDelay = 2000, fixedDelay = 20000)
public void request() {
log.info("[info] 发出hello请求!");
long time = System.currentTimeMillis();
ResponseEntity<String> responseEntity = helloRPC.hello();
log.info("hello请求结果: {} 耗时: {}ms", responseEntity.getBody(), System.currentTimeMillis() - time);
}
}
2、Server代码:
/**
Server1Application启动类
*/
@SpringBootApplication
@EnableDiscoveryClient
public class Server1Application {
public static void main(String[] args) {
SpringApplication.run(Server1Application.class, args);
}
}
/**
Server提供的接口服务类,Client调用该接口
*/
@Slf4j
@RestController
@CrossOrigin(origins = "*", maxAge = 3600)
@RequestMapping("/server")
public class HelloServer {
@Value("${addr.client.ip}") /** yml配置文件中需要配置该变量,配置的是该实例部署设备的ip */
public String ip;
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public ResponseEntity<String> response() {
log.info("收到请求!");
log.info("回复请求,IP: {}", ip);
return new ResponseEntity<>(String.format("回复IP: %s", ip), HttpStatus.OK);
}
}
3.1.3 结果
将Client部署在ip为172.26.57.7的设备上;
将Server分别部署在ip为172.26.57.9,172.26.57.10,172.26.57.19的三台设备上。启动四个设备上的服务。
1、当Server所有实例均能正常返回时,通过日志可以看到Client的请求采用轮询的机制负载分担到Server的三个实例上。
2.、修改172.26.57.9和172.26.57.10设备上的Server代码,使其在返回请求时,休眠10秒钟再返回。代码修改如下:
@Slf4j
@RestController
@CrossOrigin(origins = "*", maxAge = 3600)
@RequestMapping("/server")
public class HelloServer {
@Value("${addr.client.ip}") /** yml配置文件中需要配置该变量,配置的是该实例部署设备的ip */
public String ip;
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public ResponseEntity<String> response() {
log.info("收到请求!");
/** 休眠10秒钟,再返回请求结果,此时时间超过了Client设置的feign的readTimeout时间(3秒) */
sleep(10000);
log.info("回复请求,IP: {}", ip);
return new ResponseEntity<>(String.format("回复IP: %s", ip), HttpStatus.OK);
}
private void sleep(int mills) {
try {
Thread.sleep(mills);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
}
结果如下所示:
结论:
上图可看出,我们配置的超时时间为3秒,熔断后返回的时间是9秒,说明在熔断之前,执行了3次调用,这也跟配置保持了一致,配置是切换实例调用2次,加上首次调用,一共就是3次。
Server一共只有三个实例,其中两个实例会超时10秒再返回,还有一个实例是好的,由此我们可以推断出,切换实例后,再调用2次,并不是调用剩下未调用的实例。
通过查看其它两个实例的日志,发现切换实例后,第一次调用的是B(超时10返回),第二次又调用回了A(超时10秒返回),A是首次调用的实例。
所以,三次调用的顺序是:A->B->A,并没有调到正常返回的实例C。
3.2 Feign重试策略
3.1.1 yml配置
1、Client yml配置:
主要修改点为,将spring.cloud.loadbalancer.retry.enabled设置为false,同时删除retry下的其它配置。
其它配置保持不变
spring:
cloud:
loadbalancer:
enabled: true
retry:
# 该参数用来开启或关闭重试机制,默认是开启
enabled: false
# # 对当前实例重试的次数,默认值: 0
# max-retries-on-same-service-instance: 0
# # 切换实例进行重试的次数,默认值: 1
# max-retries-on-next-service-instance: 2
# # 对所有的操作请求都进行重试
# retry-on-all-operations: true
2、Server yml配置:
保持不变
3.1.2 代码
1、Client代码:
增加FeignConfig类,配置Feign的重试策略,重试次数为3次(包括首次调用),如下代码所示。
@Configuration
public class FeignConfig {
/**
* 请求失败后的重试配置
* */
@Bean
public Retryer feignRetryer() {
/** 重试间隔100ms,最大重试间隔时间为1秒,重试次数为3次 */
return new Retryer.Default(100, TimeUnit.SECONDS.toMillis(1L), 3);
}
}
2、Server代码:
保持不变
3.2.3 结果
将Client部署在ip为172.26.57.7的设备上;
将Server分别部署在ip为172.26.57.9,172.26.57.10,172.26.57.19的三台设备上。启动四个设备上的服务。
1、当Server所有实例均能正常返回时,通过日志可以看到Client的请求采用轮询的机制负载分担到Server的三个实例上。
2.、修改172.26.57.9和172.26.57.10设备上的Server代码,使其在返回请求时,休眠10秒钟再返回。
结果如下图所示:
经过重试两次后(即一共调用了三次),调用到了正常返回的实例C。
说明Feign的重试策略与Loadbalancer不一样,它在重试时会排除之前调用失败的实例。
4 结论
Feign的调用失败重试策略优于Spring Cloud Loadbalancer的重试策略,尽量采用Feign的重试策略。在配置时显式关闭Loadbalancer重试策略,如下所示:
spring:
cloud:
loadbalancer:
retry:
enabled: false # 该参数用来开启或关闭重试机制,默认是开启
更多推荐
所有评论(0)