共计 6064 个字符,预计需要花费 16 分钟才能阅读完成。
有读者说本人加入秋招时筹备的我的项目是秒杀零碎,他在 Redis 和 MySQL 的设计上筹备了很多,然而每次面试偏偏面试官先问他怎么限流。限流他又没筹备,答复的很不条理,刚面试开始本人就慌了。
其实在理论的秒杀零碎中,限流是特地重要的,所以面试官也特地留神这方面的问题。明天看到一篇很零碎的解说限流的文章,一起来学习下。
为什么要限流
日常生活中, 有哪些须要限流的中央?
像我旁边有一个国家景区, 平时可能基本没什么人返回, 然而一到五一或者春节就人满为患, 这时候景区管理人员就会履行一系列的政策来限度进入人流量, 为什么要限流呢? 如果景区能包容一万人, 当初进去了三万人, 势必人山人海, 整不好还会有事变产生, 这样的后果就是所有人的体验都不好, 如果产生了事变景区可能还要敞开, 导致对外不可用, 这样的结果就是所有人都感觉体验蹩脚透了。
限流的思维就是, 在保障可用的状况下尽可能多减少进入的人数, 其余的人在里面排队期待, 保障外面的一万人能够失常玩耍。
回到网络上, 同样也是这个情理, 例如某某明星颁布了恋情, 拜访从平时的 50 万减少到了 500 万, 零碎最多能够撑持 200 万拜访, 那么就要执行限流规定, 保障是一个可用的状态, 不至于服务器解体导致所有申请不可用。
限流思路
对系统服务进行限流,个别有如下几个模式:
熔断
零碎在设计之初就把熔断措施思考进去。当零碎呈现问题时,如果短时间内无奈修复,零碎要主动做出判断,开启熔断开关,回绝流量拜访,防止大流量对后端的过载申请。
零碎也应该可能动静监测后端程序的修复状况,当程序已复原稳固时,能够敞开熔断开关,恢复正常服务。常见的熔断组件有 Hystrix 以及阿里的 Sentinel,两种互有优缺点,能够依据业务的理论状况进行抉择。
服务降级
将零碎的所有性能服务进行一个分级,当零碎呈现问题须要紧急限流时,可将不是那么重要的性能进行降级解决,进行服务,这样能够开释出更多的资源供应外围性能的去用。
例如在电商平台中,如果突发流量激增,可长期将商品评论、积分等非核心性能进行降级,进行这些服务,开释出机器和 CPU 等资源来保障用户失常下单,而这些降级的性能服务能够等整个零碎恢复正常后,再来启动,进行补单 / 弥补解决。除了性能降级以外,还能够采纳不间接操作数据库,而全副读缓存、写缓存的形式作为长期降级计划。
提早解决
这个模式须要在零碎的前端设置一个流量缓冲池,将所有的申请全副缓冲进这个池子,不立刻解决。而后后端真正的业务处理程序从这个池子中取出申请顺次解决,常见的能够用队列模式来实现。这就相当于用异步的形式去缩小了后端的解决压力,然而当流量较大时,后端的解决能力无限,缓冲池里的申请可能解决不及时,会有肯定水平提早。前面具体的漏桶算法以及令牌桶算法就是这个思路。
特权解决
这个模式须要将用户进行分类,通过预设的分类,让零碎优先解决须要高保障的用户群体,其它用户群的申请就会提早解决或者间接不解决。
缓存、降级、限流区别
缓存,是用来减少零碎吞吐量,晋升访问速度提供高并发。
降级,是在零碎某些服务组件不可用的时候、流量暴增、资源耗尽等状况下,临时屏蔽掉出问题的服务,持续提供降级服务,给用户尽可能的敌对提醒,返回兜底数据,不会影响整体业务流程,待问题解决再从新上线服务
限流,是指在应用缓存和降级有效的场景。比方当达到阈值后限度接口调用频率,拜访次数,库存个数等,在呈现服务不可用之前,提前把服务降级。只服务好一部分用户。
限流的算法
限流算法很多, 常见的有三类, 别离是计数器算法、漏桶算法、令牌桶算法, 上面逐个解说。
计数器算法
简略粗犷, 比方指定线程池大小,指定数据库连接池大小、nginx 连接数等, 这都属于计数器算法。
计数器算法是限流算法里最简略也是最容易实现的一种算法。举个例子, 比方咱们规定对于 A 接口,咱们 1 分钟的拜访次数不能超过 100 个。那么咱们能够这么做:在一开 始的时候,咱们能够设置一个计数器 counter,每当一个申请过去的时候,counter 就加 1,如果 counter 的值大于 100 并且该申请与第一个申请的间隔时间还在 1 分钟之内,那么阐明申请数过多, 回绝拜访;如果该申请与第一个申请的间隔时间大于 1 分钟,且 counter 的值还在限流范畴内,那么就重置 counter, 就是这么简略粗犷。
漏桶算法
漏桶算法思路很简略,水(申请)先进入到漏桶里,漏桶以肯定的速度出水,当水流入速度过大会超过桶可接收的容量时间接溢出,能够看出漏桶算法能强行限度数据的传输速率。
这样做的益处是:
削峰: 有大量流量进入时, 会产生溢出, 从而限流爱护服务可用
缓冲: 不至于间接申请到服务器, 缓冲压力 生产速度固定 因为计算性能固定
令牌桶算法
令牌桶与漏桶类似, 不同的是令牌桶桶中放了一些令牌, 服务申请达到后, 要获取令牌之后才会失去服务, 举个例子, 咱们平时去食堂吃饭, 都是在食堂内窗口前排队的, 这就好比是漏桶算法, 大量的人员汇集在食堂内窗口外, 以肯定的速度享受服务, 如果涌进来的人太多, 食堂装不下了, 可能就有一部分人站到食堂外了, 这就没有享受到食堂的服务, 称之为溢出, 溢出能够持续申请, 也就是持续排队, 那么这样有什么问题呢?
如果这时候有非凡状况, 比方有些赶时间的志愿者啦、或者高三要高考啦, 这种状况就是突发状况, 如果也用漏桶算法那也得缓缓排队, 这也就没有解决咱们的需要, 对于很多利用场景来说,除了要求可能限度数据的均匀传输速率外,还要求容许某种程度的突发传输。这时候漏桶算法可能就不适合了,令牌桶算法更为适宜。如图所示,令牌桶算法的原理是零碎会以一个恒定的速度往桶里放入令牌,而如果申请须要被解决,则须要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
令牌桶益处就是, 如果某一瞬间访问量剧增或者有突发状况, 能够通过扭转桶中令牌数量来扭转连接数, 就好比那个食堂排队吃饭的问题, 如果当初不是间接去窗口排队, 而是先来楼外拿饭票而后再去排队, 那么有高三的学生时能够将减少饭票数量或者优先将令牌给高三的学生, 这样比漏桶算法更加灵便。
并发限流
简略来说就是设置零碎阈值总的 QPS 个数, 这些也挺常见的, 就拿 Tomcat 来说, 很多参数就是出于这个思考, 例如
配置的 acceptCount
设置响应连接数, maxConnections
设置刹时最大连接数, maxThreads
设置最大线程数, 在各个框架或者组件中, 并发限流体当初上面几个方面:
- 限度总并发数(如数据库连接池、线程池)
- 限度刹时并发数(nginx 的 limit\_conn 模块,用来限度刹时并发连接数)
- 限度工夫窗口内的均匀速率(如 Guava 的 RateLimiter、nginx 的 limit\_req 模块,限度每秒的均匀速率)
- 其余的还有限度近程接口调用速率、限度 MQ 的生产速率。
- 另外还能够依据网络连接数、网络流量、CPU 或内存负载等来限流。
有了并发限流,就意味着在解决高并发的时候多了一种爱护机制,不必放心霎时流量导致系统挂掉或雪崩,最终做到有损服务而不是不服务;然而限流须要评估好,不能乱用,否则一些失常流量呈现一些奇怪的问题而导致用户体验很差造成用户散失。
接口限流
接口限流分为两个局部, 一是限度一段时间内接口调用次数, 参照后面限流算法的计数器算法, 二是设置滑动工夫窗口算法。
接口总数
管制一段时间内接口被调用的总数量, 能够参考后面的计数器算法, 不再赘述。
接口工夫窗口
固定工夫窗口算法 (也就是后面提到的计数器算法) 的问题是统计区间太大,限流不够准确,而且在第二个统计区间 时没有思考与前一个统计区间的关系与影响(第一个区间后半段 + 第二个区间前半段也是一分钟)。为了解决下面咱们提到的临界问题,咱们试图把每个统计区间分为更小的统计区间,更准确的统计计数。
在下面的例子中, 假如 QPS 能够承受 100 次查问 / 秒, 前一分钟前 40 秒拜访很低, 后 20 秒突增, 并且这个继续了一段时间, 直到第二分钟的第 40 秒才开始降下来, 依据后面的计数办法, 前一秒的 QPS 为 94, 后一秒的 QPS 为 92, 那么没有超过设定参数, 然而! 然而在两头区域,QPS 达到了 142, 这显著超过了咱们的容许的服务申请数目, 所以固定窗口计数器不太牢靠, 须要滑动窗口计数器。
计数器算法其实就是固定窗口算法, 只是它没有对工夫窗口做进一步地划分,所以只有 1 格;由此可见,当滑动窗口的格子划分的越多,也就是将秒准确到毫秒或者纳秒, 那么滑动窗口的滚动就越平滑,限流的统计就会越准确。
须要留神的是, 耗费的空间就越多。
限流实现
这一部分是限流的具体实现, 简略说说, 毕竟长篇代码没人违心看。
guava 实现
引入包
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.1-jre</version>
</dependency>
外围代码
LoadingCache<Long, AtomicLong> counter = CacheBuilder.newBuilder().
expireAfterWrite(2, TimeUnit.SECONDS)
.build(new CacheLoader<Long, AtomicLong>() {
@Override
public AtomicLong load(Long secend) throws Exception {
// TODO Auto-generated method stub
return new AtomicLong(0);
}
});
counter.get(1l).incrementAndGet();
令牌桶实现
稳固模式(SmoothBursty: 令牌生成速度恒定)
public static void main(String[] args) {// RateLimiter.create(2)每秒产生的令牌数
RateLimiter limiter = RateLimiter.create(2);
// limiter.acquire() 阻塞的形式获取令牌
System.out.println(limiter.acquire());;
try {Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();}
System.out.println(limiter.acquire());;
System.out.println(limiter.acquire());;
System.out.println(limiter.acquire());;
System.out.println(limiter.acquire());;
System.out.println(limiter.acquire());;
System.out.println(limiter.acquire());;
}
\`RateLimiter.create(2)
容量和突发量,令牌桶算法容许将一段时间内没有生产的令牌暂存到令牌桶中,用来突发生产。
渐进模式(SmoothWarmingUp: 令牌生成速度迟缓晋升直到维持在一个稳固值)
// 平滑限流,从冷启动速率(满的)到均匀生产速率的工夫距离
RateLimiter limiter = RateLimiter.create(2,1000l,TimeUnit.MILLISECONDS);
System.out.println(limiter.acquire());;
try {Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();}
System.out.println(limiter.acquire());;
System.out.println(limiter.acquire());;
System.out.println(limiter.acquire());;
System.out.println(limiter.acquire());;
System.out.println(limiter.acquire());;
System.out.println(limiter.acquire());;
超时
boolean tryAcquire = limiter.tryAcquire(Duration.ofMillis(11));
在 timeout 工夫内是否可能取得令牌,异步执行
分布式系统限流
Nginx + Lua 实现
能够应用 resty.lock 放弃原子个性,申请之间不会产生锁的重入
https://github.com/openresty/…
应用 lua\_shared\_dict 存储数据
local locks = require "resty.lock"
local function acquire()
local lock =locks:new("locks")
local elapsed, err =lock:lock("limit_key") -- 互斥锁 保障原子个性
local limit_counter =ngx.shared.limit_counter -- 计数器
local key = "ip:" ..os.time()
local limit = 5 -- 限流大小
local current =limit_counter:get(key)
if current ~= nil and current + 1> limit then -- 如果超出限流大小
lock:unlock()
return 0
end
if current == nil then
limit_counter:set(key, 1, 1) -- 第一次须要设置过期工夫,设置 key 的值为 1,-- 过期工夫为 1 秒
else
limit_counter:incr(key, 1) -- 第二次开始加 1 即可
end
lock:unlock()
return 1
end
ngx.print(acquire())