Spring Cloud 2020.0.3 断路器 Hystrix 2.2.9.RELEASE 实践

Spring Cloud 支持多种断路器的实现,比较常见的是Netflix Hystrix,但目前最新版的spring-cloud-2020.0.3已经移除了netflix相关组件。

在微服务之间的调用链中,如果没有适当地保护,当某一个服务环节出现故障则可能会导致整体服务雪崩不可用。如底层服务因为数据库慢查询,导致接口请求耗时过长,进而导致服务线程打满等待线程数暴增出现大量超时无法对外提供服务。

本文使用spring-cloud-2020.0.3及spring-boot-2.5.4演示hystrix的使用及相关参数的配置。

1、模拟客户端(client端)
  • pom.xml关键依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    <version>3.0.3</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>3.0.3</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    <version>2.2.9.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
    <version>2.2.9.RELEASE</version>
</dependency>

需要注意spring-boot新版本引入的是openfeign,旧版本的是netflix-feign。

  • 编写客户端调用类UserService及熔断回调类UserServiceFallback
UserService.java如下
package com.mixfate.hystrix.client;

import com.mixfate.hystrix.client.fallback.UserServiceFallback;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "hystrix-server", path = "/user", fallback = UserServiceFallback.class)
public interface UserService {

    @GetMapping("/{id}")
    String findById(@PathVariable("id") String id);

}

UserServiceFallback.java如下
package com.mixfate.hystrix.client.fallback;

import com.mixfate.hystrix.client.UserService;
import org.springframework.stereotype.Component;

@Component
public class UserServiceFallback implements UserService {
    @Override
    public String findById(String id) {
        return "default value";
    }
}

FeignClient指定了服务名为hystrix-server,根路径为/user,若调用失败则使用UserServiceFallback回调类处理。

  • 编写Application启动类
package com.mixfate.hystrix;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableEurekaClient
@EnableFeignClients
@EnableHystrix
@SpringBootApplication
@EnableHystrixDashboard
@EnableScheduling
public class HystrixDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(HystrixDemoApplication.class, args);
    }

}

注意其中引入@EnableHystrix即可,@EnableCircuitBreaker已提示过时了,此组件已默认从spring-cloud的组件中移除了,因为netflix已经不在维护相应的项目了。

  • 编写一个测试类并编写好配置文件
application.yml如下
server:
  port: 8080

spring:
  application:
    name: hystrix-client
  cloud:
    circuitbreaker:
      hystrix:
        enabled: true

eureka:
  client:
    service-url:
      defaultZone: http://root:123456@localhost:8761/eureka/
    fetch-registry: true
    register-with-eureka: false

feign:
  circuitbreaker:
    enabled: true
  client:
    config:
      default:
        connectTimeout: 2000
        readTimeout: 2000

logging:
  level:
    root: info

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 2000
  dashboard:
    proxy-stream-allow-list: "localhost"

management:
  server:
    port: 8082
  endpoints:
    web:
      exposure:
        include: "*"
        
HystrixDemoTest.java如下
package com.mixfate.hystrix;

import com.mixfate.hystrix.client.UserService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@Slf4j
@SpringBootTest
public class HystrixDemoTest {

    @Autowired
    private UserService userService;

    @Test
    public void test() {
        log.info("begin");
        String result = userService.findById("test");
        log.info(result);
        log.info(userService.findById("test"));
        log.info(userService.findById("test"));
    }

}

因开启了circuitbreaker,所以即便服务端没有开启、没有注册中心运行此test也是成功的,熔断调用了Fallback回调返回结果。

先调整日志级别为debug,简单跟踪一下调用的情况,可以看到以下两条请求日志,说明无法获取到hystrix-server,启动eureka注册中心后若未有服务注册到eureka,同样会通过Fallback返回默认调用结果。

另外需要注意参数spring.cloud.circuitbreaker.hystrix.enabled默认为开启的状态,所以不需要单独配置即可。

Load balancer does not contain an instance for the service hystrix-server
Error executing HystrixCommand.run(). Proceeding to fallback logic ...
2、模拟 一个简单服务端(server端)

多数情况下,微服务既是服务端又是客户端,服务端定义一个简单接口,并将服务注册到eureka

  • pom.xml关键依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    <version>3.0.3</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
  • Controller入口类、启动类及配置文件
UserHystrixController.java如下
package com.mixfate.hystrix.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/user")
@RestController
public class UserHystrixController {

    @GetMapping("/{id}")
    public String findById(@PathVariable("id") String id) throws InterruptedException {
        String result = String.format("this is mixfate [%s] [%s]", id, System.currentTimeMillis());
        Thread.sleep(500);
        return result;
    }

}

HystrixServerApplication.java如下
package com.mixfate.hystrix;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;

@SpringBootApplication
public class HystrixServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(HystrixServerApplication.class, args);
    }

}

application.yaml如下
spring:
  application:
    name: hystrix-server

eureka:
  client:
    fetch-registry: false
    register-with-eureka: true
    service-url:
      defaultZone: http://root:123456@localhost:8761/eureka/
server:
  port: 8081

服务端启动后再使用客户端调用则可看到正常的调用结果,fetch-registry表示是否从eureka注册中心获取服务列表,此处不需要设置为falseregister-with-eureka是否将服务注册到eureka,这个是服务端所以需要注册到eureka注册中心中供别的服务调用。

3、抛出几个问题
  • 什么情况下会发生熔断呢?
场景 说明
情况一 没有连接上eureka注册中心
情况二 能连接上注册中心但获取不到服务(未注册)注册中心提示(No servers available for service: hystrix-server)
情况三 能从注册中心获取到服务,但调用目标服务超时
情况四 调用的服务端抛了异常

针对情况三,在服务端中的接口中增加Thread.sleep(2000),让程序休眠2秒模拟慢请求,观察情况会发现三个请求都到了fallback,因为熔断请求的超时时间为1秒,即参数hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds,打开源码查看类HystrixCommandProperties,可以看到有以下关键代码,即为调用超时的默认设置;但同时还需要注意openfeign的连接超时与读超时的设置feign.client.config.default.connectTimeoutfeign.client.config.default.readTimeout,结合超时参数观察客户端的调用情况。

Integer default_executionTimeoutInMilliseconds = 1000;
...省略
protected HystrixCommandProperties(HystrixCommandKey key) {
        this(key, new Setter(), "hystrix");
    }
...省略
this.executionTimeoutInMilliseconds = getProperty(propertyPrefix, key, "execution.isolation.thread.timeoutInMilliseconds", builder.getExecutionIsolationThreadTimeoutInMilliseconds(), default_executionTimeoutInMilliseconds);
...省略
private static HystrixProperty<Integer> getProperty(String propertyPrefix, HystrixCommandKey key, String instanceProperty, Integer builderOverrideValue, Integer defaultValue) {
        return forInteger()
                .add(propertyPrefix + ".command." + key.name() + "." + instanceProperty, builderOverrideValue)
                .add(propertyPrefix + ".command.default." + instanceProperty, defaultValue)
                .build();
    }

确定了此参数后,调整为3秒,再次尝试,即可调通,不过还需要feign客户的的连接超时及读超时时间的配置,如果比熔断请求超时要小的话,则依然会返回fallbackdefault值,可查看FeignClientPropertiesFeignClientConfiguration配置类;

feign:
  client:
    config:
      hystrix-server:
        connectTimeout: 5000
        readTimeout: 5000

注意看FeignClientProperties配置类中的configMap类型,上面配置hystrix-server是指单独为此服务配置超时,需要配置全局超时改为default即可。

  • 熔断后是如何恢复的?

先看配置属性类HystrixCommandProperties的几个参数,注意hystrix的版本为2.2.9.RELEASE,低版本略有差异。

参数 说明
default_metricsRollingStatisticalWindow = 10000 滑动窗口时间,默认10秒
default_circuitBreakerRequestVolumeThreshold = 20 熔断打开阈值,表示在10秒内超过20次请求才会打开
circuitBreakerErrorThresholdPercentage = 50 错误率达50%熔断
circuitBreakerSleepWindowInMilliseconds = 5000 熔断后间隔多长时间尝试恢复

所以可以简单写个定时器测试一下hystrix打开、关闭的效果

@Scheduled(cron = "* * * * * ?")
public void schedule(){
    for(int i=0;i<2;i++) {
        System.out.println(userService.findById("test"));
    }
}

定时器每一秒触发两次请求,10秒内可以有20次请求,符合参数circuitBreakerRequestVolumeThreshold默认值20的条件,接着就可以通过停掉服务或启动服务测试熔断的功能了,hystrix dashboard中可观察打开关闭的状态http://localhost:8080/hystrix

示例源码gitee https://gitee.com/viturefree/hystrix


赞赏(Donation)
微信(Wechat Pay)

donation-wechatpay