問題來源
曾經遇到過一個項目涉及到了上傳商品圖片的問題,而我在限制圖片大小的時候,是先把整個圖片都讀取到內存中,然後再判斷其大小。這種做法當出現惡意攻擊或者圖片很大時,會嚴重影響web application的性能。原先想通過先判斷首部的content-length來對大小進行限制。但後來覺得,如果圖片是先由前端的nginx完全讀取後再轉發給uwsgi的,那這樣判斷依然會影響nginx的性能。爲此,我查看了nginx的源碼,找到了nginx和uwsgi的通信過程,下面是整個通信的具體流程。
設置回調函數
我們知道,nginx把瀏覽器等發過來的請求通過proxy_pass或者uwsgi_pass轉發給上游的web application進行處理,然後把處理的結果發送給瀏覽器。uwsgi_pass命令的處理函數爲ngx_http_uwsgi_handler,也就是說,當有請求到達配置uwsgi_pass的location時,會調用ngx_http_uwsgi_handler方法,而該方法是整個uwsgi事件處理的入口方法。下面來看該方法:
static ngx_int_t ngx_http_uwsgi_handler(ngx_http_request_t *r)
{
ngx_http_upstream_t *u;
ngx_http_uwsgi_loc_conf_t *uwcf;
uwcf = ngx_http_get_module_loc_conf(r, ngx_http_uwsgi_module);
u = r->upstream;
……
u->create_request = ngx_http_uwsgi_create_request;//根據wsgi協議創建請求包體
u->process_header = ngx_http_uwsgi_process_status_line;//根據wsgi協議解析uwsgi發送來的頭部
……
rc = ngx_http_read_client_request_body(r, ngx_http_upstream_init);//從瀏覽器讀取body數據
……
}
該方法的首先創建一個ngx_http_upstream_t 結構體,然後設置該結構體的幾個回調函數。nginx是一個高度模塊化的web server,每一個功能都是通過一個模塊來實現的,和uwsgi的通信也不例外。整個通信過程是在都是由ngx_http_uwsgi_module.c這個模塊文件實現的。當然,要想把每一個模塊添加到nginx中,必須要有一些鉤子,在nginx中就是一些鏈表,然後把每一個模塊的處理方法添加到這些鏈表中,這樣就會被nginx框架代碼逐一調用。而與web application通信的的鉤子文件就是ngx_http_upstream.c文件,至於nginx是在何時觸發這些鉤子函數的,簡單的說就是把這些鉤子函數賦值給epoll的讀寫事件,當有讀寫請求時,epoll的事件響應機制就會調用這些鉤子函數。大家可以看到,上面的鉤子函數,只有構造請求體,解析響應頭部,確沒有對包體的處理,這是因爲,nginx根本不需要對包體進行處理,只是簡單的存儲轉發。
從瀏覽器讀取數據
首先看nginx如何決定需要從瀏覽器讀取多少數據,這個很明顯是由content-length首部決定的,但也會受到client_max_body_size指令的影響。我們可以在nginx.conf中添加client_max_body_size配置指令,該指令可以放在http、server和location三個地方。nginx讀取完頭部之後,會調用src/http/ngx_http_core_module.c文件中ngx_http_core_find_config_phase方法去根據重寫後的URI檢索出匹配的location塊,此時配置文件已經合併完成,只是簡單的檢索。如果發現配置了client_max_body_size配置文件,則會通過下面的比較來判斷請求體是否超過給定的大小:
ngx_int_t ngx_http_core_find_config_phase(ngx_http_request_t *r, ngx_http_phase_handler_t *ph)
{
if (clcf->client_max_body_size < r->headers_in.content_length_n)
{
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,"client intended to send too large body: %O bytes",
r->headers_in.content_length_n);
}
}
如果沒有超過設定的大小,那麼就把ngx_http_request_body_t結構體中rest設置爲content-length的大小,表示需要讀取的字節數。
下面我們來看看真正的讀取方法ngx_http_read_client_request_body,該方法傳入了一個回調函數ngx_http_upstream_init,而該回調函數就是開啓nginx和上游通信的入口。注意,雖然ngx_http_read_client_request_body方法不會阻塞,即當沒有數據接收時會立即返回,但是,只有當數據全部接收完畢,纔會調用ngx_http_upstream_init方法,具體的實現我們來看代碼:
ngx_int_t ngx_http_read_client_request_body(ngx_http_request_t *r, ngx_http_client_body_handler_pt post_handler)
{
if (rb->rest == 0) { /* 已經全部接收完body */
if (r->request_body_in_file_only) {
ngx_http_write_request_body(r); /* 把緩衝區的數據寫入文件 */
}
post_handler(r); /* 調用回調函數,即ngx_http_upstream_init */
}
r->read_event_handler = ngx_http_read_client_request_body_handler;//設置讀事件
rc = ngx_http_do_read_client_request_body(r);
}
由此可以看出,如果讀取完畢,則調用ngx_http_upstream_init開始和uwsgi進行通信,如果沒有,則首先把讀事件設置爲ngx_http_read_client_request_body_handler方法,該方法內部除了做一些錯誤檢查,還是仍然調用ngx_http_read_client_request_body方法,也就是說只要沒讀取完所有的body,nginx會通知epoll一直調用本方法進行body的讀取操作。在該方法的最後,調用ngx_http_do_read_client_request_body方法進行真正的讀取操作。
static ngx_int_t ngx_http_do_read_client_request_body(ngx_http_request_t *r)
{
for ( ;; ) {
for ( ;; ) {
if (rb->buf->last == rb->buf->end) { /* 請求的request_body成員中的buf緩衝區已寫滿 */
rc = ngx_http_request_body_filter(r, &out);/* 對數據進行過濾並把數據移入請求體緩存中,如果空間不夠,則存入臨時文件 */
}
n = c->recv(c, rb->buf->last, size); //從套接字緩衝區讀取包體到緩衝區中
if (rb->rest == 0) { //已經接收到了完整的包體
break;
}
}
}
if (rb->temp_file || r->request_body_in_file_only) {
ngx_http_write_request_body(r); //把數據移入請求體緩存中,如果空間不夠,則存入臨時文件
}
if (!r->request_body_no_buffering) { //nginx可以通過proxy_request_buffering關閉緩存
r->read_event_handler = ngx_http_block_reading;
rb->post_handler(r);//調用ngx_http_upstream_init方法
}
return NGX_OK;
}
從上面可以看到,如果開啓了proxy_request_buffering(默認開啓),則nginx是把所有的數據都讀取到之後,再發送給uwsgi。
發送數據給uwsgi
下面看整個通信的入口函數ngx_http_upstream_init:
void ngx_http_upstream_init(ngx_http_request_t *r)
{
ngx_http_upstream_init_request(r);
}
static void ngx_http_upstream_init_request(ngx_http_request_t *r)
{
u->create_request(r);//調用ngx_http_uwsgi_create_request創建請求包體
ngx_http_upstream_connect(r, u);//建立和uwsgi的連接
}
在ngx_http_uwsgi_create_request方法中,主要的工作就是根據wsgi協議構建請求報文,主要是首部信息,然後通過建立的連接把報文發送給uwsgi。在方法ngx_http_upstream_connect中,設置了和uwsgi通信的方法:
static void ngx_http_upstream_connect(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
……
rc = ngx_event_connect_peer(&u->peer); //真正進行請求建立連接工作
u->write_event_handler = ngx_http_upstream_send_request_handler; //設置向uwsgi發送請求的方法
u->read_event_handler = ngx_http_upstream_process_header; //設置接收uwsgi請求的方法
……
ngx_http_upstream_send_request(r, u, 1); //調用該方法向上遊服務器發送請求
}
從uwsgi讀取頭部信息
由此可知,當從uwsgi發送數據到nginx時,會觸發讀事件,即調用ngx_http_upstream_process_header方法:
static void ngx_http_upstream_process_header(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
for ( ;; ) {
n = c->recv(c, u->buffer.last, u->buffer.end - u->buffer.last); //從uwsgi接收頭部數據
if (n == NGX_AGAIN) { //如果數據不夠,則繼續加入epoll中
ngx_handle_read_event(c->read, 0);
}
rc = u->process_header(r); //調用uwsgi設置的回調函數ngx_http_uwsgi_process_status_line解析狀態嗎和頭部
if (rc == NGX_AGAIN) { //接收的數據不夠,繼續讀取
continue;
}
break;
}
/* rc == NGX_OK:解析到完整的包頭 */
if (!r->subrequest_in_memory) {
ngx_http_upstream_send_response(r, u);//開始轉發響應頭部給客戶端
return;
}
}
解析完頭部信息後,就會調用ngx_http_upstream_process_header中最後的ngx_http_upstream_send_response方法發送響應頭給客戶端。另外在該方法中,就會涉及到用戶在配置uwsgi時,是否開啓接收uwsgi_buffering標誌(默認開啓)。如果開啓,則nginx會儘可能多的把數據緩存下來再發送給瀏覽器,如果內存緩存不夠,則會存入臨時文件;如果被用戶關閉,則nginx最多隻緩存uwsgi_buffer_size大小的body,當緩存滿時,會導致後端基於阻塞模型的uwsgi無法send而阻塞。下面分別討論這兩種情況下的數據傳輸。
未開啓緩存的情況下從uwsgi讀取包體並轉發
在uwsgi_buffering設置爲off的情況下,從uwsgi讀取body和發送body給瀏覽器的方法分別爲ngx_http_upstream_process_non_buffered_upstream和ngx_http_upstream_process_non_buffered_downstream。而這兩個方法內部都是調用了ngx_http_upstream_process_non_buffered_request方法,下面具體來看該方法:
static void ngx_http_upstream_process_non_buffered_request(ngx_http_request_t *r,ngx_uint_t do_write)
{
/* 這是在一個大循環中執行的,也就是說,與上下游間的通信可能反覆執行,即接收一點,發送一點 */
for ( ;; )
{
if (do_write)//向下遊發送響應
{
if (u->out_bufs || u->busy_bufs)
{
/* 向下遊發送out_bufs指向的內容*/
rc = ngx_http_output_filter(r, u->out_bufs);
}
// 到目前爲止需要向下遊轉發的響應包體都已經全部發送完了
if (u->busy_bufs == NULL)
{
b->pos = b->start;
b->last = b->start;
}
}
/* do_write爲0,表示需要由上游讀取響應 */
size = b->end - b->last;//獲取buffer緩衝區中還有多少剩餘空間
if (size && upstream->read->ready)
{
n = upstream->recv(upstream, b->last, size);//將上游的響應接收到buffer緩衝區中
if (n == NGX_AGAIN)
{//期待epoll下次有讀事件時再繼續調度
break;
}
if (n > 0)
{
u->state->response_length += n;
/* 處理包體 */
u->input_filter(u->input_filter_ctx, n);
}
/* 讀取到來自上游的響應,這時設置do_write爲1,準備向下遊轉發剛收到的響應 */
do_write = 1;
continue;
}
break;
}
}
可以看到,該方法是讀一些數據就設置do_write=1,從而觸發發送事件,因此是非緩存的。
開啓緩存的情況下從uwsgi讀取包體並轉發
在uwsgi_buffering設置爲on的情況下,從uwsgi讀取body和發送body給瀏覽器的方法分別爲ngx_http_upstream_process_upstream 和 ngx_http_upstream_process_downstream。而這兩個方法最後都是調用ngx_event_pipe方法,在該方法中,通過標誌位來分別從uwsgi讀取數據和向瀏覽器發送數據,具體如下:
ngx_int_t ngx_event_pipe(ngx_event_pipe_t *p, ngx_int_t do_write)
{
for ( ;; )
{
if (do_write)
{
rc = ngx_event_pipe_write_to_downstream(p);//向下遊客戶端發送響應包體
}
p->read = 0;
p->upstream_blocked = 0;
/* 讀取上游服務器的響應 */
ngx_event_pipe_read_upstream(p);
if (!p->read && !p->upstream_blocked) {
break;
}
/* 當讀取完所有的包體後,會把p->read設置爲1,從而導致do_write爲1,即開始發送數據給瀏覽器 */
do_write = 1;
}
}
從uwsgi讀取數據:在這個過程中會涉及以下幾種情況:1)接收響應頭部時可能接收到部分包體;2)如果沒有達到bufs.num上限,那麼可以分配bufs.size大小的內存塊充當接收緩衝區;4)如果緩衝區全部寫滿,則應該寫入臨時文件:
static ngx_int_t ngx_event_pipe_read_upstream(ngx_event_pipe_t *p)
{
for ( ;; )
{
/* 從緩衝區鏈表中取出一塊ngx_buf_t緩衝區 */
if (p->free_raw_bufs)
{
chain = p->free_raw_bufs;
}
//緩衝區已空,嘗試繼續分配緩衝區
else if (p->allocated < p->bufs.num)
{
b = ngx_create_temp_buf(p->pool, p->bufs.size);//可以從內存池中分配到一塊新的緩衝區
……
}
//緩存區已滿
else if (p->cacheable|| p->temp_file->offset < p->max_temp_file_size)
{
rc = ngx_event_pipe_write_chain_to_temp_file(p);//將響應文件寫入臨時文件中
} else {
break;
}
//接收上游的響應
n = p->upstream->recv_chain(p->upstream, chain);
}
if (p->length == 0) {//數據接收完畢,開發發送數據給瀏覽器
p->upstream_done = 1;
p->read = 1;
}
}
可以看到該方法是儘可能多的把數據緩存下來再發送給瀏覽器。
總結
nginx發送數據到uwsgi:首先nginx會判斷用戶是否設置client_max_body_size指令,如果設置了,則會用該值來和content-length進行比較,如果發送的包體超過了設置的值,則nginx返回413包體過大的錯誤。如果包體在給定範圍內,則nginx會根據proxy_request_buffering是否開啓,來決定是否先緩存客戶端發來的請求。如果關閉了proxy_request_buffering(默認開啓),則nginx是接收到一部分數據就直接發送給uwsgi;如果開啓了proxy_request_buffering,則nginx是是把所有的數據都讀取到之後,再發送給uwsgi,如果body過大,則可能需要把body先存入臨時文件中。
uwsgi返回數據到nginx:如果uwsgi_buffering開啓(默認開啓),nginx會儘可能緩存uwsgi發送來的body,緩衝區的大小由uwsgi_buffers和uwsgi_buffer_size兩個指令設置的緩衝區之和;如果還是存不下,則需要寫入臨時文件,臨時文件的大小由uwsgi_max_temp_size和uwsgi_temp_file_write_size決定;如果關閉,則數據同步的發送給瀏覽器,每次最多緩存uwsgi_buffer_size的數據,可以從upstream->recv()方法看出,如果滿了,會導致uwsgi無法發送數據而阻塞。
在工作中,一般是保持proxy_request_buffering和uwsgi_buffering默認開啓的設置,這樣才能充分利用nginx高併發的特性,不會讓一個連接長時間佔用後端的web application;另外如果要限制請求body的大小,如上傳圖片,一般是nginx開到最大合理的大小,然後python按照具體接口的業務,讀一部分,超過大小就扔掉。因爲nginx可能需要限制多個請求body的大小,所以一般設置一個最大值,然後在web應用中根據需求來加以限制。