Nginx 源代码笔记 - HTTP 核心 - subrequest


NOTES

本文使用的源代码版本为 1.13.6

子请求是什么

我们先通过网络上可以找到的只言片语大致了解一下「子请求」(subrequest)。

Nginx in AOSA 中说到:

Subrequests are a very important mechanism for request/response processing. Subrequests are also
one of the most powerful aspects of nginx. With subrequests nginx can return the results from a
different URL than the one the client originally requested. Some web frameworks call this an
internal redirect. However, nginx goes further -- not only can filters perform multiple
subrequests and combine the outputs into a single response, but subrequests can also be nested
and hierarchical. A subrequest can perform its own sub-subrequest, and a sub-subrequest can
initiate sub-sub-subrequests. Subrequests can map to files on the hard disk, other handlers,
or upstream servers. Subrequests are most useful for inserting additional content based on data
from the original response. For example, the SSI (server-side include) module uses a filter to
parse the contents of the returned document, and then replaces ``include`` directives with the
contents of specified URLs. Or, it can be an example of making a filter that treats the entire
contents of a document as a URL to be retrieved, and then appends the new document to the URL
itself.

The ``postpone`` filter is used for subrequests.

agentzh's Nginx tutorials 中说到:

Main requests are those initiated externally by HTTP clients... including those doing "internal
redirections" via the ``echo_exec`` or ``rewrite`` directive.

Whereas subrequests are a special kind of requests initiated from within the Nginx core. But
please do not confuse subrequests with those HTTP requests created by the *ngx_proxy* modules!
Subrequests may look very much like an HTTP request in appearance, their implementation,
however, has nothing to do with neither the HTTP protocol nor any kind of socket communication.
A subrequest is an abstract invocation for decomposing the task of the main request into smaller
"internal requests" that can be served independently by multiple different ``location`` blocks,
either in series or in parallel.

"Subrequests" can also be recursive: any subrequest can initiate more sub-subrequests, targeting
other ``location`` blocks or even the current ``location`` itself. According to Nginx's
terminology, if request *A* initiates a subrequest *B*, the *A* is called the "parent request"
of *B*.

It should be noted that the communication of ``location`` blocks via subrequests is limited
within the same ``server`` block, so when the Nginx core processes a subrequest, it just calls
a few C functions behind the scene, without doing any kind of network or UNIX domain socket
communication. For this reason, subrequest are extremely efficient.

<Mastering Nginx> 一书的 The NGINX HTTP Server 章节中说到:

Subrequests are how NGINX can return the results of a request that differs from the URI that the
client sent. Depending on the configuration, they may be multiply nested and call other
subrequests. Filters can collect the responses from multiple subrequests and combine them into
one response to the client. The response is then finalized and sent to the client. Along the
way, multiple modules come into play. See http://www.aosabook.org/en/nginx.html for a detailed
explanation of NGINX internals.

A post reply from Maxim Dounin

Subrequests in nginx aren't really different from ordinary requests, they are handled in the
same way - with location matching and so on...

总结下来, 子请求是 Nginx 请求处理过程中的重要机制,也是 Nginx 提供的强大的特性之一。例如,通过子请 求机制,我们可以:

  • 使用和原始请求 URL 不同的 URL 响应数据作为原始请求的处理结果;
  • 为原始请求创建多个子请求,或者将原始请求拆分成多个子请求,子请求由不同的 location 处理完后, 由 postpone 模块将这些子请求的响应结果进行合并,并作为原始请求的响应数据返回给用户;
  • 根据原始响应数据内容,在原始响应中加入其它附加数据,比如 SSI(服务端 Include)功能就可以使用子请 求实现;
  • 甚至还可以把原始响应内容当成 URL,使用 subrequest 请求该 URL,并把响应数据当成原始请求的响应数据;
  • 等等。

Nginx 中由用户触发的请求,我们称为主请求(main request),主请求处理过程中可以创建子请求,子请求 也可以创建子子请求(sub-subrequest)。在 Nginx 中,如果请求 A 创建了子请求 B,我们将 A 称为 B 的父请 求(parent request)。这样一来,主请求触发的这些子请求就具有了嵌套(nested)和层级关系(hierarchical)。

子请求可以通过任意其它 location 块获取响应结果。从主请求创建子请求,到 Nginx 开始处理子请求这之 间的流程完全成 Nginx 通过 C 函数完成的,并不涉及网络通信。 For this reason, subrequest are extremely efficient.

子请求的 “一生”

接下来,我们从最一般使用场景,也就是,通过子请求响应数据组合出最终响应数据说起,从代码角度分析一下 子请求的创建和调度处理等等相关代码。

创建子请求

Nginx 模块代码可以使用函数 ngx_http_subrequest 创建子请求。新创建的子请求被添加到主请求的「就绪」 请求链表中,随后由函数 ngx_http_run_posted_request 调度处理。

创建流程
函数签名:
ngx_int_t
ngx_http_subrequest(ngx_http_request_t *r,
    ngx_str_t *uri, ngx_str_t *args, ngx_http_request_t **psr,
    ngx_http_post_subrequest_t *ps, ngx_uint_t flags);
参数说明:
参数 作用
r 用于创建子请求的请求。子请求创建成功后,该请求变成子请求的父请求。并且,它本身可能是主 请求( r->main == r ),也可能是子请求( r->main != r
uri 子请求的 uri
args 子请求的请求参数
psr 用于传递子请求结构体
ps 子请求处理完成后可以调用的回调函数
flags

子请求创建方式。目前 Nginx 支持如下 flag:

  • NGX_HTTP_SUBREQUEST_IN_MEMORY - 将该子请求的响应结果保存在内存中。
  • NGX_HTTP_SUBREQUEST_WAITED - TODO
  • NGX_HTTP_SUBREQUEST_CLONE - 继续父请求处理流程。
  • NGX_HTTP_SUBREQUEST_BACKGROUND - 创建后台子请求。此类子请求不参与主请求的响应 构造,也就不会占用主请求的响应时间,但它依然会保持对主请求的引用。

下面我们逐段分析函数 ngx_http_subrequest 的关键逻辑。

  1. Nginx 对每个主请求处理期间可以创建的子请求(包括子子请求)个数进行了限制,目前上限为 64535。同时 Nginx 也对以主请求为根「子请求关系树」(具体说明参见下一节)的层级做了限制,目前上限为 51 ( `` NGX_HTTP_MAX_SUBREQUESTS + 1`` )。

    if (r->subrequests == 0) {
        ...
        return NGX_ERROR;
    }
    
    /*
     * 1000 is reserved for other purposes.
     */
    if (r->main->count >= 65535 - 1000) {
        ...
        return NGX_ERROR;
    }
    ...
    sr->subrequests = r->subrequests - 1;
    
  2. 创建子请求结构体,并根据父请求携带的信息初始化子请求。比如,子请求共用父请求内存池、继承父请求请 求包头、复用主请求请求包体等等。但是它也有自己的一些特性:

    • 子请求默认使用 HTTP GET 方法。
    • 使用 NGX_HTTP_SUBREQUEST_CLONE 标志位创建的子请求,会从父请求当前所在的「阶段」开始,继续「 阶段处理流程」。并且子请求还会使用和父请求相同的 HTTP 方法和 location {} 作用域配置。
    • 未使用 NGX_HTTP_SUBREQUEST_CLONE 标志位创建的子请求,会从 SERVER_REWRITE 阶段进入阶段 处理流程。这类子请求从父请求继承 server {} 作用域配置,在阶段处理流程中,使用 uri 重新查找 使用合适的 location {} 作用域配置。
    sr = ngx_pcalloc(r->pool, sizeof(ngx_http_request_t));
    ...
    c = r->connection
    sr->connection = c;
    ...
    cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
    sr->main_conf = cscf->ctx->main_conf;
    sr->srv_conf = cscf->ctx->srv_conf;
    sr->loc_conf = cscf->ctx->loc_conf;
    
    sr->pool = r->pool;
    ...
    sr->request_body = r->request_body;
    ...
    sr->method = NGX_HTTP_GET;
    ...
    sr->request_line = r->request_line;
    sr->uri = *uri;
    ...
    sr->read_event_handler = ngx_http_request_empty_handler;
    sr->write_event_handler = ngx_http_handler;
    ...
    if (flags & NGX_HTTP_SUBREQUEST_CLONE) {
        sr->method = r->method;
        ...
        sr->loc_conf = r->loc_conf;
        ...
        sr->content_handler = r->content_handler;
        sr->phase_handler = r->phase_handler;
        sr->write_event_handler = ngx_http_core_run_phases;
    
        ngx_http_update_location_config(sr);
    }
    
  3. 根据参数 flags 设置子请求属性值。

    sr->subrequest_in_memory = (flags & NGX_HTTP_SUBREQUEST_IN_MEMORY) != 0;
    sr->waited = (flags & NGX_HTTP_SUBREQUEST_WAITED) != 0;
    sr->background = (flags & NGX_HTTP_SUBREQUEST_BACKGROUND) != 0;
    
  4. 建立子请求和创建者的关系,并将子请求加入「子请求关系树」。Nginx 通过「子请求关系树」控制响应数据 的构造顺序:子请求响应数据优先(于父请求)发往请求者;父请求创建了多个子请求的情况下,按照子请求 创建顺序发送响应数据。同时,Nginx 规定「连接活跃请求」生成的响应数据可以立即发往请求者,而其它请 求的响 应数据必须延迟发送。

    • 使用标志位 NGX_HTTP_SUBREQUEST_BACKGROUND 创建的「后台子请求」不参与响应生产过程,所以并不 需要加入「子请求关系树」。
    • 子请求被包装成 ngx_http_postponed_request_t 类型节点,加入「子请求链」( r->postponed )的尾部。
    • 如果父请求是「连接活跃请求」,并且还从未创建过子请求,那么将新建子请求设置为「连接活跃请求」。
    sr->main = r->main;
    sr->parent = r;
    sr->post_subrequest = ps;
    ...
    if (!sr->background) {
        if (c->data == r && r->postponed == NULL) {
            c->data = sr;
        }
        ...
        pr->request = sr;
        pr->out = NULL;
        pr->next = NULL;
    
        if (r->postponed) {
            for (p = r->postponed; p->next; p = p->next) { /* void */ }
            p->next = pr;
    
        } else {
            r->postponed = pr;
        }
    }
    
  5. 将子请求标记为内部请求。同时,由于子请求代表着一个新的异步处理分支,所以需要增加主请求的引用计数。

    sr->internal = 1;
    ...
    sr->subrequest = r->subrequests - 1;
    ...
    r->main->count++;
    
  6. 最终,函数将子请求加入主请求「就緒」请求链表,等待被 Nginx 调度处理。

    return ngx_http_post_request(sr, NULL);
    
子请求关系树

上一节我们多次提到了「子请求关系树」,本节我们来描绘一下这个树状数据结构是如何形式和它怎样被 Nginx 使用的。

为了更具体的描述子请求关系树,我们根据上节分析得到的信息设计一个模拟场景,然后根据模拟场景绘出简化 后的子请求关系图,以加深理解。

我们前面讲过:由用户直接触发的请求我们称为「主请求」,主请求可以创建「子请求」,子请求也可以创建「子 子请求」。所以我们虚构模拟场景如下:

  • 主请求依次创建子请求「1」、「2」、「3」。那么,请求「1」、「2」、「3」是主请求子节点,它们三个互为 兄弟节点。
  • 子请求「1」创建子子请求「4」。那么,请求「4」是请求「1」的子节点,无兄弟节点。
  • 子请求「2」依次创建子子请求「5」、「6」。那么,请求「5」、「6」是请求「2」的子节点,它们俩者互为兄 弟节点。
  • 子子请求「5」创建子子子请求「7」。请求「7」是请求「5」的子节点,无兄弟节点。

对上述场景,我们简化掉 ngx_http_postponed_request_t 等结构体和 r->postponed 指针,可以画出 下面 Figure.a 的结构图。

Diagram for Nginx Subrequest Tree

如果我们按照 “表示父子关系的箭头作为左子树,表示兄弟关系的箭头作为右子树” 的规则重新摆放 Figure. a 中的各节点,就得到了 Figure. b 中的这样一颗二叉树。这棵树就是我们所说说的「子请求 并系树」。

我们前面还讲过:Nginx 通过「子请求关系树」控制响应数据的构造顺序:子请求响应数据优先(于父请求)发 往请求者;父请求创建了多个子请求的情况下,按照子请求创建顺序发送响应数据。于是,我们就可以使用二叉 树 中序遍历 (Inorder traversal) 算法遍历该「子请求关系树」,按照遍历结果构造最终响应数据了。

对于我们虚构的场景来说,我们可以得到如下序列:

4 -> 1 -> 7 -> 5 -> 6 -> 2 -> 3 -> 主请求

需要注意的是,上面的序列并非子请求被 Nginx 处理的顺序。作为使用事件驱动并发模型的典型代表,Nginx 对所有请求的处理都是异步的,也就是说,各个子请求的执行流并不确定。比如,请求「3」完全可能会比请求「4」 更早被处理完,获得响应数据。

子请求关系树只约定了响应结果的最终组合顺序,这个顺序保证和组合过程由模块 ngx_http_postpone_filter_module 完成。我们会在下面的 子请求响应 一节详细分析这个过程。

处理子请求

子请求创建完成后,Nginx 将其加入到主请求的「就緒」请求队列中。然后在合适的时机调用函数 ngx_http_run_posted_requests 调度处理子请求。这个函数会调用子请求的 write_event_handler 驱 动子请求处理流程。

从上面我们知道,使用标志位 NGX_HTTP_SUBREQUEST_CLONE 创建的子请求 write_event_handler 的值 为 ngx_http_core_run_phases ,并从父请求最后所处的阶段,继续处理流程。而未使用该标志位创建的子 请求 write_event_handler 值为 ngx_http_handler ,从函数 ngx_http_handler 的实现来看, 这 样的子请求会从 SERVER_REWRITE 阶段开始其处理流程。

void
ngx_http_handler(ngx_http_request_t *r)
{
    ...
    if (!r->internal) {
        ...
    } else {
        cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
        r->phase_handler = cmcf->phase_engine.server_rewrite_index;
    }

    r->valid_location = 1;
    ...
    r->write_event_handler = ngx_http_core_run_phases;
    ngx_http_core_run_phases(r);
}

接下来,Nginx 像对待主请求一样开始子请求的处理流程。这部分流程本篇就不再详述了。

我们在对函数 ngx_http_finalize_request 的分析中已经解释了子请求处理完毕后是如何唤醒父请求的, 具体分析可以从 这里 找到。但是其中有一个关键点对理解下面的内容很有帮助,我们在这里重述一下:Nginx 执行完某个子请求完整 流程后,会将子请求的父请求设为其所有连接的「连接活跃请求」。

子请求响应

子请求响应数据组合工作由模块 ngx_http_postpone_filter_module 完成。

前面说过,由于 Nginx 使用异步机制处理子请求的特性,造成「子请求关系树」中的子请求获得响应结果的顺序 也是不确定的。如果已经处理完成的子请求并非「连接活跃请求」,那么该模块就要负责先把该响应数据缓存下来, 然后在合适的时机再将其发送出去。同时,该模块还要在「连接活跃请求」的响应数据发送完成后,选择下一 个 请求做为「连接活跃请求」。

接下来,我们详细分析一下这个模块的主处理函数 ngx_http_postpone_filter

  1. 如果当前请求不是「连接活跃请求」,我们需要暂时缓存它的响应数据。函数 ngx_http_postpone_filter_add 将响应数据追加到当前请求的 r->postponed 单链表尾部,可以看出,r->postponed 不只保存该请 求未完成处理的子请求,也会保存请求本身未发送的响应数据。

    if (r != c->data) {
    
        if (in) {
            ngx_http_postpone_filter_add(r, in);
            return NGX_OK;
        }
    
        return NGX_OK;
    }
    
  2. 如果当前请求是「连接活跃请求」,并且它没有子请求或者未发送响应数据,那么本次调用的响应数据可以直 接发送(交给下一个 body filter)。

    if (r->postponed == NULL) {
    
        if (in || c->buffered) {
            return ngx_http_next_body_filter(r->main, in);
        }
    
        return NGX_OK;
    }
    
  3. 如果当前请求是「连接活跃请求」,但是它还有未处理完的子请求或者上次缓存的响应数据,那么我们将本次 调用的响应数据追加到 r->postponed 单链表尾部。

    if (in) {
        ngx_http_postpone_filter_add(r, in);
    }
    
  4. 接下来,从当前请求的子请求中选择一个做为「连接活跃请求」,或者将暂存响应数据交给下一个 filter module。

    • 被选为「连接活跃请求」的子请求,如果有子子请求,那么在 ngx_http_postpost_filter 函数处理 该子请求时,会将该子子请求设为「连接活跃请求」,以此类推。这样就保证了子请求响应结果的正确发送 顺序。
    • 当前请求如果有响应数据,也需要在其子请求处理完毕后发送。
    do {
        pr = r->postponed;
    
        if (pr->request) {
            ...
            r->postponed = pr->next;
            c->data = pr->request;
    
            return ngx_http_post_request(pr->request, NULL);
        }
    
        if (pr->out == NULL) {
            ...
        } else {
            ...
            if (ngx_http_next_body_filter(r->main, pr->out) == NGX_ERROR) {
                return NGX_ERROR;
            }
        }
    
        r->postponed = pr->next;
    
    } while (r->postponed);
    

就这样,Nginx 在异步的模型下通过函数 ngx_http_finalize_requestngx_http_postpone_filter 协调工作实现了「子请求关系树」中请求响应的顺序发送。

另外一个值得注意的地方是,子请求响应结果也由响应包头部分和响应包体部分组成(毕竟,Nginx 是把子请求当 成普通请求处理的)。响应包头部分一般情况下并不需要发送给用户(主请求响应包头会发送给用户),所以大部 分 header filter 模块都直接丢弃了子请求响应包头,比如模块 ngx_http_header_filter_module 的处理函 数 ngx_http_header_filter 有如下处理:

void
ngx_http_header_filter(ngx_http_request_t *r)
{
    ...
    if (r != r->main) {
        return NGX_OK;
    }
    ...
}

其它应用场景

在 Nginx 各种模块中,除了利用子请求响应数据组合最终响应( ngx_http_addition_filter_module )之 外,还有很多其它应用场景。

比如,模块 ngx_http_auth_request_module 使用子请求验证用户权限,以决定是否继续处理主请求; 模块 ngx_http_ssi_filter_module 根据主请求响应内容的 Server Include 指令,创建子请求并将其 响应结果和原始响应进行拼接;模块 ngx_http_mirror_module 使用创建「后台子请求」实现流量复制功能 等等。

ngx_http_mirror_module 是 Nginx 1.13.4 引入的新模块,通过它可以方便的复制线上真实请求对未上线 模块进行压力测试。这样,在使用 HTTP 协议的系统中,就不用再使用较复杂的 tcpcopy 和 goreplay 等工具 实现类似功能了。下一篇我们将对该模块代码进行分析。

Comments

不要轻轻地离开我,请留下点什么...

comments powered by Disqus

Published

Category

Nginx

Tags

Contact