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
    3
    hints.ai_family  = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    getaddrinfo(host, NULL, &hints, &res);

    缺点是该调用会阻塞 worker 进程,故仅在启动时执行 。

配置示例

1
2
3
4
5
6
7
8
9
10
upstream backends {
server backends.example.com:8080;
}

server {
listen 80;
location / {
proxy_pass http://backends;
}
}
  • 以上配置下,NGINX 启动时会解析 backends.example.com 为一组 IP,例如 [10.0.0.11, 10.0.0.10, 10.0.0.12],并在内存中固化,使用轮询算法分发请求。
  • 若后端 IP 变更,需重启或 reload NGINX 才能获取新地址。

1.2、变量驱动的动态解析

  • 当在 proxy_passfastcgi_pass 等指令中使用变量(如 $backend)定义上游域名时,NGINX 不会在启动时固化地址,而会在每次请求阶段,调用已配置的异步 DNS resolver 来解析变量所指定的域名 。
  • 解析过程全程非阻塞,基于 resolver 指令中指定的 DNS 服务器与 TTL(或 valid 覆盖值)进行缓存与重查。

配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
http {
resolver 10.0.0.2 valid=10s; # 指定 DNS 服务器并设置缓存有效期

server {
listen 80;
# 后端主机名赋值给变量
set $backend backends.example.com;
location / {
# 使用变量触发动态解析
proxy_pass http://$backend:8080;
}
}
}
  • 每次请求时,若上次解析已超出 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
http {
# 1. 指定 DNS 服务器与自定义缓存过期时间
resolver 8.8.8.8 8.8.4.4 valid=30s ipv6=off;

# 2. 定义共享内存 zone,用于存储解析结果
upstream backend {
zone backend 64k;
server backend.example.com:80 resolve; # resolve 启用动态重查
}

# 3. 正常使用 upstream
server {
listen 80;
location / {
proxy_pass http://backend;
}
}
}
  • 当首次请求时,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源码解析

此特性使得在 upstreamserver 指令中指定主机名时,可以结合 resolver 指令,在运行时动态异步解析域名。这对于使用 DNS 实现服务发现非常关键。

3.1 入口逻辑

当 Nginx 收到客户端请求并准备建立与 upstream 的连接时,调用链如下:

1
2
3
4
5
6
7
ngx_http_upstream_init()
└── ngx_http_upstream_init_request()
└── ngx_resolve_name()
└── ngx_resolve_name_locked()
└── ngx_resolver_send_query()
└── ngx_resolver_send_tcp_query() or ngx_resolver_send_udp_query() ←★ 这里发起DNS查询

源码位置:

1
src/http/ngx_http_upstream.c

当配置中写入:

1
2
3
4
resolver 8.8.8.8;
upstream backend {
server example.com resolve;
}

Nginx 在运行期遇到 example.com 时不会立即尝试连接,而是先通过 ngx_resolve_name() 调用 resolver 模块发起异步 DNS 查询。

3.2 发送异步 DNS 查询

ngx_resolver_send_udp_query()ngx_resolver_send_tcp_query() 中,根据 DNS 查询类型构建 DNS 请求包,然后发送:

1
2
3
4
5
6
7
8
static ngx_int_t
ngx_resolver_send_udp_query(ngx_resolver_t *r,
ngx_resolver_connection_t *rec,
u_char *query, u_short qlen)
{
n = ngx_send(rec->udp, query, qlen);
...
}

这一过程是非阻塞、异步的:UDP 发送成功后即返回,等待后续事件回调处理响应。

3.3 异步回调处理

DNS 响应包到达时,通过事件机制触发 ngx_event_process_resolver(),最终调用注册的回调函数:

1
ctx->handler = ngx_http_upstream_resolve_handler;

回调函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void
ngx_http_upstream_resolve_handler(ngx_resolver_ctx_t *ctx) {
...
if (ctx->state) {
// 解析失败处理
} else {
// 更新 upstream resolved 地址列表
ur->addrs = ctx->addrs;
ur->naddrs = ctx->naddrs;
}

ngx_http_upstream_connect(r, u); // 继续后续连接流程
}

这一步的关键点是:在解析成功后,将域名解析出来的 IP 地址写入 u->resolved 结构体中,并调用 ngx_http_upstream_connect(),继续执行连接逻辑。

3.4 缓存与失效机制

解析出来的 IP 地址默认会缓存在内存中一段时间(默认是 30 秒,可以通过 resolver valid=10s; 控制):

1
resolver 8.8.8.8 valid=10s;

缓存过期后,下一次请求会触发新的解析流程。

Nginx 的 resolver 模块还支持以下容错能力:

  • 解析失败后自动重试;
  • 使用多个 DNS 服务器(配置多个 resolver);
  • fallback 到 round-robin 策略;
  • upstream 失效探测结合负载均衡策略动态选择 backend;
  • zone 搭配支持共享内存中自动更新 IP。

4、upstream实现动态域名解析

事实上,有了第三节的功能,还没有实现真正的域名动态解析,https://github.com/nginx/nginx/pull/208

存在的问题:

  1. DNS 解析结果未动态更新到 upstream peer 列表:即使配置了 resolverresolve,NGINX 只会在启动时解析一次域名,之后不会根据 DNS 的变化更新 upstream 的服务器列表。
  2. 共享内存中未管理已解析的 IP 缓存:解析结果未被存储在共享内存中,导致多个 worker 进程之间无法共享解析结果,增加了不必要的 DNS 查询负载。
  3. 多 worker 之间未同步更新后的 peer 信息:由于缺乏共享机制,DNS 解析的更新无法在多个 worker 之间同步,可能导致请求被发送到已失效的 IP 地址。

PR #208 解决的问题

该 PR 引入了以下改进,解决了上述问题:

  1. 动态更新 upstream peer 列表:通过将 DNS 解析结果与 upstream 的服务器列表动态关联,使得当域名解析的 IP 地址发生变化时,upstream 的服务器列表也会相应更新。
  2. 在共享内存中管理解析结果:解析结果被存储在共享内存中,允许多个 worker 进程共享解析结果,减少了重复的 DNS 查询,提高了效率。
  3. 多 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,设置如 staleTtlcacheSizenoSynchronisation 等参数,并用 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
2
3
4
5
6
7
8
9
10
11
12
hosts, err = utils.parseHosts(hostsfile)
...
if not hosts.localhost then
hosts.localhost = { ipv4 = "127.0.0.1", ipv6 = "[::1]" }
end
local ttl = 10*365*24*60*60 -- use ttl of 10 years for hostfile entries
for name, address in pairs(hosts) do
...
cacheinsert({{ name = name, address = address.ipv4, type = _M.TYPE_A, ... }})
...
cacheinsert({{ name = name, address = address.ipv6, type = _M.TYPE_AAAA, ... }})
end

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_ipv4check_ipv6,无需 DNS 查询。
  • DNS 查询流程
    如果缓存未命中,则根据配置的类型顺序(如 A、AAAA、SRV、CNAME)和 search 域名策略,循环调用 lookup 进行 DNS 查询。
    lookup 会先查缓存,未命中时调用 syncQueryindividualQuery 发起真实 DNS 网络请求。
    查询到 CNAME 记录时,会自动递归解析其指向的主机名,直到获得最终的 IP 结果。
1
2
3
4
5
6
7
8
local records = cacheShortLookup(qname, qtype)
if records then ... return records ... end
...
for try_name, try_type in search_iter(qname, qtype) do
...
records, err, try_list = lookup(try_name, opts, dnsCacheOnly, try_list, force_no_sync)
...
end

5.3、结果缓存

对于通过网络查询获得的 DNS 解析结果,lua-resty-dns-client 也会将其缓存在本地内存中。缓存的有效期由 DNS 响应中的 TTL 或客户端配置的 validTtl 参数决定。对于查询失败或空记录,也会按照 badTtlemptyTtl 进行缓存,避免频繁的无效请求。缓存机制采用 LRU(最近最少使用)策略,确保高频访问的记录能长期保留,而低频或过期的记录会被自动淘汰。这样设计既提升了解析性能,又能保证缓存的实时性和准确性。

DNS 查询结果的缓存机制体现在如下几个方面:

  • 缓存写入
    所有通过网络查询获得的 DNS 结果,都会通过 cacheinsert 写入 LRU 缓存。缓存的 TTL 由 DNS 响应、配置的 validTtlbadTtlemptyTtl 等参数决定。
  • 缓存结构
    缓存项包含 ttlexpireexpiredtouch 等元数据,便于判断是否过期和缓存淘汰。
  • 缓存命中与刷新
    查询时如果命中缓存但已过期,会立即返回 stale 数据,同时后台异步刷新(asyncQuery),保证高可用和实时性。
  • 错误与空记录缓存
    查询失败(如 NXDOMAIN)或空记录,也会按配置 TTL 缓存,避免频繁无效请求。
1
2
3
4
5
6
7
8
9
local function cacheinsert(entry, qname, qtype)
...
dnscache:set(key, entry, lru_ttl)
end

if entry.expired then
add_status_to_try_list(try_list, "stale")
asyncQuery(qname, r_opts, try_list)
end

openresty-域名解析
https://zjfans.github.io/2025/05/26/nginx域名解析原理/
作者
张三疯
发布于
2025年5月26日
许可协议