c-ares: This is an asynchronous resolver library. It is intended for applications which need to perform DNS queries without blocking, or need to perform multiple DNS queries in parallel.
void ChannelWrap::Setup() {
...
/* We do the call to ares_init_option for caller. */
r = ares_init_options(&channel_,
&options,
ARES_OPT_FLAGS | ARES_OPT_SOCK_STATE_CB);
...
}
/* Choose the server to send the query to. If rotation is enabled, keep track
* of the next server we want to use. */
query->server = channel->last_server;
if (channel->rotate == 1)
channel->last_server = (channel->last_server + 1) % channel->nservers;
可以看到,域名被正常解析了。下面修改 /etc/resolv.conf 内容,将 nameserver 改为一个无法访问的 IP(前面三个被注释的是原 DNS server):
#
# macOS Notice
#
# This file is not consulted for DNS hostname resolution, address
# resolution, or the DNS query routing mechanism used by most
# processes on this system.
#
# To view the DNS configuration used by this system, use:
# scutil --dns
#
# SEE ALSO
# dns-sd(1), scutil(8)
#
# This file is automatically generated.
#
#nameserver 172.18.1.166
#nameserver 192.168.43.27
#nameserver 192.168.1.1
nameserver 192.168.2.2
引言
近期在做一个 DNS 服务器切换的演练中发现我们在 NodeJS 中使用的 axios 以及默认的
dns.lookup
方法存在一些问题,会导致切换过程中的响应耗时从 ~80ms 上升至 ~3min(具体参见node中请求超时的一些坑)。计划的部分解决方案是使用 lookup-dns-cache 来替换默认的dns.lookup
方法。需要解答的疑问
如果使用 lookup-dns-cache 来替换默认的
dns.lookup
,需要确认以下三个问题:dns.lookup
一样,在 Linux 上也使用 resolv.conf 配置?下面基于 NodeJS v12.16.3 分别对这三个问题进行分析。
TL;DR
问题一:查询与缓存实现细节
lookup-dns-cache 整体代码量很少,DNS 查询相关功能都委托给了
dns.resolve*
方法。与dns.lookup
不同,dns.resolve*
并不使用getaddrinfo
,并且是异步实现。lookup-dns-cache 主要是在
dns.resolve*
之上提供了两个优化点:dns.resolve*
,其余放置在回调队列;1 - 避免额外的并行请求
该处主要是用过
TasksManager
来实现。实现很简单,发起 DNS 查询时,用 Map 存储当前正在进行查询的 hostname,查询结束后,从 Map 中删除。具体调用则在 Lookup.js 的_innerResolve
中:其中的 key 是通过
${hostname}_${ipVersion}
拼接而成(ipVersion:ipv4/ipv4)。可以看到,如果在TasksManager
实例中找到 task,则只添加回调,否则就发起一个查询,即创建一个ResolveTask
实例。2 - DNS 缓存
lookup-dns-cache 通过为 resolve* 方法设置
ttl: true
来让 DNS 查询结果返回 TTL 值。对于查询回来的结果会在当前时间基础上加上 TTL 来作为过期时间:当进行 DNS 查询前,会先查缓存,如果存在则直接返回。而在 AddressCache 中进行缓存查询时,如果判断当前时间超过过期时间,则不再返回缓存结果:
这里可能会存在一个问题:如果查询的域名名称无限,由于缓存中仅判断是否过期,并无过期清理操作,因此过期缓存可能会一直占用内存而不释放。当然,由于普通业务项目中,域名查询的种类有限,并且基本会一直重复,因此并不会暴露该问题。
问题二:是否使用 resolv.conf 配置
1 -
dns.resolve*
等方法源码分析NodeJS 部分
在
lib/dns.js
最后可以发现,dns 模块导出的相关 resolve 方法是通过这行绑定上去的。
而在
lib/internal/dns/utils.js
中会发现,getDefaultResolver
方法会返回一个 Resolver 实例。在这个模块里并没有各种 resolve 方法,而具体其上的 resolve 方法则还是在lib/dns.js
中实现的:而这里关于 DNS 查询调用的核心的方法就是
this._handle[bindingName](req, toASCII(name))
。如果我们再回到lib/internal/dns/utils.js
这个定义 Resolver 类的地方就会发现:this._handle
是ChannelWrap
的一个实例。ChannelWrap
来自于对 c-ares 的内部绑定 —— cares_wrap.cc。按照官方文档的说法,c-ares 支持 resolv.conf。但为了保险起见,具体情况如何需要继续向下进一步确认。
拉到 cares_wrap.cc 的最后就可以看到针对 NodeJS 层的一些绑定代码,这里截取和
dns.resolve
相关部分:以上代码主要包括两个部分,在 C++ 层创建了 JS 的
ChannelWrap
类,同时设置相应的原型方法。因此,在 JS 层new ChannelWrap()
基本上的调用链条为ChannelWrap::New
-->ChannelWrap::ChannelWrap
-->ChannelWrap::Setup
。其中 Setup 阶段调用了 c-ares 的初始化配置方法:注意这里的第三个参数,就是该方法的 opmask,会决定使用哪些 options。
c-ares 部分
在 c-ares 中具体配置(包括 dns server)的初始化有四个步骤,从前到后分别是:
在第一种通过 option 结构体传参中,ares 会通过
options->nservers
来获取 DNS 服务器配置。但同时,需要在操作掩码中设置ARES_OPT_SERVERS
。而在 NodeJS 中值设置了ARES_OPT_FLAGS | ARES_OPT_SOCK_STATE_CB
,因此不会设置 nservers。此外,init_by_options 中还会设置 resolvconf_path 的值,该值所指向的地址就是系统 resolv.conf 的地址:同样的,从上面节选的代码可以看出,NodeJS 调用中 optmask 并不包含
ARES_OPT_RESOLVCONF
,因此channel->resolvconf_path
为空,而此处也会影响后续的init_by_resolv_conf
方法。从
ares_init_options
代码的流程控制来看,正常情况下,设置完传参和环境变量后,最终会走到init_by_resolv_conf
中。init_by_resolv_conf
方法主要是用来解析和获取 nameservers,其中包含比较多平台相关的条件编译,我们可以关注两个条件分支:#elif defined(CARES_USE_LIBRESOLV)
CARES_USE_LIBRESOLV
这个宏表示是否使用 resolv 这个库。看起来似乎是在苹果系统下会启用。一旦使用这个库,条件分支里就会有两个重要的函数调用 ——
res_ninit
和res_getservers
。从手册中可以看出,
res_ninit
会读取 resolv.conf,因此在该分支中会使用 resolv.conf 文件。
再看另一条分支。最后条件分支(看起来应该是 Linux)的处理,其中会优先读取 resolv.conf 的配置地址,不存在则取预定义的宏变量:
PATH_RESOLV_CONF
则定义在ares_private.h
中:channel->nservers
的设置也是通过读取文件中的 nameserver 配置项来添加的:设置完成之后,当需要进行 DNS 查询时,最终会调用 ares_send.c 中的
ares_send
方法来发送查询请求。其中就会使用channel->nservers
中的值来作为本地 DNS 查询服务器,其中 last_server 默认为 0:综合上面的分析可知,在 NodeJS(v12.16.3)中,调用
dns.resolve*
相关方法,底层会调用 c-ares 这个库。根据 c-ares 的实现来分析,其最终会读取resolv.conf
的 nameserver 设置本地 DNS,并用其进行查询。P.S. c-ares 也依赖 glibc 的 resolv。
2 - 实际验证
经过上面的分析之后,可以再简单进行一下实际验证。下面是一段调用
dns.resolve
(其他 resolve 方法同理)的代码:实验一:
环境:CentOS Linux release 7.4.1708
运行输出:
用 strace 看下它的调用链:
内容比较多,下图只截取其中一部分,可以看到打开并读取了 resolv.conf。
strace 输出(第8行的 open 调用):
实验二:
环境:macOS 10.15.3
运行输出:
可以看到,域名被正常解析了。下面修改
/etc/resolv.conf
内容,将 nameserver 改为一个无法访问的 IP(前面三个被注释的是原 DNS server):此时再执行,会触发超时错误:
结论
通过源码和测试,可以确定 dns.resolve 相关方法,在 Linux 仍然会读取 resolv.conf 配置来设置本地 DNS 服务器。
问题三:关于 DNS 查询的 timeout
在 c-ares 部分有提到两个编译分支,在最后一个 else 中,并不会对 timeout 的值进行处理,因此会落到最后的默认赋值上(5s)
DEFAULT_TIMEOUT
定义在这,为 5s而对于走到 CARES_USE_LIBRESOLV 分支的代码,则因为调用了
res_ninit
,可以在__res_state
结构体中取到 retrans 值,该值会被用作 timeout 值:如果以上分析没有问题的话,在生产环境中应该是属于上一种情况,但由于 NodeJS 层没有暴露对应设置超时的入口,所以,如果替换为 lookup-dns-cache,则无法控制 timeout 的时间。
综上
参考资料
P.S. resolv 中设置 timeout(retrans)值目测是在这个地方