Nginx 的子请求(subrequest)原理分析

Nginx 的子请求设计其依托于自身的多阶段处理流程,实现了对指定url发起旁路请求的功能,通常用来鉴权、镜像等功能。当然还有其他用法这里不一一赘述,通常用户使用的接口有如内置auth_request接口或者lua的capture接口。这两个对外的接口,都使用了Nginx的ngx_http_subrequest函数。本文就稍微梳理下其子请求流程。

背景要求:对Nginx用来描述请求生命周期的各个数据结构有认识,例如ngx_http_request_t以及ngx_connection_t

简单业务

如下配置文件,当外部请求为 /login 时,先发起旁路的请求,请求url为 /auth,当该url返回200后,才继续业务处理。当然,为了简化/auth逻辑,这里直接在 /auth下面返回了2xx,通常情况下,是需要复杂的业务逻辑才能完成认证。(auth_request详细功能请大家自行查询,这里不赘述)。

location /login {
        auth_request /auth/;
        
        #业务处理
        proxy_pass http://xxxx;
    }

    location  /auth {
        return 200 ok;
    #   proxy_pass http://ups;
    }

Nginx是如何实现旁路功能的呢?我们这就探讨下。

当一个外部请求到来时,首先经历的就是Nginx的HTTP常规处理,即解析协议,然后 匹配到location,这些阶段处理后,进入 access 阶段,access阶段位于在content阶段(即proxy_pass)前,所以access阶段流流程能够先于proxy_pass执行。

对于本案例,即上面配置文件例举的auth_request会执行 ngx_http_auth_request_handler这个access阶段函数。

其调用栈如下

ngx_http_auth_request_handler
ngx_http_core_access_phase
ngx_http_core_run_phases
ngx_http_handler
ngx_http_process_request
xxxxx

如果熟悉Nginx处理流程的话,这些调用栈其实是非常熟悉的,ngx_http_core_run_phases用来循环处理各个阶段,ngx_http_core_access_phase就是一个封装,实际调用的是具体注册的ngx_http_auth_request_handler函数。 ngx_http_auth_request_handler函数是干什么的?

static ngx_int_t
ngx_http_auth_request_handler(ngx_http_request_t *r)
{
    ngx_http_request_t            *sr;
    ngx_http_post_subrequest_t    *ps;
    ngx_http_auth_request_ctx_t   *ctx;
    
    ctx = ngx_http_get_module_ctx(r, ngx_http_auth_request_module);

    if (ctx != NULL) {
        #第二次函数进来时会到这里,用来判断认证的状态码,而第一次进来时ctx还是空的。这里我们暂不关心。
    }
    
    ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_auth_request_ctx_t));
    if (ctx == NULL) {
        return NGX_ERROR;
    }

    ps = ngx_palloc(r->pool, sizeof(ngx_http_post_subrequest_t));
    ps->handler = ngx_http_auth_request_done;
    ps->data = ctx;
    
    ngx_http_subrequest(r, &arcf->uri, NULL, &sr, ps, NGX_HTTP_SUBREQUEST_WAITED);
   
   
   return NGX_AGAIN;

}

上面函数有2个关键点,第一个关键点是 调用ngx_http_subrequest,第二个关键点是 return NGX_AGAIN;,先说后者,当返回 NGX_AGAIN 时,外部 ngx_http_core_run_phases 就会退出循环,即不执行后面的阶段(content阶段),这样的目的是为了认证完成后,再执行后面的阶段,这也符合"access"的逻辑。

void
ngx_http_core_run_phases(ngx_http_request_t *r)
{
    ngx_int_t                   rc;
    ngx_http_phase_handler_t   *ph;
    ngx_http_core_main_conf_t  *cmcf;

    cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);

    ph = cmcf->phase_engine.handlers;

    while (ph[r->phase_handler].checker) {

        # ngx_http_core_access_phase 当发现 ngx_http_auth_request_handler返回 NGX_AGAIN时,会返回 NGX_OK
        rc = ph[r->phase_handler].checker(r, &ph[r->phase_handler]);

        if (rc == NGX_OK) {
            return;
        }
    }
}

我们再来看 第二个关键点 ngx_http_subrequest函数:

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

    ngx_http_request_t            *sr;
    
    #关键点:父请求的 引用计数加1,这样父请求尝试释放时,不会真的释放父请求r,比较还在等子请求业务返回
    r->main->count++;
    
    #创建一个 ngx_http_request_t ,这个就是旁路请求的 数据结构,也叫字请求
    sr = ngx_pcalloc(r->pool, sizeof(ngx_http_request_t));
    
    #根据access阶段设置的url,查找对应的location块
    ngx_http_update_location_config(sr);

    #将这个sr插入进 r的posted_requests队列。即这个字请求插入到主请求的一个队列中
    return ngx_http_post_request(sr, NULL);

}

可以看到,这个函数只是创建了个子请求数据结构,然后挂到了父请求的链表,好像也没处理对吧。

那么这个子请求在哪里被处理呢? 我们回头看上文的调用栈,最后使用了xxxxx,因为这部分是协议有关,对于HTTP请求,是ngx_http_process_request_headers,对于HTTP2请求是ngx_http_v2_run_request,这2个函数均有一个ngx_http_run_posted_requests函数(这里自行看源码吧,行数太多了不列了)。该函数就是循环处理 之前生成的子请求,即处理被ngx_http_post_request的sr。

ngx_http_run_posted_requests(ngx_connection_t *c)
{
    ngx_http_request_t         *r;
    ngx_http_posted_request_t  *pr;

    for ( ;; ) {

        if (c->destroyed) {
            return;
        }

        r = c->data;
        pr = r->main->posted_requests;

        if (pr == NULL) {
            return;
        }

        r->main->posted_requests = pr->next;

        #这个r就是ngx_http_subrequest函数生成的sr
        r = pr->request;

        ngx_http_set_log_request(c->log, r);

        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
                       "http posted request: \"%V?%V\"", &r->uri, &r->args);

        #write_event_handler 就是 ngx_http_handler,即和主请求一样,从头跑一边,所以也可以理解为子请求的逻辑可以使用Nginx所有的功能。
        r->write_event_handler(r);
    }
}

所以到这里,我们看到子请求被处理了,处理入口就是ngx_http_handler,即子请求会被从头处理一遍。那么,子请求处理完成后,如何唤醒父请求继续处理自己剩下的阶段的呢?这里就涉及到HTTP的请求释放阶段,任何HTTP请求,在其处理完成后(即响应发送完成)后,会执行到 ngx_http_finalize_request函数:

ngx_http_finalize_request(ngx_http_request_t *r, ngx_int_t rc)
{

    ......
    
    #这里是回调函数,对于auth_request模块,是在 `ngx_http_auth_request_handler`函数中注册的`ngx_http_auth_request_done`,这不是重点。
    if (r != r->main && r->post_subrequest) {
        rc = r->post_subrequest->handler(r, r->post_subrequest->data, rc);
    }

    ......
    
    #这是子请求的判断,父请求 r和r->main是一样的,但是子请求r是自己,r->main是父请求
    if (r != r->main) {
        clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);

        if (r->background) {

        }

        #获取到父请求
        pr = r->parent;

        if (r == c->data) {
            #父请求的引用计数减去
            r->main->count--;

            if (!r->logged) {
                if (clcf->log_subrequest) {
                    ngx_http_log_request(r);
                }

                r->logged = 1;

            } else {
                ngx_log_error(NGX_LOG_ALERT, c->log, 0,
                              "subrequest: \"%V?%V\" logged again",
                              &r->uri, &r->args);
            }

            r->done = 1;

            if (pr->postponed && pr->postponed->request == r) {
                pr->postponed = pr->postponed->next;
            }

            c->data = pr;

        } else {

        }

        #这里pr是父请求,将自己放到自己的post队列里面,这个是唤醒父请求的关键
        if (ngx_http_post_request(pr, NULL) != NGX_OK) {
            r->main->count++;
            ngx_http_terminate_request(r, 0);
            return;
        }

        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
                       "http wake parent request: \"%V?%V\"",
                       &pr->uri, &pr->args);

        return;
    }

}

注意一点,此时ngx_http_finalize_request函数还在我们的ngx_http_run_posted_requests中,所以上面将父请求自己调用ngx_http_post_request把自己放在post队列里面,这样外层在循环时,就能执行到父请求的handler。 这个逻辑和子请求非常像,子请求也是将自己放在post队列里面。

这样,父请求就被恢复。从access阶段接着执行(因为第一次执行的返回的是AGAIN),我们再回头看下ngx_http_auth_request_handler,当我们第二次进入时是怎么样的。

static ngx_int_t
ngx_http_auth_request_handler(ngx_http_request_t *r)
{

    #这里是上文我们忽略的地方,而现在需要重点分析
    
    if (ctx != NULL) {
        if (!ctx->done) {
            return NGX_AGAIN;
        }

        /*
         * as soon as we are done - explicitly set variables to make
         * sure they will be available after internal redirects
         */

        if (ngx_http_auth_request_set_variables(r, arcf, ctx) != NGX_OK) {
            return NGX_ERROR;
        }

        #下面是错误码,非200的,就返回错误码,只有2xx的认证结果,本函数才返回NGX_OK,即外层的阶段循环,会执行后续的阶段
        /* return appropriate status */

        if (ctx->status == NGX_HTTP_FORBIDDEN) {
            return ctx->status;
        }

        if (ctx->status == NGX_HTTP_UNAUTHORIZED) {
            sr = ctx->subrequest;

            h = sr->headers_out.www_authenticate;

            if (!h && sr->upstream) {
                h = sr->upstream->headers_in.www_authenticate;
            }

            if (h) {
                ho = ngx_list_push(&r->headers_out.headers);
                if (ho == NULL) {
                    return NGX_ERROR;
                }

                *ho = *h;

                r->headers_out.www_authenticate = ho;
            }

            return ctx->status;
        }

        if (ctx->status >= NGX_HTTP_OK
            && ctx->status < NGX_HTTP_SPECIAL_RESPONSE)
        {
            return NGX_OK;
        }

        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "auth request unexpected status: %ui", ctx->status);

        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }
}

当然,实际上坑就好多了,你搜一下nginx代码,到处都是 r->main 的各种判断到处需要判断是否子请求。