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解析