Linux 中的 DNS 工作原理(二):各级 DNS 缓存
大家好!我是大聪明-PLUS!
在第一部分中,我们讨论了 Linux 中名称解析过程的工作原理——从初始请求getaddrinfo()到 IP 地址。但是,如果每个请求都需要重新发起 DNS 请求,效率会很低,并且会给系统和网络带来沉重的负载。因此,我们需要使用缓存。
DNS 缓存随处可见——在 glibc、systemd-resolved、浏览器,甚至 Go 应用程序中。缓存有助于提升性能,但也会带来额外的调试复杂性。例如,您更改了 DNS 记录,但服务器仍然连接到旧的 IP 地址。或者,Dig 显示正确的地址,但 curl 仍然连接到过时的地址。
在本文中,我们将研究系统本身、应用程序和编程语言、容器和代理的各种缓存级别,以及如何监视和刷新它们。
什么是 DNS 缓存以及为什么需要它?
DNS 缓存并非单一的存储单元,而是一个由系统不同层级的缓存机制组成的多层系统。每个 DNS 请求都是一次漫长的网络调用,因此许多组件会尝试将结果存储在本地。
TTL(生存时间)是缓存条目的生存期。它在 DNS 响应中指定,决定了存储的 IP 地址可以使用多长时间。TTL 通常以秒为单位。TTL 过期后,必须刷新数据。
因此,缓存可以让你:
- 提高处理重复 DNS 请求的速度 - 减少服务器和网络基础设施的负载 - 确保在暂时失去连接的情况下具有容错能力 - 减少应用程序运行的延迟
但缓存也有一个缺点:当基础设施发生变化时,过时的数据可能会导致服务变得不可用。
Linux 中的 DNS 缓存到底在哪里?缓存级别
如果使用状态过滤,Linux 内核可以通过 netfilter/conntrack 子系统间接“缓存”DNS 流量。尽管 UDP 是无连接协议,但内核会跟踪属于同一请求-响应块的数据包的状态(例如,从 [src_ip:src_port] 到 [8.8.8.8:53] 的请求,以及从 [8.8.8.8:53] 到 [src_ip:src_port] 的预期响应),并临时存储此信息(默认情况下,大约 30 秒)。这对于 NAT 和类似 的规则是必要的iptables -m conntrack --ctstate ESTABLISHED,RELATED。
您可以使用以下命令检查当前记录:
conntrack -L -p udp --dport 53该列表将显示在超时到期之前被视为有效的活动 DNS 会话。在这些记录的有效期内,对相同地址的重复 DNS 查询可以在现有状态记录中处理。这不是完整的 DNS 响应缓存,但 conntrack 可能会影响网络层行为,尤其是在诊断 NAT 或防火墙问题时。
要从连接跟踪表中删除条目,可以使用以下命令:
conntrack -D -p udp --dport 53系统级
1. systemd-resolved
使用 systemd 的现代发行版通常使用它systemd-resolved作为主 DNS 客户端,它:
拥有自己的 DNS 查询缓存,包括肯定和(可选)否定响应;
支持拆分 DNS(不同域使用不同的 DNS 服务器),通过 systemd-networkd 或 NetworkManager 配置;
支持 DNS-over-TLS (DoT),包括后备服务器和 DNSSEC;
通过 D-Bus(org.freedesktop.resolve1)提供 API,允许您通过 resolvectl 获取状态、更改设置和管理缓存。
2. nscd(名称服务缓存守护进程)
如果 nscd 正在运行,它会缓存 NSS 调用的结果,包括 DNS。其设置位于 /etc/nscd.conf 中
enable-cache hosts yes
positive-time-to-live hosts 600
negative-time-to-live hosts 20重要提示:nscd 已弃用,不建议在新系统上使用。在基于 Debian 的发行版中,它通常被 systemd-resolved 取代,但在基于 RHEL 的发行版中仍然可用。
3. 流行的本地缓存解析器
dnsmasq是本地缓存的热门选择:
设置缓存
unbound是一个更强大的 DNS 递归器。
缓存管理灵活:
缓存大小由 msg-cache-size 参数(负责响应)和 rrset-cache-size(负责资源记录)设置:
msg-cache-size: 50m
rrset-cache-size: 100m可以使用 cache-min-ttl 和 cache-max-ttl 指令来限制缓存响应的 TTL。
bind是一个广泛使用的 DNS 服务器,具有递归解析和缓存支持。缓存管理和监控通过 rndc(远程名称守护进程控制)命令执行。
在 named.conf 配置文件中,您可以设置缓存的 TTL 并控制其行为:
options {max-cache-ttl 86400; max-ncache-ttl 3600;
};应用程序和编程语言级别
glibc
虽然 glibc 官方并未考虑实现完整的 DNS 缓存(与 systemd-resolved、dnsmasq 或 nscd 等守护进程不同),但其解析器实现确实包含几种充当非常简单且有限的缓存或查询优化的机制。
首先,使用 NSS(名称服务切换),它可以配置为与 nscd、systemd-resolved 或其他缓存组件配合使用。例如,在 nsswitch.conf 文件中使用以下指令:
hosts: files resolved myhostname其中 solved 也可以替换为 nscd。虽然 nscd 是一个独立的守护进程,但它在大多数发行版中都随 glibc 一起提供,并与其紧密集成。
当 nscd 运行并配置完成后(通常默认启用 passwd、group 和 hosts 命令),glibc 库(getaddrinfo、gethostbyname)会首先访问它。nscd 会缓存响应,直到其 TTL 过期。在这种情况下,缓存发生在 glibc 库本身之外,但 glibc 会透明地使用此缓存,对应用程序而言。
glibc DNS 解析器实现(位于 resolv/res_query.c 及其他位置)包含一些优化,例如套接字重用以及在单次调用中忽略重复查询的 TTL。这些优化可以被视为单次解析器调用上下文中的一种微缓存。这些优化不会在不同的 getaddrinfo 调用之间或不同的进程之间持久化结果,并且仅在单个系统调用中有效。
NSS 模块(尤其是文件)会缓存静态文件的数据。例如,nss-files(也适用于 /etc/hosts)本质上会在首次读取这些文件后将其内容“缓存”在进程内存中(直到进程重启或调用导致 NSS 缓存失效)。这并非纯粹的 DNS 缓存,但它确实会影响涉及 DNS 的名称解析过程。
值得一提的是,旧版 gethostbyname 函数有一个原始的内部缓存(固定大小、小巧、进程间不共享、忽略 TTL)。这些信息在使用旧版函数时可能会有所帮助。
Java
Java 支持内置 DNS 缓存,并可灵活设置记录有效期。
工作特点
如果不使用安全管理器(从 Java 11 开始,这是默认行为):
- 肯定响应将缓存 30 秒(最多,即使 DNS 服务器指定了更大的 TTL);
- 否定响应(NXDOMAIN)— 10 秒。
使用安全管理器:
- 肯定响应被无限期缓存(记录过时的风险);
- 否定响应不会被缓存。
控制
这些设置在 JVM 属性中指定。成功请求的 TTL(秒):
networkaddress.cache.ttl=60失败请求的 TTL(NXDOMAIN):
networkaddress.cache.negative.ttl=10可能的含义:
0 — 禁用缓存;
-1 — 无限期缓存(仅适用于肯定响应)。
定制方法
1. JVM参数:
java -Dnetworkaddress.cache.ttl=60 -jar app.jar2. 在代码中(对于 JVM 全局):
Security.setProperty("networkaddress.cache.ttl", "60");Go
Go 使用两种解析模型:
- 通过 cgo 调用系统getaddrinfo()(包括 NSS)。通过系统设置(/etc/nsswitch.conf、/etc/resolv.conf)进行操作;
- 纯 Go 解析器 - 实现自己的 DNS 客户端,绕过系统库。
选择解析器:
GODEBUG=netdns=go
GODEBUG=netdns=cgo 使用纯 Go 解析器时,Go 会在进程内实现一个内存 DNS 缓存。该缓存会根据响应的 TTL 值存储成功(A、AAAA、CNAME)和失败(NXDOMAIN)的响应。该缓存具有固定大小(实现了 LRU 启发式算法),存储在进程内存中,并在该进程内的各个 goroutine 之间共享,但不会在不同的进程或应用程序实例之间共享。
Python
默认情况下,socket 模块和大多数标准库(包括请求、urllib3)使用 getaddrinfo() 系统调用,并且没有内置的 DNS 缓存。
• 请求→使用 urllib3,它将 DNS 解析委托给系统库(socket.getaddrinfo()),而不进行缓存。
• aiohttp 默认通过 getaddrinfo() 工作,但可以配置为使用 aiodns(通过 libcares 的异步 DNS),其中可以使用内置缓存。
• dnspython 是一个独立的 DNS 客户端,可直接执行 DNS 查询。从 2.0 版本开始,它支持显式缓存启用:
import dns.resolver
resolver = dns.resolver.Resolver()
resolver.cache = dns.resolver.Cache()因此,具体行为取决于具体的库。标准套接字及其周围的大多数包装器(包括请求)均未实现缓存,而是依赖于系统解析器。
容器和编排
Docker
容器使用主机的 DNS 服务器(使用主机网络模式时)或内置解析器。
在 Docker 中,默认地址是 127.0.0.11,这是 dockerd 内置的 DNS,它使用 libnetwork。缓存可以位于主机上,也可以位于容器本身中。
Podman
Podman 可能会根据版本的不同使用不同的方案。
如果使用 CNI 网络后端(已弃用但受支持,这是 Podman ≤3.x 中的默认设置),则使用 dnsname 插件,该插件负责 Podman 虚拟网络中的 DNS 操作。
DNS 缓存发生在容器级别。如果容器使用 systemd-resolved、dnsmasq 或其他本地缓存解析器,则缓存会在那里使用。dnsname 本身不会缓存响应。
如果您使用 Netavark 网络后端(Podman ≥ 4.0 中的默认后端),Netavark 将运行 aardvark-dns,这是一个轻量级 DNS 服务器:
- 支持 A 记录(名称 → IP)和 PTR 记录(反向 DNS);
- 支持外部 DNS:容器使用 aardvark-dns 作为解析器,将外部请求(例如 ya.ru)转发到系统 DNS 服务器,但不存储响应。
与 CNI 一样,缓存只能在侧面进行:系统解析器或容器内的本地缓存。
Kubernetes
CoreDNS 通常负责 Kubernetes 中的 DNS。重要的是要了解其默认行为对于内部和外部请求是不同的:
Kubernetes 内部记录(例如 my-svc.my-namespace.svc.cluster.local)由 Kubernetes 插件处理。它们始终会缓存在 CoreDNS 内存中,缓存时间由记录的 TTL 字段指定(通常,无头服务为 5 秒,其他服务为 30 秒)。此缓存功能始终处于启用状态,无需其他插件。
外部请求(例如 google.com)由 forward 插件处理,该插件会将请求发送到 CoreDNS 的 /etc/resolv.conf 中列出的解析器。如果配置中没有 cache 插件,外部请求的响应将不会被缓存!来自 pod 的每个请求都会发送到上游解析器,从而造成负载并增加延迟。
要启用缓存,您需要将缓存插件添加到 Corefile:
.:53 {errorshealthcache 30 . forward . 8.8.8.8 1.1.1.1 kubernetes cluster.local in-addr.arpa ip6.arpa {pods insecurefallthrough in-addr.arpa ip6.arpa}prometheus :9153reload
}以 Nginx 和 HAProxy 为例的代理服务器
Nginx 和 HAProxy 对 DNS 缓存有不同的方法,在代理动态后端时考虑这一点很重要。
Nginx默认缓存 DNS 记录的 TTL,并且在它们过期之前不会重新查询它们 - 这在服务 IP 地址在重启时发生变化的容器环境中会产生问题。
proxy_pass解决方案:使用( )中的变量set $backend "http://service.local"; proxy_pass $backend;和指令resolver 8.8.8.8 valid=30s强制每 30 秒刷新一次。
HAProxyresolvers通过参数部分hold valid 10s和配置 DNS 解析更加灵活resolve-prefer ipv4。
一个关键特性是能够自动更新后端 IP 地址,而无需通过 [resolver] 重新加载配置default-server check resolvers dns_resolvers。然而,HAProxy 会主动缓存已解析的地址,并且可能会卡在多个 A 记录的单个 IP 上。在负载均衡至关重要或 IP 地址动态变化的情况下,这可能是不可取的。为了控制此行为,应在 resolvers 块中正确配置关键参数。
配置示例:
resolvers mynameserversnameserver ns1 192.168.2.10:53nameserver ns2 192.168.3.10:53...hold valid 10s hold obsolete 0s resolve_retries 3 timeout retry 1s timeout resolve 2s 一些监控技巧。监控日志中是否存在类似DNS resolution failed(HAProxy) 和upstream timed out(Nginx) 的条目——这些条目表明存在 DNS 缓存问题。对于 Nginx,您还可以error_log /var/log/nginx/dns_debug.log debug;向上下文添加解析器。
需要注意的是:并非所有级别都允许强制刷新缓存。例如,musl 库(在 Alpine Linux 中)使用极简的内置解析器,没有单独的缓存,但具有固定的超时和重试次数。它没有 DNS 缓存,每次调用都会发出新的 DNS 请求。
缓存的特殊情况
负缓存(NXDOMAIN 缓存)
即使 DNS 服务器没有响应(例如,域名不存在),也可以缓存。这称为负缓存,在 RFC 2308 中有描述。NXDOMAIN 的保留期通常由区域 SOA 记录的 TTL 或解析器设置决定:
- Unbound:默认使用 SOA.MINIMUM 或 val-ttl-negative(默认最长 15 分钟);
- Dnsmasq:保留否定响应 10 秒;
- Systemd-resolved:根本不缓存 NXDOMAIN(生存时间 = 0);
- Bind 使用来自 SOA 的 TTL(最小 TTL 字段)和 max-negative-cache-ttl 设置来缓存 NXDOMAIN。
此行为对于调试至关重要:如果最近添加了一个域,但它仍然返回 NXDOMAIN,则可能是由于负缓存造成的。
重要提示:即使 DNS 记录已创建,负向缓存仍可能存在。在某些情况下,只有重启解析器或其缓存才能重置负向缓存——调试时应考虑到这一点。
过时的 DNS(旧版响应)
一些 DNS 解析器支持“服务过期”模式(RFC 8767)——如果上游服务器无法提供新的响应,则从缓存返回陈旧的响应。
这种行为对于提高弹性很有用:在短期网络故障期间,系统会继续使用最新的已知数据运行。
可能发生这种情况的情况:
- 上游服务器不可用或未及时响应;
- 当前响应已过时(TTL 已过期),但旧副本仍保留在缓存中;
- 明确启用“serve-expired”模式(例如,在 unbound、dnsmasq、BIND 中)。
这种行为的缺点是存在使用无效或已修改记录的潜在风险。
诊断:如何检查 DNS 响应来自哪里
要了解 DNS 响应是否被缓存以及缓存级别,您可以使用不同的工具比较查询结果。以下是示例命令:
1. 通过系统解析器(libc)请求:
getent hosts example.com此方法使用 nsswitch.conf 配置,并且可以使用 systemd-resolved、dnsmasq 或其他本地 DNS 服务(如果已配置)。
2. 直接向本地systemd-resolved(环回)查询:
dig @127.0.0.53 example.com允许您直接访问 systemd-resolved,绕过 nsswitch.conf 和 libc。这将帮助您检查答案是否在其自身的缓存中。您也可以使用以下命令来实现此目的:
resolvectl query example.com3. 直接向外部 DNS 服务器(例如 Google DNS)请求:
dig @8.8.8.8 example.com保证绕过所有本地缓存层。此请求可用于与本地响应进行比较。
4.查看响应中的TTL:
dig +nocmd +noall +answer example.com仅显示响应部分 (ANSWER) 及其剩余 TTL。有助于评估缓存新鲜度。
如果对同一域名的响应因使用的命令(例如 [ getent, ]dig @127.0.0.53或dig @8.8.8.8[ ])而异,则可能表明某种程度上存在缓存。IP 地址或 TTL 值的差异尤其具有启发性:它们有助于我们了解在哪个阶段返回了过时或缓存的数据。通过此类比较,我们可以精确定位缓存的来源,并确定导致意外响应的系统组件。
监控网络流量
要分析 DNS 查询,可以使用以下命令:
tcpdump -i any udp port 53 -n -X -w dns_dump.pcap-n - 禁用 IP 到名称解析(减少噪音)。
-X — 以十六进制和 ASCII 显示数据包(对于 DNS 来说比 -A 更方便)。
-w — 将转储保存到文件(可以在 Wireshark 中打开)。
示例输出:
13:37:45.123 IP 10.0.0.1.4321 > 8.8.8.8.53: 12345+ A? google.com. (28)
13:37:45.456 IP 8.8.8.8.53 > 10.0.0.1.4321: 12345 1/0/0 A 142.250.185.174 (44)A?google.com 是针对 google.com 域的“A”查询。
142.250.185.174 是带有 IP 地址的响应。
12345 - 请求 ID(用于匹配问题和答案)。
您还可以使用 dig 实用程序,它会发起请求并立即拦截它,提供大量相关信息:
dig example.com & tcpdump -i any udp port 53 -n使用 tcpdump 可以让您区分缓存和真实的网络请求:如果没有流量,则响应很可能来自缓存。
跟踪系统调用
此命令允许您跟踪系统调用并监控 DNS 请求。
strace -e trace=network curl example.com输出片段:
sendto(3, "\264\372\1\0\0\1\0\0\0\0\0\0\7example\3com\0\0\1\0\1", 33, 0,{sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("192.168.1.1")}, 16) = 33
recvfrom(3, "\264\372\201\200\0\1\0\1\0\0\0\0\7example\3com\0\0\1\0\1"..., 512, 0, ..., ...) = 49这里,sendto() 向 53 端口发送 DNS 请求,recvfrom() 接收来自 DNS 服务器的响应。这确保了解析实际上是通过网络进行的,而不是从缓存进行的。
典型情况的解决方案
为了提供完整的画面,我将提供几种典型情况以及主要的解决方案选项。
“该网站对除我之外的所有人开放。”
原因:浏览器或系统解析器缓存已过期。
解决方案:将 ping 得到的 IP 与预期 IP 进行比较,然后清除缓存。
“我们更改了 A 记录,但流量仍然流向旧服务器。”
原因:未提前降低TTL。
解决方案:在轮班前24-48小时设置一个较短的TTL(60秒)。
“curl 可以工作,但浏览器不行”
原因:浏览器正在使用其自身的 DNS 缓存。
解决方案:清除浏览器缓存。
更改 DNS 后,Nginx 停止工作。
原因:Nginx 缓存(valid中的参数resolver)。
解决方案:减少valid=1s或重启 Nginx。
“即使在刷新后,库仍使用旧地址”
原因:语言/库维护了自己的缓存。
解决方案:调整代码中的 TTL 或重启应用程序。
完整的缓存清除清单
为了方便起见,我将所有内容收集在一张表中:
类别 | 经过 | 命令/动作 | 评论 |
系统级 | systemd-resolved | resolvectl 刷新缓存 | 清除缓存 |
非小细胞肺癌 | sudo nscd -i 主机 | nscd 中的主机缓存失效 | |
dnsmasq | sudo systemctl 重启 dnsmasq | 重新启动 dnsmasq - 清除缓存 | |
未绑定 | 未绑定控制刷新 | 彻底清除 Unbound 的缓存 | |
未绑定 | 无界控制 flush_zone example.com | 清除 Unbound 中特定区域的缓存 | |
绑定 | rndc 刷新 | 彻底清除 BIND 缓存 | |
绑定 | rndc flushname example.com | 清除 BIND 中的区域缓存 | |
绑定 | rndc flushname -type=A example.com | 清除 BIND 中记录类型 (A) 的区域缓存 | |
应用 | 铬合金 | Chrome:chrome://net-internals/#dns | 通过浏览器界面清除 DNS 缓存 |
火狐 | Firefox:选项→清除DNS缓存 | 可以通过 about:config 清除 Firefox 中的 DNS 缓存 | |
Java/Go | 需要重新启动应用程序才能清除 DNS 缓存。 | ||
容器 | docker/podman | 重启<容器> | |
核心域名 | kubectl rollout 重启部署/coredns -n kube-system | 重启 CoreDNS 会创建新的 Pod,但客户端仍然可以缓存 DNS。要彻底清除缓存,您还必须重启客户端。 |
如何正确管理缓存
有关使用缓存的一些实用技巧。
正在开发中
1. 对测试域名使用较短的 TTL(60-120 秒)。对于负面响应(NXDOMAIN),请在解析器配置中设置更短的 TTL(5-30 秒)(例如,dnsmasq 中的 cache_neg_ttl),以便快速测试修复效果。2.
了解如何在每个使用的缓存层级刷新缓存。3
. 在隔离环境中测试 DNS 更改。
在生产中
1. 规划 DNS 变更:提前降低 TTL。2
. 监控缓存:记录过期响应和 NXDOMAIN。3
. 使用可控缓存的集中式解析器。4
. 设置正确的 TTL:
- 对于频繁变更的服务:60-300 秒。-
对于静态数据:3600 秒以上。
生产环境中有效的 DNS 管理需要持续的缓存监控。关键指标包括:命中率(缓存效率)、过时响应、NXDOMAIN 率(解析错误)和延迟。
监控系统解析器
要分析 DNS 缓存性能并诊断问题,获取解析器统计信息至关重要。下表总结了从 systemd-resolved 到 BIND 等常用系统 DNS 服务的关键命令。
解析器 | 命令/设置 | 描述 |
systemd-resolved | resolvectl 统计信息 | 缓存统计信息:命中、未命中 |
nscd-g | 系统调用缓存统计 | |
dnsmasq | 添加到配置:log-querieslog-facility=/var/log/dnsmasq.log | 将 DNS 查询记录到文件中 |
未绑定 | 无界控制 stats_noreset | 常规服务器统计信息 |
未绑定 | `未绑定控制 dump_cache | 缓存内容分析 |
绑定 | rndc 统计数据 | 常规服务器统计信息 |
绑定 | rndc dumpdb-缓存 | 缓存内容分析 |
在 Kubernetes 环境中进行监控
高效的 DNS 操作对于 Kubernetes 集群性能至关重要。CoreDNS 作为标准解析器,会生成有助于识别以下指标的指标:
缓存效率(是否减轻了上游的负载);
名称解析错误(例如 NXDOMAIN 爆发);
异常延迟(网络问题或拥塞)。
通过 Prometheus 收集这些数据,您可以设置警报并在故障影响您的应用程序之前防止故障。
指标示例
- 缓存效率:
sum(rate(coredns_cache_hits_total{type="success"}[5m])) / sum(rate(coredns_cache_requests_total[5m]))
- 错误追踪:
rate(coredns_dns_responses_total{rcode="NXDOMAIN"}[5m])
- 请求延迟:
histogram_quantile(0.99, rate(coredns_dns_request_duration_seconds_bucket[5m]))
警报示例
- NXDOMAIN 错误增加:
rate(coredns_dns_responses_total{rcode="NXDOMAIN"}[5m]) > 10
- 响应速度下降:
histogram_quantile(0.99, rate(coredns_dns_request_duration_seconds_bucket[5m])) > 1
- 效率降低:
rate(coredns_cache_misses_total[15m]) / rate(coredns_cache_requests_total[15m]) > 0.5
积极主动的做法
自动对 NXDOMAIN 响应的异常增长发出警报。
监控 DNS 记录生存时间 (TTL) 值,以避免使用过时的数据。
可视化命中/未命中率来监控缓存效果。
结论
使用 DNS 缓存的关键原则主要在于了解 DNS 在系统中的缓存位置、规划 DNS 记录更改、能够诊断和清除不同级别的缓存,以及根据数据的性质调整 TTL。希望本文能够帮助您理清这些问题。
在下一节中,我们将了解各种解析器(glibc、systemd-resolved、dnsmasq、NetworkManager)如何交互,当/etc/resolv.conf它们指向 127.0.0.53 时会发生什么,以及如何找出谁在真正管理您的系统上的 DNS。
