openresty-域名解析
1、NGINX 支持的域名解析方式
- 静态解析:启动/重载时仅调用系统解析库(
getaddrinfo()
),将域名映射为 IP 列表后缓存,后续不再更新。 - 变量驱动的动态解析:在
proxy_pass
等指令中使用$variable
,结合resolver
,使 NGINX 在每次请求阶段异步更新域名解析结果。 - **异步
resolver
+resolve
**:开源版 NGINX ≥ 1.27.3,可在upstream、server
中添加resolve
参数,并配合resolver valid=…
指令定期重查 DNS。
1.1、静态解析
NGINX 在启动或执行配置重载时,调用标准 C 库函数(如
getaddrinfo()
/gethostbyname()
),根据操作系统的/etc/resolv.conf
配置一次性解析域名,生成一组 IP 列表,并缓存于内存中。解析只发生在启动/重载时,不会再次触发,无视 DNS 记录的 TTL,也不会主动更新。
底层实现示例,在
ngx_inet_resolve_host
中使用:1
2
3hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
getaddrinfo(host, NULL, &hints, &res);缺点是该调用会阻塞 worker 进程,故仅在启动时执行 。
配置示例
1 |
|
- 以上配置下,NGINX 启动时会解析
backends.example.com
为一组 IP,例如[10.0.0.11, 10.0.0.10, 10.0.0.12]
,并在内存中固化,使用轮询算法分发请求。 - 若后端 IP 变更,需重启或
reload
NGINX 才能获取新地址。
1.2、变量驱动的动态解析
- 当在
proxy_pass
、fastcgi_pass
等指令中使用变量(如$backend
)定义上游域名时,NGINX 不会在启动时固化地址,而会在每次请求阶段,调用已配置的异步 DNSresolver
来解析变量所指定的域名 。 - 解析过程全程非阻塞,基于
resolver
指令中指定的 DNS 服务器与 TTL(或valid
覆盖值)进行缓存与重查。
配置示例
1 |
|
- 每次请求时,若上次解析已超出
10s
,NGINX 会异步查询10.0.0.2
,获取最新 IP 列表并更新连接目标。 - 该方式无需重启即可感知 DNS 变更,但会在高并发下增加解析开销。
1.3、异步 resolver + resolve
- 自 NGINX 开源版 1.27.3 起,官方正式合入 “Upstream: re‑resolvable servers” 功能,允许在
upstream … server
指令后添加resolve
参数。 - 配合同级作用域的
resolver address … valid=…
,NGINX 以异步方式定期(依据 DNS TTL 或valid
覆盖值)对带resolve
的域名重新发起查询,更新后端服务器列表 nginx.orgNGINX社区博客。 - 解析由 NGINX 自身的事件驱动模块完成,无任何阻塞。
配置示例
1 |
|
当首次请求时,NGINX 背后异步解析
backend.example.com
并将结果缓存在zone
中。随后每隔 30s(或实际 TTL)触发重查,自动增删 IP;若查询失败,仍保留旧列表,保证可用性。
1.4、对比
方式 | 首次解析时机 | 重查触发 | 阻塞风险 | 配置复杂度 | 适用场景 |
---|---|---|---|---|---|
静态解析 | 启动/重载 | 无 | 中 (启动时) | 最低 | 后端 IP 稳定、不频繁变更 |
变量动态解析 | 每次请求前 | TTL/valid 到期 |
无 | 低 | 需快速感知变更,但能容忍开销 |
异步 resolver + resolve |
首次请求触发 | TTL/valid 到期 |
无 | 中 | 追求配置简洁、低开销的动态解析 |
- 静态解析:简洁但不灵活,适合 IP 长期不变场景。
- 变量驱动:适合需要频繁变更的场景,但并发高时可能带来解析压力。
resolve
参数:在新版开源 NGINX 中最推荐的方案,既可自动更新,又无额外请求开销,配置优雅。
2、openresty的问题
对于openresty而言,在lua层面连接对端使用的都是connect,但是connect时不支持静态解析,只支持resolver。详情可见此issue:https://github.com/openresty/lua-nginx-module/issues/2385
总的来说:
- 静态解析使用的是
getaddrinfo()
,是 阻塞式系统调用; - OpenResty 的执行模型依赖于 Nginx 的事件驱动模型(epoll);
- 一旦在 worker 阻塞了,就会影响所有请求的响应;
- 所以
connect
中只允许使用 非阻塞的异步 DNS 查询(通过resolver
实现);
所以最后并没有在openresty原生支持hosts,但是可以使用官方的 lua-resty-dns
https://github.com/openresty/lua-resty-dns,这里面支持了hosts的解析。
3、nginx支持resolver + resolve源码解析
此特性使得在 upstream
的 server
指令中指定主机名时,可以结合 resolver
指令,在运行时动态异步解析域名。这对于使用 DNS 实现服务发现非常关键。
3.1 入口逻辑
当 Nginx 收到客户端请求并准备建立与 upstream 的连接时,调用链如下:
1 |
|
源码位置:
1 |
|
当配置中写入:
1 |
|
Nginx 在运行期遇到 example.com
时不会立即尝试连接,而是先通过 ngx_resolve_name()
调用 resolver 模块发起异步 DNS 查询。
3.2 发送异步 DNS 查询
在 ngx_resolver_send_udp_query()
或 ngx_resolver_send_tcp_query()
中,根据 DNS 查询类型构建 DNS 请求包,然后发送:
1 |
|
这一过程是非阻塞、异步的:UDP 发送成功后即返回,等待后续事件回调处理响应。
3.3 异步回调处理
DNS 响应包到达时,通过事件机制触发 ngx_event_process_resolver()
,最终调用注册的回调函数:
1 |
|
回调函数实现:
1 |
|
这一步的关键点是:在解析成功后,将域名解析出来的 IP 地址写入 u->resolved
结构体中,并调用 ngx_http_upstream_connect()
,继续执行连接逻辑。
3.4 缓存与失效机制
解析出来的 IP 地址默认会缓存在内存中一段时间(默认是 30 秒,可以通过 resolver valid=10s;
控制):
1 |
|
缓存过期后,下一次请求会触发新的解析流程。
Nginx 的 resolver 模块还支持以下容错能力:
- 解析失败后自动重试;
- 使用多个 DNS 服务器(配置多个 resolver);
- fallback 到 round-robin 策略;
- upstream 失效探测结合负载均衡策略动态选择 backend;
- 与
zone
搭配支持共享内存中自动更新 IP。
4、upstream实现动态域名解析
事实上,有了第三节的功能,还没有实现真正的域名动态解析,https://github.com/nginx/nginx/pull/208
存在的问题:
- DNS 解析结果未动态更新到 upstream peer 列表:即使配置了
resolver
和resolve
,NGINX 只会在启动时解析一次域名,之后不会根据 DNS 的变化更新 upstream 的服务器列表。 - 共享内存中未管理已解析的 IP 缓存:解析结果未被存储在共享内存中,导致多个 worker 进程之间无法共享解析结果,增加了不必要的 DNS 查询负载。
- 多 worker 之间未同步更新后的 peer 信息:由于缺乏共享机制,DNS 解析的更新无法在多个 worker 之间同步,可能导致请求被发送到已失效的 IP 地址。
PR #208 解决的问题
该 PR 引入了以下改进,解决了上述问题:
- 动态更新 upstream peer 列表:通过将 DNS 解析结果与 upstream 的服务器列表动态关联,使得当域名解析的 IP 地址发生变化时,upstream 的服务器列表也会相应更新。
- 在共享内存中管理解析结果:解析结果被存储在共享内存中,允许多个 worker 进程共享解析结果,减少了重复的 DNS 查询,提高了效率。
- 多 worker 之间同步更新:通过共享内存机制,确保所有 worker 进程都能及时获取最新的解析结果,避免了请求被发送到已失效的 IP 地址的问题。
4.1、源码分析
o_o ….
5、lua-resty-dns-client
lua-resty-dns-client
是一个基于 OpenResty 的 Lua DNS 客户端库,它在执行 DNS 查询前,会优先检查本地的 hosts 缓存,并仅在未命中时才发送真正的网络请求。为了兼顾性能与配置更新的灵活性,它将 hosts 文件的解析结果缓存在内存中,通过可配置的 TTL 控制重载频率。主要做了3个动作
初始化阶段(init):首次载入并解析 /etc/hosts
,将所有条目存入内存缓存;
解析阶段(resolve):每次调用解析时,优先从 hosts 缓存中查找,未命中再发起网络 DNS 请求;
结果缓存:对网络 DNS 查询到的结果也进行缓存,以便后续调用可以直接复用。
5.1 初始化阶段:加载并缓存 hosts
在初始化阶段,lua-resty-dns-client
会通过 _M.init(options)
方法完成 DNS 客户端的配置和缓存初始化。此时,客户端会读取并解析指定的 hosts 文件(如 /etc/hosts
),将所有主机名及其对应的 IP 地址(包括 IPv4 和 IPv6)存入内存缓存。对于 localhost
这样的特殊主机名,会强制写入标准的本地回环地址,确保解析的健壮性。缓存的 TTL(生存时间)通常设置为较长时间(如 10 年),以保证 hosts 文件中的静态解析长期有效。这样,后续的 DNS 查询可以直接从内存中获取 hosts 记录,无需每次都重新读取和解析文件,提高了性能。
在 [client.lua](vscode-file://vscode-app/d:/vscode/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html) 的 _M.init
函数中,初始化阶段主要做了如下事情:
- 参数处理与缓存初始化
首先读取传入的options
,设置如staleTtl
、cacheSize
、noSynchronisation
等参数,并用lrucache.new(cacheSize)
初始化 LRU 缓存(dnscache
),清空已定义主机表(defined_hosts
)。 - hosts 文件解析与缓存
通过options.hosts
或默认路径utils.DEFAULT_HOSTS
获取 hosts 文件路径,然后用utils.parseHosts(hostsfile)
解析 hosts 文件,得到所有主机名和 IP 的映射表。
特别地,如果 hosts 文件中没有localhost
,则强制写入127.0.0.1
和[::1]
,保证本地回环地址始终可用。 - 写入缓存
遍历 hosts 解析结果,将每个主机名的 IPv4/IPv6 地址分别以 A/AAAA 记录的形式写入缓存(cacheinsert
),TTL 设置为 10 年。
同时,调用cachesetsuccess
标记该主机名的成功解析类型,便于后续优先查找。
1 |
|
5.2、解析阶段(resolve)
每当有 DNS 解析请求到来时,客户端会优先在本地缓存(包括 hosts 记录和之前的 DNS 查询结果)中查找目标主机名。如果缓存命中,则直接返回结果,避免了不必要的网络请求。如果缓存未命中,或者缓存中的记录已经过期,则会根据配置的 nameserver 信息,发起真实的 DNS 网络查询。解析流程还支持多种 DNS 记录类型(如 A、AAAA、SRV、CNAME 等),并能根据配置的优先级顺序依次尝试解析。对于 CNAME 等别名记录,客户端会自动递归解析其指向的真实主机名,直到获得最终的 IP 地址。
DNS 解析的主入口是 resolve
函数,源码实现如下:
- 优先查 hosts 缓存
首先调用cacheShortLookup(qname, qtype)
检查 hosts 及短名缓存,如果命中且未过期,直接返回结果。如果命中但已过期,会标记为 stale 并继续后续流程。 - IP 地址直接返回
如果qname
本身就是 IP 地址(通过hostnameType
判断),则直接调用check_ipv4
或check_ipv6
,无需 DNS 查询。 - DNS 查询流程
如果缓存未命中,则根据配置的类型顺序(如 A、AAAA、SRV、CNAME)和 search 域名策略,循环调用lookup
进行 DNS 查询。lookup
会先查缓存,未命中时调用syncQuery
或individualQuery
发起真实 DNS 网络请求。
查询到 CNAME 记录时,会自动递归解析其指向的主机名,直到获得最终的 IP 结果。
1 |
|
5.3、结果缓存
对于通过网络查询获得的 DNS 解析结果,lua-resty-dns-client
也会将其缓存在本地内存中。缓存的有效期由 DNS 响应中的 TTL 或客户端配置的 validTtl
参数决定。对于查询失败或空记录,也会按照 badTtl
或 emptyTtl
进行缓存,避免频繁的无效请求。缓存机制采用 LRU(最近最少使用)策略,确保高频访问的记录能长期保留,而低频或过期的记录会被自动淘汰。这样设计既提升了解析性能,又能保证缓存的实时性和准确性。
DNS 查询结果的缓存机制体现在如下几个方面:
- 缓存写入
所有通过网络查询获得的 DNS 结果,都会通过cacheinsert
写入 LRU 缓存。缓存的 TTL 由 DNS 响应、配置的validTtl
、badTtl
、emptyTtl
等参数决定。 - 缓存结构
缓存项包含ttl
、expire
、expired
、touch
等元数据,便于判断是否过期和缓存淘汰。 - 缓存命中与刷新
查询时如果命中缓存但已过期,会立即返回 stale 数据,同时后台异步刷新(asyncQuery
),保证高可用和实时性。 - 错误与空记录缓存
查询失败(如 NXDOMAIN)或空记录,也会按配置 TTL 缓存,避免频繁无效请求。
1 |
|