自适应限流
为了防止服务被大量的访问拖垮,需要进行限流措施,当服务无法承载更多请求时,直接拒绝请求。
在分布式系统中,限流的重要性往往超出想象,一个典型的场景是:
假设一个服务由多个服务器做负载均衡,在访问量过大拖垮了某个服务器后,所有的请求只能由更少的服务器承担,剩下的服务器也会很快崩溃,从而导致雪崩式的系统崩溃。
而当你试图增加服务器并将崩溃的服务器重启时,由于服务器启动时间不一样,先启动的服务器会承担很大的请求量,这会导致其迅速崩溃,这会最终导致系统无法恢复。
流量抛弃(Load Shedding)#
流量抛弃(load shedding)是指在软件服务器临近过载时,主动抛弃一定量的负载。
一种简单的流量抛弃实现方式是根据CPU使用量、内存使用量及队列长度等进行限流。
通常对于被抛弃的请求,系统会直接返回 HTTP 503(服务不可用)。
NGINX 本身自带 Rate Limiting 的功能,采用 leaky bucket 算法,但其配置是静态的,需要通过修改配置文件来适应现实情况。https://www.nginx.com/blog/rate-limiting-nginx/
自适应限流#
Google在多年的经验积累中得出:按照QPS来规划服务容量,或者是按照某种静态属性(认为其能指代处理所消耗的资源:例如某个请求所需要读取的键值数量)一般是错误的选择。 就算这个指标在某一个时间段内看起来工作还算良好,早晚也会发生变化。有些变动是逐渐发生的,有些则是非常突然的(例如某个软件的新版本突然使得某些请求消耗的资源大幅减少)。 这种不断变动的目标,使得设计和实现良好的负载均衡策略使用起来非常困难。
– 《SRE:Google运维解密》
上面的文字说明了静态配置的限流机制的问题,为了解决这个问题,有必要使用自适应限流。
目前互联网大厂普遍都采用了自适应限流的方式,但是在开源社区能找到的方案还不多。
自适应限流实现要点#
在哪些层进行限流#
在服务层进行限流可以保护单个服务不发生过载,在服务层限流可能需要根据服务的技术栈编写对于代码,例如Java服务可以通过servlet filter来实现(servlet filter无法访问等待队列,可能对限流效果有一定影响)。
客户端限流:在多服务架构中,调用者就是被调用者的客户端,在客户端限流可以进一步保护整个系统不发生全局过载的情况。在《SRE:Google运维解密》一书中有专门一节讲述了客户端限流(第21章 应对过载 / 客户端侧的节流机制)。
网关层限流:现代系统通常会有一个网关反向代理所有的服务,在网关层进行限流可以保护后面所有的服务,我觉得是性价比较高的一种方式,应该优先构建网关层限流措施,之后再进一步考虑其它层面的限流。
采用哪个指标进行限流#
常用的指标有:CPU,内存,延迟,队列长度,线程数
有些指标如CPU,内存只能在本机上获得,在网关层和客户端限流时是无法获得CPU,内存这些指标的,这时可以通过延迟和队列长度这种指标来进行限流。
限流算法#
如果使用CPU或内存作为指标,算法可能相对简单一些,因为只需要设定一个合理的阈值,当超过时开始进行限流就可以。
如果使用延迟作为指标(在网关层通常只能使用延迟做指标),则可以考虑下面的算法:
和性增长/乘性降低算法(additive-increase/multiplicative-decrease、AIMD),该算法被用于TCP的拥塞控制,比较易于理解。
Vegas Algorithm,该算法也是一种TCP的拥塞控制算法,与AIMD不同的是该算法配置更少,另外该算法试图在过载发生前就进行限流,而不是过载真正发生时。
Gradient Algorithm(不确定是否就是梯度下降法),可以更直观的进行解释和理解,根据Google的经验,限流算法中便于理解的算法比复杂的算法更加可控一些。
这三种算法都是在Netflix开源的Java限流库concurrency-limits中实现的,这篇文档对此有具体的说明:https://vikas-kumar.medium.com/handling-overload-with-concurrency-control-and-load-shedding-part-2-6b8b594d4405。
参考资料#
下面这篇文章描述了用Lua在OpenResty中实现自适应限流的方案:
https://tech.olx.com/load-shedding-with-nginx-using-adaptive-concurrency-control-part-2-d4e4ddb853be
下面是一个系列的文章深入讨论了自适应限流的话题:
《SRE:Google运维解密》:
- 第21章 应对过载
- 第22章 处理连锁故障 / 防止软件服务器过载 / 流量抛弃和优雅降级