Skip to content
BEE
Backend Engineering Essentials

[BEE-3002] DNS 解析 ​

背景 ​

在後端服務能夠向另一個服務建立 TCP 連線之前,它必須先將主機名稱(hostname)轉換為 IP 位址。這個轉換過程就是 DNS(Domain Name System,網域名稱系統),它影響你的服務所發出的每一個對外連線。DNS 不是一個設定好就可以忘記的已解決問題:它有失敗模式、快取行為、傳播延遲以及延遲特性,這些都經常導致正式環境事故。

本文涵蓋 DNS 解析的端對端運作方式、後端工程師常遇到的記錄類型(record types)、DNS 如何用於負載平衡(load balancing)和故障轉移(failover),以及你需要設計對應方案的失敗模式。

參考資料:

原則 ​

永遠不要寫死 IP 位址。始終使用 DNS,並且永遠遵守 TTL。

DNS 是網際網路的電話簿。每當你使用主機名稱而非 IP 位址,你就獲得了在不修改客戶端的情況下變更目標位置的能力。這種靈活性是服務探索(service discovery)、藍綠部署(blue/green deployment)以及 DNS 故障轉移的基礎。代價是增加了一層間接層,具有其自身的延遲、快取和失敗模式。理解這些代價讓你能夠主動設計因應方案,而不是在遭遇問題時措手不及。

DNS 解析的運作方式 ​

當你的應用程式呼叫 getaddrinfo("api.example.com") 或向 https://api.example.com 發出 HTTP 請求時,一連串的查詢便會展開。

四個角色 ​

Stub resolver(本地快取): 一個內建在作業系統中的小型解析器(或像 systemd-resolved、dnsmasq 這樣的本地守護程序)。它維護一個以 (名稱, 類型) 為鍵的快取。它先查詢 /etc/hosts,再查詢 /etc/resolv.conf 中設定的遞迴解析器。

遞迴解析器(Recursive resolver): 主力工作者。它接受來自 stub resolver 的完整查詢,並代表客戶端處理所有的迭代查詢,快取每個回應。例如:你的 ISP 解析器、Google Public DNS(8.8.8.8)、Cloudflare DNS(1.1.1.1)。

根域名伺服器(Root nameservers): 共有 13 個邏輯根域名伺服器身份(a–m.root-servers.net),透過 anycast 路由由全球數百台實體機器提供服務。它們不知道任何個別主機名稱的答案;它們只知道哪些域名伺服器對每個頂級網域(TLD)具有權威性。

權威域名伺服器(Authoritative nameserver): 持有某個網域實際 DNS 區域檔案(zone file)的伺服器。它給出最終答案。當你在域名註冊商或 DNS 提供商(Route 53、Cloudflare、Google Cloud DNS)設定記錄時,你就是在編輯由你的權威域名伺服器提供服務的區域。

DNS 記錄類型 ​

類型說明
A將主機名稱對應到 IPv4 位址。最常見的記錄類型。
AAAA將主機名稱對應到 IPv6 位址。
CNAME從一個名稱建立別名到另一個名稱(canonical name,正規名稱)。不能與同名的其他記錄共存。不能用於區域頂點(裸網域)。
MX郵件交換(Mail exchange)記錄。指向負責接收網域電子郵件的郵件伺服器。包含優先級值。
TXT任意文字。用於網域所有權驗證(Let's Encrypt、Google Search Console)、SPF、DKIM 和 DMARC 電子郵件身份驗證記錄。
SRV服務記錄。為服務編碼 priority(優先級)、weight(權重)、port(埠號)和 target(目標主機名稱)。被 SIP、XMPP 和 Kubernetes etcd 節點探索等協定使用。格式:_service._proto.name TTL class SRV priority weight port target。
NS指定網域的權威域名伺服器。你在域名註冊商那裡登記的頂層域名伺服器就是 NS 記錄。
SOA授權起始(Start of Authority)。每個區域一筆。包含主要域名伺服器、負責人電子郵件、序號(用於區域傳輸同步)以及更新/重試/過期間隔。

TTL 與快取 ​

TTL(Time-To-Live,存活時間) 在 RFC 1035 中定義為「一個 32 位元有符號整數,指定資源記錄(resource record)在再次查詢資訊來源之前可被快取的時間間隔(單位:秒)」。

TTL 為 300 表示解析器和 OS 快取最多可在 5 分鐘內提供該記錄而無需重新驗證。過期後,它們必須重新查詢。

實用 TTL 值:

情境建議 TTL
穩定的正式環境服務300–3600 秒
即將遷移的服務60–300 秒(在變更前降低)
主動故障轉移 / 動態路由30–60 秒
永不快取(應謹慎使用;對域名伺服器負載高)0

DNS 傳播(propagation)是個誤稱。 沒有任何推送機制會將記錄變更廣播給所有解析器。實際發生的是,快取的記錄根據其 TTL 過期,解析器在那個時間點重新查詢。如果你的 TTL 是 3600,部分客戶端在你更新記錄後最多一小時仍會使用舊記錄。在計劃變更之前降低 TTL;事後再將其恢復。

關鍵系統檔案 ​

/etc/hosts ​

大多數作業系統在 DNS 之前先查詢此檔案。讓你可以在本地覆寫 DNS。在開發環境和容器中很有用,可以在不執行 DNS 伺服器的情況下注入服務位址。

127.0.0.1   localhost
::1         localhost
192.168.1.50 db.internal

查詢順序由 /etc/nsswitch.conf 控制(hosts: files dns 表示先查 /etc/hosts,再查 DNS)。

/etc/resolv.conf ​

設定 stub resolver:使用哪個遞迴解析器,以及搜尋網域後綴(search domain suffixes)。

nameserver 10.96.0.10          # DNS 伺服器 IP(例如 Kubernetes 中的 kube-dns)
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5                 # 如果名稱包含少於 5 個點,嘗試附加搜尋網域

search 和 ndots 設定在 Kubernetes 中很重要。對 redis 的查詢在回落到簡單查詢之前,最多會變成 6 個查詢(redis.default.svc.cluster.local、redis.svc.cluster.local 等)。這為每個連線都增加了延遲。使用完全合格的網域名稱(FQDN,尾部加點,如 redis.default.svc.cluster.local.)來跳過搜尋清單。

使用 dig 檢查 DNS ​

dig(屬於 bind-utils / dnsutils 套件的一部分)是查詢 DNS 的標準工具。

bash
$ dig api.example.com A

; <<>> DiG 9.18.0 <<>> api.example.com A
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12345
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; QUESTION SECTION:
;api.example.com.               IN      A

;; ANSWER SECTION:
api.example.com.        300     IN      A       93.184.216.34

;; Query time: 12 msec
;; SERVER: 1.1.1.1#53(1.1.1.1)
;; WHEN: Mon Apr 07 10:00:00 UTC 2026
;; MSG SIZE  rcvd: 56

關鍵欄位解讀:

  • status:NOERROR(找到)、NXDOMAIN(名稱不存在)、SERVFAIL(解析器失敗)、REFUSED(解析器拒絕)。
  • flags:qr = 這是回應;rd = 遞迴所需(客戶端要求解析器遞迴查詢);ra = 遞迴可用(伺服器支援遞迴);aa = 權威答案(直接來自權威伺服器)。
  • ANSWER SECTION:TTL 欄(300)是剩餘時間(秒)。最右側欄位是 IP 位址。
  • Query time:查詢耗時。快取的回應通常 <1 毫秒;未快取的遞迴查詢可能需要 50–300 毫秒。
bash
# 追蹤 CNAME 鏈
dig www.example.com CNAME

# 顯示完整解析路徑(從根開始的迭代查詢)
dig +trace api.example.com

# 直接查詢特定域名伺服器
dig @ns1.example.com api.example.com A

# 精簡輸出(只顯示答案)
dig +short api.example.com A

# 透過 DNS over HTTPS 查詢(需要 curl)
curl -H 'accept: application/dns-json' \
  'https://cloudflare-dns.com/dns-query?name=api.example.com&type=A'

DNS 負載平衡 ​

DNS 可以透過為同一名稱返回多個位址,在不使用硬體負載平衡器(load balancer)的情況下將流量分散到多個後端。

Round-Robin DNS(輪詢 DNS) ​

為同一主機名稱返回多個 A 記錄。解析器和客戶端通常會輪流使用它們。

api.example.com.  60  IN  A  10.0.1.10
api.example.com.  60  IN  A  10.0.1.11
api.example.com.  60  IN  A  10.0.1.12

限制: 客戶端會在 TTL 期間快取它們收到的第一個位址。輪替不保證均勻。沒有健康檢查——如果某個 IP 下線,客戶端會繼續向它發送請求,直到 TTL 過期並收到更新的記錄集。標準的 round-robin DNS 不知道哪些後端是健康的。

加權 DNS(Weighted DNS) ​

AWS Route 53 和 Google Cloud DNS 等 DNS 提供商支援加權路由(weighted routing):為每個記錄分配一個權重,解析器按比例返回各記錄。適用於金絲雀部署(canary deployment)(例如將 5% 的流量導向新版本)。

地理位置 / 延遲導向路由 ​

根據客戶端解析器的 IP,將流量路由到最近或延遲最低的區域。權威域名伺服器根據查詢來源返回不同的 IP。被全球 CDN 和多區域部署廣泛使用。

DNS 故障轉移情境 ​

這是後端可靠性中最重要的模式之一。

情境: api.example.com 解析到主要伺服器 10.0.1.10。主要伺服器宕機。

  1. 一個健康檢查代理(Route 53 health check、Cloudflare Health Checks 或你自己的監控)偵測到 10.0.1.10:443 無回應。
  2. 代理更新 DNS 記錄:移除 10.0.1.10,新增 10.0.1.20(備援伺服器)。記錄的 TTL 是 30 秒。
  3. 新的 DNS 查詢返回 10.0.1.20。
  4. 快取中仍指向 10.0.1.10 的客戶端將繼續失敗,直到其快取條目過期(最多 TTL,即 30 秒)。
  5. 快取過期後,這些客戶端解析到 10.0.1.20 並恢復正常。

關鍵洞察: 恢復時間窗口受 TTL 限制,而非受你更新記錄的速度限制。如果你的故障轉移 TTL 是 300 秒,客戶端可能會中斷最多 5 分鐘。在你打算進行故障轉移的記錄上保持較低的 TTL。

此外,應用層的 DNS 快取可能會覆寫 TTL。 許多 HTTP 客戶端、基於 JVM 的服務以及連線池會無限期地快取 DNS 結果,或按與 DNS TTL 無關的固定時間快取。這些客戶端在其內部快取過期之前不會看到更新的記錄——即使 OS 快取已經刷新。這在「常見錯誤」一節中會進一步討論。

DNS 失敗模式 ​

回應含義常見原因
NOERROR 且 0 個答案名稱存在但沒有所請求類型的記錄區域中缺少記錄
NXDOMAIN名稱不存在主機名稱拼寫錯誤、網域過期、記錄缺失、區域委派問題
SERVFAIL解析器無法獲得答案權威伺服器無法連線、DNSSEC 驗證失敗、上游逾時
REFUSED解析器拒絕回答查詢只接受特定 IP 遞迴查詢的權威伺服器
逾時(Timeout)在客戶端逾時內未收到回應網路分割、UDP 封包丟失、解析器過載、大型回應需要 TCP 備援
bash
# 模擬並觀察各種失敗模式
dig @8.8.8.8 nonexistent.example.com A      # NXDOMAIN
dig @8.8.8.8 _dmarc.broken-dnssec.com TXT   # SERVFAIL(DNSSEC 失敗)
dig +time=1 +retry=0 @192.0.2.1 example.com # 逾時(黑洞 IP)

DNS 逾時值得特別關注。 DNS 查詢預設使用 UDP。如果 UDP 回應丟失,客戶端會在短暫間隔後重試(通常每次嘗試 1–5 秒,重試 2–3 次)。大於 512 位元組的 DNS 回應(EDNS0 將此上限提高到 4096 位元組)可能會迫使回退到 TCP,在 DNS 回應到達之前增加一次完整的 TCP 握手。在封鎖 DNS over TCP(53/TCP 埠)的防火牆環境中,大型回應會導致無聲的失敗。

DNS 傳播 ​

更新 DNS 記錄不會立即到達所有客戶端。所謂的「傳播延遲」實際上是每個解析器的快取 TTL 獨立過期的過程。沒有全球廣播機制。

TTL=300(5 分鐘)記錄變更的時間軸:

T+0s    你在權威域名伺服器更新記錄。
T+0s    解析器沒有快取的客戶端:他們立即獲得新記錄。
T+0s–300s  解析器有快取條目的客戶端:他們繼續獲得舊記錄。
T+300s  此記錄的所有解析器快取已過期。新查詢返回新值。

在計劃變更前最小化傳播延遲:

  1. 在變更前 24–48 小時將 TTL 降低到 60 秒(或更低),時間足夠讓所有先前 TTL 的舊快取條目過期。
  2. 進行變更。
  3. 等待一個新 TTL 間隔(60 秒),讓變更完全生效。
  4. 將 TTL 恢復到正常值。

DNS over HTTPS (DoH) 與 DNS over TLS (DoT) ​

傳統 DNS 查詢以明文形式透過 UDP/TCP 53 埠傳送。網路路徑上的任何人都可以看到你在解析哪些主機名稱——這是一個重大的隱私和安全隱患。

DNS over TLS (DoT) — 將標準 DNS 包裹在 TLS 會話中。使用專用埠(TCP 853)。查詢是加密的,但 853 埠流量是 DNS 這一事實對網路觀察者仍然可見。

DNS over HTTPS (DoH) — 將 DNS 查詢以 HTTPS 請求的形式發送(443 埠)。DNS 流量與一般 HTTPS 無法區分。定義於 RFC 8484。Firefox、Chrome 等瀏覽器以及作業系統越來越多地預設使用 DoH。

功能DoTDoH
埠853443
協定DNS over TLSDNS over HTTPS
可見性DNS 流量可透過埠號識別與 HTTPS 混合
網路控制較易集中允許/封鎖較難攔截或封鎖
採用層級OS / 解析器層級瀏覽器、應用程式、OS 層級

後端相關性: 如果程式語言執行環境或容器映像使用了寫死的 DoH 提供商,你的服務的對外 DNS 查詢可能會繞過企業或 VPC 的 DNS 解析器。這可能破壞依賴內部 DNS 的環境中的服務探索(例如 Kubernetes kube-dns、AWS VPC DNS 在 169.254.169.253)。驗證你的容器基礎映像或語言執行環境中是否無意中啟用了 DoH。

DNS 與服務探索 ​

在微服務架構中,DNS 是一種常見的服務探索機制:

  • Kubernetes: 每個 Service 都獲得一個 DNS 名稱(<service>.<namespace>.svc.cluster.local)。叢集 DNS 伺服器(kube-dns 或 CoreDNS)返回 ClusterIP,kube-proxy 將流量路由到健康的 Pod。
  • AWS ECS/EKS: AWS Cloud Map 透過 DNS 提供服務登錄與探索。
  • Consul: HashiCorp Consul 透過 8600 埠的 DNS 介面公開服務健康狀態和位址。
  • Docker Compose: 服務可透過 Docker 嵌入式 DNS 在 Compose 網路中以服務名稱相互訪問。

當服務崩潰並以新 IP 重啟時,基於 DNS 的服務探索會自動更新記錄。快取了舊 DNS 結果的客戶端將在其快取過期前持續失敗——這就是為什麼服務探索記錄使用較短的 TTL(10–60 秒),以及為什麼連線池必須設計為能夠偵測死連線(TCP keepalive、健康探針),而不是單純依賴 DNS 的新鮮度。

常見錯誤 ​

1. 快取 DNS 結果超過 TTL(故障轉移後的陳舊記錄) ​

Java 虛擬機器(JVM)歷史上會永久快取 DNS 結果(或按 JVM 全域逾時 networkaddress.cache.ttl 快取)。許多 HTTP 客戶端程式庫實作了自己的 DNS 快取,忽略解析器返回的 TTL。當故障轉移發生且 DNS 記錄指向新 IP 時,具有應用層快取的客戶端會繼續連接已宕機的伺服器。

修正方式: 設定你的 HTTP 客戶端以遵守 DNS TTL。在 Java 中,在 JVM 安全策略中設定 networkaddress.cache.ttl=30,或按客戶端設定。在 Go 中,net 套件預設遵守 TTL。驗證你使用的任何連線池或 HTTP 客戶端程式庫的行為。

2. 在應用層 HTTP 客戶端中不遵守 TTL ​

與上述相關:持有持久 TCP 連線的連線池在連線關閉之前不會重新解析 DNS。10 分鐘前建立的連線可能仍然指向 DNS 記錄不再廣播的 IP。連線有效直到該 IP 上的伺服器消失——此時連線池有一個死連線,必須建立新連線,在最糟糕的時機才支付 DNS 查詢成本。

修正方式: 在你的連線池中設定最大連線存活時間(讓連線定期回收並重新解析 DNS),或實作一個獨立的 DNS 解析迴圈,偵測記錄變更並關閉指向陳舊 IP 的連線。

3. 寫死 IP 位址而非使用 DNS ​

如果你在服務設定中寫死 10.0.1.50,因為「那是資料庫 IP,它永遠不會變」,你就放棄了在不重新部署程式碼的情況下遷移資料庫的能力,失去了進行 DNS 故障轉移的能力,並引入了一個單一耦合點(single point of coupling)。IP 確實會變更——雲端實例被替換、負載平衡器獲得新的虛擬 IP、叢集重建後 IP 地址被重新分配。

修正方式: 始終使用 DNS 名稱。DNS 查詢的開銷最多只是第一次連線到某個名稱時多一次往返,之後就會被快取。

4. DNS 中的單點故障(沒有次要域名伺服器) ​

如果你的網域由單一權威域名伺服器提供服務,而該伺服器宕機,對你網域的 DNS 查詢將返回 SERVFAIL。即使你所有的應用程式伺服器都健康,你的服務也會變得無法訪問。DNS 提供商(Route 53、Cloudflare、Google Cloud DNS)維護著全球分散的 anycast 域名伺服器叢集,具有高可用性。自管理的域名伺服器至少必須有主要 + 次要冗餘。

修正方式: 使用具有多個 anycast 域名伺服器的受管 DNS 提供商。透過檢查你的網域 NS 記錄指向至少兩個不同 IP 範圍的域名伺服器位址來驗證。

5. 在延遲預算中忽略 DNS 解析時間 ​

後端工程師會測量資料庫查詢時間、HTTP 請求時間和服務處理時間——但 DNS 查詢時間往往是不可見的。冷 DNS 查詢(無快取條目)根據解析器位置、TTL 設定和網路狀況,可能需要 50–300 毫秒。對於在每個請求上向相依服務開啟新連線的服務,這個延遲會被反覆支付。

修正方式: 將 DNS 解析時間加入你的可觀測性(observability)中。測量包含 DNS 階段的首字節時間(time-to-first-byte)。使用連線池將 DNS 成本分攤到多個請求上。在高吞吐量系統中,考慮使用本地 DNS 快取守護程序(如 nscd、dnsmasq、systemd-resolved),讓熱門條目的解析器延遲保持在毫秒以下。

相關 BEE ​