Nginx 限流模块

2,835 阅读6分钟

【转载请注明出处】:juejin.cn/post/684490…

生活中的 “限流”?

限流并非新鲜事,在生活中亦无处不在,下面例举一二:

  • 博物馆:限制每天参观总人数以保护文物
  • 高铁安检:有若干安检口,旅客依次排队,工作人员根据安检快慢决定是否放人进去。遇到节假日,可以增加安检口来提高处理能力(横向拓展),同时增加排队等待区长度(缓存待处理任务)。
  • 办理银行业务:所有人先领号,各窗口叫号处理。每个窗口处理速度根据客户具体业务而定,所有人排队等待叫号即可。若快下班时,告知客户明日再来(拒绝流量)。
  • 水坝泄洪:水坝可以通过闸门控制泄洪速度(控制处理速度)。

在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流

  • 缓存:缓存的目的是提升系统访问速度和增大系统处理容量
  • 降级:降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行
  • 限流:限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理

两大限流算法

常用的限流算法有令牌桶和和漏桶,而Google开源项目Guava中的RateLimiter使用的就是令牌桶控制算法。

漏桶算法

把请求比作是水,水来了都先放进桶里,并以限定的速度出水,当水来得过猛而出水不够快时就会导致水直接溢出,即拒绝服务。

image.png
漏斗有一个进水口 和 一个出水口,出水口以一定速率出水,并且有一个最大出水速率:

在漏斗中没有水的时候

  • 如果进水速率小于等于最大出水速率,那么,出水速率等于进水速率,此时,不会积水
  • 如果进水速率大于最大出水速率,那么,漏斗以最大速率出水,此时,多余的水会积在漏斗中

在漏斗中有水的时候

  • 出水口以最大速率出水
  • 如果漏斗未满,且有进水的话,那么这些水会积在漏斗中
  • 如果漏斗已满,且有进水的话,那么这些水会溢出到漏斗之外
令牌桶算法

对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。

image.png

令牌桶算法的原理是系统以恒定的速率产生令牌,然后把令牌放到令牌桶中,令牌桶有一个容量,当令牌桶满了的时候,再向其中放令牌,那么多余的令牌会被丢弃;当想要处理一个请求的时候,需要从令牌桶中取出一个令牌,如果此时令牌桶中没有令牌,那么则拒绝该请求。

令牌桶算法VS漏桶算法

漏桶 漏桶的出水速度是恒定的,那么意味着如果瞬时大流量的话,将有大部分请求被丢弃掉(也就是所谓的溢出)。

令牌桶 生成令牌的速度是恒定的,而请求去拿令牌是没有速度限制的。这意味,面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,而且拿令牌的过程并不是消耗很大的事情。

Nginx限流

Nginx官方版本限制IP的连接和并发分别有两个模块:

  • limit_req_zone 用来限制单位时间内的请求数,即速率限制,采用的漏桶算法 "leaky bucket"。
  • limit_req_conn 用来限制同一时间连接数,即并发限制。

ngx_http_limit_req_module 模块

Nginx按请求速率限速模块使用的是漏桶算法,即能够强行保证请求的实时处理速度不会超过设置的阈值。 指令

Syntax: limit_req zone=name [burst=number] [nodelay | delay=number]; Default: — Context: http, server, location

正常流量

limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

  • key :定义限流对象,binary_remote_addr 是一种key,表示基于 remote_addr(客户端IP) 来做限流,binary_ 的目的是压缩内存占用量。
  • zone:定义共享内存区来存储访问信息, one:10m 表示一个大小为10M,名字为one的内存区域。1M能存储16000 IP地址的访问信息,10M可以存储16W IP地址访问信息。
  • rate 用于设置最大访问速率,rate=10r/s 表示每秒最多处理10个请求。Nginx 实际上以毫秒为粒度来跟踪请求信息,因此 10r/s 实际上是限制:每100毫秒处理一个请求。这意味着,自上一个请求处理完后,若后续100毫秒内又有请求到达,将拒绝处理该请求。如果限制的频率低于1r/s,则可以使用r/m,如30r/m。
突发流量

limit_req zone=one burst=5 nodelay;

  • zone=one 设置使用哪个配置区域来做限制,与上面limit_req_zone 里的name对应。
  • burst=5,burst爆发的意思,这个配置的意思是设置一个大小为5的缓冲区当有大量请求(爆发)过来时,超过了访问频次限制的请求可以先放到这个缓冲区内。
  • nodelay,如果设置,超过访问频次而且缓冲区也满了的时候就会直接返回503,如果没有设置,则所有请求会等待排队。
日志级别

为服务器由于超过频次或延迟处理而拒绝处理请求的情况设置所需的日志记录级别。延迟的日志记录级别比拒绝的日志记录级别低1级;例如,“limit_req_log_level notice” 的延迟日志记录级别是info。

Syntax: limit_req_log_level info | notice | warn | error; Default: limit_req_log_level error; Context: http, server, location

拒绝响应状态码

Syntax: limit_req_status code; Default: limit_req_status 503; Context: http, server, location

案例
http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; 
    server {
        location /search/ {
            limit_req zone=one; 
        }
}       

限流速度为每秒10次请求,如果有10次请求同时到达一个空闲的nginx,他们都能得到执行吗?

image.png

漏桶漏出请求是匀速的。10r/s是怎样匀速的呢?每100ms漏出一个请求。在这样的配置下,桶是空的,所有不能实时漏出的请求,都会被拒绝掉。所以如果10次请求同时到达,那么只有一个请求能够得到执行,其它的,都会被拒绝。 这不太友好,大部分业务场景下我们希望这10个请求都能得到执行,添加突发流量处理机制。

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; 
    server {
        location /search/ {
            limit_req zone=one burst=12; 
        }
}       

burst=12 漏桶的大小设置为12。

image.png

逻辑上叫漏桶,实现起来是FIFO队列,把得不到执行的请求暂时缓存起来。这样漏出的速度仍然是100ms一个请求,但就并发而言,暂时得不到执行的请求,可以先缓存起来。只有当队列满了的时候,才会拒绝接受新请求。这样漏桶在限流的同时,也起到了削峰填谷的作用。

在这样的配置下,如果有10次请求同时到达,它们会依次执行,每100ms执行1个。虽然得到执行了,但因为排队执行,延迟大大增加,在很多场景下仍然是不能接受的。继续修改配置,解决Delay太久导致延迟增加的问题。

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; 
    server {
        location /search/ {
            limit_req zone=one burst=12 nodelay;
        }
}       

nodelay 把开始执行请求的时间提前,以前是delay到从桶里漏出来才执行,现在不delay了,只要入桶就开始执行。

image.png

要么立刻执行,要么被拒绝,请求不会因为限流而增加延迟了。因为请求从桶里漏出来还是匀速的(100ms释放1个),桶的空间又是固定的,最终平均下来,还是每秒执行了10次请求,限流的目的还是达到了。 但是请注意,虽然设置burst和nodelay能够降低突发请求的处理时间,但是长期来看并不会提高吞吐量的上限,长期吞吐量的上限是由rate决定的,因为nodelay只能保证burst的请求被立即处理,但Nginx会限制队列元素释放的速度,就像是限制了令牌桶中令牌产生的速度。

但这样也有缺点,限流是限了,但是限得不那么匀速。以上面的配置举例,如果有12个请求同时到达,那么这12个请求都能够立刻执行,然后后面的请求只能匀速进桶,100ms执行1个。如果有一段时间没有请求,桶空了,那么又可能出现并发的12个请求一起执行。 大部分情况下,这种限流不匀速,不算是大问题。不过nginx也提供了一个参数控制并发执行也就是nodelay的请求的数量。

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; 
    server {
        location /search/ {
            limit_req zone=one burst=12 delay=4;
        }
}       

delay=4 从桶内第5个请求开始delay

image.png

这样通过控制delay参数的值,可以调整允许并发执行的请求的数量,使得请求变的均匀起来,在有些耗资源的服务上控制这个数量,还是有必要的。

ngx_http_limit_conn_module 模块

这个模块用来限制单个IP的请求数。并非所有的连接都被计数。只有在服务器处理了请求并且已经读取了整个请求头时,连接才被计数。

Syntax: limit_conn zone number; Default: — Context: http, server, location

如:

limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;

server {
    ...
    limit_conn perip 10;
    limit_conn perserver 100;
}
  • limit_conn perip 10 作用的key 是 $binary_remote_addr,表示限制单个IP同时最多能持有10个连接。

  • limit_conn perserver 100 作用的key是 $server_name,表示虚拟主机(server) 同时能处理并发连接的总数。

需要注意的是:只有当 request header 被后端server处理后,这个连接才进行计数。

日志级别

Syntax: limit_conn_log_level info | notice | warn | error; Default: limit_conn_log_level error; Context: http, server, location

拒绝响应状态码

Syntax: limit_conn_status code; Default: limit_conn_status 503; Context: http, server, location

设置白名单

限流主要针对外部访问,内网访问相对安全,可以不做限流,通过设置白名单即可。利用 Nginx ngx_http_geo_module 和 ngx_http_map_module 两个工具模块即可搞定。

    geo $limit {
        default 1;
        10.0.0.0/8 0;
        192.168.0.0/24 0;
        172.20.0.35 0;
    }
    map $limit $limit_key {
        0 "";
        1 $binary_remote_addr;
    }
    limit_req_zone $limit_key zone=myRateLimit:10m rate=10r/s;

geo 对于白名单(子网或IP都可以) 将返回0,其他IP将返回1。

map 将 $limit 转换为 $limit_key,如果是 $limit 是0(白名单),则返回空字符串;如果是1,则返回客户端实际IP。

limit_req_zone 限流的key不再使用 $binary_remote_addr,而是 $limit_key 来动态获取值。如果是白名单,limit_req_zone 的限流key则为空字符串,将不会限流;若不是白名单,将会对客户端真实IP进行限流。

限制数据传输速度

除限流外,ngx_http_core_module 还提供了限制数据传输速度的能力(即常说的下载速度)。

例如:

location /flv/ {
        flv;
        limit_rate_after 20m;
        limit_rate       100k;
    }

这个限制是针对每个请求的,表示客户端下载前20M时不限速,后续限制100kb/s。

限制特定UA

可以限制特定UA(比如爬虫)的访问

limit_req_zone  $anti_spider  zone=one:10m   rate=10r/s;
limit_req zone=one burst=100 nodelay;
if ($http_user_agent ~* (YisouSpider|Scrapy)) {
    set $anti_spider $http_user_agent;
}

【转载请注明出处】:juejin.cn/post/684490…