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 大面积扩散。
启动时缓存的“快与慢”
我刚入行时觉得缓存总是加速的,但启动场景完全不同。我给你梳理一下启动时的真实流程:
- 进程启动,加载配置文件,其中包含对数据库、消息队列、注册中心等服务的域名。
- 第一次调用
getaddrinfo或类似接口,检查 OS 缓存。如果命中且未过期,直接返回 IP —— 这确实快。 - 但如果 OS 缓存里存的是一条“可连但不可用”的旧 IP,那么网络层连接会尝试 3~5 次,每次超时 1~3 秒。这加起来 10~15 秒 就没了。
- 更糟的是,应用层缓存可能又把这旧 IP 自己存一份。下次重试还是它,死循环。
- 一旦所有重试用尽,没有备选 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 缓存作祟,可以用下面这套步骤快速验证。我自己在线上就用过好几次,几乎一抓一个准。
- 在崩溃前最后一次正常启动的日志里,找到
gethostbyname或getaddrinfo的调用,记录返回的 IP。 - 手动在出问题的机器上执行
nslookup <域名> 8.8.8.8(绕过本地缓存),看返回的 IP 是否与之一致。 - 如果不一致,说明当时用的就是缓存。再用
systemd-resolve --cache=true <域名>(Linux)或 powershell 的Get-DnsClientCache(Windows)查看缓存内容。 - 检查缓存记录中的
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 缓存本是好东西,但在系统启动时,它可能把“上一次的错误”延续到这一次,导致连接连续失败、超时堆叠、最终进程崩溃。理解缓存的三层结构,知道启动时应该主动规避或刷新,才是工程稳定性的关键。