DNS 缓存机制及其对系统启动行为的影响

DNS 缓存,说白了就是系统或应用把查询过的域名-IP 映射暂时存下来,下次再查就不需要去上游服务器折腾了。这个机制本意是好心——减少网络延迟、降低递归服务器压力。但在我追踪过的几次启动崩溃里,它恰恰是埋雷的地方。

我记得有一次排查一个 Java 微服务的冷启动超时问题。服务启动时疯狂解析外部依赖的域名,结果恰巧上次缓存了一条过时记录,而目标 IP 已经迁移。于是客户端拿着旧 IP 拼命握手,握手失败后重试,重试又走缓存,循环三次,最终启动线程卡死,健康检查失败——被 K8s 直接杀掉。嗯,这就是典型的“缓存让你死得更快”。

缓存的三层结构

现代操作系统里 DNS 缓存不是单一的一层。我把它们分成三层来理解,每一层的失效策略和影响范围都不一样。

缓存层级典型实现TTL 影响启动时的风险
OS 内核缓存Linux nscd、systemd-resolved、Windows DNS Cache全局生效,遵守上游 TTL 但可能被覆盖全局污染,所有进程共享错误记录
应用层缓存Java JVM DNS TTL、Python requests.Session、Lua-resty-dns应用独立配置,常默认缓存 30s~300s启动时加载的配置文件如果携带过期记录,连续失败导致超时
库/框架缓存c-ares、glibc nsswitch 的缓存,某些场景下会走 host 文件取决于 nsswitch 配置,或直接忽略 TTL混合 host 文件逻辑,容易忽略真实变化

我曾经踩过的坑: 一个 Nginx 反向代理在热升级后加载新配置,如果 upstream 域名解析走 systemd-resolved 缓存,而那条记录在 DNS 服务器上已经 TTL 归零删除了,但 systemd-resolved 延后 30 秒才刷新。在这 30 秒里,Nginx 一直连接旧 IP,旧 IP 上的服务已经下线,导致 502 大面积扩散。

启动时缓存的“快与慢”

我刚入行时觉得缓存总是加速的,但启动场景完全不同。我给你梳理一下启动时的真实流程:

  1. 进程启动,加载配置文件,其中包含对数据库、消息队列、注册中心等服务的域名。
  2. 第一次调用 getaddrinfo 或类似接口,检查 OS 缓存。如果命中且未过期,直接返回 IP —— 这确实快。
  3. 但如果 OS 缓存里存的是一条“可连但不可用”的旧 IP,那么网络层连接会尝试 3~5 次,每次超时 1~3 秒。这加起来 10~15 秒 就没了。
  4. 更糟的是,应用层缓存可能又把这旧 IP 自己存一份。下次重试还是它,死循环。
  5. 一旦所有重试用尽,没有备选 IP,网络库抛出异常。如果启动时没有做优雅降级,整个进程就崩了。

关键指标: 一次 DNS 缓存命中速度是 0~1ms,一次网络往返解析是 10~200ms,而一次“虚假命中+连接超时+重试”的代价是约 5~15 秒。如果你的服务在启动时需要解析 10 个域名,最坏情况下启动可能被拖慢 2 分钟以上。

深入一点:TTL 与系统启动的微妙关系

TTL(Time-To-Live)是 DNS 缓存的金标准。但很多开发者忽略了:启动阶段,缓存的 TTL 往往是上次运行结束时留下的。 比如你凌晨 3 点更新了 DNS 记录,TTL 设为 60 秒。但系统恰好在 3:01 时重启,此时缓存已过期,看起来正常。可如果系统在 3:00 时崩溃自动重启——恰好缓存还没有过期,更新后的记录永远不会被本次启动感知到。

我曾经在电商大促前压测时遇到过。那天凌晨我们改了 CDN 切流,把 A 记录从旧 IP 换成新 IP。结果我们的负载均衡器在午夜 0:00 准时重启(日常维护),它从 systemd-resolved 里直接拿到了旧 IP 的缓存(TTL 还剩 120 秒),然后一直用旧 IP 转发流量。半个小时后用户反馈部分地区打不开页面,我们才意识到是 DNS 缓存没清就切流了。

我个人的习惯: 在重大 DNS 变更前,先降低 TTL(比如 30 秒),等 TTL 扩散 2~3 个周期后再改记录。改完后,再等一个 TTL 周期,最后才重启依赖的服务。这个流程虽然保守,但能躲掉 90% 的缓存导致的启动事故。

缓存是否应该被禁用?

你可能想问:既然缓存有这么多坑,为什么不禁用它?我的答案很直接——不能因噎废食。缓存在正常运行时能把 DNS 查询延迟从毫秒级降到微秒级,对性能收益很大。真正要解决的问题是怎么在启动这个特殊时间里,让缓存“听话”。

我见过两种主流做法:

  • 启动时显式清空缓存: 比如用 systemd-resolve --flush-caches 或调用 gethostbyname_r 之前先清空本地缓存。但要注意,这会短暂增加启动压力,如果 DNS 服务器扛不住并发,反而引发超时崩溃。
  • 应用层绕过操作系统缓存: 在应用代码里直接设置 ai_flags = AI_ADDRCONFIG 或使用 evdns_getaddrinfo 指定不走缓存。适合高可靠性场景,但实现成本略高。

我个人更推荐第二种——把控制权收回到应用层。你想想看:如果依赖系统缓存,你的进程只是缓存的一个消费者,而别的进程可能同时在污染它。把解析逻辑交给应用自己控制,配合健康检查机制,启动时先强解析,成功后再走短时间缓存,这样既快又稳。

一张图看懂:DNS 缓存如何影响启动崩溃

下面这张流程图展示了一台机器上的进程启动时,DNS 解析的完整路径。红色路径就是“缓存助纣为虐”导致崩溃的罪魁祸首。

DNS 解析流程与缓存影响(启动崩溃路径) 应用启动 → 请求解析域名 检查 OS 内核 DNS 缓存 缓存命中? 命中 → 直接返回 IP 旧 IP 连接超时 未命中 向上游 DNS 服务器查询 是否成功? 缓存结果 DNS 超时/失败 → 应用重试耗尽 → 崩溃 🟢 正常路径(缓存命中/服务器成功) 🔴 红色路径/虚线 → 启动崩溃的根因

实战排查:怎么确认是缓存的问题?

如果你怀疑某个启动崩溃是 DNS 缓存作祟,可以用下面这套步骤快速验证。我自己在线上就用过好几次,几乎一抓一个准。

  1. 在崩溃前最后一次正常启动的日志里,找到 gethostbyname 或 getaddrinfo 的调用,记录返回的 IP。
  2. 手动在出问题的机器上执行 nslookup <域名> 8.8.8.8(绕过本地缓存),看返回的 IP 是否与之一致。
  3. 如果不一致,说明当时用的就是缓存。再用 systemd-resolve --cache=true <域名>(Linux)或 powershell 的 Get-DnsClientCache(Windows)查看缓存内容。
  4. 检查缓存记录中的 TimeToLive 字段。如果 TTL 还有剩余,而实际 DNS 记录已变,那就是缓存污染。
# 典型排查命令链(Linux)
# 查看系统缓存中的域名解析结果:
systemd-resolve --cache=true example.com

# 清空所有缓存(谨慎使用,会短暂影响其他进程):
systemd-resolve --flush-caches

# 绕过缓存做一次真实查询:
dig example.com +short @1.1.1.1

# 如果使用 nscd,可以查看统计:
nscd -g | grep -A 5 "hosts"

一句话总结本节精髓: DNS 缓存本是好东西,但在系统启动时,它可能把“上一次的错误”延续到这一次,导致连接连续失败、超时堆叠、最终进程崩溃。理解缓存的三层结构,知道启动时应该主动规避或刷新,才是工程稳定性的关键。