在开发高并发系统时,一般都需要一些手段来保护系统。比如缓存,降级,限流等。
缓存用于提升系统访问速度和增大系统处理能力;降级一般当服务出现问题或者影响到核心流程的性能,需要暂时屏蔽掉一些功能,待高峰过去或问题解决后再重新打开。而对于稀缺资源的访问,频繁调用复杂查询等需要大量计算资源的请求等,需要一种手段来限制这些场景下的并发量或请求量,此时需要使用的手段就是限流。
限流的目的是通过对并发访问、请求进行限速或者限制在一个时间窗口内的请求数量来保护系统。一旦达到限流上限,可以拒绝服务,也可以采取将请求放入缓存队列等待等手段进行处理。
一般高并发系统常见的限流有:限制并发总数(数据库连接池,线程池)、限制瞬间并发数、显示时间窗口内的平均速率,以及限制远程接口调用速率,限制MQ消费速度等。限流的使用需要做好评估,否则有可能出现一些奇怪的问题,或者造成不好的用户体验。
限流算法
常见的限流算法有:令牌桶,漏桶。也可以简单的使用计数器来实现。
令牌桶
令牌桶是指将令牌存放到一个固定容量的桶中,按照固定速率向桶中添加令牌。如果桶被填满后,新增令牌会被丢弃。当请求到来时,需要消耗指定数量的令牌(例如每个请求需要1个令牌),方可进入之后的处理流程,若桶中没有组够的令牌,请求将会被抛弃或进入等待队列。
漏桶
漏桶,首先也存在一个令牌桶,同样是将令牌存放到桶中,按照固定速率向桶中添加令牌。然后将所有的请求都放入漏桶(可以是一个队列),进入漏桶的速度随意,当漏桶被填满之后,请求将被抛弃。漏桶中的请求消耗令牌,然后从漏桶中移除,请求被处理。
令牌桶的作用是限制流入速度,但是允许一定程度的突发流量。
漏桶限制的是请求的流出速率,对突发流量进行平滑处理。
与令牌桶其实是一样的,只是发生作用的方向相反。对于相同的参数,起到的效果是一样的。
有时我们还可以使用计数器来对并发总数进行限制。当在单位时间内请求达到了预设的阈值,则进行限流。这种限流是对请求总数的限制,而不是限制平均速率。
应用级限流
我们在使用Tomcat时,以 下几个参数都与限流有关
acceptCount:如果Tomcat的处理线程都被占用,新来的连接请求将会进入队列,如果超出排队大小,则拒绝连接。
MaxConnections:瞬时最大连接数,超出的会排队等待。
maxThreads:tomcat能启动用来处理请求的最大线程数如果请求处理量远大于最大线程数,则会引起相应变慢或请求假死。
限制某个接口的总并发数/请求数
如果接口有可能会有突发访问的情况,又担心访问量过大造成系统崩溃,就需要限制这个接口的总并发/请求数。因为粒度比较细,所以需要在有必要的接口上都设置相应的阈值。
private static final Long limit = 100L;
static AtomicLong atomic = new AtomicLong(1);
.
.
.
try{
if (atomic.incrementAndGet() > limit) {
//拒绝请求
}
//执行请求
}
这种方式适合对可降级业务或需要过载保护的服务进行限流。一旦请求拒绝,或者让请求排队,或者直接告诉用户请求条件不满足。此时有一个前提条件是用户对于这种结果是可以接受的。
限制某个时间窗口内的请求数量
如果想在某个时间窗口内限制某个接口的请求数量,此时我们可以对每秒/分钟/小时等的请求总量进行限制,以下是一种实现方式。
LoadingCache<Long,AtomicLong> counter = CacheBuilder.newBuilder()
.expireAfterAccess(2.TimeUnit.SECONDS)
.build(new CacheLoader<Long, AtomicLong>() {
@Override
public AtomicLong load(Long aLong) throws Exception {
return new AtomicLong(0);
}
});
long limit = 1000;
while(true){
long currentSeconds = System.currentTimeMillis()/1000;
if(counter.get(currentSeconds).incrementAndGet() > limit){
//被限流了
continue;
}
//处理请求
}
这里使用Guava的Cache来做一个计数器,过期时间设置为2秒,确保1秒内都可以正确访问。然后我们获取当前的时间戳,以秒为key来进行统计和限流,虽然简单粗暴,但是也可以满足某些场景。
平滑某个接口的请求
之前限流的方式都只是从总量上做了控制,并不能够很好的处理突发请求,即瞬间请求都可能被允许。在一些场景中,我们需要对流量进行平滑,比如每50毫秒处理一个请求。这个时候就需要用令牌桶或漏桶算法来实现。Guava提供了令牌桶的算法实现,可以直接使用。
RateLimiter limter = RateLimiter.create(20);
while(true){
limter.acquire();
System.out.println(System.currentTimeMillis());
}
可以得到类似下边的结果
1520751900133
1520751900189
1520751900237
1520751900287
1520751900339
1520751900387
1520751900439
1520751900488
1520751900537
可以看到每个输出间隔近似为50毫秒。RateLimiter.create(20)指令牌桶中每秒生成20个令牌。limter.acquire()默认消费一个令牌,如果代码改为下边这样,就可以看到不同的结果。
RateLimiter limter = RateLimiter.create(20);
while(true){
limter.acquire(2);
System.out.println(System.currentTimeMillis());
}
输出结果类似于
1520752107225
1520752107329
1520752107427
1520752107527
1520752107631
1520752107728
每个通过的请求间隔大约是100毫秒,即消耗2个令牌。
以上限流方式都仅限于在单个应用内进行请求限流,如果多点部署的话,就需要进行分布式全局限流。关于分布式限流以后再聊。
以上内容为《亿级流量网站架构核心技术》的读书笔记,如对相关内容感兴趣,大家可以购买实体书进行阅读。