高可用之——应用级限流

转载请注明出处:https://blog.csdn.net/l1028386804/article/details/100743565

1.限流总并发/连接/请求数

对于一个应用系统来说,一定会有极限并发/请求数,即总有一个TPS/QPS阈值,如果超过了阈值,则系统就会不响应用户请求或响应的非常慢,所以,需要进行过载保护,防止大量请求涌入击垮系统。
例如Tomcat,Connector其中一种配置中有如下几个参数。

  • acceptCount:如果Tomcat的线程都忙于响应,新来的连接会进行队列排队,如果超出排队大小,则拒绝连接;
  • maxConnections:瞬时最大连接数,超出的会排队等待。
  • maxThreads:Tomcat能启动用来处理请求的最大线程数,如果请求处理量一直远远大于最大线程数,则会引起响应变慢甚至僵死。

2.限流总资源数

稀缺资源(数据库连接池、线程)可以使用池化技术限制总资源数,如:连接池、线程池。如果分配给每个应用的数据库连接是100,那么本应用最多可以使用100个资源,超出则可以等待或抛出异常。

3.限制某个接口的总并发/请求数

限制某个接口的总并发/请求数粒度比较细,可以为每个接口设置相应的阈值,可以使用Java中的AtomicLong或者Semaphore进行限流。模型代码如下:

try{
	if(atomic.incrementAndGet() > 限流数){
		//拒绝处理
	}
	//处理请求
}finally{
	atomic.decrementAndGet();
}

这种方式适合对可降级业务或者需要过载保护的服务进行限流,比如抢购业务,超出限额,要么让用户排队,要么告诉用户没货了,这对用户来说是可以接受的。一些开放平台也会限制用户调用某个接口的试用请求量,这时也可以用这种计数器方式实现。

4.限流某个接口的时间窗请求数

如果限制某个接口/服务每秒/每分钟/每天的请求数/调用量。比如一些基础服务会被很多其他系统调用,可以对每秒/每分钟的调用量进行限速,一种实现方式如下所示。

LoadingCache<Long, AtomicLong> counter = CacheBuilder.newBuilder()
		.expireAfterWrite(2, TimeUnit.SECONDS)
		.build(new CacheLoader<Long, AtomicLong>(){
			@Override
			public AtomicLong load(Long seconds) throws Exception{
				return new AtomicLong(0);
			}
		});
long limit = 1000;
while(true){
	//得到当前秒
	long currentSeconds = System.currentTimeMillis() / 1000;
	if(counter.get(currentSeconds).incrementAndGet() > limit){
		System.out.println("系统限流了:" + currentSeconds);
		continue;
	}
	//业务处理
}

使用Guava的Cache来存储计数器,过期时间设置为2秒(保证能记录1秒内的计数)。然后,获取当前时间戳,取秒数作为Key进行计数统计和限流。

5.平滑限流某个接口的请求数

之前的限流方式都不能很好的应对突发请求,即瞬间请求可能都被允许,从而导致一些问题。因此,在一些场景中需要对突发请求进行整形,整形为平均速率请求处理(比如5r/s,即每隔200毫秒处理一个请求,平滑了速率)。有两种算法满足场景:令牌桶和漏桶算法。Guava框架提供了令牌桶算法,可以直接使用。
Guava RateLimiter提供的令牌桶算法可用于平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。

(1)SmoothBursty

@Test
public void testSmoothBrusty01(){
	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.197944
0.197721
0.199604
0.199207
0.199725
  • RateLimiter.create(5):表示桶容量为5并且每秒新增5个令牌,即每隔200毫秒新增一个令牌。
  • limiter.acquire():表示消费一个令牌,如果当前桶中有足够的令牌,则成功(返回0),如果桶中没有令牌,则暂停一段时间。比如:发令牌间隔是200毫秒,则等待200毫秒后再去消费令牌,这种实现将突发请求速率平均为固定请求速率。

突发请求示例一

@Test
public void testSmoothBrusty02(){
	RateLimiter limiter = RateLimiter.create(5);
	System.out.println(limiter.acquire(5));
	System.out.println(limiter.acquire(1));
	System.out.println(limiter.acquire(1));
	System.out.println(limiter.acquire(1));
}

将得到类似如下输出:

0.0
0.998038
0.197305
0.196624
  • RateLimiter.create(5):表示桶的容量为5并且每秒新增5个令牌。
  • limiter.acquire(5):令牌桶算法允许一定程度的突发请求,所以可以一次性消费5个令牌。
  • limiter.acquire(1):等待1秒,桶中才能有令牌。

接下来的请求就被整形为固定速率了。

突发请求示例二

@Test
public void testSmoothBrusty03(){
	RateLimiter limiter = RateLimiter.create(5);
	System.out.println(limiter.acquire(10));
	System.out.println(limiter.acquire(1));
	System.out.println(limiter.acquire(1));
	System.out.println(limiter.acquire(1));
}

将得到类似如下输出:

0.0
1.998626
0.196534
0.199605

突发请求示例三

@Test
public void testSmoothBrusty04() throws Exception{
	RateLimiter limiter = RateLimiter.create(2);
	System.out.println(limiter.acquire());
	Thread.sleep(2000);

	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.0
0.0
0.0
0.499875
0.496976
  • 创建了一个桶,容量为2,并且每秒新增2个令牌。
  • 调用limiter.acquire()消费一个令牌,此时令牌桶可以满足需求(返回值为0)。
  • 线程暂停2秒,接下来的两个limiter.acquire()都能消费到令牌,第三个limiter.acquire()也同样消费到了令牌,到第四个limiter.acquire()时就要等待500毫秒。

此处可以看到桶容量为2(允许的突发量),这是因为SmoothBursty中有一个参数:最大突发秒数(maxBurstSeconds),默认1s。突发量/桶容量 = 速率 * maxBurstSeconds,所以本示例中桶容量/突发量为2(本示例中速率为0.5秒,maxBurstSeconds为1秒)。
SmoothBursty通过平均速率和最后一次新增令牌的时间计算出下次新增令牌的时间。另外,需要一个桶暂存一段时间内没有使用的令牌(即可以突发的令牌数)。另外,RateLimiter还提供了tryAcquire()方法来进行无阻塞或可超时的令牌消费。

(2)SmoothWarmingUp

Guava的SmoothWarmingUp能够实现在系统冷启动后慢慢趋于平均速率(即刚开始速率小一些,然后慢慢趋于设置的固定速率)。
创建方式为:

RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit)
  • permitsPerSecond:每秒新增的令牌数。
  • warmupPeriod:从冷启动过度到平均速率的时间间隔
  • unit:时间单位

示例如下:

@Test
public void testSmoothWarmingUp() throws  Exception{
	RateLimiter limiter = RateLimiter.create(5, 1000, TimeUnit.MILLISECONDS);
	for(int i = 1; i < 5; i++){
		System.out.println(limiter.acquire());
	}
	Thread.sleep(1000);
	for(int i = 1; i < 5; i++){
		System.out.println(limiter.acquire());
	}
}

将得到类似如下输出:

0.0
0.518306
0.357487
0.220917
0.0
0.519406
0.360388
0.220326

速率是梯形上升速率,也就是说冷启动时会以一个比较大的速率慢慢达到平均速率。然后趋于平均速率(梯形下降到平均速率)。可以通过调节warmupPeriod参数实现一开始就是平滑固定速率。

注意:应用级限流只是单应用内的请求限流,如果将应用部署到多台服务器上,应用级限流不能进行全局限流。因此,需要用分布式限流和接入层限流来解决这个问题。

——总结自涛哥的《亿级流量网站架构核心技术》

上一篇:leetcode-560. 和为K的子数组


下一篇:Godot控件响应鼠标点击事件