nginx處理post請求之資料轉發
上一篇文章分析了nginx在處理post請求時,如何啟動upstream這個負載均衡模組。它是一個http框架,由它來排程具體的http模組,例如fastcgi, proxyd反向代理等,這些模組負責將來自客戶端 的請求包頭,請求包體轉為與後端伺服器通訊的格式。本篇文章來分析nginx是如何將已經轉換後的報文發給後端伺服器。
一、轉發請求資料到後端伺服器流程
ngx_http_upstream_send_request函式負責將轉換後的報文發給後端伺服器,來看下這個函式的實現過程。//與後端伺服器建立連線,並註冊讀寫事件的回撥 static void ngx_http_upstream_connect(ngx_http_request_t *r, ngx_http_upstream_t *u) { //傳送資料給後端伺服器 ngx_http_upstream_send_request(r, u) }
函式內部呼叫gx_output_chain向後端伺服器傳送資料,首次被呼叫時傳遞的引數是u->request_bufs,這個內容也就是經過轉換後的,與後端伺服器通訊的報文。但如果一次傳送不完, 需要再次被事件模組排程執行才能傳送剩餘資料時, 這個函式會再次被呼叫並且傳遞NULL空引數,這是為什麼? 因為gx_output_chain函式內部會把上一次未傳送完的資料報文儲存到ngx_output_chain_ctx_t結構中的busy成員中。 這個函式再次被呼叫時傳遞的引數為空,就可以把busy成員中的剩餘資料傳送給後端服務。這個函式實現了具體的傳送過程,比較複雜,文章最後面我們再來詳細分析。這裡先分析傳送過程的整體框架,先把控整體,再來分析細節。現在分析兩個場景://傳送資料給後端伺服器 static void ngx_http_upstream_send_request(ngx_http_request_t *r, ngx_http_upstream_t *u) { c = u->peer.connection; //向上遊伺服器傳送請求內容,內容為request_bufs。如果一次性不能傳送完,則會把 //未傳送的儲存到output中的busy成員。第二次被呼叫時,傳遞引數為空,因此函式內部會把上次 //沒傳送完的busy連結串列中的資料繼續發給後端伺服器 rc = ngx_output_chain(&u->output, u->request_sent ? NULL : u->request_bufs); //標記為已經向上遊伺服器傳送了請求 u->request_sent = 1; //先移除寫事件的超時事件 if (c->write->timer_set) { ngx_del_timer(c->write); } //如果一次沒有傳送完所有請求資料,則把重新註冊寫事件超時到epoll if (rc == NGX_AGAIN) { ngx_add_timer(c->write, u->conf->send_timeout); ngx_handle_write_event(c->write, u->conf->send_lowat); return; } /**********執行到此,表示已經向上遊伺服器傳送完所有請求資料**********/ /**********如果有讀事件,則開始接收上游伺服器的響應******************/ ngx_add_timer(c->read, u->conf->read_timeout); //將寫事件設定不做任何事情。因為nginx已經把所有請求都發給了上游伺服器,不需要傳送了, u->write_event_handler = ngx_http_upstream_dummy_handler; ngx_handle_write_event(c->write, 0); }
(1)如果一次排程這個函式不能傳送完所有資料怎麼辦? nginx會重新註冊寫事件到epoll中,這樣寫事件被觸發時,可以傳送剩餘的資料給後端伺服器。來看下這個過程;
//事件模組的讀寫回調,事件被觸發時會呼叫負載均衡模組對應的讀寫回調
static void ngx_http_upstream_handler(ngx_event_t *ev)
{
//c表示nginx與上游伺服器的連線
c = ev->data;
//r表示客戶端與nginx的請求
r = c->data;
//u表示nginx與上游伺服器的upstream
u = r->upstream;
//這時的c表示客戶端與nginx的連線
c = r->connection;
if (ev->write)
{
//向後端伺服器傳送資料
u->write_event_handler(r, u);
}
else
{
//接收後端伺服器的響應
u->read_event_handler(r, u);
}
}
對於負載均衡模組的寫回調write_event_handler為:ngx_http_upstream_send_request_handler,用於在一次沒有傳送完所有的請求資料給後端伺服器時,寫事件再次觸發時將呼叫這個函式傳送剩餘的資料。從中也可以看出,最終還是呼叫ngx_http_upstream_send_request這個函式傳送請求資料給後端伺服器。//一次沒有傳送完所有的請求資料給後端伺服器時,寫事件再次觸發時會呼叫這個函式傳送剩餘的資料
static void ngx_http_upstream_send_request_handler(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
//如果寫超時,表示傳送給上游伺服器的請求超時了
if (c->write->timedout)
{
//重新連線伺服器,會根據策略,可能連線到下一個可用的上游伺服器
ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_TIMEOUT);
return;
}
//再次呼叫這個函式傳送請求
ngx_http_upstream_send_request(r, u);
}
(2)如果傳送完了所有的請求資料給後端伺服器,那該如何處理? 繼續回到ngx_http_upstream_send_request函式進行分析。如果傳送完所有的請求資料後,並且讀事件已經就緒了,則立即讀取來自後端服務的http響應頭部;如果讀事件還未就緒,則把負載均衡模組的write_event_handler寫回調設定為不做任何事情的:ngx_http_upstream_dummy_handler,因為不需要再發送任何資料給後端伺服器了。以此同時nginx將等待讀事件被觸發,當讀事件被觸發時,開始接收來自後端伺服器的http響應頭部。
二、傳送函式分析
呼叫ngx_output_chain函式負責把呼叫層傳進來的資料發給後端伺服器,如果呼叫層傳進來的引數為空,則會把儲存在內部隱藏層中的上一次未傳送完的資料傳送給後端伺服器。在分析函式前,來看下這個函式維護的資料結構,分兩種情況。一種是直接把呼叫層傳進來的資料拷貝到內部隱藏層;另一種是把呼叫層傳進來的引數先拷貝到過濾層,過濾層處理完成後,再把結果傳給內部隱藏層;
1、直接把呼叫層資料傳給內部隱藏層;
呼叫層連結串列,也就是呼叫ngx_output_chain函式傳遞的u->request_bufs連結串列或者空連結串列,也就是應用層要發給後端伺服器的資料。通常如果呼叫層連結串列是一個空節點,或者呼叫層只有一個連結串列節點(要發給後端伺服器的資料較少,一個連結串列節點足以存放所有資料),這兩種情況下會直接把呼叫層的資料直接拷貝到內部隱藏層連結串列,也就是ngx_chain_writer_ctx_t成員out。這個內部隱藏層連結串列是做什麼呢? 用於快取要傳送給後端伺服器的報文,如果報文過大導致一次無法傳送,則會儲存上一次未傳送完成的資料,這也是最終要傳送給後端伺服器的資料。另一個問題,為什麼圖中內部隱藏層連結串列節點數比呼叫層連結串列節點數更多呢? 因為內部隱藏層保留了上一次未傳送完的資料,如果再次呼叫時,呼叫層又傳遞了新資料的話,那也會快取這個新的資料,因此內部隱藏層資料節點肯定比呼叫層過多。
//向後端伺服器傳送資料
//引數in:呼叫層連結串列
ngx_int_t ngx_output_chain(ngx_output_chain_ctx_t *ctx, ngx_chain_t *in)
{
if (ctx->in == NULL && ctx->busy == NULL)
{
if (in == NULL)
{
//ngx_chain_writer,把資料直接傳遞給內部隱藏層進行傳送
return ctx->output_filter(ctx->filter_ctx, in);
}
//呼叫層連結串列由一個節點組成,通常這種情況是要發給後端伺服器的資料較少,
//一個節點足以存放所有資料。且不需要拷貝記憶體操作。
if (in->next == NULL && ngx_output_chain_as_is(ctx, in->buf))
{
//ngx_chain_writer,把資料直接傳遞給內部隱藏層進行傳送
return ctx->output_filter(ctx->filter_ctx, in);
}
}
}
output_filter的回撥為:ngx_chain_writer, 這個函式內部會把資料發給後端伺服器,同時儲存未傳送完成的資料到內部隱藏層的out連結串列中,以便下一次寫事件被觸發時,可以傳送剩餘的資料給後端伺服器。來看下這個函式的實現過程;//將in連結串列的資料傳送到後端伺服器
ngx_int_t ngx_chain_writer(void *data, ngx_chain_t *in)
{
//將in的內容拷貝到輸出連結串列ctx->last中,也就是內部隱藏層的out連結串列。
//之所以要儲存的目的是一次不能傳送完所有的資料到後端伺服器時,可以在下一次把剩餘的資料發給後端伺服器
for (size = 0; in; in = in->next)
{
//將節點插入到out連結串列末尾
cl = ngx_alloc_chain_link(ctx->pool);
cl->buf = in->buf;
cl->next = NULL;
*ctx->last = cl;
ctx->last = &cl->next;
}
//傳送資料給後端伺服器 ngx_writev_chain,返回值為已經發送到哪個節點。
//下一次從這個節點開始傳送剩餘的資料給後端伺服器,返回後的ctx->out指向未傳送完成的連結串列節點
ctx->out = c->send_chain(c, ctx->out, ctx->limit);
}
如果一次沒有傳送完成,則下一次寫事件被排程時,ngx_output_chain傳入的請求資料為空,因此ngx_chain_writer再次被排程時,將會把內部隱藏層ngx_chain_writer_ctx_t的out成員的資料傳送給後端伺服器。2、呼叫層的資料經過過濾後,在發給內部隱藏層
通常呼叫層成傳進來的資料不是一個連結串列節點就可以存放所有資料,或者呼叫層的資料還儲存到檔案中,則這些資料不能直接傳遞給內部隱藏層,而需要經過過濾層進行處理。過濾層把資料處理完後,在發給內部隱藏層。因此對於內部隱藏層來講這是透明的操作,內部隱藏層不管資料是直接來自呼叫層,還是過濾層處理後的結果,它才不關心資料來自何處,只要有資料到來,內部隱藏層就接收。這是一種典型的分層設計思想。
從圖中可以看出,過濾層連結串列ngx_output_chain_ctx_t成員in是一個連結串列,快取來自呼叫層的資料, 而內部隱藏層ngx_chain_writer_ctx_t成員out也是一個連結串列,快取來自過濾層的處理結果或者直接來自呼叫層的資料(呼叫層只傳遞一個連結串列節點,或者傳遞的資料為空)。 為什麼要搞兩個連結串列呢? 豈不是多此一舉,其實不然,nginx這樣設計肯定有它的目的。 因為過濾層連結串列快取的是來自呼叫層的資料,經過過濾處理 ,然後把過濾結果傳遞給內部隱藏層,但有可能一次操作不能把呼叫層傳進來的資料全部過濾完成, 而需要下一次被呼叫時繼續過濾未過慮完的資料,因此需要維護這個過濾層連結串列; 而內部隱藏層連結串列是儲存未傳送給後端伺服器的資料,也因此需要這個內部隱藏層連結串列。這兩個連結串列分工不同,作用也就不同。
ngx_int_t ngx_output_chain(ngx_output_chain_ctx_t *ctx, ngx_chain_t *in)
{
//將連結串列in的內容拷貝到ctx->in連結串列的末尾,也就是儲存到過濾連結串列
if (in)
{
ngx_output_chain_add_copy(ctx->pool, &ctx->in, in);
}
//這個迴圈是為了多次進行過濾操作
for ( ;; )
{
//對過濾連結串列ctx->in中的每一個節點內容進行過濾處理,過濾後儲存到out結果連結串列。
//這裡說的過濾只是判斷是否需要為這個連結串列節點拷貝一份資料到記憶體, 為了方便理解,暫且稱之為過濾。
//ctx->in連結串列中的資料不就是在記憶體嗎?為什麼還需要拷貝一份資料到記憶體。沒錯,如果ctx->in連結串列中的資料在
//記憶體中的話,那肯定不需要在拷貝資料到記憶體了,但ctx->in連結串列中的節點有可能指向的是檔案,而檔案的資料
//是沒有拷貝到記憶體中的,因此對於檔案則需要拷貝到記憶體中來。
while (ctx->in)
{
//計算每一個連結串列節點的大小
bsize = ngx_buf_size(ctx->in->buf);
//如果資料不是在檔案中,則這個函式返回0,表示資料已經在記憶體中了,因此不需要拷貝資料到記憶體中。
//直接把該節點插入到out結果連結串列末尾,相當於這個節點就過濾完成了。否則需要申請buf空間
if (ngx_output_chain_as_is(ctx, ctx->in->buf))
{
cl = ctx->in;
ctx->in = cl->next;
*last_out = cl;
last_out = &cl->next;
cl->next = NULL;
continue;
}
if (ctx->buf == NULL)
{
//建立bsize大小的bufer緩衝區,存放到ctx->buf,通常返回的結果不等於NGX_OK
rc = ngx_output_chain_align_file_buf(ctx, bsize);
if (rc != NGX_OK)
{
if (ctx->free)
{
//從空閒連結串列中獲取buf空間
cl = ctx->free;
ctx->buf = cl->buf;
ctx->free = cl->next;
ngx_free_chain(ctx->pool, cl);
}
else if (out || ctx->allocated == ctx->bufs.num)
{
//申請的buf空間總數超過限制,則先把快取中的資料傳送完後,在來發送剩餘的資料
//傳送完成後,可以重複利用這些快取
break;
}
else if (ngx_output_chain_get_buf(ctx, bsize) != NGX_OK)
{
//重新申請一個buff空間,最多不能申請超過ctx->bufs.num個數
return NGX_ERROR;
}
}
}
//將ctx->in->buf緩衝區的內容拷貝到ctx->buf緩衝區中
rc = ngx_output_chain_copy_buf(ctx);
//已經將該過濾節點的資料移動到了out結果連結串列中,因此可以刪除該過濾節點
if (ngx_buf_size(ctx->in->buf) == 0)
{
ctx->in = ctx->in->next;
}
//建立一個連結串列節點
cl = ngx_alloc_chain_link(ctx->pool);
cl->buf = ctx->buf;
cl->next = NULL;
//將連結串列節點插入到out連結串列末尾
*last_out = cl;
last_out = &cl->next;
ctx->buf = NULL;
}
//傳送資料給後端伺服器 ngx_chain_writer
last = ctx->output_filter(ctx->filter_ctx, out);
//將out中已經發送完的資料移動到free空閒連結串列,之後需要buf空間的話就可以直接從free空閒連結串列中獲取
//而對應沒有傳送完的資料則儲存到busy連結串列,以便下一次寫事件觸發時傳送剩餘的資料
ngx_chain_update_chains(&ctx->free, &ctx->busy, &out, ctx->tag);
//此時out此時一定是空,因為ngx_chain_update_chains函式內部把out未傳送完的資料儲存到busy後,將out設定為空
last_out = &out;
}
}
函式有點長,不過註釋得很清楚了。這裡所說的過濾,是我方便理解而稱之為過濾。說白了就是判斷呼叫層傳進來的資料是否需要做一個記憶體拷貝操作。當呼叫層傳進來的連結串列節點指向的資料是儲存到檔案中時,則需要把檔案中的資料拷貝到記憶體中,拷貝結束後將這個節點插入到過濾結果連結串列。而如果呼叫層傳進來的連結串列指向的資料已經儲存到了記憶體,則不需要在做記憶體拷貝操作了,直接將該節點插入到過濾鏈結果連結串列末尾。
需要注意的是,如果需要執行記憶體拷貝操作,則會有兩種方式獲取一個新的buf空間。一種是從空閒連結串列free中獲取,那這個空閒連結串列從何而來呢? 當傳送資料給後端伺服器完成後,會將已經發送完的連結串列節點插入到空閒連結串列中,從ngx_chain_update_chains函式可以看出這個過程,這樣就可以複用這個緩衝區。另一種方式是,如果空閒連結串列沒有空間容納新的資料了,則重新從記憶體池中申請一個空間,但申請緩衝區的總個數不能超過限制,超過限制時,需要等緩衝區中的資料發給後端伺服器完成後,在複用這片空間,而不是重新開闢。
到此為止, 對於nginx是如何發生fastcgi報文給後端伺服器,以及一次沒傳送完所有資料時,寫事件再次被觸發時是如何傳送剩餘資料給後端伺服器的,這塊邏輯應該清晰了吧! 做個總結,如果呼叫層傳進來的資料比較小,一個節點足以存放所有資料,則直接把資料發給內部隱藏層處理; 如果呼叫層傳進來的資料很大時,需要經過過濾層過濾處理,再把過濾後的結果傳遞給內部隱藏層。 內部隱藏層把資料傳送給後端伺服器,以及儲存一次未傳送完的資料,以便寫事件觸發時再次傳送。通過傳送邏輯的分析,能很好的體現分層設計的思想。
下一篇文章將分析nginx是如何接收來自後端伺服器響應的。