第5章 访问第三方服务

当需要访问第三方服务时,Nginx提供了两种全异步方式来与第三方服务器通信:upstream与subrequest。upstream可以保证在与第三方服务器交互时(包括三次握手建立TCP连接、发送请求、接收响应、四次握手关闭TCP连接等)不会阻塞Nginx进程处理其他请求,也就是说,Nginx仍然可以保持它的高性能。因此,在开发HTTP模块时,如果需要访问第三方服务是不能自己简单地用套接字编程实现的,这样会破坏Nginx优秀的全异步架构。subrequest只是分解复杂请求的一种设计模式,它本质上与访问第三方服务没有任何关系,但从HTTP模块开发者的角度而言,使用subrequest访问第三方服务却很常用,当然,subrequest访问第三方服务最终也是基于upstream实现的。这两种机制是HTTP框架为用户准备的、无阻塞访问第三方服务的利器。

upstream和subrequest的设计目标是完全不同的。从名称中可以看出,upstream被定义为访问上游服务器,也就是说,它把Nginx定义为代理服务器,首要功能是透传,其次才是以TCP获取第三方服务器的内容。Nginx的HTTP反向代理模块就是基于upstream方式实现的。顾名思义,subrequest是从属请求的意思,在这里我们更倾向于称它为子请求,也就是说,subrequest将会为客户请求创建子请求,这是为什么呢?因为异步无阻塞程序的开发过于复杂,所以HTTP框架提供了这种机制将一个复杂的请求分解为多个子请求,每个子请求负责一种功能,而最初的原始请求负责构成并发送响应给客户端。例如,用subrequest访问第三方服务,一般都是派生出子请求访问上游服务器,父请求在完全取得上游服务器的响应后再决定如何处理来自客户端的请求。这样做的好处是每个子请求专注于一种功能。例如,对于一个子请求,通常在NGX_HTTP_CONTENT_PHASE阶段仅会使用一个HTTP模块处理,这大大降低了模块开发的复杂度。从HTTP框架的内部来说,subrequest与upstream也完全不同,upstream是从属于用户请求的,subrequest与原始的用户请求相比是一个(或多个)独立的新请求,只是新的子请求与原始请求之间可以并发的处理。

因此,当我们希望把第三方服务的内容几乎原封不动地返回给用户时,一般使用upstream方式,它可以非常高效地透传HTTP(第12章详细描述了upstream机制的两种透传方式)。可如果我们访问第三方服务只是为了获取某些信息,再依据这些信息来构造响应并发送给用户,这时应该用subrequest方式,因为从业务上来说,这是两件事:获取上游响应,再根据响应内容处理请求,应由两个请求处理。

本章仍然以mytest模块为例进行说明,但会扩展mytest的功能。注意,文中没有提及的代码(如定义mytest模块)都与第3章完全相同。

5.1 upstream的使用方式

Nginx的核心功能——反向代理是基于upstream模块(该模块属于HTTP框架的一部分)实现的。在弄清楚upstream的用法后,完全可以根据自己的需求重写Nginx的反向代理功能。例如,反向代理模块是在先接收完客户请求的HTTP包体后,才向上游服务器建立连接并转发请求的。假设用户要上传大小为1GB的文件,由于网速限制,文件完整地到达Nginx需要10小时,恰巧Nginx与上游服务器间的网络也很差(当然这种情况很少见),反向代理这个请求到上游服务也需要10小时,因此,根据用户的网速也许本来只要10个小时的上传过程,最终可能需要20个小时才能完成。在了解了upstream功能后,可以试着改变反向代理模块的这种特性,比如模仿squid反向代理模式,在接收完整HTTP请求的头部后就与上游服务器建立连接,并开始将请求向上游服务器透传。

upstream的使用方式并不复杂,它提供了8个回调方法,用户只需要视自己的需要实现其中几个回调方法就可以了。在了解这8个回调方法之前,首先要了解upstream是如何嵌入到一个请求中的。

从第3章中的内容可以看到,模块在处理任何一个请求时都有ngx_http_request_t结构的对象r,而请求r中又有一个ngx_http_upstream_t类型的成员upstream。


typedef struct ngx_http_request_s

ngx_http_request_t;

struct ngx_http_request_s{

……

ngx_http_upstream_t

*upstream;

……

};


如果没有使用upstream机制,那么ngx_http_request_t中的upstream成员是NULL空指针,如果使用upstream机制,那么关键在于如何设置r->upstream成员。

图5-1列出了使用HTTP模块启用upstream机制的示意图。下面以mytest模块为例简单地解释一下图5-1。

第5章 访问第三方服务 - 图1

图 5-1 启动upstream的流程图

1)首先需要创建上面介绍的upstream成员,注意,upstream在初始状态下是NULL空指针。可以调用HTTP框架提供好的ngx_http_upstream_create方法来创建upstream。

2)接着设置上游服务器的地址。在HTTP反向代理功能中似乎只能使用在nginx.conf中配置好的上游服务器(参见2.5节的upstream配置块内容),而实际上upstream机制并没有这种要求,用户能够以任意方式指定上游服务器的IP地址。例如,可以从请求的URL或HTTP头部中动态地获取上游服务器地址,ngx_http_upstream_t中的resolved成员就可以帮助用户设置上游服务器(详见5.1.3节)。

3)由于upstream非常灵活,在各个执行阶段中都会试图回调使用它的HTTP模块实现的8个方法(详见5.1.4节),因此,在mytest模块例子中,用户要定义好这些回调方法。

4)在mytest模块中,调用ngx_http_upstream_init方法即可启动upstream机制。注意,ngx_http_mytest_handler方法此时必须返回NGX_DONE,这是在要求HTTP框架不要按阶段继续向下处理请求了,同时它告诉HTTP框架请求必须停留在当前阶段,等待某个HTTP模块主动地继续处理这个请求(例如,在上游服务器主动关闭连接时,upstream模块就会主动地继续处理这个请求,很可能会向客户端发送502响应码)。

使用upstream模块提供的ngx_http_upstream_init方法后,HTTP框架到底如何运行upstream机制呢?图5-2给出了一个常见的upstream执行示意图,它仅在概念上表示主要流程,与代码的执行没有关系。第12章将详细介绍upstream机制到底是如何执行的。

第5章 访问第三方服务 - 图2

图 5-2 upstream执行的一般流程

图5-2所示的upstream流程包含了epoll模块多次调度、处理一个请求的过程,它虽然与实际代码执行关系不大,但却指出了最常用的3个回调方法——create_request、process_header、finalize_request是如何回调的。

注意 upstream提供了3种处理上游服务器包体的方式,包括交由HTTP模块使用input_filter回调方法直接处理包体、以固定缓冲区转发包体、以多个缓冲加磁盘文件的方式转发包体等。在后两种转发包体的方式中,upstream还与文件缓存功能紧密相关,但为了让大家更清晰地理解upstream,本章中将不涉及文件缓存。

5.1.1 ngx_http_upstream_t结构体

上面了解了upstream机制运行的主要流程,现在来看一下ngx_http_upstream_t结构体。ngx_http_upstream_t结构体里有些成员仅仅是在upstream模块内部使用的,这里就不一一列出了(由于C语言是面向过程语言,所以ngx_http_upstream_t结构体里会出现第三方HTTP模块并不关心的成员。在12.1.2节中会完整地介绍ngx_http_upstream_t中的所有成员)。


typedef struct ngx_http_upstream_s

ngx_http_upstream_t;

struct ngx_http_upstream_s{

……

/request_bufs决定发送什么样的请求给上游服务器,在实现create_request方法时需要设置它/

ngx_chain_t

*request_bufs;

//upstream访问时的所有限制性参数,在5.1.2节会详细讨论它

ngx_http_upstream_conf_t

*conf;

//通过resolved可以直接指定上游服务器地址,在5.1.3节会详细讨论它

ngx_http_upstream_resolved_t

*resolved;

/buffer成员存储接收自上游服务器发来的响应内容,由于它会被复用,所以具有下列多种意义:a)在使用process_header方法解析上游响应的包头时,buffer中将会保存完整的响应包头;b)当下面的buffering成员为1,而且此时upstream是向下游转发上游的包体时,buffer没有意义;c)当buffering标志位为0时,buffer缓冲区会被用于反复地接收上游的包体,进而向下游转发;d)当upstream并不用于转发上游包体时,buffer会被用于反复接收上游的包体,HTTP模块实现的input_filter方法需要关注它/

ngx_buf_t

buffer;

//构造发往上游服务器的请求内容

ngx_int_t(create_request)(ngx_http_request_tr);

/收到上游服务器的响应后就会回调process_header方法。如果process_header返回NGX_AGAIN,那么是在告诉upstream还没有收到完整的响应包头,此时,对于本次upstream请求来说,再次接收到上游服务器发来的TCP流时,还会调用process_header方法处理,直到process_header函数返回非NGX_AGAIN值这一阶段才会停止/

ngx_int_t(process_header)(ngx_http_request_tr);

//销毁upstream请求时调用

void(finalize_request)(ngx_http_request_tr,

ngx_int_t rc);

//5个可选的回调方法

ngx_int_t

input_filter_init)(voiddata);

ngx_int_t

input_filter)(voiddata,ssize_t bytes);

ngx_int_t(reinit_request)(ngx_http_request_tr);

void(abort_request)(ngx_http_request_tr);

ngx_int_t(rewrite_redirect)(ngx_http_request_tr,

ngx_table_elt_t*h,size_t prefix);

//是否基于SSL协议访问上游服务器

unsigned

ssl:1;

/在向客户端转发上游服务器的包体时才有用。当buffering为1时,表示使用多个缓冲区以及磁盘文件来转发上游的响应包体。当Nginx与上游间的网速远大于Nginx与下游客户端间的网速时,让Nginx开辟更多的内存甚至使用磁盘文件来缓存上游的响应包体,这是有意义的,它可以减轻上游服务器的并发压力。当buffering为0时,表示只使用上面的这一个buffer缓冲区来向下游转发响应包体/

unsigned

buffering:1;

……

};


上文介绍过,upstream有3种处理上游响应包体的方式,但HTTP模块如何告诉upstream使用哪一种方式处理上游的响应包体呢?当请求的ngx_http_request_t结构体中subrequest_in_memory标志位为1时,将采用第1种方式,即upstream不转发响应包体到下游,由HTTP模块实现的input_filter方法处理包体;当subrequest_in_memory为0时,upstream会转发响应包体。当ngx_http_upstream_conf_t配置结构体中的buffering标志位为1时,将开启更多的内存和磁盘文件用于缓存上游的响应包体,这意味上游网速更快;当buffering为0时,将使用固定大小的缓冲区(就是上面介绍的buffer缓冲区)来转发响应包体。

注意 上述的8个回调方法中,只有create_request、process_header、finalize_request是必须实现的,其余5个回调方法——input_filter_init、input_filter、reinit_request、abort_request、rewrite_redirect是可选的。第12章会详细介绍如何使用这5个可选的回调方法。另外,这8个方法的回调场景见5.2节。