08 服务容错-Resilience4j

服务容错和雪崩效应

现有架构总结

Consul->服务发现与配置管理

Ribbon->负载均衡

Feign->代码优雅

本章目标

高并发 -》 服务容错?

雪崩效应

比如存在C->B->A三个微服务,某个时刻微服务A在高并发场景下崩溃,那么微服务B对A的请求就会等待,在java中,每个请求都是一个线程,服务等待,就会导致线程阻塞,直到超时之后才会释放,当在高并发情况下,微服务B会不断创建新的线程,直到资源耗尽,最后导致微服务B崩溃,同理,微服务C也会崩溃,导致整个调用链路中的服务都会崩溃。

我们把基础服务故障导致级联故障的现象称为雪崩效应

也称为“cascading failure”,级联失败、级联失效

服务容错的五种解决方案
  1. 超时 -> 给每个请求分配一个最长的时间 如果超过时间 就释放线程

  2. 限流 -> 只有高并发才会阻塞大量的线程,在大量压测的情况下,设置最大的线程数,也可以防止线程过多导致资源耗尽

  3. 仓壁模式 -> 泰坦尼克号 船舱与船舱进行分开 之间使用钢板焊死 因此一个船舱进水不会导致船的沉没 软件里面的仓壁模式 每个服务使用独立的线程池 互不影响

  4. 断路器模式 -> 当一个服务调用错误率达到了50% 错误次数 20次 则启动断路器模式
    关闭 半开 打开 滑动窗口
    在这里插入图片描述

  5. 重试 -> 不是为了保护自己 而是为了容错去设计的

主要掌握思想

Spring Cloud生态容错组件对比与选择

在这里插入图片描述

使用Resilience4j保护实现容错-限流

Resilience4J是一个轻量级的容错框架,灵感来自于Netflix的Hystrix

https://resilience4j.readme.io/

https://github.com/resilience4j/resilience4j

官方示例:

https://github.com/resilience4j/resilience4j-spring-cloud2-demo

  1. 在课程微服务引入依赖pom
    <dependency>
    	<groupId>io.github.resilience4j</groupId>
    	<artifactId>resilience4j-spring-cloud2</artifactId>
    	<version>1.1.0</version>
    </dependency>
  1. 添加注解@RateLimiter(name = “lessonController”)

可以在指导类或者方法上添加注解,下面在方法上添加注解

    package com.cloud.msclass.controller;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.cloud.msclass.domain.entity.Lesson;
    import com.cloud.msclass.service.LessonService;
    
    import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
    
    @RestController
    @RequestMapping("lesssons")
    public class LessonController {
    
    	@Autowired
    	private LessonService lessonService;
    
    	/**
    	 * http://localhost:8010/lesssons/buy/1
    	 * 购买指定id的课程
    	 * @param id
    	 */
    	@GetMapping("/buy/{id}")
    	@RateLimiter(name = "buyById")
    	public Lesson buyById(@PathVariable Integer id) {
    		return this.lessonService.buyById(id);
    	}
    }
  1. 添加配置
    resilience4j:
      ratelimiter:
        instances:
          # 此处必须与注解中的name一致 否则不会起效
          buyById:
            # 在刷新周期内,请求的最大频次
            limit-for-period: 1
            # 刷新周期
            limit-refresh-period: 1s
            # 线程等待许可的时间 线程不等待 直接抛异常
            timeout-duration: 0

以上配置:1s内只能请求相关服务1次 http://localhost:8010/lesssons/buy/1

启动日志包含如下信息

    2020-02-24 16:22:53.204  INFO 16700 --- [           main] i.g.r.utils.RxJava2OnClasspathCondition  : RxJava2 related Aspect extensions are not activated, because RxJava2 is not on the classpath.
    2020-02-24 16:22:53.206  INFO 16700 --- [           main] i.g.r.utils.ReactorOnClasspathCondition  : Reactor related Aspect extensions are not activated because Resilience4j Reactor module is not on the classpath.

引入以下模块可以解决 但是不引入也不影响使用 所以不引入

    		<dependency>
    			<groupId>io.github.resilience4j</groupId>
    			<artifactId>resilience4j-rxjava2</artifactId>
    			<version>1.2.0</version>
    		</dependency>
    
    		<dependency>
    			<groupId>io.reactivex.rxjava2</groupId>
    			<artifactId>rxjava</artifactId>
    		</dependency>
    
    		<dependency>
    			<groupId>io.github.resilience4j</groupId>
    			<artifactId>resilience4j-reactor</artifactId>
    		</dependency>

快速刷新页面会出现如下的错误:

io.github.resilience4j.ratelimiter.RequestNotPermitted: RateLimiter 'buyById' does not permit further calls

在这里插入图片描述

但是不希望在触发限流的时候不要出现这样的错误页面,而是采取一些其他的策略,在@RateLimiter注解中添加fallbackMethod属性定义即可

    package com.cloud.msclass.controller;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.cloud.msclass.domain.entity.Lesson;
    import com.cloud.msclass.service.LessonService;
    
    import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
    
    @RestController
    @RequestMapping("lesssons")
    public class LessonController {
    
    	private static final Logger logger = LoggerFactory.getLogger(LessonController.class);
    
    	@Autowired
    	private LessonService lessonService;
    
    	/**
    	 * http://localhost:8010/lesssons/buy/1 购买指定id的课程
    	 * 
    	 * @param id
    	 */
    	@GetMapping("/buy/{id}")
    	@RateLimiter(name = "buyById", fallbackMethod = "buyByIdFallBack")
    	public Lesson buyById(@PathVariable Integer id) {
    		return this.lessonService.buyById(id);
    	}
    
    	// 必须与原方法有相同的返回值和参数(侯曼带一个Throwable参数)
    	public Lesson buyByIdFallBack(@PathVariable Integer id, Throwable throwable) {
    		// 表示从本地缓存获取
    		logger.error("发生fallback", throwable);
    		return new Lesson();
    	}
    }

此时如果触发限流则返回如下结果:
在这里插入图片描述

Resilicence44j限流实现

  1. 漏桶算法
  2. 令牌桶算法

io.github.resilience4j.ratelimiter.internal.AtomicRateLimiter 默认 基于令牌桶算法

io.github.resilience4j.ratelimiter.internal.SemaphoreBasedRateLimiter 基于Semaphore类

使用Resilience4j保护实现容错-仓壁模式
    	@GetMapping("/buy/{id}")
    //	@RateLimiter(name = "buyById", fallbackMethod = "buyByIdFallBack")
    	@Bulkhead(name = "buyById", fallbackMethod = "buyByIdFallBack")
    	public Lesson buyById(@PathVariable Integer id) {
    		return this.lessonService.buyById(id);
    	}
    resilience4j:
      ratelimiter:
        instances:
          buyById:
            # 在刷新周期内,请求的最大频次
            limit-for-period: 1
            # 刷新周期
            limit-refresh-period: 1s
            # 线程等待许可的时间 线程不等待 直接抛异常
            timeout-duration: 0
      bulkhead:
        instances:
          buyById:
            # 最大并发请求数
            max-concurrent-calls: 3
            # 仓壁饱和时的最大等待时间 默认0
    #        max-wait-duration: 10ms
            # 事件缓冲区大小
    #        event-consumer-buffer-size: 1

两种实现方式:

Semaphore:每个请求去获取信号量 没有获取到 则拒绝请求

ThreadPool:每个请求去获取线程 如果没有获取到 会进入等待队列 等待队列满了之后 在执行拒绝策略

从性能角度来看 基于Semaphore要比基于线程池要好 如果基于线程池 可能会导致过多的小型的隔离线程池 会导致整个微服务的线程数过多 而线程数过多会导致线程上下文切换过多

默认情况下 是基于Semaphore来实现的,如果要使用基于ThreadPool模式,则按照如下进行设置:

    @Bulkhead(name = "buyById", fallbackMethod = "buyByIdFallBack", type = Type.THREADPOOL)
    resilience4j:
      thread-pool-bulkhead:
        instances:
          buyById:
            # 最大线程池大小
            max-thread-pool-size: 1
            # 核心线程数
            core-thread-pool-size: 1
            # 队列容量 默认100
            queue-capacity: 1
            # 当线程数大于内核数时 多余的空闲线程等待信任无的最长时间 默认20ms
            keep-alive-duration: 20ms
            # 事件缓冲区大小
            event-consumer-buffer-size: 100
// 备注
java.lang.IllegalStateException: ThreadPool bulkhead is only applicable for completable futures 
io.github.resilience4j.bulkhead.internal.SemaphoreBulkhead
io.github.resilience4j.bulkhead.internal.FixedThreadPoolBulkhead
使用Resilience4j保护实现容错-断路器模式
    	/**
    	 * http://localhost:8010/lesssons/buy/1 购买指定id的课程
    	 * 
    	 * @param id
    	 */
    	@GetMapping("/buy/{id}")
    //	@RateLimiter(name = "buyById", fallbackMethod = "buyByIdFallBack")
    //	@Bulkhead(name = "buyById", fallbackMethod = "buyByIdFallBack")
    	@CircuitBreaker(name = "buyById", fallbackMethod = "buyByIdFallBack")
    	public Lesson buyById(@PathVariable Integer id) {
    		return this.lessonService.buyById(id);
    	}

io.github.resilience4j.circuitbreaker.internal.CircuitBreakerStateMachine 基于有限状态机的实现

使用Resilience4j保护实现容错-重试
    @Retry(name = "buyById", fallbackMethod = "buyByIdFallBack")

io.github.resilience4j.retry.internal.RetryImpl

Resilience4j配置管理
  1. 配置可视化
    package com.cloud.msclass.controller;
    
    import java.util.List;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cloud.client.ServiceInstance;
    import org.springframework.cloud.client.discovery.DiscoveryClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import io.github.resilience4j.ratelimiter.RateLimiter;
    import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
    import io.vavr.collection.Seq;
    
    @RestController
    public class TestController {
    
    	@Autowired
    	private DiscoveryClient discoveryClient;
    
    	@GetMapping("/test-discovery")
    	public List<ServiceInstance> testDiscovery() {
    		// 到Consul上查询指定微服务的所有势力
    		return discoveryClient.getInstances("ms-user");
    	}
    
    	@Autowired
    	private RateLimiterRegistry rateLimiterRegistry;
    	
    	/*
    	 * http://localhost:8010/rate-limiter-configs 
    	 */
    	@GetMapping("/rate-limiter-configs")
    	public Seq<RateLimiter> testRateLimiter() {
    		return this.rateLimiterRegistry.getAllRateLimiters();
    	}
    }

发起请求http://localhost:8010/rate-limiter-configs

  1. 默认配置

通过如下配置就可以添加默认设置了

注:对于RateLimiter,当且仅当指定名称的RateLimiter没有任何自定义配置时,名为default的配置才有效

    resilience4j:
      ratelimiter:
        configs:
          default:
            # 在刷新周期内,请求的最大频次
            limit-for-period: 1
            # 刷新周期
            limit-refresh-period: 1s
            # 线程等待许可的时间 线程不等待 直接抛异常
            timeout-duration: 0
  1. 配置刷新

可以将配置存档到consul中进行管理

注解配合使用与执行顺序

在实际项目中,以上四种限流模式可能会混合使用,如果多个注解一起使用时,作用的先后顺序是什么呢?

查看io.github.resilience4j.ratelimiter.configure.RateLimiterAspect类,发现该类实现了Ordered接口,对应的方法为:

    @Override
    public int getOrder() {
       return properties.getRateLimiterAspectOrder();
    }

    private int rateLimiterAspectOrder = Ordered.LOWEST_PRECEDENCE - 1;
    
    int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

通过类似的方法可以获取到所有注解的先后顺序如下:

BulkHead(LOWEST_PRECEDENCE)

RateLimiter(LOWEST_PRECEDENCE - 1)

CircuitBreaker(LOWEST_PRECEDENCE - 2)

Retry(LOWEST_PRECEDENCE - 3

如果不满意上面的顺序,可以自定义顺序(bulk不满足自定义顺序):

    resilience4j:
      retry:
        retry-aspect-order: 1
      circuitbreaker:
        circuit-breaker-aspect-order: 2
      ratelimiter:
        rate-limiter-aspect-order: 3
Feign与Resilience4j配合使用

在feign对应接口中添加Resilience4j注解即可

本章总结
  1. 服务容错的常见思路

  2. 常用玩法及相关策略

  3. 监控

发布了21 篇原创文章 · 获赞 1 · 访问量 337

猜你喜欢

转载自blog.csdn.net/m0_37607945/article/details/104482625