我的服务用域名连下游,对方机器扩缩容换了 IP 之后,我的服务却死活还在连那台早已下线的旧机器、疯狂报连接失败,我排查了大半天才发现是 DNS 被永久缓存了的复盘
这是一个让我对 DNS 缓存刻骨铭心的故事。我有一个服务,通过域名去调用一个下游服务(比如 api.internal.com)。它一直跑得好好的。直到有一天,下游服务做了一次扩缩容/迁移,换了一批机器、IP 也变了(他们更新了 DNS 记录,把域名指向了新 IP)。从那一刻起,我的服务就出问题了:它死活还在连那台早已下线的旧 IP,疯狂地报 连接被拒绝(connection refused)/ 连接超时;可我用 dig/nslookup 去查那个域名,解析出来明明已经是新 IP 了啊!更诡异的是,这个问题,重启一下我的服务,就好了;可下次下游再换 IP,又会复现。
我当时非常困惑:DNS 记录明明早就更新成新 IP 了,我的服务为什么还顽固地连旧 IP?它是从哪儿拿到这个旧 IP 的?我顺着这个现象深挖,才终于揭开真相,补上了我对 DNS 一个最致命的认知漏洞:问题的核心,是"DNS 缓存(DNS caching)"——更准确地说,是我的服务(一个 JVM 应用),把那个域名解析出来的旧 IP,给"永久地缓存"了下来。我一直想当然地以为,"程序每次用域名发请求,都会去重新解析一次、拿到最新的 IP";可真相是:解析 DNS 是有成本的(要走网络去问 DNS 服务器),所以各个层级,都会缓存解析结果以提速;而 JVM,有一个臭名昭著的默认行为:对于解析成功的 DNS 结果,它的默认缓存策略,在很多配置下,是"永久缓存(TTL = -1)"——也就是说,一旦它第一次把 api.internal.com 解析成了某个 IP,它就会把这个 IP 记一辈子,在整个进程生命周期内,都再也不会去重新解析了!所以,当下游换了 IP,我的 JVM 压根不知道——它还死抱着启动时解析到的那个旧 IP 不放,自然就一直去连那台已经下线的旧机器,而重启服务之所以能"治好",正是因为重启会清空缓存、重新解析一次,拿到了新 IP。我这才痛彻地明白:"用域名,就一定能自动连到最新的机器"是一个危险的幻觉;DNS 解析结果,在从客户端到操作系统的每一层,都可能被缓存;而缓存的存在,就意味着"变更的延迟"——你的程序看到的 IP,可能是"过期的、陈旧的"。在一个机器会动态扩缩容、IP 频繁变化的云原生时代,理解并正确控制 DNS 缓存的 TTL,是保证服务能"跟得上"下游变化的、一道不起眼却致命的关卡。
故障现场:JVM 永久缓存了 DNS 解析结果
我把这个"顽固连旧 IP"的现场,摊开给你看:
# ✗ 灾难: JVM 永久缓存 DNS 解析结果, 下游换 IP 后还连旧的
# 时间线:
# T0: 我的服务启动, 第一次解析 api.internal.com → 1.1.1.1, 缓存!
# T1: 下游扩缩容, 换机器, 更新 DNS: api.internal.com → 2.2.2.2
# T2: dig api.internal.com → 2.2.2.2 (DNS 记录确实更新了)
# T3: 但我的 JVM 还在用缓存里的 1.1.1.1 → 连 1.1.1.1(已下线) → 失败!
# T4: 重启我的服务 → 重新解析 → 拿到 2.2.2.2 → 恢复正常。
# 根源: JVM 的 DNS 缓存策略(InetAddress)
# - networkaddress.cache.ttl: 解析"成功"结果的缓存秒数。
# * 默认值在装了 SecurityManager 时是 -1 = 永久缓存(整个进程不再重解析)!
# * 即使没装, 不同版本/配置下默认缓存时间也可能很长。
# - networkaddress.cache.negative.ttl: 解析"失败"结果的缓存秒数(默认 10)。
# 为什么会有缓存?
# - DNS 解析要走网络问 DNS 服务器, 有延迟和开销。
# - 缓存能避免每次请求都解析 → 提速。但代价是: 看不到"变更"。
# 哪些层级都会缓存 DNS?
# - 应用层(JVM InetAddress / 各语言/库的解析缓存)。
# - 操作系统(nscd / systemd-resolved 等本地 DNS 缓存)。
# - DNS 服务器自身的 TTL(权威记录设的过期时间)。
# → 任何一层缓存过久, 都会让你看到"过期的 IP"。
# 现象: 下游换 IP 后, 我的服务持续连旧 IP, connection refused/timeout;
# dig 查到的是新 IP, 但重启前我的服务就是不认。
# 根因: JVM(及各层)缓存 DNS 解析结果, 默认 TTL 过长甚至永久,
# 下游 IP 变更后, 缓存未过期 → 持续连旧 IP。
看着这条时间线,我才算彻底想明白了这场"顽固连旧 IP"的根源。问题的核心,是 JVM 的 DNS 缓存策略:networkaddress.cache.ttl 控制"解析成功"结果的缓存秒数,而它的默认值,在装了 SecurityManager 时是 -1(永久缓存,整个进程都不再重新解析);即使没装,不同版本/配置下默认缓存时间也可能很长。于是时间线就清楚了:我的服务启动时把域名解析成了旧 IP 1.1.1.1 并永久缓存;下游换成 2.2.2.2 后,dig 查到的是新 IP,但我的 JVM 还死抱着缓存里的 1.1.1.1,持续去连那台已下线的旧机器、失败;重启服务会清空缓存、重新解析,所以能"治好"。为什么会有缓存?因为 DNS 解析要走网络问 DNS 服务器、有开销,缓存能提速;但代价是看不到"变更"。而且,缓存 DNS 的层级,不止 JVM:应用层(各语言/库)、操作系统(nscd/systemd-resolved)、DNS 服务器自身的 TTL——任何一层缓存过久,都会让你看到"过期的 IP"。归根结底:JVM(及各层)缓存 DNS 解析结果,默认 TTL 过长甚至永久,下游 IP 变更后缓存没过期,就会持续连旧 IP——这,就是根源。
第一件事:搞懂 DNS 缓存与 TTL
定位到根源,我必须把"DNS 缓存到底是怎么回事"从根上彻底搞清楚:
DNS 缓存: 解析结果被各层缓存以提速, 但代价是"变更有延迟"
# 域名 → IP 的解析链路:
# 程序 → 应用层缓存 → 操作系统缓存 → 本地DNS → 递归DNS → 权威DNS
# 任何一层有缓存命中, 就直接返回, 不再往下问。
# TTL(Time To Live): 缓存能存多久
# - 权威 DNS 给每条记录设了 TTL(如 60s/300s/3600s)。
# - 理论上各层缓存应遵守这个 TTL, 过期就重新解析。
# - 但: 客户端/应用层可能"自作主张"用自己的缓存策略, 无视记录 TTL!
# JVM 的坑(本文重点):
# - InetAddress 的 networkaddress.cache.ttl:
# * 装 SecurityManager: 默认 -1 = 永久缓存(最坑!)。
# * 不装: 默认实现会缓存约 30 秒(版本相关), 也未必读记录 TTL。
# - 即程序的缓存, 不一定听 DNS 记录 TTL 的话!
# "变更延迟"的本质:
# - 你改了 DNS 记录, 不等于全世界立刻看到新 IP。
# - 必须等"各层缓存都过期", 新 IP 才完全生效。
# - 所以切 IP 前要把 TTL 提前调小, 切完观察一段时间。
# 怎么验证是哪一层在缓存?
# - dig 看权威/递归返回的 IP 和剩余 TTL。
# - 对比"你程序实际连的 IP"(抓包/日志) vs "dig 的 IP"。
# - 不一致 → 是你程序/OS 这层缓存了旧的。
# 关键认知: 用域名 ≠ 总连到最新 IP; 缓存让你可能看到过期 IP。
# - 动态扩缩容时代, 要主动控制 DNS 缓存 TTL, 让程序跟得上变更。
# 核心: DNS 各层都缓存解析结果(按 TTL), 但客户端(尤其 JVM)可能用
# 自己的过长/永久缓存策略, 无视记录 TTL, 导致 IP 变更后还连旧的。
原理终于清晰了。域名到 IP 的解析链路是:程序 → 应用层缓存 → 操作系统缓存 → 本地 DNS → 递归 DNS → 权威 DNS,任何一层缓存命中,就直接返回、不再往下问。而 TTL 控制缓存能存多久:权威 DNS 给每条记录设了 TTL,理论上各层应遵守它、过期就重新解析;但客户端/应用层可能"自作主张"用自己的缓存策略,无视记录 TTL!JVM 的坑正在于此:networkaddress.cache.ttl 在装 SecurityManager 时默认 -1(永久缓存,最坑),不装时默认也缓存约 30 秒、且未必读记录 TTL——即程序的缓存,不一定听 DNS 记录 TTL 的话。这就引出了"变更延迟"的本质:你改了 DNS 记录,不等于全世界立刻看到新 IP;必须等各层缓存都过期,新 IP 才完全生效;所以切 IP 前要提前把 TTL 调小。怎么验证是哪一层在缓存?dig 看权威/递归返回的 IP 和剩余 TTL,对比"你程序实际连的 IP"(抓包/日志)——不一致,就是你程序/OS 这层缓存了旧的。由此,我刻下一个关键认知:用域名 ≠ 总连到最新 IP;缓存让你可能看到过期 IP;动态扩缩容时代,要主动控制 DNS 缓存 TTL,让程序跟得上变更。归根结底:DNS 各层都按 TTL 缓存解析结果,但客户端(尤其 JVM)可能用自己过长/永久的缓存策略、无视记录 TTL,导致 IP 变更后还连旧的。
第二件事:正解——设置合理的 DNS 缓存 TTL
搞懂了原理,正解就清晰了:把 JVM(及各层)那个过长甚至永久的 DNS 缓存 TTL,改成一个合理的、较短的值,让程序能定期重新解析、跟上 IP 变更。
// ✓ 正解一: 显式设置 JVM 的 DNS 缓存 TTL(改成较短, 如 30~60 秒)
// 方式A: 在代码最早期(main 入口)设置
java.security.Security.setProperty("networkaddress.cache.ttl", "30"); // 成功缓存 30 秒
java.security.Security.setProperty("networkaddress.cache.negative.ttl", "5"); // 失败缓存 5 秒
// 方式B: 在 $JAVA_HOME/jre/lib/security/java.security 文件里配置
// networkaddress.cache.ttl=30
// networkaddress.cache.negative.ttl=5
// 方式C: 启动参数(JVM 系统属性, 注意优先级与上面不同, 视版本)
// -Dsun.net.inetaddr.ttl=30
// ✓ 正解二: 不要在程序启动时"解析一次就长期持有 IP"
// - 别把 InetAddress / 解析出的 IP 缓存成全局变量长期复用。
// - 让 HTTP 客户端在"每次新建连接时"重新解析域名(多数客户端默认如此, 但要确认)。
// ✓ 正解三: 连接池要能感知 DNS 变更
// - 有些连接池会长期持有到旧 IP 的连接 → 配置连接最大存活时间, 强制定期重建。
// - 如 OkHttp/HikariCP 等设置 connection max-lifetime, 让旧连接定期淘汰。
// ✓ 正解四: 切 IP 前, 先把权威 DNS 记录的 TTL 调小(运维侧)
// - 切换前几小时把 TTL 从 3600 调到 60, 切完观察, 再调回。
// 核心: 把 JVM DNS 缓存 TTL 设为合理的较短值(如30~60s), 别长期持有解析的 IP,
// 连接池配最大存活时间定期重建, 运维侧切 IP 前先调小记录 TTL。
修复的方向,是"让缓存别那么顽固"。正解一,显式设置 JVM 的 DNS 缓存 TTL:把 networkaddress.cache.ttl 从默认的永久/过长,改成一个合理的较短值(如 30~60 秒)——可以在 main 入口用 Security.setProperty 设置,或在 java.security 文件里配置;同时把失败缓存 negative.ttl 也设短一点(默认才 10 秒,一般够)。正解二,别在启动时"解析一次就长期持有 IP":不要把解析出的 IP 缓存成全局变量长期复用,让 HTTP 客户端在每次新建连接时重新解析域名。正解三,连接池要能感知 DNS 变更:有些连接池会长期持有到旧 IP 的连接,要配置连接最大存活时间(max-lifetime),让旧连接定期被淘汰、重建。正解四(运维侧),切 IP 前先把权威 DNS 记录的 TTL 调小:切换前几小时把 TTL 从 3600 调到 60,切完观察、再调回——让变更能更快地在全网生效。归根结底:把 JVM DNS 缓存 TTL 设为合理的较短值(如 30~60s)、别长期持有解析的 IP、连接池配最大存活时间定期重建、运维侧切 IP 前先调小记录 TTL。
第三件事:DNS 问题的排查路径
这次踩坑也让我沉淀了一套排查"连不上/连错机器"时,判断"是不是 DNS 缓存问题"的方法:
DNS 问题排查: 对比"程序连的 IP" 和 "DNS 实际解析的 IP"
# 1. 确认 DNS 记录现在解析成什么(权威/递归)
dig api.internal.com # 看 ANSWER 段的 IP 和 TTL
dig +trace api.internal.com # 看完整解析链路(排查哪一级返回旧的)
nslookup api.internal.com
# 2. 确认"你的程序实际在连哪个 IP"
# - 抓包: tcpdump -i any host api.internal.com 或 port 443
# - 看连接: ss -tnp | grep <你的进程> → 看 peer IP
# - 应用日志里打印解析到的 IP
# 3. 对比第1步和第2步:
# - 程序连的 IP == dig 的 IP → 不是 DNS 缓存问题, 查别的。
# - 程序连的 IP != dig 的 IP(连的是旧 IP) → 你程序/OS 缓存了旧的!
# 4. 缩小到哪一层缓存:
# - 重启程序就好 → 应用层(JVM)缓存(本文)。
# - 重启程序没好, 但清 OS 缓存(systemd-resolve --flush-caches)好了 → OS 层。
# - 都没好, dig 自己都返回旧 IP → 上游 DNS 还没更新/TTL 没到。
# 5. 验证 JVM 的实际 TTL 设置:
# - 看 java.security 配置 / 启动参数 / 是否装了 SecurityManager。
# 经验:
# - "重启就好、过段时间又犯" 是 DNS 缓存问题的典型特征。
# - 域名连下游 + 下游会换 IP 的场景, 都要警惕 DNS 缓存。
# 核心: 排查时对比"程序连的IP"和"dig解析的IP", 不一致就是缓存了旧的;
# 按"重启/清OS缓存/dig"逐层定位是哪层缓存, 再针对性设 TTL。
这套排查路径,让我面对"连不上/连错机器"时,能快速判断是不是 DNS 缓存的锅。核心方法,是对比两个 IP:第一,用 dig/nslookup 看"DNS 现在实际解析成什么 IP"(dig +trace 还能看完整解析链路);第二,用 tcpdump 抓包或 ss -tnp 看"你的程序实际在连哪个 IP";第三,对比二者——如果程序连的 IP 和 dig 的不一致(连的是旧 IP),那就是你程序/OS 缓存了旧的。接着缩小到哪一层:重启程序就好 → 应用层(JVM)缓存(本文);重启没好但清 OS 缓存(systemd-resolve --flush-caches)好了 → OS 层;都没好、连 dig 自己都返回旧 IP → 上游 DNS 还没更新。我也总结了经验:"重启就好、过段时间又犯",是 DNS 缓存问题的典型特征;域名连下游 + 下游会换 IP 的场景,都要警惕 DNS 缓存。归根结底:排查时对比"程序连的 IP"和"dig 解析的 IP",不一致就是缓存了旧的;按"重启/清 OS 缓存/dig"逐层定位是哪层缓存,再针对性地设 TTL。
下面这张图,是这次"DNS 缓存"事故的成因与解法:
第四件事:各层 DNS 缓存的速查对照
这次踩坑后,我把"DNS 解析链路上各层的缓存",整理成一张速查表,排查时能快速锁定是哪一层。
| 缓存层 | 谁在缓存 | 怎么控制/清除 |
|---|---|---|
| 应用层(JVM) | InetAddress 缓存 | networkaddress.cache.ttl 设短; 重启清空 |
| 应用层(其他语言/库) | 各 HTTP 客户端/解析库 | 看库的 DNS 缓存配置, 多数较短或不缓存 |
| 操作系统 | nscd / systemd-resolved | systemd-resolve --flush-caches |
| 本地/递归 DNS | 运营商/内网 DNS 服务器 | 遵守记录 TTL, 等过期或刷新 |
| 权威 DNS | 域名记录本身的 TTL | 改记录 + 等 TTL 过期 |
| 连接池 | 已建立到旧IP的长连接 | 设 max-lifetime 定期重建 |
这张表,让"DNS 缓存在哪一层"变得一目了然。排查时,从离程序最近的层往外查:应用层(JVM 的 InetAddress 缓存,本文的元凶)→ 操作系统(nscd/systemd-resolved)→ 本地/递归 DNS → 权威 DNS 记录本身;此外还有一个容易被忽略的"隐形缓存"——连接池:就算 DNS 重新解析了,连接池里那些已经建立到旧 IP 的长连接,如果不淘汰,照样在连旧机器。每一层都有对应的控制/清除手段:JVM 设 cache.ttl、OS 用 flush-caches、递归 DNS 等 TTL 过期、连接池设 max-lifetime。它给我的最大启发是:"DNS 解析"这个看似简单的"域名变 IP"的动作,背后其实是一条横跨应用、系统、网络的多级缓存链路;任何一级"缓存得太久",都会让"变更"卡在半路。理解了这条完整的链路,你才能在 IP 变更出问题时,精准地定位到"是哪一级缓存在作祟",而不是无头苍蝇般乱撞、最后只会"重启大法"。
第五件事:用域名通信时,那些被忽略的"动态性"问题
顺着这次的教训,我把"用域名做服务间通信"时,其他容易被忽略的、和"动态变化"相关的问题,系统梳理了一遍。
| 问题 | 表现 | 应对 |
|---|---|---|
| DNS 缓存过久 | 下游换IP后还连旧的(本文) | 设短 TTL + 连接池定期重建 |
| 只用了解析的第一个 IP | 域名多 IP 时没负载均衡/容错 | 轮询/随机用所有解析出的 IP |
| 负缓存太久 | 下游短暂故障后, 恢复了还连不上 | 调小 negative.ttl |
| 长连接不感知后端变化 | 下游缩容了, 连接还在打到该走的机器 | 连接 max-lifetime + 健康检查 |
| DNS 解析阻塞主流程 | DNS 慢时拖慢整个请求 | 设解析超时, 或异步/缓存解析 |
| 直接硬编码 IP | IP 变了就全线崩, 无法迁移 | 用域名/服务发现, 别硬编码 IP |
这张表,让我对"用域名通信"这件看似简单的事,有了远超"填个域名就行"的认识。它们的共同主题,是"动态性":在云原生时代,下游的机器、IP、状态,都是动态变化的;而我们的客户端,如果抱着"它一直不变"的静态假设,就会在各种"变化"面前出问题。无论是 DNS 缓存过久(本文)、只用第一个解析 IP 而没负载均衡、负缓存太久导致下游恢复了还连不上、长连接不感知后端缩容、还是 DNS 解析阻塞主流程——它们都在提醒我同一件事:客户端不能假设下游是"静态"的,必须设计成能持续地、动态地感知并适应下游的变化。而最该警惕的反面,是那个"图省事直接硬编码 IP"的做法:它一旦 IP 变了就全线崩、无法迁移,是把"静态假设"做到了极致的危险写法。它给我的最大启发是:写分布式系统的客户端,要有一种"拥抱变化"的心态:不要假设你依赖的任何东西(IP、机器、可用性)是一成不变的,而要主动去设计"当它们变化时,我如何能及时、平滑地跟上"。在一个万物皆动态的时代,"对变化的适应能力",本身就是系统健壮性的一部分。
第六件事:用域名连下游时,我现在会怎么决策
现在,每当我准备用域名去连一个下游服务,脑子里都会过一遍这张决策图——核心就一问:下游的 IP 会变吗?我跟得上吗?
这张图的灵魂,是那个必问的问题:下游的 IP 会变吗?我跟得上吗?如果下游 IP 基本不变,风险低(但仍建议设合理 TTL);如果会动态变化(扩缩容/迁移/漂移),就必须保证能跟上变更:设 JVM/客户端的 DNS 缓存 TTL 为较短值、连接池配 max-lifetime 定期淘汰旧连接;用了长连接的,还要加健康检查、剔除连到旧/坏机器的连接;用短连接的,确保每次新连接都重新解析。更进一步:别硬编码 IP;复杂场景上服务发现/SLB(让一个稳定的入口,去屏蔽后端的动态变化)。最后,也是我以前最缺的一步:做演练——主动模拟一次"下游换 IP",验证我的服务确实能自动跟上,而不是等真出事了才发现跟不上。
我立下的几条规矩
这场"顽固连旧 IP"的事故,换来了我做服务间通信时,刻进骨子里的几条铁律:
- JVM 默认 DNS 缓存可能是永久的,必须显式设短。networkaddress.cache.ttl 设成 30~60 秒,别让它死抱启动时的 IP。
- 用域名 ≠ 自动连到最新机器。解析结果在应用/OS/DNS 各层都会被缓存,缓存意味着变更有延迟。
- 连接池要配 max-lifetime。否则就算重新解析了,池里到旧 IP 的长连接还在打到下线的机器。
- 切 IP 前先把 DNS 记录 TTL 调小。运维侧提前调小、切完观察再调回,让变更更快全网生效。
- 排查时对比"程序连的 IP"和"dig 的 IP"。不一致就是缓存了旧的;按重启/清 OS 缓存/dig 逐层定位。
- 别硬编码 IP。用域名/服务发现,把"动态变化"屏蔽在稳定的抽象之后。
- 对下游保持"拥抱变化"的心态。不假设 IP/机器/可用性不变,主动设计成能及时跟上变更。
附:验证与排查 DNS 缓存的实用命令
纸上得来终觉浅。下面这组命令,能帮你亲手验证 DNS 缓存问题、并定位到是哪一层,排查时照着敲一遍即可:
# ===== 1. 看 DNS 现在到底解析成什么 =====
dig api.internal.com +short # 只看 IP, 最简洁
dig api.internal.com # 看 ANSWER 段的 IP 和剩余 TTL
dig +trace api.internal.com # 看完整解析链路(定位哪一级返回旧的)
# ===== 2. 看你的程序实际连的是哪个 IP =====
# 看进程当前的 TCP 连接和对端 IP:
ss -tnp | grep <你的进程名或pid> # ESTAB 那行的 peer 地址就是实连 IP
# 抓包确认:
sudo tcpdump -i any -n host api.internal.com # 看实际握手到哪个 IP
# ===== 3. 对比 =====
# dig 的 IP == ss/tcpdump 的 IP → 不是缓存问题
# dig 的 IP != 程序连的 IP(连旧的) → 程序/OS 缓存了旧 IP
# ===== 4. 逐层定位/清除缓存 =====
# OS 层(systemd-resolved):
resolvectl flush-caches # 清 OS DNS 缓存
resolvectl statistics # 看缓存命中情况
# nscd(若用):
sudo systemctl restart nscd
# ===== 5. 验证 JVM 的 DNS 缓存设置 =====
# 看 java.security 里的配置:
grep networkaddress $JAVA_HOME/conf/security/java.security
# 在程序里临时打印验证(最早期设置后):
# System.out.println(Security.getProperty("networkaddress.cache.ttl"));
# ===== 6. 验证 TTL 改动是否生效(最直接) =====
# 改短 TTL 重启服务后, 模拟下游换 IP(改 hosts 或测试域名),
# 等 TTL 秒数后看程序是否自动连到新 IP → 自动跟上 = 生效。
# 核心: dig 看"该连的IP", ss/tcpdump 看"实连的IP", 二者对比锁定缓存问题;
# resolvectl flush / 重启程序 逐层定位是哪层缓存, 改短 TTL 后演练验证。
这组命令,是我现在排查"连不上/连错机器"的标准动作。它的核心逻辑,就是前面反复强调的那句:对比"DNS 该解析成的 IP"(dig)和"程序实际连的 IP"(ss/tcpdump)——这两个 IP 一不一致,直接决定了"是不是 DNS 缓存的锅"。一旦确认是缓存问题,再逐层定位:清 OS 缓存(resolvectl flush-caches)、重启程序、看 JVM 的 java.security 配置,一层层缩小范围,直到找到那个"缓存得太久"的元凶。而最有价值的,是最后那个"演练验证":改短 TTL、重启服务后,主动模拟一次下游换 IP(改 hosts 或用测试域名),看程序是否能在 TTL 秒数后自动连到新 IP——只有亲眼看到它"自动跟上了",才算真正确认问题被解决了。这,正是我想用这组命令,留给每一个后端工程师的最后一课:对于 DNS 缓存这种"看不见、摸不着、平时还总是对"的东西,不要靠脑补和猜测,而要用 dig、tcpdump 这些工具,把它"缓存了什么、连了哪里"实实在在地看出来。能把一个"玄学问题",变成一个"可观测、可验证的工程问题",你就已经解决它一大半了。
写在最后
回头看,这场由"DNS 被永久缓存"引发的、顽固连接旧 IP 的事故,真正教给我的,是一个比"设置 TTL"本身更深的道理:几乎每一个让我们"省心"的缓存,背后都藏着一个"数据可能过期"的代价;而缓存最危险的地方,恰恰在于它"大部分时候都对",从而让你彻底忘记了它的存在,也忘记了它"会过期"这件事。DNS 缓存,默默地为我加速了无数次请求,我享受着它的便利,却从未意识到它的存在;直到下游换了 IP——那个"缓存与现实出现分歧"的时刻——它才以一种让我措手不及的方式,暴露了自己。这正是"缓存一致性"这个计算机科学的经典难题,在 DNS 上的一次具体上演:缓存,是用"可能过期的旧数据",去换取"不必每次都获取的性能";而这笔交易的风险,就是当源头变化时,你的缓存,还活在过去。所以,面对系统里的任何一层缓存(DNS 缓存、HTTP 缓存、应用缓存、CDN……),都要时刻保持一份清醒:问自己——"这份缓存,多久会过期?当源头变了,它能多快感知到?如果它没及时过期,会发生什么?"。真正驾驭缓存的人,不仅会用它来加速,更始终对它"可能陈旧"这一面,保持着警惕和掌控。享受缓存的快,但永远记得它可能是"旧"的——这,是我用一次"连接旧 IP"的事故,换来的、关于 DNS、也关于一切缓存的、最朴素也最深刻的领悟。如果这篇复盘,能让你在下一次依赖某个缓存时,多问一句"它会过期吗,多久",那我对着那个顽固的旧 IP 熬的这大半天,就值了。
—— 别看了 · 2026