天天看点

[Spring Cloud] Spring Cloud Hystrix

Spring Cloud Hystrix

目录

  • Spring Cloud Hystrix
  • Hystrix特性
  • 断路器机制
  • Fallback
  • 资源隔离
  • 服务熔断
  • 服务降级
  • 测试
  • REFERENCES
  • 更多
手机用户请​

​横屏​

​​获取最佳阅读体验,​

​REFERENCES​

​中是本文参考的链接,如需要链接和更多资源,可以关注其他博客发布地址。
平台 地址
简书 https://www.jianshu.com/u/3032cc862300
个人博客 https://yiyuery.github.io/NoteBooks/
前言

​雪崩效应​

​,在微服务架构中通常会有多个服务层调用,基础服务的故障可能会导致级联故障,进而造成整个系统不可用的情况,这种现象被称为服务雪崩效应。服务雪崩效应是一种因“服务提供者”的不可用导致“服务消费者”的不可用,并将不可用逐渐放大的过程。例如,A作为服务提供者,B为A的服务消费者,C和D是B的服务消费者。A不可用引起了B的不可用,并将不可用像滚雪球一样放大到C和D时,雪崩效应就形成了。

​Hystrix​

​​是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等。​

​Hystrix​

​能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。

​熔断器​

​原理很简单,如同电力过载保护器。它可以实现快速失败,如果它在一段时间内侦测到许多类似的错误,会强迫其以后的多个调用快速失败,不再访问远程服务器,从而防止应用程序不断地尝试执行可能会失败的操作,使得应用程序继续执行而不用等待修正错误,或者浪费CPU时间去等到长时间的超时产生。熔断器也可以使应用程序能够诊断错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。

​熔断器模式​

​就像是那些容易导致错误的操作的一种代理。这种代理能够记录最近调用发生错误的次数,然后决定使用允许操作继续,或者立即返回错误。熔断器就是保护服务高可用的最后一道防线。

  • 服务熔断:熔断机制是应对雪崩效应的一种微服务链路保护机制。
  • 服务降级:在由于非业务异常导致的服务不可用时,返回默认值,避免异常影响主业务的处理

Hystrix特性

断路器机制

断路器很好理解, 当Hystrix Command请求后端服务失败数量超过一定比例(默认50%), 断路器会切换到开路状态(Open). 这时所有请求会直接失败而不会发送到后端服务. 断路器保持在开路状态一段时间后(默认5秒), 自动切换到半开路状态(HALF-OPEN). 这时会判断下一次请求的返回情况, 如果请求成功, 断路器切回闭路状态(CLOSED), 否则重新切换到开路状态(OPEN). Hystrix的断路器就像我们家庭电路中的保险丝, 一旦后端服务不可用, 断路器会直接切断请求链, 避免发送大量无效请求影响系统吞吐量, 并且断路器有自我检测并恢复的能力.

Fallback

Fallback相当于是降级操作. 对于查询操作, 我们可以实现一个fallback方法, 当请求后端服务出现异常的时候, 可以使用fallback方法返回的值. fallback方法的返回值一般是设置的默认值或者来自缓存.

资源隔离

在Hystrix中, 主要通过线程池来实现资源隔离. 通常在使用的时候我们会根据调用的远程服务划分出多个线程池. 例如调用产品服务的Command放入A线程池, 调用账户服务的Command放入B线程池. 这样做的主要优点是运行环境被隔离开了. 这样就算调用服务的代码存在bug或者由于其他原因导致自己所在线程池被耗尽时, 不会对系统的其他服务造成影响. 但是带来的代价就是维护多个线程池会对系统带来额外的性能开销. 如果是对性能有严格要求而且确信自己调用服务的客户端代码不会出问题的话, 可以使用Hystrix的信号模式(Semaphores)来隔离资源.

现在让我们做一些更有趣的事情。由于网关背后的服务可能会对我们的客户产生不良影响,所以我们可能希望将创建的路由封装在熔断器中。您可以在Spring Cloud Gateway中使用Hystrix实现这一点。这是通过一个简单的过滤器实现的,您可以将其添加到您的请求中。让我们创建另一个路径来演示这一点。

服务熔断

在下面的例子中,我们将利用HTTPBin的延迟API,它在发送响应之前等待一定数量的秒。因为这个API可能需要很长时间来发送它的响应,所以我们可以在HystrixCommand中包装使用这个API的路由。向RouteLocator对象添加一个新路由,如下所示:

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
        .route(p -> p
            .path("/get")
            .filters(f -> f.addRequestHeader("Hello", "World"))
            .uri("http://httpbin.org:80"))
        .route(p -> p
            .host("*.hystrix.com")
            .filters(f -> f.hystrix(config -> config.setName("mycmd")))
            .uri("http://httpbin.org:80")).
        build();
}      

这个新路由配置与我们之前创建的路由配置有一些不同。首先,我们使用的是主机地址的正则匹配而不是路径的正则匹配。这意味着只要主机是​

​hystrix.com​

​​,我们就会将请求路由到HTTPBin,并将该请求包装在​

​HystrixCommand​

​​中。我们通过应用过滤器到路由上来实现这一点。Hystrix过滤器可以使用配置对象来实现配置。在本例中,我们只是将​

​HystrixCommand​

​​命名为​

​mycmd​

​。

让我们测试一下这条新路线。启动应用程序,但这一次我们将请求​

​/get​

​​和​

​/delay/3​

​​。同样重要的是,我们要包含一个​

​Host​

​​的Header,它拥有​

​hystrix.com​

​​的主机,否则请求将不会被路由转发。使用​

​curl​

​测试是这样的:

curl --dump-header - --header 'Host:www.hystrix.com' http://localhost:8080/get
curl --dump-header - --header 'Host:www.hystrix.com' http://localhost:8080/delay/3      
比较几种请求返回
  • ​curl http://localhost:8080/get​

{
  "args": {}, 
  "headers": {
    "Accept": "*/*", 
    "Forwarded": "proto=http;host=\"localhost:8080\";for=\"0:0:0:0:0:0:0:1:62044\"", 
    "Hello": "World", 
    "Host": "httpbin.org", 
    "User-Agent": "curl/7.54.0", 
    "X-Forwarded-Host": "localhost:8080"
  }, 
  "origin": "0:0:0:0:0:0:0:1, 115.198.1.0, ::1", 
  "url": "https://localhost:8080/get"
}      
  • ​curl --dump-header - --header 'Host:www.hystrix.com' http://localhost:8080/get​

HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Content-Type: application/json
Date: Thu, 28 Mar 2019 13:58:24 GMT
Server: nginx
Content-Length: 361

{
  "args": {}, 
  "headers": {
    "Accept": "*/*", 
    "Forwarded": "proto=http;host=www.hystrix.com;for=\"0:0:0:0:0:0:0:1:62096\"", 
    "Hello": "World", 
    "Host": "httpbin.org", 
    "User-Agent": "curl/7.54.0", 
    "X-Forwarded-Host": "www.hystrix.com"
  }, 
  "origin": "0:0:0:0:0:0:0:1, 115.198.1.0, ::1", 
  "url": "https://www.hystrix.com/get"
}      

当我们请求​

​delay/3​

​时,

curl --dump-header - --header 'Host: www.hystrix.com' http://localhost:8080/delay/3      
HTTP/1.1 504 Gateway Timeout
Content-Type: application/json;charset=UTF-8
Content-Length: 158

{"timestamp":"2019-03-28T14:08:34.337+0000","path":"/delay/3","status":504,"error":"Gateway Timeout","message":"Response took longer than configured timeout"}      
[Spring Cloud] Spring Cloud Hystrix

服务降级

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
        .route(p -> p
            .path("/get")
            .filters(f -> f.addRequestHeader("Hello", "World"))
            .uri("http://httpbin.org:80"))
        .route(p -> p
            .host("*.hystrix.com")
            .filters(f -> f.hystrix(config -> config
                .setName("mycmd")
                .setFallbackUri("forward:/fallback")))
            .uri("http://httpbin.org:80"))
        .build();
}      
定义处理器
/*
 * @ProjectName: 编程学习
 * @Copyright:   2019 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
 * @address:     http://xiazhaoyang.tech
 * @date:        2019/3/28 23:00
 * @email:       [email protected]
 * @description: 本内容仅限于编程技术学习使用,转发请注明出处.
 */
package com.example.gateway.controller;

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

/**
 * <p>
 *
 * </p>
 *
 * @author xiazhaoyang
 * @version v1.0.0
 * @date 2019/3/28 23:00
 * @modificationHistory=========================逻辑或功能性重大变更记录
 * @modify By: {修改人} 2019/3/28
 * @modify reason: {方法名}:{原因}
 * ...
 */
@RestController
public class FallBackController {

    @RequestMapping(value = "/fallback")
    public String errorFallBack(){
        return "服务器开小差了,请稍等...";
    }
}      
[Spring Cloud] Spring Cloud Hystrix

测试

package com.example.gateway;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;

/**
 * Our test is actually taking advantage of WireMock from Spring Cloud Contract in order stand up a server that can mock the APIs from HTTPBin.
 * The first thing to notice is the use of @AutoConfigureWireMock(port = 0). This annotation will start WireMock on a random port for us.
 * 我们的测试实际上利用了Spring Cloud Contract中的WireMock,以便建立一个可以模拟HTTPBin中的api的服务器。
 * Next notice that we are taking advantage of our UriConfiguration class and setting the httpbin property in the @SpringBootTest annotation
 * to the WireMock server running locally. Within the test we then setup "stubs" for the HTTPBin APIs we call via the Gateway and
 * mock the behavior we expect. Finally we use WebTestClient to actually make requests to the Gateway and validate the responses.
 *
 * @author xiazhayang
 * @return a
 * @date 2019/3/28 23:25
 * @modify by: {修改人} 2019/3/28 23:25
 * @modify by reason:
 * @since 1.0.0
 */
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    properties = {"httpbin=http://localhost:${wiremock.server.port}"})
@AutoConfigureWireMock(port = 0)//使用随机端口
public class CapsuleGatewayApplicationTests {

  @Autowired
  private WebTestClient webClient;

  @Test
  public void contextLoads(){
    //使用WireMock伪造请求 /get 的返回
    stubFor(get(urlEqualTo("/get"))
        .willReturn(aResponse()
            .withBody("{\"headers\":{\"Hello\":\"World2\"}}")
            .withHeader("Content-Type", "application/json")));
    //伪造请求 /delay/3 的返回
    stubFor(get(urlEqualTo("/delay/3"))
        .willReturn(aResponse()
            .withBody("fallback msg")
            .withFixedDelay(3000)));

    //get & /delay/3 发起请求
        //并对请求结果进行断言,若不考虑断言,会返回上面mock的数据
    webClient
        .get().uri("/get")
        .exchange()
        .expectStatus().isOk()
        .expectBody()
                //下面是断言判断,如果失败则抛出异常
        .jsonPath("$.headers.Hello").isEqualTo("World2");

    webClient
        .get().uri("/delay/3")
        .header("Host", "www.hystrix.com")
        .exchange()
        .expectStatus().isOk()
        .expectBody();
                //下面是断言判断,如果失败则抛出异常
        //.consumeWith(
            //response -> assertThat(response.getResponseBody()).isEqualTo("服务器开小差了,请稍等...".getBytes()));
  }
}      

REFERENCES

  • springcloud(四):熔断器Hystrix
  • Spring Cloud Hystrix

更多

扫码关注“架构探险之道”,获取更多源码和文章资源