当系统并发流量过大的时候,有可能会造成系统被压垮导致整个服务不可用的问题。
针对这个场景,一般的解决方案是:如果超过这个流量,我们就拒绝提供服务,从而使得我们的服务不会挂掉。
当然,限流虽然能够保护系统不被压垮,但是对于被限流的用户,就会很不开心。所以限流其实是一种有损的解决方案。但是相比于全部不可用,有损服务是最好的一种解决办法
限流的作用
除了前面说的限流使用场景之外,限流的设计还能防止恶意请求流量、恶意攻击
所以,限流的基本原理是通过对并发访问/请求进行限速或者一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务(定向到错误页或者告知资源没有了)、排队或等待(秒杀、下单)、降级(返回兜底数据或默认数据或默认数据,如商品详情页库存默认有货)
一般互联网企业常见的限流有:限制总并发数(如数据库连接池、线程池)、限制瞬时并发数(nginx的limit_conn模块,用来限制瞬时并发连接数)、限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limit_req模块,限制每秒的平均速率);其他的还有限制远程接口调用速率、限制MQ的消费速率。另外还可以根据网络连接数、网络流量、CPU或内存负载等来限流。
有了缓存以后再加上限流,在处理高并发的时候就能够从容应对,不用担心瞬间流量导致系统挂掉或雪崩,最终做到有损服务而不是不服务;但是限流需要评估好,不能乱用,否则一些正常流量出现一些奇怪的问题而导致用户体验很差造成用户流失。
常见的限流算法
滑动窗口
发送和接受方都会维护一个数据帧的序列,这个序列被称作窗口。发送方的窗口大小由接受方确定,目的在于控制发送速度,以免接受方的缓存不够大,而导致溢出,同时控制流量也可以避免网络拥塞。下面图中的4,5,6号数据帧已经被发送出去,但是未收到关联的ACK,7,8,9帧则是等待发送。可以看出发送端的窗口大小为6,这是由接受端告知的。此时如果发送端收到4号ACK,则窗口的左边缘向右收缩,窗口的右边缘则向右扩展,此时窗口就向前“滑动了”,即数据帧10也可以被发送。
漏桶(控制传输速率Leaky bucket)
漏桶算法思路是,不断的往桶里面注水,无论注水的速度是大还是小,水都是按固定的速率往外漏水;如果桶满了,水会溢出;
桶本身具有一个恒定的速率往下漏水,而上方时快时慢的会有水进入桶内。当桶还未满时,上方的水可以加入。一旦水满,上方的水就无法加入。桶满正是算法中的一个关键的触发条件(即流量异常判断成立的条件)。而此条件下如何处理上方流下来的水,有两种方式
在桶满水之后,常见的两种处理方式为:
暂时拦截住上方水的向下流动,等待桶中的一部分水漏走后,再放行上方水。
溢出的上方水直接抛弃。
特点
漏水的速率是固定的
即使存在注水burst(突然注水量变大)的情况,漏水的速率也是固定的
令牌桶(能够解决突发流量)
令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。
令牌桶是一个存放固定容量令牌(token)的桶,按照固定速率往桶里添加令牌; 令牌桶算法实际上由三部分组成:两个流和一个桶,分别是令牌流、数据流和令牌桶
令牌流与令牌桶
系统会以一定的速度生成令牌,并将其放置到令牌桶中,可以将令牌桶想象成一个缓冲区(可以用队列这种数据结构来实现),当缓冲区填满的时候,新生成的令牌会被扔掉。这里有两个变量很重要:
第一个是生成令牌的速度,一般称为 rate 。比如,我们设定 rate = 2 ,即每秒钟生成 2 个令牌,也就是每 1/2 秒生成一个令牌;
第二个是令牌桶的大小,一般称为 burst 。比如,我们设定 burst = 10 ,即令牌桶最大只能容纳 10 个令牌。
数据流
数据流是真正的进入系统的流量,对于http接口来说,如果平均每秒钟会调用2次,则认为速率为 2次/s。
有以下三种情形可能发生:
数据流的速率 等于 令牌流的速率。这种情况下,每个到来的数据包或者请求都能对应一个令牌,然后无延迟地通过队列;
数据流的速率 小于 令牌流的速率。通过队列的数据包或者请求只消耗了一部分令牌,剩下的令牌会在令牌桶里积累下来,直到桶被装满。剩下的令牌可以在突发请求的时候消耗掉。
数据流的速率 大于 令牌流的速率。这意味着桶里的令牌很快就会被耗尽。导致服务中断一段时间,如果数据包或者请求持续到来,将发生丢包或者拒绝响应。
比如前面举的例子,生成令牌的速率和令牌桶的大小分别为 rate = 2, burst = 10 ,则系统能承受的突发请求速率为 10次/s ,平均请求速率为 2次/s 。 三种情形中的最后一种情景是这个算法的核心所在,这个算法非常精确,实现非常简单并且对服务器的压力可以忽略不计,因此应用得相当广泛,值得学习和利用
特点
令牌可以积累:桶中最大的令牌数是b,表示可以积累的最大令牌数
允许突发流量:桶中token可以积累到n(b<=n<=0),此时如果有n个突发请求同时到达,这n个请求是可以同时允许处理的
限流算法实战
Semaphore
Semaphore比较常见的就是用来做限流操作了。比如下面的场景中,模拟20个客户端请求过来,我们为了减少访问的压力,通过Semaphore来限制请求的流量。
public class SemaphoreTest {
public static void main(String[] args) {
// 线程池
ExecutorService exec = Executors.newCachedThreadPool();
// 只能5个线程同时访问
final Semaphore semp = new Semaphore(5);
// 模拟20个客户端访问
for (int index = 0; index < 20; index++) {
final int NO = index;
Runnable run = new Runnable() {
public void run() {
try {
// 获取许可
semp.acquire();
System.out.println("Accessing: " \+ NO);
Thread.sleep((long) (Math.random() * 10000));
// 访问完后,释放
semp.release();
} catch (InterruptedException e) {
}
}
};
exec.execute(run);
}
// 退出线程池
exec.shutdown();
}
}
Guava的RateLimiter实现
在Guava中RateLimiter的实现有两种: Bursty和WarmUp
bursty是基于token bucket的算法实现,比如
RateLimiter rateLimiter=RateLimiter.create(permitPerSecond); //创建一个bursty实例
rateLimiter.acquire(); //获取1个permit,当令牌数量不够时会阻塞直到获取为止
-
引入jar包
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>23.0</version> </dependency>
-
编写测试代码
public class PayService { RateLimiter rateLimiter=RateLimiter.create(10);//qps=10 public void doRequest(String threadName){ if(rateLimiter.tryAcquire()){ System.out.println(threadName+": 支付成功"); }else{ System.out.println(threadName+": 当前支付人数过多,请稍候再试"); } } public static void main(String[] args) throws IOException { PayService payService=new PayService(); CountDownLatch latch=new CountDownLatch(1); Random random=new Random(10); for (int i = 0; i < 20; i++) { int finalI = i; new Thread(()->{ try { latch.await(); int sleepTime = random.nextInt(1000); Thread.sleep(sleepTime); payService.doRequest("t-"+ finalI); }catch (Exception e){ e.printStackTrace(); } }).start(); } latch.countDown(); System.in.read(); } }
下一篇文章来分析Alibaba开源的限流框架Sentinel!