Spring Cloud 学习笔记 - No.4 断路器 Hystrix

请先阅读之前的内容:

在微服务架构中,各单元应用间通过服务注册与订阅的方式互相依赖。
由于每个单元都在不同的进程中运行,依赖通过远程调用的方式执行,这样就有可能因为网络原因或是依赖服务自身问题出现调用故障延迟,而这些问题会直接导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最后就会出现因等待出现故障的依赖方响应而形成任务积压,线程资源无法释放,最终导致自身服务的瘫痪,进一步甚至出现故障的蔓延最终导致整个系统的瘫痪。

Spring Cloud Netflix Hystrix 介绍

https://spring.io/guides/gs/circuit-breaker/

针对上述问题,在 Spring Cloud Hystrix 中实现了线程隔离、断路器等一系列的服务保护功能,该框架目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix具备了服务降级、服务熔断、线程隔离、请求缓存、请求合并以及服务监控等强大功能。

一个需要断路器的场景

场景描述:在之前的例子中,我们的 eureka-consumer 调用 eureka-client 来提供加法服务,eureka-client 启动了两个实例,端口分别是 2001 和 2002。

对于 eureka-client 提供的加法服务,可能会出现如下的情况:

  • 两个 eureka-client 实例都挂了,即出现了故障,例如我们手动关闭这两个实例,然后调用 eureka-consumerconsumer 接口 http://127.0.0.1:3001/consumer,会出现如下的结果:

    调用失败

  • eureka-client 服务没有挂,但是延时较大,例如我们可以手动增加 5 秒的延时:
    修改 eureka-clientCalculatorController.java 类:

@GetMapping("/add")
public Integer add(@RequestParam Integer operand1, @RequestParam Integer operand2)  throws InterruptedException {

    Thread.sleep(5000L);

    return operand1 + operand2;
}

调用 eureka-consumerconsumer 接口 http://127.0.0.1:3001/consumer,会出现如下的结果:

调用超时

从后台日志中可以看出调用超时:
调用超时

因为 Feign 默认的 read timeout 就是5秒,参见 https://cloud.spring.io/spring-cloud-netflix/multi/multi_spring-cloud-feign.html

这样会有一个问题:每一个请求都至少需要执行5秒,这样会出现请求堆积的情况。
我们希望:如果在某一个时间窗口内,超过一定数量的请求发生了超时,我们启动某个机制,使得后来的请求直接返回,不需要再次等待5秒。这就是断路器。

Hystrix 服务降级

我们利用之前创建的服务消费者 eureka-consumer
首先在 pom.xml 中增加如下的依赖:

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

注意,如果是 Finchley 版本的 Spring Cloud,需要再添加如下依赖:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.7.1</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.7.1</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
</dependency>

否则,启动时会报如下的错误:

ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.0.3.RELEASE:run (default-cli) on project eureka-consumer: An exception occurred while running. null: InvocationTargetException: Error creating bean with name 'hystrixCommandAspect' defined in class path resource [org/springframework/cloud/netflix/hystrix/HystrixCircuitBreakerConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect]: Factory method 'hystrixCommandAspect' threw exception; nested exception is java.lang.NoClassDefFoundError: org/aspectj/lang/JoinPoint: org.aspectj.lang.JoinPoint -> [Help 1]

随后在应用主类中使用 @EnableCircuitBreaker@EnableHystrix 注解开启 Hystrix 的使用:

@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
public class EurekaConsumerApplication {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

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

随后改造服务消费方式,新增 ConsumerService 类,然后将在 ConsumerController 中的逻辑迁移过去。最后,在为具体执行逻辑的函数上增加 @HystrixCommand注解来指定服务降级方法

@Service
public class ConsumerService {
    @Autowired
    RestTemplate restTemplate;

    @HystrixCommand(
            commandProperties = {
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1000"),
                    @HystrixProperty(name = "execution.isolation.strategy",value = "THREAD")},
            fallbackMethod = "fallback")
    public String consumer() {
        // 调用加法服务
        String url = "http://eureka-client/add";

        UriComponentsBuilder builder = UriComponentsBuilder
                .fromUriString(url)
                // Add query parameter
                .queryParam("operand1", 1)
                .queryParam("operand2", 2);

        return restTemplate.getForObject(builder.toUriString(), Integer.class)+"";
    }

    public String fallback() {
        return "fallback";
    }

}
@RestController
public class ConsumerController {

    private final static Logger logger = LoggerFactory.getLogger(ConsumerController.class);

    @Autowired
    private ConsumerService consumerService;

    @GetMapping("/consumer")
    public String consumer() {
        return consumerService.consumer();
    }

}

此时,再次调用 eureka-consumerconsumer 接口 http://127.0.0.1:3001/consumer,会出现如下的结果:

服务降级

Hystrix 依赖隔离

Hystrix 会为每一个 Hystrix 命令创建一个独立的线程池,这样就算某个在 Hystrix 命令包装下的依赖服务出现延迟过高的情况,也只是对该依赖服务的调用产生影响,而不会拖慢其他的服务。

在上面的示例中,我们使用了@HystrixCommand 来将某个函数包装成了 Hystrix 命令,这里除了定义服务降级之外,Hystrix 框架就会自动的为这个函数实现调用的隔离。所以,依赖隔离、服务降级在使用时候都是一体化实现的

Hystrix 断路器

在上面的示例中,当我们把服务提供者 eureka-client 中加入了模拟的时间延迟之后,在服务消费端的服务降级逻辑因为 Hystrix 命令调用依赖服务超时,触发了降级逻辑,但是即使这样,受限于 Hystrix 超时时间的问题,我们的调用依然很有可能产生堆积。

断路器的三个重要参数:

  • 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
  • 请求总数下限:在快照时间窗内,必须满足请求总数下限才有资格根据熔断。默认为20,意味着在10秒内,如果该 Hystrix 命令的调用此时不足20次,即时所有的请求都超时或其他原因失败,断路器都不会打开。
  • 错误百分比下限:当请求总数在快照时间窗内超过了下限,比如发生了30次调用,如果在这30次调用中,有16次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%下限情况下,这时候就会将断路器打开。

断路器未打开之前,对于之前那个示例的情况就是每个请求都会在当 Hystrix 超时之后返回 fallback,每个请求时间延迟就是近似 Hystrix 的超时时间,如果设置为5秒,那么每个请求就都要延迟5秒才会返回。

断路器打开之后,再有请求调用的时候,将不会调用主逻辑,而是直接调用降级逻辑,这个时候就不会等待5秒之后才返回 fallback。
通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果。

在断路器打开之后,处理逻辑并没有结束,我们的降级逻辑已经被成了主逻辑,那么原来的主逻辑要如何恢复呢?对于这一问题,Hystrix 也为我们实现了自动恢复功能。当断路器打开,对主逻辑进行熔断之后,Hystrix 会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。

Hystrix 监控面板

在上面的示例中,断路器是根据一段时间窗内的请求情况来判断并操作断路器的打开和关闭状态的。
而这些请求情况的指标信息都是 HystrixCommandHystrixObservableCommand 实例在执行过程中记录的重要度量信息,它们除了 Hystrix 断路器实现中使用之外,对于系统运维也有非常大的帮助。
这些指标信息会以“滚动时间窗”与“桶”结合的方式进行汇总,并在内存中驻留一段时间,以供内部或外部进行查询使用,Hystrix Dashboard 就是这些指标内容的消费者之一。

hystrix-dashboard 监控面板

可以通过如下的 Spring Assistant 插件来创建项目 hystrix-dashboard,添加 Hystrix Dashboard 等作为依赖。

hystrix-dashboard 的创建

hystrix-dashboard 的创建

hystrix-dashboard 的创建

<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</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>

为应用主类加上 @EnableHystrixDashboard,启用 Hystrix Dashboard 功能:

@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardApplication {

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

application.properties 文件配置如下:

spring.application.name=hystrix-dashboard
server.port=5001

最后通过命令 mvn spring-boot:run 启动该项目,访问 http://127.0.0.1:5001/hystrix 可以看到:

Hystrix Dashboard 监控首页

Hystrix Dashboard 共支持三种不同的监控方式,依次为:

我们这里先看第三种:单体应用的监控,例如我们想监控上面的例子 eureka-consumer,它对应的监控数据接口为 http://127.0.0.1:3001/actuator/hystrix.stream

注意:

  • 对于 Finchley 版本的 Spring Cloud,接口是 /actuator/hystrix.stream,其他版本的接口是 /hystrix.stream
  • 对于 Finchley 版本的 Spring Cloud,需要在 application.properties 中添加如下配置:
management.endpoints.web.exposure.include=*

随后通过如下的方式创建一个针对 eureka-consumer 的监控面板

创建一个针对 eureka-consumer 的监控面板

创建一个针对 eureka-consumer 的监控面板

我们可以在监控信息的左上部分找到两个重要的图形信息:一个实心圆和一条曲线。

  • 实心圆:共有两种含义。它通过颜色的变化代表了实例的健康程度,它的健康度从绿色、黄色、橙色、红色递减。该实心圆除了颜色的变化之外,它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。所以通过该实心圆的展示,我们就可以在大量的实例中快速的发现故障实例和高压力实例。
  • 曲线:用来记录2分钟内流量的相对变化,我们可以通过它来观察到流量的上升和下降趋势。
其他一些数量指标

此时的系统架构如下:图片引自 http://blog.didispace.com/spring-cloud-starter-dalston-5-2/

监控 单个实例 系统架构

Hystrix 监控数据聚合

上面的例子中,只能实现对服务 单个实例 的数据展现,在生产环境我们的服务是肯定需要做高可用的,那么对于 多个实例 的情况,我们就需要将这些度量指标数据进行聚合。

这里需要另外一个工具:Turbine。
此时的系统架构如下:图片引自 http://blog.didispace.com/spring-cloud-starter-dalston-5-2/

监控 多个实例 系统架构

可以通过如下的 Spring Assistant 插件来创建项目 turbine,添加 Hystrix Dashboard 等作为依赖。

turbine 的创建

turbine 的创建

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-turbine</artifactId>
</dependency>

为应用主类加上 @EnableTurbine 注解开启 Turbine:

@SpringBootApplication
@EnableTurbine
@EnableDiscoveryClient
public class TurbineApplication {

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

application.properties 加入eureka和turbine的相关配置:

spring.application.name=turbine
server.port=6001
management.port=6002

eureka.client.serviceUrl.defaultZone=http://localhost:1234/eureka/

turbine.app-config=eureka-consumer
turbine.cluster-name-expression="default"
turbine.combine-host-port=true

management.endpoints.web.exposure.include=urbine.stream
  • turbine.app-config 参数指定了需要收集监控信息的服务名;
  • turbine.cluster-name-expression 参数指定了集群名称为 default,当我们服务数量非常多的时候,可以启动多个 Turbine 服务来构建不同的聚合集群,而该参数可以用来区分这些不同的聚合集群,同时该参数值可以在 Hystrix 仪表盘中用来定位不同的聚合集群,只需要在 Hystrix Stream 的 URL 中通过 cluster参数来指定;
  • turbine.combine-host-port 参数设置为 true,可以让同一主机上的服务通过主机名与端口号的组合来进行区分,默认情况下会以 host 来区分不同的服务,这会使得在本地调试的时候,本机上的不同服务聚合成一个服务来统计。

最后通过命令 mvn spring-boot:run 启动该项目,访问 http://127.0.0.1:6001/turbine.stream 可以看到 ping 的结果:

http://127.0.0.1:6001/turbine.stream

我们可以将 http://127.0.0.1:6001/turbine.stream 配置到 Hystrix Dashboard 中:

将集群的监控配置到 Hystrix Dashboard

我们也可以通过消息代理收集聚合,此时的架构如下:图片引自 http://blog.didispace.com/spring-cloud-starter-dalston-5-2/

通过消息代理收集聚合

具体的使用,参见 http://blog.didispace.com/spring-cloud-starter-dalston-5-2/


引用:
程序猿DD Spring Cloud基础教程
Spring Cloud构建微服务架构:服务容错保护(Hystrix服务降级)【Dalston版】
Spring Cloud构建微服务架构:服务容错保护(Hystrix依赖隔离)【Dalston版】
Spring Cloud构建微服务架构:服务容错保护(Hystrix断路器)【Dalston版】
Spring Cloud构建微服务架构:Hystrix监控面板【Dalston版】
Spring Cloud构建微服务架构:Hystrix监控数据聚合【Dalston版】
Spring Cloud Dalston中文文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容