文章目录
一、限流
1.1 常见限流方式
1.1.1 固定窗口、滑动窗口、漏斗、令牌桶
1.1.2 令牌桶限流原理-公式
1.2Guava令牌桶使用方式
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";
}