在Nginx作为反向代理或七层负载时,很多配置会把proxy_pass写成带域名的地址,例如 http://api.ippipp.com。此时Nginx需要在建立连接前把域名变成IP,这一步就是DNS解析。如果解析慢,用户看到的请求延迟就会升高,但默认的访问日志并不会告诉你时间花在了这里。要解决这个问题,我们必须理解Nginx处理上游域名时的内部阶段,并利用它提供的变量把解析耗时单独记下来。

一、Nginx解析DNS的触发机制与可用变量
Nginx只有在配置里出现域名形式的上游地址,并且使用了resolver指令时,才会在运行时做动态解析。如果直接写IP,或者依赖系统静态/etc/hosts,根本不存在Nginx层面的DNS查询,自然也无从记录解析时间。在带resolver的场景中,Nginx核心模块提供了$upstream_addr、$upstream_connect_time等变量,但并没有一个现成的$dns_time。我们需要通过组合变量和计算差值的方式间接得到。
具体做法是利用Nginx在处理请求时的内部计时点。虽然官方变量列表未直接暴露DNS耗时,但可以在log_format中借助第三方模块如nginx-module-vts,或使用Lua配合ngx.now()打点。更轻量的原生思路是:在resolver配置中打开日志,然后用error_log的debug级别观察,但这不适合生产。生产环境常用OpenResty或嵌入Lua,在访问阶段前记录ngx.now(),解析后再次记录,两者相减写入自定义变量。
下面是一段在OpenResty中利用Lua打点DNS耗时的简化示例。它先记下进入时间,再用ngx.socket.tcp()手动做一次域名解析,从而精确拿到耗时。注意这只是演示原理,真实代理不必手动建连,可结合cosocket的dns解析接口。
local sock = ngx.socket.tcp()
local start = ngx.now()
-- 手动触发一次DNS解析,拿到所有IP
local ips, err = sock:getreusedtimes() -- 仅为示意,实际应使用resolver查询
local ok, resolved = pcall(function()
return sock:connect("api.ipipp.com", 80)
end)
local cost = ngx.now() - start
ngx.var.dns_cost = tostring(cost)
sock:close()
二、通过log_format把解析时间写入访问日志
拿到解析耗时变量后,下一步是把它输出到access_log。Nginx的log_format指令允许组合任意变量,包括用set指令定义的自定义变量。我们可以在http块中先声明一个变量,例如$dns_cost,默认值为0,再由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()
-- 假设此处调用了解析函数得到cost
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的timeout设小一些,避免解析卡死拖垮整体。
三、原生Nginx无Lua时的替代记录方案
并非所有环境都能上OpenResty。纯Nginx用户若想记录DNS解析时间,可以借助error_log加resolver调试,或者用split_clients与map做间接标记。例如,把解析失败的请求单独打标,再在日志中用标志位反映。虽然拿不到精确毫秒,但能知道哪些请求经历了重解析。
另一个办法是使用Nginx Plus或商业模块,它们内置了更丰富的$upstream指标,其中包含解析阶段细分。开源版则可以考虑在系统层用tcpdump抓53端口,离线算耗时,再和access_log的$request_time对齐。这种方法不侵入Nginx,适合短期排查。下面的命令示例展示如何抓取DNS包并简单统计。
tcpdump -i any udp port 53 -tttt -nn 2> /tmp/dns.log &
# 请求发生后停止抓包,用awk筛选耗时
awk '/A?/ {print $1, $NF}' /tmp/dns.log无论采用哪种方案,核心原则都是把DNS阶段从整体延迟中剥离。只有明确记录了解析时间,才能在下一次接口抖动时,第一时间判断是该扩容解析缓存,还是该把域名换成IP直连。对于大规模微服务架构,建议在网关层统一埋点,避免每个业务Nginx重复造轮子。
NginxDNS解析access_log修改时间:2026-08-13 12:57:36