我們用大約 1000 行 Go 程式碼編寫了一個生產環境的 DNS 伺服器,從 Hetzner DNS 遷移了數千筆記錄,並將傳播時間從「最多 90 分鐘」縮短到幾秒鐘。它使用隱藏主伺服器模式,Postgres 作為事件總線,並使用 AXFR + IXFR 將區域推送到公共輔助伺服器。以下是我們的實作方法和原因!

Sliplane 上的每個服務都會獲得一個託管子域名,例如 my-app-abc123.sliplane.app。這表示每個運作中的服務都會有一個 A 記錄和 AAAA 記錄,指向容器所在的伺服器 IP 位址。記錄數量隨平台規模線性擴展。
我們最初選擇 Hetzner DNS 是因為它是免費的,而且我們的大部分基礎設施都執行在那裡。一開始運作良好,但兩年後我們遇到了兩個問題:
記錄數限制:Hetzner DNS 對每個區域都有硬性限制。最初是 500 條記錄,他們後來幫我們提高了 1 萬筆(非常感謝!),但按照我們的增長速度,幾週內就會超出這個限制。顯然,以記錄數計算,我們是他們最大的 DNS 用戶之一 :D
速度:透過 API 建立記錄後,Hetzner 的網域伺服器可能需要長達 90 分鐘才能傳回該記錄。對於一個剛剛部署了服務並想存取 URL 的 PaaS 平台來說,這無疑是個糟糕的體驗。雖然這種情況並非總是如此嚴重,但每次發生都會直接影響使用者體驗。這看起來就像我們的平台出了問題(而事實上也確實如此!)。
問得好。對大多數人來說,託管式 DNS 服務商是最佳選擇。但一旦像我們這樣,在規模和限制條件下開始尋找合適的服務商,事情很快就會變得令人頭痛:
「聯繫銷售」定價。很多完全有能力處理我們這種記錄數量的供應商,都要求填寫「聯繫銷售」表格。我討厭這樣。直接告訴我價格不就好了嗎?
按記錄計費還是按查詢計費?那些公佈定價方案的服務商通常會按記錄或按查詢收費。我們根本不知道實際處理了多少 DNS 查詢,所以遷移到這個未知的定價模式就像簽了一張空白支票。
僅限歐盟地區。我們公司位於歐盟,也希望DNS伺服器留在歐盟境內。這就大大縮小了選擇範圍。
說實話,聽起來還蠻有趣的。我有點控制狂,寫個 DNS 伺服器這種事兒我平常都夢寐以求。寫上千行 Go 程式碼,感覺自由的感覺真值。結果,搭建這個伺服器花的時間比跟託管服務商約個會還短😵💫
所以我們自己動手建造了它,這也引出了讓它出奇簡單的模式。
事情的起因比我原先想的要簡單得多:我們的 DNS 伺服器從來不回應任何公開查詢。
在 DNS 中,區域的主名稱伺服器持有權威記錄。輔助伺服器使用AXFR (本質上是透過 TCP 傳輸完整的區域轉儲)來取得副本,並像主伺服器一樣回應公共查詢。當主伺服器發生變更時,它會向輔助伺服器發送 NOTIFY 通知,輔助伺服器會取得新的副本。
隱藏主伺服器更進一步,它完全不公開。它的存在只是為了將區域資料推送到輔助伺服器。您在網域名稱註冊商看到的那些公開網域伺服器,實際上都是輔助伺服器。
這意味著我們可以將 DNS 伺服器執行在任何我們想要的地方,使用任何支援AXFR 的輔助 DNS 供應商,並且可以在不更改伺服器的情況下更換提供者。由於 AXFR 和 NOTIFY 是標準協議,因此不存在任何提供者鎖定問題,任何相容的輔助 DNS 提供者都可以工作。
沒有任播,也沒有在全球部署冗餘的、受DDoS攻擊保護的DNS伺服器。只有少量的主隱藏伺服器實例。

設定非常簡單:
Postgres 是資料來源。我們安裝了觸發器,每當服務被建立、更新或刪除時,都會呼叫pg_notify('dns_zone_changed', '') 。沒有訊息佇列,也沒有 Webhook。 Postgres就是事件總線。
為什麼不用 Redis、NATS 或合適的隊列呢?原因有二。我們已經在使用 Postgres 作為主資料庫,所以LISTEN / NOTIFY是「免費」(天下沒有免費的午餐,但已經非常免費了)的基礎設施,無需額外運維、監控或付費。而且資料量很小。即使在高峰期,區域變更也只有每分鐘幾次,對於任何隊列式資料處理來說,這個頻率都低得可笑。在這種情況下使用 Kafka 就好比租個貨櫃來寄明信片一樣。
sliplane-dns是一個小型 Go 伺服器(約 1000 行,基於miekg/dns建置),它透過LISTEN訂閱,查詢 Postgres 以取得所有受管網域及其 IP 位址,建置 DNS 區域,並透過 AXFR 提供服務。
為了避免不必要的工作,我們會對所有記錄進行雜湊處理。如果雜湊值與先前的區域匹配,則不進行任何操作,不更新序號,也不發送 NOTIFY 請求。當區域實際發生變更時,我們會更新 SOA 序號,並向 Hetzner 的三個輔助 IP 位址發送 DNS NOTIFY 請求。它們會取得新的區域,記錄隨即生效。
為了了解區域傳輸的實際運作方式,這裡有一個僅支援 AXFR 協定的最小 DNS 伺服器。它為example.com提供一個硬編碼的區域,該區域只有一個 A 記錄(完整程式碼可在 GitHub 上找到):
package main
import (
"context"
"log"
"net/netip"
"codeberg.org/miekg/dns"
"codeberg.org/miekg/dns/rdata"
)
func main() {
soa := &dns.SOA{
Hdr: dns.Header{Name: "example.com.", TTL: 3600, Class: dns.ClassINET},
SOA: rdata.SOA{Ns: "ns1.example.com.", Mbox: "admin.example.com.", Serial: 1},
}
records := []dns.RR{
soa,
&dns.A{
Hdr: dns.Header{Name: "app.example.com.", TTL: 300, Class: dns.ClassINET},
A: rdata.A{Addr: netip.MustParseAddr("1.2.3.4")},
},
soa,
}
mux := dns.NewServeMux()
mux.HandleFunc("example.com.", func(_ context.Context, w dns.ResponseWriter, r *dns.Msg) {
r.Unpack()
w.Hijack()
env := make(chan *dns.Envelope, len(records))
for _, rr := range records {
env <- &dns.Envelope{Answer: []dns.RR{rr}}
}
close(env)
dns.NewClient().TransferOut(w, r, env)
w.Close()
})
srv := dns.NewServer()
srv.Addr = ":5553"
srv.Net = "tcp"
srv.Handler = mux
log.Fatal(srv.ListenAndServe())
}
執行它並用 dig 命令拉出該區域:
dig @localhost -p 5553 example.com AXFR
example.com. 3600 IN SOA ns1.example.com. admin.example.com. 1 0 0 0 0
app.example.com. 300 IN A 1.2.3.4
example.com. 3600 IN SOA ns1.example.com. admin.example.com. 1 0 0 0 0
完整的區域傳輸就是 SOA 架構,所有記錄,再進行一次 SOA 傳輸。這大致就是 Hetzner 的輔助伺服器從我們的生產伺服器拉取的資料,只是在兩次 SOA 傳輸之間多了幾千筆記錄。
DNS 名稱伺服器無法逐步遷移。註冊商處的 NS 記錄要麼指向舊的伺服器群組,要麼指向新的伺服器群組。存在切換視窗期,沒有其他辦法。
我們必須從 Hetzner 的名稱伺服器( hydrogen.ns.hetzner.com 、 oxygen.ns.hetzner.com 、 helium.ns.hetzner.de )切換到 Hetzner Robot 的輔助名稱伺服器( ns1.first-ns.de 、 robotns2.second-ns.de 、ns1.first-ns.de 、 robotns3.second-ns.com -ns-de .d
在過渡期間,快取了舊 NS 記錄的解析器仍會向舊伺服器請求資料,並獲取過時的資料,直到 TTL 過期。有兩個因素使得這種情況可以控制:NS 委託 TTL 為 5 分鐘,只有在該時間段內部署的新服務才會受到影響。兩組名稱伺服器上現有的 A/AAAA 記錄完全相同。
我們選擇在周六晚上平台用戶最少的時候進行操作。過程很順利,沒有用戶投訴!
我一開始覺得AXFR就夠了。每個教程都用它,每個範例都用它,而且我也是第一個用它建構的。完整的區域轉儲,開頭用SOA,結尾用SOA,搞定。
事實證明,Hetzner Robot 的從屬節點並非只執行 AXFR。當它們已經擁有一個區域,並透過 NOTIFY 接收到新的 SOA 序號時,它們會先要求增量區域傳輸( IXFR,RFC 1995 ),即僅傳輸自舊序號以來發生變更的記錄。如果主節點不支援 IXFR,則行為良好的從屬節點會回退到 AXFR。但 Hetzner Robot 顯然並非在所有情況下都能完美回退,因此在我們實現 IXFR 之前,區域更新一直不可靠。
IXFR 並不難,你只需要維護一個包含近期區域版本歷史記錄的小日誌,並在收到請求時傳回客戶端序號與目前序號之間的差值即可。但這只有透過實際部署到真實的備用伺服器才能發現。感謝撰寫該 RFC 的作者。
目前為止,成功率達 100%。傳播時間從「最多 90 分鐘」縮短到區域轉移所需的時間,對於我們目前的區域大小來說,幾乎是瞬間完成。區域會隨著平台的發展而擴展,不會達到任何記錄上限,而且我們還內建了完整的可觀測性。
可能不行。用 Cloudflare DNS、Route 53 或你的服務提供者提供的任何託管 DNS 服務。它們速度快、穩定可靠,你不用擔心。
但如果您最終確實遇到了託管 DNS 服務商的限制,那麼了解隱藏主 DNS 模式就很有幫助了。您的主 DNS 不需要公開,您可以使用任何相容 AXFR 的輔助 DNS,而且您可以在不更改伺服器的情況下更換服務提供者。
乾杯,
Jonas, sliplane.io共同創辦人
原文出處:https://dev.to/code42cate/how-we-built-our-own-dns-server-4d3k