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 的各种判断到处需要判断是否子请求。