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 # 该参数用来开启或关闭重试机制,默认是开启
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐