ngx_http_postpone_filter
1 定义ngx_http_postpone_filter 函数 定义在 src/http/ngx_http_postpone_filter_module.cstaticngx_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;cr-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-postponedNULL){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{prr-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-postponedpr-next;c-datapr-request;returnngx_http_post_request(pr-request,NULL);}if(pr-outNULL){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-postponedpr-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 或事件处理器传入不能为 NULLinngx_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 NULLpr-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.hunsigned 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:359size_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:123void *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_copy把in链拷贝/浅引用到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-postponedNULL){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{prr-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-postponedpr-next;c-datapr-request;returnngx_http_post_request(pr-request,NULL);}if(pr-outNULL){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-postponedpr-next;}while(r-postponed);ngx_http_postponed_request_s结构体src/http/ngx_http_request.h:358-362structngx_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 子请求节点——调度子请求处理逻辑从推迟链中摘下当前节点r-postponed pr-next。将连接的活动请求切换为该子请求c-data pr-request。调用ngx_http_post_request(pr-request, NULL)将该子请求放入主请求的posted_requests队列尾部。返回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_filter、ngx_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_tstructngx_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.cstaticngx_int_tngx_http_postpone_filter_init(ngx_conf_t*cf){ngx_http_next_body_filterngx_http_top_body_filter;ngx_http_top_body_filterngx_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末尾将新节点的next和out初始化为 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_palloc、ngx_chain_add_copy、下游 filter 调用都检查返回值失败即返回NGX_ERROR由上游处理请求终止逻辑。