一句话结论(30s)
DNS 默认走 UDP 不是因为 UDP「足够好」,而是因为 UDP 的无连接、无状态、1-RTT 开销恰好匹配 DNS「海量小查询 + 简单请求-响应」的使用模式——因为 TCP 的三次握手、连接状态、缓冲区管理对 99% 的查询都是不必要的负担,可靠性由应用层的超时重传 + 多服务器 + 缓存来补偿;只有响应大到 UDP 装不下(>512 字节、区域传送、DNSSEC、DoT/DoH)才退回到 TCP。
核心原理(2min)
- 设计约束:查询量巨大、响应极小、延迟敏感、简单请求-响应 → 共同指向 UDP。
- UDP 优势:1 RTT(vs TCP 约 2.5 RTT)、无状态低资源(一个 udp_sock 服务所有客户端)、协议栈路径短。
- 可靠性补偿:超时重传(指数退避 1s→2s→4s→8s)、多服务器切换、分层缓存、消息 ID 匹配。
- 何时用 TCP:响应超 512 字节(TC 截断标志后重发)、区域传送(AXFR/IXFR 强制 TCP)、DNSSEC 大响应、DoT/DoH(天然基于 TCP)。
底层深入(5-10min)
DNS 的设计约束
DNS(Domain Name System)是全球最大的分布式数据库之一,处理着每秒数百万次的查询。在设计 DNS 时,Paul Mockapetris(DNS 发明者)面临几个核心约束:
- 查询量巨大:每个网页访问可能触发几十次 DNS 查询
- 响应极小:典型的 A 记录查询响应只有几十到几百字节
- 延迟敏感:DNS 延迟直接影响页面加载速度,用户感知极强
- 简单请求-响应模式:一次查询一次回答,不需要长连接
这些约束共同指向了 UDP。
UDP 在 DNS 中的优势
极低的延迟
UDP 是无连接的。发送 DNS 查询时不需要三次握手:
UDP DNS 查询:
客户端 → DNS 服务器:发送 1 个 UDP 数据报(查询)
客户端 ← DNS 服务器:接收 1 个 UDP 数据报(响应)
总共:1 RTT(往返时间)
TCP DNS 查询(如果不复用连接):
客户端 → 服务器:SYN
客户端 ← 服务器:SYN + ACK
客户端 → 服务器:ACK(0.5 RTT,连接建立)
客户端 → 服务器:发送查询
客户端 ← 服务器:接收响应
客户端 → 服务器:FIN
客户端 ← 服务器:FIN + ACK
客户端 → 服务器:ACK
总共:约 2.5 RTT
在典型的 DNS 查询场景下(一个请求约 100 字节,回复约 200 字节),TCP 能发送的最小数据量本身就超过了 DNS 消息大小。UDP 的 1 RTT 对 TCP 的 2.5 RTT 是决定性的优势。
思考:1 RTT 和 2.5 RTT 的差距为什么是决定性的?因为 DNS 查询的量级是「每个网页访问触发几十次」,且它卡在页面加载的关键路径上。多出的 1.5 个 RTT 会被几十次查询放大成用户可感知的卡顿;而 DNS 请求/响应本身才一两百字节,TCP 握手与挥手这些「为可靠服务的开销」对如此小的数据完全是浪费。
轻量级资源消耗
DNS 服务器需要同时维护数万甚至数十万个并发连接。TCP 连接需要为每个客户端维护一个 socket、发送/接收缓冲区、连接状态(LISTEN/SYN_RCVD/ESTABLISHED 等)、重传队列。这对服务器是巨大的内存和 CPU 开销。
UDP 是无状态的——服务器收到一个数据报,处理,发送响应,然后忘记这一切。内核中一个 udp_sock 可以服务所有客户端。这让 DNS 服务器可以用相对低配的硬件处理海量并发查询。
思考:为什么「无状态」能让一台低配服务器扛住海量查询?因为 UDP 服务器不需要为每个客户端维护 socket、缓冲区、重传队列和状态机——收到一个数据报、处理、回包、忘掉。一个
udp_sock就能服务所有客户端,省下的内存和 CPU 让同样硬件能处理数量级的并发。
简单快速
UDP 协议栈的代码路径远短于 TCP。收到 DNS 查询时,内核的 UDP 层只做:
- 校验和验证(可选)
- 查找目标 socket(基于目的端口)
- 将数据报放入 socket 的接收队列
而 TCP 需要:校验和、序列号检查、SACK 处理、窗口更新、拥塞控制状态更新、乱序队列管理、ACK 延迟定时器等。
UDP 的可靠性补偿
DNS 虽然基于 UDP,但不能接受”查询丢了就丢了”。它通过应用层机制补偿 UDP 的不可靠:
超时重传
DNS 客户端(stub resolver)在发出查询后启动定时器,通常超时时间从 1-5 秒开始。如果超时未收到响应,重传查询。大多数实现使用指数退避策略(1s → 2s → 4s → 8s),最多重试 2-3 次。
请求重试(多服务器)
/etc/resolv.conf 通常配置了 2-3 个 DNS 服务器(如 nameserver 8.8.8.8 + nameserver 8.8.4.4)。如果第一个服务器在规定时间内无响应,客户端自动切换到下一个服务器。这种多服务器的冗余设计进一步提高了整体可靠性。
缓存
DNS 的缓存机制(每条记录有 TTL)极大地减轻了对单次查询可靠性的依赖。即使某次查询失败,只要记录还在缓存有效期内,后续请求就不会受影响。缓存的分层结构(浏览器缓存 → OS 缓存 → 本地 DNS 解析器缓存 → 权威 DNS 服务器)每层都有缓存,可靠性逐层叠加。
DNS 消息 ID 匹配
每个 DNS 查询携带一个 16 位的消息 ID。响应必须携带相同的 ID。客户端可以同时发出多个查询(不同 ID),收到响应时通过 ID 匹配对应查询。这是一种简单的多路复用,允许并发查询而不需要连接。
什么时候 DNS 使用 TCP
DNS 消息超过 512 字节
传统 DNS over UDP 的消息大小硬限制是 512 字节(DNS 协议规定的最小 MTU,确保不会 IP 分片)。当响应超过 512 字节时:
- DNS 服务器在 UDP 响应中设置 TC(Truncated,截断)标志位
- 客户端收到 TC 标志后,知道响应被截断
- 客户端使用 TCP 重新发起同样的查询
EDNS0(Extension Mechanisms for DNS)扩展了这个限制,允许协商更大的 UDP 缓冲区(如 4096 字节),但仍然是可选的。
思考:为什么要设 512 字节这个硬限制?因为这是「最小 MTU」的安全线——确保 UDP 响应不会被 IP 层分片,而分片会带来丢包重组的脆弱性。响应一大过这个阈值,服务器就设 TC 标志「截断」,客户端再用 TCP 重查;EDNS0 只是把这个协商上限提高,本质还是「小数据走 UDP,大数据让位 TCP」。
区域传送(Zone Transfer)
主 DNS 服务器到从 DNS 服务器的完整区域数据同步(AXFR/IXFR)会传输整个域的所有记录。一个 example.com 区域可能包含几万条记录,总大小远超过 512 字节甚至单个 UDP 数据报的理论上限(65535 字节)。区域传送强制使用 TCP。
DNSSEC 签名记录
DNSSEC 在响应中附加数字签名(RRSIG)和公钥(DNSKEY),大大增加了响应大小。带 DNSSEC 签名的响应很容易超过 512 字节,会触发 TC 标志导致客户端退回到 TCP。
DNS over TLS / DNS over HTTPS
现代 DNS 协议演进出了 DNS over TLS(DoT,端口 853)和 DNS over HTTPS(DoH,端口 443)。它们天然基于 TCP(TLS over TCP),提供加密和身份认证,防止 DNS 劫持和中间人攻击。
选型的核心权衡
| 维度 | DNS over UDP | DNS over TCP |
|---|---|---|
| 延迟 | 1 RTT | ~2.5 RTT(新建连接) |
| 连接状态 | 无状态 | 有状态 |
| 服务器资源 | 极低 | 较高(每连接缓冲区) |
| 消息大小 | 512 字节(传统) | 无限制 |
| 适用场景 | 标准查询(99%+) | 区域传送、大响应、DoT/DoH |
| 可靠性 | 应用层超时重传 + 缓存 | 传输层保证 |
总结
DNS 选择 UDP 作为默认传输协议,不是因为 UDP “足够好”,而是因为它在 DNS 的使用模式(大量小查询、简单请求-响应)中恰好最佳。这里的架构经验值得深思:选择协议不是看谁功能强,而是看谁的开销与使用模式最匹配。TCP 的可靠性是有代价的——三次握手、连接状态、缓冲区管理——对于 99% 的 DNS 查询来说这个代价是不必要的,因为应用层的超时重传 + 缓存 + 多服务器冗余已经提供了足够的可靠性。只有当响应数据大到 UDP 承载不了时,TCP 才作为”后备方案”上场。这是一种务实的工程设计:不追求理论上的完美,而追求实际场景中的最优。
章末提问
Q1:DNS 为什么默认用 UDP 而不是 TCP?
结论先行:因为 DNS 是「海量小查询 + 简单请求-响应」,UDP 的无连接、无状态、1 RTT 恰好最匹配。因为 TCP 的三次握手、连接状态、缓冲区管理对一两百字节的查询是纯负担,多出来的往返会被放大成页面卡顿;而可靠性可以由应用层超时重传 + 多服务器 + 缓存补偿,所以 UDP 是更优解。
Q2:UDP 不可靠,DNS 怎么保证查询可靠?
结论先行:靠应用层机制补偿——超时重传(指数退避)、多服务器切换、分层缓存、消息 ID 匹配。因为 UDP 本身丢了就丢了,但 DNS 把「重试」和「冗余」放到应用层:收不到响应就重发、换下一个 nameserver,缓存又让一次失败不影响后续请求,整体可靠性依然够用。
Q3:什么情况下 DNS 会退回到 TCP?
结论先行:当响应大到 UDP 装不下时——超过 512 字节触发 TC 截断标志、区域传送(AXFR/IXFR)、DNSSEC 大响应,以及 DoT/DoH 这类加密传输。因为大数据量下 UDP 的分片脆弱性和不可靠得不偿失,此时 TCP 的可靠传输和「无大小限制」才是更合适的选择。