Nginx 的子請求(subrequest)原理分析
Nginx 的子請求設計其依託於自身的access階段,實現了對指定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;
#創建一個 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_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
是有意義的,這樣外層在循環時,就能執行到父請求的handler。
這樣,父請求就被恢復。從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;
}
}