一、问题背景
公司核心交易链路里有一批微服务(跑在 K8s 上,也有部分裸金属虚拟机),它们调用多家三方能力:支付网关、短信通道、地图地理编码、风控评分等。这些三方域名全部走公网域名解析,集群内的 /etc/resolv.conf 统一指向自建内网 DNS 转发器(基于 CoreDNS,上游为两个运营商递归 + 一个公共递归 119.19.19.19)。这套解析体系已经稳定运行一年,平时解析成功率在 99.9% 以上,P99 解析延迟 < 30ms。
变故出现在一次大促前的压测期间。订单服务在压到日常 3 倍流量时,监控里开始零星冒出上游 504(Gateway Timeout),每天约几十笔,分散在不同时段、不同节点,单笔失败率不到 0.1%,但恰好都集中在“调用三方 API”的环节。因为量太小、又不可复现,前两周一直被当成“三方接口抖动”挂了外部工单,直到大促临近,我们决定把它彻底查清楚。
二、故障现象
现场能串起来的现象有四条,且都指向“解析”而非“后端慢”:
- 业务层偶发 504:订单服务调支付/短信时,HTTP 客户端抛
upstream connect error or disconnect/reset before headers或504,但后端真实接口监控一切正常,三方厂商侧也查不到对应请求日志——说明请求压根没出公司网络。 - 应用侧报解析失败:在出问题的 Pod 里手动
curl -v https://api.sms-provider.com,约 3%~5% 概率直接返回curl: (6) Could not resolve host: api.sms-provider.com;同一个域名重试一次又好了。 - 只在特定时段更频繁:压测或晚高峰(出口带宽吃紧时)失败率明显上升,闲时几乎为 0。这暗示问题跟“链路质量/拥塞”有关,而非单纯配置错。
- 并非所有域名都中招:公司自有的
*.internal和内网服务发现域名从不失败,只有走公网递归的三方域名会偶发失败——范围被天然缩小到“内网 DNS 转发器 → 公网递归”这一段。
最关键是:它是间歇性的,单看任何一时刻的监控都不算“故障”,必须靠抓包和统计才能暴露规律。
三、排查过程
1. 先确认是不是应用端的问题
先排除应用自身:在出问题的节点上循环 curl 一个三方域名 200 次,统计失败率,同时开着应用日志对照。结果是 curl 本身的失败率(4.1%)与应用层 504 的失败率高度吻合,且 curl 失败全是 Could not resolve host。基本锁定:问题在 DNS 解析环节,不在业务代码、不在 LB、不在三方接口。
2. 抓包看 DNS 到底丢没丢
在节点上 tcpdump 抓 DNS 流量,复现失败那一刻:
|
|
用 Wireshark 打开 pcap,现象非常清晰:约 4% 的 DNS 查询发出后,在 5 秒超时(转发器 timeout 设的 5s)内没有任何响应返回,然后客户端重试一次才成功。 也就是说,内网 DNS 把请求转给上游后,上游“哑了”。
3. 定位到“内网 DNS → 上游”这一段
直接用 dig 把枪口对准转发器上游,连续探测并统计:
|
|
结果:Query time 在 8ms~200ms 之间抖动,但有约 4% 的探测根本没有 Query time 行(即超时无响应),与业务失败率吻合。换成另一个运营商上游,失败率类似但略低(2%)。结论:两条上游链路都偶发丢包,且转发器只有这两条、无备用、无快速失败切换。
4. 为什么“偏偏是某些域名”更容易丢
进一步对比:对 api.sms-provider.com 这类容易失败的域名,和 www.baidu.com 这类几乎不失败的域名,分别用不同 bufsize 探测:
|
|
差异显著:+noedns 时失败率几乎归零;+bufsize=4096 时失败率飙到 8% 以上。再结合 dig +trace 看到该域名返回了大量 A/AAAA 记录且开启了 DNSSEC,响应包体积常超过 1200 字节。
根因线索浮现:内网 DNS 转发器开了 EDNS0,向运营商递归请求大响应;但中间某一跳(运营商出口或公司边界防火墙)会对 UDP 53 上 >512 字节的包做分片/限速/丢弃,导致大响应在回程被吃掉。UDP 本身无重传,于是这次查询“凭空消失”,只能等转发器 5s 超时后客户端重试——而重试往往走不同路径或不同上游,所以“重试一次就好了”。
5. 验证 TCP fallback 能否救场
EDNS0 大包在 UDP 上被丢,理论上 DNS 应自动降级到 TCP 53 重传。但实测发现转发器对超时的处理是“傻等超时再让客户端重试”,自己并不会主动切 TCP 或切备用上游;且公司边界防火墙默认只放行了 UDP 53,没放行 TCP 53,导致即使想 fallback 也连不上。用 dig +tcp @上游 域名 验证:TCP 路径 100% 成功,彻底坐实“UDP 大包被丢、TCP 正常”的判断。
四、解决方案
1. 立刻止血:收敛 EDNS0 包体积 + 多上游
修改 CoreDNS 转发配置(Corefile),把 EDNS0 缓冲区压到 MTU 安全值 1232,并配置多上游 + 合理超时:
|
|
bufsize 1232 是 1500 MTU 减去 IP/UDP 头后的安全上限,确保响应不需要 IP 分片;timeout 2s + 多上游让单条链路丢包时快速切走,而不是干等 5 秒。
2. 放开 TCP 53,启用 fallback
在边界防火墙放通 DNS 的 TCP 53(不只是 UDP),并在转发器层面确认大响应超 UDP 时能走 TCP 重传。验证:
|
|
3. 客户端兜底:缩短超时与重试
在节点 resolv.conf 上加保守参数,避免单点 DNS 抖动拖垮整个调用:
|
|
K8s 场景则通过 kubelet --resolv-conf 或 Pod dnsConfig 下发同样的 options,让解析失败快速重试到另一个转发器实例。
4. 关键三方域名本地缓存兜底
对支付、短信这类核心且域名固定的三方,在 CoreDNS 用 hosts 或 file 插件做本地静态兜底,即便公网递归全挂也能解析,把“外部依赖”降级为“本地可控”。
五、根因分析
本质是**“单点上游 + EDNS0 大包在中间网络被丢弃 + 缺乏快速失败/备用链路”**三者叠加。内网 DNS 转发器开启了 EDNS0 并请求大响应,而运营商出口/边界防火墙对 UDP 53 上的大包做了分片或限速丢弃;UDP 无重传,导致约 3%~5% 的查询在回程“凭空消失”。转发器又只有单/双上游、超时长达 5 秒且不主动切 TCP/备用,于是把“一次解析超时”放大成了“应用侧 5 秒白等 → 上游 504”。失败只有在“出口拥塞或大响应域名”时才显著,因此呈现出难复现的间歇性特征。问题不在三方接口、不在业务代码,而在 DNS 转发链路的一条隐蔽 UDP 路径。
六、预防措施
- DNS 高可用:转发器至少配 2~3 个不同运营商/公共递归上游,开启
health_check与健康剔除,单链路丢包时秒级切走;转发器本身也部署多实例 + Service 负载均衡。 - 控制 EDNS0 包体积:
bufsize统一设为 1232,从源头规避 UDP 分片被丢;同时放通 TCP 53,保证大响应能走 TCP 可靠传输。 - 监控解析质量而非只看“能通”:用 Prometheus + blackbox_exporter 对核心三方域名做持续 DNS 探测,把
probe_success、解析延迟 P99 接入告警,对失败率 > 1% 即时报警,而不是等业务 504 才察觉。 - 客户端防御性配置:
resolv.conf设timeout:1 attempts:2 rotate,缩短单点抖动的影响窗口;容器场景统一通过dnsConfig下发。 - 关键域名本地兜底:核心三方域名用
hosts/file插件做静态解析兜底,外部递归全断也不影响主链路。 - 复盘中补充的边界项:防火墙策略变更必须同步 DNS 的 TCP/UDP 双端口放行,避免“放 UDP 忘 TCP”这类隐性单点。
七、总结
这次故障的典型之处在于:业务报的是 504,根因却藏在 DNS 转发器一个 UDP 包被丢的瞬间。它不报错、不宕机,只在出口拥塞或大响应域名上偶发,单看监控谁都像“无辜”。给运维的启示是——遇到“三方接口偶发超时、但对方查无此请求”的现象,要敢于从应用层一路下沉到 DNS/UDP 链路,用 tcpdump + dig +stats 把“丢包率”这项指标量化出来;同时 DNS 这种“看似简单”的基础设施,恰恰最该做多上游、控包体、开 TCP、配监控,别让一条被丢弃的 UDP 53 包,悄悄拖垮整条交易链路。