限流
一般开发高并发系统常见的限流有:限制总并发数(比如数据库连接池、线程池)、限制瞬时并发数(如Nginx的limit_conn模块,用来限制瞬时并发连接数)、限制时间窗口内的平均速率(如Guava的RateLimiter、Nginx的limit_req模块,用来限制每秒的平均速率),以及限制远程接口调用速率、限制MQ的消费速率等。另外,还可以根据网络连接数、网络流量、CPU或内存负载等来限流。
限流算法
令牌桶算法
令牌桶算法,是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌:
- 假设限制2r/s,则按照500毫秒的固定速率往桶中添加令牌。
- 桶中最多存放b 个令牌,当桶满时,新添加的令牌被丢弃或拒绝。
- 当一个n 个字节大小的数据包到达,将从桶中删除n 个令牌,接着数据包被发送到网络上。
- 如果桶中的令牌不足n 个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么在缓冲区等待)。
漏桶算法
漏桶作为计量工具(The Leaky Bucket Algorithm as a Meter)时,可以用于流量整形(Traffic Shaping)和流量控制(Traffic Policing):
- 一个固定容量的漏桶,按照常量固定速率流出水滴。
- 如果桶是空的,则不需流出水滴。
- 可以以任意速率流入水滴到漏桶。
- 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。
两者区别:
-
令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时,则拒绝新的请求。
-
漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝。
-
令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,或4个令牌),并允许一定程度的突发流量。
-
漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),从而平滑突发流入速率。
-
令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率。
-
两个算法实现可以一样,但是方向是相反的,对于相同的参数得到的限流效果是一样的。
分布式限流
分布式限流最关键的是要将限流服务做成原子化,而解决方案可以使用Redis+Lua或者Nginx+Lua技术进行实现,通过这两种技术可以实现高并发和高性能。
Redis+Lua实现
Nginx+Lua实现
实现中我们需要使用lua-resty-lock互斥锁模块来解决原子性问题(在实际工程中使用时请考虑获取锁的超时问题),并使用ngx.shared.DICT共享字典来实现计数器。如果需要限流,则返回0,否则返回1。使用时需要先定义两个共享字典(分别用来存放锁和计数器数据)。
http {
......
lua_shared_dict locks 10s;
lus_shared_dict limit_counter 10m;
}
接入层限流
在流量入口一般能做很多事情::负载均衡、非法请求过滤、请求聚合、缓存、降级、限流、A/B测试、服务质量监控等。
对于Nginx接入层限流可以使用Nginx自带的两个模块:连接数限流模块ngx_http_limit_conn_module和漏桶算法实现的请求限流模块ngx_http_limit_req_module。还可以使用OpenResty提供的Lua限流模块lua-resty-limit-traffic应对更复杂的限流场景。
limit_conn用来对某个key对应的总的网络连接数进行限流,可以按照如IP、域名维度进行限流。limit_req用来对某个key对应的请求的平均速率进行限流,有两种用法:平滑模式(delay)和允许突发模式(nodelay)。
http {
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn_log_level error;
limit_conn_status 503;
...
server {
...
location /download/ {
limit_conn addr 1;
}
http://nginx.org/en/docs/http/ngx_http_limit_conn_module.html 官方文档
- limit_conn:共享内存区域和给定键值的最大允许连接数。当超过此限制时,服务器将返回错误以回复请求。此处指定的最大连接数是1,表示Nginx最多同时并发处理1个连接。
- limit_conn_zone:用来配置限流key及存放key对应信息的共享内存区域大小。此处的key是“$binary_remote_addr”,表示IP地址,也可以使用$server_name作为key来限制域名级别的最大连接数。
- limit_conn_status: 配置被限流后返回的状态码,默认返回503。
- limit_conn_log_level: 配置记录被限流后的日志级别,默认error级别。
limit_conn_log_level: info | notice | warn | error;
ngx_http_limit_req_module
limit_req是漏桶算法实现,用于对指定key对应的请求进行限流,比如,按照IP维度限制请求速率。配置示例如下:
http {
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
...
server {
...
location /search/ {
limit_req zone=one burst=5 nodelay;
}
http://nginx.org/en/docs/http/ngx_http_limit_req_module.html 官方文档
- limit_req: 配置限流区域、桶容量(突发容量,默认为0)、是否延迟模式(默认延迟)。
设置共享内存区域和请求的最大突发大小。如果请求速率超过为区域配置的速率,则会延迟其处理,以便以定义的速率处理请求。过多的请求将被延迟,直到其数量超过最大突发大小为止,在这种情况下,该请求将因错误而终止 。nodelay
- limit_req_zone: 配置限流key、存放key对应信息的共享内存区域大小、固定请求速率。此处指定的key是“$binary_remote_addr”,表示IP地址。固定请求速率使用rate参数配置,支持10r/s和60r/m,即每秒10个请求和每分钟60个请求。不过,最终都会转换为每秒的固定请求速率(10r/s为每100毫秒处理一个请求,60r/m为每1000毫秒处理一个请求)。
limit_req的主要执行过程:
- 请求进入后首先判断最后一次请求时间相对于当前时间(第一次是0)是否需要限流,如果需要限流,则执行步骤2,否则执行步骤3。
- 如果没有配置桶容量(burst),则桶容量为0,按照固定速率处理请求。如果请求被限流,则直接返回相应的错误码(默认为503)。
a.如果配置了桶容量(burst>0)及延迟模式(没有配置nodelay)。如果桶满了,则新进入的请求被限流。如果没有满,则请求会以固定平均速率被处理(按照固定速率并根据需要延迟处理请求,延迟使用休眠实现)。
b.如果配置了桶容量(burst>0)及非延迟模式(配置了nodelay),则不会按照固定速率处理请求,而是允许突发处理请求。如果桶满了,则请求被限流,直接返回相应的错误码。 - 如果没有被限流,则正常处理请求。
- Nginx会在相应时机选择一些(3个节点)限流key进行过期处理,进行内存回收。
lua-resty-limit-traffic
如果想根据实际情况变化key、速率、桶大小等这种动态特性,那么使用标准模块就很难去实现了。
OpenResty提供了Lua限流模块lua-resty-limit-traffic,通过它可以按照更复杂的业务逻辑进行动态限流处理。其提供了limit.conn和limit.req实现,算法与nginx limit_conn和limit_req是一样的。
节流
有时候我们想在特定时间窗口内对重复的相同事件最多只处理一次,或者想限制多个连续相同事件最小执行时间间隔,那么可使用节流(Throttle)实现,其防止多个相同事件连续重复执行。节流主要有如下几种用法:throttleFirst、throttleLast、throttleWithTimeout。
throttleFirst/throttleLast: 指在一个时间窗口内,如果有重复的多个相同事件要处理,则只处理第一个或最后一个。其相当于一个事件频率控制器,把一段时间内重复的多个相同事件变为一个,减少事件处理频率,从而减少无用处理,提升性能。
throttleWithTimeout:限制两个连续事件的先后执行时间不得小于某个时间窗口。当两个连续事件的间隔时间小于最小间隔时间窗口,就会丢弃上一个事件,而如果最后一个事件等待了最小间隔时间窗口后还没有新的事件到来,那么会处理最后一个事件。
如搜索关键词自动补全,如果用户每录入一个字就发送一次请求,而先输入的字的自动补全会被很快到来的下一个字符覆盖,那么会导致先期的自动补全是无用的。throttleWithTimeout就是来解决这个问题的,通过它来减少频繁的网络请求,避免每输入一个字就导致一次请求。