前言
分布式环境下, 服务直接相互调用, 一个复杂的业务可能要调用多个服务, 例如 A -> B -> C -> D, 当某个服务出现异常 (调用超时, 调用失败等) 将导致整个流程阻塞崩溃, 严重的整个系统都会崩掉, 为了实现高可用, 必要的保护机制必不可少
本文记录限流, 熔断, 降级的实现处理
限流
我们采用令牌桶限流法, 并自己实现一个简单令牌桶限流
有个任务线程以恒定速率向令牌桶添加令牌
一个请求会消耗一个令牌, 令牌桶里的令牌大于 0, 才会放行, 反正不允许通过
- /**
- * 简单的令牌桶限流
- */
- public class RateLimiter {
- /**
- * 桶的大小
- */
- private Integer limit;
- /**
- * 桶当前的 token
- */
- private static Integer tokens = 0;
- /**
- * 构造参数
- */
- public RateLimiter(Integer limit, Integer speed){
- // 初始化桶的大小, 且桶一开始是满的
- this.limit = limit;
- tokens = this.limit;
- // 任务线程: 每秒新增 speed 个令牌
- new Thread(() ->{
- while (true){
- try {
- Thread.sleep(1000L);
- int newTokens = tokens + speed;
- if(newTokens> limit){
- tokens = limit;
- System.out.println("令牌桶满了!!!");
- }else{
- tokens = newTokens;
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }).start();
- }
- /**
- * 根据令牌数判断是否允许执行, 需要加锁
- */
- public synchronized boolean execute() {
- if (tokens> 0) {
- tokens = tokens - 1;
- return true;
- }
- return false;
- }
- }
main 简单测试
- public static void main(String[] args) {
- // 令牌桶限流: 峰值每秒可以处理 10 个请求, 正常每秒可以处理 3 个请求
- RateLimiter rateLimiter = new RateLimiter(10, 3);
- // 模拟请求
- while (true){
- // 在控制台输入一个值按回车, 相对于发起一次请求
- Scanner scanner = new Scanner(System.in);
- scanner.next();
- // 令牌桶返回 true 或者 false
- if(rateLimiter.execute()){
- System.out.println("允许访问");
- }else{
- System.err.println("禁止访问");
- }
- }
- }
在 SpringCloud 分布式下实现限流, 需要把令牌桶的维护放到一个公共的地方, 比如 Zuul 路由, 当然也可以同时针对具体的每个服务进行单独限流
另外, guava 里有现成的基于令牌桶的限流实现, 引入
- <dependency>
- <groupId>com.google.guava</groupId>
- <artifactId>guava</artifactId>
- <version>26.0-jre</version>
- </dependency>
具体用法这里就不阐述了
我们找出之前的 springcloud 项目, 在 zuul-server 中的 AccessFilter 过滤器进行限流, 其他的都不变, 只需要做如下修改
PS: 我这里为了方便测试, 调小了令牌桶的大小, 跟速率, 正常情况下要服务器的承受能力来定
- /**
- * Zuul 过滤器, 实现了路由检查
- */
- public class AccessFilter extends ZuulFilter {
- // 令牌桶限流: 峰值每秒可以处理 10 个请求, 正常每秒可以处理 3 个请求
- //PS: 我这里为了方便测试, 调小了令牌桶的大小, 跟速率, 正常情况下按服务器的承受能力来定
- private RateLimiter rateLimiter = new RateLimiter(2, 1);
- // 业务不变, 省略其他代码...
- /**
- * 过滤器的具体逻辑
- */
- @Override
- public Object run() {
- RequestContext ctx = RequestContext.getCurrentContext();
- HttpServletRequest request = ctx.getRequest();
- HttpServletResponse response = ctx.getResponse();
- // 限流
- if(!rateLimiter.execute()){
- try {
- ctx.setSendZuulResponse(false);
- ctx.setResponseStatusCode(200);
- // 直接写入浏览器
- response.setContentType("text/html;charset=UTF-8");
- PrintWriter writer = response.getWriter();
- writer.println("系统繁忙, 请稍后在试!<br/>System busy, please try again later!");
- writer.flush();return null;
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- // 业务不变, 省略其他代码..
- }
- }
按照我们设置的值, 一秒能处理一个请求, 峰值一秒能处理两个请求, 下面疯狂刷新进行测试
熔断
YAML 配置开启 Hystrix 熔断功能, 进行容错处理
- feign:
- hystrix:
- enabled: true
设置 Hystrix 的 time-out 时间
- hystrix:
- command:
- default:
- execution:
- isolation:
- thread:
- timeoutInMilliseconds: 5000 #毫秒
- #或者设置从不超时
- #timeout:
- # enabled: false
在使用 Feign 调用服务提供者时配置 @FeignClient 的 fallback, 进行容错处理(服务提供者发生异常), 如果需要获取到异常信息, 则要配置 fallbackFactory<T>
- @FeignClient(name = "sso-server", path = "/",/*fallback = SsoFeign.SsoFeignFallback.class,*/fallbackFactory = SsoFeign.SsoFeignFallbackFactory.class)
- /**
- * 容错处理(服务提供者发生异常, 将会进入这里)
- */
- @Component
- public class SsoFeignFallback implements SsoFeign {
- @Override
- public Boolean hasKey(String key) {
- System.out.println("调用 sso-server 失败, 进行 SsoFeignFallback.hasKey 处理: return false;");
- return false;
- }
- }
- /**
- * 只打印异常, 容错处理仍交给 SsoFeignFallback
- */
- @Component
- public class SsoFeignFallbackFactory implements FallbackFactory<SsoFeign> {
- private final SsoFeignFallback ssoFeignFallback;
- public SsoFeignFallbackFactory(SsoFeignFallback ssoFeignFallback) {
- this.ssoFeignFallback = ssoFeignFallback;
- }
- @Override
- public SsoFeign create(Throwable cause) {
- cause.printStackTrace();
- return ssoFeignFallback;
- }
- }
FallbackFactory 也可以这样写
- /**
- * 容错处理
- */
- @Component
- public class SsoFeignFallbackFactory implements FallbackFactory<SsoFeign> {
- @Override
- public SsoFeign create(Throwable cause) {
- // 打印异常
- cause.printStackTrace();
- return new SsoFeign() {
- @Override
- public Boolean hasKey(String key) {
- System.out.println("调用 sso-server 失败: return false;");
- return false;
- }
- };
- }
- }
因为我们没有启动 Redis, 报错, 但我们进行容错处理, 所以还是返回了 false
降级
当调用服务发送异常, 容错处理的方式有多种, 我们可以:
1, 重连, 比如服务进行了限流, 本次连接被限制, 重连一次或 N 次就可以得到数据
2, 直接返回一个友好提示
3, 降级调用备用服务, 返回缓存的数据等
后记
降级也可以叫做 "备胎计划"...
来源: https://www.cnblogs.com/huanzi-qch/p/11053061.html