ngx_http_copy_filter
1 定义ngx_http_copy_filter 函数 定义在 src/http/ngx_http_copy_filter_module.cstaticngx_int_tngx_http_copy_filter(ngx_http_request_t*r,ngx_chain_t*in){ngx_int_trc;ngx_connection_t*c;ngx_output_chain_ctx_t*ctx;ngx_http_core_loc_conf_t*clcf;ngx_http_copy_filter_conf_t*conf;cr-connection;ngx_log_debug2(NGX_LOG_DEBUG_HTTP,c-log,0,http copy filter: \%V?%V\,r-uri,r-args);ctxngx_http_get_module_ctx(r,ngx_http_copy_filter_module);if(ctxNULL){ctxngx_pcalloc(r-pool,sizeof(ngx_output_chain_ctx_t));if(ctxNULL){returnNGX_ERROR;}ngx_http_set_ctx(r,ctx,ngx_http_copy_filter_module);confngx_http_get_module_loc_conf(r,ngx_http_copy_filter_module);clcfngx_http_get_module_loc_conf(r,ngx_http_core_module);ctx-sendfilec-sendfile;ctx-need_in_memoryr-main_filter_need_in_memory||r-filter_need_in_memory;ctx-need_in_tempr-filter_need_temporary;ctx-alignmentclcf-directio_alignment;ctx-poolr-pool;ctx-bufsconf-bufs;ctx-tag(ngx_buf_tag_t)ngx_http_copy_filter_module;ctx-output_filter(ngx_output_chain_filter_pt)ngx_http_next_body_filter;ctx-filter_ctxr;#if(NGX_HAVE_FILE_AIO)if(ngx_file_aioclcf-aioNGX_HTTP_AIO_ON){ctx-aio_handlerngx_http_copy_aio_handler;}#endif#if(NGX_THREADS)if(clcf-aioNGX_HTTP_AIO_THREADS){ctx-thread_handlerngx_http_copy_thread_handler;}#endifif(inin-bufngx_buf_size(in-buf)){r-request_output1;}}#if(NGX_HAVE_FILE_AIO||NGX_THREADS)ctx-aior-aio;#endifrcngx_output_chain(ctx,in);if(ctx-inNULL){r-buffered~NGX_HTTP_COPY_BUFFERED;}else{r-buffered|NGX_HTTP_COPY_BUFFERED;}ngx_log_debug3(NGX_LOG_DEBUG_HTTP,c-log,0,http copy filter: %i \%V?%V\,rc,r-uri,r-args);returnrc;}2 目的1 设计意图ngx_http_copy_filter是 Nginx HTTP 输出过滤链中承上启下的缓冲区管理节点其核心职责是将上游过滤器产生的零散、异构的输出缓冲区统一复制到 copy filter 自己管理的输出缓冲区中实现数据的聚合与对齐然后批量传递给下游的 write filter 进行实际网络发送。在 Nginx 的过滤链架构中上游过滤器如 gzip、SSI、sub、range 等会产生各种类型的缓冲区有些在内存中memory、temporary有些在文件中in_file有些共享只读内存mmap有些甚至可能已经被标记为可回收recycled。如果任由这些异构缓冲区直接流入 write filtersendfile可能因缓冲区不可直接发送如mmap的数据需要临时拷贝而退化为writev小缓冲区过多时会触发大量系统调用降低吞吐某些下游 filter如 SSL要求数据必须在可写的内存中而memory/mmap类型的缓冲区不可写。copy filter 通过委托ngx_output_chain通用框架在 body 过滤链的末端统一完成这些缓冲区的归化工作判断每个缓冲区是否需要拷贝需要拷贝的就复制到自己的缓冲区池中不需要拷贝的就直接透传。最终将整理好的一批数据一次性传给 write filter实现高效发送。3 详解1 函数签名staticngx_int_tngx_http_copy_filter(ngx_http_request_t*r,ngx_chain_t*in)1 返回值ngx_int_t返回值含义NGX_OK处理成功缓冲区已全部传递给下游NGX_AGAIN部分数据未处理完需等待事件重试NGX_ERROR内存分配失败或底层错误注意返回值直接来自ngx_output_chain(ctx, in)copy filter 本身不修改它只做透传。2 函数名ngx_http_copy_filter词段含义ngx_Nginx 核心命名空间前缀http_归属于 HTTP 子系统copy_核心操作——将上游数据拷贝到统一的输出缓冲区filter表明这是一个 body 过滤链中的处理节点3 参数列表参数类型含义来源约束rngx_http_request_t *当前 HTTP 请求对象上游 filter 透传非 NULLinngx_chain_t *上游传入的待输出缓冲区链上游 filter 产生可为 NULL表示本轮无数据2 逻辑流程ngx_http_copy_filter(r, in) ├─ [1] 上下文初始化 │ └─ ctx NULL → 首次调用分配并初始化 ngx_output_chain_ctx_t │ ├─ [1.1] 基本字段sendfile / need_in_memory / need_in_temp / alignment / pool / bufs / tag │ ├─ [1.2] 输出回调output_filter ngx_http_next_body_filter, filter_ctx r │ ├─ [1.3] AIO 处理器注册NGX_HAVE_FILE_AIO 编译条件 │ ├─ [1.4] 线程处理器注册NGX_THREADS 编译条件 │ └─ [1.5] 首次数据标记in 有数据 → r-request_output 1 ├─ [2] AIO 状态同步编译期条件 │ └─ ctx-aio r-aio每次调用都同步确保并发操作协调 ├─ [3] 委托 ngx_output_chain │ └─ rc ngx_output_chain(ctx, in) → 核心缓冲区管理 │ ├─ 快速路径ctx-in/busy 均为空 in 为空或单 buf 可通过 as_is 检查 → 直接透传 │ ├─ 慢速路径in 追加到 ctx-in 累积 → 逐 buf 拷贝到输出缓冲区池 → 批量 output_filter │ └─ AIO 路径ctx-aio 为真 → 返回 NGX_AGAIN等待 AIO 完成事件 ├─ [4] 缓冲区状态标记 │ ├─ ctx-in NULL → 清除 r-buffered 中的 NGX_HTTP_COPY_BUFFERED 位 │ └─ ctx-in ! NULL → 设置 r-buffered 中的 NGX_HTTP_COPY_BUFFERED 位 └─ [5] 返回 rc └─ 透传 ngx_output_chain 的返回值给上游{ngx_int_trc;ngx_connection_t*c;ngx_output_chain_ctx_t*ctx;ngx_http_core_loc_conf_t*clcf;ngx_http_copy_filter_conf_t*conf;局部变量声明cr-connection;获取连接ngx_log_debug2(NGX_LOG_DEBUG_HTTP,c-log,0,http copy filter: \%V?%V\,r-uri,r-args);调试日志1 上下文初始化ctx NULL 分支ctxngx_http_get_module_ctx(r,ngx_http_copy_filter_module);if(ctxNULL){ctxngx_pcalloc(r-pool,sizeof(ngx_output_chain_ctx_t));if(ctxNULL){returnNGX_ERROR;}ngx_http_set_ctx(r,ctx,ngx_http_copy_filter_module);进入条件copy filter 的模块上下文ctx为 NULL表明这是整个请求生命周期中第一次调用本函数。Nginx body 过滤链上的每个模块使用ngx_http_get_module_ctx从请求对象的上下文数组中取出自己的私有数据如果尚未分配则返回 NULL。分配上下文ngx_pcalloc从请求内存池中分配并零初始化一个ngx_output_chain_ctx_t结构体。选择ngx_pcalloc而非ngx_palloc的原因是确保所有位域sendfile、need_in_memory等初始值为 0字段in、busy、free等指针初始值为 NULL无需逐字段赋零。ngx_http_get_module_ctx(r, module)宏定义在src/http/ngx_http.h#definengx_http_get_module_ctx(r,module)(r)-ctx[module.ctx_index]展开为数组索引访问ngx_http_set_ctx(r, c, module)宏定义在src/http/ngx_http.h#definengx_http_set_ctx(r,c,module)r-ctx[module.ctx_index]c;ngx_output_chain_ctx_t1-1 基本字段初始化confngx_http_get_module_loc_conf(r,ngx_http_copy_filter_module);clcfngx_http_get_module_loc_conf(r,ngx_http_core_module);ctx-sendfilec-sendfile;ctx-need_in_memoryr-main_filter_need_in_memory||r-filter_need_in_memory;ctx-need_in_tempr-filter_need_temporary;ctx-alignmentclcf-directio_alignment;ctx-poolr-pool;ctx-bufsconf-bufs;ctx-tag(ngx_buf_tag_t)ngx_http_copy_filter_module;1-2 输出回调设置ctx-output_filter(ngx_output_chain_filter_pt)ngx_http_next_body_filter;ctx-filter_ctxr;这是 copy filter 设计中最为关键的两行代码。它们建立了ngx_output_chain框架与 Nginx HTTP 过滤链之间的桥梁output_filter指向过滤链中下一个 body filter即 copy filter 的下游。ngx_output_chain内部在处理完缓冲区之后会调用这个函数指针将处理好的链传给下游。filter_ctxoutput_filter的第一个参数。过滤链中的每个 body filter 都以ngx_http_request_t *r作为第一个参数所以这里直接传r。(ngx_output_chain_filter_pt)是一个函数指针类型转换。这是因为ngx_output_chain框架是一个通用组件定义在src/core/中不依赖 HTTP 层它的输出回调签名为typedefngx_int_t(*ngx_output_chain_filter_pt)(void*ctx,ngx_chain_t*in);而 Nginx HTTP body filter 的实际签名为typedefngx_int_t(*ngx_http_output_body_filter_pt)(ngx_http_request_t*r,ngx_chain_t*in);两者在二进制层面兼容都是int (*)(void*, void*)通过强制类型转换实现通用框架与 HTTP 专用过滤链的对接。1-3 AIO 与线程处理器注册#if(NGX_HAVE_FILE_AIO)if(ngx_file_aioclcf-aioNGX_HTTP_AIO_ON){ctx-aio_handlerngx_http_copy_aio_handler;}#endif#if(NGX_THREADS)if(clcf-aioNGX_HTTP_AIO_THREADS){ctx-thread_handlerngx_http_copy_thread_handler;}#endif这两个代码块受编译期条件控制仅在编译 Nginx 时启用了文件 AIO 或线程池支持时才会被编译。1-4 首次数据标记if(inin-bufngx_buf_size(in-buf)){r-request_output1;}}进入条件in非 NULL有传入的数据链、in-buf非 NULL第一个链节有有效缓冲区、且缓冲区有实际数据ngx_buf_size 0。三个条件同时满足才设置r-request_output。r-request_output的作用这是ngx_http_request_t结构体中的一个unsigned:1位域定义在ngx_http_request.h:544标记该请求已经开始产生输出。它在日志模块中被使用当请求结束时如果request_output 1日志中会记录发送的字节数如果是 0说明请求在产生任何输出前就出错了或被拒绝了。ngx_buf_size(b)宏定义src/core/ngx_buf.h:136-138#definengx_buf_size(b)\(ngx_buf_in_memory(b)?(off_t)((b)-last-(b)-pos):\((b)-file_last-(b)-file_pos))根据缓冲区类型自动选择内存区间或文件区间来计算数据大小。2 AIO 状态同步#if(NGX_HAVE_FILE_AIO||NGX_THREADS)ctx-aior-aio;#endif这段代码在每次调用时执行不仅限于首次初始化位于ngx_output_chain调用之前。设计意图r-aio标记的是当前请求是否有 AIO 操作正在进行中。这个值可能被异步事件处理函数修改如ngx_http_copy_aio_event_handler在 AIO 完成时将r-aio清零而ctx-aio是ngx_output_chain内部判断是否处于 AIO 模式的依据。每次调用前同步这两个值确保ngx_output_chain拿到的是最新的 AIO 状态。如果不做这个同步可能出现AIO 已完成但ctx-aio仍为 1导致ngx_output_chain误判并返回NGX_AGAIN造成不必要的延迟。在未启用 AIO/线程的编译配置下这段代码不会生成任何指令。3 委托 ngx_output_chainrcngx_output_chain(ctx,in);这是 copy filter 的核心——将缓冲区的整理和发送逻辑完全委托给通用的ngx_output_chain框架。copy filter 自身不做任何数据拷贝或转发它只是一个适配层把 Nginx HTTP 过滤链的上下文转换为ngx_output_chain所需的数据结构。ngx_output_chain函数概述ngx_output_chain是 Nginx 核心层src/core/ngx_output_chain.c提供的通用输出链管理引擎被 HTTP copy filter、Mail 代理模块、Stream 代理模块等多个子系统复用。其核心步骤如下第一步快速路径判断如果ctx-in累积中的输入链和ctx-busy正在发送中的链都为空且不在 AIO 模式中则检查是否可以跳过拷贝直接转发in NULL→ 直接透传给output_filter即下游 body filter无操作。in只有一个节点且ngx_output_chain_as_is(ctx, in-buf)返回真 → 直接透传该节点不做拷贝。ngx_output_chain_as_is函数ngx_output_chain.c:244-300判断一个 buffer 是否可以原样传递而不需要拷贝到新缓冲区中。判断条件包括ngx_buf_special(buf)为真 → 是控制标记 bufferflush/sync/last_buf原样传递sendfile未启用且 buffer 不在内存中 → 否需要拷贝need_in_memory为真且 buffer 不在内存中 → 否需要拷贝need_in_temp为真且 buffer 是只读内存memory 或 mmap→ 否需要拷贝。第二步累积输入链如果快速路径未命中将in的所有节点追加到ctx-in累积输入链。ngx_output_chain_add_copy为in的每个节点分配新的ngx_chain_t节点浅拷贝 buf 指针挂到ctx-in的尾部。第三步循环处理累积的数据进入无限循环每次迭代检查 AIO 状态ctx-aio为真 → 直接返回NGX_AGAIN。遍历ctx-in中的每个 buffer校验 buffer 大小不能为 0 且非 special不能为负数如果ngx_output_chain_as_is返回真 → buffer 直接从ctx-in移到输出链否则 → 调用ngx_output_chain_get_buf从缓冲区池中获取或分配输出 buffer然后调用ngx_output_chain_copy_buf将数据拷贝进去再追加到输出链一轮遍历完成后调用ctx-output_filter(ctx-filter_ctx, out)将输出链传递给下游即 write filter。如果下游返回NGX_ERROR或NGX_DONE直接返回。否则调用ngx_chain_update_chains将已发送的 buffer 归还到空闲链ctx-free继续下一轮循环。核心设计思想按需拷贝ngx_output_chain_as_is的快速路径判断避免了不必要的缓冲区拷贝。大多数正常场景下文件 buffer sendfile 可用 无特殊需求数据直接透传零拷贝。缓冲区池ctx-bufs配置限定了输出缓冲区的数量默认 2 个防止无限分配内存。当池中所有 buffer 都在发送中busy时循环退出等待下次output_filter调用后 buffer 被回收。4 缓冲区状态标记if(ctx-inNULL){r-buffered~NGX_HTTP_COPY_BUFFERED;}else{r-buffered|NGX_HTTP_COPY_BUFFERED;}进入条件ngx_output_chain返回后检查ctx-in是否为空。ctx-in的含义ngx_output_chain将待处理的输入数据累积在ctx-in链中每次循环尝试将其拷贝到输出 buffer 并通过output_filter发送。如果output_filter即下游 write filter返回NGX_AGAINsocket 写阻塞则ngx_output_chain会中断循环并返回此时ctx-in中还残留着未处理完的 buffer 节点。因此ctx-in NULL→ 所有累积的数据都已处理完毕并下传copy filter 无积压数据。ctx-in ! NULL→ 还有数据未被下传write filter 阻塞或其他原因中断需要等待重试。r-buffered的作用这是ngx_http_request_t中的一个unsigned:8位掩码字段每一位代表一个 filter 是否还有缓冲数据// 定义在 src/http/ngx_http_request.h:149-151#defineNGX_HTTP_SSI_BUFFERED0x01// bit 0SSI filter 有缓冲#defineNGX_HTTP_SUB_BUFFERED0x02// bit 1sub filter 有缓冲#defineNGX_HTTP_COPY_BUFFERED0x04// bit 2copy filter 有缓冲当r-buffered非零时Nginx 会在ngx_http_finalize_request请求终结函数中阻止关闭连接——还有缓冲数据没发完就不能关闭。注册写事件处理函数ngx_http_writer在 socket 可写时被回调重新进入 body 过滤链。位操作的意义r-buffered ~NGX_HTTP_COPY_BUFFERED清除 bit 2设为 0不影响其他 filter 的标记位。r-buffered | NGX_HTTP_COPY_BUFFERED设置 bit 2设为 1同样不影响其他位。这种位掩码设计允许多个 filter 同时独立标记自己的缓冲状态互不干扰。例如SSI filter 和 copy filter 可能同时有缓冲r-buffered的值会是0x01 | 0x04 0x05。5 返回ngx_log_debug3(NGX_LOG_DEBUG_HTTP,c-log,0,http copy filter: %i \%V?%V\,rc,r-uri,r-args);returnrc;}逐字透传ngx_output_chain的返回值rc。copy filter 自身不改变返回值语义NGX_OK数据已全部处理完。NGX_AGAIN部分数据积压AIO 进行中或下游阻塞等待事件再触发。NGX_ERROR发生不可恢复的错误。这个返回值会沿过滤链上浮最终回到调用源头如 handler 或 upstream 模块由源头决定是否继续产生数据还是终结请求。调试日志第二处ngx_log_debug记录返回值与函数开头的调试日志形成首尾呼应。%i打印ngx_int_t值——在 Nginx 内部NGX_OK0、NGX_ERROR-1、NGX_AGAIN-2。两处日志的存在使得可以在 debug 级别下追踪每次 copy filter 调用的入出全貌。3 关键数据结构ngx_output_chain_ctx_tsrc/core/ngx_buf.h:78-110structngx_output_chain_ctx_s{ngx_buf_t*buf;// 当前用于拷贝的输出缓冲区ngx_chain_t*in;// 累积中待处理的输入链ngx_chain_t*free;// 空闲输出缓冲区链可复用ngx_chain_t*busy;// 正在被下游write filter使用的输出缓冲区链unsignedsendfile:1;// 是否允许 sendfileunsigneddirectio:1;// 是否启用 direct I/Ounsignedunaligned:1;// 是否需要对齐处理unsignedneed_in_memory:1;// 数据是否必须在内存中unsignedneed_in_temp:1;// 数据是否必须在可写内存中unsignedaio:1;// AIO 操作是否进行中#if(NGX_HAVE_FILE_AIO||NGX_COMPAT)ngx_output_chain_aio_pt aio_handler;// AIO 回调#endif#if(NGX_THREADS||NGX_COMPAT)ngx_int_t(*thread_handler)(...);// 线程池回调ngx_thread_task_t*thread_task;// 线程任务对象#endifoff_talignment;// Direct I/O 对齐粒度ngx_pool_t*pool;// 内存池ngx_int_tallocated;// 已分配的缓冲区数量ngx_bufs_tbufs;// 缓冲区池规格数量 大小ngx_buf_tag_ttag;// 缓冲区所有权标签ngx_output_chain_filter_pt output_filter;// 输出回调指向下游 body filtervoid*filter_ctx;// 输出回调的第一个参数HTTP 请求对象};缓冲区生命周期ctx-free ctx-busy (空闲可用) (正在发送) │ │ ngx_output_chain_get_buf │ 分配新 buffer ngx_alloc_chain_link │ ↓ │ ctx-buf │ (当前拷贝用) │ │ │ ├─ 拷贝数据 ──────────────┤ │ ↓ │ output_filter │ (传给下游) ↓ ↓ ctx-free ←── ngx_chain_update_chains ── 已发送 (回收复用)ngx_chain_update_chains在output_filter返回后将已发送完的ngx_chain_t节点从 busy 链归还到 free 链。归还的依据是tag字段只有 tag 匹配的 buffer 才会被回收防止误回收其他模块分配的缓冲区。4 设计要点与边界分析4.1 为什么 copy filter 需要一个独立模块而非内嵌到 write filter将缓冲区管理copy filter和网络发送write filter分离是 Nginx 的经典关注点分离设计复用ngx_output_chainngx_output_chain是通用框架不绑定 HTTP 层。copy filter 作为适配层将其接入 HTTP 过滤链而 stream/mail 模块有各自的适配层。如果嵌入 write filter通用框架就无法被非 HTTP 模块复用。独立配置output_buffers指令只影响 copy filter 的缓冲区池大小不影响 write filter 的发送行为postpone_output、sendfile_max_chunk等由 write filter 独立管理。状态隔离copy filter 的ctx-in累积状态和 write filter 的r-out待发送链是独立的两个层次——前者管理还没整理好的数据后者管理已经整理好要发送的数据。4.2 上下文的生命周期ctx是在第一次调用时通过ngx_pcalloc(r-pool, ...)分配的其生命周期与r-pool绑定。当请求处理完成ngx_http_finalize_request调用ngx_destroy_pool时ctx及其所有关联的缓冲区都会被整体释放无需显式清理。4.3 缓冲区池耗尽场景ctx-bufs默认配置为{num2, size32768}即只有 2 个输出缓冲区。当上游传入大量数据且ngx_output_chain_as_is判断无法直接透传时如数据在 mmap 中且need_in_temp为真两个缓冲区都拷贝满后会被传给下游进入 busy 链此时ngx_output_chain内循环会退出break等待output_filter回调后由ngx_chain_update_chains回收 buffer。这个过程会产生多次NGX_AGAIN→ 写事件 → 再进入的循环但每次循环都向前推进数据不会死锁。4.4 与 upstream 模块的关系当 Nginx 作为反向代理时upstream 模块直接从上游服务器接收响应数据这些数据通常是temporary类型的缓冲区。即使sendfile和need_in_memory都不需要拷贝ngx_output_chain的快速路径会检测到in-next NULL单 buffer且as_is返回真直接将这个 buffer 透传给 write filter实现零拷贝代理。这也是 Nginx 作为反向代理性能卓越的重要原因之一。4.5 编译期条件与运行时灵活性AIO 和线程相关代码由#if (NGX_HAVE_FILE_AIO)/#if (NGX_THREADS)等编译期条件包裹。在不支持这些特性的平台上相关代码根本不会被编译不产生任何二进制大小或运行时开销。在支持这些特性的平台上具体的 AIO/线程模式由配置指令aio onvsaio threads在运行时决定而不是编译期写死。