目录

spirngcloud服务降级Hystrix

概述

官网资料

使用说明

运行机制

springcloud中使用断路器

Hystrix目前已经入维护模式,官方推荐使用resilience4j,但是国内用的比较多的是阿里巴巴的sentinel

分布式系统面临的问题

复杂分布式体系结构中的应用程序 有数10个依赖关系,每个依赖关系在某些时候将不可避免地失败

http://img.cana.space/picStore/20201110104418.png

服务血崩

多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其他的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,也就是所谓的“雪崩效应”

对于高流量的应用来说,单一的后端依赖可能会导致所有的服务器上的所有资源都在几秒内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便某个依赖关系的失败不能影响整个应用程序或系统。

所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接受流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。

Hystrix是什么

Hystrix是一个用于处理分布式系统的延迟容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。

“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(Fallback),而不是长时间的等待或抛出调用方无法处理的异常,这样就保证了当服务调用方的线程不被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至血崩。

Hystrix功能

  • 服务降级
  • 服务熔断
  • 接近实时地监控
  • 服务限流
  • 服务隔离

Hystrix重要概念

服务降级(fallback)和 服务熔断(break)

降级

举个例子解释,我们去银行排队办理业务,大部分的银行分为普通窗口、特殊窗口(VIP窗口,老年窗口)。某一天银行大厅排普通窗口的人巨多。这时特殊窗口贴出告示说某时刻之后再开放。那么这时特殊窗口的工作人员就可以空出来去帮其他窗口办理业务,提高办事效率,已达到解决普通窗口排队的人过的目的。这时即为降级,降级的目的是为了解决整体项目的压力,而牺牲掉某一服务模块而采取的措施。

哪些情况需要降级

  • 程序运行异常
  • 超时
  • 服务熔断触发服务降级
  • 线程池/信号量打满

熔断

熔断的目的是当A服务模块中的某块程序出现故障后为了不影响其他客户端的请求而做出的及时回应。

两者对比

把两者放在一起说是因为两者从有些角度看是有一定的类似性的:

  1. 目的很一致,都是从可用性可靠性着想,为防止系统的整体缓慢甚至崩溃,采用的技术手段;
  2. 最终表现类似,对于两者来说,最终让用户体验到的是某些功能暂时不可达或不可用;
  3. 粒度一般都是服务级别,当然,业界也有不少更细粒度的做法,比如做到数据持久层(允许查询,不允许增删改);
  4. 自治性要求很高,熔断模式一般都是服务基于策略的自动触发,降级虽说可人工干预,但在微服务架构下,完全靠人显然不可能,开关预置、配置中心都是必要手段;

当然两者的区别也是明显的:

  1. 触发原因不太一样,服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑;
  2. 管理目标的层次不太一样,熔断其实是一个框架级的处理,每个微服务都需要(无层级之分),而降级一般需要对业务有层级之分(比如降级一般是从最外围服务开始)

服务限流(flowlimit)

秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行

Hystrix案例

准备工作

实际演示hystrix的各项功能在项目中如何集成,首先新建一个服务提供者工程提供两个接口,一个处理正常,一个处理超时。

  1. 新建工程

    cloud-provider-hystrix-payment8001

  2. service

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    package com.eh.cloud2020.payment.service;
       
    import lombok.SneakyThrows;
    import org.springframework.stereotype.Service;
       
    import java.util.concurrent.TimeUnit;
       
    @Service
    public class PaymentService {
        public String paymentOK(Integer id) {
            return "线程号:" + Thread.currentThread().getName() + " payment ok, id:" + id;
        }
       
        @SneakyThrows
        public String paymentTimeout(Integer id) {
            long consumeTime = 3;
            TimeUnit.SECONDS.sleep(consumeTime);
            return "线程号:" + Thread.currentThread().getName() + " payment timeout, id:" + id + "耗时(s):" + consumeTime;
        }
    }
    
  3. controller

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    
    package com.eh.cloud2020.payment.controller;
       
    import com.eh.cloud2020.payment.service.PaymentService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RestController;
       
    @RestController
    @Slf4j
    public class PaymentController {
       
        private final PaymentService paymentService;
       
        public PaymentController(PaymentService paymentService) {
            this.paymentService = paymentService;
        }
       
        @GetMapping("/payment/hystrix/ok/{id}")
        public String paymentOK(@PathVariable Integer id) {
            String result = paymentService.paymentOK(id);
            return "result========> " + result;
        }
       
        @GetMapping("/payment/hystrix/timeout/{id}")
        public String paymentTimeout(@PathVariable Integer id) {
            String result = paymentService.paymentTimeout(id);
            return "result========> " + result;
        }
    }
    
  4. 正常测试

    • http://localhost:8001/payment/hystrix/ok/1 非常快,几乎感受不到转圈圈
    • http://localhost:8001/payment/hystrix/timeout/1

高并发测试

Jmeter压测

开启Jmeter,来20000个并发压死8001,20000个请求都去访问paymentInfo_TimeOut服务

http://img.cana.space/picStore/20201110154738.png

http://img.cana.space/picStore/20201110154752.png

再来访问http://localhost:8001/payment/hystrix/ok/1 ,会发现响应没有之前快了,会转一会儿圈圈

Jmeter压测结论

如果一个服务大量耗时严重甚至超时会影响系统整体性能,本例中本来正常的ok服务也受到了影响,因为tomcat线程池里面的工作线程数有限,大量耗时的线程会严重占用线程资源从而影响其他服务的运行,所以需要对大量超时的服务进行降级以缓解系统整体压力。

从客户端进行访问

  1. 新建单机环境下的EurekaServer注册中心工程

    yml

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    server:
      port: 7000
       
    eureka:
      instance:
        hostname: localhost # eureka服务端实例名称
      client:
        register-with-eureka: false # false, 表示不向注册中心注册自己
        fetch-registry: false # false表示自己就是注册中心,我的职责就是维护服务实例,并不去检索服务
        service-url:
          defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
    
  2. cloud-provider-hystrix-payment8001将服务注册到注册中心

    pom

    1
    2
    3
    4
    
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    

    yaml

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    server:
      port: 8001
       
    spring:
      application:
        name: cloud-payment-hystrix-service
       
    eureka:
      client:
        # 表示向注册中心注册自己 默认为true
        register-with-eureka: true
        # 是否从EurekaServer抓取已有的注册信息,默认为true,单节点无所谓,
        # 集群必须设置为true才能配合ribbon使用负载均衡
        fetch-registry: false
        service-url:
          defaultZone: http://localhost:7000/eureka/ # 入驻地址
    

    主启动类上加注解@EnableEurekaClient

  3. 新建客户端工程cloud-consumer-feign-hystrix-order80

    从cloud-consumer-feign-order80拷贝修改

    yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    server:
      port: 80
    eureka:
      client:
        # 只需要从注册中心拉取服务地址列表即可,无需向注册中心注册自己
        register-with-eureka: false
        fetch-registry: true
        service-url:
          defaultZone: http://localhost:7000/eureka
    

    com.eh.cloud2020.payment.service.PaymentHystrixService

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    package com.eh.cloud2020.payment.service;
       
       
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
       
    @FeignClient(value = "CLOUD-PAYMENT-HYSTRIX-SERVICE")
    public interface PaymentHystrixService {
       
        @GetMapping("/payment/hystrix/ok/{id}")
        String paymentOK(@PathVariable("id") Integer id);
       
        @GetMapping("/payment/hystrix/timeout/{id}")
        String paymentTimeout(@PathVariable("id") Integer id);
    }
    

正常访问ok

给timeout接口进行负载,这次更狠一点,对timeout接口 5w/s qps打到服务器,再访问ok接口

http://img.cana.space/picStore/20201110164925.png

因为feign使用ribbion做超时控制,ribbion默认read time 是1秒,说明消费者客户端收到提供者服务端的影响,资源也被消耗尽了,原本瞬间响应的服务现在超过1秒还没有响应。

如何解决

问题

  • 服务提供者的服务耗时严重导致消费者服务变慢甚至超时
  • 对方服务ok,调用者自己有超时控制要求

解决

服务降级

服务降级

使用注解@HystrixCommand

  • 设置调用的超时时间,一旦超过超时时间就调用兜底方法(服务降级fallback)进行处理;
  • 一旦方法出错也会调用fallback进行处理

服务端和消费端分别引入hystrix依赖

1
2
3
4
5
<!--hystrix-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

服务端降级

com.eh.cloud2020.payment.controller.PaymentController#paymentTimeout

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@HystrixCommand(fallbackMethod = "fallback", commandProperties = {
        // 超过3s就超时,调用回调方法
        @HystrixProperty(name ="execution.isolation.thread.timeoutInMilliseconds", value = "1000")
})
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentTimeout(@PathVariable Integer id) {
    String result = paymentService.paymentTimeout(id);
    return "result========> " + result;
}

public String fallback(Integer id) {
    return "Payment服务已降级,请稍后再试";
}

主启动类上加注解@EnableCircuitBreaker

测试访问:http://localhost:8001/payment/hystrix/timeout/1

1
Payment服务已降级,请稍后再试

如果方法内部出错也会降级,一旦出错立即调用fallback返回

属性名参考com.netflix.hystrix.HystrixCommandProperties

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Hystrix会有一个单独的线程池进行处理,起到隔离的效果
@HystrixCommand(fallbackMethod = "fallback", commandProperties = {
        // 超过3s就超时,调用回调方法
        @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000")
})
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentTimeout(@PathVariable Integer id) {
    int i = 10 / 0; // 模拟异常
    String result = paymentService.paymentTimeout(id);
    return "result========> " + result;
}

public String fallback(Integer id) {
    return "Payment服务已降级,请稍后再试";
}

代码重构

服务端每个Controller每个接口都配置一个fallback方法会很麻烦,可以在Controler上使用注解@DefaultProperties,之后只需要在接口上增加@HystrixCommand注解即可,示例程序如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.eh.cloud2020.payment.controller;

import com.eh.cloud2020.payment.service.PaymentService;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
// 注意:default fallback method cannot have parameters.
@DefaultProperties(defaultFallback = "fallback", commandProperties = {
        @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
})
public class PaymentController {

    private final PaymentService paymentService;

    public PaymentController(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentOK(@PathVariable Integer id) {
        String result = paymentService.paymentOK(id);
        return "result========> " + result;
    }


    @HystrixCommand
    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentTimeout(@PathVariable Integer id) {
        String result = paymentService.paymentTimeout(id);
        return "result========> " + result;
    }

    public String fallback() {
        return "Payment服务已降级,请稍后再试";
    }
}

客户端降级

客户端降级是指在调用服务端服务时如果调用情况不满足客户端自身要求则对这次服务端调用降级

如何降级:根据cloud-consumer-feign-hystrix-order80已经有的PaymentHystrixService接口,重新新建一个类(PaymentFallbackService)实现接口,统一为接口里面的方法进行降级处理

yml

1
2
3
feign:
  hystrix:
    enabled: true

主启动类添加注解@EnableHystrix

com.eh.cloud2020.payment.service.PaymentFallbackService

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package com.eh.cloud2020.payment.service;

import org.springframework.stereotype.Component;

@Component
public class PaymentFallbackService implements PaymentHystrixService {
    @Override
    public String paymentOK(Integer id) {
        return "我对服务端/payment/ok 调用情况不满意,本次调用已经降级,请稍后再试";
    }

    @Override
    public String paymentTimeout(Integer id) {
        return "我对服务端/payment/timeout 调用情况不满意,本次调用已经降级,请稍后再试";
    }
}

调用服务端接口增加fallback属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package com.eh.cloud2020.payment.service;


import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "CLOUD-PAYMENT-HYSTRIX-SERVICE", fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {

    @GetMapping("/payment/hystrix/ok/{id}")
    String paymentOK(@PathVariable("id") Integer id);

    @GetMapping("/payment/hystrix/timeout/{id}")
    String paymentTimeout(@PathVariable("id") Integer id);
}

测试

http://img.cana.space/picStore/20201110193802.png

上面容错降级只是使用系统默认设置,下面我们单独给某一个timeout接口设置相关属性

Feign Hystrix设置单独的接口超时时间和FallBack

先说结论:HystrixCommonKey生成方法:类名#方法名(入参类型)

再说原理

在package feign.hystrix.SetterFactory中看是如何生产commandKey的:

http://img.cana.space/picStore/20201110224337.png

生成key的实现很简单,在此不展开

注意事项

Feign+Hystrix

默认基本配置

最基本的配置,是 Hystrix 自己的一长串配置:hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds,但在 Feign 模块中,单独设置这个超时时间不行,还要额外设置 Ribbon 的超时时间,比如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 5000

ribbon:
  ReadTimeout: 5000
  ConnectTimeout: 5000

关于 Hystrix 的配置,这里有官方的说明

Title Desc
Default Value 1000
Default Property hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
Instance Property hystrix.command.HystrixCommandKey.execution.isolation.thread.timeoutInMilliseconds
How to Set Instance Default HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(int value)

不同实例分别配置

如果更进一步,想把超时时间细分到不同的 service 实例上也可以实现,比如:

1

Ribbon + Hystrix

在使用 Ribbon 时,只需要配置 Hystrix 的超时时间就可以生效,不需要额外配置 Ribbon 的超时时间,比如:

1
2
3
4
5
6
7
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 9000

最后实操

综上,我们可以如下配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
server:
  port: 80

eureka:
  client:
    # 只需要从注册中心拉取服务地址列表即可,无需向注册中心注册自己
    register-with-eureka: false
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7000/eureka

hystrix:
  threadpool:
    default:
      # 核心线程池大小  默认10
      coreSize: 20
      # 最大最大线程池大小
      maximumSize: 30
      # 此属性允许maximumSize的配置生效。 那么该值可以等于或高于coreSize。 设置coreSize <maximumSize会创建一个线程池,
      # 该线程池可以支持maximumSize并发,但在相对不活动期间将向系统返回线程。 (以keepAliveTimeInMinutes为准)
      allowMaximumSizeToDivergeFromCoreSize: true
      # 请求等待队列
      maxQueueSize: 10
      # 队列大小拒绝阀值 在还未超过请求等待队列时也会拒绝的大小
      queueSizeRejectionThreshold: 10
  command:
    # default全局有效 默认值为 commonKey commonKey生成方法在 Feign.configKey(target.type(), method) 中
    default:
      fallback:
        enabled: true
      execution:
        timeout:
          #如果enabled设置为false,则请求超时交给ribbon控制,为true,则超时作为熔断根据
          enabled: true
        isolation:
          #隔离策略,有THREAD和SEMAPHORE
          #THREAD - 它在单独的线程上执行,并发请求受线程池中的线程数量的限制
          #SEMAPHORE - 它在调用线程上执行,并发请求受到信号量计数的限制
          #对比:https://www.cnblogs.com/java-synchronized/p/7927726.html
          thread:
            timeoutInMilliseconds: 1000 #断路器超时时间,默认1000ms
    PaymentHystrixService#paymentTimeout(Integer):
      fallback:
        enabled: true
      execution:
        timeout:
          #如果enabled设置为false,则请求超时交给ribbon控制,timeoutInMilliseconds不起作用
          #如果为true,则timeoutInMilliseconds作为熔断的超时根据,注意!!!此时ribbon.ReadTimeout要大于timeoutInMilliseconds
          #否则timeoutInMilliseconds不起作用,因为此时两个超时策略同时进行,
          #要想让timeoutInMilliseconds生效那么此时ribbon.ReadTimeout一定要大于timeoutInMilliseconds
          enabled: true
        isolation:
          #隔离策略,有THREAD和SEMAPHORE
          #THREAD - 它在单独的线程上执行,并发请求受线程池中的线程数量的限制
          #SEMAPHORE - 它在调用线程上执行,并发请求受到信号量计数的限制
          #对比:https://www.cnblogs.com/java-synchronized/p/7927726.html
          thread:
            timeoutInMilliseconds: 2800 #断路器超时时间,默认1000ms

ribbon:
  ReadTimeout: 10000
  ConnectTimeout: 5000

feign:
  hystrix:
    #开启feign的hystrix支持,默认是false
    enabled: true

上面有段注释很重要,单独拎出来

1
2
3
4
#如果enabled设置为false,则请求超时交给ribbon控制,timeoutInMilliseconds不起作用
#如果为true,则timeoutInMilliseconds作为熔断的超时根据,注意!!!此时ribbon.ReadTimeout要大于timeoutInMilliseconds
#否则timeoutInMilliseconds不起作用,因为此时两个超时策略同时进行,
#要想让timeoutInMilliseconds生效那么此时ribbon.ReadTimeout一定要大于timeoutInMilliseconds

服务端暂停时间3s,示例程序结果:

  • timeoutInMilliseconds设置2800,“我对服务端/payment/timeout 调用情况不满意,本次调用已经降级,请稍后再试”
  • timeoutInMilliseconds设置3550, “result========> 线程号:http-nio-8001-exec-9 payment timeout, id:1耗时(s):3”

使用注解给服务单独设置超时时间

Feign的超时设置

目前基本使用Feign都是与ribbon结合使用的,最重要的两个超时是连接超时ConnectTimeout和读超时ReadTimeout 在Spring Cloud中使用Feign进行微服务调用分为两层:Ribbon的调用及Hystrix的调用。所以Feign的超时时间就是Ribbon和Hystrix超时时间的结合,而如果不启用Hystrix则Ribbon的超时时间就是Feign的超时时间配置,Feign自身的配置会被覆盖。 而如果开启了Hystrix,那么Ribbon的超时时间配置与Hystrix的超时时间配置则存在依赖关系,因为涉及到Ribbon的重试机制,所以一般情况下都是Ribbon的超时时间小于Hystrix的超时时间,否则会出现以下错误:

1
2019-07-12 11:10:20,238 397194 [http-nio-8084-exec-2] WARN  o.s.c.n.z.f.r.s.AbstractRibbonCommand - The Hystrix timeout of 40000ms for the command operation is set lower than the combination of the Ribbon read and connect timeout, 80000ms.

在Ribbon超时但Hystrix没有超时的情况下,Ribbon便会采取重试机制;而重试期间如果时间超过了Hystrix的超时配置则会立即被熔断(fallback)。 下面按优先级从高到低配置

默认配置 在默认配置下,Feign的超时时间配置如下:

1
2
3
    public class DefaultClientConfigImpl implements IClientConfig {
        public static final int DEFAULT_READ_TIMEOUT = 5000;
        public static final int DEFAULT_CONNECT_TIMEOUT = 2000;

从上面一看是2s和5s,但是这是个坑,因为在构造完这个类后,又使用ribbon的配置把默认配置覆盖掉了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    @Bean
    @ConditionalOnMissingBean
    public IClientConfig ribbonClientConfig() {
        DefaultClientConfigImpl config = new DefaultClientConfigImpl();
        //此处还将DefaultClientConfigImpl内部的一个属性enableDynamicProperties改成了ture
        config.loadProperties(this.name);
        config.set(CommonClientConfigKey.ConnectTimeout, DEFAULT_CONNECT_TIMEOUT);
        config.set(CommonClientConfigKey.ReadTimeout, DEFAULT_READ_TIMEOUT);
        return config;
    }

ribbon全局配置:

1
2
3
ribbon:  
    ReadTimeout: 60000  
    ConnectTimeout: 60000

ribbon指定服务配置: app-server为服务名

1
2
3
4
app-server:
  ribbon:
    ReadTimeout: 30000
    ConnectTimeout: 30000

Feign全局配置 需要注意的是connectTimeout和readTimeout必须同时配置,要不然不会生效,还是以ribbon为准

1
2
3
4
5
6
feign:
  client:
    config:
      default:
        connectTimeout: 10000
        readTimeout: 10000

Feign指定服务配置 和全局配置类似

1
2
3
4
5
6
feign:
  client:
    config:
      app-server:
        connectTimeout: 10000
        readTimeout: 10000

hystrix指定方法配置:

1
2
3
4
5
hystrix.command.xxx#yyy(zzz).execution.isolation.thread.timeoutInMilliseconds=mmm
xxx:要设置的某个FeignClient的类名
yyy:方法名
zzz:参数,如果是基础类型,就直接填基础类型:String/int;如果是某个对象,就直接填对象的类名
mmm:要设置的超时时间(毫秒)

示例:

1
hystrix.command.HelloService#getMessage().execution.isolation.thread.timeoutInMilliseconds=10000

上述设置可以使feign的方法超时时间设置为10秒钟

服务熔断

熔断机制概述

熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。

在Spring Cloud框架里,熔断机制是通过Hystrix实现,Hystrix会监控微服务间调用的状况。当失败的调用到达一定阈值后,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand

Martin Fowler’s 论文 断路器

It’s common for software systems to make remote calls to software running in different processes, probably on different machines across a network. One of the big differences between in-memory calls and remote calls is that remote calls can fail, or hang without a response until some timeout limit is reached. What’s worse if you have many callers on a unresponsive supplier, then you can run out of critical resources leading to cascading failures across multiple systems. In his excellent book Release It, Michael Nygard popularized the Circuit Breaker pattern to prevent this kind of catastrophic cascade.

The basic idea behind the circuit breaker is very simple. You wrap a protected function call in a circuit breaker object, which monitors for failures. Once the failures reach a certain threshold, the circuit breaker trips, and all further calls to the circuit breaker return with an error, without the protected call being made at all. Usually you’ll also want some kind of monitor alert if the circuit breaker trips.

http://img.cana.space/picStore/20201110220629.png

使用Hystrix做熔断演示

演示步骤

  1. 创建client包,用于调用FeignClient接口

  2. 编写Client类, 使用@HystrixCommand注解设置断路器属性

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    
    package com.eh.cloud2020.payment.client;
       
    import com.eh.cloud2020.payment.service.PaymentHystrixService;
    import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
    import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
    import org.springframework.stereotype.Component;
       
    @Component
    public class PaymentHystrixClient {
       
        private final PaymentHystrixService paymentHystrixService;
       
        public PaymentHystrixClient(PaymentHystrixService paymentHystrixService) {
            this.paymentHystrixService = paymentHystrixService;
        }
       
        // 属性参考com.netflix.hystrix.HystrixCommandProperties
        @HystrixCommand(fallbackMethod = "fallback", commandProperties = {
                @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),// 是否开启断路器
                @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),// 请求次数
                @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 短路多久以后开始尝试恢复,默认5秒
                @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"),// 失败率达到多少后跳闸
        })
        public String paymentOK(Integer id) {
            if (id < 0) {
                throw new RuntimeException("******id 不能负数");
            } else {
                return paymentHystrixService.paymentOK(id);
            }
        }
       
        public String fallback(Integer id) {
            return "程序异常导致降级,请稍后再试";
        }
    }
    
  3. 修改Controller,改成调用Client

    1
    2
    3
    4
    
    @GetMapping("/order/payment/ok/{id}")
    public String paymentOK(@PathVariable("id") Integer id) {
        return paymentHystrixClient.paymentOK(id);
    }
    
  4. 验证

    多次访问:http://localhost/order/payment/ok/-1,之后再访问http://localhost/order/payment/ok/1

    发现刚开始不满足条件,就算是正确的访问也不能进行,后面就恢复正常访问了。

注意:yaml还是之前的配置,熔断降级和容错降级各自工作。

原理研究

springcloud中使用断路器

This simple circuit breaker avoids making the protected call when the circuit is open, but would need an external intervention to reset it when things are well again. This is a reasonable approach with electrical circuit breakers in buildings, but for software circuit breakers we can have the breaker itself detect if the underlying calls are working again. We can implement this self-resetting behavior by trying the protected call again after a suitable interval, and resetting the breaker should it succeed.

http://img.cana.space/picStore/20201111012024.png

状态类型:

  • Closed

    熔断关闭后不会对服务进行熔断

  • Open

    请求不再调用当前服务走降级分支,当打开持续时间超过reset timeout时则进入半熔断状态

  • Half Open

    部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断

官网执行步骤说明

http://img.cana.space/picStore/20201111012732.png

The precise way that the circuit opening and closing occurs is as follows:

  1. Assuming the volume across a circuit meets a certain threshold (HystrixCommandProperties.circuitBreakerRequestVolumeThreshold())…
  2. And assuming that the error percentage exceeds the threshold error percentage (HystrixCommandProperties.circuitBreakerErrorThresholdPercentage())…
  3. Then the circuit-breaker transitions from CLOSED to OPEN.
  4. While it is open, it short-circuits all requests made against that circuit-breaker.
  5. After some amount of time (HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()), the next single request is let through (this is the HALF-OPEN state). If the request fails, the circuit-breaker returns to the OPEN state for the duration of the sleep window. If the request succeeds, the circuit-breaker transitions to CLOSED and the logic in 1. takes over again.
1
2
3
4
5
6
7
    // 属性参考com.netflix.hystrix.HystrixCommandProperties
    @HystrixCommand(fallbackMethod = "fallback", commandProperties = {
            @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),// 是否开启断路器
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),// 请求次数
            @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 短路多久以后开始尝试恢复,默认5秒
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"),// 失败率达到多少后跳闸
    })

当最近10次失败率达到60%则打开断路器,此时所有请求都降级,10秒之后断路器进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器闭合,主逻辑恢复;如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗口重新计时。

服务限流

hystrix已经入维护模式,在此先不展开,之后会着重介绍alibaba的Sentinel

Hystrix工作流程

官网说明

http://img.cana.space/picStore/20201111014527.png

The following sections will explain this flow in greater detail:

  1. Construct a HystrixCommand or HystrixObservableCommand Object

    构造一个HystrixCommand或者HystrixObservableCommand对象,一般使用@HystrixCommand注解

  2. Execute the Command

    执行Command

  3. Is the Response Cached?

    查看缓存里是否存在返回值

  4. Is the Circuit Open?

    熔断器是否处于打开状态

  5. Is the Thread Pool/Queue/Semaphore Full?

    检查Hystrix线程池、队列和信号量是否已经满了

  6. HystrixObservableCommand.construct() or HystrixCommand.run()

    HystrixObservableCommand执行构造方法,HystrixCommand对象执行run方法

  7. Calculate Circuit Health

    计算断路器健康情况

  8. Get the Fallback

    断路器不健康则返回fallback结果

  9. Return the Successful Response

    断路器健康则返回正常执行结果

Hystrix图形化Dashboard

在hystrix的回退方法中做好报警通知就可以了,Hystrix的监控仪表盘在实际开发中用得不多,此处只是作为了解。

hystrix的监控可以检测消费者调用提供者的情况,hystrix是在消费者中设置的,hystrix的监控自然也是在消费者中设置的。

actuator 服务调用监控

1、在消费者中添加依赖:

1
2
3
4
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency> 

2、配置文件

1
2
3
4
5
management:
  endpoints:
    web:
      exposure:
        include: "*"

默认只会监控部分数据,此配置是监控服务调用所有的数据

3、在浏览器地址栏输入http://localhost:80/actuator/hystrix.stream ,ip、port都是消费者的

刷新一下服务调用http://localhost/order/payment/ok/1,已经监控到数据

http://img.cana.space/picStore/20201111110718.png

dashboard 仪表盘

上面密密麻麻的数据不直观,Hystrix提供了仪表盘可以将数据直观地展示出来。可以在消费者中配置仪表盘,也可以单独写一个子模块作为仪表盘。

  1. 手动添加依赖

    1
    2
    3
    4
    
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
    </dependency>
    

    如果是单独用一个服务来作为仪表盘,只加这个依赖即可,里面已经包含了spring-boot-start-web。

  2. yml

    1
    2
    
    server:
      port: 9090
    
  3. 引导类上加@EnableHystrixDashboard

    1
    2
    3
    4
    5
    6
    7
    
    @EnableHystrixDashboard
    @SpringBootApplication
    public class HystrixDashboardMain9090 {
        public static void main(String[] args) {
            SpringApplication.run(HystrixDashboardMain9090.class, args);
        }
    }
    
  4. 浏览器地址栏输入 http://localhost:9090/hystrix, ip、port都是 dasdboard 所在应用的。输入要监控的 actuator 的地址(也就是上文的http://localhost:80/actuator/hystrix.stream),dasdboard 启动时会在控制台打印出可监控的actuator地址。

    http://img.cana.space/picStore/20201111111608.png

  5. 多次访问:http://localhost/order/payment/ok/-1,之后再访问http://localhost/order/payment/ok/1,查看仪表盘变化

    http://img.cana.space/picStore/20201111111726.png

    可以看到熔断已经打开,等待resetTime后再多次访问http://localhost/order/payment/ok/1,可以发现熔断关闭。

    http://img.cana.space/picStore/20201111111829.png

仪表盘说明

http://img.cana.space/picStore/20201111112007.png

七色:七种状态颜色说明

1圈:实心圈共有两种含义,颜色的变化代表了实例的健康程度,它的健康从绿色<黄色<橙色<红色递减。除了颜色的变化外,它的大小也根据实例的请求量发生变化,流量越大该实心圆就越大。所以通过实心圆的展示,就可以在大量的实例中快速的发现故障实例和高压实例。

1线:用来记录2分钟流量的相对变化,可以通过它来观察到流量的上升和下降趋势

整图说明

http://img.cana.space/picStore/20201111112226.png

当消费者调用的服务实例较多时就会呈现下面这张图的情况

http://img.cana.space/picStore/20201111112826.png