nginx限流模块源码分析(nginx限速限流)

nginx限流模块源码分析(nginx限速限流)

浏览次数:
信息来源: 用户投稿
更新日期: 2026-04-22
文章简介

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

2025阿里云双十一服务器活动

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_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;

staticngx_int_tngx_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请求时,都会调用此方法判断应该继续执行还是拒绝请求

}

2.2nginx事件处理简单介绍

nginx需要将所有关心的fd注册到epoll,添加方法生命如下:

staticngx_int_tngx_epoll_add_event(ngx_event_t*ev,ngx_int_tevent,ngx_uint_tflags);

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

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

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

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

};

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

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

ngx_msec_tngx_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;

voidngx_event_expire_timers(void)

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));

}

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

staticngx_command_tngx_http_limit_req_commands[]={

//一般用法:limit_req_zone$binary_remote_addrzone=one:10mrate=1r/s;

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

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

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

{ngx_string("limit_req_zone"),

//用法:limit_reqzone=oneburst=5nodelay;

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

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

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

{ngx_string("limit_req"),

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

{ngx_string("limit_req_log_level"),

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

{ngx_string("limit_req_status"),

};

staticngx_http_variable_tngx_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},

}

当用户第一次请求时,会新增一条记录(主要记录访问计数、访问时间),以客户端ip地址(配置$binary_remote_addr)的hash值作为key存储在红黑树中(快速查找),同时存储在lru队列中(存储空间不够时,淘汰记录,每次都是从尾部删除);当用户再次请求时,会从红黑树中查找这条记录并更新,同时移动记录到lru队列首部;

3.2.1数据结构

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

ngx_http_limit_req_shctx_t*sh;

ngx_slab_pool_t*shpool;//内存池

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

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

ngx_str_tvar;//限流变量名称

ngx_http_limit_req_node_t*node;

}ngx_http_limit_req_ctx_t;

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

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

ngx_shm_zone_init_ptinit;//初始化方法函数指针

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

};

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

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

ngx_uint_tburst;//队列大小

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

}ngx_http_limit_req_limit_t;

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

ngx_msec_tlast;//上次访问时间

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

ngx_uint_tcount;//此类记录请求的总数

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

}ngx_http_limit_req_node_t;

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

ngx_rbtree_node_t*parent;

ngx_rbtree_trbtree;//红黑树

ngx_rbtree_node_tsentinel;//nil节点

ngx_queue_tqueue;//lru队列

}ngx_http_limit_req_shctx_t;

};

ngx_http_limit_req_ctx_t*ctx;

nginx限流模块源码分析,nginx限速限流

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

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

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

+offsetof(ngx_http_limit_req_node_t,data)

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

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

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

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

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

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

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

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

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

    staticngx_int_tngx_http_limit_req_lookup(ngx_http_limit_req_limit_t*limit,ngx_uint_thash,u_char*data,size_tlen,ngx_uint_t*ep,ngx_uint_taccount)

    if(hash<node->key){

    if(hash>node->key){

    //hash值相等,比较数据是否相等

    lr=(ngx_http_limit_req_node_t*)&node->color;

    rc=ngx_memn2cmp(data,lr->data,len,(size_t)lr->len);

    ngx_queue_remove(&lr->queue);

    ngx_queue_insert_head(&ctx->sh->queue,&lr->queue);//将记录移动到lru队列头部

    ms=(ngx_msec_int_t)(now-lr->last);//当前时间减去上次访问时间

    excess=lr->excess-ctx->rate*ngx_abs(ms)/1000+1000;//待处理请求书-限流速率*时间段+1个请求(速率,请求数等都乘以1000了)

    //待处理数目超过burst(等待队列大小),返回ngx_busy拒绝请求(没有配置burst时,值为0)

    if((ngx_uint_t)excess>limit->burst){

    if(account){//如果是最后一条限流策略,则更新上次访问时间,待处理请求数目,返回ngx_ok

    returnngx_again;//非最后一条限流策略,返回ngx_again,继续校验下一条限流策略

    node=(rc<0)?node->left:node->right;

    //假如没有查找到节点,需要新建一条记录

    //存储空间大小计算方法参照3.2.1节数据结构

    size=offsetof(ngx_rbtree_node_t,color)

    +offsetof(ngx_http_limit_req_node_t,data)

    ngx_http_limit_req_expire(ctx,1);

    node=ngx_slab_alloc_locked(ctx->shpool,size);//分配空间

    if(node==null){//空间不足,分配失败

    ngx_http_limit_req_expire(ctx,0);//强制淘汰记录

    node=ngx_slab_alloc_locked(ctx->shpool,size);//分配空间

    if(node==null){//分配失败,返回ngx_error

    lr=(ngx_http_limit_req_node_t*)&node->color;

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

    ngx_rbtree_insert(&ctx->sh->rbtree,node);//插入记录到红黑树与lru队列

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

    if(account){//如果是最后一条限流策略,则更新上次访问时间,待处理请求数目,返回ngx_ok

    returnngx_again;//非最后一条限流策略,返回ngx_again,继续校验下一条限流策略

    }

    第二个参数n,当n==0时,强制删除末尾一条记录,之后再尝试删除一条或两条记录;n==1时,会尝试删除一条或两条记录;代码实现如下:

    staticvoidngx_http_limit_req_expire(ngx_http_limit_req_ctx_t*ctx,ngx_uint_tn)

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

    lr=ngx_queue_data(q,ngx_http_limit_req_node_t,queue);

    //注意:当为0时,无法进入if代码块,因此一定会删除尾部节点;当n不为0时,进入if代码块,校验是否可以删除

    ms=(ngx_msec_int_t)(now-lr->last);

    //短时间内被访问,不能删除,直接返回

    //有待处理请求,不能删除,直接返回

    excess=lr->excess-ctx->rate*ms/1000;

    node=(ngx_rbtree_node_t*)

    ((u_char*)lr-offsetof(ngx_rbtree_node_t,color));

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

    ngx_slab_free_locked(ctx->shpool,node);

    }

    //计算当前请求还需要排队多久才能处理

    delay=ngx_http_limit_req_account(limits,n,&excess,&limit);

    if(ngx_handle_read_event(r->connection->read,0)!=ngx_ok){

    returnngx_http_internal_server_error;

    r->read_event_handler=ngx_http_test_reading;

    r->write_event_handler=ngx_http_limit_req_delay;//可写事件处理函数

    ngx_add_timer(r->connection->write,delay);//可写事件添加定时器(超时之前是不能往客户端返回的)

    计算delay的方法很简单,就是遍历所有的限流策略,计算处理完所有待处理请求需要的时间,返回最大值;

    if(limits[n].nodelay){//配置了nodelay时,请求不会被延时处理,delay为0

    delay=excess*1000/ctx->rate;

    }

    staticvoidngx_http_limit_req_delay(ngx_http_request_t*r)

    wev=r->connection->write;

    if(!wev->timedout){//没有超时不会处理

    if(ngx_handle_write_event(wev,0)!=ngx_ok){

    ngx_http_finalize_request(r,ngx_http_internal_server_error);

    r->read_event_handler=ngx_http_block_reading;

    r->write_event_handler=ngx_http_core_run_phases;

    ngx_http_core_run_phases(r);//超时了,继续处理http请求

    }

    4.实战

    4.1测试普通限流

    1)配置nginx限流速率为1qps,针对客户端ip地址限流(返回状态码默认为503),如下:

    limit_req_zone$binary_remote_addrzone=test:10mrate=1r/s;

    indexindex.htmlindex.htm;

    }

    4.2测试burst

    1)限速1qps时,超过请求会被直接拒绝,为了应对突发流量,应该允许请求被排队处理;因此配置burst=5,即最多允许5个请求排队等待处理;

    limit_req_zone$binary_remote_addrzone=test:10mrate=1r/s;

    limit_reqzone=testburst=5;

    indexindex.htmlindex.htm;

    }

    4)ab统计的响应时间见下面,最小响应时间87ms,最大响应时间5128ms,平均响应时间为1609ms:

    processing:4615661916.610935084

    waiting:4615651916.710925084

    total:8716091916.211355128

    4.3测试nodelay

    1)4.2显示,配置burst后,虽然突发请求会被排队处理,但是响应时间过长,客户端可能早已超时;因此添加配置nodelay,使得nginx紧急处理等待请求,以减小响应时间:

    limit_req_zone$binary_remote_addrzone=test:10mrate=1r/s;

    limit_reqzone=testburst=5nodelay;

    indexindex.htmlindex.htm;

    }

    4)ab统计的响应时间见下面,最小响应时间85ms,最大响应时间92ms,平均响应时间为88ms:

    total:85882.89092

    以上就是nginx限流模块源码分析的详细内容,更多请关注主机测评网其它相关文章!

    标签:
    物联网如何影响我们的生活
    « 上一篇
    返回列表
    下一篇 »

    如本文对您有帮助,就请抽根烟吧!