### subrequest原理解析[](http://tengine.taobao.org/book/chapter_12.html#subrequest-99 "永久鏈接至標題")
子請求并不是http標準里面的概念,它是在當前請求中發起的一個新的請求,它擁有自己的ngx_http_request_t結構,uri和args。一般來說使用subrequest的效率可能會有些影響,因為它需要重新從server rewrite開始走一遍request處理的PHASE,但是它在某些情況下使用能帶來方便,比較常用的是用subrequest來訪問一個upstream的后端,并給它一個ngx_http_post_subrequest_t的回調handler,這樣有點類似于一個異步的函數調用。對于從upstream返回的數據,subrequest允許根據創建時指定的flag,來決定由用戶自己處理(回調handler中)還是由upstream模塊直接發送到out put filter。簡單的說一下subrequest的行為,nginx使用subrequest訪問某個location,產生相應的數據,并插入到nginx輸出鏈的相應位置(創建subrequest時的位置),下面用nginx代碼內的addition模塊(默認未編譯進nginx核心,請使用–with-http_addition_module選項包含此模塊)來舉例說明一下:
[](http:// "點擊提交Issue,反饋你的意見...")
location /main.htm {
# content of main.htm: main
add_before_body /hello.htm;
add_after_body /world.htm;
}
location /hello.htm {
#content of hello.htm: hello
}
location /world.htm {
#content of world.htm: world
}
訪問/main.htm,將得到如下響應:
[](http:// "點擊提交Issue,反饋你的意見...")
hellomainworld
上面的add_before_body指令發起一個subrequest來訪問/hello.htm,并將產生的內容(hello)插入主請求響應體的開頭,add_after_body指令發起一個subrequest訪問/world.htm,并將產生的內容(world)附加在主請求響應體的結尾。addition模塊是一個filter模塊,但是subrequest既可以在phase模塊中使用,也可以在filter模塊中使用。
在進行源碼解析之前,先來想想如果是我們自己要實現subrequest的上述行為,該如何來做?subrequest還可能有自己的subrequest,而且每個subrequest都不一定按照其創建的順序來輸出數據,所以簡單的采用鏈表不好實現,于是進一步聯想到可以采用樹的結構來做,主請求即為根節點,每個節點可以有自己的子節點,遍歷某節點表示處理某請求,自然的可以想到這里可能是用后根(序)遍歷的方法,沒錯,實際上Igor采用樹和鏈表結合的方式實現了subrequest的功能,但是由于節點(請求)產生數據的順序不是固定按節點創建順序(左->右),而且可能分多次產生數據,不能簡單的用后根(序)遍歷。Igor使用了2個鏈表的結構來實現,第一個是每個請求都有的postponed鏈表,一般情況下每個鏈表節點保存了該請求的一個子請求,該鏈表節點定義如下:
[](http:// "點擊提交Issue,反饋你的意見...")
struct ngx_http_postponed_request_s {
ngx_http_request_t *request;
ngx_chain_t *out;
ngx_http_postponed_request_t *next;
};
可以看到它有一個request字段,可以用來保存子請求,另外還有一個ngx_chain_t類型的out字段,實際上一個請求的postponed鏈表里面除了保存子請求的節點,還有保存該請求自己產生的數據的節點,數據保存在out字段;第二個是posted_requests鏈表,它掛載了當前需要遍歷的請求(節點), 該鏈表保存在主請求(根節點)的posted_requests字段,鏈表節點定義如下:
[](http:// "點擊提交Issue,反饋你的意見...")
struct ngx_http_posted_request_s {
ngx_http_request_t *request;
ngx_http_posted_request_t *next;
};
在ngx_http_run_posted_requests函數中會順序的遍歷主請求的posted_requests鏈表:
[](http:// "點擊提交Issue,反饋你的意見...")
void
ngx_http_run_posted_requests(ngx_connection_t *c)
{
...
for ( ;; ) {
/* 連接已經斷開,直接返回 */
if (c->destroyed) {
return;
}
r = c->data;
/* 從posted_requests鏈表的隊頭開始遍歷 */
pr = r->main->posted_requests;
if (pr == NULL) {
return;
}
/* 從鏈表中移除即將要遍歷的節點 */
r->main->posted_requests = pr->next;
/* 得到該節點中保存的請求 */
r = pr->request;
ctx = c->log->data;
ctx->current_request = r;
ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
"http posted request: \"%V?%V\"", &r->uri, &r->args);
/* 遍歷該節點(請求) */
r->write_event_handler(r);
}
}
ngx_http_run_posted_requests函數的調用點后面會做說明。
了解了一些實現的原理,來看代碼就簡單多了,現在正式進行subrequest的源碼解析, 首先來看一下創建subrequest的函數定義:
[](http:// "點擊提交Issue,反饋你的意見...")
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為當前的請求,uri和args為新的要發起的uri和args,當然args可以為NULL,psr為指向一個ngx_http_request_t指針的指針,它的作用就是獲得創建的子請求,ps的類型為ngx_http_post_subrequest_t,它的定義如下:
[](http:// "點擊提交Issue,反饋你的意見...")
typedef struct {
ngx_http_post_subrequest_pt handler;
void *data;
} ngx_http_post_subrequest_t;
typedef ngx_int_t (*ngx_http_post_subrequest_pt)(ngx_http_request_t *r,
void *data, ngx_int_t rc);
它就是之前說到的回調handler,結構里面的handler類型為ngx_http_post_subrequest_pt,它是函數指針,data為傳遞給handler的額外參數。再來看一下ngx_http_subrequest函數的最后一個是flags,現在的源碼中實際上只有2種類型的flag,分別為NGX_HTTP_SUBREQUEST_IN_MEMORY和NGX_HTTP_SUBREQUEST_WAITED,第一個就是指定文章開頭說到的子請求的upstream處理數據的方式,第二個參數表示如果該子請求提前完成(按后序遍歷的順序),是否設置將它的狀態設為done,當設置該參數時,提前完成就會設置done,不設時,會讓該子請求等待它之前的子請求處理完畢才會將狀態設置為done。
進入ngx_http_subrequest函數內部看看:
[](http:// "點擊提交Issue,反饋你的意見...")
{
...
/* 解析flags, subrequest_in_memory在upstream模塊解析完頭部,
發送body給downsstream時用到 */
sr->subrequest_in_memory = (flags & NGX_HTTP_SUBREQUEST_IN_MEMORY) != 0;
sr->waited = (flags & NGX_HTTP_SUBREQUEST_WAITED) != 0;
sr->unparsed_uri = r->unparsed_uri;
sr->method_name = ngx_http_core_get_method;
sr->http_protocol = r->http_protocol;
ngx_http_set_exten(sr);
/* 主請求保存在main字段中 */
sr->main = r->main;
/* 父請求為當前請求 */
sr->parent = r;
/* 保存回調handler及數據,在子請求執行完,將會調用 */
sr->post_subrequest = ps;
/* 讀事件handler賦值為不做任何事的函數,因為子請求不用再讀數據或者檢查連接狀態;
寫事件handler為ngx_http_handler,它會重走phase */
sr->read_event_handler = ngx_http_request_empty_handler;
sr->write_event_handler = ngx_http_handler;
/* ngx_connection_s的data字段比較關鍵,它保存了當前可以向out chain輸出數據的請求,
具體意義后面會做詳細介紹 */
if (c->data == r && r->postponed == NULL) {
c->data = sr;
}
/* 默認共享父請求的變量,當然你也可以根據需求在創建完子請求后,再創建子請求獨立的變量集 */
sr->variables = r->variables;
sr->log_handler = r->log_handler;
pr = ngx_palloc(r->pool, sizeof(ngx_http_postponed_request_t));
if (pr == NULL) {
return NGX_ERROR;
}
pr->request = sr;
pr->out = NULL;
pr->next = NULL;
/* 把該子請求掛載在其父請求的postponed鏈表的隊尾 */
if (r->postponed) {
for (p = r->postponed; p->next; p = p->next) { /* void */ }
p->next = pr;
} else {
r->postponed = pr;
}
/* 子請求為內部請求,它可以訪問internal類型的location */
sr->internal = 1;
/* 繼承父請求的一些狀態 */
sr->discard_body = r->discard_body;
sr->expect_tested = 1;
sr->main_filter_need_in_memory = r->main_filter_need_in_memory;
sr->uri_changes = NGX_HTTP_MAX_URI_CHANGES + 1;
tp = ngx_timeofday();
r->start_sec = tp->sec;
r->start_msec = tp->msec;
r->main->subrequests++;
/* 增加主請求的引用數,這個字段主要是在ngx_http_finalize_request調用的一些結束請求和
連接的函數中使用 */
r->main->count++;
*psr = sr;
/* 將該子請求掛載在主請求的posted_requests鏈表隊尾 */
return ngx_http_post_request(sr, NULL);
}
到這時,子請求創建完畢,一般來說子請求的創建都發生在某個請求的content handler或者某個filter內,從上面的函數可以看到子請求并沒有馬上被執行,只是被掛載在了主請求的posted_requests鏈表中,那它什么時候可以執行呢?之前說到posted_requests鏈表是在ngx_http_run_posted_requests函數中遍歷,那么ngx_http_run_posted_requests函數又是在什么時候調用?它實際上是在某個請求的讀(寫)事件的handler中,執行完該請求相關的處理后被調用,比如主請求在走完一遍PHASE的時候會調用ngx_http_run_posted_requests,這時子請求得以運行。
這時實際還有1個問題需要解決,由于nginx是多進程,是不能夠隨意阻塞的(如果一個請求阻塞了當前進程,就相當于阻塞了這個進程accept到的所有其他請求,同時該進程也不能accept新請求),一個請求可能由于某些原因需要阻塞(比如訪問io),nginx的做法是設置該請求的一些狀態并在epoll中添加相應的事件,然后轉去處理其他請求,等到該事件到來時再繼續處理該請求,這樣的行為就意味著一個請求可能需要多次執行機會才能完成,對于一個請求的多個子請求來說,意味著它們完成的先后順序可能和它們創建的順序是不一樣的,所以必須有一種機制讓提前完成的子請求保存它產生的數據,而不是直接輸出到out chain,同時也能夠讓當前能夠往out chain輸出數據的請求及時的輸出產生的數據。作者Igor采用ngx_connection_t中的data字段,以及一個body filter,即ngx_http_postpone_filter,還有ngx_http_finalize_request函數中的一些邏輯來解決這個問題。
下面用一個圖來做說明,下圖是某時刻某個主請求和它的所有子孫請求的樹結構:
[](http://tengine.taobao.org/book/_images/chapter-12-1.png)
圖中的root節點即為主請求,它的postponed鏈表從左至右掛載了3個節點,SUB1是它的第一個子請求,DATA1是它產生的一段數據,SUB2是它的第2個子請求,而且這2個子請求分別有它們自己的子請求及數據。ngx_connection_t中的data字段保存的是當前可以往out chain發送數據的請求,文章開頭說到發到客戶端的數據必須按照子請求創建的順序發送,這里即是按后序遍歷的方法(SUB11->DATA11->SUB12->DATA12->(SUB1)->DATA1->SUB21->SUB22->(SUB2)->(ROOT)),上圖中當前能夠往客戶端(out chain)發送數據的請求顯然就是SUB11,如果SUB12提前執行完成,并產生數據DATA121,只要前面它還有節點未發送完畢,DATA121只能先掛載在SUB12的postponed鏈表下。這里還要注意一下的是c->data的設置,當SUB11執行完并且發送完數據之后,下一個將要發送的節點應該是DATA11,但是該節點實際上保存的是數據,而不是子請求,所以c->data這時應該指向的是擁有改數據節點的SUB1請求。
下面看下源碼具體是怎樣實現的,首先是ngx_http_postpone_filter函數:
[](http:// "點擊提交Issue,反饋你的意見...")
static ngx_int_t
ngx_http_postpone_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
...
/* 當前請求不能往out chain發送數據,如果產生了數據,新建一個節點,
將它保存在當前請求的postponed隊尾。這樣就保證了數據按序發到客戶端 */
if (r != c->data) {
if (in) {
ngx_http_postpone_filter_add(r, in);
return NGX_OK;
}
...
return NGX_OK;
}
/* 到這里,表示當前請求可以往out chain發送數據,如果它的postponed鏈表中沒有子請求,也沒有數據,
則直接發送當前產生的數據in或者繼續發送out chain中之前沒有發送完成的數據 */
if (r->postponed == NULL) {
if (in || c->buffered) {
return ngx_http_next_filter(r->main, in);
}
/* 當前請求沒有需要發送的數據 */
return NGX_OK;
}
/* 當前請求的postponed鏈表中之前就存在需要處理的節點,則新建一個節點,保存當前產生的數據in,
并將它插入到postponed隊尾 */
if (in) {
ngx_http_postpone_filter_add(r, in);
}
/* 處理postponed鏈表中的節點 */
do {
pr = r->postponed;
/* 如果該節點保存的是一個子請求,則將它加到主請求的posted_requests鏈表中,
以便下次調用ngx_http_run_posted_requests函數,處理該子節點 */
if (pr->request) {
ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
"http postpone filter wake \"%V?%V\"",
&pr->request->uri, &pr->request->args);
r->postponed = pr->next;
/* 按照后序遍歷產生的序列,因為當前請求(節點)有未處理的子請求(節點),
必須先處理完改子請求,才能繼續處理后面的子節點。
這里將該子請求設置為可以往out chain發送數據的請求。 */
c->data = pr->request;
/* 將該子請求加入主請求的posted_requests鏈表 */
return ngx_http_post_request(pr->request, NULL);
}
/* 如果該節點保存的是數據,可以直接處理該節點,將它發送到out chain */
if (pr->out == NULL) {
ngx_log_error(NGX_LOG_ALERT, c->log, 0,
"http postpone filter NULL output",
&r->uri, &r->args);
} else {
ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
"http postpone filter output \"%V?%V\"",
&r->uri, &r->args);
if (ngx_http_next_filter(r->main, pr->out) == NGX_ERROR) {
return NGX_ERROR;
}
}
r->postponed = pr->next;
} while (r->postponed);
return NGX_OK;
}
再來看ngx_http_finalzie_request函數:
[](http:// "點擊提交Issue,反饋你的意見...")
void
ngx_http_finalize_request(ngx_http_request_t *r, ngx_int_t rc)
{
...
/* 如果當前請求是一個子請求,檢查它是否有回調handler,有的話執行之 */
if (r != r->main && r->post_subrequest) {
rc = r->post_subrequest->handler(r, r->post_subrequest->data, rc);
}
...
/* 子請求 */
if (r != r->main) {
/* 該子請求還有未處理完的數據或者子請求 */
if (r->buffered || r->postponed) {
/* 添加一個該子請求的寫事件,并設置合適的write event hander,
以便下次寫事件來的時候繼續處理,這里實際上下次執行時會調用ngx_http_output_filter函數,
最終還是會進入ngx_http_postpone_filter進行處理 */
if (ngx_http_set_write_handler(r) != NGX_OK) {
ngx_http_terminate_request(r, 0);
}
return;
}
...
pr = r->parent;
/* 該子請求已經處理完畢,如果它擁有發送數據的權利,則將權利移交給父請求, */
if (r == c->data) {
r->main->count--;
if (!r->logged) {
clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
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;
/* 如果該子請求不是提前完成,則從父請求的postponed鏈表中刪除 */
if (pr->postponed && pr->postponed->request == r) {
pr->postponed = pr->postponed->next;
}
/* 將發送權利移交給父請求,父請求下次執行的時候會發送它的postponed鏈表中可以
發送的數據節點,或者將發送權利移交給它的下一個子請求 */
c->data = pr;
} else {
/* 到這里其實表明該子請求提前執行完成,而且它沒有產生任何數據,則它下次再次獲得
執行機會時,將會執行ngx_http_request_finalzier函數,它實際上是執行
ngx_http_finalzie_request(r,0),也就是什么都不干,直到輪到它發送數據時,
ngx_http_finalzie_request函數會將它從父請求的postponed鏈表中刪除 */
r->write_event_handler = ngx_http_request_finalizer;
if (r->waited) {
r->done = 1;
}
}
/* 將父請求加入posted_request隊尾,獲得一次運行機會 */
if (ngx_http_post_request(pr, NULL) != NGX_OK) {
r->main->count++;
ngx_http_terminate_request(r, 0);
return;
}
return;
}
/* 這里是處理主請求結束的邏輯,如果主請求有未發送的數據或者未處理的子請求,
則給主請求添加寫事件,并設置合適的write event hander,
以便下次寫事件來的時候繼續處理 */
if (r->buffered || c->buffered || r->postponed || r->blocked) {
if (ngx_http_set_write_handler(r) != NGX_OK) {
ngx_http_terminate_request(r, 0);
}
return;
}
...
}
[](http:// "點擊提交Issue,反饋你的意見...")
- 上篇:nginx模塊開發篇
- nginx平臺初探
- 初探nginx架構
- nginx基礎概念
- connection
- request
- keepalive
- pipe
- lingering_close
- 基本數據結構
- ngx_str_t
- ngx_pool_t
- ngx_array_t
- ngx_hash_t
- ngx_hash_wildcard_t
- ngx_hash_combined_t
- ngx_hash_keys_arrays_t
- ngx_chain_t
- ngx_buf_t
- ngx_list_t
- ngx_queue_t
- nginx的配置系統
- 指令參數
- 指令上下文
- nginx的模塊化體系結構
- 模塊的分類
- nginx的請求處理
- handler模塊
- handler模塊簡介
- 模塊的基本結構
- 模塊配置結構
- 模塊配置指令
- 模塊上下文結構
- 模塊的定義
- handler模塊的基本結構
- handler模塊的掛載
- handler的編寫步驟
- 示例: hello handler 模塊
- handler模塊的編譯和使用
- 更多handler模塊示例分析
- http access module
- http static module
- http log module
- 過濾模塊
- 過濾模塊簡介
- 過濾模塊的分析
- upstream模塊
- upstream模塊
- upstream模塊接口
- memcached模塊分析
- 本節回顧
- 負載均衡模塊
- 配置
- 指令
- 鉤子
- 初始化配置
- 初始化請求
- peer.get和peer.free回調函數
- 本節回顧
- 其他模塊
- core模塊
- event模塊
- 模塊開發高級篇
- 變量
- 下篇:nginx原理解析篇
- nginx架構詳解
- nginx的源碼目錄結構
- nginx的configure原理
- 模塊編譯順序
- nginx基礎設施
- 內存池
- nginx的啟動階段
- 概述
- 共有流程
- 配置解析
- nginx的請求處理階段
- 接收請求流程
- http請求格式簡介
- 請求頭讀取
- 解析請求行
- 解析請求頭
- 請求體讀取
- 讀取請求體
- 丟棄請求體
- 多階段處理請求
- 多階段執行鏈
- POST_READ階段
- SERVER_REWRITE階段
- FIND_CONFIG階段
- REWRITE階段
- POST_REWRITE階段
- PREACCESS階段
- ACCESS階段
- POST_ACCESS階段
- TRY_FILES階段
- CONTENT階段
- LOG階段
- Nginx filter
- header filter分析
- body filter分析
- ngx_http_copy_filter_module分析
- ngx_http_write_filter_module分析
- subrequest原理解析
- https請求處理解析
- 附錄A 編碼風格
- 附錄B 常用API
- 附錄C 模塊編譯,調試與測試