场景:
各种微服务之间进行调用时,有可能会调用到一写故障的微服务,此时可以用CircuitBreaker进行断路等操作,保证系统正常运行
基础知识:
熔断(CircuitBreaker)(服务熔断和服务降级)
步骤一:导入依赖
<!--resilience4j-circuitbreaker-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<!-- 由于断路保护等需要AOP实现,所以必须导入AOP包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
步骤二:编写配置文件
按次数 # Resilience4j CircuitBreaker 按照次数:_BASEDCOUNT 的例子 # 6次访问中当执行方法的失败率达到50%时CircuitBreaker将进入开启OPEN状态(保险丝跳闸断电)拒绝所有请求。 # 等待5秒后,CircuitBreaker 将自动从开启OPEN状态过渡到半开HALF_OPEN状态,允许一些请求通过以测试服务是否恢复正常。 # 如还是异常CircuitBreaker 将重新进入开启OPEN状态;如正常将进入关闭CLOSE闭合状态恢复正常处理请求。 #resilience4j: # circuitbreaker: # configs: # 编译默认配置文件 # default: # failureRateThreshold: 50 #设置50%的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。 # slidingWindowType: COUNT_BASED # 滑动窗口的类型 # slidingWindowSize: 6 #滑动窗⼝的⼤⼩配置COUNT_BASED表示6个请求,配置TIME_BASED表示6秒 # minimumNumberOfCalls: 6 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。如果minimumNumberOfCalls为10,则必须最少记录10个样本,然后才能计算失败率。如果只记录了9次调用,即使所有9次调用都失败,断路器也不会开启。 # automaticTransitionFromOpenToHalfOpenEnabled: true # 是否启用自动从开启状态过渡到半开状态,默认值为true。如果启用,CircuitBreaker将自动从开启状态过渡到半开状态,并允许一些请求通过以测试服务是否恢复正常 # waitDurationInOpenState: 5s #从OPEN到HALF_OPEN状态需要等待的时间 # permittedNumberOfCallsInHalfOpenState: 2 #半开状态允许的最大请求数,默认值为10。在半开状态下,CircuitBreaker将允许最多permittedNumberOfCallsInHalfOpenState个请求通过,如果其中有任何一个请求失败,CircuitBreaker将重新进入开启状态。 # recordExceptions: # - java.lang.Exception # 作用于consul上的服务 # instances: # cloud-payment-service: # baseConfig: default按时间
# Resilience4j CircuitBreaker 按照时间:TIME_BASED 的例子 #resilience4j: # timelimiter: # configs: # default: # timeout-duration: 10s #神坑的位置,timelimiter 默认限制远程1s,超于1s就超时异常,配置了降级,就走降级逻辑 # circuitbreaker: # configs: # default: # failureRateThreshold: 50 #设置50%的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。 # slowCallDurationThreshold: 2s #慢调用时间阈值,高于这个阈值的视为慢调用并增加慢调用比例。 # slowCallRateThreshold: 30 #慢调用百分比峰值,断路器把调用时间⼤于slowCallDurationThreshold,视为慢调用,当慢调用比例高于阈值,断路器打开,并开启服务降级 # slidingWindowType: TIME_BASED # 滑动窗口的类型 # slidingWindowSize: 2 #滑动窗口的大小配置,配置TIME_BASED表示2秒 # minimumNumberOfCalls: 2 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。 # permittedNumberOfCallsInHalfOpenState: 2 #半开状态允许的最大请求数,默认值为10。 # waitDurationInOpenState: 5s #从OPEN到HALF_OPEN状态需要等待的时间 # recordExceptions: # - java.lang.Exception # instances: # cloud-payment-service: # baseConfig: default
步骤三:编写controller
被调用者 //=========Resilience4j bulkhead 的例子 @GetMapping(value = "/pay/bulkhead/{id}") public String myBulkhead(@PathVariable("id") Integer id) { if(id == -4) throw new RuntimeException("----bulkhead id 不能-4"); if(id == 9999) { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } return "Hello, bulkhead! inputId: "+id+" \t " + IdUtil.simpleUUID(); }中间PayFeignApi @GetMapping(value = "/pay/circuit/{id}") public String myCircuit(@PathVariable("id") Integer id);调用者 @RestController public class OrderCircuitController { @Resource private PayFeignApi payFeignApi; @GetMapping(value = "/feign/pay/circuit/{id}") //添加该注解,name指定该类要掉用的微服务(与配置文件内的设置的服务名相同) //fallbackMethod指定,服务降级后的兜底处理方法 @CircuitBreaker(name = "cloud-payment-service", fallbackMethod = "myCircuitFallback") public String myCircuitBreaker(@PathVariable("id") Integer id) { return payFeignApi.myCircuit(id); } //myCircuitFallback就是服务降级后的兜底处理方法 public String myCircuitFallback(Integer id,Throwable t) { // 这里是容错处理逻辑,返回备用结果 return "myCircuitFallback,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~"; }按次数总结:
滑动窗口长度为6,样本数为6,运行6次操作,3对3错,
达到最小样本数,触发断路器检查机制,发现错误率达到30%
触发开路熔断,进行服务降级,触发保底方法
5秒后,变为半开路状态,开始小心试探,
隔段时间,放2个过去,只要有一个失败,就变回开路
往复循环
按时间总结:
滑动窗口长度为2秒,样本数为2,请求超过两2秒为慢查询,
进行2次操作,一个5秒(慢查询),一个1秒(正常)
达到最小样本数,触发断路器检查机制,两个请求都成功,
但其中慢查询百分比超过30%
触发开路熔断,进行服务降级,触发保底方法
5秒后,变为半开路状态,开始小心试探,
隔段时间,放2个过去,只要有一个失败,就变回开路
往复循环
隔离(BulkHead)
步骤一:导入依赖
<!--resilience4j-bulkhead-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</dependency>
步骤二:编写配置文件
信号量
####resilience4j bulkhead 的例子
resilience4j:
bulkhead:
configs:#编写默认配置文件
default:
maxConcurrentCalls: 2 # 隔离允许并发线程执行的最大数量
maxWaitDuration: 1s # 当达到并发调用数量时,新的线程的阻塞时间,我只愿意等待1秒,过时不候进舱壁兜底fallback
instances:#作用于服务
cloud-payment-service:
baseConfig: default
timelimiter:
configs:
default:
timeout-duration: 20s#这个配置项定义了操作的超时时间。在这个例子中,超时时间被设置为20s
,意味着任何超过 20 秒的操作都会被 TimeLimiter 拦截,并抛出一个TimeoutException
线程池
####resilience4j bulkhead -THREADPOOL的例子 #resilience4j: # timelimiter: # configs: # default: # timeout-duration: 10s #timelimiter默认限制远程1s,超过报错不好演示效果所以加上10秒 # thread-pool-bulkhead: # configs: # default: # core-thread-pool-size: 1 #允许最大并发量 # max-thread-pool-size: 1 #线程池就一个位 # queue-capacity: 1 #只允许一个人排队 # instances: # cloud-payment-service: # baseConfig: default # spring.cloud.openfeign.circuitbreaker.group.enabled 请设置为false 新启线程和原来主线程脱离
步骤三:编写Controller
被调用者
//=========Resilience4j bulkhead 的例子
@GetMapping(value = "/pay/bulkhead/{id}")
public String myBulkhead(@PathVariable("id") Integer id)
{
if(id == -4) throw new RuntimeException("----bulkhead id 不能-4");
if(id == 9999)
{
try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
}
return "Hello, bulkhead! inputId: "+id+" \t " + IdUtil.simpleUUID();
}中间PayFeignApi
/**
* Resilience4j Bulkhead 的例子
* @param id
* @return
*/
@GetMapping(value = "/pay/bulkhead/{id}")
public String myBulkhead(@PathVariable("id") Integer id);调用者
/** * (船的)舱壁,隔离 * * @param id * @return */ @GetMapping(value = "/feign/pay/bulkhead/{id}") 信号量 //@Bulkhead(name = "cloud-payment-service", fallbackMethod = "myBulkheadFallback", //type = Bulkhead.Type.SEMAPHORE) 线程池 @Bulkhead(name = "cloud-payment-service", fallbackMethod = "myBulkheadFallback", type = Bulkhead.Type.THREADPOOL) public String myBulkhead(@PathVariable("id") Integer id) { return payFeignApi.myBulkhead(id); } public String myBulkheadFallback(Throwable t) { return "myBulkheadFallback,隔板超出最大数量限制,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~"; }信号量总结:
就是抢车位,有位就进去,没位就等1秒,
1秒后,如果还是没位,直接触发降级
timeout-duration: 20s 防止僵尸车一直占着
线程池总结:
固定线程池舱壁(FixedThreadPoolBulkhead)
FixedThreadPoolBulkhead的功能与SemaphoreBulkhead一样也是用于限制并发执行的次数的,但是二者的实现原理存在差别而且表现效果也存在细微的差别。FixedThreadPoolBulkhead使用一个固定线程池和一个等待队列来实现舱壁。
当线程池中存在空闲时,则此时进入系统的请求将直接进入线程池开启新线程或使用空闲线程来处理请求。
当线程池中无空闲时时,接下来的请求将进入等待队列,
若等待队列仍然无剩余空间时接下来的请求将直接被拒绝,
在队列中的请求等待线程池出现空闲时,将进入线程池进行业务处理。
另外:ThreadPoolBulkhead只对CompletableFuture方法有效,所以我们必创建返回CompletableFuture类型的方法
限流(RateLimiter)
步骤一:导入依赖
<!--resilience4j-ratelimiter-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
</dependency>
步骤二:编写配置文件
####resilience4j ratelimiter 限流的例子
resilience4j:
ratelimiter:
configs:
default:
limitForPeriod: 2 #在一次刷新周期内,允许执行的最大请求数
limitRefreshPeriod: 1s # 限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量重置为limitForPeriod
timeout-duration: 1 # 线程等待权限的默认等待时间
instances:
cloud-payment-service:
baseConfig: default
步骤三:编写Controller
被调用者
//=========Resilience4j ratelimit 的例子
@GetMapping(value = "/pay/ratelimit/{id}")
public String myRatelimit(@PathVariable("id") Integer id)
{
return "Hello, myRatelimit欢迎到来 inputId: "+id+" \t " + IdUtil.simpleUUID();
}中间PayFeignApi
/**
* Resilience4j Ratelimit 的例子
* @param id
* @return
*/
@GetMapping(value = "/pay/ratelimit/{id}")
public String myRatelimit(@PathVariable("id") Integer id);调用者
@GetMapping(value = "/feign/pay/ratelimit/{id}")
@RateLimiter(name = "cloud-payment-service",fallbackMethod = "myRatelimitFallback")
public String myBulkhead(@PathVariable("id") Integer id)
{
return payFeignApi.myRatelimit(id);
}
public String myRatelimitFallback(Integer id,Throwable t)
{
return "你被限流了,禁止访问/(ㄒoㄒ)/~~";
}