Nginx 中的 upstream 與 subrequest 機制

概述

        Nginx 提供了兩種全異步方式與第三方服務進行通信:upstreamsubrequestupstream 在與第三方服務器交互時(包括建立TCP 連接、發送請求、接收響應、關閉TCP 連接),不會阻塞Nginx 進程處理其他請求。subrequest 只是分解複雜請求的一種設計模式,它可以把原始請求分解爲多個子請求,使得諸多請求協同完成一個用戶請求,並且每個請求只關注一個功能。subrequest 訪問第三方服務最終也是基於upstream 實現的。

        upstream 被定義爲訪問上游服務器,它把Nginx 定義爲反代理服務器,首要功能是透傳,其次纔是以TCP 獲取第三方服務器的內容。NginxHTTP 反向代理模塊是基於 upstream 方式實現的。subrequest 是子請求,也就是說subrequest 將會爲用戶創建子請求,即將一個複雜的請求分解爲多個子請求,每個子請求負責一種功能項,而最初的原始請求負責構成併發送響應給用戶。當subrequest 訪問第三服務時,首先派生出子請求訪問上游服務器,父請求在完全取得上游服務器的響應後再決定如何處理來自客戶端的請求。

        因此,若希望把是第三方服務的內容原封不動地返回給用戶時,則使用 upstream 方式。若訪問第三方服務是爲了獲取某些信息,再根據這些信息來構造響應併發給用戶,則應使用 subrequest 方式。

upstream 使用方式

        upstream 模塊不產生自己的內容,而是通過請求後端服務器得到內容。Nginx 內部封裝了請求並取得響應內容的整個過程,所以upstream 模塊只需要開發若干回調函數,完成構造請求和解析響應等具體的工作。

ngx_http_request_t 結構體

        首先了解 upstream 是如何嵌入到一個請求中,這裏必須從請求結構體 ngx_http_request_t  入手,在該結構體中具有一個ngx_http_upstream_t  結構體類型的成員upstream。請求結構體 ngx_http_request_t 定義在文件 src/http/ngx_http_request.h 中如下:

struct ngx_http_request_s {
    uint32_t                          signature;         /* "HTTP" */

    /* 當前請求所對應的客戶端連接 */
    ngx_connection_t                 *connection;

    /*
     * 以下四個成員是保存模塊對應的上下文結構指針;
     * ctx 對應的是自定義的上下文結構指針數組,若是HTTP框架,則存儲所有HTTP模塊上下文結構;
     * main_conf 對應的是main級別配置結構體的指針數組;
     * srv_conf 對應的是srv級別配置結構體的指針數組;
     * loc_conf 對應的是loc級別配置結構體的指針數組;
     */
    void                            **ctx;
    void                            **main_conf;
    void                            **srv_conf;
    void                            **loc_conf;

    /*
     * 以下兩個是處理http請求;
     * 當http頭部接收完畢,第一次在業務上處理http請求時,http框架提供的處理方法是ngx_http_process_request;
     * 若該方法無法一次性處理完該請求的全部業務時,當控制權歸還給epoll事件模塊後,若該請求再次被回調時,
     * 此時,將通過ngx_http_request_handler方法進行處理,而這個方法中對於可讀或可寫事件的處理就是由函數
     * read_event_handler或write_event_handler 來處理請求;
     */
    ngx_http_event_handler_pt         read_event_handler;
    ngx_http_event_handler_pt         write_event_handler;

#if (NGX_HTTP_CACHE)
    ngx_http_cache_t                 *cache;
#endif

    /* 若使用upstream機制,則需要以下的結構體 */
    ngx_http_upstream_t              *upstream;
    ngx_array_t                      *upstream_states;
                                         /* of ngx_http_upstream_state_t */

    /* 當前請求的內存池 */
    ngx_pool_t                       *pool;
    /* 主要用於接收http請求頭部內容的緩衝區 */
    ngx_buf_t                        *header_in;


    /*
     * 調用函數ngx_http_request_headers 接收並解析http請求頭部完畢後,
     * 則把解析完成的每一個http頭部加入到結構體headers_in的成員headers鏈表中,
     * 同時初始化該結構體的其他成員;
     */
    ngx_http_headers_in_t             headers_in;
    /*
     * http模塊將待發送的http響應的信息存放在headers_out中,
     * 並期望http框架將headers_out中的成員序列化爲http響應包體發送個客戶端;
     */
    ngx_http_headers_out_t            headers_out;

    /* 接收請求包體的數據結構 */
    ngx_http_request_body_t          *request_body;

    /* 延遲關閉連接的時間 */
    time_t                            lingering_time;
    /* 當前請求初始化的時間 */
    time_t                            start_sec;
    /* 相對於start_sec的毫秒偏移量 */
    ngx_msec_t                        start_msec;

    /*
     * 以下的 9 個成員是函數ngx_http_process_request_line在接收、解析http請求行時解析出的信息 */
    ngx_uint_t                        method;       /* 方法名稱 */
    ngx_uint_t                        http_version; /* 協議版本 */

    ngx_str_t                         request_line; /* 請求行 */
    ngx_str_t                         uri;          /* 客戶請求中的uri */
    ngx_str_t                         args;         /* uri 中的參數 */
    ngx_str_t                         exten;        /* 客戶請求的文件擴展名 */
    ngx_str_t                         unparsed_uri; /* 沒經過URI 解碼的原始請求 */

    ngx_str_t                         method_name;  /* 方法名稱字符串 */
    ngx_str_t                         http_protocol;/* 其data成員指向請求中http的起始地址 */

    /*
     * 存儲待發送給客戶的http響應;
     * out保存着由headers_out序列化後的表示http頭部的TCP流;
     * 調用ngx_http_output_filter方法後,out還保存這待發送的http包體;
     */
    ngx_chain_t                      *out;
    /*
     * 當前請求可能是用戶請求,或是派生的子請求;
     * main標識一序列相關的派生子請求的原始請求;
     * 即通過main與當前請求的地址對比來判斷是用戶請求還是派生子請求;
     */
    ngx_http_request_t               *main;
    /*
     * 當前請求的父親請求,但不一定是原始請求 */
    ngx_http_request_t               *parent;
    /* 以下兩個是與subrequest子請求相關的功能 */
    ngx_http_postponed_request_t     *postponed;
    ngx_http_post_subrequest_t       *post_subrequest;
    /* 連接子請求的鏈表 */
    ngx_http_posted_request_t        *posted_requests;

    /*
     * 全局結構體ngx_http_phase_engine_t定義了一個ngx_http_phase_handler_t回調方法的數組;
     * 而這裏的phase_handler作爲該數組的序列號表示指定數組中的回調方法,相當於數組的下標;
     */
    ngx_int_t                         phase_handler;
    /*
     * 表示NGX_HTTP_CONTENT_PHASE階段提供給http模塊請求的一種方式,它指向http模塊實現的請求處理方法 */
    ngx_http_handler_pt               content_handler;
    /*
     * 在NGX——HTTP_CONTENT_PHASE階段需要判斷請求是否具有訪問權限時,
     * 可通過access_code來傳遞http模塊的handler回調方法的返回值來判斷,
     * 若爲0表示具備權限,否則不具備;
     */
    ngx_uint_t                        access_code;

    ngx_http_variable_value_t        *variables;

#if (NGX_PCRE)
    ngx_uint_t                        ncaptures;
    int                              *captures;
    u_char                           *captures_data;
#endif

    /* 限制當前請求的發送的速率 */
    size_t                            limit_rate;
    size_t                            limit_rate_after;

    /* http響應的長度,不包括http響應頭部 */
    /* used to learn the Apache compatible response length without a header */
    size_t                            header_size;

    /* http請求的長度,包括http請求頭部、http請求包體 */
    off_t                             request_length;

    /* 表示錯誤狀態標誌 */
    ngx_uint_t                        err_status;

    /* http 連接 */
    ngx_http_connection_t            *http_connection;
#if (NGX_HTTP_SPDY)
    ngx_http_spdy_stream_t           *spdy_stream;
#endif

    /* http日誌處理函數 */
    ngx_http_log_handler_pt           log_handler;

    /* 釋放資源 */
    ngx_http_cleanup_t               *cleanup;

    /* 以下都是一些標誌位 */
    /* 派生子請求 */
    unsigned                          subrequests:8;
    /* 作爲原始請求的引用計數,每派生一個子請求,原始請求的成員count會增加1 */
    unsigned                          count:8;
    /* 阻塞標誌位,僅用於aio */
    unsigned                          blocked:8;

    /* 標誌位:爲1表示當前請求是異步IO方式 */
    unsigned                          aio:1;

    unsigned                          http_state:4;

    /* URI with "/." and on Win32 with "//" */
    unsigned                          complex_uri:1;

    /* URI with "%" */
    unsigned                          quoted_uri:1;

    /* URI with "+" */
    unsigned                          plus_in_uri:1;

    /* URI with " " */
    unsigned                          space_in_uri:1;

    unsigned                          invalid_header:1;

    unsigned                          add_uri_to_alias:1;
    unsigned                          valid_location:1;
    unsigned                          valid_unparsed_uri:1;
    /* 標誌位:爲1表示URI已經被重寫 */
    unsigned                          uri_changed:1;
    /* 表示URI被重寫的次數 */
    unsigned                          uri_changes:4;

    unsigned                          request_body_in_single_buf:1;
    unsigned                          request_body_in_file_only:1;
    unsigned                          request_body_in_persistent_file:1;
    unsigned                          request_body_in_clean_file:1;
    unsigned                          request_body_file_group_access:1;
    unsigned                          request_body_file_log_level:3;

    /* 決定是否轉發響應,若該標誌位爲1,表示不轉發響應,否則轉發響應 */
    unsigned                          subrequest_in_memory:1;
    unsigned                          waited:1;

#if (NGX_HTTP_CACHE)
    unsigned                          cached:1;
#endif

#if (NGX_HTTP_GZIP)
    unsigned                          gzip_tested:1;
    unsigned                          gzip_ok:1;
    unsigned                          gzip_vary:1;
#endif

    unsigned                          proxy:1;
    unsigned                          bypass_cache:1;
    unsigned                          no_cache:1;

    /*
     * instead of using the request context data in
     * ngx_http_limit_conn_module and ngx_http_limit_req_module
     * we use the single bits in the request structure
     */
    unsigned                          limit_conn_set:1;
    unsigned                          limit_req_set:1;

#if 0
    unsigned                          cacheable:1;
#endif

    unsigned                          pipeline:1;
    unsigned                          chunked:1;
    unsigned                          header_only:1;
    /* 標誌位,爲1表示當前請求是keepalive模式請求 */
    unsigned                          keepalive:1;
    /* 延遲關閉標誌位,爲1表示需要延遲關閉 */
    unsigned                          lingering_close:1;
    /* 標誌位,爲1表示正在丟棄HTTP請求中的包體 */
    unsigned                          discard_body:1;
    /* 標誌位,爲1表示請求的當前狀態是在做內部跳轉 */
    unsigned                          internal:1;
    unsigned                          error_page:1;
    unsigned                          ignore_content_encoding:1;
    unsigned                          filter_finalize:1;
    unsigned                          post_action:1;
    unsigned                          request_complete:1;
    unsigned                          request_output:1;
    /* 標誌位,爲1表示待發送的HTTP響應頭部已發送給客戶端 */
    unsigned                          header_sent:1;
    unsigned                          expect_tested:1;
    unsigned                          root_tested:1;
    unsigned                          done:1;
    unsigned                          logged:1;

    /* 標誌位,表示緩衝區是否存在待發送內容 */
    unsigned                          buffered:4;

    unsigned                          main_filter_need_in_memory:1;
    unsigned                          filter_need_in_memory:1;
    unsigned                          filter_need_temporary:1;
    unsigned                          allow_ranges:1;
    unsigned                          single_range:1;

#if (NGX_STAT_STUB)
    unsigned                          stat_reading:1;
    unsigned                          stat_writing:1;
#endif

    /* used to parse HTTP headers */

    /* 當前的解析狀態 */
    ngx_uint_t                        state;

    ngx_uint_t                        header_hash;
    ngx_uint_t                        lowcase_index;
    u_char                            lowcase_header[NGX_HTTP_LC_HEADER_LEN];

    u_char                           *header_name_start;
    u_char                           *header_name_end;
    u_char                           *header_start;
    u_char                           *header_end;

    /*
     * a memory that can be reused after parsing a request line
     * via ngx_http_ephemeral_t
     */

    u_char                           *uri_start;
    u_char                           *uri_end;
    u_char                           *uri_ext;
    u_char                           *args_start;
    u_char                           *request_start;
    u_char                           *request_end;
    u_char                           *method_end;
    u_char                           *schema_start;
    u_char                           *schema_end;
    u_char                           *host_start;
    u_char                           *host_end;
    u_char                           *port_start;
    u_char                           *port_end;

    unsigned                          http_minor:16;
    unsigned                          http_major:16;
};

        若沒有實現 upstream 機制,則請求結構體 ngx_http_request_t 中的upstream成員設置爲NULL,否則必須設置該成員。首先看下 HTTP 模塊啓動upstream 機制的過程:

  1. 調用函數 ngx_http_upstream_create 爲請求創建upstream
  2. 設置上游服務器的地址;可通過配置文件 nginx.conf 配置好上游服務器地址;也可以通過ngx_http_request_t 中的成員resolved 設置上游服務器地址;
  3. 設置 upstream 的回調方法;
  4. 調用函數 ngx_http_upstream_init 啓動upstream

        upstream 啓動過程如下圖所示:


ngx_http_upstream_t 結構體

        upstream 結構體是 ngx_http_upstream_t,該結構體只在 upstream 模塊內部使用,其定義在文件:src/http/ngx_http_upstream.h

/* ngx_http_upstream_t 結構體 */
struct ngx_http_upstream_s {
    /* 處理讀事件的回調方法,每一個階段都有不同的 read_event_handler */
    ngx_http_upstream_handler_pt     read_event_handler;
    /* 處理寫事件的回調方法,每一個階段都有不同的 write_event_handler */
    ngx_http_upstream_handler_pt     write_event_handler;

    /* 表示主動向上游服務器發起的連接 */
    ngx_peer_connection_t            peer;

    /*
     * 當向 下游客戶端轉發響應時(此時,ngx_http_request_t 結構體中的subrequest_in_memory標誌位爲0),
     * 若已打開緩存且認爲上游網速更快,此時會使用pipe成員來轉發響應;
     * 使用這種方式轉發響應時,在HTTP模塊使用upstream機制前必須構造pipe結構體;
     */
    ngx_event_pipe_t                *pipe;

    /* 發送給上游服務器的請求,在實現create_request方法時需設置它 */
    ngx_chain_t                     *request_bufs;

    /* 定義了向下遊發送響應的方式 */
    ngx_output_chain_ctx_t           output;
    ngx_chain_writer_ctx_t           writer;

    /* 指定upstream機制的運行方式 */
    ngx_http_upstream_conf_t        *conf;

    /*
     * HTTP模塊實現process_header方法時,若希望upstream直接轉發響應,
     * 則需把解析出來的響應頭部適配爲HTTP的響應頭部,同時需要把包頭中的
     * 信息設置到headers_in結構體中
     */
    ngx_http_upstream_headers_in_t   headers_in;

    /* 解析主機域名,用於直接指定的上游服務器地址 */
    ngx_http_upstream_resolved_t    *resolved;

    /* 接收客戶信息的緩衝區 */
    ngx_buf_t                        from_client;

    /*
     * 接收上游服務器響應包頭的緩衝區,當不直接把響應轉發給客戶端,
     * 或buffering標誌位爲0的情況轉發包體時,接收包體的緩衝區仍然使用buffer
     */
    ngx_buf_t                        buffer;
    off_t                            length;

    /*
     * out_bufs有兩種不同意義:
     * 1、當不需要轉發包體,且默認使用input_filter方法處理包體時,
     *    out_bufs將會指向響應包體,out_bufs鏈表中產生多個ngx_but_t緩衝區,
     *    每個緩衝區都指向buffer緩存中的一部分,而這裏只是調用recv方法接收到的一段TCP流;
     * 2、當需要向下遊轉發包體時,這個鏈表指向上一次向下遊轉發響應到現在這段時間內接收自上游的緩存響應;
     */
    ngx_chain_t                     *out_bufs;
    /*
     * 當需要向下遊轉發響應包體時,它表示上一次向下遊轉發響應時沒有發送完的內容;
     */
    ngx_chain_t                     *busy_bufs;
    /*
     * 這個鏈表用於回收out_bufs中已經發送給下游的ngx_buf_t結構體;
     */
    ngx_chain_t                     *free_bufs;

    /*
     * 處理包體前的初始化方法;
     * 其中data參數用於傳遞用戶數據結構,就是下面成員input_filter_ctx
     */
    ngx_int_t                      (*input_filter_init)(void *data);
    /*
     * 處理包體的方法;
     * 其中data參數用於傳遞用戶數據結構,就是下面成員input_filter_ctx,
     * bytes表示本次接收到包體的長度;
     */
    ngx_int_t                      (*input_filter)(void *data, ssize_t bytes);
    /* 用於傳遞HTTP自定義的數據結構 */
    void                            *input_filter_ctx;

#if (NGX_HTTP_CACHE)
    ngx_int_t                      (*create_key)(ngx_http_request_t *r);
#endif
    /* HTTP模塊實現的create_request方法用於構造發往上游服務器的請求 */
    ngx_int_t                      (*create_request)(ngx_http_request_t *r);
    /* 與上游服務器的通信失敗後,若想再次向上遊服務器發起連接,則調用該函數 */
    ngx_int_t                      (*reinit_request)(ngx_http_request_t *r);
    /*
     * 解析上游服務器返回的響應包頭,該函數返回四個值中的一個:
     * NGX_AGAIN                            表示包頭沒有接收完整;
     * NGX_HTTP_UPSTREAM_INVALID_HEADER     表示包頭不合法;
     * NGX_ERROR                            表示出現錯誤;
     * NGX_OK                               表示解析到完整的包頭;
     */
    ngx_int_t                      (*process_header)(ngx_http_request_t *r);
    /* 當客戶端放棄請求時被調用,由於系統會自動關閉連接,因此,該函數不會進行任何具體操作 */
    void                           (*abort_request)(ngx_http_request_t *r);
    /* 結束upstream請求時會調用該函數 */
    void                           (*finalize_request)(ngx_http_request_t *r,
                                         ngx_int_t rc);
    /*
     * 在上游返回的響應出現location或者refresh頭部表示重定向時,
     * 會通過ngx_http_upstream_process_headers方法調用到可由HTTP模塊
     * 實現的rewrite_redirect方法;
     */
    ngx_int_t                      (*rewrite_redirect)(ngx_http_request_t *r,
                                         ngx_table_elt_t *h, size_t prefix);
    ngx_int_t                      (*rewrite_cookie)(ngx_http_request_t *r,
                                         ngx_table_elt_t *h);

    ngx_msec_t                       timeout;

    /* 用於表示上游響應的狀態:錯誤編碼、包體長度等信息 */
    ngx_http_upstream_state_t       *state;

    ngx_str_t                        method;
    /* 用於記錄日誌 */
    ngx_str_t                        schema;
    ngx_str_t                        uri;

    /* 清理資源 */
    ngx_http_cleanup_pt             *cleanup;

    /* 以下是一些標誌位 */

    /* 指定文件緩存路徑 */
    unsigned                         store:1;
    /* 啓用文件緩存 */
    unsigned                         cacheable:1;
    unsigned                         accel:1;
    /* 基於ssl協議訪問上游服務器 */
    unsigned                         ssl:1;
#if (NGX_HTTP_CACHE)
    unsigned                         cache_status:3;
#endif

    /* 開啓更大的內存及臨時磁盤文件用於緩存來不及發送到下游的響應包體 */
    unsigned                         buffering:1;
    /* keepalive機制 */
    unsigned                         keepalive:1;
    unsigned                         upgrade:1;

    /* 表示是否已向上遊服務器發送請求 */
    unsigned                         request_sent:1;
    /* 表示是否已經轉發響應報頭 */
    unsigned                         header_sent:1;
};

        下面看下 upstream 處理上游響應包體的三種方式:

  1. 當請求結構體 ngx_http_request_t 中的成員subrequest_in_memory 標誌位爲 1 時,upstream 不轉發響應包體到下游,並由HTTP 模塊實現的 input_filter() 方法處理包體;
  2. 當請求結構體 ngx_http_request_t 中的成員subrequest_in_memory 標誌位爲 0 時,且ngx_http_upstream_conf_t 配置結構體中的成員buffering 標誌位爲 1 時,upstream 將開啓更多的內存和磁盤文件用於緩存上游的響應包體(此時,上游網速更快),並轉發響應包體;
  3. 當請求結構體 ngx_http_request_t 中的成員subrequest_in_memory 標誌位爲 0 時,且ngx_http_upstream_conf_t 配置結構體中的成員buffering 標誌位爲 0 時,upstream 將使用固定大小的緩衝區來轉發響應包體;

ngx_http_upstream_conf_t 結構體

        在結構體 ngx_http_upstream_t 的成員conf 中,conf 是一個結構體ngx_http_upstream_conf_t 變量,該變量設置了upstream 的限制性參數。ngx_http_upstream_conf_t 結構體定義如下:src/http/ngx_http_upstream.h

/* ngx_http_upstream_conf_t 結構體 */
typedef struct {
    /*
     * 若在ngx_http_upstream_t結構體中沒有實現resolved成員時,
     * upstream這個結構體纔會生效,定義上游服務器的配置;
     */
    ngx_http_upstream_srv_conf_t    *upstream;

    /* 建立TCP連接的超時時間 */
    ngx_msec_t                       connect_timeout;
    /* 發送請求的超時時間 */
    ngx_msec_t                       send_timeout;
    /* 接收響應的超時時間 */
    ngx_msec_t                       read_timeout;
    ngx_msec_t                       timeout;

    /* TCP的SO_SNOLOWAT選項,表示發送緩衝區的下限 */
    size_t                           send_lowat;
    /* ngx_http_upstream_t中的buffer大小 */
    size_t                           buffer_size;

    size_t                           busy_buffers_size;
    /* 臨時文件的最大長度 */
    size_t                           max_temp_file_size;
    /* 表示緩衝區中的響應寫入到臨時文件時一次寫入字符流的最大長度 */
    size_t                           temp_file_write_size;

    size_t                           busy_buffers_size_conf;
    size_t                           max_temp_file_size_conf;
    size_t                           temp_file_write_size_conf;

    /* 以緩存響應的方式轉發上游服務器的包體時所使用的內存大小 */
    ngx_bufs_t                       bufs;

    /* ignore_headers使得upstream在轉發包頭時跳過對某些頭部的處理 */
    ngx_uint_t                       ignore_headers;
    /*
     * 以二進制位來處理錯誤碼,若處理上游響應時發現這些錯誤碼,
     * 那麼在沒有將響應轉發給下游客戶端時,將會選擇一個上游服務器來重發請求;
     */
    ngx_uint_t                       next_upstream;
    /* 表示所創建的目錄與文件的權限 */
    ngx_uint_t                       store_access;
    /*
     * 轉發響應方式的標誌位,爲1表示啓用更多內存和磁盤文件緩存來自上游響應(即上游網速優先);
     * 若爲0,則啓用固定內存大小緩存上游響應(即下游網速優先);
     */
    ngx_flag_t                       buffering;
    ngx_flag_t                       pass_request_headers;
    ngx_flag_t                       pass_request_body;

    /* 不檢查Nginx與下游之間的連接是否斷開 */
    ngx_flag_t                       ignore_client_abort;
    ngx_flag_t                       intercept_errors;
    /* 複用臨時文件中已使用過的空間 */
    ngx_flag_t                       cyclic_temp_file;

    /* 存放臨時文件的目錄 */
    ngx_path_t                      *temp_path;

    /* 不轉發的頭部 */
    ngx_hash_t                       hide_headers_hash;
    /*
     * 當轉發上游響應頭部到下游客戶端時,
     * 若不希望將某些頭部轉發,則設置在這個數組中
     */
    ngx_array_t                     *hide_headers;
    /*
     * 當轉發上游響應頭部到下游客戶端時,
     * 若希望將某些頭部轉發,則設置在這個數組中
     */
    ngx_array_t                     *pass_headers;

    /* 連接上游服務器的本機地址 */
    ngx_http_upstream_local_t       *local;

#if (NGX_HTTP_CACHE)
    ngx_shm_zone_t                  *cache;

    ngx_uint_t                       cache_min_uses;
    ngx_uint_t                       cache_use_stale;
    ngx_uint_t                       cache_methods;

    ngx_flag_t                       cache_lock;
    ngx_msec_t                       cache_lock_timeout;

    ngx_flag_t                       cache_revalidate;

    ngx_array_t                     *cache_valid;
    ngx_array_t                     *cache_bypass;
    ngx_array_t                     *no_cache;
#endif

    /*
     * 當ngx_http_upstream_t 中的store標誌位爲1時,
     * 如果需要將上游的響應存放在文件中,
     * store_lengths表示存放路徑的長度;
     * store_values表示存放路徑;
     */
    ngx_array_t                     *store_lengths;
    ngx_array_t                     *store_values;

    /* 文件緩存的路徑 */
    signed                           store:2;
    /* 直接將上游返回的404錯誤碼轉發給下游 */
    unsigned                         intercept_404:1;
    /* 根據返回的響應頭部,動態決定是以上游網速還是下游網速優先 */
    unsigned                         change_buffering:1;

#if (NGX_HTTP_SSL)
    ngx_ssl_t                       *ssl;
    ngx_flag_t                       ssl_session_reuse;
#endif

    /* 使用upstream的模塊名稱,僅用於記錄日誌 */
    ngx_str_t                        module;
} ngx_http_upstream_conf_t;

        在 HTTP 反向代理模塊在配置文件 nginx.conf 提供的配置項大都是用來設置結構體 ngx_http_upstream_conf_t 的成員。3 個超時時間成員是必須要設置的,因爲他們默認是 0,即若不設置這 3 個成員,則無法與上游服務器建立TCP 連接。每一個請求都有獨立的ngx_http_upstream_conf_t 結構體,因此,每個請求都可以擁有不同的網絡超時時間等配置。

        例如,將 nginx.conf 文件中的 upstream_conn_timeout 配置項解析到 ngx_http_hello_conf_t 結構體中的成員upstream.conn_timeout 中。可定義如下的連接超時時間,並把ngx_http_hello_conf_t 配置項的 upstream 成員賦給 ngx_http_upstream_t 中的conf 即可;

typedef struct  
{  
        ... 
        ngx_http_upstream_conf_t    upstream;
}ngx_http_hello_conf_t;  
  
  
static ngx_command_t ngx_http_hello_commands[] = {  
   {  
                ngx_string("upstream_conn_timeout"),  
                NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,  
                ngx_conf_set_msec_slot,  
                NGX_HTTP_LOC_CONF_OFFSET,  
                offsetof(ngx_http_hello_conf_t, upstream.conn_timeout),  
                NULL },  
  
  
        ngx_null_command  
};  
/* 在 ngx_http_hello_handler 方法中如下定義 */
  
static ngx_int_t  
ngx_http_hello_handler(ngx_http_request_t *r)  
{  
     ...
     ngx_http_hello_conf_t *mycf = (ngx_http_hello_conf_t *)ngx_http_get_module_loc_conf(r,ngx_http_hello_module);
     r->upstream->conf = &mycf->upstream;
     ...
}

設置第三方服務器地址

        在 ngx_http_upstream_t 結構體中的resolved 成員可直接設置上游服務器的地址,也可以由nginx.conf 文件中配置upstream 模塊,並指定上游服務器的地址。resolved 類型定義如下:

typedef struct {
    /* 主機名稱 */
    ngx_str_t                        host;
    /* 端口號 */
    in_port_t                        port;
    ngx_uint_t                       no_port; /* unsigned no_port:1 */

    /* 地址個數 */
    ngx_uint_t                       naddrs;
    /* 地址 */
    ngx_addr_t                      *addrs;

    /* 上游服務器地址 */
    struct sockaddr                 *sockaddr;
    /* 上游服務器地址長度 */
    socklen_t                        socklen;

    ngx_resolver_ctx_t              *ctx;
} ngx_http_upstream_resolved_t;

設置回調方法

        在結構體 ngx_http_upstream_t 中定義了 8 個回調方法:

    /*
     * 處理包體前的初始化方法;
     * 其中data參數用於傳遞用戶數據結構,就是下面成員input_filter_ctx
     */
    ngx_int_t                      (*input_filter_init)(void *data);
    /*
     * 處理包體的方法;
     * 其中data參數用於傳遞用戶數據結構,就是下面成員input_filter_ctx,
     * bytes表示本次接收到包體的長度;
     */
    ngx_int_t                      (*input_filter)(void *data, ssize_t bytes);
    /* 用於傳遞HTTP自定義的數據結構 */
    void                            *input_filter_ctx;

    /* HTTP模塊實現的create_request方法用於構造發往上游服務器的請求 */
    ngx_int_t                      (*create_request)(ngx_http_request_t *r);
    /* 與上游服務器的通信失敗後,若想再次向上遊服務器發起連接,則調用該函數 */
    ngx_int_t                      (*reinit_request)(ngx_http_request_t *r);
    /*
     * 解析上游服務器返回的響應包頭,該函數返回四個值中的一個:
     * NGX_AGAIN                            表示包頭沒有接收完整;
     * NGX_HTTP_UPSTREAM_INVALID_HEADER     表示包頭不合法;
     * NGX_ERROR                            表示出現錯誤;
     * NGX_OK                               表示解析到完整的包頭;
     */
    ngx_int_t                      (*process_header)(ngx_http_request_t *r);
    /* 當客戶端放棄請求時被調用,由於系統會自動關閉連接,因此,該函數不會進行任何具體操作 */
    void                           (*abort_request)(ngx_http_request_t *r);
    /* 結束upstream請求時會調用該函數 */
    void                           (*finalize_request)(ngx_http_request_t *r,
                                         ngx_int_t rc);
    /*
     * 在上游返回的響應出現location或者refresh頭部表示重定向時,
     * 會通過ngx_http_upstream_process_headers方法調用到可由HTTP模塊
     * 實現的rewrite_redirect方法;
     */
    ngx_int_t                      (*rewrite_redirect)(ngx_http_request_t *r,
                                         ngx_table_elt_t *h, size_t prefix);

        在這些回調方法中,其中有 3 個非常重要,在模塊中是必須要實現的,這 3 個回調函數爲:

/* HTTP模塊實現的create_request方法用於構造發往上游服務器的請求 */
    ngx_int_t                      (*create_request)(ngx_http_request_t *r);
/*
     * 解析上游服務器返回的響應包頭,該函數返回四個值中的一個:
     * NGX_AGAIN                            表示包頭沒有接收完整;
     * NGX_HTTP_UPSTREAM_INVALID_HEADER     表示包頭不合法;
     * NGX_ERROR                            表示出現錯誤;
     * NGX_OK                               表示解析到完整的包頭;
     */
    ngx_int_t                      (*process_header)(ngx_http_request_t *r);
/* 結束upstream請求時會調用該函數 */
    void                           (*finalize_request)(ngx_http_request_t *r,
                                         ngx_int_t rc);

        create_request 在初始化 upstream 時被調用,生成發送到後端服務器的請求緩衝(緩衝鏈)。reinit_request 在某臺後端服務器出錯的情況,Nginx 會嘗試連接到另一臺後端服務器。Nginx 選定新的服務器以後,會先調用此函數,以重新初始化upstream 模塊的工作狀態,然後再次進行 upstream 連接。process_header 是用於解析上游服務器返回的基於TCP 的響應頭部。finalize_request 在正常完成與後端服務器的請求後 或 失敗 導致銷燬請求時,該方法被調用。input_filter_initinput_filter 都用於處理上游的響應包體,因爲在處理包體前HTTP 模塊可能需要做一些初始化工作。初始化工作由input_filter_init 完成,實際處理包體由 input_filter 方法完成。

啓動 upstream 機制

        調用 ngx_http_upstream_init 方法便可啓動upstream 機制,此時,必須通過返回NGX_DONE 通知HTTP 框架暫停執行請求的下一個階段,並且需要執行r->main->count++ 告知HTTP 框架將當前請求的引用計數增加 1,即告知ngx_http_hello_handler 方法暫時不要銷燬請求,因爲HTTP 框架只有在引用計數爲 0 時才真正銷燬請求。例如:

static ngx_int_t ngx_http_hello_handler(ngx_http_request_t *r)
{
    ...
    r->main->count++;
    ngx_http_upstream_init(r);
    return NGX_DONE;
}

subrequest 使用方式

        subrequest 只是分解複雜請求的一種設計模式,它可以把原始請求分解爲多個子請求,使得諸多請求協同完成一個用戶請求,並且每個請求只關注一個功能。首先,若不是完全將上游服務器的響應包體轉發到下游客戶端,基本都會使用subrequest 創建子請求,並由子請求使用upstream 機制訪問上游服務器,然後由父請求根據上游響應重新構造返回給下游客戶端的響應。

        subrequest 的使用步驟如下:

  1. nginx.conf 配置文件中配置好子請求的處理方式;
  2. 啓動 subrequest 子請求;
  3. 實現子請求執行結束時的回調函數;
  4. 實現父請求被激活時的回調函數;

配置子請求的處理方式

        子請求並不是由 HTTP 框架解析所接收到客戶端網絡包而得到的,而是由父請求派生的。它的配置和普通請求的配置相同,都是在nginx.conf 文件中配置相應的處理模塊。例如:可以在配置文件nginx.conf 中配置以下的子請求訪問 https://github.com

location /subrq { 
  	    rewrite ^/subrq(.*)$ $1 break;
  	    proxy_pass https://github.com;
	}

啓動 subrequest 子請求

        subrequest 是在父請求的基礎上派生的子請求,subrequest 返回的內容會被附加到父請求上面,他的實現方法是調用ngx_http_subrequest 函數,該函數定義在文件:src/http/ngx_http_core_module.h

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);

        該函數的參數如下:引用自文件《Emiller's Advanced Topics In Nginx Module Development

  • *r is the original request(當前的請求,即父請求);
  • *uri and*argsrefer to the sub-request*uri 是子請求的URI*args是子請求URI 的參數);
  • **psr is a reference to a NULL pointer that will point to the new (sub-)request structure**psr 是指向返回子請求,相當於值-結果傳遞,作爲參數傳遞進去是指向 NULL 指針,輸出結果是指向新創建的子請求);
  • *ps is a callback for when the subrequest is finished.*ps 是指出子請求結束時必須回調的處理方法);
  • flags can be a bitwise-OR'ed combination of:
    • NGX_HTTP_ZERO_IN_URI: the URI contains a character with ASCII code 0 (also known as '\0'), or contains "%00"
    • NGX_HTTP_SUBREQUEST_IN_MEMORY: store the result of the subrequest in a contiguous chunk of memory (usually not necessary) (將子請求的subrequest_in_memory 標誌位爲 1,表示發起的子請求,訪問的網絡資源返回的響應將全部在內存中處理);
    • NGX_HTTP_SUBREQUEST_WAITED: store the result of the subrequest in a contiguous chunk of memory (usually not necessary) (將子請求的waited 標誌位爲 1,表示子請求完成後會設置自身的r->done 標誌位,可以通過判斷該標誌位得知子請求是否完成);

        該函數 ngx_http_subrequest 的返回值如下:

  • NGX_OK:the subrequest finished without touching the network(成功建立子請求);
  • NGX_DONE:the client reset the network connection(客戶端重置網絡連接);
  • NGX_ERROR:there was a server error of some sort(建立子請求失敗);
  • NGX_AGAIN:the subrequest requires network activity(子請求需要激活網絡);

        該子請求返回的結果附加在你期望的位置。若要修改子請求的結果,可以使用 another filter(或同一個)。並告知該 filter 對父請求或子請求進行操作:具體實例可參照模塊"addition" module

if (r == r->main) { 
    /* primary request */
} else {
    /* subrequest */
}

        以下是子請求函數 ngx_http_subrequest 的源碼剖析,其源碼定義在文件:src/http/ngx_http_core_module.c

/* 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_time_t                    *tp;
    ngx_connection_t              *c;
    ngx_http_request_t            *sr;
    ngx_http_core_srv_conf_t      *cscf;
    ngx_http_postponed_request_t  *pr, *p;

    /* 原始請求的子請求減少一個 */
    r->main->subrequests--;

    /* 若沒有子請求則出錯返回 */
    if (r->main->subrequests == 0) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "subrequests cycle while processing \"%V\"", uri);
        r->main->subrequests = 1;
        return NGX_ERROR;
    }

    /* 分配內存sr */
    sr = ngx_pcalloc(r->pool, sizeof(ngx_http_request_t));
    if (sr == NULL) {
        return NGX_ERROR;
    }

    /* 設置爲 HTTP 模塊 */
    sr->signature = NGX_HTTP_MODULE;

    /* 設置sr的客戶端連接 */
    c = r->connection;
    sr->connection = c;

    /* 爲自定義上下文結構分配內存 */
    sr->ctx = ngx_pcalloc(r->pool, sizeof(void *) * ngx_http_max_module);
    if (sr->ctx == NULL) {
        return NGX_ERROR;
    }

    /* 初始化headers鏈表,該鏈表存儲待發送的http響應包體 */
    if (ngx_list_init(&sr->headers_out.headers, r->pool, 20,
                      sizeof(ngx_table_elt_t))
        != NGX_OK)
    {
        return NGX_ERROR;
    }

    /* 設置main、server、location級別的配置結構體指針 */
    cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
    sr->main_conf = cscf->ctx->main_conf;
    sr->srv_conf = cscf->ctx->srv_conf;
    sr->loc_conf = cscf->ctx->loc_conf;

    /* 設置內存池 */
    sr->pool = r->pool;

    /* 設置headers_in成員,該成員保存解析完成的http頭部 */
    sr->headers_in = r->headers_in;

    ngx_http_clear_content_length(sr);
    ngx_http_clear_accept_ranges(sr);
    ngx_http_clear_last_modified(sr);

    /* 設置接收請求包體的數據結構 */
    sr->request_body = r->request_body;

#if (NGX_HTTP_SPDY)
    sr->spdy_stream = r->spdy_stream;
#endif

    /* 請求的方法名稱 */
    sr->method = NGX_HTTP_GET;
    /* 請求協議的版本 */
    sr->http_version = r->http_version;

    /* 請求行 */
    sr->request_line = r->request_line;
    /* 請求中的uri */
    sr->uri = *uri;

    /* uri中的參數 */
    if (args) {
        sr->args = *args;
    }

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
                   "http subrequest \"%V?%V\"", uri, &sr->args);

    /* 標誌位 */
    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);

    /* 原始請求 */
    sr->main = r->main;
    sr->parent = r;/* 當前請求,即新創建子請求的父請求 */
    sr->post_subrequest = ps;/* 子請求執行結束時,執行的回調方法 */
    /* http請求的可讀或可寫事件的處理方法 */
    sr->read_event_handler = ngx_http_request_empty_handler;
    sr->write_event_handler = ngx_http_handler;

    /* 保存當前可以向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;
    }

    /* 子請求爲內部請求 */
    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();
    sr->start_sec = tp->sec;
    sr->start_msec = tp->msec;

    /* 增加原始請求的引用計數 */
    r->main->count++;

    *psr = sr;/* 指向新創建的子請求 */

    /* 將該子請求掛載到原始請求的posted_requests鏈表隊尾 */
    return ngx_http_post_request(sr, NULL);
}
/* 其中 ngx_http_post_request 定義在文件 src/http/ngx_http_request.c */
ngx_int_t
ngx_http_post_request(ngx_http_request_t *r, ngx_http_posted_request_t *pr)
{
    ngx_http_posted_request_t  **p;

    if (pr == NULL) {
        pr = ngx_palloc(r->pool, sizeof(ngx_http_posted_request_t));
        if (pr == NULL) {
            return NGX_ERROR;
        }
    }

    pr->request = r;
    pr->next = NULL;

    for (p = &r->main->posted_requests; *p; p = &(*p)->next) { /* void */ }

    *p = pr;

    return NGX_OK;
}

子請求結束時的回調函數

        在子請求結束時(正常或異常結束)Nginx 會調用ngx_http_post_subrequest_pt 回調處理方法。下面是回調方法的定義:

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);

        在結構體 ngx_http_post_subrequest_t 中,生成該結構體的變量時,可把用戶的任意數據賦給指針datangx_http_post_subrequest_pt 回調方法的參數data 就是用戶把數據賦給結構體 ngx_http_post_subrequest_t 中的成員指針data 所指的數據。ngx_http_post_subrequest_pt 回調方法中的參數rc 是子請求結束時的狀態,它的取值由函數ngx_http_finalize_request 銷燬請求時傳遞給參數rc。 函數ngx_http_finalize_request 的部分源碼,具體可查閱文件:src/http/ngx_http_request.c

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);
    }

  ...
    
    /* 若 r 是子請求 */
    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;
    }

 ...
} 

父請求被激活後的回調方法

        父請求被激活後的回調方法由指針 ngx_http_event_pt 實現。該方法負責把響應包發送給用戶。如下所示:

typedef void(*ngx_http_event_handler_pt)(ngx_http_request_t *r);

struct ngx_http_request_s{
      ...
      ngx_http_event_handler_pt      write_event_handler;
      ...
};

        一個請求中,只能調用一次 subrequest,即不能一次創建多個子請求,但是可以在新創建的子請求中再創建新的子請求。


參考資料:

《深入理解Nginx 》

Emiller's Advanced Topics In Nginx Module Development

nginx subrequest的實現解析

ngx_http_request_t結構體

發佈了214 篇原創文章 · 獲贊 44 · 訪問量 42萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章