限流、熔断、降级、线程池隔离

一、限流

1.1 常见限流方式

1.1.1 固定窗口、滑动窗口、漏斗、令牌桶

四种常见限流方式

1.1.2 令牌桶限流原理-公式

令牌桶

1.2Guava令牌桶使用方式

Guava-RateLimiter

1.3 其他限流

流控规则配置

集群非精确 + 720次/s (限流周期1s、限流次数720)、阻塞类型:不限时阻塞 + 突发流量1s(缓存一定时间的阈值,应对流量蜂刺)、返回码429、返回信息:导出查询接口被限流

补充详解策略

序号策略类型含义备注极端场景
1集群非精确限流限流策略:5s/一个请求。5台机器平均每个机器0.2个请求/s令牌是会先通过当前的,再阻塞未来的,所以限流不准用户A和B,同一时刻或者5s之内,两次访问请求都可以打进去接口
2集群精确限流限流策略:5s/一个请求 ,即整个集群,5s内只允许通过一个请求5s内,整个集群只会通过1次请求
阻塞类型
序号策略类型使用场景极端场景备注
1限时阻塞集群非精确限流
2不限时阻塞会阻塞线程100qps,5台机器。每台处理20QPS300请求打过来,极端场景全部打到同一个机器A。A,1s只能处理20请求,300请求需要15s,也就是,最后一个线程可能等待15s才能获得令牌执行线程因为限流线程等待了近10s
突发流量

1、定义:

在突发模式中,Guava限流器的桶中令牌是有一个有效期的,有效期的作用是让限流器具有一定的“弹性”,可以根据空闲情况临时超额放行一些请求用于平滑处理突发流量。

2、设置值:1

即保留上1s内,剩下的所有令牌。给下一秒用。

eg:1s生产100令牌。12:00:00没有请求进来,12:00:01的时候,现有可用令牌数量 = 本秒的100 + 上一秒的100 = 200。所以,即时限制了100QPS,在12:00:01的时候,也可能有200个请求打进来

踩坑1: 使用限流器后实际流量总是超过配置阈值

**解释:**这种情况只可能发生下突发模式下,是由2.3小节中介绍的突发流量处理机制导致的。突发模式限流器在向请求发放令牌包括存量令牌与新令牌,新令牌的生成速度等于限流速度,而超额部分的请求来自于存量令牌。在实际流量超过阈值不多的情况下,令牌桶中的令牌需要很长时间才能被耗尽。

踩坑2:在客户端匀速调用的场景中,服务端使用了限流器后发现实际流量无法达到阈值上限【很少见】

**解释:**这种情况是非常偶然的,实际是由于限流配置不当导致的。假设某个客户端以50QPS的速度发出请求,即每20ms一个,服务端的限流器配置为30QPS,且不支持突发流量(突发模式下设置突发时间为0,或者使用了预热模式)。这种情况下服务端会严格按照每30+ms一个的速度接收请求,因此,客户端在20ms之后发出第二个请求时,服务端尚未满足30ms的间隔时间,就出现了每两个请求就有一个被拒绝的现象。读者可参考下图理解这一现象。

**建议:**在遇到这种问题时首先考虑限流阈值是否合理。其次,如果在服务端使用突发模式限流,尽量不要把突发时间(maxBrustSeconds)设置为0。如果使用预热模式限流,应该参考服务容量,配置一个足够大的限流阈值。

限流并且拒绝流量请求,友好提示限流了

自定义blockHandle处理方法

方法的入参和出参 和 限流方法的一致

    public ExportDataTResponse blockHandler(Request request, Operator operator) {
        ExportDataTResponse response = new ExportDataTResponse();
        response.setCode(429);
        response.setMsg("当前有用户正在操作导出,请 3 s后重试");
        response.setData("");
        return response;
    }

二、 熔断

1.1 Thfirt熔断器

1、默认配置

  • 请求试探窗口:5s(心跳机制)
  • 恢复策略:正常(立即、限时)
  • 降级方法:自定义降级方法(返回常量、抛出异常、脚本)
    @Degrade(key = "自定义key", fallBackMethod = "fallBack")
    public int getPrice(int param) {
        //方法内部不要捕获异常,熔断器通过异常判断调用结果
        int n = random.nextInt(10000);
        int m = 1;
        if (n > 9900) {
            m = 1 / 0;
        }
        return m;
    }
	
	 // 降级方法,参数和主方法保持一致,注意降级方法必须是public,否则无法被cglib代理增强
    public int fallBack(int param) {
        return 0;
    }
  • 统计窗口:10s
  • 请求总数20
  • 失败率:50%
  • 失败数:2

三、 线程池隔离

3.1 背景

  • 如果某个接口的QPS过高,可能会影响服务提供其他功能接口。
  • 因为thrift单机默认256个工作线程,如果某接口的性能不好,QPS又高,则某个时间点,工作线程可能都被这个接口占用了,导致其他接口不可用。这时候我们就可以使用线程池隔离,动态!!!的给这个接口分配线程池。当超过一定的工作线程,则采用拒绝策略或直接抛出异常,再结合熔断降级方法,10s内20次调用超过50%的失败率,则这个接口会被降级,调用降级方法fallbackMethod。
  • 然后5s之后,心跳机制会去验证这个接口是否恢复正常,如果恢复正常,则接口限时|正常 节奏恢复可用,否则再等5s

3.2 原理

/**
 * 线程池隔离原理:
 * 1、创建多个线程池-ExecutorService-ES实例
 *    1.1 针对不同类型的任务(IO密集型任务和cpu密集型任务),分别创建多个线程池ES实例;
 *    1.2 每个ES实例都具有独立的的线程数、队列大小、线程工厂等属性资源,以支持不同类型任务的执行
 *        这里是通过Executors.newFixedThreadPool方法,其内部是new ThreadPoolExecutor来设置不同的线程数、队列大小、线程池工厂等属性,以满足不同类型任务的执行需求
 *    1.3 使用适当的线程工厂:为避免线程之间的类加载器干扰,选择合适线程工厂来创建线程;
 *        1) 此处为每个ES实例设置不同的线程工厂,这样就可以为不同的ES实例绑定不同的ClassLoader,从而避免线程之间的类加载器干扰;
 *        3) 线程间的类加载干扰:当多个线程同时执行时,可能会共享类A,在某些情况下可能会导致类A加载错误、类型转换异常等问题;
 *        eg:线程A使用系统ClassLoader加载了一个类,而线程B使用自定义ClassLoader重新加载该类,就可能导致类加载失败或者类型转换异常.
 * 2、提交任务到不同的ExecutorService实例
 *    2.1 使用submit()或execute()方法将不同类型的任务提交到相应的ES实例中进行处理。
 *    2.2 最终通过使用不同的ES实例,实现隔离不同类型的任务,避免相互之间的影响和干扰;
 */
public class ThreadPoolIsolation {
    // 创建IO密集型任务专用的线程池
    private static ExecutorService ioThreadPool = Executors.newCachedThreadPool(new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setDaemon(true);  // 设置为守护线程
            return t;
        }
    });

    // 创建计算密集型任务专用的线程池
    private static ExecutorService cpuThreadPool = Executors.newFixedThreadPool(4, new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setPriority(Thread.MAX_PRIORITY);  // 设置为最高优先级
            return t;
        }
    });

    // 提交IO密集型任务到线程池中执行
    public static void submitIOJob(Runnable job) {
        ioThreadPool.submit(job);
    }

    // 提交计算密集型任务到线程池中执行
    public static void submitCPUJob(Runnable job) {
        cpuThreadPool.submit(job);
    }
}

3.3 使用

  • 线程池隔离 + 降级熔断方法
  • 同理,限流也可以接口熔断降级方法一起使用
@ThreadPoolExecute(key = "method.querySkuStock", coreSize = 15, maxSize = 30, maxQueueSize = 500, rejectHandler =
            ThreadPoolExecutor.CallerRunsPolicy.class)
@Degrade(key = "asyncMethod", fallBackMethod = "fallbackMethod")
public Future<String> asyncMethod() {
    return new AsyncResult<String>() {
        @Override
        public String invoke() throws Exception {
            TimeUnit.MILLISECONDS.sleep(500);
            return "asyncMethod";
        }
    };
}

public String fallbackMethod(Throwable e) {
     //do fallback business
    return "fallback";
}
  • 25
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值