NGINX在配置上游的服务器时,支持域名配置。根据不同的配置,NGINX提供了静态和动态解析两种方式。本文试图从代码层面分析动态dns解析是如何实现的。
a. 静态解析
如上的配置,在NGINX启动运行时,会使用本机在/etc/hosts和/etc/resolve.conf中配置的主机和dns服务器对域名http://private.server1.com.cn和http://private.server2.com.cn进行解析。这个解析过程是通过lib C的函数getaddrinfo进行的同步操作。
如果解析失败,NGINX就不能成功启动。解析得到的ip地址会一直伴随着NGINX运行的整个生命周期。如果在运行期间对应域名的ip地址发生变化,服务就会中断。唯一的解决办法就是重新启动NGINX。
b. 动态解析
开源版的NGINX提供了resolver这种动态的dns解决方案。核心思想是NGINX自身充当dns的客户端进行动态dns解析。
如上配置,当访问服务器的根目录时,会把请求转移到test变量定义的服务器中。而且,这个test变量定义的服务器http://private.server1.com.cn会通过resolver 定义的dns 服务器进行动态解析。
在此配置中,通过resolver得到的解析结果有效期是10秒。有效期过后,再次访问根目录时就会对域名进行重新解析。
需要注意的是,如果proxy_pass后面是一个域名而不是一个变量,那么对域名的解析也是发生在启动解析期间,无法完成动态域名解析的功能。
动态域名解析是通过resolver指令和变量来实现的。指令resolve可以在http范围内全局设定,也可以在某一个server甚至某一个location里面单独设定。
在如上配置中,如果访问服务的根目录和/duplicate/目录,需要反向代理的服务器同为 private.server1.com.cn。但是当访问这两个不同的目录时,使用的dns服务器分别是8.8.8.8和114.114.114.114。而且,通过这两个dns服务器解析的结果不能被针对根目录和/duplicate/目录的访问共享。
指令resolver的配置语法是: resolver 114.114.114.114 8.8.8.8 valid=10s ipv6=off;
这个配置中指定了两个dns 服务器114.114.114.114和8.8.8.8,这两个dns服务器会被依次轮流用而不是按照主从的角色去使用。
如果某一个dns服务器不可达,会尝试另外的dns服务,直到有dns服务器能返回解析结果。无论返回的结果是成功还是失败,它都会被采用。
即使是失败也不会再去尝试另外的dns服务器。 另外,如果因为网络原因导致dns服务器暂时不可达,原来的dns过期缓存也没有办法得到重复使用。
参数valid指定了解析结果的有效期。
参数ipv6用来指明是否接收解析结果中的ipv6地址。对于IPv6的配置,默认是开启的,也就是当域名解析到既有
ipv4又有ipv6时,都会解析到。可以通过ipv6=on|off,来控制ipv6解析
与resolver相关的数据结构如下图所示。主要相关的数据结构有:
ngx_http_request_t , ngx_http_upstream_t ,ngx_http_upstream_resolved_t, ngx_resolver_ctx_t, ngx_resolver_t, ngx_resolver_connection_t, ngx_connection_t, ngx_http_core_loc_conf_t, ngx_resolver_node_t.
从这个数据结构关系图中,我们可以看到一个http请求需要进行动态的dns解析时,主要的数据结构是如何连接起来的。
有了数据结构的大体概念以后,我们下面试着从代码层面分析整个resolver的工作流程和工作原理。
- 配置层面
与动态dns解析功能相关的指令有proxy_pass和resolver两个指令。
a. 在配置阶段,与resolver指令对应的解析函数是ngx_http_core_resolver。函数会生成一个ngx_resolver_t结构并且和location对应的ngx_http_core_loc_conf_t结构连接起来。如上图中的A点所示。
b. 指令proxy_pass对应的解析函数是 ngx_http_proxy_pass 。如果proxy_pass后面的参数是变量,解析函数会把变量存放到ngx_http_proxy_loc_conf_t结构中的proxy_values数组中。在此阶段不会试图对变量进行解析。
如果proxy-pass后面的参数不是变量,则会在配置解析阶段解析后面upstream主机的ip地址并且生成upstream结构并且和ngx_http_proxy_loc_conf_t中的upstream结构连接起来。
与此同时,设置http_proxy模块的处理函数为ngx_http_proxy_handler 。如上图中的B点所示。此函数会在http处理各个模块的回调函数时被调用。
2. 数据层面
动态dns的解析发生在NGINX接收完客户端的请求,然后和上游的upstream服务器进行连接时。
下面我们分析从NGINX打开服务端口接收客户请求到dns域名得到解析并且完成连接这一完整过程。
2.1.
a. 当有客户端发送tcp连接请求时,ngx_epoll_process_events返回listenfd可读事件,调用ngx_event_accept函数接收客户端请求。再调用对应的listening socket的handler函数ngx_http_init_connection函数进入http处理。函数ngx_http_init_connection是在ngx_http_optimize_servers函数中和listenging socket进行连接的。
b. 在函数ngx_http_init_connection中,生成ngx_http_connection_t 结构hc。然后查找对应的服务器地址并且赋值到hc的addr_conf属性中。最后把connection对应的读写的回调函数分别设置为ngx_http_wait_request_handler 和ngx_http_empty_handler 。这样再有数据读入事件发生时,函数ngx_http_wait_request_handler就会得到调用。
c. 函数ngx_http_wait_request_handler会通过ngx_http_create_request创建http request(r)。同时设置读事件回调函数为ngx_http_process_request_line 。当再有数据读入事件发生时,函数ngx_http_process_request_line就会得到调用。与此同时,还会同时调用函数ngx_http_process_request_line来处理已经接受到的请求。
d. 函数ngx_http_process_request_line先是调用ngx_http_read_request_header将请求行读取到缓存中,然后调用ngx_http_parse_request_line解析出请求行信息,最后把读事件的回调函数设置为ngx_http_process_request_headers并且调用ngx_http_process_request_headers处理请求头。
e. 在函数ngx_http_process_request_headers 内部先是调用函数ngx_http_read_request_header 读取请求头,然后调用ngx_http_parse_header_line 函数解析出请求头,接着调用ngx_http_process_request_header 函数对请求头进行必要的验证,最后调用ngx_http_process_request 函数处理请求。
f. 函数ngx_http_process_request中,把event的读写回调函数全部设置为ngx_http_request_handler,把http request(r)的read_event_handler设置为ngx_http_block_reading。同时调用ngx_http_handler函数。而在ngx_http_handler(ngx_http_request_t r) 函数内部调用ngx_http_core_run_phases进行HTTP多阶段处理。函数ngx_http_handler同时会把http request(r)的write_event_handler设置为ngx_http_core_run_phases。
g. 在ngx_http_core_run_phases循环中,迭代所有http模块handler,然后在handler函数中根据请求结构体ngx_http_request_t做出相应的处理。与动态dns解析相关的http proxy模块的回调函数ngx_http_proxy_handler也会在此期间得到调用。
2.2.
如上所述就是一个普通的NGINX http请求的处理流程。到现在为止http的处理逻辑已经到达了各个模块自身的处理中。对应动态的dns对应的处理函数是ngx_http_proxy_handler。下面我们就从这个处理函数开始,分析动态dns解析是如何实现的。
a. 在ngx_http_proxy_handler函数中,首先通过ngx_http_upstream_create为http request(r)生成一个ngx_http_upstream_t结构用来存放所有的upstream服务器信息。然后得到请求对应的ngx_http_proxy_loc_conf_t数据。在这个结构中存放着我们配置的proxy_pass后面变量信息。再通过ngx_http_proxy_eval和ngx_http_script_run等函数获取对应变量的具体数值。再把这些信息存放到http request(r)的upstream中的resolved的host变量中。
b. 函数ngx_http_proxy_handler会继续初始化http request(r)的众多回调函数比如create_request reinit_request process_headerabort_request finalize_request。 同时调用ngx_http_read_client_request_body处理请求的数据体。在调用ngx_http_read_client_request_body时会把函数ngx_http_upstream_init当做参数传入。
c. 在函数ngx_http_read_client_request_body中,处理完请求body以后会调用ngx_http_upstream_init函数对upstream服务器进行初始化。同时把http requst(r)read_event_handler 和write_event_handler进行重新赋值。
d. 函数ngx_http_upstream_init会调用ngx_http_upstream_init_request。
函数ngx_http_upstream_init_request首先检查http request(r)的upstream中的resolved的host是否已经被解析成ip地址。对应我们分析的这种情况,现在的host还是一个域名而不是ip地址。这时,函数ngx_http_upstream_init_request就会调用ngx_resolve_start开始对host进行域名解析。至此真正的动态dns解析逻辑正式被触发。
e. 函数ngx_resolve_start会生成一个ngx_resolver_ctx_t的数据结构ctx 。同时把ctx的resolver设置为对应location结构ngx_http_core_loc_conf_t中的的resolver成员,此成员是在配置解析时生成的。这个ctx的handler会同时被设置成ngx_http_upstream_resolve_handler。最后,这个ctx结构会被关联到http request(r)的upstream中的resolved的ctx变量中。
f. 函数ngx_http_upstream_init_request通过ngx_resolve_start创建完ngx_resolver_ctx_t结构后,会通过ngx_resolve_name调用ngx_resolve_name_locked进行实质的域名解析。
g. 函数ngx_resolve_name_locked逻辑流程如下:
如果该域名在resolver中已存在节点:
i. 如果该节点仍有效,则更新node超时时间,将resolver中的DNS解析结果赋值给ctx,调用ctx的回调ngx_http_upstream_resolve_handler。函数ngx_http_upstream_resolve_handler的逻辑我们下面在解释。
ii. 如果该节点已失效。若因DNS响应还未返回(rn->waiting),则将该cxt挂至rn->waiting;若因响应后失效,则重新发起DNS请求。
如果该域名在resolver中不存在节点:
i. 分配并初始化rn节点,加入resolver红黑树。
ii. 建立DNS请求字符串(rn->query).
iii. 发送DNS请求(ngx_resolver_send_query/ngx_resolver_send_tcp_query/ngx_resolver_send_udp_query)。
iv. 使能ctx->event超时定时器,用于ctx超时。
v. 将rn加入resolver的resend_queue队列,用于DNS的超时重传。如果这是resend_queue中的首个元素,则需要使能r->event重传定时器。该定时器超时时,会遍历resolver的resend_queue,对所有需要重传的node进行判断。
h. 函数ngx_resolver_send_query根据协议配置选用ngx_resolver_send_tcp_query或者ngx_resolver_send_udp_query发送dns请求。我们以ngx_resolver_send_udp_query为例。函数ngx_resolver_send_udp_query会通过ngx_udp_connect创建一个socket并且连接到dns server的服务端口。同时把对应的socket的读事件的回调函数设置为ngx_resolver_udp_read。
i. 当dns响应包到达时,函数ngx_resolver_udp_read通过ngx_resolver_process_response来处理响应数据包。函数ngx_resolver_process_response调用ngx_resolver_process_a来处理域名对应的v4和v6地址。
j. 函数ngx_resolver_process_a会首先根据域名查找rn节点,然后把解析响应的结果存放到rn中。同时copy一份结果赋值给ngx_resolver_ctx_t ctx。此时dns解析成功然后遍历rn->waiting并且调用ctx->handler也就是ngx_http_upstream_resolve_handler函数。同时把rn从resend_queue队列中删除加入name_expire_queue节点超时队列。
k. 函数ngx_http_upstream_resolve_handler首先调用ngx_http_upstream_create_round_robin_peer对http request(r)的upstream服务器进行初始化。然后调用ngx_resolve_name_done执行一些清理工作。最后调用ngx_http_upstream_connect用来和上游服务器进行连接。
至此,整个dns解析过程完成而且解析结果也被成功用来进行上游服务器的连接。
开源版本的NGINX对动态dns解析提供了一定的支持。通过进行源码分析我们会发现这一机制还是有一些局限性。比如,只能通过proxy_pass加变量的方式实现。
很多upstream模块的负载均衡等属性都没法被使用。而且,各个worker process需要独立进行dns解析,而且结果不能共享。
相比于开源版本的原生态NGINX,我们可以采用NGINX Plus, 或者很多第三方模块实现更多更实用的动态dns解析功能。
想要更及时全面地获取NGINX相关的技术干货、互动问答、系列课程、活动资源?
请前往NGINX开源社区:
- 官网:nginx.org.cn
- 微信公众号:https://mp.weixin.qq.com/s/XVE5yvDbmJtpV2alsIFwJg
- 微信群:https://www.nginx.org.cn/static/pc/images/homePage/QR-code.png?v=1621313354
- B站:https://space.bilibili.com/6283