Nginx 中的 upstream 与 subrequest 机制

概述

Nginx 提供了两种全异步方式与第三方服务进行通信:upstreamsubrequest。upstream 在与第三方服务器交互时(包括建立TCP 连接、发送请求、接收响应、关闭TCP 连接),不会阻塞Nginx 进程处理其他请求。subrequest 只是分解复杂请求的一种设计模式,它可以把原始请求分解为多个子请求,使得诸多请求协同完成一个用户请求,并且每个请求只关注一个功能。subrequest 访问第三方服务最终也是基于upstream 实现的。

upstream 被定义为访问上游服务器,它把Nginx 定义为反代理服务器,首要功能是透传,其次才是以TCP 获取第三方服务器的内容。Nginx 的HTTP 反向代理模块是基于 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;         

    
    ngx_connection_t                 *connection;

    
    void                            **ctx;
    void                            **main_conf;
    void                            **srv_conf;
    void                            **loc_conf;

    
    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

    
    ngx_http_upstream_t              *upstream;
    ngx_array_t                      *upstream_states;
                                         

    
    ngx_pool_t                       *pool;
    
    ngx_buf_t                        *header_in;

    
    ngx_http_headers_in_t             headers_in;
    
    ngx_http_headers_out_t            headers_out;

    
    ngx_http_request_body_t          *request_body;

    
    time_t                            lingering_time;
    
    time_t                            start_sec;
    
    ngx_msec_t                        start_msec;

    
    ngx_uint_t                        method;       
    ngx_uint_t                        http_version; 

    ngx_str_t                         request_line; 
    ngx_str_t                         uri;          
    ngx_str_t                         args;         
    ngx_str_t                         exten;        
    ngx_str_t                         unparsed_uri; 

    ngx_str_t                         method_name;  
    ngx_str_t                         http_protocol;

    
    ngx_chain_t                      *out;
    
    ngx_http_request_t               *main;
    
    ngx_http_request_t               *parent;
    
    ngx_http_postponed_request_t     *postponed;
    ngx_http_post_subrequest_t       *post_subrequest;
    
    ngx_http_posted_request_t        *posted_requests;

    
    ngx_int_t                         phase_handler;
    
    ngx_http_handler_pt               content_handler;
    
    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;

    
    
    size_t                            header_size;

    
    off_t                             request_length;

    
    ngx_uint_t                        err_status;

    
    ngx_http_connection_t            *http_connection;
#if (NGX_HTTP_SPDY)
    ngx_http_spdy_stream_t           *spdy_stream;
#endif

    
    ngx_http_log_handler_pt           log_handler;

    
    ngx_http_cleanup_t               *cleanup;

    
    
    unsigned                          subrequests:8;
    
    unsigned                          count:8;
    
    unsigned                          blocked:8;

    
    unsigned                          aio:1;

    unsigned                          http_state:4;

    
    unsigned                          complex_uri:1;

    
    unsigned                          quoted_uri:1;

    
    unsigned                          plus_in_uri:1;

    
    unsigned                          space_in_uri:1;

    unsigned                          invalid_header:1;

    unsigned                          add_uri_to_alias:1;
    unsigned                          valid_location:1;
    unsigned                          valid_unparsed_uri:1;
    
    unsigned                          uri_changed:1;
    
    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;

    
    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;

    
    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;
    
    unsigned                          keepalive:1;
    
    unsigned                          lingering_close:1;
    
    unsigned                          discard_body: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;
    
    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

    

    
    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;

    

    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

下面看下 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

在 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 即可;

设置第三方服务器地址

在 ngx_http_upstream_t 结构体中的resolved 成员可直接设置上游服务器的地址,也可以由nginx.conf 文件中配置upstream 模块,并指定上游服务器的地址。resolved 类型定义如下:

设置回调方法

在结构体 ngx_http_upstream_t 中定义了 8 个回调方法:

在这些回调方法中,其中有 3 个非常重要,在模块中是必须要实现的,这 3 个回调函数为:

create_request 在初始化 upstream 时被调用,生成发送到后端服务器的请求缓冲(缓冲链)。reinit_request 在某台后端服务器出错的情况,Nginx 会尝试连接到另一台后端服务器。Nginx 选定新的服务器以后,会先调用此函数,以重新初始化upstream 模块的工作状态,然后再次进行 upstream 连接。process_header 是用于解析上游服务器返回的基于TCP 的响应头部。finalize_request 在正常完成与后端服务器的请求后 或 失败 导致销毁请求时,该方法被调用。input_filter_init 和input_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 时才真正销毁请求。例如:

subrequest 使用方式

subrequest 只是分解复杂请求的一种设计模式,它可以把原始请求分解为多个子请求,使得诸多请求协同完成一个用户请求,并且每个请求只关注一个功能。首先,若不是完全将上游服务器的响应包体转发到下游客户端,基本都会使用subrequest 创建子请求,并由子请求使用upstream 机制访问上游服务器,然后由父请求根据上游响应重新构造返回给下游客户端的响应。

subrequest 的使用步骤如下:

  1. 在 nginx.conf 配置文件中配置好子请求的处理方式;

  2. 启动 subrequest 子请求;

  3. 实现子请求执行结束时的回调函数;

  4. 实现父请求被激活时的回调函数;

配置子请求的处理方式

子请求并不是由 HTTP 框架解析所接收到客户端网络包而得到的,而是由父请求派生的。它的配置和普通请求的配置相同,都是在nginx.conf 文件中配置相应的处理模块。例如:可以在配置文件nginx.conf 中配置以下的子请求访问 https://github.com

启动 subrequest 子请求

subrequest 是在父请求的基础上派生的子请求,subrequest 返回的内容会被附加到父请求上面,他的实现方法是调用ngx_http_subrequest 函数,该函数定义在文件:src/http/ngx_http_core_module.h

该函数的参数如下:引用自文件《Emiller's Advanced Topics In Nginx Module Development

  • *r is the original request(当前的请求,即父请求);

  • uri andargsrefer 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

以下是子请求函数 ngx_http_subrequest 的源码剖析,其源码定义在文件:src/http/ngx_http_core_module.c

子请求结束时的回调函数

在子请求结束时(正常或异常结束)Nginx 会调用ngx_http_post_subrequest_pt 回调处理方法。下面是回调方法的定义:

在结构体 ngx_http_post_subrequest_t 中,生成该结构体的变量时,可把用户的任意数据赋给指针data ,ngx_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

父请求被激活后的回调方法

父请求被激活后的回调方法由指针 ngx_http_event_pt 实现。该方法负责把响应包发送给用户。如下所示:

一个请求中,只能调用一次 subrequest,即不能一次创建多个子请求,但是可以在新创建的子请求中再创建新的子请求。

参考资料:

《深入理解Nginx 》

Emiller's Advanced Topics In Nginx Module Development

nginx subrequest的实现解析

ngx_http_request_t结构体

Last updated

Was this helpful?