ngx_http_postpone_filter

📅 2026/7/21 16:31:06 👁️ 阅读次数 📝 编程学习
ngx_http_postpone_filter

1 定义

`ngx_http_postpone_filter` 函数 定义在 `src/http/ngx_http_postpone_filter_module.c`
staticngx_http_output_body_filter_pt ngx_http_next_body_filter;staticngx_int_tngx_http_postpone_filter(ngx_http_request_t*r,ngx_chain_t*in){ngx_connection_t*c;ngx_http_postponed_request_t*pr;c=r->connection;ngx_log_debug3(NGX_LOG_DEBUG_HTTP,c->log,0,"http postpone filter \"%V?%V\" %p",&r->uri,&r->args,in);if(r->subrequest_in_memory){returnngx_http_postpone_filter_in_memory(r,in);}if(r!=c->data){if(in){if(ngx_http_postpone_filter_add(r,in)!=NGX_OK){returnNGX_ERROR;}returnNGX_OK;}#if0/* TODO: SSI may pass NULL */ngx_log_error(NGX_LOG_ALERT,c->log,0,"http postpone filter NULL inactive request");#endifreturnNGX_OK;}if(r->postponed==NULL){if(in||c->buffered){returnngx_http_next_body_filter(r->main,in);}returnNGX_OK;}if(in){if(ngx_http_postpone_filter_add(r,in)!=NGX_OK){returnNGX_ERROR;}}do{pr=r->postponed;if(pr->request){ngx_log_debug2(NGX_LOG_DEBUG_HTTP,c->log,0,"http postpone filter wake \"%V?%V\"",&pr->request->uri,&pr->request->args);r->postponed=pr->next;c->data=pr->request;returnngx_http_post_request(pr->request,NULL);}if(pr->out==NULL){ngx_log_error(NGX_LOG_ALERT,c->log,0,"http postpone filter NULL output");}else{ngx_log_debug2(NGX_LOG_DEBUG_HTTP,c->log,0,"http postpone filter output \"%V?%V\"",&r->uri,&r->args);if(ngx_http_next_body_filter(r->main,pr->out)==NGX_ERROR){returnNGX_ERROR;}}r->postponed=pr->next;}while(r->postponed);returnNGX_OK;}

2 目的

1 设计意图

ngx_http_postpone_filter是 Nginx body filter 链中最核心的编排节点,
负责将子请求(subrequest)的异步输出按声明顺序串行化注入主请求的输出流。

在 Nginx 架构中,子请求(如 SSI include、ngx_http_subrequest
独立运行且拥有自己的输出过滤链,
但它们产生的响应体必须以原始声明的先后顺序合并到主请求中。


3 详解

1 函数签名

staticngx_int_tngx_http_postpone_filter(ngx_http_request_t*r,ngx_chain_t*in);

1 返回值:ngx_int_t

返回值含义
NGX_OK处理成功
NGX_ERROR处理失败(内存分配失败、输出失败),将导致当前请求终止

2 函数名:ngx_http_postpone_filter

词段含义
ngx_Nginx 命名空间前缀
http属于 HTTP 子系统
postpone核心语义:推迟、暂缓——将子请求的输出"暂存"到合适的时机再发送
filter是一个 body filter,遵循ngx_http_output_body_filter_pt的函数签名约定

3 参数列表

参数名类型含义来源约束
rngx_http_request_t *当前 HTTP 请求对象由上游 body filter 或事件处理器传入不能为 NULL
inngx_chain_t *待处理的输出数据链上游 body filter 传递或本模块调用可为 NULL(仅用作信号/刷新)

2 逻辑流程

ngx_http_postpone_filter(r, in) │ ├─ [1] 内存子请求路由 │ └─ r->subrequest_in_memory == 1 → in_memory(r, in),返回 │ ├─ [2] 非活动请求处理(r != c->data) │ ├─ [2.1] 有待处理数据(in != NULL) │ │ └─ 将 in 追加到 r->postponed 链尾 → 返回 NGX_OK/N │ │ GX_ERROR │ └─ [2.2] 无数据(in == NULL) │ └─ 直接返回 NGX_OK(挂起等待) │ ├─ [3] 活动请求且无推迟链 │ └─ r->postponed == NULL 且 r == c->data │ ├─ [3.1] 有数据或连接已缓冲(in != NULL || c->buffered) │ │ └─ 透传给下游 filter(r->main, in),返回 │ └─ [3.2] 既无数据也无缓冲 │ └─ 返回 NGX_OK │ └─ [4] 活动请求且有推迟链(r == c->data 且 r->postponed != NULL) ├─ [4.1] 追加新数据 │ └─ in != NULL → postpone_filter_add(r, in) │ └─ 失败 → NGX_ERROR └─ [4.2] 轮转推迟链(do...while 循环) │ ├─ [4.2.1] 子请求节点(pr->request != NULL) │ ├─ 从 postponed 链摘除此节点(r->postponed = pr->next) │ ├─ 切换活动请求:c->data = pr->request │ └─ 将子请求投递到 posted_requests 队列 → 返回 NGX_OK │ ├─ [4.2.2] 数据节点(pr->request == NULL,pr->out 为数据链) │ ├─ pr->out == NULL → 记录 ALERT 日志(异常状态) │ └─ pr->out != NULL → 调用下游 filter 输出 pr->out 到主请求 │ └─ 失败 → NGX_ERROR │ └─ 移动指针:r->postponed = pr->next └─ r->postponed 仍非 NULL → 继续循环 └─ r->postponed == NULL → 退出循环,返回 NGX_OK

{ngx_connection_t*c;ngx_http_postponed_request_t*pr;}

局部变量


1 内存子请求路由

if(r->subrequest_in_memory){returnngx_http_postpone_filter_in_memory(r,in);}

进入条件:请求r是一个subrequest_in_memory模式的子请求。

subrequest_in_memory字段src/http/ngx_http_request.h):

  • unsigned subrequest_in_memory:1;——1 位标志位。
  • 当子请求以NGX_HTTP_SUBREQUEST_IN_MEMORY标志创建时设置(src/http/ngx_http_request.h:65#define NGX_HTTP_SUBREQUEST_IN_MEMORY 2)。
  • 典型场景:SSI<!--# include virtual="..." -->等内联包含,子请求的整个响应体需要在内存中收集完成后一次性使用,而不是逐块输出。

处理逻辑

  • in链的所有数据拷贝到r->out的单一内存缓冲区中(通过内部函数ngx_http_postpone_filter_in_memory,详见下文)。
  • 不是r->postponed推迟链的标准路径,而是一条完全独立的快速路径。

设计意图:内存子请求的输出不会被下游 filter 逐块消费,而是被父请求的内容处理器(如 SSI filter)在子请求完成后一次性读取。因此需要一个专门的内存收集路径来避免不必要的缓冲区分配和链表操作。

ngx_http_postpone_filter_in_memory函数概述

定义在src/http/ngx_http_postpone_filter_module.c

核心职责:将子请求的所有 body 数据累积到一个预分配的内存缓冲区中(r->out指向的单 buf 链)。首次调用时根据Content-Length或配置的subrequest_output_buffer_size分配缓冲区;随后每次调用将in链中的有效数据通过ngx_cpymem逐个拷贝到该缓冲区,同时推进in->buf->pos("消费"源数据)。若数据超过缓冲区容量或超过大小限制,返回NGX_ERROR。忽略ngx_buf_special标记的特殊缓冲区(flush/sync/last_buf 等控制帧)。

ngx_cpymem宏定义src/core/ngx_string.h:97):

#definengx_cpymem(dst,src,n)(((u_char*)ngx_memcpy(dst,src,n))+(n))

封装memcpy,返回值指向目标缓冲区拷贝结束后的下一个字节位置(即更新后的写入游标),便于b->last = ngx_cpymem(...)这种链式更新。

subrequest_output_buffer_size配置项

subrequest_output_buffer_sizesrc/http/ngx_http_core_module.h:359):size_t类型,默认值为NGX_CONF_UNSET_SIZE。当子请求未声明Content-Length时,以此为回退缓冲区大小。


2 非活动请求处理

if(r!=c->data){if(in){if(ngx_http_postpone_filter_add(r,in)!=NGX_OK){returnNGX_ERROR;}returnNGX_OK;}#if0/* TODO: SSI may pass NULL */ngx_log_error(NGX_LOG_ALERT,c->log,0,"http postpone filter NULL inactive request");#endifreturnNGX_OK;}

进入条件:当前请求r不是连接上当前活动的请求(即r != c->data)。

c->data字段src/core/ngx_connection.h:123):void *data;——通用指针,Nginx HTTP 模块始终将其指向"当前正在该连接上被处理的请求对象"。当主请求处理过程中被子请求打断时,c->data会临时指向子请求;子请求完成后恢复指向主请求。

2.1 有待处理数据(in != NULL

处理逻辑

  • 调用ngx_http_postpone_filter_add(r, in),将in追加到r->postponed链表的尾部。
  • 成功返回NGX_OK,失败返回NGX_ERROR

设计意图:子请求在非活动状态下产生了 body 数据(例如 SSI 子请求的响应体通过 upstream 逐步到达)。此时主请求或其他子请求正在活跃,不能立即输出——必须"暂存"到该子请求在postponed链中对应的位置,等轮到该子请求时再消费。

2.2 无数据(in == NULL

处理逻辑

  • 直接返回NGX_OK
  • #if 0注释掉的是一个 ALERT 日志:开发者曾考虑对非活动请求的 NULL 输入做告警,但后来发现 SSI 模块在某些场景下会传入 NULL,故关闭。

设计意图in == NULL在这里是一个"空推送"——上游 filter 链需要刷新但不产生数据。非活动请求没有刷新操作的需要,直接忽略即可。

ngx_http_postpone_filter_add函数概述

定义在src/http/ngx_http_postpone_filter_module.c

核心职责:将输入数据链in追加到r->postponed链表的尾部。先遍历链表找到末尾节点:若末尾节点的request == NULL(即它是一个数据节点),则直接将in追加到该节点的out链上(复用已有节点);否则(末尾为子请求节点或链表为空)分配一个新的ngx_http_postponed_request_t,将其request置为 NULL(标记为数据节点),再通过ngx_chain_add_copyin链拷贝/浅引用到pr->out。内存分配失败返回NGX_ERROR

ngx_chain_add_copy函数概述

定义在src/core/ngx_buf.c

核心职责:将一个ngx_chain_t链表in追加到另一个链表的尾部。遍历in链,对每个节点分配一个新的ngx_chain_t结构体,但不拷贝缓冲区内容——只拷贝buf指针(浅拷贝/零拷贝语义),实现 O(n) 的时间复杂度和常量内存开销。追加完成后将尾部next置 NULL。


3 活动请求且无推迟链

if(r->postponed==NULL){if(in||c->buffered){returnngx_http_next_body_filter(r->main,in);}returnNGX_OK;}

进入条件:当前请求是活动请求(r == c->data
且推迟链表为空(r->postponed == NULL)。
这意味着没有待处理的子请求输出。

r->postponed字段src/http/ngx_http_request.h:):
ngx_http_postponed_request_t *postponed;
推迟链表头指针,链表中交替存储子请求节点的指针和数据节点的指针。

3.1 有数据或连接已缓冲

处理逻辑

  • 调用ngx_http_next_body_filter(r->main, in),将数据透传给主请求的下一个 body filter。
  • 注意这里传递的第一个参数是r->main(主请求),而不是r
  • 因为 body filter 链的输出目标始终是主请求。

c->buffered字段src/core/ngx_connection.h:):
unsigned buffered:8;——连接级缓冲标记。
当连接的下游 filter(如 write filter)仍有缓冲数据未发送时,该标记为 1。
此时即使in == NULL,也需要通知下游 filter 尝试刷新缓冲区。

3.2 无数据且无缓冲

处理逻辑:直接返回NGX_OK。没有任何工作要做。

设计意图:这是最简路径——当前就是主请求、无子请求待处理、无新数据、无待刷新缓冲。
最常见的场景比如响应头刚发送完毕、body 尚未来到。


4 活动请求且有推迟链

if(in){if(ngx_http_postpone_filter_add(r,in)!=NGX_OK){returnNGX_ERROR;}}

进入条件:当前请求是活动请求(r == c->data)且推迟链表非空(r->postponed != NULL)。即存在待消费的子请求输出。

4.1 追加新数据
  • 如果本轮还有新的in数据,先将它追加到推迟链表尾部(复用已有数据节点或新分配节点)。
  • 失败时返回NGX_ERROR
4.2 轮转推迟链
do{pr=r->postponed;if(pr->request){ngx_log_debug2(NGX_LOG_DEBUG_HTTP,c->log,0,"http postpone filter wake \"%V?%V\"",&pr->request->uri,&pr->request->args);r->postponed=pr->next;c->data=pr->request;returnngx_http_post_request(pr->request,NULL);}if(pr->out==NULL){ngx_log_error(NGX_LOG_ALERT,c->log,0,"http postpone filter NULL output");}else{ngx_log_debug2(NGX_LOG_DEBUG_HTTP,c->log,0,"http postpone filter output \"%V?%V\"",&r->uri,&r->args);if(ngx_http_next_body_filter(r->main,pr->out)==NGX_ERROR){returnNGX_ERROR;}}r->postponed=pr->next;}while(r->postponed);

ngx_http_postponed_request_s结构体src/http/ngx_http_request.h:358-362):

structngx_http_postponed_request_s{ngx_http_request_t*request;ngx_chain_t*out;ngx_http_postponed_request_t*next;};
字段类型含义
requestngx_http_request_t *若非 NULL,此节点代表一个待调度的子请求
outngx_chain_t *若非 NULL(且request == NULL),此节点代表待输出的数据链
nextngx_http_postponed_request_t *链表后继指针

推迟链表的数据结构语义

链表中的节点按顺序交替排列:[子请求A] → [数据A] → [子请求B] → [数据B] → ...

  • 子请求节点request != NULL, out == NULL):表示子请求自身还未完成,需要被唤醒继续执行。
  • 数据节点request == NULL, out != NULL):表示子请求已经产生并被暂存的数据,可以直接输出。

当处理数据节点时,ngx_http_next_body_filter(r->main, pr->out)将子请求的输出注入主请求的 filter 链。这是 body filter 的递归调用——同一轮事件循环中先处理主请求的输出,然后可能触发对下游 filter 的调用。

4.2.1 子请求节点——调度子请求

处理逻辑

  1. 从推迟链中摘下当前节点(r->postponed = pr->next)。
  2. 将连接的活动请求切换为该子请求(c->data = pr->request)。
  3. 调用ngx_http_post_request(pr->request, NULL)将该子请求放入主请求的posted_requests队列尾部。
  4. 返回NGX_OK不继续处理后续节点

设计意图

  • 协作式调度:子请求可能因 I/O 未就绪而在其 filter 链的某个位置挂起。当它的数据通过ngx_http_postpone_filter_add被收集到推迟链中时,它事实上暂停了。现在轮到该子请求了——必须"唤醒"它。
  • 为什么返回而不是继续:当前函数的调用者本身就在某个请求的事件处理中。唤醒子请求后,当前调用帧正常返回;事件循环在ngx_http_run_posted_requests中轮询posted_requests队列,后续会调用子请求的write_event_handler,让子请求从它上次挂起的位置继续执行。子请求完成后会再次调用本 filter,推动推迟链消费继续前进。
ngx_http_post_request函数概述

定义在src/http/ngx_http_request.c

核心职责:将一个请求对象注册到主请求的posted_requests队列尾部。若调用者未提供ngx_http_posted_request_t节点(pr == NULL),则从内存池分配一个新节点并绑定到r。该队列由ngx_http_run_posted_requests在每次 HTTP 请求事件处理完毕后轮询消费(见src/http/ngx_http_request.c:2397),调用每个已投递请求的write_event_handler。这是 Nginx 实现事件驱动的请求级协作调度的核心机制——避免了多线程上下文切换开销,同时保证了请求执行的公平性(FIFO)。

4.2.2 数据节点——输出数据到主请求

处理逻辑

  • 异常分支pr->out == NULL):记录NGX_LOG_ALERT日志。这是一个逻辑错误——数据节点的out字段不应为空。但不因此中断处理,会继续进入下一轮循环。
  • 正常分支pr->out != NULL):调用ngx_http_next_body_filter(r->main, pr->out),将子请求已产生的数据链注入主请求的 body filter 链。若下游 filter 返回NGX_ERROR,立即终止并向上传播错误。

设计意图

  • 子请求的数据链在此处正式合并到主请求的输出流中。下游 filter(如ngx_http_copy_filterngx_http_write_filter)对这些数据的来源无感知——它们只看到"主请求的 body 数据来了"。
  • 注意r->main的传递:子请求和主请求共享同一套 body filter 链。这是 Nginx 输出过滤架构的关键设计:body filter 注册在模块级别,所有请求(主请求与子请求)通过同一个 filter 链指针ngx_http_top_body_filter串联。
4.2.3 循环继续条件

处理逻辑

  • 处理完当前节点后,r->postponed = pr->next移动指针。
  • while (r->postponed):只要推迟链中还有节点,就继续循环。

关键设计

  • 如果链中下一个节点又是子请求节点,则会在 4.2.1 中立即返回(只调度一个子请求就退出)。
  • 如果连续多个节点都是数据节点,则会在同一轮调用中全部输出——这是一种批量优化:数据已经就绪,不需要等待任何 I/O,一次输出完毕。
  • 循环保证在r->postponed == NULL时优雅退出,返回NGX_OK

4 关键依赖总结

核心数据结构

ngx_http_postponed_request_t
structngx_http_postponed_request_s{ngx_http_request_t*request;ngx_chain_t*out;ngx_http_postponed_request_t*next;};

推迟链表节点。两种用途:

  • request != NULL:子请求调度节点,表示该子请求待唤醒继续执行。
  • request == NULL && out != NULL:数据节点,表示该子请求已产生的待输出数据链。

关键宏

ngx_buf_special宏定义src/core/ngx_buf.h:128):

#definengx_buf_special(b)\(((b)->flush||(b)->last_buf||(b)->sync)\&&!ngx_buf_in_memory(b)&&!(b)->in_file)

其中ngx_buf_in_memory(b)定义为(src/core/ngx_buf.h:125):

#definengx_buf_in_memory(b)((b)->temporary||(b)->memory||(b)->mmap)

ngx_buf_special为 true 当且仅当:缓冲区设置了 flush、last_buf 或 sync 标志,且不含实际数据——既不在内存中(非 temporary/memory/mmap),也不在文件中。这类缓冲区是控制帧/信号帧,不含有效数据内容,在内存子请求的收集中被跳过。

关键全局变量

ngx_http_next_body_filter:静态变量,保存 body filter 链中本模块的下一个 filter 的函数指针。通过ngx_http_postpone_filter_init在模块初始化时保存当前ngx_http_top_body_filter,然后将自身插入链顶。遵循 Nginx filter 链的标准"责任链"模式。

模块初始化

ngx_http_postpone_filter_initsrc/http/ngx_http_postpone_filter_module.c):

staticngx_int_tngx_http_postpone_filter_init(ngx_conf_t*cf){ngx_http_next_body_filter=ngx_http_top_body_filter;ngx_http_top_body_filter=ngx_http_postpone_filter;returnNGX_OK;}

在配置解析完成后(postconfiguration阶段)执行。将自身插入ngx_http_top_body_filter链的最顶端,确保所有 body filter 的数据都会先经过本模块。这保证了任何子请求的 body 输出都能被正确编排。


5 性能与安全视角

性能

  • 零拷贝传递ngx_chain_add_copy只拷贝ngx_chain_t节点指针(cl->buf = in->buf),不拷贝缓冲区内容。推迟链中的out指针直接指向原始数据链的buf,避免了数据复制。
  • 对象池复用ngx_alloc_chain_link优先从pool->chain空闲链表中获取节点(src/core/ngx_buf.c:48-65),减少ngx_palloc调用。
  • 批量数据输出:在 4.2 循环中,连续的多个数据节点会在一次调用中全部输出,减少了函数调用开销。
  • 快速路径:活动主请求无子请求时(分支 3),只需一次指针比较和一次 NULL 检查即透传,近乎零开销。

安全

  • postponed链完整性ngx_http_postpone_filter_add末尾将新节点的nextout初始化为 NULL,防止悬挂指针。ngx_chain_add_copy失败时会清零*ll,避免半截链表。
  • 内存越界防护ngx_http_postpone_filter_in_memory中在拷贝前检查len > (size_t) (b->end - b->last),防止缓冲区溢出。
  • 大小限制ngx_http_postpone_filter_in_memory对声明了Content-Length的子请求检查len > subrequest_output_buffer_size,防止恶意响应撑爆内存。
  • 错误传播:每个可能失败的操作(ngx_pallocngx_chain_add_copy、下游 filter 调用)都检查返回值,失败即返回NGX_ERROR,由上游处理请求终止逻辑。