三、SpringCloud断路器

CY 2019年03月18日 669次浏览

Hystrix

关于概念,网上很多了,例如这一篇:点击查看

服务降级(fallback)

什么情况下需要服务降级,例如程序运行出异常了,服务调用超时了,服务熔断了,线程池/信号量打满了...

什么意思呢,现在假如A,B两台服务器,B服务的某个接口被高并发访问中,导致B服务的其他接口也会被拖慢,A服务这个时候要使用B服务的这个接口,这就意味着A服务也会变慢,假如B服务被拖死了,A服务随之也会被拖死。

A和B两台服务器可能出现的情况如下:

  • B服务当前正在卡着,A服务访问B服务,A服务需要等待B服务的响应,所以A服务也被卡死。

  • B服务现在宕机了,A服务访问B服务,怎么也得不到结果,A服务被拖慢了,最终还是会被卡死。

  • B服务好好的,但是A服务发生了一点故障,导致A服务变慢了,最终照样被卡死。

为了防止这个情况,那么现在就应该加入服务降级,防止A服务被拖死了,也可以在B服务中加入降级防止自己被拖死。

发现问题

现在模拟一下这个情况。

在Provider里面写三个接口:

@Value("${server.port}")
private String serverPort;

@GetMapping("normal")
public String normal() {
    return "返回了一个信息!" + serverPort + " --> " + UUID.randomUUID().toString();
}

@GetMapping("timeout")
public String timeout() {
    try {
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "超时返回了一个信息!!!" + serverPort + " --> " + UUID.randomUUID().toString();
}

@GetMapping("exception")
public String exception() {
    int i = 10 / 0;
    return "返回异常...";
}

三个接口分别是正常,超时,异常。

然后用在Consumer端用OpenFeign去调用这三个接口。

@FeignClient("cloud-provider-paymanet")
public interface OpenFeignService {

    @GetMapping("normal")
    String normal();

    @GetMapping("timeout")
    String timeout();

    @GetMapping("exception")
    String exception();
}
@RestController
@Slf4j
public class PayController {

    @Resource
    private OpenFeignService openFeignService;

    @GetMapping("normal")
    public String normal() {
        return openFeignService.normal();
    }

    @GetMapping("timeout")
    public String timeout() {
        return openFeignService.timeout();
    }

    @GetMapping("exception")
    public String exception() {
        return openFeignService.exception();
    }
}

此时访问Consumer端的接口,发现因为Provider犯下的错误,导致Consumer也受到了影响。

Provider规避自己的错误

现在只好让Provider不犯错,或者自己把错误处理好,不影响别人。那么这个时候Provider就可以用Hystrix进行服务的降级了。

首先导入坐标:

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

这样就可以使用Hystrix了,接下来在可能超时的方法上面做点手脚:

public String timeoutHandler() {
    return "对不起,服务超时了...";
}

@HystrixCommand(fallbackMethod = "timeoutHandler", commandProperties = {
    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000")
})
@GetMapping("timeout")
public String timeout() {
    try {
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "超时返回了一个信息!!!" + serverPort + " --> " + UUID.randomUUID().toString();
}

@HystrixCommand这个注解用在方法上,可以为这个方法规定一些约束,例如上面规定了他的降级方法为timeoutHandler,然后规定了在execution.isolation.thread.timeoutInMilliseconds等于1000的时候进行服务降级。

execution.isolation.thread.timeoutInMilliseconds这个长长的字符串在哪里找到的呢?

com.netflix.hystrix.HystrixCommandProperties这个类中规定了这些属性,使用的时候可以到这个类中去寻找。

光使用上面的注解是不够的,还需要再主启动类上面使用@EnableCircuitBreaker注解。例如下面的样子:

@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class PaymentApplication {

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

circuitbreaker是断路器的意思。

好了,启动Provider,访问接口http://localhost:8001/timeout可以看到不再等待那么长时间了,而是使用了断路器规定的1秒钟。然后就调用了降级方法,返回了:“对不起,服务超时了...”

如果打印出线程的名称,可以看到使用的线程是Hystrix专属的线程,例如:HystrixTimer-1

同样的对于exception来说,可以用下面的代码:

public String exceptionHandler() {
    return "对不起,服务报错了...";
}

@HystrixCommand(fallbackMethod = "exceptionHandler")
@GetMapping("exception")
public String exception() {
    int i = 10 / 0;
    return "返回异常...";
}

上面只是把Provider做了服务降级的保护,当然也是可以让Consumer做降级保护的,方法都是一样的,不重复了。

全局服务降级

@DefaultProperties(defaultFallback = "globalHandler", commandProperties = {
    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000")
})
@RestController
@Slf4j
public class PayController {

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("normal")
    public String normal() {
        return "返回了一个信息!" + serverPort + " --> " + UUID.randomUUID().toString();
    }

    public String globalHandler() {
        return "全局的降级处理...";
    }

    @HystrixCommand
    @GetMapping("timeout")
    public String timeout() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "超时返回了一个信息!!!" + serverPort + " --> " + UUID.randomUUID().toString();
    }

    @HystrixCommand
    @GetMapping("exception")
    public String exception() {
        int i = 10 / 0;
        return "返回异常...";
    }
}

上面代码使用了@DefaultProperties注解重复了之前的配置,给了一个默认的降级方法globalHandler,这样需要服务降级的方法上就可以不用写一堆的东西了,直接使用@HystrixCommand注解就可以了。

服务降级类

当然上面的代码还有一个问题,降级方法和一堆的业务逻辑都混合在一起,显然不是很漂亮。

可以在OpenFeign上面做手脚:

写一个类,实现标注了@FeignClient注解的接口:

@Component
public class OpenFeignServiceFallback implements OpenFeignService {

    @Override
    public String normal() {
        return "正常方法的服务降级...";
    }

    @Override
    public String timeout() {
        return "超时方法的服务降级...";
    }

    @Override
    public String exception() {
        return "异常方法的服务降级...";
    }
}

注意一定要写上@Component注解,不然的话启动都没法启动。

然后修改@FeignClient,加入属性fallback

@FeignClient(value = "cloud-provider-paymanet", fallback = OpenFeignServiceFallback.class)
public interface OpenFeignService {

    @GetMapping("normal")
    String normal();

    @GetMapping("timeout")
    String timeout();

    @GetMapping("exception")
    String exception();
}

因为在Feign中使用Hystrix,但是Feign默认是关闭了Hystrix功能的,所以需要在配置文件中启用。

feign:
  hystrix:
    enabled: true

Hystrix和Ribbon超时时间问题

Hystrix设置的超时时间和Ribbon设置的超时时间是不冲突的,谁设置的时间短,谁会先生效:

举个例子:

如果Hystrix设置了execution.isolation.thread.timeoutInMilliseconds1SRibbon配置了超时时间为3S,调用接口需要2S,就会在1S的时候走降级方法。

反过来Hystrix设置为3SRibbon设置为1S,也会在1S的时候走降级方法,只不过后台会报错调用超时:

2020-03-23 15:06:33.788 DEBUG [cloud-consumer-order,4a78d6a77e1687c8,3e3fcb80db6a3b12,true] 3208 --- [ider-paymanet-4] c.cy.consumer.service.OpenFeignService   : [OpenFeignService#timeout] java.net.SocketTimeoutException: Read timed out
	at java.base/java.net.SocketInputStream.socketRead0(Native Method)
	at java.base/java.net.SocketInputStream.socketRead(SocketInputStream.java:115)
	at java.base/java.net.SocketInputStream.read(SocketInputStream.java:168)
	...

配置文件的Hystrix

之前的@HystrixProperty配置的Hystrix其实也可以写在配置文件里面:

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 1000

default,意味着这是全局的配置

如果要为某一个方法配置一下:

hystrix:
  command:
    timeout:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 4000

可以看到把default改为了timeout,这就意味着给timeout方法设置了超时时间了。

服务熔断(break)

如果一个接口访问错误到达一定的比例,例如官方默认的10秒有50%(10个以上)的服务发生了错误。将会切断当前接口的线路,无论是否正常都会被强行切断服务,过5秒后会再次尝试这个接口的服务是否可用,决定是否要恢复当前接口的线路。

要实现的效果就是访问接口发生错误达到一定的比例后,正确的访问也在一定的时间内不能访问了。

服务熔断也是马丁·弗勒提出的概念,大神就是令人敬仰。

和要实现的效果一样,有三种状态,ClosedOpenHalf Open

Closed的时候服务可以正常的访问,当访问的时候失败的次数达到了阈值,就变成了Open状态,也就不让访问了,过了一段时间后变成了Half Open状态,在这个状态的时候可以尝试访问,如果访问成功,就变为Closed状态,如果访问失败,继续保持Open状态。

接下来用代码来实现一下,不用官方的默认值,用自己定义的值。

@RestController
@Slf4j
public class PayController {

    @HystrixCommand(defaultFallback = "breakHandler", commandProperties = {
            // 是否启用服务熔断,默认就是true
            @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
            // 服务熔断的阈值,默认是20,也就是说访问20次,有多少次失败了...
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
            // 多久之后恢复成Half Open状态,默认就是5000
            @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000"),
            // 请求失败达到的百分比是多少,开启服务熔断,默认就是50%
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
    })
    @GetMapping("break/{id}")
    public String breakService(@PathVariable("id") Integer id) {
        if (id > 0) {
            return "访问正常...";
        }
        throw new RuntimeException("访问异常...");
    }

    public String breakHandler() {
        return "服务已经被熔断了...";
    }
}

注释中已经解释的很详细了,怎么测试呢?

访问接口http://localhost:8001/break/-1会发生错误,会调用breakHandler方法

然后访问http://localhost:8001/break/1接口是正常的访问。

这个时候重复的刷新错误的接口,让访问出错的频率变高,达到阈值,然后再去访问正确的接口,会发现正确的接口也无法访问了。

服务熔断测试

需要注意的是,服务熔断是针对同一个接口而言的。

Hystix Dashboard

这个东西是Hystrix的图形化界面,他长这样:

Hystrix Dashboard

用来观察服务的状态。

首先需要安装它,新建一个项目,然后导入坐标:

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

然后规定一个端口号9001,在启动类上面加上:@EnableHystrixDashboard

@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardApplication {

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

启动后就可以访问:http://localhost:9001/hystrix

要被监控的服务必须要引入:

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

还需要进行一些配置:

management:
  endpoints:
    web:
      exposure:
        include: health, info, hystrix.stream

这样就可以了!

dashboard界面输入:http://localhost:8001/actuator/hystrix.stream,然后点击Monitor Stream按钮就可以进入监控界面了。

但是刚进入界面的时候是一个Loading ...,并且啥反应都没有,解决这个问题的方法也很简单,只需要访问一下Hystrix控制的接口一次就可以了。

Dashboard界面查看方法

Hystrix Dashboard查看技巧

服务限流(flowlimit)

服务限流后面有更加好用的工具,参考本系列第九篇SpringCloud Alibaba Sentinel