1.相关配置
upstream模块的典型应用是反向代理,这里就以ngx_http_proxy_module模块为例。假定我们有如下这样的实例环境,客户端对服务器80端口的请求都被Nginx Proxy Server转发到另外两个真实的Nginx Web Server实例上进行处理(下图是实验环境,Web Server和Proxy Server都只是Nginx进程,并且运行在同一台服务器):
那么,Nginx Proxy Server的核心配置多半是这样:
1 |
|
上面的proxy_buffering off;配置是为了禁用nginx反向代理的缓存功能,保证客户端的每次请求都被转发到后端真实服务器,以便我们每次跟踪分析的nginx执行流程更加简单且完整。而另外两个配置指令upstream和proxy_pass在此处显得更为重要,其中upstream配置指令的回调处理函数为ngx_http_upstream(),该函数除了申请内存、设置初始值等之外,最主要的动作就是切换配置上下文并调用ngx_conf_parse()函数继续进行配置解析:
2.源码解析
1 | Filename : ngx_http_upstream.c |
进入到upstream配置块内,最主要的配置指令也就是server,其对应的处理函数为ngx_http_upstream_server(),对于每一个后端真实服务器,除了其uri地址外,还有诸如down、weight、max_fails、fail_timeout、backup这样的可选参数,所有这些都需要ngx_http_upstream_server()函数来处理。
在ngx_http_upstream.c的第4173行下个断点,我们可以看到这里给出示例的解析结果:
另外一个重要配置指令proxy_pass主要出现在location配置上下文中,而其对应的处理函数为ngx_http_proxy_pass(),抹去该函数内的众多细节,我们重点关注两个赋值语句:
1 | Filename : ngx_http_proxy_module.c |
上面片段代码里的第一个赋值语句给当前location的http处理设置回调函数,而第二个赋值语句则是查找(没有找到则会创建,比如如果配置文件中upstream指令出现在proxy_pass指令的后面)其对应的upstream配置,我们这里就一个名为load_balance的upstream,所以找到的配置就是它了:
前面曾提到,Nginx将对客户端的http请求处理分为多个阶段,而其中有个NGX_HTTP_FIND_CONFIG_PHASE阶段主要就是做配置查找处理,如果当前请求location设置了upstream,即回调函数指针clcf->handler不为空,则表示对该location的请求需要后端真实服务器来处理:
1 | Filename : ngx_http_core_module.c |
在其它有location更新的情况下,比如redirect重定向location或named命名location或if条件location等,此时也会调用ngx_http_update_location_config()函数进行location配置更新。我们知道upstream模块的主要功能是产生响应数据,虽然这些响应数据来自后端真实服务器,所以在NGX_HTTP_CONTENT_PHASE 阶段的checker函数ngx_http_core_content_phase()内,我们可以看到在r->content_handler不为空的情况下会优先对r->content_handler函数指针进行回调:
1 | Filename : ngx_http_core_module.c |
如果r->content_handler不为空,即存在upstream,那么进入处理,注意第1397行直接返回NGX_OK,也即不再调用挂在该阶段的其它模块回调函数,所以说upstream模块的优先级是最高的。根据前面的回调赋值,调用r->content_handler()指针函数,实质上就是执行函数ngx_http_proxy_handler(),直到这里,我们才真正走进upstream代理模块的处理逻辑里。
3.回调函数
对于任何一个Upstream模块而言,最核心的实现主要是7个回调函数,upstream代理模块自然也不例外,它实现并注册了这7个回调函数:
回调指针 | 函数功能 | upstream代理模块 |
---|---|---|
create_request | 根据nginx与后端服务器通信协议(比如HTTP、Memcache),将客户端的HTTP请求信息转换为对应的发送到后端服务器的真实请求 | ngx_http_proxy_create_request 由于nginx与后端服务器通信协议也为HTTP,所以直接拷贝客户端的请求头、请求体(如果有)到变量r->upstream->request_bufs内。 |
process_header | 根据nginx与后端服务器通信协议,将后端服务器返回的头部信息转换为对客户端响应的HTTP响应头。 | ngx_http_proxy_process_status_line 此时后端服务器返回的头部信息已经保存在变量r->upstream->buffer内,将这串字符串解析为HTTP响应头存储到变量r->upstream->headers_in内。 |
input_filter_init | 根据前面获得的后端服务器返回的头部信息,为进一步处理后端服务器将返回的响应体做初始准备工作。 | ngx_http_proxy_input_filter_init 根据已解析的后端服务器返回的头部信息,设置需进一步处理的后端服务器将返回的响应体的长度,该值保存在变量r->upstream->length内。 |
input_filter | 正式处理后端服务器返回的响应体 | ngx_http_proxy_non_buffered_copy_filter 本次收到的响应体数据长度为bytes,数据长度存储在r->upstream->buffer内,把它加入到r->upstream->out_bufs响应数据链等待发送给客户端。 |
finalize_request | 正常结束与后端服务器的交互,比如剩余待取数据长度为0或读到EOF等,之后就会调用该函数。由于nginx会自动完成与后端服务器交互的清理工作,所以该函数一般仅做下日志,标识响应正常结束。 | ngx_http_proxy_finalize_request 记录一条日志,标识正常结束与后端服务器的交互,然后函数返回。 |
reinit_request | 对交互重新初始化,比如当nginx发现一台后端服务器出错无法正常完成处理,需要尝试请求另一台后端服务器时就会调用该函数。 | ngx_http_proxy_reinit_request设置初始值,设置回调指针,处理比较简单。 |
abort_request | 异常结束与后端服务器的交互后就会调用该函数。大部分情况下,该函数仅做下日志,标识响应异常结束。 | ngx_http_proxy_abort_request记录一条日志,标识异常结束与后端服务器的交互,然后函数返回。 |
上表格中前面5个函数执行的先后次序如下图所示,由于在Client/Proxy/Server之间,一次请求/响应数据可以发送多次(下图中只画出一次就发送完毕的情况),所以下图中对应的函数也可能被执行多次,不过一般情况下,这5个函数执行的先后次序就是这样了。
4.总结
这些回调函数如何夹杂到nginx中被调用并不需要完全搞清楚,要写一个upstream模块,我们只要实现上面提到的这7个函数即可,当然,可以看到最主要的也就是create_request、process_header和input_filter这三个回调,它们实现从HTTP协议到Nginx与后端服务器之间交互协议的来回转换,使得在用户看来,他访问的就是一台功能完整的Web服务器,而也许事实上,显示在他面前的数据来自Memcache或别的什么服务器。
参考文献: