從源碼角度理解nginx和uwsgi的通信過程

問題來源

曾經遇到過一個項目涉及到了上傳商品圖片的問題,而我在限制圖片大小的時候,是先把整個圖片都讀取到內存中,然後再判斷其大小。這種做法當出現惡意攻擊或者圖片很大時,會嚴重影響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;
    }
}

可以看到該方法是儘可能多的把數據緩存下來再發送給瀏覽器。

總結

  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先存入臨時文件中。

  2. 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無法發送數據而阻塞。

  3. 在工作中,一般是保持proxy_request_buffering和uwsgi_buffering默認開啓的設置,這樣才能充分利用nginx高併發的特性,不會讓一個連接長時間佔用後端的web application;另外如果要限制請求body的大小,如上傳圖片,一般是nginx開到最大合理的大小,然後python按照具體接口的業務,讀一部分,超過大小就扔掉。因爲nginx可能需要限制多個請求body的大小,所以一般設置一個最大值,然後在web應用中根據需求來加以限制。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章