SpringCloud(五)SpringCloud的限流、降级和熔断——Hystrix

SpringCloud(五)SpringCloud的限流、熔断和降级——Hystrix

在分布式系统中,远程系统或服务不可避免的调用失败(超时或者异常)。假设客户端依赖多个服务,在一次请求中,某一个服务出现异常,则整个请求会处理失败;当某一服务等待时间过长,则所有的请求都会阻塞在这个服务的请求上。这样因为一个服务就导致了整个系统的可用性。Netflix的组件Hystrix可以将这些请求隔离,针对服务限流,当服务不可用时能够熔断并降级,防止级联故障。

认识Hystrix

在理想状态下,一个应用依赖的服务都是健康可用的,我们可以正常的处理所有的请求。
服务健康可用
当某一个服务出现延迟时,所有的请求都阻塞在依赖的服务Dependency I
当某一服务出现异常时
所有的用户请求阻塞
当我们使用了Hystrix时,Hystrix将所有的外部调用都封装成一个HystrixCommand或者HystrixObservableCommand对象,这些外部调用将会在一个独立的线程中运行。我们可以将出现问题的服务通过熔断、降级等手段隔离开来,这样不影响整个系统的主业务。
HystrixCommand封装外部调用

新建依赖的服务

为了演示一个异常的服务,我们使用了一个超时接口和一个异常的接口。

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

    private static Logger LOGGER = LoggerFactory.getLogger(UserController.class);

    @RequestMapping("/timeout")
    public String timeout() throws InterruptedException {
        LOGGER.info("invoking timeout endpoint");
        Thread.sleep(10000L);
        return "success";
    }

    @RequestMapping("/exception")
    public String exception() {
        LOGGER.info("invoking exception endpoint");
        if (System.currentTimeMillis() % 2 == 0) {
            throw new RuntimeException("random exception");
        }
        return "success";
    }
}

这个服务的端口为8002,服务名称为user-service

客户端引入依赖

在服务调用方的pom文件中引入hystrix的依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>

客户端配置Hystrix

Hystrix的配置都在客户端web-app-service进行。

配置文件和API配置Hystrix

作为Netflix的组件之一,Hystrix也使用了Archaius的默认配置文件。在classppath下我们新建config.properties文件(archaius-core包中com.netflix.config.WebApplicationProperties#initApplicationProperties默认加载文件)。

# Hystrix 默认加载的配置文件 - 限流、 熔断示例

# 线程池大小
hystrix.threadpool.default.coreSize=1
# 缓冲区大小, 如果为-1,则不缓冲,直接进行降级 fallback
hystrix.threadpool.default.maxQueueSize=200
# 缓冲区大小超限的阈值,超限就直接降级
hystrix.threadpool.default.queueSizeRejectionThreshold=2

# 执行策略
# 资源隔离模式,默认thread。 还有一种叫信号量
hystrix.command.default.execution.isolation.strategy=THREAD
# 是否打开超时
hystrix.command.default.execution.timeout.enabled=true
# 超时时间,默认1000毫秒
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=15000
# 超时时中断线程
hystrix.command.default.execution.isolation.thread.interruptOnTimeout=true
# 取消时候中断线程
hystrix.command.default.execution.isolation.thread.interruptOnFutureCancel=false
# 信号量模式下,最大并发量
hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests=2

# 降级策略
# 是否开启服务降级
hystrix.command.default.fallback.enabled=true
# fallback执行并发量
hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests=100

# 熔断策略
# 启用/禁用熔断机制
hystrix.command.default.circuitBreaker.enabled=true
# 强制开启熔断
hystrix.command.default.circuitBreaker.forceOpen=false
# 强制关闭熔断
hystrix.command.default.circuitBreaker.forceClosed=false
# 前提条件,一定时间内发起一定数量的请求。  也就是5秒钟内(这个5秒对应下面的滚动窗口长度)至少请求4次,熔断器才发挥起作用。  默认20
hystrix.command.default.circuitBreaker.requestVolumeThreshold=4
# 错误百分比。达到或超过这个百分比,熔断器打开。  比如:5秒内有4个请求,2个请求超时或者失败,就会自动开启熔断
hystrix.command.default.circuitBreaker.errorThresholdPercentage=50
# 10秒后,进入半打开状态(熔断开启,间隔一段时间后,会让一部分的命令去请求服务提供者,如果结果依旧是失败,则又会进入熔断状态,如果成功,就关闭熔断)。 默认5秒
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=10000


# 度量策略
# 5秒为一次统计周期,术语描述:滚动窗口的长度为5秒
hystrix.command.default.metrics.rollingStats.timeInMilliseconds=5000
# 统计周期内 度量桶的数量,必须被timeInMilliseconds整除。作用:
hystrix.command.default.metrics.rollingStats.numBuckets=10
# 是否收集执行时间,并计算各个时间段的百分比
hystrix.command.default.metrics.rollingPercentile.enabled=true
# 设置执行时间统计周期为多久,用来计算百分比
hystrix.command.default.metrics.rollingPercentile.timeInMilliseconds=60000
# 执行时间统计周期内,度量桶的数量
hystrix.command.default.metrics.rollingPercentile.numBuckets=6
# 执行时间统计周期内,每个度量桶最多统计多少条记录。设置为50,有100次请求,则只会统计最近的10次
hystrix.command.default.metrics.rollingPercentile.bucketSize=100
# 数据取样时间间隔
hystrix.command.default.metrics.healthSnapshot.intervalInMilliseconds=500

# 设置是否缓存请求,request-scope内缓存
hystrix.command.default.requestCache.enabled=false
# 设置HystrixCommand执行和事件是否打印到HystrixRequestLog中
hystrix.command.default.requestLog.enabled=false



# 限流策略

#如果没有定义HystrixThreadPoolKey,HystrixThreadPoolKey会默认定义为HystrixCommandGroupKey的值
hystrix.threadpool.userGroup.coreSize=1
hystrix.threadpool.userGroup.maxQueueSize=-1
hystrix.threadpool.userGroup.queueSizeRejectionThreshold=800


hystrix.threadpool.userThreadPool.coreSize=1
hystrix.threadpool.userThreadPool.maxQueueSize=-1
hystrix.threadpool.userThreadPool.queueSizeRejectionThreshold=800
hystrix.command.userCommandKey.execution.isolation.thread.timeoutInMilliseconds=5000

创建两个HystrixCommand,分别用于请求/user/timeout接口和/user/exception接口。

public class UserTimeOutCommand extends HystrixCommand<String> {

    public static final Logger LOGGER = LoggerFactory.getLogger(UserTimeOutCommand.class);

    public UserTimeOutCommand() {
        super(Setter
                // 分组名称用于统计
                .withGroupKey(HystrixCommandGroupKey.Factory.asKey("userGroup"))
                // 用于监控、熔断、度量发布、缓存的Key值
                .andCommandKey(HystrixCommandKey.Factory.asKey("userCommandKey"))
                // 线程池命名,默认是HystrixCommandGroupKey的名称。
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("userThreadPool"))
                // command 熔断相关参数配置
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                        // 隔离方式:线程池和信号量。默认使用线程池
                        // .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
                        // 超时时间500毫秒
                        // .withExecutionTimeoutInMilliseconds(500)
                        // 信号量隔离的模式下,最大的请求数。和线程池大小的意义一样
                        // .withExecutionIsolationSemaphoreMaxConcurrentRequests(2)
                        // 熔断时间(熔断开启后,各5秒后进入半开启状态,试探是否恢复正常)
                        // .withCircuitBreakerSleepWindowInMilliseconds(5000)
                )
                // 设置线程池参数
                .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
                        // 线程池大小
                        .withCoreSize(1)));
    }

    @Override
    protected String run() throws Exception {
        LOGGER.info("start query timeout endpoint");
        URL url = new URL("http://localhost:8002/user/timeout");
        byte[] result = new byte[1024];
        url.openStream().read(result);
        return new String(result);
    }

    @Override
    protected String getFallback() {
        // 执行超时、出错或者开启熔断之后,使用这个方法返回
        // 这种策略称为服务降级
        // 这里可以是一个固定返回值,查询缓存等
        return "服务降级,暂时不可用";
    }
}
public class UserExceptionCommand extends HystrixCommand<String> {

    public static final Logger LOGGER = LoggerFactory.getLogger(UserTimeOutCommand.class);

    public UserExceptionCommand() {
        super(HystrixCommandGroupKey.Factory.asKey("userGroup"));
    }

    @Override
    protected String run() throws Exception {
        LOGGER.info("start query exception endpoint");
        URL url = new URL("http://localhost:8002/user/exception");
        byte[] result = new byte[1024];
        url.openStream().read(result);
        return new String(result);
    }

    @Override
    protected String getFallback() {
        return "服务降级,暂时不可用";
    }
}

在Java配置中我们定义了HystrixCommandGroupKey,HystrixCommandKeyHystrixThreadPoolKey,这三个key值是和config.properties文件中配置项对应的关键因素。如果没定义HystrixThreadPoolKey默认地会使用HystrixCommandGroupKey的值。所以hystrix.threadpool.userGroup.coreSize会在UserExceptionCommand中生效,而hystrix.threadpool.userThreadPool.coreSize会在UserTimeOutCommand中生效。当然所有的属性都是可以通过Java API进行配置的。
getFallback是我们在执行run方法请求超时、异常或者服务不可用时执行的方法。通常可以返回固定值,查询缓存或者备用服务。
配置完成后我们在web-app-service新增一个请求接口

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

    @RequestMapping("/command/timeout")
    public String commandTimeout() {
        return new UserTimeOutCommand().execute();
    }

    @RequestMapping("/command/exception")
    public String commandException() {
        return new UserExceptionCommand().execute();
    }
}

这里HystrixCommand的每次都必须使用新的对象(保证每次请求都是一个新的线程)调用其execute方法,而不能使用单例。否则会提示

This instance can only be executed once. Please instantiate a new instance.

HystrixCommand除了上面的同步执行,还有异步执行和响应式执行的方式。

/**同步**/
new UserExceptionCommand().execute();

/**异步**/
//UserExceptionCommand().execute()和new UserExceptionCommand().queue().get()是等效的。
Future<String> fs = new UserExceptionCommand().queue();
fs.get();

/**响应式**/
//立即执行
Observable<String> ho = new UserExceptionCommand().observe();
//只有当被订阅的时候才会开始执行
Observable<String> co = new UserExceptionCommand().toObservable();
//阻塞的
ho.toBlockingObservable().single();
//非阻塞的
ho.subscribe(new Observer<String>() {
    @Override
    public void onCompleted() {
        // nothing needed here
    }
    @Override
    public void onError(Throwable e) {
        e.printStackTrace();
    }
    @Override
    public void onNext(String v) {
        System.out.println("onNext: " + v);
    }
});
// 非阻塞的
ho.subscribe(new Action1<String>() {
    @Override
    public void call(String v) {
        System.out.println("onNext: " + v);
    }
});

降级
我们请求web-app-service/user/command/exception接口,因为user-service/user/exception会随机抛出运行时异常。当服务异常时,将返回服务降级,暂时不可用,而服务正常时将返回success。这个示例说明我们的服务因为异常降级。
请求/user/command/timeout接口时,返回服务降级,暂时不可用。如果修改hystrix.command.userCommandKey.execution.isolation.thread.timeoutInMilliseconds=15000保证Hstrix限制的超时时间大于接口的返回时长,就能成功返回success。这个示例说明我们的服务因为请求超时,所以降级了。
限流
在微服务架构中我们服务端的请求承载量是有限的,我们希望在客户端请求时,限制请求的并发数,防止请求数量过大对服务端产生压力。我们可以通过semaphore.maxConcurrentRequests,coreSize,maxQueueSizequeueSizeRejectionThreshold设置信号量模式下的最大并发量、线程池大小、缓冲区大小和缓冲区降级阈值。在示例中我们作如下设置

#不设置缓冲区,当请求数超过coreSize时直接降级
hystrix.threadpool.userThreadPool.maxQueueSize=-1
#超时时间大于我们的timeout接口返回时间
hystrix.command.userCommandKey.execution.isolation.thread.timeoutInMilliseconds=15000

这个时候我们连续多次请求/user/command/timeout接口,在第一个请求还没有成功返回时,查看输出日志可以发现只有第一个请求正常的进入到user-service的接口中,其它请求会直接返回降级信息。这样我们就实现了对服务请求的限流。
熔断
在配置文件中我们开启了熔断,并且以5秒为度量周期,当5秒内请求超过4个错误超过50%时,就会开启熔断器,所有的请求都会直接降级,如果5秒内的请求不够4个,就算有三个请求且全部失败也不会开启熔断器。10秒后熔断器进入半打开状态会让一部分请求向服务端发起调用,如果成功关闭熔断器,否则再次进入熔断状态。
熔断状态转换
我们对web-app-serivce/user/command/exception连续发起请求(5秒内至少4次),当我们的请求异常超过50%时,服务会直接返回降级信息。
实际上当服务异常、超时、宕机并满足熔断条件时,都会开启熔断。

注解配置Hystrix

Hystrix也支持以注解的形式配置。通过@HystrixCommand注解的fallbackMethod属性指定降级方法。groupKeycommandKey默认为方法名(当然threadPoolKey不指定时,默认和groupKey一致,所以也是方法名),也可以指定这三个key值,配置文件通过groupKey,commandKey,threadPoolKey使用恰当的配置。commandPropertiesthreadPoolProperties是通过@HystrixPropertyname value键值对进行配置。

@Component
public class UserAnnotationCommand {

    @Autowired
    @Qualifier("lbRestTemplate")
    RestTemplate lbRestTemplate;

    @HystrixCommand(fallbackMethod = "timeoutFallback", threadPoolProperties = {
            @HystrixProperty(name = "coreSize", value = "20"),
            @HystrixProperty(name = "queueSizeRejectionThreshold", value = "20")
    }, commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "8000")
    })
    public String timeout() {
        return lbRestTemplate.getForObject("http://user-service/user/timeout", String.class);
    }

    public String timeoutFallback() {
        return "timeout 降级";
    }


    @HystrixCommand(fallbackMethod = "exceptionFallback", threadPoolProperties = {
            @HystrixProperty(name = "coreSize", value = "20"),
            @HystrixProperty(name = "queueSizeRejectionThreshold", value = "20")
    }, commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
    })
    public String exception() {
        return lbRestTemplate.getForObject("http://user-service/user/exception", String.class);
    }

    public String exceptionFallback() {
        return "exception 降级";
    }
}

查看com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect源码,我们可以看到@HystrixCommand注解配置的方式是使用了AOP动态的生成了一个HystrixInvokable对象,通过调用HystrixInvokable的方法实现了HystrixCommand的功能。

// 使用AOP让注解的@HystrixCommand生效
@Aspect
public class HystrixCommandAspect {
    @Pointcut("@annotation(com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand)")
    public void hystrixCommandAnnotationPointcut() {
    }
    @Pointcut("@annotation(com.netflix.hystrix.contrib.javanica.annotation.HystrixCollapser)")
    public void hystrixCollapserAnnotationPointcut() {
    }

    @Around("hystrixCommandAnnotationPointcut() || hystrixCollapserAnnotationPointcut()")
    public Object methodsAnnotatedWithHystrixCommand(final ProceedingJoinPoint joinPoint) throws Throwable {
        Method method = getMethodFromTarget(joinPoint);
        /***省略***/
        MetaHolderFactory metaHolderFactory = META_HOLDER_FACTORY_MAP.get(HystrixPointcutType.of(method));
        MetaHolder metaHolder = metaHolderFactory.create(joinPoint);
        HystrixInvokable invokable = HystrixCommandFactory.getInstance().create(metaHolder);
        ExecutionType executionType = metaHolder.isCollapserAnnotationPresent() ?
                metaHolder.getCollapserExecutionType() : metaHolder.getExecutionType();

        Object result;
        try {
            if (!metaHolder.isObservable()) {
                result = CommandExecutor.execute(invokable, executionType, metaHolder);
            } else {
                result = executeObservable(invokable, executionType, metaHolder);
            }
        } catch (HystrixBadRequestException e) {
            throw e.getCause() != null ? e.getCause() : e;
        } catch (HystrixRuntimeException e) {
            throw hystrixRuntimeExceptionToThrowable(metaHolder, e);
        }
        return result;
    }
}

同样的为了测试这个配置是否生效我们新增接口

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    UserAnnotationCommand userAnnotationCommand;

    @RequestMapping("/command/annotation/timeout")
    public String commandAnnotationTimeout() {
        return userAnnotationCommand.timeout();
    }

    @RequestMapping("/command/annotation/exception")
    public String commandAnnotationException() {
        return userAnnotationCommand.exception();
    }
}

我们可以通过测试超时和随机异常接口查看Hystrix设置是否成功。

http://localhost:8101/user/command/annotation/exception
http://localhost:8101/user/command/annotation/timeout

和FeignClient集成

在SpringCloud中Hystrix和Feign的集成十分方便。在客户端我们需要使用@EnableCircuitBreaker启用熔断机制。

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(defaultConfiguration = FeignClientsConfiguration.class)
@EnableCircuitBreaker
public class WebApplicationStarter {

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

同时application.yml配置文件中开启Hystrix,并进行Hystrix配置。

# 开启熔断机制
feign:
  hystrix:
    enabled: true

ribbon:
  # 开启eureka与ribbon的集成
  eureka:
    enabled: true
  # 暂不开启熔断机制
  hystrix:
    enabled: false
  # 配置ribbon默认的超时时间
  ConnectTimeout: 20000
  ReadTimeout: 20000
  # 是否开启重试
  OkToRetryOnAllOperations: true
  # 重试的时候实例切换次数
  MaxAutoRetriesNextServer: 3
  # 每个实例重试次数
  MaxAutoRetries: 2

## hystrix相关配置
## hystrix默认会读取classpath下的config.properties文件,application会覆盖config.properties中的属性
hystrix:
  threadpool:
    # 指定服务的配置
    user-service:
      coreSize: 20
      maxQueueSize: 200
      queueSizeRejectionThreshold: 3
    # userThreadPool是UserTimeOutCommand中配置的threadPoolKey
    userThreadPool:
      coreSize: 20
      maxQueueSize: 20
      queueSizeRejectionThreshold: 3
    # 这是默认的配置
    default:
      coreSize: 10
      maxQueueSize: 200
      queueSizeRejectionThreshold: 2
  command:
    # 指定feign客户端中具体的方法
    UserService#timeout():
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 20000
    userCommandKey:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 15000
    # 这是默认的配置
    default:
      execution:
        timeout:
          enabled: true
        isolation:
          strategy: THREAD
          thread:
            timeoutInMilliseconds: 15000
            interruptOnTimeout: true
            interruptOnFutureCancel: false
          semaphore:
            maxConcurrentRequests: 2
      fallback:
        enabled: true
        isolation:
          semaphore:
            maxConcurrentRequests: 10
      circuitBreaker:
        enabled: true
        forceOpen: false
        forceClosed: false
        requestVolumeThreshold: 4
        errorThresholdPercentage: 50
        sleepWindowInMilliseconds: 10000
      metrics:
        rollingStats:
          timeInMilliseconds: 5000
          numBuckets: 10
        rollingPercentile:
          enabled: true
          timeInMilliseconds: 60000
          numBuckets: 6
          bucketSize: 100
        healthSnapshot:
          intervalInMilliseconds: 500

这里为了测试超时接口,所以Ribbon配置的超时时间(20s)大于接口的返回时间(10s),否则在Ribbon超时后将自动重试,直到超过Hystrix的超时时间时返回降级信息。在实际中,Hystrix的超时时间应该大于Ribbon的超时时间*Ribbon的重试次数。

如果项目中config.propertiesapplication.yml文件中都有Hystrix的配置,对于同一配置项,application.yml中的属性会覆盖config.properties中的属性值。当然使用Java API和注解方式也是优先读取application.yml中的配置。

在这个配置中我们甚至可以指定FeignClient中的某一方法的Hystrix配置。比如上面的配置文件中我们定义了UserServicetimeout()方法的超时时间为20s。
新增Feign客户端和降级类,降级类必须实现对应的Feign客户端。

@FeignClient(name="user-service", fallback = UserServiceFallback.class)
public interface UserService {

    @RequestMapping(value = "/user/timeout", method = RequestMethod.GET)
    public String timeout();

    @RequestMapping(value = "/user/exception", method = RequestMethod.GET)
    public String exception();
}
@Component
public class UserServiceFallback implements UserService {

    @Override
    public String timeout() {
        return "timeout 降级";
    }

    @Override
    public String exception() {
        return "exception 降级";
    }
}

同样的,我们新增接口测试Hystrix设置是否成功。

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

    @Autowired
    UserService userService;

    @RequestMapping("/feign/timeout")
    public String feignTimeout() {
        return userService.timeout();
    }

    @RequestMapping("/feign/exception")
    public String feignException() {
        return userService.exception();
    }
}
http://localhost:8101/user/feign/annotation/exception
http://localhost:8101/user/feign/annotation/timeout

Hystrix属性配置参考

Hystrix的wiki上详细地罗列了所有属性的含义、默认配置、实例配置以及Java API的配置方式供我们参考。
Hystrix wiki——属性配置


相关代码

SpringCloudDemo-Hystrix


参考:
CircuitBreaker Pattern
Hystrix wiki

评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值