Skip to content
On this page

服务熔断与降级

官方文档:文档地址

我们知道,微服务之间是可以进行相互调用的,那么如果出现了下面的情况会导致什么问题?

image-20220324141230070

由于位于最底端的服务提供者E发生故障,那么此时会直接导致服务ABCD全线崩溃,就像雪崩了一样。

这种问题实际上是不可避免的,由于多种因素,比如网络卡顿、系统故障、硬件问题等,都存在一定可能,会导致这种极端的情况发生。因此,我们需要寻找一个应对这种极端情况的解决方案。

为了解决分布式系统的雪崩问题,SpringCloud提供了Hystrix熔断器组件,他就像我们家中的保险丝一样,当电流过载就会直接熔断,防止危险进一步发生, 从而保证家庭用电安全。可以想象一下,如果整条链路上的服务已经全线崩溃,这时还在不断地有大量的请求到达,需要各个服务进行处理,肯定是会使得情况越来越糟糕的。

我们来详细看看它的工作机制。

服务降级

注意一定要区分开服务降级和服务熔断的区别,服务降级并不会直接返回错误, 而是可以提供一个补救措施,正常响应给请求者。 这样相当于服务依然可用,但是服务能力肯定是下降了的。

我们就基于借阅管理服务来进行讲解,我们不开启用户服务和图书服务,表示用户服务和图书服务已经挂掉了。

这里我们导入Hystrix的依赖(此项目已经停止维护,SpringCloud依赖中已经不自带了,所以说需要自己单独导入):

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

接着我们需要在启动类添加注解开启:

java
@SpringBootApplication
//启用Hystrix
@EnableHystrix
public class BorrowApplication {
    public static void main(String[] args) {
        SpringApplication.run(BorrowApplication.class, args);
    }
}
1
2
3
4
5
6
7
8

那么现在,由于用户服务和图书服务不可用,所以查询借阅信息的请求肯定是没办法正常响应的, 这时我们可以提供一个备选方案,也就是说当服务出现异常时,返回我们的备选方案:

java
@RestController
public class BorrowController {

    @Resource
    private BorrowService service;

    @HystrixCommand(fallbackMethod = "onError")
    @GetMapping("/borrow/{uid}")
    public UserBorrowDetail findUserBorrows(@PathVariable("uid") int uid) {
        return service.getUserBorrowDetailByUid(uid);
    }

    UserBorrowDetail onError(int uid) {
        User user = new User();
        user.setUid(uid);
        List<Book> list = new ArrayList<>();
        return new UserBorrowDetail(user, list);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

可以看到,虽然我们的服务无法正常运行了,但是依然可以给浏览器正常返回响应数据:

json
{
  "user": {
    "uid": 1,
    "name": null,
    "age": null,
    "sex": null
  },
  "bookList": []
}
1
2
3
4
5
6
7
8
9

服务降级是一种比较温柔的解决方案,虽然服务本身的不可用,但是能够保证正常响应数据。

服务熔断

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

实际上,熔断就是在降级的基础上进一步升级形成的,也就是说,在一段时间内多次调用失败,那么就直接升级为熔断。

接着,我们在浏览器中疯狂点击刷新按钮,对此服务疯狂发起请求,可以看到后台:

image-20220324152044551

一开始的时候,会正常地去调用Controller对应的方法findUserBorrows,发现失败然后进入备选方法,但是我们发现在持续请求一段时间之后,没有再调用这个方法,而是直接调用备选方案,这便是升级到了熔断状态。

我们可以继续不断点击,继续不断地发起请求:

image-20220324152750797

可以看到,过了一段时间之后,会尝试正常执行一次findUserBorrows,但是依然是失败状态,所以继续保持熔断状态。

所以得到结论,它能够对一段时间内出现的错误进行侦测,当侦测到出错次数过多时,熔断器会打开,所有的请求会直接响应失败,一段时间后,只执行一定数量的请求,如果还是出现错误,那么则继续保持打开状态,否则说明服务恢复正常运行,关闭熔断器。

我们可以测试一下,开启另外两个服务之后,继续点击:

image-20220324153044583

可以看到,当另外两个服务正常运行之后,当再次尝试调用findUserBorrows之后会成功,于是熔断机制就关闭了,服务恢复运行。

总结一下:

image-20220324153935858

OpenFeign实现降级

Hystrix也可以配合Feign进行降级,我们可以对应接口中定义的远程调用单独进行降级操作。

比如我们还是以用户服务挂掉为例,那么这个时候肯定是会远程调用失败的,也就是说我们的Controller中的方法在执行过程中会直接抛出异常, 进而被Hystrix监控到并进行服务降级。

而实际上导致方法执行异常的根源就是远程调用失败,所以我们换个思路,既然用户服务调用失败,那么我就给这个远程调用添加一个替代方案, 如果此远程调用失败,那么就直接上替代方案。那么怎么实现替代方案呢?我们知道Feign都是以接口的形式来声明远程调用, 那么既然远程调用已经失效,我们就自行对其进行实现,创建一个实现类,对原有的接口方法进行替代方案实现:

java
//注意,需要将其注册为Bean,Feign才能自动注入
@Component
public class UserFallbackClient implements UserClient {
    //这里我们自行对其进行实现,并返回我们的替代方案
    @Override
    public User getUserById(int uid) {
        User user = new User();
        user.setName("我是替代方案");
        return user;
    }
}
1
2
3
4
5
6
7
8
9
10
11

实现完成后,我们只需要在原有的接口中指定失败替代实现即可:

java
//fallback参数指定为我们刚刚编写的实现类
@FeignClient(value = "service-user", fallback = UserFallbackClient.class)
public interface UserClient {

    @RequestMapping("/user/{uid}")
    User getUserById(@PathVariable("uid") int uid);
}
1
2
3
4
5
6
7

现在去掉BorrowController@HystrixCommand注解和备选方法:

java
@RestController
public class BorrowController {

    @Resource
    BorrowService service;

    @RequestMapping("/borrow/{uid}")
    UserBorrowDetail findUserBorrows(@PathVariable("uid") int uid) {
        return service.getUserBorrowDetailByUid(uid);
    }
}
1
2
3
4
5
6
7
8
9
10
11

最后我们在配置文件中开启熔断支持:

yaml
feign:
  circuitbreaker:
    enabled: true
1
2
3

启动服务,调用接口试试看:

image-20220325122629016

image-20220325122301779

可以看到,现在已经采用我们的替代方案作为结果。

BulkHead

试想一个问题,有一艘货船,穿上装满货物,如果船底破了一个大洞,导致船沉了。如何解决这种问题? 这就引出了这里要讲的概念。

img.png

上图是泰坦尼克号的一个横截面,可以发现。一艘船被划分出了许多的空间,来分类管理不同职责做成不同的舱室,这个东西叫舱壁。 如果船底破洞也只会灌满一个舱室,从而保证整艘船的正常运行。

在我们的项目中也是这样的设计,比如线程池的使用。我们不会将所有的任务交给一个线程池去工作,而是不同的业务使用不同的线程池。 说了这么多 Hystrix也是这么考虑的

java

public class BorrowController {
    @HystrixCommand(
            fallbackMethod = "onError",
            threadPoolKey = "findUserBorrowsFallback",
            threadPoolProperties = {
                    @HystrixProperty(name = "coreSize", value = "2"),
            },
            commandProperties = {
                    @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "3000"),
                    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "2"),
                    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
                    @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "3000"),
            }
    )
    @GetMapping("/borrowProperties/{uid}")
    public UserBorrowDetail findUserBorrowsFallback(@PathVariable("uid") int uid) {
        System.out.println("开始向其他服务获取信息");
        return service.getUserBorrowDetailByUid(uid);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

不指定会有一个默认的线程池去处理熔断,但是不同业务的熔断交给不同的线程池去工作,使服务达到尽可能的高可用。

原理

Hystrix 工作流程

  1. 当调用出现错误的时候,开启一个时间窗口(默认10s)

  2. 在这个窗口时间,统计调用次数是否达到最小次数

    2.1. 如果达到了,则终止统计信息返回第1步 (如果没达到最小次数,即使全部失败也回到第一步)

    2.2. 如果没有达到,则统计失败请求次数占所有请求次数的百分比是否达到阈值

    2.2.1. 如果达到了就跳闸(不在请求对应的服务)

    2.2.2. 如果没达到,则丛植统计信息,回到第一步。

  3. 如果跳闸,则会开启一个活动窗口(默认5s ) 每隔5s Hystrix 会一个请求通过,到达服务看是否调用成功

    3.1. 如果成功,重置断路器,回到第1步

    3.2. 如果失败,回到第3步

Released under the MIT License.