前言算法
分布式环境下应对高并发保证服务稳定几招,按照我的理解,优先级从高到低分别为缓存、限流、降级、熔断,每招都有它的做用,本文重点就讲讲限流这部分。api
坦白讲,其实上面的说法也不许确,由于服务降级、熔断自己也是限流的一种,由于它们本质上也是阻断了流量进来,可是本文但愿你们能够把限流当作一个单纯的名词来理解,看一下对请求作流控的几种算法及具体实现方式。缓存
为何要限流服务器
其实很好理解的一个问题,为何要限流,天然就流量过大了呗,一个对外服务有不少场景都会流量增大:网络
注意这个"大",1000QPS大吗?5000QPS大吗?10000QPS大么?没有答案,由于没有标准,所以,"大"必定是和正常流量相比的大。流量一大,服务器扛不住,扛不住就挂了,挂了无法提供对外服务致使业务直接熔断。怎么办,最直接的办法就是从源头把流量限制下来,例如服务器只有支撑1000QPS的处理能力,那就每秒放1000个请求,天然保证了服务器的稳定,这就是限流。并发
下面看一下常见的两种限流算法。分布式
漏桶算法ide
漏桶算法的原理比较简单,水(请求)先进入到漏桶里,人为设置一个最大出水速率,漏桶以<=出水速率的速度出水,当水流入速度过大会直接溢出(拒绝服务):高并发
所以,这个算法的核心为:ui
所以这是一种强行限制请求速率的方式,可是缺点很是明显,主要有两点:
因此,一般来讲利用漏桶算法来限流,实际场景下用得很少。
令牌桶算法
令牌桶算法是网络流量整形(Traffic Shaping)和限流(Rate Limiting)中最常使用的一种算法,它可用于控制发送到网络上数据的数量并容许突发数据的发送。
从某种意义上来讲,令牌桶算法是对漏桶算法的一种改进,主要在于令牌桶算法可以在限制调用的平均速率的同时还容许必定程度的突发调用,来看下令牌桶算法的实现原理:
整个的过程是这样的:
那么,咱们再看一下,为何令牌桶算法能够防止必定程度的突发流量呢?能够这么理解,假设咱们想要的速率是1000QPS,那么往桶中放令牌的速度就是1000个/s,假设第1秒只有800个请求,那意味着第2秒能够允许1200个请求,这就是必定程度突发流量的意思,反之咱们看漏桶算法,第一秒只有800个请求,那么所有放过,第二秒这1200个请求将会被打回200个。
注意上面屡次提到必定程度这四个字,这也是我认为令牌桶算法最须要注意的一个点。假设仍是1000QPS的速率,那么5秒钟放1000个令牌,第1秒钟800个请求过来,第2~4秒没有请求,那么按照令牌桶算法,第5秒钟能够接受4200个请求,可是实际上这已经远远超出了系统的承载能力,所以使用令牌桶算法特别注意设置桶中令牌的上限便可。
总而言之,做为对漏桶算法的改进,令牌桶算法在限流场景下被使用更加普遍。
RateLimiter使用
上面说了令牌桶算法在限流场景下被使用更加普遍,接下来咱们看一下代码示例,模拟一下每秒最多过五个请求:
public class RateLimiterTest { private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private static final int THREAD_COUNT = 25; @Test public void testRateLimiter1() { RateLimiter rateLimiter = RateLimiter.create(5); Thread[] ts = new Thread[THREAD_COUNT]; for (int i = 0; i < THREAD_COUNT; i++) { ts[i] = new Thread(new RateLimiterThread(rateLimiter), "RateLimiterThread-" + i); } for (int i = 0; i < THREAD_COUNT; i++) { ts[i].start(); } for (;;); } public class RateLimiterThread implements Runnable { private RateLimiter rateLimiter; public RateLimiterThread(RateLimiter rateLimiter) { this.rateLimiter = rateLimiter; } @Override public void run() { rateLimiter.acquire(1); System.out.println(Thread.currentThread().getName() + "获取到了令牌,时间 = " + FORMATTER.format(new Date())); } } }
利用RateLimiter.create这个构造方法能够指定每秒向桶中放几个令牌,比方说上面的代码create(5),那么每秒放置5个令牌,即200ms会向令牌桶中放置一个令牌。这边代码写了一条线程模拟实际场景,拿到令牌那么就能执行下面逻辑,看一下代码执行结果:
RateLimiterThread-0获取到了令牌,时间 = 2019-08-25 20:58:53 RateLimiterThread-23获取到了令牌,时间 = 2019-08-25 20:58:54 RateLimiterThread-21获取到了令牌,时间 = 2019-08-25 20:58:54 RateLimiterThread-19获取到了令牌,时间 = 2019-08-25 20:58:54 RateLimiterThread-17获取到了令牌,时间 = 2019-08-25 20:58:54 RateLimiterThread-13获取到了令牌,时间 = 2019-08-25 20:58:54 RateLimiterThread-9获取到了令牌,时间 = 2019-08-25 20:58:55 RateLimiterThread-15获取到了令牌,时间 = 2019-08-25 20:58:55 RateLimiterThread-5获取到了令牌,时间 = 2019-08-25 20:58:55 RateLimiterThread-1获取到了令牌,时间 = 2019-08-25 20:58:55 RateLimiterThread-11获取到了令牌,时间 = 2019-08-25 20:58:55 RateLimiterThread-7获取到了令牌,时间 = 2019-08-25 20:58:56 RateLimiterThread-3获取到了令牌,时间 = 2019-08-25 20:58:56 RateLimiterThread-4获取到了令牌,时间 = 2019-08-25 20:58:56 RateLimiterThread-8获取到了令牌,时间 = 2019-08-25 20:58:56 RateLimiterThread-12获取到了令牌,时间 = 2019-08-25 20:58:56 RateLimiterThread-16获取到了令牌,时间 = 2019-08-25 20:58:57 RateLimiterThread-20获取到了令牌,时间 = 2019-08-25 20:58:57 RateLimiterThread-24获取到了令牌,时间 = 2019-08-25 20:58:57 RateLimiterThread-2获取到了令牌,时间 = 2019-08-25 20:58:57 RateLimiterThread-6获取到了令牌,时间 = 2019-08-25 20:58:57 RateLimiterThread-10获取到了令牌,时间 = 2019-08-25 20:58:58 RateLimiterThread-14获取到了令牌,时间 = 2019-08-25 20:58:58 RateLimiterThread-18获取到了令牌,时间 = 2019-08-25 20:58:58 RateLimiterThread-22获取到了令牌,时间 = 2019-08-25 20:58:58
看到,很是标准,在每次消耗一个令牌的状况下,RateLimiter能够保证每一秒内最多只有5个线程获取到令牌,使用这种方式能够很好的作单机对请求的QPS数控制。
至于为何2019-08-25 20:58:53这个时间点只有1条线程获取到了令牌而不是有5条线程获取到令牌,由于RateLimiter是按照秒计数的,可能第一个线程是2019-08-25 20:58:53.999秒来的,算在2019-08-25 20:58:53这一秒内;下一个线程2019-08-25 20:58:54.001秒来,天然就算到2019-08-25 20:58:54这一秒去了。
上面的写法是RateLimiter最经常使用的写法,注意:
RateLimiter预消费
处理请求,每次来一个请求就acquire一把是RateLimiter最多见的用法,可是咱们看acquire还有个acquire(int permits)的重载方法,即容许每次获取多个令牌数。这也是有可能的,请求数是一个大维度每次扣减1,有可能服务器按照字节数来进行限流,例如每秒最多处理10000字节的数据,那每次扣减的就不止1了。
接着咱们再看一段代码示例:
@Test public void testRateLimiter2() { RateLimiter rateLimiter = RateLimiter.create(1); System.out.println("获取1个令牌开始,时间为" + FORMATTER.format(new Date())); double cost = rateLimiter.acquire(1); System.out.println("获取1个令牌结束,时间为" + FORMATTER.format(new Date()) + ", 耗时" + cost + "ms"); System.out.println("获取5个令牌开始,时间为" + FORMATTER.format(new Date())); cost = rateLimiter.acquire(5); System.out.println("获取5个令牌结束,时间为" + FORMATTER.format(new Date()) + ", 耗时" + cost + "ms"); System.out.println("获取3个令牌开始,时间为" + FORMATTER.format(new Date())); cost = rateLimiter.acquire(3); System.out.println("获取3个令牌结束,时间为" + FORMATTER.format(new Date()) + ", 耗时" + cost + "ms"); }
代码运行结果为:
获取1个令牌开始,时间为2019-08-25 21:21:09.973 获取1个令牌结束,时间为2019-08-25 21:21:09.976, 耗时0.0ms 获取5个令牌开始,时间为2019-08-25 21:21:09.976 获取5个令牌结束,时间为2019-08-25 21:21:10.974, 耗时0.997237ms 获取3个令牌开始,时间为2019-08-25 21:21:10.976 获取3个令牌结束,时间为2019-08-25 21:21:15.974, 耗时4.996529ms
看到这就是标题所说的预消费能力,也是RateLimiter中容许必定程度突发流量的实现方式。第二次须要获取5个令牌,指定的是每秒放1个令牌到桶中,咱们发现实际上并无等5秒钟等桶中积累了5个令牌才能让第二次acquire成功,而是直接等了1秒钟就成功了。咱们能够捋一捋这个逻辑:
也就是说,前面的请求若是流量大于每秒放置令牌的数量,那么容许处理,可是带来的结果就是后面的请求延后处理,从而在总体上达到一个平衡总体处理速率的效果。
突发流量的处理,在令牌桶算法中有两种方式,一种是有足够的令牌才能消费,一种是先消费后还令牌。后者就像咱们0首付买车似的,30万的车不多有等攒到30万才全款买的,先签了相关合同把车子给你,而后贷款慢慢还,这样就爽了。RateLimiter也是一样的道理,先让请求获得处理,再慢慢还上预支的令牌,客户端一样也爽了,不然我假设预支60个令牌,1分钟以后才能处理个人请求,不合理也不人性化。
RateLimiter的限制
特别注意RateLimiter是单机的,也就是说它没法跨JVM使用,设置的1000QPS,那也在单机中保证平均1000QPS的流量。
假设集群中部署了10台服务器,想要保证集群1000QPS的接口调用量,那么RateLimiter就不适用了,集群流控最多见的方法是使用强大的Redis:
总得来讲,集群限流的实现也比较简单。
总结
本文主要写了常见的两种限流算法漏桶算法与令牌桶算法,而且演示了Guava中RateLimiter的实现,相信看到这里的朋友必定都懂了,恭喜大家!
令牌桶算法是最经常使用的限流算法,它最大的特色就是允许必定程度的突发流量。
漏桶算法一样也有本身的应用之处,例如Nginx的限流模块就是基于漏桶算法的,它最大的特色就是强行限制流量按照指定的比例下发,适合那种对流量有绝对要求的场景,就是流量能够允许在我指定的值之下,能够被屡次打回,可是不管如何决不能超过指定的。
虽然令牌桶算法相对更好,可是仍是我常常说的,使用哪一种彻底就看你们各自的场景,适合的才是最好的。