在 Nginx 作为反向代理或七层负载均衡的场景中,许多配置会将 proxy_pass 的目标写成带域名的地址,例如 http://api.ipipp.com。此时 Nginx 在建立上游连接之前,必须先将域名解析为 IP 地址,这一过程就是 DNS 解析。如果解析本身耗时较长,用户感知到的请求延迟就会明显上升,但 Nginx 默认的访问日志并不会把这段时间单独体现出来。因此,想要排查这类延迟问题,就需要理解 Nginx 处理上游域名时的内部阶段,并利用它提供的变量与扩展能力,把解析耗时从整体请求时间中剥离并记录下来。

一、DNS解析的触发机制与可用的计时变量
Nginx 只有在配置里出现域名形式的上游地址,并且使用了 resolver 指令时,才会在运行时进行动态 DNS 解析。如果上游地址直接写成 IP,或者仅依赖系统静态 /etc/hosts 文件进行本地解析,那么 Nginx 层面根本不存在真正的 DNS 查询过程,自然也就无法从 Nginx 变量中获取解析耗时。因此,记录解析时间的前提是先确保配置中已经启用了 resolver,并且 proxy_pass 使用的是域名。
Nginx 核心模块提供了 $upstream_addr、$upstream_connect_time、$upstream_response_time 等变量,但它们分别表示上游地址、建立 TCP 连接耗时和接收响应耗时,并没有一个现成的 $dns_time 变量来直接输出 DNS 解析时间。要拿到解析耗时,需要借助组合变量和计算差值的方式间接实现。常见做法包括使用第三方模块 nginx-module-vts 查看细分指标,或者在生产环境使用 OpenResty 嵌入 Lua 脚本,通过 ngx.now() 在解析前后打点求差。
下面是一段在 OpenResty 中利用 Lua 记录 DNS 解析耗时的简化示例。示例中先记录开始时间,再通过 ngx.socket.tcp() 触发一次域名解析与连接,最后将耗时写入自定义变量。这里的耗时包含了 TCP 连接建立时间,仅用于说明打点思路;实际项目中可以结合 cosocket 的 DNS 解析接口进一步细化。
local function measure_dns_lookup(host, port)
local sock = ngx.socket.tcp()
local start_time = ngx.now()
-- connect 会触发域名解析与 TCP 连接,此处以整体耗时近似表示解析阶段耗时
local ok, err = sock:connect(host, port)
local cost = ngx.now() - start_time
sock:close()
return ok, err, cost
end
local host = "api.ipipp.com"
local port = 80
local connected, err, dns_cost = measure_dns_lookup(host, port)
if not connected then
ngx.log(ngx.ERR, "connect failed: ", err)
dns_cost = 0
end
ngx.var.dns_cost = string.format("%.3f", dns_cost)
二、通过 log_format 输出解析耗时到访问日志
拿到解析耗时变量后,下一步就是将它写入访问日志。Nginx 的 log_format 指令允许组合任意变量,包括通过 set 指令定义的自定义变量。我们可以在 http 块中先声明一个变量,例如 $dns_cost,并为其设置默认值,再由 Lua 脚本或 map 指令赋值。随后在 log_format 中追加该字段,即可让每行访问日志都携带解析耗时。
配置时需要注意变量的作用域。自定义变量应当在 server 或 location 之外使用 set 初始化,否则 Lua 赋值时可能因为变量未创建而报错。另外,如果上游启用了 keepalive 长连接,连接复用后后续请求不会再触发 DNS 解析,此时 $dns_cost 应当保持为 0 或结合 $connection 复用标识来判断。下面给出一份典型的 nginx.conf 片段,展示格式定义与变量初始化。
http {
log_format main "$remote_addr - $time_local "$request" "
"$status $body_bytes_sent "
"dns=$dns_cost";
server {
set $dns_cost "0";
location / {
access_by_lua_block {
local start = ngx.now()
-- 实际项目中应在此调用解析函数,计算得到耗时
local cost = 0.012
ngx.var.dns_cost = string.format("%.3f", cost)
}
proxy_pass http://api.ipipp.com;
access_log /var/log/nginx/access.log main;
}
}
}
上述配置中,log_format 里的 dns=$dns_cost 字段会在每条日志末尾追加解析耗时。若某个请求没有走代理域名,或者命中了连接复用,则变量保持默认值 0。通过这种方式,运维人员可以使用 awk 或 ELK 等工具筛选 dns 值偏大的记录,快速锁定解析异常。同时,建议把 resolver 的超时时间设置得小一些,避免解析卡死时拖垮整体请求处理。
三、原生 Nginx 无 Lua 时的替代记录方案
并非所有环境都能安装 OpenResty 或 Lua 扩展。对于纯 Nginx 用户,虽然无法直接在应用层打点拿精确耗时,但仍有一些替补手段。一种方式是利用 error_log 的 debug 级别打开 resolver 相关日志,观察解析过程;这种方式适合开发环境或临时调试,不适合长期运行在生产环境。另一种思路是使用 split_clients 与 map 做间接标记,把经历过重新解析或解析失败的请求单独打标,再在访问日志中用标志位反映出来。这种方式虽然拿不到精确毫秒数,但可以快速识别哪些请求产生了解析行为。
此外,商业版 Nginx Plus 内置了更为丰富的 $upstream 指标,其中包含解析阶段的细分数据。开源版则可以考虑在系统层使用 tcpdump 抓取 53 端口的 DNS 流量,离线计算查询耗时,再与 access_log 中的 $request_time 对齐分析。这种方法
这种方法虽然能获得完整的 DNS 查询序列,但需要额外的抓包权限和分析成本,适合在排查疑难解析问题时使用。实际生产环境中,建议优先考虑 OpenResty 或 Nginx Plus 内置指标,因为它们在应用层直接采集,侵入性小且易于接入现有监控。对于纯开源 Nginx,只能退而求其次,使用 debug 日志或 map 标记,再配合系统层抓包进行离线关联。
无论采用哪种方案,都应注意以下几点:第一,DNS 解析耗时指标不宜单独作为告警阈值,因为解析时间受网络抖动、缓存命中率、上游 DNS 服务器负载等多因素影响,需要结合 $request_time 和 $upstream_connect_time 一起判断。第二,resolver 配置中的 valid 参数与连接池复用会直接影响解析频率,调大 valid 可以降低解析次数,但可能在上游 IP 变更后出现短时间连接失败;调小 valid 能更快感知变更,但会增加解析开销。第三,如果使用 Lua 打点记录,注意变量作用域和连接复用的逻辑,避免把上一次解析耗时错误地记到当前请求上。
最后,建议把解析耗时作为访问日志的一个补充维度,而不是核心监控指标。通过 log_format 中预留 dns 字段,在生产中静默收集,当发现某类域名解析频繁超时或耗时异常时,再结合 resolver 日志与抓包进一步定位。这样既能控制日志体量,也能在问题发生时快速回溯。
至此,从 Lua 计时到纯 Nginx 的间接方案,再到系统层抓包对齐,我们已经覆盖了记录 Nginx DNS 解析耗时的主要路径。希望这些方法能帮助你在实际运维中更精准地把握 DNS 解析这一关键环节。
NginxDNS解析access_log修改时间:2026-08-13 12:57:36