18. Nginx 限流模块源码深度解析:limit_conn / limit_req / limit_rate 实现原理

📅 2026/7/15 19:06:14
18. Nginx 限流模块源码深度解析:limit_conn / limit_req / limit_rate 实现原理
1. 引言在高并发场景下限流是保障系统稳定性的关键手段。Nginx 作为最流行的反向代理和 Web 服务器内置了三个核心限流模块ngx_http_limit_conn_module连接数限制、ngx_http_limit_req_module请求速率限制和ngx_http_core_module中的limit_rate响应速率限制。本文将深入 Nginx 1.27.x 源码剖析这三个模块的实现机制并对比漏桶、令牌桶等主流限流算法帮助读者从原理层面理解 Nginx 的限流能力。2. 核心数据结构与共享内存Nginx 的限流模块都依赖于共享内存ngx_shm_zone_t来存储状态这使得限流可以在多 worker 进程间协同工作。2.1 红黑树与 LRU 队列每个限流模块在共享内存中维护一棵红黑树ngx_rbtree_t和一个LRU 队列ngx_queue_t。红黑树以key如$binary_remote_addr为索引用于快速查找某个客户端是否已有记录。LRU 队列按节点的最近访问时间排序。当共享内存不足时Nginx 会从 LRU 队列尾部淘汰最久未使用的节点以腾出空间给新节点。// 以 limit_conn 为例其共享内存节点结构typedefstruct{ngx_rbtree_node_trbtree_node;// 红黑树节点key 为 hash 值ngx_queue_tqueue;// LRU 队列节点u_char color;// 红黑树颜色u_char len;// key 长度ngx_uint_tconn;// 当前连接数u_char data[1];// 变长 key 数据}ngx_http_limit_conn_node_t;3.limit_conn连接数限制3.1 配置指令http { limit_conn_zone $binary_remote_addr zoneaddr:10m; server { location / { limit_conn addr 10; } } }limit_conn_zone定义共享内存区域$binary_remote_addr作为 key10m表示分配 10MB 共享内存。limit_conn在 location 中引用该区域并设置最大连接数为 10。3.2 源码实现流程limit_conn的钩子挂在NGX_HTTP_PREACCESS_PHASE阶段在访问控制之前执行。核心函数ngx_http_limit_conn_handler获取节点根据 key如$binary_remote_addr在共享内存的红黑树中查找或创建ngx_http_limit_conn_node_t节点。递增计数将节点中的conn字段加 1。阈值判断如果conn limit则触发限流返回NGX_HTTP_SERVICE_UNAVAILABLE503。请求结束回调注册一个ngx_http_cleanup_t回调在请求结束时将conn减 1。// 简化后的核心逻辑ngx_int_tngx_http_limit_conn_handler(ngx_http_request_t*r){// 1. 获取共享内存区域ctxngx_http_get_module_ctx(r,ngx_http_limit_conn_module);zonectx-shm_zone-data;// 2. 在红黑树中查找或创建节点nodengx_http_limit_conn_lookup(zone-rbtree,key,key_len);if(nodeNULL){nodengx_slab_alloc_locked(zone-shpool,sizeof(ngx_http_limit_conn_node_t));// 插入红黑树和 LRU 队列}// 3. 递增连接数node-conn;// 4. 判断是否超过限制if(node-connctx-limit){node-conn--;// 回滚returnNGX_HTTP_SERVICE_UNAVAILABLE;}// 5. 注册清理回调clnngx_http_cleanup_add(r,0);cln-handlerngx_http_limit_conn_cleanup;cln-datanode;returnNGX_DECLINED;}// 请求结束时的清理函数voidngx_http_limit_conn_cleanup(void*data){ngx_http_limit_conn_node_t*nodedata;node-conn--;}3.3 关键特性精确计数基于请求的生命周期精确控制并发连接数。无延迟超过限制直接拒绝不排队。粒度通常基于 IP但可以自定义 key如$server_name。4.limit_req请求速率限制4.1 配置指令http { limit_req_zone $binary_remote_addr zonereq:10m rate10r/s; server { location / { limit_req zonereq burst20 nodelay; } } }rate10r/s平均速率限制为每秒 10 个请求。burst20允许突发 20 个请求的排队队列。nodelay排队中的请求不延迟处理而是立即处理但会消耗未来的配额。4.2 算法核心漏桶算法Leaky BucketNginx 的limit_req模块实现的是漏桶算法的变体。其核心思想是请求以不确定的速率进入桶中。桶以固定的速率rate漏水处理请求。如果桶满了超过burst新的请求被拒绝。4.3 源码实现流程limit_req同样挂在NGX_HTTP_PREACCESS_PHASE阶段。核心数据结构ngx_http_limit_req_node_ttypedefstruct{ngx_rbtree_node_trbtree_node;ngx_queue_tqueue;u_char color;u_char len;ngx_msec_tlast;// 上一次请求的时间戳毫秒ngx_uint_texcess;// 当前累积的超出量以毫秒为单位u_char data[1];}ngx_http_limit_req_node_t;核心函数ngx_http_limit_req_handler计算时间差ms ngx_current_msec - node-last。计算可消耗的配额excess node-excess - ms。如果excess 0说明桶已经漏完重置为 0。计算本次请求的消耗每个请求消耗1000 / rate毫秒的配额。例如rate10r/s则每个请求消耗 100ms。判断是否超限excess excess 1000/rate。如果excess burst * 1000/rate则拒绝请求。更新节点node-last ngx_current_msec; node-excess excess;。// 简化后的核心逻辑ngx_int_tngx_http_limit_req_handler(ngx_http_request_t*r){// ... 获取节点 nodenowngx_current_msec;ms(ngx_msec_t)(now-node-last);// 1. 计算桶内剩余水量excess 是累积的超出时间excessnode-excess;if(msexcess){excess0;}else{excess-ms;}// 2. 每个请求消耗的配额毫秒cost1000/ctx-rate;// 例如 rate10, cost100ms// 3. 判断是否超过 burst 限制if(excesscostctx-burst*cost){returnNGX_HTTP_SERVICE_UNAVAILABLE;// 503}// 4. 更新节点状态node-lastnow;node-excessexcesscost;// 5. 如果配置了 nodelay且 excess burst立即处理if(ctx-nodelayexcessctx-burst*cost){// 直接返回不延迟}else{// 否则延迟 excess 毫秒后再处理ngx_add_timer(r-connection-write,excess);}returnNGX_DECLINED;}4.5 数值推演限速 100r/s 时不同请求间隔的处理为了更直观地理解limit_req的漏桶算法我们以rate100r/s即每个请求消耗 10ms 配额为例推演请求间隔分别为1ms、10ms和100ms时excess值的变化以及请求的处理结果。假设burst0无排队队列。4.5.1 基础参数rate 100r/s→ 每个请求的配额cost 1000 / 100 10ms。burst 0→ 桶的容量为burst * cost 0ms即excess不能超过 0。初始状态node-last T0node-excess 0。4.5.2 场景一请求间隔 100ms远大于 cost请求到达时间线T0、T0100ms、T0200ms……时间点距上次请求ms漏水后excess加cost后excess是否超限说明T0—00 10 10否第一个请求excess10T0100ms10010 - 100 -90→ 00 10 10否间隔远大于 10ms桶已漏空T0200ms10010 - 100 -90→ 00 10 10否同上持续正常处理结论当请求间隔100ms远大于每个请求的配额10ms时桶始终处于排空状态每个请求都能正常通过excess始终在 10ms 左右徘徊不会累积。4.5.3 场景二请求间隔 10ms等于 cost请求到达时间线T0、T010ms、T020ms……时间点距上次请求ms漏水后excess加cost后excess是否超限说明T0—00 10 10否第一个请求T010ms1010 - 10 00 10 10否刚好漏完再进一个T020ms1010 - 10 00 10 10否稳定状态结论当请求间隔10ms恰好等于cost时系统达到稳态。每次请求到来时上一个请求的配额刚好被漏完excess始终在 0~10ms 之间波动请求以精确的 100r/s 速率被处理。4.5.4 场景三请求间隔 1ms远小于 cost请求到达时间线T0、T01ms、T02ms、T03ms……时间点距上次请求ms漏水后excess加cost后excess是否超限说明T0—00 10 10否第一个请求T01ms110 - 1 99 10 19是19 0桶已满拒绝T02ms119 - 1 1818 10 28是28 0继续拒绝T03ms128 - 1 2727 10 37是37 0继续拒绝结论当请求间隔1ms远小于cost10ms时桶的漏水速度每次漏 1ms远小于进水速度每次进 10msexcess迅速累积并超过桶容量0ms。从第二个请求开始所有请求都会被拒绝返回 503直到请求间隔拉长让桶有足够时间漏空。4.5.5 加入burst参数的影响如果配置burst5则桶容量变为5 * 10ms 50ms。在场景三中时间点漏水后excess加cost后excess是否超限说明T0010否正常T01ms919否19 ≤ 50排队T02ms1828否28 ≤ 50排队T03ms2737否37 ≤ 50排队T04ms3646否46 ≤ 50排队T05ms4555是55 50拒绝无nodelay前 5 个请求进入队列每个延迟excess毫秒后处理第 1 个延迟 10ms第 2 个延迟 19ms……第 6 个请求被拒绝。有nodelay前 5 个请求立即处理但excess依然累积到 55ms第 6 个请求被拒绝。后续请求必须等待excess漏到 50ms 以下才能继续。4.4nodelay参数的影响无nodelay请求进入队列按固定速率处理。客户端会感受到延迟。有nodelay只要excess未超过burst请求立即被处理。这允许在短时间内处理大量突发请求但后续请求会因为excess累积而被拒绝从而保证长期平均速率。5.limit_rate响应速率限制5.1 配置指令location / { limit_rate 200k; limit_rate_after 1m; # 下载 1MB 后开始限速 }5.2 源码实现流程limit_rate的实现不在独立的模块中而是集成在ngx_http_core_module的请求处理流程里。核心机制ngx_http_write_filter当 Nginx 准备向客户端发送响应体时会调用ngx_http_write_filter函数。该函数会检查r-limit_rate字段。// ngx_http_write_filter 中的关键逻辑ngx_int_tngx_http_write_filter(ngx_http_request_t*r,ngx_chain_t*in){// ... 计算本次要发送的字节数 size// 1. 检查是否已超过 limit_rate_afterif(r-limit_rater-limit_rate_after){if(r-writtenr-limit_rate_after){// 2. 计算本次发送的延迟delay(ngx_msec_t)(size*1000/r-limit_rate);// 3. 设置定时器延迟发送ngx_add_timer(r-connection-write,delay);returnNGX_AGAIN;}}// ... 实际发送数据}5.3 关键特性基于令牌桶的变体可以理解为每个请求有一个独立的令牌桶limit_rate是令牌的生成速率。按连接限速每个连接独立计算不共享状态。limit_rate_after允许在传输初期全速发送提升用户体验。6. 主流限流算法对比下面通过 Mermaid 流程图直观对比漏桶算法与令牌桶算法的核心流程核心差异核心差异令牌桶算法 (Token Bucket)是否桶满 → 令牌丢弃令牌生成器固定速率添加令牌桶存储令牌请求到达桶中有令牌消耗令牌请求通过请求被拒绝或等待令牌溢出漏桶算法 (Leaky Bucket)请求流入速率不确定桶固定容量固定速率流出处理请求桶满 → 请求被拒绝503关注流出速率强制整形输出恒定关注平均流入速率允许突发输出可波动核心差异总结漏桶算法强制输出速率恒定无论流入多快都按固定速率处理请求适合需要精确整形的场景如limit_req令牌桶算法允许短时间内的突发流量只要桶中有令牌即可立即处理适合需要允许突发的场景如limit_rate允许连接开始时全速下载。选择哪种算法取决于业务需求保护后端防止被冲垮选漏桶提升用户体验允许短时突发选令牌桶。算法核心思想优点缺点Nginx 对应模块计数器固定时间窗口内计数超过阈值则拒绝。实现简单内存占用低。存在临界突变问题窗口切换时流量尖刺。无可自行用 Lua 实现滑动窗口将时间窗口细分为多个小格滑动统计。解决了计数器的临界突变问题精度高。实现复杂度较高需要存储多个时间格的数据。无可自行用 Lua 实现漏桶算法请求进入桶中以固定速率漏出。能够平滑流量强制输出速率恒定。无法应对突发流量即使有 burst 参数也只是排队。limit_req核心实现令牌桶算法以固定速率向桶中添加令牌请求需获取令牌才能执行。允许一定程度的突发流量桶内令牌可累积。实现比漏桶稍复杂需要定时生成令牌。limit_rate变体实现6.1 漏桶 vs 令牌桶核心差异漏桶关注的是流出速率。无论流入多快流出都是恒定的。它像一个强制整形器。令牌桶关注的是平均流入速率。允许短时间内的流量超过平均速率只要桶里有令牌。它像一个允许突发的限流器。Nginx 的选择limit_req选择漏桶是因为它需要精确控制请求的处理速率防止后端被突发流量冲垮。即使配置了burst也只是让请求排队输出速率依然恒定。limit_rate选择令牌桶的变体是因为它希望允许连接在开始时全速下载利用累积的令牌之后再平滑限速。7. 总结Nginx 的三个限流模块各有侧重limit_conn基于计数的并发连接数控制简单直接。limit_req基于漏桶算法的请求速率整形是保护后端最常用的手段。limit_rate基于令牌桶思想的响应带宽控制用于优化带宽分配。理解它们的源码实现和算法差异能帮助我们在实际运维中做出更合理的配置选择例如使用limit_req配合burst和nodelay来应对秒杀场景使用limit_rate来限制大文件下载的带宽占用。