微服务专题10- Spring Cloud 服务熔断

前言

前面的章节我们讲了Spring Cloud 负载均衡并实现了客户端的负载均衡。

本节,继续微服务专题的内容分享,共计16小节,分别是:

本节内容重点为:

  • Spring Cloud Hystrix:作为服务端服务短路实现,介绍 Spring Cloud Hystrix 常用限流的功能,同时,说明健康指标以及数据指标在生产环境下的现实意义
  • 生产准备特性:介绍聚合数据指标 Turbine 、Turbine Stream,以及整合 Hystrix Dashboard

熔断机制

Netflix Hystrix 官方 wiki
一般在分布式系统里,客户端(client)发起远程调用至服务端(Service)的过程中,如果客户端有很多的时候,每个服务器的并发承受量是有限的,当超过访问上限以后,其他的客户端的响应可能反应变慢甚至失效。这就是所谓的熔断,通常有两种实现方式。

  • 通过超时时间控制

在这里插入图片描述

假设在Service服务端通过dubbo/http提供的线程池数量为200个,理论上可以承受的并发就是200,若是client的QPS超过200,其他客户端便会陷入等待状态,直至线程池里有空闲线程。

这里的问题是:客户端的请求时间是不稳定的,有的客户端请求时间短,有的请求时间较长。而服务端若一直等待请求较慢的客户端显然是不合理的。

如果此时设置超时时间,比如为200ms,不论是否请求成功,放弃超时的client,让处于排队状态的客户端进入队列,则会极大的缓解“交通拥堵”。

Q:通过超时时间机制容错后,服务端通常会返回什么?
A:关于容错返回值的问题:通常可以返回null,或者是空对象,也可以返回异常。

QPS:Queries Per Second意思是“每秒查询率”,是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。
TPS:是TransactionsPerSecond的缩写,也就是事务数/秒。它是软件测试结果的测量单位。一个事务是指一个客户机向服务器发送请求然后服务器做出反应的过程。客户机在发送请时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数。

  • 通过QPS计数形式控制
    在这里插入图片描述
    通常在分布式场景下,每个通过控制每个服务端线程池的配置来平衡客户端的QPS数量以达到目的。

Spring Cloud Hystrix Client

实验内容为:通过设定的超时时间如果大于100ms,则进行服务熔断。
我们首先引用Hystrix的配置实现服务熔断。

启动类加入注解:

@EnableHystrix               // 激活 Hystrix

核心实现:

 @HystrixCommand(
            fallbackMethod = "errorContent",
            commandProperties = {
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
                            value = "100")
            }

    )
    @GetMapping("/say")
    public String say(@RequestParam String message) throws InterruptedException {
        // 如果随机时间 大于 100 ,那么触发容错
        int value = random.nextInt(200);

        System.out.println("say() costs " + value + " ms.");

        // > 100
        Thread.sleep(value);

        System.out.println("ServerController 接收到消息 - say : " + message);
        return "Hello, " + message;
    }

    public String errorContent(String message) {
        return "Fault";
    }

实现结果测试:
访问地址:http://localhost:9090/say?message=test

此时访问显示未超过100ms,处理程序,打印信息。
在这里插入图片描述

多次点击以后发现,如果访问超过100ms,则会熔断,不会打印之后的内容:
在这里插入图片描述
下图为对应的请求所打印的数据:
在这里插入图片描述

这里的原理是怎么实现的呢?请继续往下看。

熔断不仅局限于 Hystrix, 我们自己是否可以实现这样的方法实现熔断?

方法签名

  • 访问限定符

  • 方法返回类型

  • 方法名称

  • 方法参数

    • 方法数量
    • 方法类型+顺序
    • 方法名称(编译时预留,IDE,Debug)

手写实现服务熔断(Future)

低级版本(无容错实现)

    private final ExecutorService executorService = Executors.newSingleThreadExecutor();

    /**
     * 简易版本
     *
     * @param message
     * @return
     * @throws InterruptedException
     */
    @GetMapping("/say2")
    public String say2(@RequestParam String message) throws Exception {
        Future<String> future = executorService.submit(() -> {
            return doSay2(message);
        });
        // 100 毫秒超时
        String returnValue = future.get(100, TimeUnit.MILLISECONDS);
        return returnValue;
    }

实验结果测试:

访问地址:http://localhost:9090/say2?message=test

在这里插入图片描述

这种实现方式的问题就在于没有容错,即超过100ms并未做任何拦截,程序依然执行。

低级版本+(带容错实现)

    private final ExecutorService executorService = Executors.newSingleThreadExecutor();

    /**
     * 简易版本
     *
     * @param message
     * @return
     * @throws InterruptedException
     */
    @GetMapping("/say2")
    public String say2(@RequestParam String message) throws Exception {
        Future<String> future = executorService.submit(() -> {
            return doSay2(message);
        });
        // 100 毫秒超时
        String returnValue = null;
        try {
            returnValue = future.get(100, TimeUnit.MILLISECONDS);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            // 超级容错 = 执行错误 或 超时
            returnValue = errorContent(message);
        }
        return returnValue;
    }

实验结果测试:

访问地址:http://localhost:9090/say2?message=test

大于100ms:
在这里插入图片描述

小于100ms:
在这里插入图片描述

中级版本

使用aop切面解决问题:

  1. 首先在启动类声明aop:
@EnableAspectJAutoProxy(proxyTargetClass = true) // 激活 AOP
  1. Web MVC 配置:
/**
 * Web MVC 配置
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CircuitBreakerHandlerInterceptor());
    }
}
  1. 中级版本
    /**
     * 中级版本
     *
     * @param message
     * @return
     * @throws InterruptedException
     */
    @GetMapping("/middle/say")
    public String middleSay(@RequestParam String message) throws Exception {
        Future<String> future = executorService.submit(() -> {
            return doSay2(message);
        });
        // 100 毫秒超时
        String returnValue = null;

        try {
            returnValue = future.get(100, TimeUnit.MILLISECONDS);
        } catch (TimeoutException e) {
            future.cancel(true); // 取消执行
            throw e;
        }
        return returnValue;
    }
  1. 捕获超时异常。
@RestControllerAdvice(assignableTypes = ServerController.class)
public class CircuitBreakerControllerAdvice {

    @ExceptionHandler
    public void onTimeoutException(TimeoutException timeoutException,
                                   Writer writer) throws IOException {
        writer.write(errorContent("")); // 网络 I/O 被容器
        writer.flush();
        writer.close();
    }

    public String errorContent(String message) {
        return "Fault";
    }

}

实验结果测试:

访问地址:http://localhost:9090/middle/say?message=test

大于100ms:
在这里插入图片描述
小于100ms:

在这里插入图片描述

高级版本(无注解实现)

  1. 接口声明
    /**
     * 高级版本
     *
     * @param message
     * @return
     * @throws InterruptedException
     */
    @GetMapping("/advanced/say")
    public String advancedSay(@RequestParam String message) throws Exception {
        return doSay2(message);
    }
  1. ServerControllerAspect
@Aspect
@Component
public class ServerControllerAspect {

    private ExecutorService executorService = newFixedThreadPool(20);

    @Around("execution(* com.test.micro.services.spring.cloud." +
            "server.controller.ServerController.advancedSay(..)) && args(message) ")
    public Object advancedSayInTimeout(ProceedingJoinPoint point, String message) throws Throwable {
        Future<Object> future = executorService.submit(() -> {
            Object returnValue = null;
            try {
                returnValue = point.proceed(new Object[]{message});
            } catch (Throwable ex) {
            }
            return returnValue;
        });

        Object returnValue = null;
        try {
            returnValue = future.get(100, TimeUnit.MILLISECONDS);
        } catch (TimeoutException e) {
            future.cancel(true); // 取消执行
            returnValue = errorContent("");
        }
        return returnValue;
    }

    public String errorContent(String message) {
        return "Fault";
    }

    @PreDestroy
    public void destroy() {
        executorService.shutdown();
    }

}

实验结果测试:

http://localhost:9090/advanced/say?message=test

大于100ms:
在这里插入图片描述

小于100ms:
在这里插入图片描述

高级版本(带注解实现)

  • TimeoutCircuitBreaker 注解
@Target(ElementType.METHOD) // 标注在方法
@Retention(RetentionPolicy.RUNTIME) // 运行时保存注解信息
@Documented
public @interface TimeoutCircuitBreaker {

    /**
     * 超时时间
     * @return 设置超时时间
     */
    long timeout();

}

  • Aspect 注解实现
    @Around("execution(* com.test.micro.services.spring.cloud." +
            "server.controller.ServerController.advancedSay2(..)) && args(message) && @annotation(circuitBreaker)")
    public Object advancedSay2InTimeout(ProceedingJoinPoint point,
                                        String message,
                                        CircuitBreaker circuitBreaker) throws Throwable {
        long timeout = circuitBreaker.timeout();
        return doInvoke(point, message, timeout);
    }
  • 反射API 实现
    @Around("execution(* com.test.micro.services.spring.cloud." +
            "server.controller.ServerController.advancedSay2(..)) && args(message) ")
    public Object advancedSay2InTimeout(ProceedingJoinPoint point,
                                        String message) throws Throwable {

        long timeout = -1;
        if (point instanceof MethodInvocationProceedingJoinPoint) {
            MethodInvocationProceedingJoinPoint methodPoint = (MethodInvocationProceedingJoinPoint) point;
            MethodSignature signature = (MethodSignature) methodPoint.getSignature();
            Method method = signature.getMethod();
            CircuitBreaker circuitBreaker = method.getAnnotation(CircuitBreaker.class);
            timeout = circuitBreaker.timeout();
        }
        return doInvoke(point, message, timeout);
    }

实验结果测试:

http://localhost:9090/advanced/say2?message=test

大于100ms:
在这里插入图片描述

小于100ms:
在这里插入图片描述

高级版本(信号灯实现 = 单机版限流方案)

@Target(ElementType.METHOD) // 标注在方法
@Retention(RetentionPolicy.RUNTIME) // 运行时保存注解信息
@Documented
public @interface SemaphoreCircuitBreaker {

    /**
     * 信号量
     *
     * @return 设置超时时间
     */
    int value();

}

    @Around("execution(* com.test.micro.services.spring.cloud." +
            "server.controller.ServerController.advancedSay3(..))" +
            " && args(message)" +
            " && @annotation(circuitBreaker) ")
    public Object advancedSay3InSemaphore(ProceedingJoinPoint point,
                                          String message,
                                          SemaphoreCircuitBreaker circuitBreaker) throws Throwable {
        int value = circuitBreaker.value();
        if (semaphore == null) {
            semaphore = new Semaphore(value);
        }
        Object returnValue = null;
        try {
            if (semaphore.tryAcquire()) {
                returnValue = point.proceed(new Object[]{message});
                Thread.sleep(1000);
            } else {
                returnValue = errorContent("");
            }
        } finally {
            semaphore.release();
        }

        return returnValue;

    }

实验结果测试:

http://localhost:9090/advanced/say3?message=test

大于100ms:
在这里插入图片描述

通过加入信号量,实际上这里会延迟一会,即所谓的熔断,然后进行处理。

小于100ms:

在这里插入图片描述

后记

本节代码地址:Hystrix

更多架构知识,欢迎关注本套Java系列文章Java架构师成长之路

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值