精心設計的 DNS Failover 策略在 Go 中竟然帶來了反效果,發生了什麼?
來源:嗶哩嗶哩技術
一. 背景
如下配置所示,我們在 /etc/resolv.conf 中配置了兩個 nameserver,其中 server2 在災備機房 ,作為一種 failover 策略。
nameserver server1nameserver server2options timeout:1 attempts:1
我們的預期是如果 server1 服務正常,則所有的 DNS 請求應該由 server1 處理,且 server2 故障不應對業務有任何影響 。只有當 server1 服務異常,DNS 請求才應該重試到 server2。
然而我們線上上觀察到一直有 AAAA 型別的 DNS 請求傳送到 server2,而且如果 client 到 server2 的網路異常時,業務的 http 請求耗時會增加 1s,這並不符合預期。同時因為我們的內網域名都沒有 AAAA 記錄,且內網伺服器也是關閉了 IPv6 協議的,AAAA 請求也不符合預期。
二. 問題排查
經過和業務同學求證,相關程式語言為 Go ,請求使用的是 Go 原生 net 庫。在 Go net 庫中,最經常使用的方式如下:
package main import ( "net" "net/http") func main() { http.Get(") net.Dial("tcp", "internal.domain.name:443")}
1. 梳理原始碼
讓我們順著原始碼分析 net 庫的解析邏輯。無論是 http.Get 還是 net.Dial 最終都會到 func (d *Dialer) DialContext() 這個方法。然後層層呼叫到 func (r *Resolver) lookupIP() 方法,這裡定義了何時使用 Go 內建解析器或呼叫作業系統 C lib 庫提供的解析方法,以及 /etc/hosts 的優先順序。
同時補充一個比較重要的資訊:windows 、darwin(MacOS等)優先使用 C lib 庫解析,debug 時需要注意。
func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error) { ... addrs, err := d.resolver().resolveAddrList(resolveCtx, "dial", network, address, d.LocalAddr) ...} func (r *Resolver) resolveAddrList(ctx context.Context, op, network, addr string, hint Addr) (addrList, error) { ... addrs, err := r.internetAddrList(ctx, afnet, addr) ...} func (r *Resolver) internetAddrList(ctx context.Context, net, addr string) (addrList, error) { ... ips, err := r.lookupIPAddr(ctx, net, host) ...} func (r *Resolver) lookupIPAddr(ctx context.Context, network, host string) ([]IPAddr, error) { ... resolverFunc := r.lookupIP ... ch := r.getLookupGroup().DoChan(lookupKey, func() (any, error) { return testHookLookupIP(lookupGroupCtx, resolverFunc, network, host) }) ...} func (r *Resolver) lookupIP(ctx context.Context, network, host string) (addrs []IPAddr, err error) { if r.preferGo() { return r.goLookupIP(ctx, network, host) } order, conf := systemConf().hostLookupOrder(r, host) if order == hostLookupCgo { return cgoLookupIP(ctx, network, host) } ips, _, err := r.goLookupIPCNAMEOrder(ctx, network, host, order, conf) return ips, err}
我們線上的作業系統是 Debain,確認會使用 Go 內建解析器。所以下一步來到了 func (r *Resolver) goLookupIPCNAMEOrder() 方法。這裡我們可以透過 qtypes 看到如果 net.Dial 的 network 引數傳入的是 tcp ,域名的 A 和 AAAA 記錄都會被查詢,無論伺服器是否關閉 ipv6。
func (r *Resolver) goLookupIPCNAMEOrder(ctx context.Context, network, name string, order hostLookupOrder, conf *dnsConfig) (addrs []IPAddr, cname dnsmessage.Name, err error) { ... lane := make(chan result, 1) qtypes := []dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeAAAA} switch ipVersion(network) { case '4': qtypes = []dnsmessage.Type{dnsmessage.TypeA} case '6': qtypes = []dnsmessage.Type{dnsmessage.TypeAAAA} } var queryFn func(fqdn string, qtype dnsmessage.Type) var responseFn func(fqdn string, qtype dnsmessage.Type) result if conf.singleRequest { queryFn = func(fqdn string, qtype dnsmessage.Type) {} responseFn = func(fqdn string, qtype dnsmessage.Type) result { dnsWaitGroup.Add(1) defer dnsWaitGroup.Done() p, server, err := r.tryOneName(ctx, conf, fqdn, qtype) return result{p, server, err} } } else { queryFn = func(fqdn string, qtype dnsmessage.Type) { dnsWaitGroup.Add(1) go func(qtype dnsmessage.Type) { p, server, err := r.tryOneName(ctx, conf, fqdn, qtype) lane <- result{p, server, err} dnsWaitGroup.Done() }(qtype) } responseFn = func(fqdn string, qtype dnsmessage.Type) result { return <-lane } } for _, fqdn := range conf.nameList(name) { for _, qtype := range qtypes { queryFn(fqdn, qtype) } } ... for _, qtype := range qtypes { result := responseFn(fqdn, qtype) } ...}
從 goLookupIPCNAMEOrder 方法中我們可以看到由 tryOneName 方法分別處理 A 和 AAAA 記錄。深入 tryOneName 內部,我們終於發現具體的 nameserver 選擇邏輯,在某些錯誤情況下會重試請求到下一個 nameserver。
func (r *Resolver) tryOneName(ctx context.Context, cfg *dnsConfig, name string, qtype dnsmessage.Type) (dnsmessage.Parser, string, error) { ... q := dnsmessage.Question{ Name: n, Type: qtype, Class: dnsmessage.ClassINET, } for i := 0; i < cfg.attempts; i++ { for j := uint32(0); j < sLen; j++ { server := cfg.servers[(serverOffset+j)%sLen] p, h, err := r.exchange(ctx, server, q, cfg.timeout, cfg.useTCP, cfg.trustAD) ... if err := checkHeader(&p, h); err != nil { dnsErr := &DNSError{ Err: err.Error(), Name: name, Server: server, } if err == errServerTemporarilyMisbehaving { dnsErr.IsTemporary = true } if err == errNoSuchHost { // The name does not exist, so trying // another server won't help. dnsErr.IsNotFound = true return p, server, dnsErr } lastErr = dnsErr continue } ...}
2. 線上 debug
接下來我們可以構造一個簡單的程式線上上 debug,看看到底是因為原因導致 AAAA 請求重試到了下一個 nameserver。(tips: debug 需要把 resolv.conf 的 timeout 調長一些)
package main import ( "net") func main() { c, err := net.Dial("tcp", "internal.domain.name:80") if err != nil { return } _ = c.Close()}
dlv debug main.go(dlv) break /usr/local/go/src/net/dnsclient_unix.go:279(dlv) break /usr/local/go/src/net/dnsclient_unix.go:297(dlv) continue(dlv) print errerror(*errors.errorString) *{ s: "lame referral",}
透過 debug 我們最終定位到 err 由下面這段程式碼丟擲。
func checkHeader(p *dnsmessage.Parser, h dnsmessage.Header) error { ... // libresolv continues to the next server when it receives // an invalid referral response. See golang.org/issue/15434. if h.RCode == dnsmessage.RCodeSuccess && !h.Authoritative && !h.RecursionAvailable && err == dnsmessage.ErrSectionDone { return errLameReferral } ....}
原來如果返回的 DNS response 以下4個條件全部滿足,就會觸發重試邏輯:
響應沒有錯誤
應答 Server 不是權威伺服器
應答 Server 不支援遞迴請求
應答的 records 為空
這裡有一個疑點是我們的 DNS Server 是支援遞迴請求的,經過排查,我們發現是因為在 DNS Server 有一層 NetScaler 作為負載均衡器,負載均衡是以 DNS proxy server 的方式執行,預設並沒有開啟對遞迴請求的支援。
我們可以執行 dig 命令觀察是否有如下輸出來判斷 server 是否支援遞迴請求。
;; WARNING: recursion requested but not available
3. 原因梳理
至此,我們已經弄清楚了為什麼會有 AAAA 型別的請求傳送到 nameserver2。而文章開頭提到的業務 http 請求耗時增加 1s 的原因則是因為 client 到 server2 網路異常時,需要等待重試的 AAAA 請求超時,才會返回解析結果。
還有一個問題困擾著我們,為什麼用 ping 等程式驗證,並沒有發現類似的問題。我們透過直接用 C getaddrinfo 函式測試,以及透過 -tags 'netcgo' 編譯相同的 go 程式驗證,發現在 A 記錄有值的情況下,AAAA 請求都不會重試到下一個 nameserver。回到 Go 中觸發重試的這段程式碼深入分析,註釋中可以看到由 golang.org/issue/15434 引入,提交程式碼的作者是為了解決 issue 中的問題複製了 libresolv 的行為。然而翻閱 glibc 的程式碼可以看到 next_ns 中還有這樣一段邏輯:只要 A 或者 AAAA 任意一個有記錄值,都不會重試到下一個 nameserver。這段邏輯並沒有引入 Go 中。所以我們需要注意 Go 內建解析器與 glibc 中的行為和結果都有差異,它可能會影響到我們的服務。
next_ns: if (recvresp1 || (buf2 != NULL && recvresp2)) { *resplen2 = 0; return resplen; } ... if (anhp->rcode == NOERROR && anhp->ancount == 0 && anhp->aa == 0 && anhp->ra == 0 && anhp->arcount == 0) { goto next_ns;}
三. 最佳化
經過上面的排查,我們已經確認了 AAAA 請求的源頭,以及為什麼會重試到下一個 server。接下來可以針對性的最佳化。
1. 對於 Go 程式中 AAAA 請求重試到下一個 server 的最佳化方案:
a. 代價相對較小的方案,程式構建時新增 -tags 'netcgo' 編譯引數,指定使用 cgo-based 解析器。
b. DNS Server proxy 層支援遞迴請求。這裡有必要說明遞迴支援不能在 proxy 層簡單的直接開啟,proxy 和 recursion 在邏輯上有衝突的地方,務必做好必要的驗證和確認,否則可能會帶來新的問題。
2. 如果業務程式不需要支援 IPv6 網路,可以透過指定網路型別為 IPv4,來消除 AAAA 請求,同時避免隨之帶來的問題。(也順帶減少了相關開銷)
a. net.Dial 相關方法可以指定 network 為 tcp4、udp4 來強制使用 IPv4
net.Dial("tcp4", "internal.domain.name:443")net.Dial("udp4", "internal.domain.name:443")
b. net/http 相關方法可以透過如下示例來強制使用 IPv4
package main import ( "context" "log" "net" "net/http" "time") func main() { dialer := &net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, } transport := http.DefaultTransport.(*http.Transport).Clone() transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { return dialer.DialContext(ctx, "tcp4", addr) } httpClient := &http.Client{ Timeout: 30 * time.Second, } httpClient.Transport = transport resp, err := httpClient.Get(") if err != nil { log.Fatal(err) } log.Println(resp.StatusCode)}
四. 總結
Go net 庫中提供了兩種解析邏輯:自實現的內建解析器和系統提供的解析函式。windows 、darwin(MacOS等)優先使用系統提供的解析函式,常見的 Debain、Centos 等優先使用內建解析器。
Go net 庫中的內建解析器和系統提供的解析函式行為和結果並不完全一致,它可能會影響到我們的服務。
業務應設定合理的超時時間,不易過短,以確保基礎設施的 failover 策略有足夠的響應時間。
來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70027824/viewspace-3006913/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- go func 時發生了什麼Go
- 區塊鏈現在帶來了什麼區塊鏈
- 我學習的程式設計,都給我帶來了什麼?程式設計
- DNS是什麼?DNS在網路通訊中的作用是什麼?DNS
- AI會給晶片設計帶來什麼?AI晶片
- 寫了 100 萬行程式碼的程式設計師身上發生了什麼故事行程程式設計師
- 在瀏覽器中輸入url到頁面顯示出來的過程發生了什麼?瀏覽器
- new Vue發生了什麼Vue
- 深度神經網路中深度究竟帶來了什麼?神經網路
- 智慧家居給年輕人帶來了什麼?
- JS每日一題:new Vue()中發生了什麼?JS每日一題Vue
- 模型中到底什麼決定了效果模型
- 5G給區塊鏈開發帶來了什麼區塊鏈
- 計設模式在軟體開發中起到什麼作用模式
- 精讀《Serverless 給前端帶來了什麼》Server前端
- Node.js 給前端帶來了什麼Node.js前端
- 從在瀏覽器中輸 URL 網址之後到底發生了什麼?瀏覽器
- 網路程式設計(四):輸入一個URL後發生了什麼?程式設計
- 智慧設計效果保障策略
- 本地生活小程式突出重圍,對商家和運營者帶來了什麼效果?
- 大資料的發展,給我們生活帶來了什麼影響?大資料
- 經典面試題—在瀏覽器中輸入URL之後發生了什麼?面試題瀏覽器
- 當執行時,發生了什麼?
- kubelet 建立 Pod 前發生了什麼?
- 猜猜體育課發生了什麼?
- [React技術內幕] key帶來了什麼React
- 微信小程式的出現給前端帶來了什麼微信小程式前端
- 什麼是反應式程式設計?程式設計
- DNS解析是什麼?DNS解析在網路通訊中作用有哪些?DNS
- Android在橫豎屏切換時到底發生了什麼?Android
- 教你如何檢視 Git 提交中發生了什麼變化Git
- RAG實戰4-RAG過程中發生了什麼?
- 網頁開啟時都發生了什麼?我被嚇著了網頁
- 反轉!BAT程式設計吸金榜來了,AI程式設計師刷爆了......BATAI程式設計師
- 什麼仇?什麼怨?我竟然愛上大資料了!大資料
- 什麼是前端開發中的 mobile first 策略前端
- 併發程式設計帶來的挑戰程式設計
- 在powerpoint中設定幻燈片背景的填充效果選項中包含什麼