Nginx域名解析流程,源码分析

2020-06-15 15:16:31 浏览数 (1)

nginx在做正向代理、反向代理的时候,或upstream使用域名的时候,要做频繁的域名解析,为了更快的响应,nginx有一套自己的域名解析过程

今天详细分析一下nginx的域名解析过程

在nginx中,只有两个配置指令关于域名解析,就是resolver,和resolver_timeout,resolver_timeout不多说,就是域名解析超时时间,这里具体就说resolver指令

简单配置了个nginx反向代理,如图,然后为了便于调试,只起了一个工作进程

先用strace看了下系统调用,在connect调用中已经解析到了baidu.com的IP地址

这和预想的不一样,原本以为是每次调用都会去查一次系统DNS,但是这里却看到没有查系统DNS,难道没有调用系统dns吗?自有一套?下面验证猜想

首先我把/etc/resolv.conf的nameserver改成个不可访问的,然后启动nginx

发现无法正常启动,报错解析不到域名的地址,那应该还是调用系统dns了,接着用strace看一下启动过程

前面部分就不截图了,基本就是调用各种系统组件,初始化的过程,到这里开始读取default.conf配置文件,然后开始解析proxy_pass后面的域名地址

可以看到过程如下:

首先查询nscd

接着查询/etc/host.conf

然后查询/etc/resolve.conf

接着通过nsswith进行解析,利用resolve文件中提供的nameserver地址

尝试发送解析多次后,解析失败

最后调用wirte输出错误

通过以上strace追踪发现,nginx是在启动的时候就调用系统dns进行域名解析操作,下面结合源码看下nginx启动的时候如何初始化域名解析

从上面分析,是在解析配置文件的时候才去做域名解析操作的,所以根据nginx初始化流程判断,直接查看nginx的http_core_module中可以看到对resolver的声明

然后在core/ngx_resolver.c中查看ngx_resolver_t的结构体

首先是typedef定义了别名

找到ngx_resolver_s查看结构体变量声明如下:

可以看到声明了dns查询,以及红黑树缓存dns数据,以及IPv6的处理

nginx在初始化的时候,通过core/ngx_resolver.c中的ngx_resolver_create来初始化上面的结构体,如果在配置文件中有设置resolver指令,则在启动的时候通过http/ngx_http_core_resolver进行调用

接着看下ngx_resolver_create做了什么

太长了,不贴代码了,这里解释下过程,有兴趣可以去看源码

这里主要就是配置解析阶段:

  • 设置cleanup的handler(ngx_resolver_cleanup)
  • 初始化保存域名节点信息的红黑树(r->name_rbtree)
  • 初始化重传和过期队列(r->name_resend_queue、r->name_expire_queue)
  • 设置超时时间的handler(ngx_resolver_resend_handler)
  • 解析dns server的ip并设置到地址数据(r->connections)
  • 解析参数(valid,ipv6)等

请求构造阶段:

通过ngx_resolver_start开始做解析,判断如果是IP地址,则temp->quick=1,直接返回IP地址

我们知道,通常只有在proxy_pass和upstream中进行域名配置,所以接着看下proxy_pass指令源码和upstream指令源码

代码比较长,在http/ngx_http_proxy_module.c中,在ngx_http_proxy_passs中,首先判断upstream中的域名配置和proxy_length,接着判断proxy_pass后面的url中最后是否是"/",如果是,自动跳转,接着判断url中变量数量,根据数量判断是http还是https协议,接着还是通过调用ngx_http_upstream_add,将域名添加到upstream解析队列中,所以所有的调用解析,还是从upstream中调用,接着看upstream

接着刚才ngx_http_upstream_add,proxy_pass中的url传入之后,开始通过ngx_http_upstream_create创建upstream,接着在upstream初始化中声明resolver并调用ngx_resolve_start解析域名

整个过程总结如下:

proxy_pass http://$host;

ngx_resolver_ctx_t ctx 每次域名解析都会生成这个结构体, 直接malloc,未使用r->pool.ctx = ngx_resolve_start()

• 如果$host是ip地址, 直接设置ctx->quick = 1, 表示后续逻辑不需要走dns解析逻辑.

• 如果r->udp_connections 不存在, 返回NGX_NO_RESOLVER, 最终请求返回502.初始化ctx参数

• ctx->type = NGX_RESOLVE_A;

• ctx->handler = ngx_http_upstream_resolve_handler;

• ctx->timeout = clcf->resolver_timeout;

• ngx_resolve_name(ctx)

• 如果 ctx->quick == 1, 直接调用 ctx->handler, 跳过dns解析.

• 否则调用 ngx_resolve_name_locked, 执行dns解析.

• ngx_resolve_name_locked(r, ctx)

1 调用ngx_resolver_lookup_name查找域名节点rn是否在r->name_rbtree缓存节点中, 存在进入(2), 否者进入 (5)

2 判断rn->valid是否过期,没有过期进入(3), 否者进入(4).

3 如果存在 rn->naddrs, 是A记录节点, 循环调用rn->waiting链表上的 ctx->handler, 然后函数返回OK; 如果不存在 rn->naddrs, 表示是CNAME记录节点, 那么递归调用ngx_resolve_name_locked,进入步骤 (1).

4 rn->valid已经过期, 如果存在rn->waiting, 表示已经触发了新的dns请求, 只需要把ctx挂在到链表上, 函数返回NGX_AGAIN. 如果不存在rn->waiting,表示这是域名失效之后的第一个请求, 需要清空上一次dns请求申请的内存, 进入 (6)

5 不存在rn, 表示第一次域名请求, 初始化rn节点, 并加入 r->name_rbtree红黑树.

6 创建域名查询请求 ngx_resolver_create_name_query

7 发送域名查询请求 ngx_resolver_send_query, 并设置dns查询的读事件 uc->connection->read->handler = ngx_resolver_read_response

8 挂载超时事件 ngx_add_timer(ctx->event, ctx->timeout) ctx->event->handler->ngx_resolver_timeout_handler

9 函数结束, 返回NGX_AGAIN.

过程比较复杂,总的来说,当proxy_pass后面是连接的时候,即使不定义upstream,nginx也会隐式的,将proxy_pass后面的url创建一个upstream,由upstream模块进行调用resolver来做域名的解析

解析是在初始化的时候就进行的,首先会根据服务器DNS配置或host配置进行一个缓存队列,队列中缓存的IP及域名对是有过期时间的,过期后清理,重新进行解析

我通过正常的配置,curl请求,反向代理到百度正常,接着我修改我的hosts文件,将百度代理到一个随意的内网地址,再次请求,仍然可以请求到,所以可以证明上面的缓存时间,所以当你更新DNS后,为了让nginx更快更新,需要重启nginx

resolver对于IPv6的配置,默认是开启的,也就是当域名解析到既有ipv4又有ipv6时,都会解析到,官方提供ipv6=on|off,来控制ipv6解析

0 人点赞