先聊一个很多做服务端的朋友都遇到过的场景:业务上线后压测数据很漂亮,但真实流量一进来,偶尔会出现白屏、接口两三秒才响应。抓包一看,请求本身处理得很快,问题全出在“域名解析等待”上。这类故障并不偶发,流量越大越容易被放大。我处理过一起线上事故,登录接口在某个时段集体超时,查了数据库、缓存、网关都没问题,最后才发现是应用服务器的底层 DNS 解析请求全部打到同一台公共递归出口上,瞬间排队,原本几十毫秒的解析被拖成了好几秒。
那次之后我把“架构之 DNS 解析优化”这件事彻底重新梳理了一遍。它不只是把解析速度从 100ms 压到 5ms,而是让整个系统对 DNS 的依赖降到最低,同时对无法避免的解析过程做兜底。这篇内容适配正在做服务端架构、中间件开发、SRE 或运维的朋友,尤其适合流量增长后频繁遭遇“响应慢但不知道慢在哪”的团队参考。
1. 先把 DNS 慢的根因看清楚:一次解析请求完整走过的链路
1.1 从一次线上事故说起:慢不是原因,而是结果
那次故障的典型表现是:某个模块在高峰期出现大量连接超时,CPU、内存、带宽指标都正常,但错误率飘红。最开始怀疑是数据库连接池被打满,看了慢查询也没有明显异常。随后在服务器上抓网络包,看到大量的 DNS 查询请求堆积在同一个 socket 上,有的查询重复发了好几遍,最终超时之后应用层才抛错。
这个现象很有代表性。大多数业务系统不会直接把 DNS 查询当成瓶颈来监控,因为它属于系统底层行为,平时毫秒级完成,根本感知不到。可当出口解析服务器出现抖动、丢包或者排队时,所有新建连接都卡在同一条路上。而应用层的连接池如果没有做好复用,每个线程每次新建连接都可能触发新的解析,等于把压力又放大了几倍。
从那次之后,我在复盘时定下了一条原则:DNS 解析在架构里属于高杠杆因素,表面占比小,但一旦出问题,影响的是全量新建连接。优化它的第一步不是急着调配置,而是先搞清楚一条解析请求到底是怎么走完的。
1.2 一个域名从发起解析到拿到 IP,一共要过几道关卡
以一台应用服务器请求api.example.org为例,完整的解析链路通常是这样的:
- 应用进程调用解析函数,先查本地 DNS 缓存(操作系统级)。
- 缓存未命中,再查看本机 hosts 文件。
- 还没有,就组装查询报文发送给这台机器配置的递归解析服务器。
- 递归服务器收到请求后,如果自己有缓存就直接返回;没有缓存,就依次去查根域名服务器、顶级域服务器、权威域名服务器。
- 拿到最终 IP 后逐级返回,并在每一级按 TTL 缓存一段时间。
你以为只有“递归查权威”这一段耗时间?其实前几步也可能出问题。比如某些语言运行时和网络库自带一层缓存,缓存策略跟操作系统不一致;又比如部分框架默认不做连接复用,导致每次请求都新建连接,每新建一条连接就触发一次系统调用级别的解析。这些细节叠加起来,最终延迟分布图上就会多出一条长长的尾巴。
1.3 解析过程中真正的耗时点在哪儿
多数情况下,热点缓存命中时解析时间是 0~1ms,本地系统缓存命中时也是亚毫秒级。真正耗时的场景有三个:
- 递归服务器缓存未命中,需要迭代查询,常见耗时在 20~200ms 之间。
- 递归服务器本身负载高或网络链路抖动,查询会出现重传和超时。
- 应用侧没有缓存或缓存失效机制异常,导致同一域名在短时间内被重复查询。
所以优化的核心顺序就出来了:先用缓存和连接复用把高频查询降下来,再优化剩余的、不得不发出去的查询质量,最后在权威解析侧做好高可用兜底。下面几个章节我会按照这个顺序逐层展开。
2. 架构层面先动刀:缓存体系和连接复用是见效最快的两招
2.1 连接复用:让 DNS 请求从“高频重复”变成“低频偶发”
很多团队在优化 DNS 时第一反应是改 TTL,但往往忽略了一个前置问题:应用层到底在频繁创建多少新连接?如果 HTTP 客户端每次请求都新建 TCP 连接,那每一次 TCP 握手都可能伴随一次 DNS 解析,连接池形同虚设,TTL 再长也拦不住重复查询。
我实际验证过,一个每秒处理 2000 请求的服务,如果连接不复用,每秒就是几千次解析请求;加上合理的 keep-alive 连接池之后,长连接维持在几十到几百条,DNS 查询频率断崖式下降。操作系统会缓存解析结果,但系统缓存的 TTL 是有限的,真正把请求次数降下来,靠的还是复用机制。
落地时建议做三件事:
- 在网关、负载均衡层开启长连接,并且把后端空闲超时时间调得比客户端空闲超时更长。
- 业务代码里的 HTTP 客户端统一走连接池,设置合理的最大连接数、空闲存活时间。
- 对不支持连接池的老代码做改造时,至少在进程内维护一层域名解析缓存,过期时间与权威解析的 TTL 对齐。
如果你在服务器上看到大量 DNS 查询报文,先别急着怀疑公共解析服务,先把应用层的连接复用指标查一遍。我遇到过不止一次,优化完连接池之后,公共解析负载压力直接减半。
2.2 TTL 既能救你也能坑你:不同记录类型该给多大值
TTL 是域名记录在各级缓存里的存活时间。它平衡的是“解析灵活性”和“缓存命中率”:TTL 越短,IP 变更后生效越快,但各级缓存命中率越低,递归服务器压力越大;TTL 越长,缓存效果越好,但一旦节点故障或者做容灾切换,客户端可能还盯着旧 IP 不放,故障恢复时间被拉长到和 TTL 一个量级。
我通常按业务属性把域名分成几类来管理:
| 记录类型 | 典型场景 | 推荐 TTL 范围 | 说明 |
|---|---|---|---|
| 高动态记录 | 全局负载均衡、需要快速容灾切换的服务 | 30~60 秒 | 配合健康检查,快速摘除故障节点 |
| 普通业务域名 | 内部服务、常规 HTTP 接口 | 60~300 秒 | 兼顾缓存效率和切换速度 |
| 稳定基础设施 | 数据中台、消息队列地址 | 300~600 秒 | 变更频率极低,可以适当放宽 |
| 静态资源域名 | CDN、对象存储 | 600~3600 秒 | 资源节点多,单点故障影响有限 |
这里有个容易踩的坑:不是所有解析服务都严格遵循你设置的 TTL,部分公共递归服务会有最小缓存时长限制,小于这个值的 TTL 会被强制拉长。所以如果你计划做快速切换,不要只依赖调低 TTL,还需要在业务侧预留主动刷新机制。
2.3 浏览器和应用客户端里的 DNS 预解析怎么安排
对 Web 架构来说,减少 DNS 等待的另一个有效手段是预解析。浏览器支持通过资源提示提前解析后续要访问的域名,移动端 App 也有对应的预查询接口。
网页端常见的做法是在 HTML 里声明:
<link rel="dns-prefetch" href="//static.example.org"> <link rel="preconnect" href="//api.example.org">dns-prefetch的作用是在页面加载早期就把域名解析好,后续请求实际发起时省去等待。preconnect更进一步,不仅解析,还会提前完成 TCP 握手和 TLS 协商。注意,对跨域的第三方资源,尽量只用dns-prefetch而不要滥用preconnect,否则可能建立一堆用不上的连接。
App 端更直接一些,在启动流程或者网络初始化阶段,把常用域名提前解析并缓存到内存里,后续所有请求直接复用这份结果。唯一的提醒是:预解析缓存必须设置过期时间,并且要在网络切换时主动失效,不然会出现“缓存里还是旧 IP,而权威解析已经切到新节点”的错位。
3. 自建本地 DNS 缓存:当公共解析成为瓶颈时的标准解法
3.1 判断要不要自建递归缓存的三个条件
公共 DNS 服务大部分时间都是稳定的,但它有一个架构上绕不开的问题:你无法控制它的容量、缓存策略和路径。当你的业务出现下面三种情况时,就该考虑在自建侧加一道缓存:
- 服务器到公共解析服务的链路经常出现延迟抖动或丢包,抓包能看到大量重传。
- 业务对解析成功率要求极高,不能容忍外部服务的偶发故障影响新建连接。
- 服务器规模大,每秒 DNS 查询总量很高,公共解析服务已经出现限流或排队。
自建本地缓存不是要替代权威解析,它只是把“递归查询”这一步放到离业务最近的地方。真正上游的根域、顶级域、权威域查询,依然由你配置的上游解析服务完成。
3.2 一套最小可用配置的落地示例
这里以一款轻量级递归缓存软件为例,它的配置思路和绝大多数同类方案一致。先安装,然后修改核心配置文件:
# 监听本机回环地址和容器网段地址 interface: 127.0.0.1 interface: 10.0.0.1 # 允许哪些网段来查询 access-control: 127.0.0.0/8 allow access-control: 10.0.0.0/8 allow # 上游递归解析地址,可以配置多个做负载 forward-addr: 1.1.1.1 forward-addr: 8.8.8.8 # 缓存兜底时间,防止极端情况下的全量回源 cache-min-ttl: 30 cache-max-ttl: 300 # 开启缓存预取,临近过期时提前刷新 prefetch: yes prefetch-delay: 3prefetch是很多人忽略的功能。它的意思是当某条记录快过期时,后台提前向上游刷新,而不是等客户端查到才被动刷新。开启之后,热域名的解析时间会更稳定,不会周期性出现尖刺。
3.3 部署方式与上线后的观察项
我推荐先在单台应用服务器上做灰度验证,把/etc/resolv.conf里的 nameserver 指到本机缓存,观察两三天。确认稳定后再批量推广。上线后重点观察四个指标:
- 查询命中率:正常情况下应该持续稳定,不出现大幅波动。
- 平均解析耗时:命中缓存时应低于 1ms。
- 上游解析请求量:自建缓存生效后,这一项应大幅下降。
- 重启后的冷缓存表现:一批服务器同时重启时,会不会对上游造成瞬时压力。
这里有个教训:若上千台机器同时重启,本地 DNS 缓存同时为空,所有机器的查询会一瞬间全部涌向上游,造成雪崩。解决办法是分批重启,或者缓存服务开启预取后先在少量节点上预热关键域名。
4. 权威域解析侧的高可用设计:别让业务在最后一公里被掐断
4.1 多线路、多区域的解析容灾设计
很多团队把精力都花在递归侧优化,却忽略了权威侧配置。实际上,域名解析的“最后一公里”同样需要高可用设计。
所谓多线路、多区域容灾,就是让同一个域名在解析层面具备多个可用节点。常见的做法是把业务部署在多个集群,每个集群提供一组出口 IP,然后在权威解析服务商处配置多条解析线路。当某个集群整体不可用时,解析侧能够把流量切到其他正常集群。
这里的关键是不要把所有节点都放到同一条线路上,否则“多线路”形同虚设。你至少要保证不同线路在物理链路、供电、带宽层面是隔离的。条件允许的话,让解析服务商提供主备域名服务器,并把权威区域数据同步到多个区域,避免单点故障导致全球解析中断。
4.2 健康检查与自动摘除故障节点
解析侧的高可用,不能只靠“解析服务商稳定”,还要有主动的健康检查机制。理想状态是:当后端节点故障时,解析系统能自动把故障 IP 从解析结果里摘除,而不是把请求继续引向病节点。
我见过一个典型的反面案例:某服务的 IP 早就不通了,但因为 TTL 还没过期,大量客户端继续拿着旧 IP 访问,报错率持续了十几分钟。原因就是没有做健康检查联动,解析结果没有任何“活性”判断。
现在的解析服务商大多支持健康检查,原理是定时向后端端口发起探测,失败达到阈值就从解析结果中移除。配置时注意三点:
- 探测协议要贴近真实业务,TCP 探测能覆盖大多数场景。
- 探测间隔要小于 TTL,否则失败后的摘除动作还是慢半拍。
- 摘除后要保留一段观察期,防止节点抖动导致频繁上下线。
4.3 解析变更的标准操作流程
DNS 变更最怕“直接改”,因为改错了影响面是全局的。我给自己总结的标准化流程是:
- 提前 24~48 小时把目标域名 TTL 调低,比如从 600 秒降到 60 秒。
- 等待旧 TTL 在各级缓存中基本过期。
- 执行解析变更,比如切换 IP 或调整线路权重。
- 观察业务流量、错误率、延迟曲线。
- 确认稳定至少一个完整周期后,再把 TTL 调回正常水平。
这个流程看起来繁琐,但能避免大多数“改完解析后局部地区迟迟不生效”的问题。调低 TTL 是让客户端更快接触到新结果,把切换窗口从“不确定”压缩到“可预期”。
5. 实操复盘:一次完整 DNS 优化从诊断到上线的全过程记录
5.1 现场问题描述与初步分析
某项目在做峰值流量压测时发现,接口响应 P99 长时间偏高,服务端 CPU 占用却不高,网络线程大量阻塞。我们从压测端发起请求,同时在生产环境抓包,看到每次 HTTPS 请求建立前都伴随一次 DNS 查询,而查询响应时间在 30~80ms 之间浮动,偶尔超过 200ms。
进一步排查后发现,该服务使用的 HTTP 客户端没有配置连接复用,而且系统级 DNS 缓存因为某些自定义配置被关闭了。导致的结果是:高并发下每来一个请求,都要现查一次域名,查询本身倒不算特别慢,但它把所有请求的起始时间都拉长了,叠加之后整体延迟就上去了。
5.2 优化方案的执行清单
我按优先级把整个优化拆成了五个步骤:
- 让应用层统一使用带连接池的 HTTP 客户端,设置长连接空闲时间为 60 秒。
- 在应用服务器上部署轻量级本地 DNS 缓存服务,并把系统 nameserver 指向本机。
- 将核心域名的 TTL 调整为 60 秒,让缓存刷新更快地感知到节点变更。
- 在权威解析侧增加健康检查,后端故障 15 秒内自动摘除。
- 在服务启动阶段做一次域名预热解析,避免进程刚启动时首次请求发生长时间等待。
这次改动没有做任何业务逻辑上的调整,全部围绕解析链路本身。整个执行过程用了一个变更窗口完成,先灰度了 10% 的流量,观察异常率后全量放开。
5.3 优化前后数据对比与结论
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 单次新建连接解析耗时 | 30~200ms 波动 | 命中本地缓存,均值低于 2ms |
| 每台服务器的出站 DNS 查询量 | 高峰期上千 QPS | 降到几十 QPS |
| 接口 P99 响应时间 | 890ms | 310ms |
| 高峰错误率 | 0.35% | 0.02% |
这个结果验证了一个判断:DNS 优化的核心收益并不只在那几十毫秒的解析时间上,而在于它消除了“每次新建连接都要付出不确定成本”的问题。连接池负责降低解析频率,本地缓存负责兜底,权威侧健康检查负责切换兜底,三者配合才能把整个链路的确定性提升上来。
6. 给后来者的一份排查速查表与避坑心得
6.1 你迟早会用到的排查命令和场景
排查 DNS 问题时,我经常先用这几个命令快速定位:
# 查看系统当前使用的解析服务器 cat /etc/resolv.conf # 查一个域名当前解析结果,并显示详细耗时 dig example.org +time=2 +tries=1 # 追踪完整解析链路,看卡在哪一级 dig example.org +trace # 对比不同解析服务返回结果 dig @1.1.1.1 example.org dig @8.8.8.8 example.org # 统计本机 DNS 查询流量 tcpdump -i any -n port 53 # 查看进程实际发起的解析行为 strace -e trace=network -p PID使用+time=2 +tries=1是我个人的习惯,默认的 5 秒超时和 3 次重试会让问题不明显,缩小超时后反而能快速暴露出丢包和延迟抖动。
6.2 我踩过且不希望你再踩的几个坑
第一,不要相信“改了 TTL 就会立即生效”这句话。有些公共递归服务会忽略小于某个阈值的 TTL,尤其是移动网络环境下,客户端的缓存策略更激进。做快速切换时,TTL 是辅助手段,不是唯一手段。
第二,不要把 all in one 的 DNS 缓存当成终极方案。本地缓存确实能挡住大量公共侧抖动,但它本身也是单点。缓存服务挂了,所有依赖它的进程解析都会失败。所以部署时至少要保证进程有守护机制,同时保留 fallback nameserver。
第三,警惕“多域名”带来的解析放大效应。很多系统为了隔离服务,把接口拆了几十个子域名,每一项资源一个域名。看起来合理,但每次请求要解析的域名数量成倍增加。我倾向于把同类资源合并到同一个域名下,用路径区分,从根上减少解析次数。
第四,变更域名 IP 之前,一定要先把旧 IP 的流量习惯摸清楚。因为客户端不一定会按 TTL 刷新,有些 App 自身还有内存级缓存,有些旧版本客户端会固执地重试旧 IP。灰度切换期间,观察旧 IP 上的连接是否仍在持续,确认降到很低再收尾。
第五,监控一定要做,但指标要选对。不要只监控解析成功率,还要监控解析耗时分布、查询量变化、上游解析服务响应码。成功率 100% 但耗时普遍超 500ms,一样会拖垮业务。
DNS 解析优化的很多坑,都是在流量上来之后才显性的。平时它安静地待在系统底层,你几乎感受不到它的存在;可一旦出问题,它会让整条链路都变得不可预测。把连接池、缓存、TTL、健康检查这些基础动作做到位,可能不会让你觉得“变快了”,但你会发现那些莫名其妙的超时和毛刺少了很多。这大概就是基础架构优化最有价值的地方。