服务容错和雪崩效应
现有架构总结
Consul->服务发现与配置管理
Ribbon->负载均衡
Feign->代码优雅
本章目标
高并发 -》 服务容错?
雪崩效应
比如存在C->B->A三个微服务,某个时刻微服务A在高并发场景下崩溃,那么微服务B对A的请求就会等待,在java中,每个请求都是一个线程,服务等待,就会导致线程阻塞,直到超时之后才会释放,当在高并发情况下,微服务B会不断创建新的线程,直到资源耗尽,最后导致微服务B崩溃,同理,微服务C也会崩溃,导致整个调用链路中的服务都会崩溃。
我们把基础服务故障导致级联故障的现象称为雪崩效应
也称为“cascading failure”,级联失败、级联失效
服务容错的五种解决方案
-
超时 -> 给每个请求分配一个最长的时间 如果超过时间 就释放线程
-
限流 -> 只有高并发才会阻塞大量的线程,在大量压测的情况下,设置最大的线程数,也可以防止线程过多导致资源耗尽
-
仓壁模式 -> 泰坦尼克号 船舱与船舱进行分开 之间使用钢板焊死 因此一个船舱进水不会导致船的沉没 软件里面的仓壁模式 每个服务使用独立的线程池 互不影响
-
断路器模式 -> 当一个服务调用错误率达到了50% 错误次数 20次 则启动断路器模式
关闭 半开 打开 滑动窗口
-
重试 -> 不是为了保护自己 而是为了容错去设计的
主要掌握思想
Spring Cloud生态容错组件对比与选择
使用Resilience4j保护实现容错-限流
Resilience4J是一个轻量级的容错框架,灵感来自于Netflix的Hystrix
https://resilience4j.readme.io/
https://github.com/resilience4j/resilience4j
官方示例:
https://github.com/resilience4j/resilience4j-spring-cloud2-demo
- 在课程微服务引入依赖pom
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-cloud2</artifactId>
<version>1.1.0</version>
</dependency>
- 添加注解@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);
}
}
- 添加配置
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限流实现
- 漏桶算法
- 令牌桶算法
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配置管理
- 配置可视化
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
- 默认配置
通过如下配置就可以添加默认设置了
注:对于RateLimiter,当且仅当指定名称的RateLimiter没有任何自定义配置时,名为default的配置才有效
resilience4j:
ratelimiter:
configs:
default:
# 在刷新周期内,请求的最大频次
limit-for-period: 1
# 刷新周期
limit-refresh-period: 1s
# 线程等待许可的时间 线程不等待 直接抛异常
timeout-duration: 0
- 配置刷新
可以将配置存档到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注解即可
本章总结
-
服务容错的常见思路
-
常用玩法及相关策略
-
监控