点赞
评论
收藏
分享
举报
NGINX动态DNS解析原理及源码分析
发表于2020-11-18 19:37

浏览 5.2k

文章标签

概述

NGINX在配置上游的服务器时,支持域名配置。根据不同的配置,NGINX提供了静态和动态解析两种方式。本文试图从代码层面分析动态dns解析是如何实现的。

a.静态解析

http {

upstream test {

                            server   private.server1.com.cn;

                            server   private.server2.com.cn;

}

server {

                            listen 80;

                            location / {

                                proxy_pass test;

                            }

}

}

如上的配置,在NGINX启动运行时,会使用本机在/etc/hosts/etc/resolve.conf中配置的主机和dns服务器对域名private.server1.com.cn和private.server2.com.cn进行解析。这个解析过程是通过lib C的函数getaddrinfo进行的同步操作。如果解析失败,NGINX就不能成功启动。解析得到的ip地址会一直伴随着NGINX运行的整个生命周期。如果在运行期间对应域名的ip地址发生变化,服务就会中断。唯一的解决办法就是重新启动NGINX

b.动态解析

开源版的NGINX提供了resolver这种动态的dns解决方案。核心思想是NGINX自身充当dns的客户端进行动态dns解析。

http {

server {

                            listen 80;

                            resolver 8.8.8.8 valid=10s;

                            set   $test   private.server1.com.cn;

                            location / {

                                proxy_pass http://$test;

                            }

                    }

            }

如上配置,当访问服务器的根目录时,会把请求转移到test变量定义的服务器中。而且,这个test变量定义的服务器private.server1.com.cn会通过resolver 定义的dns 服务器进行动态解析。在此配置中,通过resolver得到的解析结果有效期是10秒。有效期过后,再次访问根目录时就会对域名进行重新解析。

需要注意的是,如果proxy_pass后面是一个域名而不是一个变量,那么对域名的解析也是发生在启动解析期间,无法完成动态域名解析的功能。

配置参数

动态域名解析是通过resolver指令和变量来实现的。指令resolve可以在http范围内全局设定,也可以在某一个server甚至某一个location里面单独设定。

http {

server {

        listen 80;

        set   $test  private.server1.com.cn;

        location / {

                resolver 8.8.8.8 valid=10s;

                proxy_pass http://$test;

        }

        location /duplicate/ {

            resolver 114.114.114.114 valid=10s;

            proxy_pass http://$test;

        }

}

}

在如上配置中,如果访问服务的根目录和/duplicate/目录,需要反向代理的服务器同为 private.server1.com.cn。但是当访问这两个不同的目录时,使用的dns服务器分别是8.8.8.8114.114.114.114。而且,通过这两个dns服务器解析的结果不能被针对根目录和/duplicate/目录的访问共享。

指令resolver的配置语法是: resolver 114.114.114.114 8.8.8.8 valid=10s ipv6=off;

这个配置中指定了两个dns 服务器114.114.114.1148.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的工作流程和工作原理。

1.配置层面

与动态dns解析功能相关的指令有proxy_passresolver两个指令。

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域名得到解析并且完成连接这一完整过程。

a.当有客户端发送tcp连接请求时,ngx_epoll_process_events返回listenfd可读事件,调用ngx_event_accept函数接收客户端请求。再调用对应的listening sockethandler函数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。然后查找对应的服务器地址并且赋值到hcaddr_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也会在此期间得到调用。

如上所述就是一个普通的NGINX http请求的处理流程。到现在为止http的处理逻辑已经到达了各个模块自身的处理中。对应动态的dns对应的处理函数是ngx_http_proxy_handler。下面我们就从这个处理函数开始,分析动态dns解析是如何实现的。

a.ngx_http_proxy_handler函数中,首先通过ngx_http_upstream_createhttp request(r)生成一个ngx_http_upstream_t结构用来存放所有的upstream服务器信息。然后得到请求对应的ngx_http_proxy_loc_conf_t数据。在这个结构中存放着我们配置的proxy_pass后面变量信息。再通过ngx_http_proxy_evalngx_http_script_run等函数获取对应变量的具体数值。再把这些信息存放到http request(r)upstream中的resolvedhost变量中。

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中的resolvedhost是否已经被解析成ip地址。对应我们分析的这种情况,现在的host还是一个域名而不是ip地址。这时,函数ngx_http_upstream_init_request就会调用ngx_resolve_start开始对host进行域名解析。至此真正的动态dns解析逻辑正式被触发。

e.函数ngx_resolve_start会生成一个ngx_resolver_ctx_t的数据结构ctx 。同时把ctxresolver设置为对应location结构ngx_http_core_loc_conf_t中的的resolver成员,此成员是在配置解析时生成的。这个ctxhandler会同时被设置成ngx_http_upstream_resolve_handler。最后,这个ctx结构会被关联到http request(r)upstream中的resolvedctx变量中。

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加入resolverresend_queue队列,用于DNS的超时重传。如果这是resend_queue中的首个元素,则需要使能r->event重传定时器。该定时器超时时,会遍历resolverresend_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来处理域名对应的v4v6地址。

j.函数ngx_resolver_process_a会首先根据域名查找rn节点,然后把解析响应的结果存放到rn中。同时copy一份结果赋值给ngx_resolver_ctx_t ctx。此时dns解析成功然后遍历rn->waiting并且调用ctx->handler也就是ngx_http_upstream_resolve_handler函数。同时把rnresend_queue队列中删除加入name_expire_queue节点超时队列。

k.函数ngx_http_upstream_resolve_handler首先调用ngx_http_upstream_create_round_robin_peerhttp requestr)的upstream服务器进行初始化。然后调用ngx_resolve_name_done执行一些清理工作。最后调用ngx_http_upstream_connect用来和上游服务器进行连接。

至此,整个dns解析过程完成而且解析结果也被成功用来进行上游服务器的连接。

五结语

开源版本的NGINX对动态dns解析提供了一定的支持。通过进行源码分析我们会发现这一机制还是有一些局限性。比如,只能通过proxy_pass加变量的方式实现。很多upstream模块的负载均衡等属性都没法被使用。而且,各个worker process需要独立进行dns解析,而且结果不能共享。

相比于开源版本的原生态NGINX,我们可以采用NGINX Plus, 或者很多第三方模块实现更多更实用的动态dns解析功能。


已修改于2023-03-09 02:03
本作品系原创
创作不易,留下一份鼓励
皮皮鲁

暂无个人介绍

关注



写下您的评论
发表评论
全部评论(1)

按点赞数排序

按时间排序

有没有关于DNS SRV记录解析过程的代码分析?

赞同

0

回复举报

发表于2023-12-04 10:32



回复Alive
回复
关于作者
皮皮鲁
这家伙很懒还未留下介绍~
85
文章
2
问答
41
粉丝
相关文章