高并发下服务超载问题及如何限流
超载问题
生活中一个很普通的超载问题就是电闸,当有大功率的用电器使用时会自动跳闸以保护其他电器不会被强电流烧坏。
我们的服务系统大体处理逻辑如下图:
rateStage.png其中用户将请求发送到服务代理层,服务代理层路由分发到对应的服务调用者,服务调用者请求具体的服务接收方,当流量过大的时候,每一层都有可能到达极限发生超载的情况,所以为了保证我们的服务稳定,不因为外部突然的大流量而发生异常,我们可以借鉴上述电闸的思路,通过拒绝或者引流的方式来避免大流量引起的冲击。
如何限流
在用户层,可以通过控制用户的点击页面的频度来适当降低进入服务层的请求数。
在服务代理层,我们可以通过限制访问频率和限制并发连接数来适当降低透传到服务处理方的请求数。
针对服务调用者,可以在本地限流,通过限制某个远程调用的调用速度或者频度,当超过一定阈值时,可以直接阻塞或者拒绝服务,这样可以减少进入服务接收方的请求数,降低处理压力。
对于服务接收方,它是最底层的,具体的服务是由他处理的,通过前面三层的协作限流,到这层的流量已经不多了,但是我们也不能马虎,如果此层的某一个服务挂掉了,有可能会发生服务雪崩,导致整个系统不可用。所以我们也要保护自己。
限流算法
限流方式具体可以通过以下三种方式实现
-
限制并发的数量
-
限制并发访问的速率
-
限制单位时间窗口内的请求数量
常见的算法有:计数器算法、滑动窗口算法、漏桶算法、令牌桶算法等等。
漏桶算法
漏桶的意思就是水一直会进入桶里,桶按一定的速度将水漏出。对应到应用系统上就是请求会持续发到系统,但我们的系统会按照一定的速度处理,这样不会因为瞬时的高并发请求造成系统扛不住。
该算法主要是通过限制请求的处理速率来达到限流的目的。
但是如果进来的请求太多,我们的桶中装不下,则会直接拒绝这些请求,该算法没法应对请求量突发的情形。
该算法我们可以通过线程池的方式实现:
-
有限队列(桶)
-
漏出(线程池任务正常处理)
-
拒绝策略(桶满时的处理策略)
我们也可以自己实现,如下:
/**
* 漏桶算法demo.
*/
public class LeakBucketLimiter {
private Lock lock = new ReentrantLock();
//平均漏水速率
private Integer qps = 1;
//当前桶中的容量
private volatile Double curBuffer = 0.0;
//上次漏水时间
private volatile Long lastTime = 0L;
//桶的容量
private Integer bufferCap = 100;
//漏桶中是否有位置
public boolean tryAcquire() {
lock.lock();
try {
//计算当前时间和上次更新时间这段时间可以漏出多少
long now = System.currentTimeMillis();
double outflow = (double) (now - lastTime) / 1000 * qps;
if (outflow > 0) {
lastTime = now;
}
//计算桶中剩余数
curBuffer = Math.max(0, curBuffer - outflow);
//如果桶没满
if (curBuffer < bufferCap) {
bufferCap++;
return true;
} else {
return false;
}
} finally {
lock.unlock();
}
}
}
令牌桶算法
令牌桶的核心是我们以一定速率产生令牌并放到桶中,当桶满时,则新产生的令牌会被丢掉,请求来了之后先申请令牌,只有拿到令牌的请求才会继续处理,当申请时桶里没有令牌时,拒绝处理。
此算法能适当的应对请求量突发的情形,但是不能处理持续多次的突发情况。主要是前期并发量小的时候桶里的令牌不会时产时销,会有一定的积累,当请求量突然上来之后,桶里剩余令牌有盈余,能一定程度应对大量的请求,但是这次消耗后,接着的下一次突发情况桶里已经没有足够的令牌了,所以也处理不了。
我们可以利用Guava
提供的RateLimiter
实现:
//根据指定的稳定吞吐率创建RateLimiter,这里的吞吐率是指每秒多少许可数(通常是指QPS,每秒多少查询),这里是申请每秒产生10个令牌,
RateLimiter rateLimiter = RateLimiter.create(10);
//从RateLimiter获取一个许可,该方法会被阻塞直到获取到请求。如果存在等待的情况的话,告诉调用者获取到该请求所需要的睡眠时间。该方法等同于acquire(1)。
rateLimiter.acquire();
当然我们也可以自己实现:
/**
* 令牌桶算法demo.
*/
public class TokenBucketLimiter {
private Lock lock = new ReentrantLock();
//平均放入速度速率
private Integer qps = 1;
//当前桶中token个数
private volatile Double curToken = 0.0;
//上次放入token时间
private volatile Long lastTime = 0L;
//桶中能存放令牌的总数
private Integer tokenCap = 100;
//是否能获取到token
public boolean tryAcquire() {
lock.lock();
try {
//计算当前时间和上次更新时间这段时间可以产生多少令牌
long now = System.currentTimeMillis();
double produceToken = (double) (now - lastTime) / 1000 * qps;
if (produceToken > 0) {
lastTime = now;
}
//计算桶中总的令牌数
curToken = Math.min(tokenCap, curToken + produceToken);
//如果桶中有剩余令牌
if (curToken >= 1) {
curToken--;
return true;
}
} finally {
lock.unlock();
}
return false;
}
}
上述两种算法比较
差异 | 漏桶算法 | 令牌桶算法 |
---|---|---|
处理请求方面 | 按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝 | 按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求 |
限制方面 | 限制的是常量流出速率,从而平滑突发流入速率 | 限制的是平均流入速率,允许一定程度的突发 |
限流方向 | 限制的是流入 | 限制的是流出 |