DNS 缓存是如何工作的?TTL 怎么设置才合理?
DNS 缓存是域名解析系统的核心加速机制——每次浏览器访问一个域名,背后可能经历多级缓存的命中或穿透。理解 DNS 缓存的工作原理和 TTL 配置策略,是后端和网络面试中的高频考点。
DNS 缓存的工作原理
当你在浏览器输入一个域名时,解析请求不会每次都从根域名服务器开始逐级查询。DNS 系统在多个层级设置了缓存,尽可能复用之前的查询结果:
- 浏览器缓存:Chrome 等浏览器会在进程内维护 DNS 缓存, chrome://net-internals/#dns 可以查看。默认缓存时间约 1-5 分钟,部分浏览器会根据 TTL 自行调整
- 操作系统缓存:系统级 DNS 解析器缓存。Windows 默认缓存时间约 120 秒;Linux 上由 nsswitch.conf 和 systemd-resolved 控制
- 递归解析器缓存:ISP 或公共 DNS(如 8.8.8.8、1.1.1.1)的缓存,严格遵循记录的 TTL 值
- 权威服务器:权威 DNS 本身不缓存外部记录,但 SOA 记录中的 minimum 字段控制否定缓存的 TTL
一个关键点:TTL 不是强制刷新时间,而是最大允许缓存时间。解析器可以在 TTL 到期前的任意时刻清除缓存,但不能在 TTL 未过期时继续使用已过期的缓存记录。
TTL 的作用与权衡
TTL(Time To Live)是 DNS 记录的一个字段,单位为秒,决定了这条记录在各级缓存中的最长有效期。
TTL 的核心矛盾在于性能与灵活性的平衡:
- TTL 长:缓存命中率高,查询延迟低,但记录变更后传播慢
- TTL 短:记录变更能快速生效,但查询量增加,响应延迟上升
面试中常问的一个场景:如果你的服务要做 IP 迁移,TTL 该怎么调?
答案是三步走:
bash# 第一步:迁移前 24-48 小时,将 TTL 降至 300 秒 example.com. 300 IN A 192.0.2.1 # 第二步:等旧 TTL 过期后,更新 IP 地址 example.com. 300 IN A 203.0.113.1 # 第三步:确认新 IP 生效后,恢复 TTL example.com. 3600 IN A 203.0.113.1
为什么要提前降 TTL?因为降 TTL 本身也需要等旧的(较长的)TTL 过期才能生效。如果你原来 TTL 是 86400 秒(24 小时),那就至少需要提前 24 小时降低 TTL。
不同记录类型的 TTL 设置
DNS 记录类型不同,TTL 的合理范围也不同:
A/AAAA 记录——指向 IP 地址,变更可能性最高:
bash# 静态 IP,不常变动 example.com. 3600 IN A 192.0.2.1 # CDN 或负载均衡后端,可能随时切换 dynamic.example.com. 300 IN A 203.0.113.1
CNAME 记录——指向另一个域名,通常很稳定:
bashwww.example.com. 86400 IN CNAME example.com.
MX 记录——邮件服务器地址,极少变动:
bashexample.com. 7200 IN MX 10 mail.example.com.
NS 记录——域名服务器,变更成本高,应设长 TTL:
bashexample.com. 86400 IN NS ns1.example.com.
一个实用的参考范围:稳定记录 3600-86400 秒,可能变化的记录 300-1800 秒,临时记录 60-300 秒。公共 DNS 服务商通常有最小 TTL 限制,比如阿里云免费版最低 600 秒,企业版可设到 1 秒。
Java 应用中的 DNS 缓存
这是面试中容易踩坑的地方。JVM 有自己独立的 DNS 缓存机制,不走操作系统的 TTL 设置:
java// 默认情况下,JVM 成功解析的 DNS 记录会缓存很久 // 在 security/java.policy 或启动参数中设置 // 方式一:通过 JVM 启动参数 // -Dsun.net.inetaddr.ttl=30 成功解析缓存 30 秒 // -Dsun.net.inetaddr.negative.ttl=5 失败解析缓存 5 秒 // 方式二:在代码中设置 java.security.Security.setProperty("networkaddress.cache.ttl", "30"); java.security.Security.setProperty("networkaddress.cache.negative.ttl", "5");
默认值的问题:JDK 默认将成功解析的缓存时间设为 -1(永久缓存),失败解析缓存 10 秒。这意味着如果你的服务依赖 DNS 做服务发现(比如通过域名连接后端集群),IP 变更后 Java 应用可能一直连旧地址。
Spring Boot / 微服务场景:如果你的服务通过 Nginx 域名反向代理访问后端,或者使用 Consul/Eureka 等做服务发现,务必设置 networkaddress.cache.ttl,否则节点上下线后客户端无法感知。
负缓存与缓存预热
负缓存(Negative Caching)缓存的是查询失败的结果。比如查询一个不存在的子域名,NXDOMAIN 响应也会被缓存,避免重复请求。SOA 记录的 minimum 字段控制负缓存 TTL,RFC 2308 建议不超过 3 小时:
bash# BIND 配置负缓存 options { max-ncache-ttl 10800; # 负缓存最大 TTL min-ncache-ttl 60; # 负缓存最小 TTL };
缓存预热是在系统启动时主动查询常用域名,填充缓存:
pythonimport dns.resolver import time def warmup_cache(domains): resolver = dns.resolver.Resolver() for domain in domains: try: resolver.resolve(domain, "A") except Exception: pass time.sleep(0.1) common_domains = ["api.example.com", "db.example.com", "cache.example.com"] warmup_cache(common_domains)
缓存预热适合服务冷启动场景,比如容器新扩容的 Pod 首次启动时,预热内部服务域名可以减少首次请求的延迟毛刺。
缓存清理与手动刷新
当 DNS 记录变更后需要立即生效时,可以手动清理各级缓存:
bash# BIND 服务器 rndc flush # 清理全部缓存 rndc flushname example.com # 清理指定域名 # Windows DNS 服务器 Clear-DnsServerCache # Linux systemd-resolved resolvectl flush-caches # Windows 客户端 ipconfig /flushdns # macOS 客户端 dscacheutil -flushcache
注意:你只能清理自己控制的缓存。公共 DNS(如 8.8.8.8)的缓存你无法手动清理,只能等 TTL 自然过期。这也是为什么变更前必须提前降 TTL。
监控与问题排查
缓存命中率
缓存命中率是衡量 DNS 性能的核心指标。命中率低意味着大量查询穿透到权威服务器,增加延迟和负载:
bash# BIND 统计 rndc stats # 输出中查看 cache hits 和 cache misses # 计算命中率 # hit_rate = hits / (hits + misses) * 100%
TTL 查看与追踪
bash# 查看记录当前 TTL dig +noall +answer example.com # 查看完整解析路径和各环节 TTL dig +trace example.com # 从指定 DNS 服务器查询 dig @8.8.8.8 example.com
常见问题
DNS 变更不生效:最常见的原因是旧 TTL 未过期。用 dig +trace 检查各级缓存中的 TTL 剩余时间,确认是否还有未过期的旧记录。
缓存命中率低:通常是 TTL 设置过短。分析查询日志,对稳定域名适当增加 TTL。
缓存污染:攻击者向递归解析器注入伪造的 DNS 响应。防护措施包括启用 DNSSEC 验证、限制递归查询来源、使用可信的 DNS 服务器。
面试追问方向
- DNS 缓存有几层?每层的 TTL 策略有什么区别?
- JVM 的 DNS 缓存和操作系统有什么不同?怎么配?
- 服务迁移时 TTL 应该怎么调整?为什么不能直接改 IP?
- 什么是负缓存?SOA 的 minimum 字段控制什么?
- DNSSEC 如何防止缓存污染?对 TTL 有什么影响?