写点什么

Java 必备技能之源码篇(Nginx 源码研究之 nginx 限流模块)

  • 2022 年 5 月 16 日
  • 本文字数:6277 字

    阅读完需:约 21 分钟

2.nginx 基础知识




Nginx 主要有两种限流方式:按连接数限流(ngx_http_limit_conn_module)、按请求速率限流(ngx_http_limit_req_module);


学习限流模块之前还需要了解 nginx 对 HTTP 请求的处理过程,nginx 事件处理流程等;


2.1HTTP 请求处理过程


nginx 将 HTTP 请求处理流程分为 11 个阶段,绝大多数 HTTP 模块都会将自己的 handler 添加到某个阶段(其中有 4 个阶段不能添加自定义 handler),nginx 处理 HTTP 请求时会挨个调用所有的 handler;


NGX_HTTP_PREACCESS_PHASE, ?//访问控制,限流模块会注册 handler 到此阶段(本文重点限流 ,其他不看,熟悉大体流程即可)!!!


typedef enum {


NGX_HTTP_POST_READ_PHASE = 0, //目前只有 realip 模块会注册 handler(nginx 作为代理服务器时有用,后端以此获取客户端原始 ip)


NGX_HTTP_SERVER_REWRITE_PHASE, //server 块中配置了 rewrite 指令,重写 url


NGX_HTTP_FIND_CONFIG_PHASE, //查找匹配 location;不能自定义 handler;


NGX_HTTP_REWRITE_PHASE, //location 块中配置了 rewrite 指令,重写 url


NGX_HTTP_POST_REWRITE_PHASE, //检查是否发生了 url 重写,如果有,重新回到 FIND_CONFIG 阶段;不能自定义 handler;


NGX_HTTP_PREACCESS_PHASE, //访问控制,限流模块会注册 handler 到此阶段


NGX_HTTP_ACCESS_PHASE, //访问权限控制


NGX_HTTP_POST_ACCESS_PHASE, //根据访问权限控制阶段做相应处理;不能自定义 handler;


NGX_HTTP_TRY_FILES_PHASE, //只有配置了 try_files 指令,才会有此阶段;不能自定义 handler;


NGX_HTTP_CONTENT_PHASE, //内容产生阶段,返回响应给客户端


NGX_HTTP_LOG_PHASE //日志记录


} ngx_http_phases;


nginx 使用结构体 ngx_module_s 表示一个模块,其中字段 ctx,是一个指向模块上下文结构体的指针;nginx 的 HTTP 模块上下文结构体如下所示(上下文结构体的字段都是一些函数指针):


该结构体是整个 Nginx 模块化架构最基本的数据结构体。它描述了 Nginx 程序中一个模块应该包括的基本属性,在 tengine/src/core/ngx_conf_file.h 中定义了该结构体


其?结构?定义如下,其中的注释为功能说明: (需要熟悉 c 语言的结构体类型,类似 java 的类)


struct ngx_module_s {


ngx_uint_t ctx_index;


/*分类的模块计数器


nginx 模块可以分为四种:core、event、http 和 mail


每个模块都会各自计数,ctx_index 就是每个模块在其所属类组的计数*/


ngx_uint_t index;


/一个模块计数器,按照每个模块在 ngx_modules[]数组中的声明顺序,从 0 开始依次给每个模块赋值/


ngx_uint_t spare0;


ngx_uint_t spare1;


ngx_uint_t spare2;


ngx_uint_t spare3;


ngx_uint_t version; //nginx 模块版本


void *ctx;


/模块的上下文,不同种类的模块有不同的上下文,因此实现了四种结构体/


ngx_command_t *commands;


/*命令定义地址


模块的指令集


每一个指令在源码中对应着一个 ngx_command_t 结构变量*/


ngx_uint_t type; //模块类型,用于区分 core event http 和 mail


ngx_int_t (*init_master)(ngx_log_t *log); //初始化 master 时执行


ngx_int_t (*init_module)(ngx_cycle_t *cycle); //初始化 module 时执行


ngx_int_t (*init_process)(ngx_cycle_t *cycle); //初始化 process 时执行


ngx_int_t (*init_thread)(ngx_cycle_t *cycle); //初始化 thread 时执行


void (*exit_thread)(ngx_cycle_t *cycle); //退出 thread 时执行


void (*exit_process)(ngx_cycle_t *cycle); //退出 process 时执行


void (*exit_master)(ngx_cycle_t *cycle); //退出 master 时执行


//以下功能不明


uintptr_t spare_hook0;


uintptr_t spare_hook1;


uintptr_t spare_hook2;


uintptr_t spare_hook3;


uintptr_t spare_hook4;


uintptr_t spare_hook5;


uintptr_t spare_hook6;


uintptr_t spare_hook7;


};


typedef struct ngx_module_s ngx_module_t;


nginx 定义了很多模块,每个模块都有自己所属的类型,可以定义一些属于自己的操作函数,通过把这些函数赋值为对应类型?结构?体中的函数指针,就可以注册了不同的回调函数接口,以此实现不同的功能,有点类似 java 多态


typedef struct {


ngx_int_t (*preconfiguration)(ngx_conf_t *cf);


ngx_int_t (*postconfiguration)(ngx_conf_t *cf); //此方法注册 handler 到相应阶段


void *(*create_main_conf)(ngx_conf_t *cf); //http 块中的主配置


char *(*init_main_conf)(ngx_conf_t *cf, void *conf);


void *(*create_srv_conf)(ngx_conf_t *cf); //server 配置


char *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);


void *(*create_loc_conf)(ngx_conf_t *cf); //location 配置


char *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);


} ngx_http_module_t;


以 ngx_http_limit_req_module 模块为例,postconfiguration 方法简单实现如下:(本文重点是限流)


static ngx_int_t ngx_http_limit_req_init(ngx_conf_t *cf)


{


h = ngx_array_push(&cmcf->phases[NGX_HTTP_PREACCESS_PHASE].handlers);


*h = ngx_http_limit_req_handler; //ngx_http_limit_req_module 模块的限流方法;nginx 处理 HTTP 请求时,都会调用此方法判断应该继续执行还是拒绝请求


return NGX_OK;


}


2.2 nginx 事件处理简单介绍


假设 nginx 使用的是 epoll 【不同 linux 发行版本不同,centos 是 epoll 的 IO 模型】。


nginx 需要将所有关心的 fd(file?describe?文件描述符【linux 基于文件表示网络 io 资源】)注册到 epoll,添加方法声明如下:


static ngx_int_t ngx_epoll_add_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);


方法第一个参数是 ngx_event_t 结构体指针,代表关心的一个读或者写事件;nginx 为事件可能会设置一个超时定时器,从而能够处理事件超时情况;定义如下:


struct ngx_event_s {


ngx_event_handler_pt handler; //函数指针:事件的处理函数


ngx_rbtree_node_t timer; //超时定时器,存储在红黑树中(节点的 key 即为事件的超时时间)


unsigned timedout:1; //记录事件是否超时


};


一般都会循环调用 epoll_wait 监听所有 fd,处理发生的读写事件;epoll_wait 是阻塞调用,最后一个参数 timeout 是超时时间,即最多阻塞 timeout 时间如果还是没有事件发生,方法会返回;


nginx 在设置超时时间 timeout 时,会从上面说的记录超时定时器的红黑树中查找最近要过时的节点,以此作为 epoll_wait 的超时时间,如下面代码所示;


ngx_msec_t ngx_event_find_timer(void)


{


node = ngx_rbtree_min(root, sentinel);


timer = (ngx_msec_int_t) (node->key - ngx_current_msec);


return (ngx_msec_t) (timer > 0 ? timer : 0);


}


同时 nginx 在每次循环的最后,会从红黑树中查看是否有事件已经过期,如果过期,标记 timeout=1,并调用事件的 handler;


void ngx_event_expire_timers(void)


{


for ( ;; ) {


node = ngx_rbtree_min(root, sentinel);


if ((ngx_msec_int_t) (node->key - ngx_current_msec) <= 0) { //当前事件已经超时


ev = (ngx_event_t *) ((char *) node - offsetof(ngx_event_t, timer));


ev->timedout = 1;


ev->handler(ev);


continue;


}


break;


}


}


nginx 就是通过上面的方法实现了 socket 事件的处理,定时事件的处理;


ngx_http_limit_req_module 模块解析


ngx_http_limit_req_module 模块是对请求进行限流,即限制某一时间段内用户的请求速率;并且使用的是令牌桶算法;


3.1 配置指令


ngx_http_limit_req_module 模块提供一些配置指令,供用户配置限流策略


//每个配置指令主要包含两个字段:名称,解析配置的处理方法


static ngx_command_t ngx_http_limit_req_commands[] = {


//一般用法:limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;


//$binary_remote_addr 表示远程客户端 IP;


//zone 配置一个存储空间(需要分配空间记录每个客户端的访问速率,超时空间限制使用 lru 算法淘汰;注意此空间是在共享内存分配的,所有 worker 进程都能访问)


//rate 表示限制速率,此例为 1qps


{ ngx_string("limit_req_zone"),


ngx_http_limit_req_zone,


},


//用法:limit_req zone=one burst=5 nodelay;


//zone 指定使用哪一个共享空间


//超出此速率的请求是直接丢弃吗?burst 配置用于处理突发流量,表示最大排队请求数目,当客户端请求速率超过限流速率时,请求会排队等待;而超出 burst 的才会被直接拒绝;


//nodelay 必须与 burst 一起使用;此时排队等待的请求会被优先处理;否则假如这些请求依然按照限流速度处理,可能等到服务器处理完成后,客户端早已超时


{ ngx_string("limit_req"),


ngx_http_limit_req,


},


//当请求被限流时,日志记录级别;用法:limit_req_log_level info | notice | warn | error;


{ ngx_string("limit_req_log_level"),


ngx_conf_set_enum_slot,


},


//当请求被限流时,给客户端返回的状态码;用法:limit_req_status 503


{ ngx_string("limit_req_status"),


ngx_conf_set_num_slot,


},


};


注意:$binary_remote_addr 是 nginx 提供的变量,用户在配置文件中可以直接使用;nginx 还提供了许多变量,在 ngx_http_variable.c 文件中查找 ngx_http_core_variables 数组即可:


static ngx_http_variable_t ngx_http_core_variables[] = {


{ ngx_string("http_host"), NULL, ngx_http_variable_header,


offsetof(ngx_http_request_t, headers_in.host), 0, 0 },


{ ngx_string("http_user_agent"), NULL, ngx_http_variable_header,


offsetof(ngx_http_request_t, headers_in.user_agent), 0, 0 },


…………


}


3.2 源码解析


ngx_http_limit_req_module 在 postconfiguration 过程会注册 ngx_http_limit_req_handler 方法到 HTTP 处理的 NGX_HTTP_PREACCESS_PHASE 阶段;


ngx_http_limit_req_handler 会执行漏桶算法,判断是否超出配置的限流速率,从而进行丢弃或者排队或者通过;


当用户第一次请求时,会新增一条记录(主要记录访问计数、访问时间),以客户端 IP 地址(配置 $binary_remote_addr)的 hash 值作为 key 存储在红黑树中(快速查找),同时存储在 LRU 队列中(存储空间不够时,淘汰记录,每次都是从尾 《一线大厂 Java 面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 部删除);当用户再次请求时,会从红黑树中查找这条记录并更新,同时移动记录到 LRU 队列首部;


3.2.1 数据结构


limit_req_zone 配置限流算法所需的存储空间(名称及大小),限流速度,限流变量(客户端 IP 等),结构如下:


typedef struct {


ngx_http_limit_req_shctx_t *sh;


ngx_slab_pool_t *shpool;//内存池


ngx_uint_t rate; //限流速度(qps 乘以 1000 存储)


ngx_int_t index; //变量索引(nginx 提供了一系列变量,用户配置的限流变量索引)


ngx_str_t var; //限流变量名称


ngx_http_limit_req_node_t *node;


} ngx_http_limit_req_ctx_t;


//同时会初始化共享存储空间


struct ngx_shm_zone_s {


void *data; //data 指向 ngx_http_limit_req_ctx_t 结构


ngx_shm_t shm; //共享空间


ngx_shm_zone_init_pt init; //初始化方法函数指针


void *tag; //指向 ngx_http_limit_req_module 结构体


};


limit_req 配置限流使用的存储空间,排队队列大小,是否紧急处理,结构如下:


typedef struct {


ngx_shm_zone_t *shm_zone; //共享存储空间


ngx_uint_t burst; //队列大小


ngx_uint_t nodelay; //有请求排队时是否紧急处理,与 burst 配合使用(如果配置,则会紧急处理排队请求,否则依然按照限流速度处理)


} ngx_http_limit_req_limit_t;



前面说过用户访问记录会同时存储在红黑树与 LRU 队列中,结构如下:


//记录结构体


typedef struct {


u_char color;


u_char dummy;


u_short len; //数据长度


ngx_queue_t queue;


ngx_msec_t last; //上次访问时间


ngx_uint_t excess; //当前剩余待处理的请求数(nginx 用此实现令牌桶限流算法)


ngx_uint_t count; //此类记录请求的总数


u_char data[1];//数据内容(先按照 key(hash 值)查找,再比较数据内容是否相等)


} ngx_http_limit_req_node_t;


//红黑树节点,key 为用户配置限流变量的 hash 值;


struct ngx_rbtree_node_s {


ngx_rbtree_key_t key;


ngx_rbtree_node_t *left;


ngx_rbtree_node_t *right;


ngx_rbtree_node_t *parent;


u_char color;


u_char data;


};


typedef struct {


ngx_rbtree_t rbtree; //红黑树


ngx_rbtree_node_t sentinel; //NIL 节点


ngx_queue_t queue; //LRU 队列


} ngx_http_limit_req_shctx_t;


//队列只有 prev 和 next 指针


struct ngx_queue_s {


ngx_queue_t *prev;


ngx_queue_t *next;


};


思考 1:ngx_http_limit_req_node_t 记录通过 prev 和 next 指针形成双向链表,实现 LRU 队列;最新访问的节点总会被插入链表头部,淘汰时从尾部删除节点;



ngx_http_limit_req_ctx_t *ctx;


ngx_queue_t *q;


q = ngx_queue_last(&ctx->sh->queue);


lr = ngx_queue_data(q, ngx_http_limit_req_node_t, queue);//此方法由 ngx_queue_t 获取 ngx_http_limit_req_node_t 结构首地址,实现如下:


#define ngx_queue_data(q, type, link) (type *) ((u_char *) q - offsetof(type, link)) //queue 字段地址减去其在结构体中偏移,为结构体首地址


思考 2:限流算法首先使用 key 查找红黑树节点,从而找到对应的记录,红黑树节点如何与记录 ngx_http_limit_req_node_t 结构关联起来呢?在 ngx_http_limit_req_module 模块可以找到以下代码:


size = offsetof(ngx_rbtree_node_t, color) //新建记录分配内存,计算所需空间大小


  • offsetof(ngx_http_limit_req_node_t, data)

  • len;


node = ngx_slab_alloc_locked(ctx->shpool, size);


node->key = hash;


lr = (ngx_http_limit_req_node_t *) &node->color; //color 为 u_char 类型,为什么能强制转换为 ngx_http_limit_req_node_t 指针类型呢?


lr->len = (u_char) len;


lr->excess = 0;


ngx_memcpy(lr->data, data, len);


ngx_rbtree_insert(&ctx->sh->rbtree, node);


ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);


通过分析上面代码,ngx_rbtree_node_s 结构体的 color 与 data 字段其实是无意义的,结构体的生命形式与最终存储形式是不同的,nginx 最终使用以下存储形式存储每条记录;



3.2.2 限流算法


上面提到在 postconfiguration 过程会注册 ngx_http_limit_req_handler 方法到 HTTP 处理的 NGX_HTTP_PREACCESS_PHASE 阶段;


因此在处理 HTTP 请求时,会执行 ngx_http_limit_req_handler 方法判断是否需要限流;


3.2.2.1 漏桶算法实现


用户可能同时配置若干限流,因此对于 HTTP 请求,nginx 需要遍历所有限流策略,判断是否需要限流;


ngx_http_limit_req_lookup 方法实现了漏桶算法,方法返回 3 种结果:


  • NGX_BUSY:请求速率超出限流配置,拒绝请求;

  • NGX_AGAIN:请求通过了当前限流策略校验,继续校验下一个限流策略;

  • NGX_OK:请求已经通过了所有限流策略的校验,可以执行下一阶段;

  • NGX_ERROR:出错


//limit,限流策略;hash,记录 key 的 hash 值;data,记录 key 的数据内容;len,记录 key 的数据长度;ep,待处理请求数目;account,是否是最后一条限流策略


static ngx_int_t ngx_http_limit_req_lookup(ngx_http_limit_req_limit_t *limit, ngx_uint_t hash, u_char *data, size_t len, ngx_uint_t *ep, ngx_uint_t account)


{


//红黑树查找指定界定


while (node != sentinel) {


if (hash < node->key) {


node = node->left;


continue;

用户头像

还未添加个人签名 2022.04.13 加入

还未添加个人简介

评论

发布
暂无评论
Java必备技能之源码篇(Nginx源码研究之nginx限流模块)_Java_爱好编程进阶_InfoQ写作社区