草庐IT

【Spring Cloud】OpenFeign和Spring Cloud Loadbalancer调用失败后的重试机制比较

小蘑菇啊 2023-06-30 原文

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

有关【Spring Cloud】OpenFeign和Spring Cloud Loadbalancer调用失败后的重试机制比较的更多相关文章

  1. ruby - Ruby 的 Hash 在比较键时使用哪种相等性测试? - 2

    我有一个围绕一些对象的包装类,我想将这些对象用作散列中的键。包装对象和解包装对象应映射到相同的键。一个简单的例子是这样的:classAattr_reader:xdefinitialize(inner)@inner=innerenddefx;@inner.x;enddef==(other)@inner.x==other.xendenda=A.new(o)#oisjustanyobjectthatallowso.xb=A.new(o)h={a=>5}ph[a]#5ph[b]#nil,shouldbe5ph[o]#nil,shouldbe5我试过==、===、eq?并散列所有无济于事。

  2. ruby - 即使失败也继续进行多主机测试 - 2

    我已经构建了一些serverspec代码来在多个主机上运行一组测试。问题是当任何测试失败时,测试会在当前主机停止。即使测试失败,我也希望它继续在所有主机上运行。Rakefile:namespace:specdotask:all=>hosts.map{|h|'spec:'+h.split('.')[0]}hosts.eachdo|host|begindesc"Runserverspecto#{host}"RSpec::Core::RakeTask.new(host)do|t|ENV['TARGET_HOST']=hostt.pattern="spec/cfengine3/*_spec.r

  3. 使用 ACL 调用 upload_file 时出现 Ruby S3 "Access Denied"错误 - 2

    我正在尝试编写一个将文件上传到AWS并公开该文件的Ruby脚本。我做了以下事情:s3=Aws::S3::Resource.new(credentials:Aws::Credentials.new(KEY,SECRET),region:'us-west-2')obj=s3.bucket('stg-db').object('key')obj.upload_file(filename)这似乎工作正常,除了该文件不是公开可用的,而且我无法获得它的公共(public)URL。但是当我登录到S3时,我可以正常查看我的文件。为了使其公开可用,我将最后一行更改为obj.upload_file(file

  4. c# - 如何在 ruby​​ 中调用 C# dll? - 2

    如何在ruby​​中调用C#dll? 最佳答案 我能想到几种可能性:为您的DLL编写(或找人编写)一个COM包装器,如果它还没有,则使用Ruby的WIN32OLE库来调用它;看看RubyCLR,其中一位作者是JohnLam,他继续在Microsoft从事IronRuby方面的工作。(估计不会再维护了,可能不支持.Net2.0以上的版本);正如其他地方已经提到的,看看使用IronRuby,如果这是您的技术选择。有一个主题是here.请注意,最后一篇文章实际上来自JohnLam(看起来像是2009年3月),他似乎很自在地断言RubyCL

  5. java - 从 JRuby 调用 Java 类的问题 - 2

    我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www

  6. ruby - 调用其他方法的 TDD 方法的正确方法 - 2

    我需要一些关于TDD概念的帮助。假设我有以下代码defexecute(command)casecommandwhen"c"create_new_characterwhen"i"display_inventoryendenddefcreate_new_character#dostufftocreatenewcharacterenddefdisplay_inventory#dostufftodisplayinventoryend现在我不确定要为什么编写单元测试。如果我为execute方法编写单元测试,那不是几乎涵盖了我对create_new_character和display_invent

  7. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  8. ruby-on-rails - 创建 ruby​​ 数据库时惰性符号绑定(bind)失败 - 2

    我正在尝试在Rails上安装ruby​​,到目前为止一切都已安装,但是当我尝试使用rakedb:create创建数据库时,我收到一个奇怪的错误:dyld:lazysymbolbindingfailed:Symbolnotfound:_mysql_get_client_infoReferencedfrom:/Library/Ruby/Gems/1.8/gems/mysql2-0.3.11/lib/mysql2/mysql2.bundleExpectedin:flatnamespacedyld:Symbolnotfound:_mysql_get_client_infoReferencedf

  9. C51单片机——实现用独立按键控制LED亮灭(调用函数篇) - 2

    说在前面这部分我本来是合为一篇来写的,因为目的是一样的,都是通过独立按键来控制LED闪灭本质上是起到开关的作用,即调用函数和中断函数。但是写一篇太累了,我还是决定分为两篇写,这篇是调用函数篇。在本篇中你主要看到这些东西!!!1.调用函数的方法(主要讲语法和格式)2.独立按键如何控制LED亮灭3.程序中的一些细节(软件消抖等)1.调用函数的方法思路还是比较清晰地,就是通过按下按键来控制LED闪灭,即每按下一次,LED取反一次。重要的是,把按键与LED联系在一起。我打算用K1来作为开关,看了一下开发板原理图,K1连接的是单片机的P31口,当按下K1时,P31是与GND相连的,也就是说,当我按下去时

  10. ruby - 如何找到调用当前方法的方法 - 2

    如何找到调用此方法的位置?defto_xml(options={})binding.pryoptions=options.to_hifoptions&&options.respond_to?(:to_h)serializable_hash(options).to_xml(options)end 最佳答案 键入caller。这将返回当前调用堆栈。文档:Kernel#caller.例子[0]%rspecspec10/16|===================================================62=====

随机推荐