限流控制
<p>[TOC]</p>
<h1>高并发系统的限流控制</h1>
<h2>1.处理概述</h2>
<p>在高并发的系统中,常见的处理方式包括:</p>
<ul>
<li><strong>1.缓存</strong>
缓存是拿空间换时间,通过数据库缓存,文件系统缓存队列,减少磁盘io,提高服务器的承载能力。使用缓存不仅能够提高系统的访问速度,提高并发量,也是保护数据库保护系统的有效方式。</li>
<li><strong>2.降级</strong>
降级是指当服务器的压力较大时,根据当前的业务情况及流量对一些页面和服务进行策略性的降级,从而提供充足的服务器资源保证核心服务正常运行。降级往往会根据不同的情况执行不同等级的策略。根据服务方式,可以分为:拒绝服务,延迟服务,随机服务。根据业务范围,可以分为:砍掉某个功能,砍掉某个模块。降级的目的就是使得服务器根据一定的策略保持有损运行。</li>
<li><strong>3.限流</strong>
限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。常见的限流算法有:计数器,漏桶算法,令牌桶算法。</li>
</ul>
<h2>2.限流方式</h2>
<ul>
<li>限制总并发数(比如数据库连接池、线程池)</li>
<li>限制瞬时并发数(如nginx的limit_conn模块,用来限制瞬时并发连接数)</li>
<li>限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limit_req模块,限制每秒的平均速率)</li>
<li>限制远程接口调用速率</li>
<li>限制远程接口调用速率</li>
<li>限制远程接口调用速率</li>
<li>限制MQ的消费速率。</li>
<li>可以根据网络连接数、网络流量、CPU或内存负载等来限流</li>
</ul>
<h2>3.限流算法</h2>
<h3>3.1计数器</h3>
<pre><code>计数器来进行限流,主要用来限制总并发数,比如数据库连接池、线程池、秒杀的并发数;只要全局总请求数或者一定时间段的总请求数设定的阀值则进行限流,是简单粗暴的总数量限流,而不是平均速率限流</code></pre>
<h3>3.2令牌桶</h3>
<p><img src="https://www.showdoc.cc/server/api/common/visitfile/sign/129f75439e96923122b7ea59f58a275d?showdoc=.jpg" alt="令牌桶算法" title="令牌桶算法" /></p>
<p>令牌桶算法是一个存放固定容量令牌(token)的桶,按照固定速率往桶里添加令牌。令牌桶算法基本可以用下面的几个概念来描述:</p>
<pre><code>令牌将按照固定的速率被放入令牌桶中。比如每秒放10个。
桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝。
当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上。
如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。</code></pre>
<h3>3.3漏桶</h3>
<p><img src="https://www.showdoc.cc/server/api/common/visitfile/sign/2d7838a052bed050dcb6fc2c96ce3bf5?showdoc=.jpg" alt="漏桶算法" title="漏桶算法" /></p>
<p>漏桶算法即leaky bucket是一种非常常用的限流算法,可以用来实现流量整形(Traffic Shaping)和流量控制(Traffic Policing)。漏桶算法的主要概念如下:</p>
<pre><code>一个固定容量的漏桶,按照常量固定速率流出水滴;
如果桶是空的,则不需流出水滴;
可以以任意速率流入水滴到漏桶;
如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。
漏桶算法比较好实现,在单机系统中可以使用队列来实现,在分布式环境中消息中间件或者Redis都是可选的方案</code></pre>
<h3>3.4漏桶算法和令牌桶算法的区别</h3>
<p>令牌桶可以在运行时控制和调整数据处理的速率,处理某时的突发流量。放令牌的频率增加可以提升整体数据处理的速度,而通过每次获取令牌的个数增加或者放慢令牌的发放速度和降低整体数据处理速度。而漏桶不行,因为它的流出速率是固定的,程序处理速度也是固定的。
<strong>整体而言,令牌桶算法更优,但是实现更为复杂一些。</strong></p>
<h2>4.不同级别的限流</h2>
<h3>4.1应用级别限流</h3>
<h4>4.1.1限流总并发/连接/请求数</h4>
<p>使用Tomcat的server.xml配置,Connector其中一种配置有如下几个参数:</p>
<pre><code>acceptCount:如果Tomcat的线程都忙于响应,新来的连接会进入队列排队,如果超出排队大小,则拒绝连接
maxConnections:瞬时最大连接数,超出的会排队等待
maxThreads:Tomcat能启动用来处理请求的最大线程数,如果请求处理量一直远远大于最大线程数则可能会僵死</code></pre>
<h4>4.1.2限流总资源数</h4>
<pre><code>可以使用池化技术来限制总资源数:连接池、线程池。比如分配给每个应用的数据库连接是100,那么本应用最多可以使用100个资源,超出了可以等待或者抛异常</code></pre>
<h4>4.1.3限流某个接口的总并发/请求数</h4>
<p>可以使用Java中的AtomicLong,示意代码:</p>
<pre><code>try{
if(atomic.incrementAndGet() > 限流数) {
//拒绝请求
}
//处理请求
}finally{
atomic.decrementAndGet();
}</code></pre>
<h4>4.1.4限流某个接口的时间窗请求数</h4>
<p>使用Guava的Cache,示意代码:</p>
<pre><code>LoadingCache counter =
CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.SECONDS)
.build(newCacheLoader() {
@Override
publicAtomicLong load(Long seconds)throwsException {
return newAtomicLong(0);
}
});
longlimit =1000;
while(true) {
//得到当前秒longcurrentSeconds = System.currentTimeMillis() /1000;
if(counter.get(currentSeconds).incrementAndGet() > limit) {
System.out.println("限流了:"+ currentSeconds);
continue;
}
//业务处理
}</code></pre>
<h4>4.1.5平滑限流某个接口的请求数</h4>
<p>之前的限流方式都不能很好地应对突发请求,即瞬间请求可能都被允许从而导致一些问题;因此在一些场景中需要对突发请求进行整形,整形为平均速率请求处理</p>
<p>Guava框架提供了令牌桶算法实现</p>
<p>Guava RateLimiter提供了令牌桶算法实现:<strong>平滑突发限流</strong>(SmoothBursty)和<strong>平滑预热限流</strong>(SmoothWarmingUp)实现</p>
<p><strong>平滑突发限流</strong>(SmoothBursty)</p>
<pre><code>RateLimiter limiter = RateLimiter.create(5);
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());
将得到类似如下的输出:
0.0
0.198239
0.196083
0.200609
0.199599
0.19961</code></pre>
<p>limiter.acquire(5)表示桶的容量为5且每秒新增5个令牌</p>
<p><strong>平滑预热限流</strong>(SmoothWarmingUp)</p>
<pre><code>RateLimiter limiter = RateLimiter.create(5,1000, TimeUnit.MILLISECONDS);
for(inti =1; i <5;i++) {
System.out.println(limiter.acquire());
}
Thread.sleep(1000L);
for(inti =1; i <5;i++) {
System.out.println(limiter.acquire());
}
将得到类似如下的输出:
0.0
0.51767
0.357814
0.219992
0.199984
0.0
0.360826
0.220166
0.199723
0.199555</code></pre>
<p>SmoothWarmingUp创建方式:RateLimiter.create(doublepermitsPerSecond, long warmupPeriod, TimeUnit unit)
permitsPerSecond表示每秒新增的令牌数,warmupPeriod表示在从冷启动速率过渡到平均速率的时间间隔。</p>
<p>速率是梯形上升速率的,也就是说冷启动时会以一个比较大的速率慢慢到平均速率;然后趋于平均速率(梯形下降到平均速率)。可以通过调节warmupPeriod参数实现一开始就是平滑固定速率。</p>
<h3>4.2接入层限流</h3>
<p>接入层通常指请求流量的入口,该层的主要目的有:负载均衡、非法请求过滤、请求聚合、缓存、降级、限流、A/B测试、服务质量监控等等</p>
<p>对于Nginx接入层限流可以使用Nginx自带了两个模块:连接数限流模块ngx_http_limit_conn_module和漏桶算法实现的请求限流模块ngx_http_limit_req_module。还可以使用OpenResty提供的Lua限流模块lua-resty-limit-traffic进行更复杂的限流场景。</p>
<p>limit_conn用来对某个KEY对应的总的网络连接数进行限流,可以按照如IP、域名维度进行限流。</p>
<p>limit_req用来对某个KEY对应的请求的平均速率进行限流,并有两种用法:平滑模式(delay)和允许突发模式(nodelay)。</p>
<p>OpenResty提供的Lua限流模块lua-resty-limit-traffic进行更复杂的限流场景</p>