当前位置: 首页 > news >正文

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 zone=addr:10m; server { location / { limit_conn addr 10; } } }
  • limit_conn_zone:定义共享内存区域,$binary_remote_addr作为 key,10m表示分配 10MB 共享内存。
  • limit_conn:在 location 中引用该区域,并设置最大连接数为 10。

3.2 源码实现流程

limit_conn的钩子挂在NGX_HTTP_PREACCESS_PHASE阶段,在访问控制之前执行。

核心函数:ngx_http_limit_conn_handler

  1. 获取节点:根据 key(如$binary_remote_addr)在共享内存的红黑树中查找或创建ngx_http_limit_conn_node_t节点。
  2. 递增计数:将节点中的conn字段加 1。
  3. 阈值判断:如果conn > limit,则触发限流,返回NGX_HTTP_SERVICE_UNAVAILABLE(503)。
  4. 请求结束回调:注册一个ngx_http_cleanup_t回调,在请求结束时将conn减 1。
// 简化后的核心逻辑ngx_int_tngx_http_limit_conn_handler(ngx_http_request_t*r){// 1. 获取共享内存区域ctx=ngx_http_get_module_ctx(r,ngx_http_limit_conn_module);zone=ctx->shm_zone->data;// 2. 在红黑树中查找或创建节点node=ngx_http_limit_conn_lookup(zone->rbtree,key,key_len);if(node==NULL){node=ngx_slab_alloc_locked(zone->shpool,sizeof(ngx_http_limit_conn_node_t));// 插入红黑树和 LRU 队列}// 3. 递增连接数node->conn++;// 4. 判断是否超过限制if(node->conn>ctx->limit){node->conn--;// 回滚returnNGX_HTTP_SERVICE_UNAVAILABLE;}// 5. 注册清理回调cln=ngx_http_cleanup_add(r,0);cln->handler=ngx_http_limit_conn_cleanup;cln->data=node;returnNGX_DECLINED;}// 请求结束时的清理函数voidngx_http_limit_conn_cleanup(void*data){ngx_http_limit_conn_node_t*node=data;node->conn--;}

3.3 关键特性

  • 精确计数:基于请求的生命周期,精确控制并发连接数。
  • 无延迟:超过限制直接拒绝,不排队。
  • 粒度:通常基于 IP,但可以自定义 key(如$server_name)。

4.limit_req:请求速率限制

4.1 配置指令

http { limit_req_zone $binary_remote_addr zone=req:10m rate=10r/s; server { location / { limit_req zone=req burst=20 nodelay; } } }
  • rate=10r/s:平均速率限制为每秒 10 个请求。
  • burst=20:允许突发 20 个请求的排队队列。
  • nodelay:排队中的请求不延迟处理,而是立即处理,但会消耗未来的配额。

4.2 算法核心:漏桶算法(Leaky Bucket)

Nginx 的limit_req模块实现的是漏桶算法的变体。其核心思想是:

  • 请求以不确定的速率进入桶中。
  • 桶以固定的速率(rate)漏水(处理请求)。
  • 如果桶满了(超过burst),新的请求被拒绝。

4.3 源码实现流程

limit_req同样挂在NGX_HTTP_PREACCESS_PHASE阶段。

核心数据结构:ngx_http_limit_req_node_t

typedefstruct{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

  1. 计算时间差ms = ngx_current_msec - node->last
  2. 计算可消耗的配额excess = node->excess - ms。如果excess < 0,说明桶已经漏完,重置为 0。
  3. 计算本次请求的消耗:每个请求消耗1000 / rate毫秒的配额。例如rate=10r/s,则每个请求消耗 100ms。
  4. 判断是否超限excess = excess + 1000/rate。如果excess > burst * 1000/rate,则拒绝请求。
  5. 更新节点node->last = ngx_current_msec; node->excess = excess;
// 简化后的核心逻辑ngx_int_tngx_http_limit_req_handler(ngx_http_request_t*r){// ... 获取节点 nodenow=ngx_current_msec;ms=(ngx_msec_t)(now-node->last);// 1. 计算桶内剩余水量(excess 是累积的超出时间)excess=node->excess;if(ms>excess){excess=0;}else{excess-=ms;}// 2. 每个请求消耗的配额(毫秒)cost=1000/ctx->rate;// 例如 rate=10, cost=100ms// 3. 判断是否超过 burst 限制if(excess+cost>ctx->burst*cost){returnNGX_HTTP_SERVICE_UNAVAILABLE;// 503}// 4. 更新节点状态node->last=now;node->excess=excess+cost;// 5. 如果配置了 nodelay,且 excess <= burst,立即处理if(ctx->nodelay&&excess<=ctx->burst*cost){// 直接返回,不延迟}else{// 否则,延迟 excess 毫秒后再处理ngx_add_timer(r->connection->write,excess);}returnNGX_DECLINED;}

4.5 数值推演:限速 100r/s 时不同请求间隔的处理

为了更直观地理解limit_req的漏桶算法,我们以rate=100r/s(即每个请求消耗 10ms 配额)为例,推演请求间隔分别为1ms10ms100ms时,excess值的变化以及请求的处理结果。假设burst=0(无排队队列)。

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)

请求到达时间线:T0T0+100msT0+200ms……

时间点距上次请求ms漏水后excesscostexcess是否超限说明
T000 + 10 = 10第一个请求,excess=10
T0+100ms10010 - 100 = -90→ 00 + 10 = 10间隔远大于 10ms,桶已漏空
T0+200ms10010 - 100 = -90→ 00 + 10 = 10同上,持续正常处理

结论:当请求间隔(100ms)远大于每个请求的配额(10ms)时,桶始终处于排空状态,每个请求都能正常通过,excess始终在 10ms 左右徘徊,不会累积。

4.5.3 场景二:请求间隔 10ms(等于 cost)

请求到达时间线:T0T0+10msT0+20ms……

时间点距上次请求ms漏水后excesscostexcess是否超限说明
T000 + 10 = 10第一个请求
T0+10ms1010 - 10 = 00 + 10 = 10刚好漏完,再进一个
T0+20ms1010 - 10 = 00 + 10 = 10稳定状态

结论:当请求间隔(10ms)恰好等于cost时,系统达到稳态。每次请求到来时,上一个请求的配额刚好被漏完,excess始终在 0~10ms 之间波动,请求以精确的 100r/s 速率被处理。

4.5.4 场景三:请求间隔 1ms(远小于 cost)

请求到达时间线:T0T0+1msT0+2msT0+3ms……

时间点距上次请求ms漏水后excesscostexcess是否超限说明
T000 + 10 = 10第一个请求
T0+1ms110 - 1 = 99 + 10 = 19(19 > 0)桶已满,拒绝
T0+2ms119 - 1 = 1818 + 10 = 28(28 > 0)继续拒绝
T0+3ms128 - 1 = 2727 + 10 = 37(37 > 0)继续拒绝

结论:当请求间隔(1ms)远小于cost(10ms)时,桶的漏水速度(每次漏 1ms)远小于进水速度(每次进 10ms),excess迅速累积并超过桶容量(0ms)。从第二个请求开始,所有请求都会被拒绝(返回 503),直到请求间隔拉长,让桶有足够时间漏空。

4.5.5 加入burst参数的影响

如果配置burst=5,则桶容量变为5 * 10ms = 50ms。在场景三中:

时间点漏水后excesscostexcess是否超限说明
T0010正常
T0+1ms919否(19 ≤ 50)排队
T0+2ms1828否(28 ≤ 50)排队
T0+3ms2737否(37 ≤ 50)排队
T0+4ms3646否(46 ≤ 50)排队
T0+5ms4555(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_rate&&r->limit_rate_after){if(r->written>r->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配合burstnodelay来应对秒杀场景,使用limit_rate来限制大文件下载的带宽占用。

http://www.jsqmd.com/news/1195544/

相关文章:

  • 嵌入式实战笔记 -- 跨平台通信中的大小端转换策略
  • ChatGPT iOS App 从下载到精通:解锁移动端AI助手的完整指南
  • 盘点:Flutter开发中那些提升效率的UI组件库(持续收录)
  • MCP服务器性能测试完全指南:从零开始构建高可用AI服务
  • 毕设项目分享 基于Kmeans的图像分割算法软件设计
  • 2026年论文查重免费网站靠谱吗?真实测评告诉你哪个最准
  • NUSspli完整指南:Wii U游戏下载安装终极教程
  • LM5141电流模式控制:原理、斜率补偿与EMI优化实战
  • ChatGPT薪资模拟器上线倒计时:输入岗位/城市/年限,3秒生成对抗性议价脚本(限前200名免费)
  • 基于SpringBoot心晴疗愈社平台的设计与实现任务书
  • VC++ ATL连接点编程实战:从COM事件模型到可运行示例
  • 25G/50G以太网信号调理:DS250DF210重定时器设计实战与调优
  • AutoRemesher:从三角网格到四边网格的智能重构革命
  • Obsidian Spreadsheets:如何在知识笔记中构建完整的数据处理工作流?
  • 微信聊天记录如何永久保存?WeChatMsg数据留痕完整指南
  • PPT Master终极指南:3分钟从零打造专业演示文稿的秘诀
  • MOS管在电路供电中的关键作用与选型设计指南
  • 从“地理学第一定律”到实际应用:空间自相关与异质性的深度辨析
  • 2026年7月最新南昌芝柏官方售后服务热线与网点地址查询 - 亨得利官方服务中心
  • 主流大模型技术特性与开发环境集成实践指南
  • 基于大数据爬虫+Hadoop+Spark的旅游推荐系统任务书
  • 2026 年新消息:评价高的G657A1光纤生产厂家怎么联系,揭秘:为什么你的光纤选择错了,正在损失巨大成本? - 品质体验官
  • 辅助服务关闭手机广告基本勉强能用了
  • 智能人脸裁剪:3分钟学会用autocrop批量处理照片
  • 无人机如何在狂风中纹丝不动?ArduPilot定点悬停技术深度解析
  • TI TMS570LS0714安全MCU:锁步双核与ECC内存的汽车电子设计实践
  • HTTP 103 Early Hints:从性能优化到现代Web架构的实践指南
  • MATLAB | 深入隐藏属性,解锁图例图标DIY的无限可能
  • 小米笔记本Pro黑苹果安装指南:5步实现完美macOS体验
  • 学生综合素质评价源头厂家