域名解析缓存带来的隐性故障,很多排查都漏掉了这一层

前段时间线上碰到一个很奇怪的故障:我们更换了第三方服务商的域名解析记录,把域名切到新的后端地址。一部分流量很快切过去了,但还有一部分机器,隔了大半天还在往老 IP 发请求,持续报连接失败。第三方那边确认解析已经生效,公共 DNS 也已经刷新,一开始怀疑是我们服务内部缓存了 IP,翻代码却没找到硬编码 IP 的地方。

查了很久才定位清楚,问题出在操作系统和 JDK 内置的 DNS 缓存上,这一层经常被大家忽略。

很多人的固有认知是,改完域名的 A 记录,TTL 到期之后所有客户端就自动刷新解析。这个逻辑只适用于递归 DNS 服务器本身。业务服务器作为解析客户端,本身还会有一层缓存。以 Linux 为例,默认不一定开启 nscd 或者 systemd-resolved,但应用运行时的 Runtime,往往自带独立 DNS 缓存,不跟着系统走。

就拿 JVM 来说,它的 DNS 缓存策略很容易踩坑。默认情况下,成功的解析结果会永久缓存,只有解析失败的时候才会短时间过期。也就是说,域名第一次解析拿到老 IP 之后,只要没有解析报错,哪怕上游 DNS 记录早就变了,JVM 进程会一直拿着旧 IP,进程不重启就不会更新。这就是这次故障最核心的原因。机器上的 nslookup 早就拿到新 IP 了,但是 Java 应用内部的缓存完全没刷新,继续访问旧地址。

除了 JVM,Go、Python 这些语言行为又不一样。Go 标准库默认不做长期 DNS 缓存,每次请求都会向系统 resolver 发起查询;Python 则依赖系统配置,如果开了 nscd,缓存时长由 nscd 控制。不同语言的差异很大,同一个域名,同一台机器上,不同服务解析出来的 IP 可能不一样,排查的时候很容易混乱。

还有一个坑是容器环境。很多业务跑在 K8s Pod 里面,容器里的 /etc/resolv.conf 配置了集群 DNS。集群 DNS 本身有缓存,加上节点层面的缓存,叠加之后实际生效的缓存时间,往往比域名设置的 TTL 要长。有时候域名 TTL 设成 60 秒,结果集群 DNS 缓存了几分钟,Pod 内的应用拿到的依旧是旧记录。更麻烦的是,很多 Pod 是长期运行的,不会频繁重建,缓存就一直保留。

另外就是 HTTP 客户端连接池的叠加效应。就算解析已经拿到新 IP,老的连接还在连接池里面活着。HttpClient、OkHttp 这类组件,会复用已经建立好的长连接。哪怕 DNS 已经刷新,只要旧连接没到空闲超时,请求依旧走老连接到老 IP。这就会出现一种现象:一部分请求走新 IP 正常,一部分请求走老 IP 报错,错误是断断续续的,不是全量失败,故障特征非常迷惑。当时我们就是一边有成功请求、一边报错,很长一段时间没往连接池方向想。

还有公共 DNS 的问题,很多测试的时候用手机、本地电脑 dig,看到解析更新了,就直接认为全量没问题。但线上服务器用的是内网 DNS、自建递归 DNS,和外面的公共 DNS 不是一套体系,刷新节奏完全独立。本地验证通过,不等于线上服务器的 DNS 已经同步。

这次故障处理完之后,我们梳理了几种处理手段,也划定了边界。

第一种,紧急止血。对于 JVM 应用,最简单粗暴的方式就是滚动重启实例,清空进程内的 DNS 缓存,适合紧急故障。但重启不是优雅方案,不能每次改解析都重启服务。可以配置 networkaddress.cache.ttl 参数,把成功解析的缓存时间缩短,和域名 TTL 对齐,从根源避免永久缓存。

第二种,区分 DNS 缓存和连接池缓存。DNS 刷新之后,还需要等待连接池空闲连接释放,或者主动关闭旧连接。如果是切换机房、切流量这种重要变更,除了等 TTL,还要评估客户端连接池的空闲超时时间,预留对应的等待窗口。

第三种,变更前做验证,不能只在本地 dig。要登录业务所在的 Pod、服务器,用应用同环境下的工具去解析,而不是只在宿主机或者自己电脑上验证。同时分开验证:DNS 解析结果、建立 TCP 连接、业务接口调用三层,逐层确认。

还有一个长期方案:核心外部依赖尽量不要直接裸域名调用,用服务发现或者网关收口。域名变更只改网关一层,下游业务不需要感知后端 IP 变化,从源头规避 DNS 缓存带来的批量切换风险。域名解析本质上是一层基础依赖,一旦链路长、缓存层级多,变更的实际生效时间很难精准控制。

很多时候排查网络类故障,大家第一反应看防火墙、看端口、看中间件配置,DNS 缓存这种底层环节,优先级放得很低。它不像超时、报错那样直接明显,故障往往是渐进式、分片式的,等到发现的时候,已经持续很久了。

相关推荐
wuminyu1 小时前
JVM执行引擎学习总结:热点字节码编译成本地机器码
java·linux·c语言·jvm·c++
真上帝的左手1 小时前
2. 编程语言-Java-Spring Boot-JAR
java·spring boot·jar
Z21-X1 小时前
云原生运维实战笔记:华为云计算与网络类服务全景解析
运维·笔记·学习·云原生·华为云
Sayai1 小时前
Elasticsearch ingest pipeline 被禁用之后:性能代价、安全治理与数据入库时间的四种替代方案
大数据·运维·安全·elasticsearch·搜索引擎·性能优化
Jack_hsfs1 小时前
第6章Spring 管理的是对象,还是对象的说明书?
java·后端·spring
kiros_wang1 小时前
通用工具模块封装:防抖节流、参数校验公共工具类
linux·运维·ubuntu
Android系统攻城狮1 小时前
Linux FFmpeg深度解析之avcodec_default_get_buffer2调用流程与实战(二十)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频
深圳恒讯1 小时前
香港云主机硬盘满了怎么办?Linux和Windows在线扩容操作教程
linux·运维·windows
饭后一颗花生米1 小时前
GitHub Actions 自动化:5 个真实运维提效案例
运维·自动化·github