NGINX并发限制原理及源码分析
1.5k 次浏览
发表于 2020-12-17 18:56

概述

上篇我们分析了NGINX速率限制的原理,本篇我们继续分析NGINX并发限制的原理,后面一篇,我们将分析NGINX带宽限制的原理。

速率限制用来限制单个客户端在单位时间的请求数目,而并发限制用来限制同一时间的连接总数。本篇文章我们对并发限制功能的原理和源代码进行分析,从而可以更好地理解和使用此功能。

原理

并发限制实现原理相对简单。它不像速率限制那样需要采用漏桶或者令牌头算法进行流量的控制和整形。它只需维护一个计数表示当前的连接数,然后在建立连接时增加计数,连接断开时减少计数。当新的连接到来时比较当前计数和配置参数的大小关系就可以决定是否可以建立连接。

配置语法

NGINX通过limit_conn_zonelimit_conn两条指令来实现并发限制。指令limit_conn_zone定义了并发限制的参数,指令limit_conn在所在的location使能并发限制。

http {

    limit_conn_zone $binary_remote_addr zone=addr:10m;

    ...

    server {

        ...

        location /download/ {

        limit_conn addr 1;

    }

}

指令limit_conn_zone

配置语法:

Syntax:limit_conn_zone key zone=name:;

Default:—

Context:http

参数key定义了基于什么样的参数进行速率控制。比如下面的例子中的$binary_remote_addr就是表示使用客户端的ip地址(remote_addr)进行速率控制。而且为了节省存储空间采用了二进制的方式表示。

参数zone定义一块共享内存区域。此共享区域用来存储各种key相关的状态信息特别是当前的连接数。参数key可以包含text,变量或者是他们的组合,但是不能为空。因为采用了共享内存的方式(关于NGINX共享内存可以参考这篇文章),所以,这些信息被所有的worker进程共用。

通过此参数可以定义共享内存的名字和大小。在上述采用$binary_remote_addr作为key的例子中,所需要的存储空间比速率控制所需要的空间要少,所以在此配置下,相同大小的内存存储的节点数目要把速率控制的节点数目多。

指令limit_conn

配置语法:

Syntax: limit_conn zone number;

Default: —

Context: http, server, location

设置共享内存zone已经基于特定key值可允许的最多连接数。当到达最大连接数,服务器将会返回错误码。而不会像速率控制那样还会试图去释放内存。

指令limit_conn_dry_run

Syntax:limit_conn_dry_run on | off;

Default:limit_conn_dry_run off;

Context:http, server, location

使能演习(dry run)模式。在这种模式下,只会把请求的状态信息保存在共享内存中,并发限制功能不会生效。

指令limit_conn_log_level

Syntax:limit_conn_log_level info | notice | warn | error;

Default:limit_conn_log_level error;

Context:http, server, location

设置与并发限制功能相关的log级别。比如设置为limit_conn_log_level notice 则并发限制相关的log会按照notice的级别进行记录。

指令limit_conn_status

Syntax:limit_conn_status code;

Default:limit_conn_status 503;

Context:http, server, location

设置NGINX拒绝连接请求时发送给客户端的HTTP代码,默认是503

配置说明

可以同时配置多条limit_conn指令。比如下面的配置会不仅会限制单个客户端到服务器的并发连接数目为10,还会限制所有客户端到server并发的连接数是100

http {

    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;

}

}

配置效果测试

现在我们通过实验来验证NGINX在不同的配置情况下的请求并发限制行为。

测试环境

客户端通过脚本发送HTTP请求到 jikui.test.com服务器的path1路径上,然后返回一个比较大的index.html文件,这样可以让一个请求持续时间相对较长,有利于我们测试观察。然后我们通过NGINX服务器的access.log来观察NGINX在不同的配置策略下的处理行为,

用来发送HTTP请求的脚本存放在https://github.com/pei-jikui/limit_req中。

场景1

指令limit_conn_zone定义了一个大小是10m的名称为one的区域用来根据客户端地址存储状态信息。

指令limit_conn引用zone one然后针对每一个客户端并发限制为1个连接。

http {

    limit_conn_zone $binary_remote_addr zone=one:10m;

    server {

        listen 2048;

        location^~/path1 {

        limit_conn one 1;

        alias /usr/local/nginx/html/index.html;

        index index.html;

    }

}

预期效果是,同一个客户只能有一个并发连接。

观察NGINXaccess.log日志,我们可以看到在第一个连接没有断开时,第二个连接被拒绝。

场景2

指令limit_conn_zone分定义了一个大小是10m的名称为one的区域用来根据客户端地址来限制并发连接和另外一个大小是10m名称为two的区域根据server_name来限制并发连接。

指令limit_conn引用zone one然后针对每一个客户端并发限制为1个连接。

第二个limit_conn引用zone two限制针对path1的并发请求总数为2个连接。

http {

limit_conn_zone $binary_remote_addr zone=one:10m;

limit_conn_zone $server_name zone=tw10m;

    server {

        listen 2048;

        location^~/path1 {

        limit_conn one 1;

        limit_conn two 2;

        alias /usr/local/nginx/html/index.html;

        index index.html;

    }

}

预期效果是,如果多个不同的客户端同时访问服务器的path1路径,每个客户只能有一个请求,总共只能有两个并行连接。

观察NGINXaccess.log日志,我们可以看到在第一个客户端的只被允许了一个请求,第二个客户端也只被允许了一个请求,而第三个客户端因为针对server URI的并行总量已经达到限制,所以,第三个客户端所有的连接都被拒绝。

源码分析

本节,我们从源代码角度去分析并发控制功能是如何实现的。

控制平面

速率控制只要是通过limit_conn_zonelimit_conn两条指令完成的。在NGINX的启动阶段对这两条指令进行解析。

指令limit_conn_zone解析流程:

1.     解析指令limit_conn_zone 的函数为ngx_http_limit_conn_zone。它会首先解析key属性以及zone的名称和大小参数,然后通过函数ngx_shared_memory_add 申请一片共享内存。并且把函数ngx_http_limit_conn_init_zone 设置为这片共享内存的初始化函数。

2.     NGINX解析完成以后,会生成系统申请的共享内存,并且调用对应的初始化函数进行初始化。对于为并发限制生成的内存会调用ngx_http_limit_conn_init_zone完成初始化工作。

3.     函数ngx_http_limit_conn_init_zone会把上述的共享内存区域初始化为红黑树用来存储连接信息。

指令limit_conn解析流程:

  1. 解析指令limit_conn的函数为ngx_http_limit_conn
  2. 函数主要的工作是解析并发数量参数。然后生成一个ngx_http_limit_conn_limit_t结构并且把并发数量参数数值存放到结构的conn成员中。
  3.  把上述的ngx_http_limit_conn_limit_t结构存放到ngx_http_limit_conn_conf_t结构中的limits数组中。这个数组存放的某一个location中所有定义的limit_conn。每一个limit_conn元素对应数组某一个元素。

指令解析完毕以后相关数据结构的关系如下图所示:

数据平面

NGINX并发限制对应的模块是ngx_http_limit_conn_module,它处于HTTP11个处理阶段的NGX_HTTP_PREACCESS_PHASE阶段,处理函数是ngx_http_limit_conn_handler

我们忽略NGINX是如何处理HTTP请求的,直接从函数ngx_http_limit_conn_handler的处理逻辑开始分析。

  1. 函数ngx_http_limit_conn_handler首先会通过http_request结构得到结构ngx_http_limit_conn_conf_t,对应上面数据结构中左上角的部分。
  2. 然后遍历ngx_http_limit_conn_conf_t结构中所有的limits数组元素(limits数组中每一个元素对应location中一条limit_conn指令)。对于每一个数组元素计算对应的key值,然后调用函数ngx_http_limit_conn_lookup 进行匹配。
  3. 如果函数ngx_http_conn_req_lookup返回空,表示没有找到合适的节点,然后就尝试去生成新的key值节点。如果共享内存区域被耗尽,生成失败,则会调用ngx_http_limit_conn_cleanup_all把已经匹配到的limitconn计数恢复。如果生成成功,则把新的节点插入红黑树。与速率控制模块不同,内存申请失败后并不会去尝试释放老内存块。
  4. 如果函数ngx_http_conn_req_lookup找到对应的key节点,判断当前的连接数(ngx_http_limit_conn_node_t结构中的conn字段)是否已经超过了配置的最大连接数(limit中的conn字段)。
  5. 如果已经超过,则调用ngx_http_limit_conn_cleanup_all把已经匹配的limitconn计数通过回调函数ngx_http_limit_conn_cleanup恢复,然后返回错误代码。
  6. 如果没有超过配置,则把当前连接数增加1并且生成一个ngx_pool_cleanup_t结构把结构的回调函数设置为ngx_http_limit_conn_cleanup。然后调用ngx_pool_cleanup_add把结构ngx_pool_cleanup_t加入到当前http请求的poolcleanup链表中。这样做的目的是,如果limits数字中有一个是元素要把当前请求拒绝,则会通过poolcleanup链表把前面已经匹配并且已经增加的连接数恢复。
  7. 在上面第6步允许当前请求的情况下,生成了ngx_pool_cleanup_t结构并且把此结构加入到当前请求的poolcleanup链表中。这样当前请求结束时,回去是否当前请求对应的pool,同时会调用poolcleanup链表中的所有节点的回调函数,对应此模块就是ngx_http_limit_conn_cleanup。此函数会把对应节点的计数减1,表示此连接对应计数释放。

结语

并发控制是NGINX流量管理功能中一个很重要的模块。通过它可以有效地减缓上游服务器的负载,合理分配各个用户的连接数目从而有效地对客户流量进行管理。从代码实现的角度上来看,它和速率控制有非常多的相似的地方,但是因为只需要简答的计数维护和比较不需要相对复杂的流控算法,所以它又比速率控制要简单很多。下篇,我们讲试着分析流量管理的最后一个功能,也就是带宽控制的原理。


发表评论
发表者

皮皮鲁

暂无个人介绍

  • 27

    文章

  • 19

    关注

  • 27

    粉丝

活动推荐
Copyright 公安部网络安全保卫局 All Rights Reserved
京公网安备 11010502047880号    京ICP备05070602号