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

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;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章