下游换了 IP 发布完成后我们死活连不上、下游明明健康重启自己就好:网络第一步 DNS 缓存导致连旧 IP 刻舟求剑的避坑复盘

这是一次下游明明好好的我却死活连不上的诡异故障。起因是我们依赖的一个下游服务做了一次发布,它换了新机器 IP 变了但对外的域名没变,本来嘛用域名访问的好处就是 IP 可以随便换域名不变。下游发布顺利完成自测一切正常,可就在它发布完成的那一刻我们的服务却开始大面积报错:调用那个下游持续地连接超时或者连接被拒绝。我赶紧检查下游服务确确实实是健康的能正常访问的,我们自己的网络代码也都没动,可我们就是连不上它,更诡异的是把我们自己的服务重启一下立刻就好了。排查到最后真凶是一个我平时几乎从不会去想的网络调用最最开头的那一步——DNS 解析以及它的缓存。原来当我们的服务第一次用域名去调用那个下游时要先做一次 DNS 解析把域名翻译成 IP 地址然后才能连过去,而这个域名到 IP 的解析结果被我们的服务缓存了起来,下游发布后 IP 变了域名指向了新 IP,可我们的服务还死死地用着缓存里那个旧 IP一直在尝试连接一台已经下线的旧机器。这篇文章从这次下游换了 IP 我却还连着旧 IP 的事故出发,讲透 DNS 避坑:理解域名访问的隐藏前提 DNS 解析加它的缓存、把 DNS 缓存时间配置合理别用永久缓存、用专门的服务发现替代裸 DNS、DNS 解析慢与失败和长连接绕过刷新等其它坑、学会用 dig 对比解析 IP 和实连 IP 排查,以及一个根本认知——最容易被忽略的环节往往藏着最隐蔽的坑,要把隐形环节也纳入认知地图。

这是一次"下游明明好好的,我却死活连不上"的诡异故障。事情的起因是,我们依赖的一个下游服务做了一次发布——它换了新机器、IP 变了,但对外的域名没变(本来嘛,用域名访问的好处就是 IP 可以随便换、域名不变)。下游发布顺利完成、自测一切正常。可就在它发布完成的那一刻,我们的服务却开始大面积报错:调用那个下游,持续地连接超时、或者连接被拒绝。我赶紧检查,下游服务确确实实是健康的、能正常访问的;我们自己的网络、代码也都没动。可我们就是连不上它。更诡异的是:把我们自己的服务重启一下,立刻就好了。

排查到最后,真凶是一个我平时几乎从不会去想的、网络调用最最开头的那一步——DNS 解析,以及它的"缓存"。原来,当我们的服务第一次用域名去调用那个下游时,要先做一次 DNS 解析,把"域名"翻译成"IP 地址",然后才能连过去。而这个"域名→IP"的解析结果,被我们的服务缓存了起来(为了避免每次调用都重新解析、提升性能)。问题就出在这:下游发布后 IP 变了,域名指向了"新 IP",可我们的服务还死死地用着缓存里那个"旧 IP",于是一直在尝试连接一台已经下线的旧机器——它当然连不上。而重启之所以能解决,是因为重启清空了那个 DNS 缓存,让服务重新解析了一次域名、拿到了新 IP。这篇文章,就从这次"下游换了 IP、我却还连着旧 IP"的事故讲起,把 DNS 解析这个"网络调用第一步、却最容易被忽视"的环节,以及它的缓存陷阱,讲清楚。

故障现场:连着一个已经不存在的 IP

先把这个"刻舟求剑"的过程理一理:

用域名访问下游的完整链路:
  1. 你的代码: client.call("http://downstream.example.com/api")
  2. 第一步(常被忽略): DNS 解析 —— 把 "downstream.example.com" 翻译成一个 IP
     比如解析出 10.0.0.5
  3. 然后才: 拿着 10.0.0.5 这个 IP, 去建立 TCP 连接、发请求

故障的时序:
  T0: 我们第一次调用, DNS 解析出下游 IP = 10.0.0.5, 并【缓存】了这个结果
  T1: 一切正常, 我们一直连着 10.0.0.5
  T2: 下游发布, 换了新机器, 域名 downstream.example.com 现在指向 10.0.0.8
      (旧的 10.0.0.5 那台机器已经下线)
  T3: 我们再调用 —— 用的还是缓存里的旧IP 10.0.0.5!
      可 10.0.0.5 已经下线了 → 连接超时 / 连接被拒
  T4: 重启我们的服务 → DNS缓存清空 → 重新解析得到新IP 10.0.0.8 → 恢复正常

看明白这个"刻舟求剑"的故障了吗?用域名访问一个服务,中间隐藏着一个我们平时几乎意识不到的关键步骤——DNS 解析:在真正建立连接之前,得先把"域名"这个人类友好的名字,翻译成"IP 地址"这个机器实际用来通信的地址。而域名访问最大的好处,本应是"IP 可以随时变,域名不变,调用方无感"——下游换 IP,只要更新一下域名的解析记录,调用方下次解析时就能拿到新 IP,自动连到新机器上。可这一切的前提是:调用方得"重新解析",才能拿到变化后的新 IP。而我们的服务,因为缓存了第一次解析的旧 IP、之后再也没重新解析,就成了那个"刻舟求剑"的人——船(下游 IP)已经移了,我还守着刻在船舷上的旧记号(缓存的旧 IP)去找,自然找不到了。

这个故障的迷惑性,在于它的"反直觉":从我们的视角看,我们的代码没动、网络没动、下游也是好的,凭什么连不上?因为出问题的,既不是我们、也不是下游,而是夹在我们和下游之间的那个"地址翻译"环节——它给我们的,是一个过期的、错误的翻译结果(旧 IP)。DNS 解析是网络调用链路上"第一个、也最容易被忽视"的环节;它平时默默工作、毫无存在感,可一旦它给你的解析结果"过期"了(因为缓存),你就会连向一个错误的地方,而且因为它太隐蔽,你排查时往往最后才会想到它。而"重启就好"这个特征,更是把人引向歧途——它让人误以为是"我们服务自己的什么状态出了问题",而真相是重启只是恰好清空了那个罪魁祸首的 DNS 缓存。

第一件事:理解域名访问的隐藏前提——DNS 解析 + 它的缓存

要避开这个坑,首先要把那个"隐藏的第一步"显式地装进脑子里:任何用"域名"发起的网络调用,在真正连接之前,都隐含着一次"DNS 解析"(把域名翻译成 IP);而这个解析结果,出于性能考虑,在很多层面都会被"缓存"——而缓存,就意味着"它可能不是最新的"。

DNS 解析结果, 可能被缓存在好几个层面(每一层都可能让你拿到旧IP):

  1. 应用层缓存: 比如 Java 的 InetAddress 默认会缓存解析结果
     (老版本JVM在某些配置下甚至"永久缓存"! 这是Java里最经典的DNS坑)
  2. 操作系统/本地 DNS 缓存: 系统的 nscd、本地 resolver 缓存
  3. DNS 服务器的缓存: 各级 DNS 服务器按 TTL 缓存解析记录
  4. DNS 记录的 TTL: 决定一条解析记录"多久后才该被重新查询"

  关键: DNS 设计了 TTL(生存时间)来控制缓存多久 ——
        理论上 TTL 到期后就该重新解析、拿到新IP。
        但如果某一层缓存"不尊重 TTL"(如 JVM 永久缓存),
        或缓存时间设得太长, 你就会长时间用着过期的旧IP。

关键认知是:"域名→IP"的解析结果是会被缓存的,而缓存的存在,意味着"你手里的这个 IP,可能已经不是这个域名当前真正指向的 IP 了"。 DNS 本身设计了 TTL(Time To Live,生存时间)这个机制来管理缓存——一条解析记录的 TTL 是多少秒,就表示它"应该被缓存多久、多久后该重新解析"。理论上,只要各层缓存都老老实实尊重 TTL,那 IP 一变、TTL 一到,大家重新解析就都拿到新 IP 了。可现实中,缓存常常"不那么听话":最臭名昭著的就是 Java 的 InetAddress——它默认会缓存 DNS 解析结果,而且老版本 JVM 在默认配置下,甚至会"永久缓存"成功的解析结果(完全无视 TTL)!这意味着一个 Java 服务一旦解析了某个域名,就可能"一辈子"用着那个 IP,直到重启——这正是我那次事故的元凶。所以,用域名做网络调用时,心里要装着这个隐藏前提:我连的那个 IP,是 DNS 解析来的、是被缓存的、是可能过期的;一旦下游 IP 会变(发布、扩缩容、故障转移),我就必须确保自己能"及时地重新解析、拿到新 IP",而不能死守着缓存里的旧 IP 刻舟求剑。

第二件事:正解之一——把 DNS 缓存时间配置合理

针对"JVM 永久缓存"这个最经典的坑,最直接的解法是:显式地把 DNS 缓存的时间(TTL)配置成一个合理的值,别让它"永久缓存"。这样,即便缓存了旧 IP,过了这个时间也会自动重新解析、拿到新 IP。

# 正解1: 配置 JVM 的 DNS 缓存 TTL, 别用永久缓存
# 在 java.security 文件里, 或代码里设置:
networkaddress.cache.ttl=30        # 成功解析的缓存时间, 设为30秒(别用-1永久)
networkaddress.cache.negative.ttl=10  # 失败解析的缓存时间

# 代码里设置(启动时):
java.security.Security.setProperty("networkaddress.cache.ttl", "30");

# 说明:
# - cache.ttl = -1   → 永久缓存(危险! 老JVM某些情况的默认值, 就是这个坑)
# - cache.ttl = 30   → 缓存30秒, 之后重新解析 → IP变了最多30秒后就能感知
# - cache.ttl = 0    → 不缓存, 每次都重新解析(最实时, 但解析开销大)

# 其它语言/系统层面也类似: 让 DNS 缓存有一个合理的、不太长的过期时间

这个方案的核心,是给 DNS 缓存设一个"合理的过期时间",在"实时性"和"性能"之间取个平衡。缓存时间太长(尤其是"永久缓存"),IP 变了你长时间感知不到(就像我那次);缓存时间太短(比如 0,每次都重新解析),又会让每次调用都多一次 DNS 解析的开销、增加延迟。一个常见的合理值是几十秒到几分钟——这样既享受了缓存带来的性能(几十秒内不用反复解析),又保证了 IP 变化后能在可接受的时间内(几十秒)被感知到。关键是:绝不要用"永久缓存"(JVM 的 cache.ttl=-1)。对长期运行、又依赖域名访问会变 IP 的下游的服务,显式地把这个 TTL 配成一个合理值,是必做的一项配置。我把"DNS 缓存时间"这个权衡画成图:

这张图的结论很明确:DNS 缓存时间,要避开"永久缓存"(感知不到变化)和"完全不缓存"(开销大)这两个极端,取一个"几十秒"级别的合理值——既享受缓存的性能红利,又能在 IP 变化后及时地、自动地感知并切换。这是应对 DNS 缓存陷阱最基础、也最该做的一步。

第三件事:正解之二——用专门的服务发现,而非裸 DNS

配置 DNS 缓存时间能缓解问题,但在"下游 IP 会频繁、动态变化"的现代微服务环境里(实例随时扩缩容、随时上下线),单纯依赖 DNS 来感知这些变化,是有点力不从心的(DNS 的设计初衷,本不是为了应对秒级、频繁的 IP 变动)。这时,更专业的方案是引入服务发现(Service Discovery)

# 正解2: 用服务发现, 实时感知下游实例的上下线和IP变化

  裸 DNS 的局限:
    - 解析结果靠 TTL 过期才更新, 对"实例频繁上下线"反应不够实时
    - 一个域名通常只给你一个(或一组)IP, 缺乏健康检查、负载均衡等能力

  服务发现(Nacos/Consul/Eureka/K8s Service 等)的做法:
    - 下游每个实例启动时, 向"注册中心"注册自己(IP、端口、健康状态)
    - 实例下线/故障时, 注册中心实时摘除它
    - 调用方从注册中心实时拿到"当前健康的实例列表", 而非靠DNS缓存
    - 还自带: 健康检查、负载均衡、实例变化推送/订阅
  → 下游实例一变化, 调用方能近乎实时地感知, 不会再连旧实例

# K8s 环境: Service + DNS 已经帮你处理了大部分, 但也要注意客户端缓存

服务发现,是为"动态变化的分布式环境"而生的、比裸 DNS 更专业的"找服务"方案。它的核心区别在于"实时性"和"主动性":裸 DNS 是"被动、靠缓存过期才更新"的——你解析一次、缓存一段时间,IP 变了得等缓存过期才能重新感知;而服务发现是"主动、实时同步"的——下游的每个实例,在上线时主动向注册中心"报到"、下线时被实时"摘除",调用方则从注册中心实时地拿到"当前还活着的实例列表"。这样,下游实例一发生变化(扩容、缩容、故障下线),调用方几乎能立刻感知,绝不会再去连一个已经下线的旧实例。此外,服务发现通常还自带健康检查、负载均衡、变更推送等能力,这些都是裸 DNS 给不了的。所以,在下游实例会动态、频繁变化的微服务架构里,优先用专门的服务发现组件(Nacos、Consul、Eureka、或 K8s 的 Service 机制)来定位下游,而不是裸用域名 + DNS——前者能从根本上、实时地应对实例的动态变化,而后者的 DNS 缓存,在这种场景下就是个隐患。(不过要注意,即便用了 K8s Service 这种基于 DNS 的服务发现,客户端的 DNS 缓存问题依然要留意。)

第四件事:DNS 不止"缓存旧 IP"一个坑

顺着这次事故,我把 DNS 相关的坑梳理了一遍,发现"缓存旧 IP"只是其中之一,还有几个同样会咬人的:

DNS 的其它高发坑:

坑1: DNS 解析"慢", 拖累整个请求的耗时
  解析也是要花时间的(尤其要走外部DNS服务器时)。如果DNS解析慢/抖动,
  你的每次网络请求都会被这"第一步"拖慢 —— 而且很多人排查请求慢时, 根本想不到是DNS。
  → 缓解: 合理缓存(别每次都解析)、用稳定快速的DNS、监控DNS解析耗时

坑2: DNS 解析"失败", 直接导致请求失败
  DNS服务器挂了/网络到DNS不通, 解析就失败, 你连IP都拿不到, 请求直接GG。
  → 防护: DNS服务器要高可用/多个; 对解析失败也要有合理的超时和重试

坑3: 长连接会"绕过"DNS的刷新
  如果你和下游用的是长连接(连接复用), 那连接一旦建立, 就一直用着当初那个IP,
  即便DNS缓存过期、解析出了新IP, 那条已建立的长连接还是连着旧IP!
  → 下游IP变了, 已有的长连接不会自动切换, 要靠连接的健康检查/失败重连来更新

坑4: 一个域名多个IP时的负载均衡/故障转移
  域名可能解析出多个IP(一组实例)。客户端怎么在这几个IP间做负载均衡、
  某个IP挂了怎么自动切到其它IP —— 这些裸DNS不一定帮你处理好。

这几个坑提醒我们,DNS 这个"网络第一步",影响的远不止"连对还是连错 IP":它慢,会拖累你每个请求的延迟;它失败,会让你的请求直接挂掉;而它和长连接、负载均衡的相互作用,又会衍生出更多隐蔽的问题。尤其是坑3(长连接绕过 DNS 刷新)特别值得一提——它和我之前讲的"用长连接复用"形成了一个微妙的矛盾:长连接为了性能复用着同一条连接,可这条连接"绑死"在了建立时的那个 IP 上,即便 DNS 后来解析出了新 IP,这条老连接也不会自动切换,得靠连接本身的健康检查、超时断开、失败重连,才能在下次重连时用上新 IP。所以,完整地应对"下游 IP 变化",不仅要管好 DNS 缓存,还要确保连接层有健康检查和失败重连机制,让已有的、连着旧 IP 的长连接,在旧 IP 失效后能被及时地断开、重连到新 IP。把 DNS 解析结果可能被缓存的几个层次整理成一张表:

缓存层次 谁在缓存 怎么控制
应用层 JVM InetAddress / 客户端库 配 networkaddress.cache.ttl 等
操作系统 nscd / systemd-resolved 等 系统 DNS 缓存配置
本地 resolver 本机 DNS 解析器 resolv.conf / 本地缓存
DNS 服务器 各级 DNS 服务器 受 DNS 记录的 TTL 控制
已建立的长连接 连接本身(绑死旧IP) 健康检查 + 失败重连

第五件事:学会排查 DNS 问题

DNS 问题之所以难,一半在于"想不到是它";一旦想到了、会用工具去查,它其实并不难定位。我把排查 DNS 问题的常用工具和思路整理成一张表:

排查动作 命令/方法 看什么
查域名当前解析到哪个IP nslookup / dig 域名 域名真正指向的IP, 和你连的对不对
看记录的 TTL dig 域名(看 TTL 字段) 这条记录该缓存多久
看你的服务实际连的IP netstat / ss 看连接 你连的IP是不是已经是旧的
对比"解析IP"和"实连IP" dig 结果 vs ss 结果 两者不一致 = DNS缓存问题铁证
测DNS解析耗时 dig 看 Query time 解析是不是慢, 拖累请求

这张表里,最能"一锤定音"的是那个对比:dig/nslookup 查出"这个域名现在真正解析到哪个 IP",再用 ss/netstat 看"我的服务实际连着哪个 IP"——如果这两个 IP 对不上(域名现在指向新 IP,而我还连着旧 IP),那就是 DNS 缓存问题的铁证。我那次事故,如果一开始就做这个对比,就能立刻定位,而不必绕一大圈。所以,记住这个排查 DNS 问题的核心动作:把"域名当前该解析到的 IP"和"你的服务实际连着的 IP"对比一下——它们一致,DNS 没问题;不一致,十有八九就是缓存让你连了旧 IP。同时,把 DNS 解析这一步,也纳入你对网络调用的认知地图:排查任何"连接类"问题(连不上、连得慢、连错了)时,都别忘了回到最开头去看一眼那个最容易被遗忘的环节——DNS 解析,它给你的地址,对不对、新不新、快不快。

一张"DNS 相关问题怎么查、怎么防"的决策图

把这次踩坑沉淀成一张图。遇到"连不上/连错/连得慢"的诡异问题,或设计依赖域名的调用时,照着它走:

这张图的核心,是那个一锤定音的对比——dig 出"域名现在该连的 IP" vs ss 看"我实际连的 IP",不一致就是 DNS 缓存问题。确诊后,按"配 TTL、上服务发现、长连接加重连"去根治。把"连接问题先看一眼 DNS"这个动作加进你的排查直觉,这类隐蔽的坑就能快速现形。

我立下的几条 DNS / 服务定位规矩

这次"连着旧 IP"的事故后,团队的规范里加了这么几条:

  1. 显式配置 DNS 缓存 TTL:长期运行的服务显式配置合理的 DNS 缓存时间(几十秒),绝不用永久缓存(JVM 的 -1)。
  2. 动态环境用服务发现:下游实例会频繁动态变化的微服务架构,用 Nacos/Consul/Eureka/K8s Service 等服务发现,别裸用域名+DNS。
  3. 长连接要有健康检查重连:用长连接的客户端配健康检查和失败重连,保证旧 IP 失效后能断开重连到新 IP。
  4. 下游发布要考虑调用方感知:下游换 IP 发布时,评估调用方多久能感知到(DNS TTL / 服务发现实时性),避免调用方长时间连旧 IP。
  5. 排查连接问题先验 DNS:遇到连不上/连错/重启就好,先 dig 域名 + ss 看实连 IP 做对比,快速排除 DNS 缓存问题。
  6. 监控 DNS 解析:把 DNS 解析的成功率、耗时纳入监控,别让 DNS 慢/失败成为看不见的请求杀手。
  7. 别忘了网络第一步:分析网络调用的延迟和失败时,把"DNS 解析"这一最容易被忽略的第一步也纳入考量。

这几条里,第五条"排查连接问题先验 DNS"是最实用的应急手段。而我最大的收获,其实是认识到一个更普遍的盲区:我们在分析一个网络调用时,注意力几乎全在"建立连接之后"的事情上——连接快不快、请求对不对、响应慢不慢;却几乎完全忽略了"建立连接之前"那个静默的第一步——DNS 解析。而恰恰是这个被忽略的第一步,决定了你到底连向哪里、连得快不快、能不能连上。我那次绕了一大圈才查到 DNS,根源就是它从一开始就不在我的"排查雷达"里。所以,把"DNS 解析"这个隐藏的第一步,郑重地加进你对"一次网络调用"的完整认知里——一次调用,不是从"建立连接"开始的,而是从"把域名翻译成 IP"开始的;而很多诡异的连接问题,答案就藏在这个最容易被遗忘的起点上。

写在最后:最容易被忽略的环节,往往藏着最隐蔽的坑

这次 DNS 缓存的经历,给我一个深刻的提醒:在一条由很多环节串起来的链路里,那些"平时默默工作、毫无存在感、我们几乎从不会去注意"的环节,恰恰最容易藏着最隐蔽、最难排查的坑。 DNS 解析就是这样一个环节——它在每一次域名访问里都默默地工作着,可它太"理所当然"了,以至于我们写代码、排查问题时,根本不会想到它的存在;而正是这份"被忽略",让它出问题时格外难被发现。我们的注意力,总是被那些"显眼的、我们亲手写的"环节(业务代码、SQL、接口逻辑)吸引,而对那些"隐形的、自动发生的"环节(DNS 解析、连接建立、序列化、GC……)视而不见——可坑,偏偏最爱长在这些视野的盲区里。

想通这一点,我对"排查问题"和"理解系统"有了一个更全面的视角:要真正掌控一个系统,不能只盯着那些"显眼的、你写的"部分,更要主动地把那些"隐形的、自动的、你没写但确实在发生"的环节,也纳入你的认知地图。一次网络调用,完整的链路是"DNS 解析 → 建立连接 → 发送请求 → 接收响应",而不只是你写的那行 client.call();一次数据库查询,完整的链路是"连接池取连接 → 发送 SQL → 数据库执行 → 返回结果 → 映射成对象",而不只是你写的那行 ORM 方法。把这些隐形环节都画进你的认知地图,你才拥有了"完整地看到一条链路"的能力——而这种能力,正是快速定位那些藏在盲区里的隐蔽问题的关键。因为你没法排查一个你"根本没意识到它存在"的环节;只有当 DNS 解析、连接建立这些环节,都清清楚楚地在你的脑海地图里时,你才能在出问题时,顺着这张完整的地图,一个环节一个环节地查下去,而不会因为"漏看了某个隐形环节"而绕一大圈、甚至彻底卡住。

所以,如果你也想成为一个能快速定位疑难杂症的工程师,我想把这次踩坑最想说的话送给你:主动地去补全你的"系统认知地图",尤其要把那些平时被你忽略的、隐形的、自动发生的环节,一个个地画上去。了解一次网络调用,就别漏了 DNS 解析这第一步;了解一次数据库操作,就别漏了连接池和 SQL 执行;了解任何一条链路,都努力看到它"完整"的样子,而不只是你亲手写的那一小段。因为最难查的 bug,往往不在你反复审视的那些代码里,而在你从未留意过的那些环节里;而能不能想到去那些环节里看一眼,取决于它们在不在你的认知地图上。那个静默地连着旧 IP 的 DNS 缓存,最终教给我的,正是这份"把隐形环节也纳入视野"的全局观——它让我明白,真正的排查高手,不是查得多快,而是"想得多全"、视野里没有盲区。愿你我都能不断补全自己的认知地图,让那些藏在盲区里的隐蔽坑,再也无处可藏。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

订单列表接口越来越慢、代码里明明只查一次,打开 ORM 的 SQL 日志却刷出几百条 SQL:数据库 N+1 查询问题的避坑复盘

2026-6-1 17:24:21

技术教程

容器设了 2GB 内存上限服务却反复被 OOMKilled、可 JVM 堆明明没满:JVM 不感知容器限制按宿主机算堆撑爆容器的避坑复盘

2026-6-1 17:36:42

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索