1. 为什么值得研究DNS负载均衡:先看懂解析链路
1.1 用户在敲下回车之前,流量就已经被分发过一次
我以前总觉得负载均衡是Nginx、LVS、网关这些组件的事,直到接手一套多机房系统,才意识到最容易被忽略的入口其实在DNS。DNS负载均衡的原理说穿了并不复杂:当用户请求一个域名时,权威DNS服务器返回的不再是“一个IP”,而是一个IP列表,或者根据请求来源返回不同IP,客户端从这个列表里挑一个去连接。至此,负载均衡在“域名解析”这一步就发生了,完全不需要等TCP连接建立。
要理解这套机制为什么有用,得先把DNS解析流程过一遍。用户输入域名后,真正的解析路径是这样的:浏览器缓存 -> 操作系统hosts文件 -> 本地系统解析器缓存 -> 本地递归DNS服务器 -> 根/顶级域服务器 -> 权威DNS服务器。任何一级有了答案,都会直接返回给用户,不再继续往下查。DNS负载均衡配置是在最后一层权威服务器上做文章,但最终能否生效,却要看前面那些缓存层配不配合。
我把这个过程类比为“外卖派单”:用户下单时并不知道骑手是谁,他只会告诉平台“我要吃这家”。平台手里有“商家列表”,DNS就相当于那个决定“哪个商家接单”的调度员。可问题在于,用户手机里的旧订单记录、平台缓存的历史商家列表,都会让新派单延迟生效。这也是DNS负载均衡和普通负载均衡器最大的心智差异:它不是纯粹实时转发,而是在“一片可能有过期信息的分布式缓存”之上做调度决策。
1.2 DNS负载均衡和L4/L7负载均衡不是一回事
很多初学者会把DNS负载均衡和Nginx、HAProxy混在一起,其实两者分工完全不同。Nginx这类七层负载均衡器,每一个HTTP请求都会经过它中转,因此能准确地感知后端实例的健康状态、做精细的流量控制。而DNS负载均衡只在用户首次解析域名时做一次“引流”,后续真正请求的数据包并不会经过DNS服务器。
我用一个表格把这层差异说透:
| 对比维度 | DNS负载均衡 | L4/L7负载均衡(如Nginx、HAProxy) |
|---|---|---|
| 调度时机 | 域名解析阶段,每个客户端定期执行一次 | 每个请求或每个连接都会经过负载节点 |
| 客户端可见性 | 客户端能看到最终IP列表 | 客户端一般只看到负载均衡器地址 |
| 健康检查 | 不自带链路探测,需靠外部工具动态更新记录 | 自带TCP/HTTP健康检查,秒级摘除故障节点 |
| 故障转移速度 | 受TTL和递归缓存影响,通常需要数十秒到数分钟 | 秒级甚至毫秒级转移 |
| 承载压力 | 只处理轻量DNS查询,压力远小于业务流量 | 需要转发和代理全部业务流量 |
| 典型作用域 | 跨机房、跨地域的全局流量调度 | 单机房/单集群内部流量均衡 |
所以正确姿势是:让DNS负责“把用户引到哪个机房、哪个集群入口”,再让L4/L7负责“进入机房后分给哪台机器”。两者是上下游配合,不是替代关系。DNS这层一旦设计得好,后面的负载均衡器压力反而会小很多。
1.3 这套机制到底能解决哪些业务问题
DNS负载均衡在我实际见到的系统里,主要解决四类问题。
第一类是入口高可用。一个域名绑定多个机房VIP,A机房故障时,只要把那条A记录删掉或调低权重,用户刷新后就会走B机房。相比硬切IP,域名层切换的运维成本低很多。
第二类是跨地域就近访问。用户在上海和用户在广州,解析同一个域名时,权威服务器按来源地址返回不同机房的IP。这种能力在很多商业场景里是刚需,尤其面向全国用户的分发型业务。
第三类是集群扩容时平滑加减机器。新增节点时在DNS记录里加一条IP,不需要改动任何客户端配置;下线机器时提前把TTL调小,再删记录,就能把影响降到最低。
第四类是内部微服务的“客户端兜底寻址”。在Kubernetes环境里,Headless Service的Pod地址通过DNS记录暴露,服务消费者通过多次解析拿到全部后端地址。这本质上就是DNS层的负载均衡,只是很多人没意识到。
这套东西适合谁认真学?我认为至少四类人值得深挖:后端研发,排查线上问题时能从“应用层报错”一路追到“缓存层旧IP”;运维和SRE,DNS变更应该像发布代码一样有流程和回滚方案;架构师,做多活和容灾设计时,DNS策略是顶层入口;以及所有维护自建注册中心或微服务网关的人,理解DNS能帮你少踩很多地址缓存方面的坑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 架构怎么搭:从“多记录轮询”到“规则化智能调度”
2.1 最朴素的形态:一域名多A记录
先看一个最常见的配置样例。你在权威DNS上给某个业务域名添加三条A记录,正常工具查询时,输出会是这样:
bash复制$ dig +short ops.example.com
192.0.2.10
192.0.2.11
192.0.2.12
这看起来像是把流量平均分给三台机器了,但现实没那么理想。传统BIND默认情况下,如果客户端连续多次查询,它可能返回固定顺序,客户端往往只取第一条记录;而操作系统的解析器面对多个IP,会按RFC 6724做地址排序,不一定每次都变换顺序。结果就是:配置了三台机器,流量也可能主要压在第一条记录对应的机器上。
所以只靠“多A记录”做真正意义上的轮询,至少要在两个环节做配合。一是权威DNS侧,打开round-robin或随机返回功能,让每次查询时IP顺序尽量不同;二是客户端侧,应用代码最好显式对返回的多个IP做随机选择或轮流连接,不能依赖系统默认行为。
我见过很多维护了几年DNS记录的老系统,都是A记录里堆了五六台机器,以为天然就负载均衡。直到某台机器流量打满,才发现客户端的连接池和系统解析器只认第一个IP,其他IP早就是“僵尸配置”。这个坑想告诉大家的是:DNS负载均衡只是“给客户端多一个选择”,最终是否均匀取决于客户端怎么用这个选择权。
2.2 进阶形态:视图解析和基于来源的调度
当业务跨机房后,简单的多A记录就不够了。你希望上海用户去上海机房,广州用户去广州机房,而不是让所有用户随机抽取。这时要用到“视图解析”或者叫“智能DNS调度”。
BIND的view语法可以按来源IP或网段返回不同结果,商业云解析产品通常在后台维护IP库,把递归服务器的出口IP归属到具体城市/运营商,然后返回对应区域的记录。核心原理大同小异:在权威服务器侧增加规则判断,对查询来源进行归类。
这种架构需要重点考虑的是:来源IP未必真实。很多用户的流量会经过公司出口、公共DNS转发,甚至经过多层递归,权威收到的可能是某台公共递归服务器的IP,未必能代表最终用户位置。为了缓解这个问题,出现了EDNS Client Subnet(ECS)机制,递归服务器可以把用户子网信息附带在查询里传给权威,权威按这个更准确的来源决策。后面故障排查部分会专门提到它。
2.3 自建和托管怎么选
DNS系统虽然只是一个“翻译服务”,但它挂了业务全挂,所以选型时不能光看功能,还要看可靠性和成本。
| 选型方向 | 代表方案 | 适合场景 | 主要注意点 |
|---|---|---|---|
| 纯自建权威 | BIND、PowerDNS | 私网域名、定制化极强、离线环境 | 需要自己做双机热备、部署成本高、升级要谨慎 |
| 自建+动态接口 | PowerDNS + API、CoreDNS | 内部服务发现、自动化要求高 | 需要写管理平台,防止误操作 |
| 托管云解析 | 云服务商DNS、商业GSLB | 公网业务、全球多地域接入 | 控制台操作简单,但需要关心API限流和TTL支持 |
| 混合模式 | 公网托管+内网自建 | 大多数中大型团队 | 两套配置要分开管理,避免混淆 |
以我自己的经验,小团队刚起步时公网解析直接托管是最省心的,不需要考虑权威服务器的容灾和抗抖动问题。但私网域名或者Kubernetes集群内部的Service解析,无论如何都要有一层自建解析能力,因为内网地址不能暴露到公共DNS上,而且集群Pod地址是动态变化的,托管DNS的API实时性不一定够用。混合模式是绝大多数公司最终落地的形态。
2.4 内部微服务场景:CoreDNS配合Headless Service
内部微服务场景里,DNS负载均衡最常见的就是Kubernetes的Headless Service。当你把Service的clusterIP设为None时,DNS会为这个Service创建一条特殊记录,查询时返回所有Pod的IP,而不是虚拟ServiceIP。
bash复制$ kubectl run -it --rm dnsutils --image=registry.k8s.io/e2e-test-images/agnhost:2.45 --restart=Never -- nslookup myapp.default.svc.cluster.local
Name: myapp.default.svc.cluster.local
Address 1: 10.244.0.5
Address 2: 10.244.0.6
CoreDNS作为集群内的DNS服务器,会把查询结果按顺序返回给调用方。表面上这是很理想的服务发现方式,但实际使用中有一个常见问题:如果客户端长连接复用一个Pod地址,负载就不均衡。比如Java应用使用连接池,解析一次域名后把连接固定到一个Pod,后续流量都走同一个实例,扩容白扩。
解决思路通常是两级:框架层处理Headless Service返回的多个IP,做客户端侧的负载均衡;或者直接让Service由ClusterIP模式负责流量转发,而不是用Headless。判断标准很简单,如果你的业务是短连接或无状态API,Headless模式配合DnsRR还能用;如果是长连接、gRPC、消息队列这类场景,必须把负载均衡位置提前到客户端,否则结果大概率倾斜。
3. 优化落地:从TTL到全局调度的一整套调优方法
3.1 TTL不是越大越好,需要放到变更流程里一起设计
TTL是DNS记录在递归服务器和本地缓存里可以保存多长时间。它直接决定了DNS负载均衡的“指挥时效”。
主流公有云和自建DNS都支持按记录设置TTL,最小可以到1秒,但实际生产里没人会真设1秒,因为每一个查询都要穿透到权威,缓存收益会大幅下降。TTL设得太大又面临另一个问题:当你把故障机器从记录里摘除或切换机房时,客户端还拿着旧IP往故障节点上打。
我建议按业务属性分档设置:
- 面向用户的主站域名:TTL建议60~300秒,兼顾容灾切换速度和查询压力。
- 需要快速故障转移的关键业务:TTL可以低到30秒,同时要确保权威DNS的QPS有余量。
- 极少变动的MX记录、验证记录:可以设到3600秒或更高,减少不必要的权威查询。
需要说明的是,低TTL会放大权威服务器的请求量。假设一个域名在递归层没有命中时平均每秒被查询1000次,TTL从3600秒降到60秒,意味着每60秒内同一递归器都要重新到权威取一次,权威负载可能提升到原来的几十倍。所以做低TTL之前,先看权威服务器和托管的API QPS有没有余量,不要拍脑袋把全站TTL都改成30秒。
3.2 隐藏在发布流程里的“缓存生效盲区”
很多人把TTL当成精确计时器,认为“过了TTL就一定生效”,但这里有一个隐藏盲区:递归服务器并不一定严格按照权威返回的TTL缓存。部分公共递归服务会设置TTL最小值,即使你返回10秒,它也可能缓存60秒。某些老旧的本地DNS服务甚至完全忽略短TTL,强制按自己的策略缓存。
所以发布流程里的正确做法是分三步走。第一步,计划变更前1到2小时,先把目标记录的TTL临时降到60秒甚至更低,让全网递归器尽快抛弃旧缓存;第二步,执行真正的记录变更,比如切换机房IP、删除故障机器;第三步,在确认新结果稳定后,再把TTL恢复到常规值。这个过程相当于给DNS做了一次“缓存预热”。
我见过最典型的翻车现场是:团队觉得把TTL改成10秒后,等十分钟就能让所有用户完成切换,于是半夜直接改了记录。结果早上高峰期,大量用户还在访问旧IP,因为公司出口的递归DNS根本不认低TTL。后来排查时用dig @公共递归地址逐一查询,发现有的解析器24小时前的老记录都还在。从那以后,我把“变更前先降TTL”写进了运维规范,凡是涉及线上域名变更,必须提前半天执行TTL降温。
3.3 让流量更均匀的几个专项优化手段
均匀性是DNS负载均衡最核心的优化目标。除了在权威服务器开启随机返回,还可以做下面几件事。
第一,利用权重记录做不完全均匀分配。有些GSLB产品支持为每个IP设置权重,比如机房A权重70,机房B权重30,让流量按比例分配。需要扩容时,临时调高某个机房的权重,观察曲线平稳后再改回。
第二,开启EDNS Client Subnet支持。客户端流量经过公共递归服务器转发时,权威侧默认只能看到递归服务器的IP,无法判断用户真实地理位置。开启ECS后,递归器会把用户子网信息透传给权威,权威按用户地址段返回更合适的目标。很多商业云解析默认开启此能力,自建BIND则要额外配置插件或采用支持ECS的方案。
第三,建立“健康状态驱动的记录自动变更”机制。DNS本身不知道后端服务有没有挂,所以要通过外部监控系统定期检测各记录的可用性。检测逻辑需要做多探测点确认,至少有两个独立位置同时探活失败,才允许摘除记录;恢复时也要连续多次成功后再加回来,避免抖动误伤。手工维护的缺点是容易漏,推荐把监控系统和DNS服务商API对接,实现自动化。
3.4 用分层调度模型看待整套体系
我后来想明白一点:DNS负载均衡不是万能的,它只负责把用户引导到某个“入口”,不负责入口内部的精确定位。整套系统可以抽象成三层模型:
第一层是客户端本地的缓存和连接策略。应用代码选择连接哪个IP,这是最靠近用户的一层,决定最终连接是否均匀。第二层是递归DNS与公共DNS的缓存。这一层决定新记录能否及时到达用户侧。第三层才是权威DNS的规则和策略。这层负责按地域、权重、健康状态动态调整记录内容。
理解了这三层,做优化时就有一个完整思路:想让用户访问更快,第一层和第二层要最大化缓存命中;想让故障转移更快,第二层和第三层要能尽快刷新;想让流量更均匀,第一层的客户端行为和第三层的策略都要配合。很多问题的根因不在第三层,而在于客户端的“懒缓存”或递归器的“强缓存”,这也是排查DNS负载均衡问题时最难的地方。
4. 故障排查:从现象到根因的完整思路
4.1 先把命令工具箱准备好
我排查DNS负载均衡问题,很少一上来就翻监控面板,而是先从客户端视角做一组解析测试。最常用的命令是dig,以下是几个场景化用法。
bash复制# 查询权威返回的完整结果
dig @权威DNS地址 ops.example.com A +noall +answer +comments
# 模拟递归解析,看用户实际拿到什么
dig @223.5.5.5 ops.example.com A +noall +answer
# 查看从根开始的完整解析路径
dig ops.example.com A +trace
# 检查当前域名在哪台服务器上授权
dig ops.example.com NS +short
这条命令里用了223.5.5.5作为公共递归DNS示例,实际排查时可以替换为你团队所处的递归DNS地址,也可以同时对比多个公共DNS的结果。如果权威查询正常,而某台递归返回旧结果,说明问题在缓存层。如果连权威查询都不对,问题就在配置或记录本身。
Windows环境下,客户端缓存问题经常表现为“突然无法解析内网域名”,此时建议看Windows事件管理器里DNS Client相关事件,比如事件ID 1014或1016,这类日志通常表示系统解析器向DNS服务器发送请求后没有收到正常响应。先flush本地缓存和查看事件日志,能快速区分是“缓存导致”还是“服务器网络不通”。
4.2 故障案例一:新增记录后用户还在访问旧机房
现象:源站扩容完成,权威DNS里新记录已经加了半小时,但监控显示流量始终没有进入新机房。
这类问题十有八九是递归缓存。首先不要急着改配置,按顺序做以下检查。
第一步,用几条不同的递归DNS做对比查询:
bash复制$ dig @223.5.5.5 ops.example.com A +short
192.0.2.10
$ dig @1.1.1.1 ops.example.com A +short
192.0.2.11
192.0.2.10
如果223.5.5.5返回的还是只有一个老IP,而1.1.1.1已经返回两个IP,说明全国范围内的缓存并未同步过期。此时能做的就是等待TTL自然过期,或者对递归服务器主动发起缓存刷新请求。很多公共DNS提供了刷新接口,不过不一定保证实时生效。
第二步,判断是不是ECS干扰了调度。用户在某地使用支持ECS的公共DNS,权威会基于用户子网返回“最优记录”。如果你用普通查看工具,得到的结果可能和用户实际结果不一致。建议在递归侧加+subnet=用户地址段参数模拟ECS查询,或直接到用户所在网络里反复解析看结果。
第三步,查客户端是否缓存了连接层的IP。很多Java和Go程序一旦拿到IP列表就缓存在进程内部,根本不重新查域名。可以用日志或抓包确认应用是否持续解析,如果进程一直复用旧连接,就不是DNS的锅,而是应用连接池配置问题。
4.3 故障案例二:配置了三条A记录,流量却只打满第一台
现象:域名有三条A记录对应三台服务器,权威配置也正确,但监控上第一台机器CPU已经接近90%,另外两台长期在10%左右徘徊。
我的经验是,这种流量倾斜通常来自以下三个原因。
第一,客户端只认第一条。权威服务器默认按固定顺序返回,部分解析器或应用进程会把第一个IP当作固定目标。遇到这种情况,先在权威侧开启round-robin,让每次查询返回的IP顺序变化;同时检查客户端程序是否有Resolver类缓存,Java应用有addressCache,可以在JVM参数里设置-Dsun.net.inetaddr.ttl=0。
第二,连接池复用导致“看起来只连第一台”。一个服务实例一旦成功建立了到某个IP的长连接,只要连接不中断,它会一直用这个IP。Docker、Kubernetes环境里这类问题尤其明显,旧的Pod已经删了,客户端连接池还留着旧连接,新建连接才走到新Pod。
第三,操作系统地址排序逻辑导致偏移。RFC 6724要求客户端对返回地址做排序,某些平台会认为特定网段的地址“更合适”,会优先选择。如果A记录里的三个IP分属不同网段,客户端可能总是选到同一种类型的IP。解决方式是尽量让记录里IP保持同网段,或者在应用层关闭地址排序能力。
4.4 故障案例三:健康检查自动摘除,结果把整个域名删空了
这个案例比较惨烈。当时我们搭建了一套自愈系统:监控脚本每30秒访问一次各后端健康检查URL,一旦失败就调用DNS服务商的API删除对应记录。某天后端因发布上线,健康检查URL在新版本里路径变了,返回503。脚本按预期执行,把三台后端记录全摘了,域名解析结果瞬间为空,线上直接不可用。
复盘时发现两个核心问题。第一,健康检查脚本只在一个探测点执行,没有做多点确认。单点网络抖动、监控脚本所在机器与后端之间的链路中断,都可能造成误判。第二,摘除动作没有设置“最小存活数”保护。不管怎样,域名下至少应该保留一条可服务记录,否则再好的自愈系统也会造成全量故障。
建议所有做DNS自动摘除的方案都遵循三个原则:摘除前至少有两点以上独立探测都失败;摘除动作必须配置冷却窗口,不能每次失败都立刻操作;摘除时检查当前可用记录数量,如果已经是最后一条,则改为告警而不是摘除。DNS记录是业务的生死线,自动化一定要把“保护底线”放在第一位。
4.5 把排查过程固化成一套SOP
排查DNS负载均衡问题最忌讳的是一上来就改配置。我习惯先按固定顺序收集信息,下面这个表格可以直接当排查模板用。
| 检查项 | 命令或入口 | 判断标准 | 可能结论 |
|---|---|---|---|
| 本地系统缓存 | ipconfig /displaydns 或 dig | 本地记录是否过期 | 本地缓存污染 |
| 权威查询 | dig @权威DNS地址 | 权威返回是否符合预期 | 配置错误或记录异常 |
| 递归查询 | dig @公共DNS 对比 | 多个递归是否一致 | 上游缓存未过期 |
| ECS调度 | dig +subnet | 不同来源是否返回不同目标 | 调度规则生效情况 |
| 应用连接池 | 查看应用日志和连接统计 | 是否频繁新建连接 | 客户端长期复用固定IP |
| 健康检查结果 | 监控系统或脚本 | 所有后端是否存活 | 是否有人为摘除记录 |
大多数DNS负载均衡故障都能在这个流程里找到答案。核心原则是“先看清现状,再推断根因”,不要用猜测代替验证,不要用反复修改权威配置掩盖缓存层的问题。
4.6 一个容易被忽略的坑:递归服务器返回截断结果
还有一种故障表面上是因为负载均衡失效,但实际上是由UDP响应大小限制引起的。当权威服务器返回的A记录比较多,或者启用了DNSSEC、附加信息较多时,响应包可能超过512字节。如果客户端和递归器都不支持EDNS0扩展,递归器就会只返回部分记录,甚至返回TC截断标志。
出现这种现象时,你可以用下面两条命令对比:
bash复制$ dig ops.example.com A +short +dnssec
$ dig +tcp ops.example.com A +short +dnssec
当UDP查询返回的结果不完整,而TCP查询结果完整时,大概率是链路中某个设备不支持EDNS0,或者响应包被中间设备截断。排查方向不是DNS记录本身,而是网络设备或递归服务器的EDNS0支持情况。这也是DNS负载均衡场景里容易被画进“玄学”里的一个常见问题。
5. 日常维护DNS负载均衡体系的心得
5.1 变更要有“降温-变更-恢复”的节奏感
我特别建议把DNS变更当成一次生产发布来看待,而不是随手在控制台改两条记录。标准流程可以定成:先查当前TTL,如果TTL超过300秒,提前一小时降到60秒;变更执行后在多个递归节点验证新记录;确认无误后把TTL恢复成常规值;整个过程的每一步都有人审。
如果你们没有单独的管理平台,也可以把配置记录放在Git仓库,用脚本定期对比线上实际解析结果与仓库文件的差异。这样做的好处是,万一有人改了记录忘了同步,下次巡检时就能发现。
5.2 建立一份自己的“解析基线”
哪怕平时没有故障,我也建议每个月做一次主动巡检:把这套系统所有重要域名的A记录、CNAME、TTL值打印出来,与预期配置做一次全量对比。这样能发现一些静默异常,比如被云服务商控制台误操作改掉的记录、因证书续期被自动流程删除的验证记录、以及带宽切换后没有同步到权威的旧VIP。
我自己的习惯是维护一份Markdown表格,包含每个域名的作用、当前解析值、期望解析值、上次变更人和变更原因。排查故障时直接打开这张表,可以节省大量猜测时间。DNS系统的维护难点往往不是技术复杂,而是变更太分散、记忆容易错位,有一份基线文档能解决大半问题。
5.3 最后分享一个实际帮助过我的小技巧
之前处理一个频繁超时的线上问题时,我反复查权威、查递归都没有发现异常。后来无意中开启了ECS模拟查询,才发现用户通过某公共DNS解析时,权威返回了一个距离极远的备用机房IP,因为那条公共DNS的出口IP刚好被识别成了另一个地域。当时如果只看第一条解析结果,根本不会发现问题。
所以遇到任何“DNS解析结果看起来对,但用户就是访问慢/访问错”的情况,一定要多换几个递归DNS,并且用ECS模拟用户真实网络环境去查询。DNS负载均衡的调试本质上就是在不同缓存和不同视角之间来回切换,你看得越多,越接近真相。
