/limiting

Primary LanguageJava

Spring Boot 单体应用熔断技术选型

1.入围方案

  • Sentinel

    • github地址
    • https://sentinelguard.io/zh-cn/docs/introduction.html
    • 阿里出品,Spring Cloud Alibaba限流组件,目前持续更新中
    • 自带Dashboard,可以查看接口Qps等,并且可以动态修改各种规则
    • 流量控制,直接限流、冷启动、排队
    • 熔断降级,限制并发限制数和相应时间
    • 系统负载保护,提供系统级别防护,限制总体CPU等
    • 主要核心:资源,规则(流量控制规则、熔断降级规则、系统保护规则、来源访问控制规则 和 热点参数规则。),和指标
    • 文档非常清晰和详细,中文
    • 支持动态规则(推模式和拉模式)
  • Hystrix

    • github地址
    • https://github.com/Netflix/Hystrix/wiki
    • Netflix出品,Spring Cloud Netflix限流组件,已经停止新特性开发,只进行bug修复,最近更新为2018年,功能稳定
    • 有简单的dashboard页面
    • 以隔离和熔断为主的容错机制,超时或被熔断的调用将会快速失败,并可以提供 fallback 机制的初代熔断框架,异常统计基于滑动窗口
  • resilience4j

    • github地址
    • https://resilience4j.readme.io/docs
    • 是一款轻量、简单,并且文档非常清晰、丰富的熔断工具。是Hystrix替代品,实现思路和Hystrix一致,目前持续更新中
    • 需要自己对micrometer、prometheus以及Dropwizard metrics进行整合
    • CircuitBreaker 熔断
    • Bulkhead 隔离
    • RateLimiter QPS限制
    • Retry 重试
    • TimeLimiter 超时限制
    • Cache 缓存
  • 自己实现(基于Guava)

    • 基于Guava的令牌桶,可以轻松实现对QPS进行限流

2.技术对比

Sentinel Hystrix resilience4j 使用Guava实现
隔离策略 信号量隔离(并发线程数限流) 线程池隔离/信号量隔离 信号量隔离
熔断降级策略 基于响应时间、异常比率、异常数 基于异常比率 基于异常比率、响应时间
实时统计实现 滑动窗口(LeapArray) 滑动窗口(基于 RxJava) Ring Bit Buffer 令牌桶
动态规则配置 支持多种数据源 支持多种数据源 有限支持
扩展性 多个扩展点 插件的形式 接口的形式
基于注解的支持 支持 支持 支持 支持
单机限流 基于 QPS,支持基于调用关系的限流 有限的支持 Rate Limiter 基于 QPS
集群流控 支持 不支持 不支持
流量整形 支持预热模式与匀速排队控制效果 不支持 简单的 Rate Limiter 模式
系统自适应保护 支持 不支持 不支持
热点识别/防护 支持 不支持 不支持
Service Mesh 支持 支持 Envoy/Istio 不支持 不支持
控制台 提供开箱即用的控制台,可配置规则、实时监控、机器发现等 简单的监控查看 不提供控制台,可对接其它监控系统
是否支持默认规则 不支持,需要针对每个接口配置规则 支持 支持
是否支持过滤异常 注解单个接口支持 注解和全局默认配置 注解和全局默认配置

3.应用改造

3.1 sentinel

3.1.1引入依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    <version>2.0.3.RELEASE</version>
</dependency>

3.1.2改造接口或者service层

@SentinelResource(value = "allInfos",fallback = "errorReturn")

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface SentinelResource {
    //资源名称
    String value() default "";

    //流量方向
    EntryType entryType() default EntryType.OUT;

    //资源类型
    int resourceType() default 0;

    //异常处理方法
    String blockHandler() default "";

    //异常处理类
    Class<?>[] blockHandlerClass() default {};

    //熔断方法
    String fallback() default "";

    //默认熔断方法
    String defaultFallback() default "";

    //熔断类
    Class<?>[] fallbackClass() default {};

    //统计异常
    Class<? extends Throwable>[] exceptionsToTrace() default {Throwable.class};

    //忽略异常
    Class<? extends Throwable>[] exceptionsToIgnore() default {};
}
@RequestMapping("/get")
@ResponseBody
@SentinelResource(value = "allInfos",fallback = "errorReturn")
public JsonResult allInfos(HttpServletRequest request, HttpServletResponse response, @RequestParam Integer num){
        try {
            if (num % 2 == 0) {
                log.info("num % 2 == 0");
                throw new BaseException("something bad with 2", 400);
            }
            return JsonResult.ok();
        } catch (ProgramException e) {
            log.info("error");
            return JsonResult.error("error");
        }
    }

3.1.3针对接口配置熔断方法或者限流方法

默认过滤拦截所有Controller接口

/**
     * 限流,参数需要和方法保持一致
     * @param request
     * @param response
     * @param num
     * @return
     * @throws BlockException
     */
    public JsonResult errorReturn(HttpServletRequest request, HttpServletResponse response, @RequestParam Integer num) throws BlockException {
        return JsonResult.error("error 限流" + num );
    }

    /**
     * 熔断,参数需要和方法保持一直,并且需要添加BlockException异常
     * @param request
     * @param response
     * @param num
     * @param b
     * @return
     * @throws BlockException
     */
    public JsonResult errorReturn(HttpServletRequest request, HttpServletResponse response, @RequestParam Integer num,BlockException b) throws BlockException {
        return JsonResult.error("error 熔断" + num );
    }

注意也可以不配置限流或者熔断方法。通过全局异常去捕获UndeclaredThrowableException或者BlockException避免大量的开发量

3.1.4接入dashboard

spring:
  cloud:
    sentinel:
      transport:
        port: 8719
        dashboard: localhost:8080

sentinel

3.1.5 规则持久化和动态更新

接入配置中心如:zookeeper等等,并对规则采用推模式

3.2 hystrix

3.2.1引入依赖

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

3.2.2改造接口

@HystrixCommand(fallbackMethod = "timeOutError")

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface HystrixCommand {
    String groupKey() default "";

    String commandKey() default "";

    String threadPoolKey() default "";

    String fallbackMethod() default "";

    HystrixProperty[] commandProperties() default {};

    HystrixProperty[] threadPoolProperties() default {};

    Class<? extends Throwable>[] ignoreExceptions() default {};

    ObservableExecutionMode observableExecutionMode() default ObservableExecutionMode.EAGER;

    HystrixException[] raiseHystrixExceptions() default {};

    String defaultFallback() default "";
}
@RequestMapping("/get")
@ResponseBody
@HystrixCommand(fallbackMethod = "fallbackMethod")
public JsonResult allInfos(HttpServletRequest request, HttpServletResponse response, @RequestParam Integer num){
    try {
        if (num % 3 == 0) {
            log.info("num % 3 == 0");
            throw new BaseException("something bad whitch 3", 400);
        }

        return JsonResult.ok();
    } catch (ProgramException | InterruptedException exception) {
        log.info("error");
        return JsonResult.error("error");
    }
}

3.2.3针对接口配置熔断方法

/**
 * 该方法是熔断回调方法,参数需要和接口保持一致
 * @param request
 * @param response
 * @param num
 * @return
 */
public JsonResult fallbackMethod(HttpServletRequest request, HttpServletResponse response, @RequestParam Integer num) {
    response.setStatus(500);
    log.info("发生了熔断!!");
    return JsonResult.error("熔断");
}

3.2.4配置默认策略

hystrix:
  command:
    default:
      execution:
        isolation:
          strategy: THREAD
          thread:
            # 线程超时15秒,调用Fallback方法
            timeoutInMilliseconds: 15000
      metrics:
        rollingStats:
          timeInMilliseconds: 15000
      circuitBreaker:
        # 10秒内出现3个以上请求(已临近阀值),并且出错率在50%以上,开启断路器.断开服务,调用Fallback方法
        requestVolumeThreshold: 3
        sleepWindowInMilliseconds: 10000

3.2.5接入监控

hystrix

hystrix示意图

曲线:用来记录2分钟内流量的相对变化,我们可以通过它来观察到流量的上升和下降趋势。

集群监控需要用到注册中心

3.3 resilience4j

3.3.1引入依赖

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

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

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
    <version>1.6.1</version>
</dependency>

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-bulkhead</artifactId>
    <version>1.6.1</version>
</dependency>

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-ratelimiter</artifactId>
    <version>1.6.1</version>
</dependency>

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-timelimiter</artifactId>
    <version>1.6.1</version>
</dependency>

可以按需要引入:bulkhead,ratelimiter,timelimiter等

3.3.2改造接口

@RequestMapping("/get")
@ResponseBody
//@TimeLimiter(name = "BulkheadA",fallbackMethod = "fallbackMethod")
@CircuitBreaker(name = "BulkheadA",fallbackMethod = "fallbackMethod")
@Bulkhead(name = "BulkheadA",fallbackMethod = "fallbackMethod")
public JsonResult allInfos(HttpServletRequest request, HttpServletResponse response, @RequestParam Integer num){
    log.info("param----->" + num);
    try {
        //Thread.sleep(num);

        if (num % 2 == 0) {
            log.info("num % 2 == 0");
            throw new BaseException("something bad with 2", 400);
        }

        if (num % 3 == 0) {
            log.info("num % 3 == 0");
            throw new BaseException("something bad whitch 3", 400);
        }

        if (num % 5 == 0) {
            log.info("num % 5 == 0");
            throw new ProgramException("something bad whitch 5", 400);
        }
        if (num % 7 == 0) {
            log.info("num % 7 == 0");
            int res = 1 / 0;
        }
        return JsonResult.ok();
    } catch (BufferUnderflowException e) {
        log.info("error");
        return JsonResult.error("error");
    }
}

3.3.3 针对接口配置熔断方法

/**
 * 需要参数一致,并且加上相应异常
 * @param request
 * @param response
 * @param num
 * @param exception
 * @return
 */
public JsonResult fallbackMethod(HttpServletRequest request, HttpServletResponse response, @RequestParam Integer num, BulkheadFullException exception) {
    return JsonResult.error("error 熔断" + num );
}

3.3.4配置规则

resilience4j.circuitbreaker:
    instances:
        backendA:
            registerHealthIndicator: true
            slidingWindowSize: 100
        backendB:
            registerHealthIndicator: true
            slidingWindowSize: 10
            permittedNumberOfCallsInHalfOpenState: 3
            slidingWindowType: TIME_BASED
            minimumNumberOfCalls: 20
            waitDurationInOpenState: 50s
            failureRateThreshold: 50
            eventConsumerBufferSize: 10
            recordFailurePredicate: io.github.robwin.exception.RecordFailurePredicate

resilience4j.retry:
    instances:
        backendA:
            maxRetryAttempts: 3
            waitDuration: 10s
            enableExponentialBackoff: true
            exponentialBackoffMultiplier: 2
            retryExceptions:
                - org.springframework.web.client.HttpServerErrorException
                - java.io.IOException
            ignoreExceptions:
                - io.github.robwin.exception.BusinessException
        backendB:
            maxRetryAttempts: 3
            waitDuration: 10s
            retryExceptions:
                - org.springframework.web.client.HttpServerErrorException
                - java.io.IOException
            ignoreExceptions:
                - io.github.robwin.exception.BusinessException

resilience4j.bulkhead:
    instances:
        backendA:
            maxConcurrentCalls: 10
        backendB:
            maxWaitDuration: 10ms
            maxConcurrentCalls: 20

resilience4j.thread-pool-bulkhead:
  instances:
    backendC:
      maxThreadPoolSize: 1
      coreThreadPoolSize: 1
      queueCapacity: 1

resilience4j.ratelimiter:
    instances:
        backendA:
            limitForPeriod: 10
            limitRefreshPeriod: 1s
            timeoutDuration: 0
            registerHealthIndicator: true
            eventConsumerBufferSize: 100
        backendB:
            limitForPeriod: 6
            limitRefreshPeriod: 500ms
            timeoutDuration: 3s

resilience4j.timelimiter:
    instances:
        backendA:
            timeoutDuration: 2s
            cancelRunningFuture: true
        backendB:
            timeoutDuration: 1s
            cancelRunningFuture: false

可以被代码覆盖

3.3.5配置监控

如grafana等

4.关注点

  • 是否需要过滤部分异常
  • 是否需要全局默认规则
  • 可能需要引入其他中间件
  • k8s流量控制
  • 规则存储和动态修改
  • 接入改造代价