背景分享
遇到過這么一個問題,有童鞋的 Go 程序用 DNS 解析做服務發現(內網用的 CoreDNS 做的域名解析服務器)。比如,內網有個服務域名,對應 7 個后端節點。為了做服務發現,故障的剔除等服務,在 Client 端對一個給定的域名調用 Go 標準庫的 Resolver.LookupHost 方法來解析 ip 列表。如果解析得到的 ip 列表有變化,那么在 Client 內對相應的對后端節點的鏈接做創建和銷毀。
addrs,err:=resolver.LookupHost(ctx,/*某服務域名A*/) //addrs的結果會變化,一會返回6個ip,一會返回7個ip
就是這么一個典型的服務發現的應用場景,還是精準踩坑。那什么坑?
坑就是:解析得到的 ip 列表反復變化,導致反復創、刪連接和對應的結構體。讓人誤以為 DNS 的后端節點一直在故障,從而導致一系列的問題。
還遇到另一個有趣的問題:同一份業務代碼,Go 1.15 編譯的版本總會頻繁截斷成 6 個 ip ,Go 1.16 以上的版本則非常穩定,一直返回 7 個 ip ? 這又是為啥呢。
這個問題很簡單,但其實也很隱蔽。因為很少人會這么用,也很少人會注意到這個問題。
Go 的 DNS Lookup 的接口語義
先看下 Go 標準庫的接口語義,看下 Resolver.LookupHost 在 Go 的注釋怎么說的。文件在 Go 的標準庫 net/lookup.go :
//LookupHostlooksupthegivenhostusingthelocalresolver. //Itreturnsasliceofthathost'saddresses. func(r*Resolver)LookupHost(ctxcontext.Context,hoststring)(addrs[]string,errerror){ //... }
LookupHost 查詢一個給定的域名,返回值是一個地址列表。注意:它并沒有保證,要返回該域名的所有 ip 列表。 所以啊,這本來就是用法不對,Go 的接口沒聲明說要返回全部的 ip 。哪怕有域名對應有 100 個 ip ,這個接口只返回 1 個也是對的。
Go 1.15 和 Go 1.16之上的區別 ?
域名對應 7 個 ip ,同一份解析代碼, Go 1.15 編譯的程序時而返回 6 個?但 Go 1.16 之上的版本編譯則總是 7 個,感覺非常穩定。為什么呢?
筆者還真翻了一下 Go 1.15 和 Go 1.16 的區別,DNS 解析的代碼幾乎一致,只在 dnsPacketRoundTrip 函數中,改了一個 buffer 的大小。
Go 1.15 是這樣的( 文件:src/net/dnsclient_unix.go ):
funcdnsPacketRoundTrip(cConn,iduint16,querydnsmessage.Question,b[]byte)(dnsmessage.Parser,dnsmessage.Header,error){ //發送請求 if_,err:=c.Write(b);err!=nil{ } //創建一個裝響應包的buffer b=make([]byte,512)//seeRFC1035 for{ //讀取dns響應 n,err:=c.Read(b) //... returnp,h,nil } }
Go 1.16 是這樣的( 文件:src/net/dnsclient_unix.go ):
const( //MaximumDNSpacketsize. //Valuetakenfromhttps://dnsflagday.net/2020/. maxDNSPacketSize=1232 ) funcdnsPacketRoundTrip(cConn,iduint16,querydnsmessage.Question,b[]byte)(dnsmessage.Parser,dnsmessage.Header,error){ //發送請求 if_,err:=c.Write(b);err!=nil{ } //創建一個裝響應包的buffer b=make([]byte,maxDNSPacketSize) for{ //讀取dns響應 n,err:=c.Read(b) //... returnp,h,nil } }
函數邏輯是發送請求給 DNS Server ,并等待它的響應。兩個版本完全一致,只有 buffer 的大小不一樣,Go 1.16 之上用了 1232 這個大小。請注意,這個大小其實是有講究的,這個值是在盡量避免 IP 包分片又能盡量多裝數據而拍的一個值。詳細看 DNS FLAG DAY 2020[1] 。
這就是 Go 1.15 ,Go 1.16 版本在內網域名解析中的差異。DNS 服務端雖然發了 7 個 ip 過來,但是 Go 1.15 編譯的版本用 512 個字節 buffer 裝不下,只解析到 6 個有效 ip,Go 1.16 版本則好點,客戶端用的 1232 個字節的 buffer 大一點,差別就在這個地方。
這里有個細節提一下:
DNS 的協議,Message 的 Header 有四個字段 QDCOUNT,ANCOUNT,NSCOUNT,ARCOUNT,是指明了數據包里各個 Record 有多少個,Answer 有多少個的。但是在協議實現的時候,往往不依賴于這幾個字段,因為它們可能被偽造攻擊。所以解析的 ip 列表都是按照實際解析結果來的,解析到多少個就多少個,而不是 Header 里聲明了多少個。
//Qdcount,Ancount,Nscount,Arcountcan'tbetrusted,astheyare //attackercontrolled.
簡單說下 DNS
DNS 協議默認使用 UDP 協議作為其傳輸層協議。UDP 的數據包是有限制的,DNS 的消息大小也是有限制的,基本大小限制為 512 字節,長消息會被截斷并且設置標記位。
所以,DNS 協議本身就從來沒承諾過,給你解析完整的 IP 列表。它這個名字對應的 IP 而已,至于全不全,它從沒承諾過。
本文并不詳細介紹 DNS 協議的原理。詳細見 RFC 1035[2] 和相關的文檔。為了突破數據包大小,或者其他的限制,也有擴展協議 EDNS ,可以參考 RFC 6891[3] 。
總之,用 DNS 解析 ip 這種方式來替代 consul 這種服務發現。感覺還是有欠缺的,或者說它的使用場景是不一樣的。
總結
Go 默認的解析方式其實有兩種,一種是 cgo 的方式調用 libc 庫的函數去解析,一種是 Go 自己的實現。本文討論的是默認的 Go 的方式。
DNS 做服務發現好像并不合適,和 consul 等組件不同。它有自己特定的協議約束著,如果一定要用 DNS 來做服務發現,那么請千萬要注意本文提到的知識點。
DNS 解析的 ip 列表,并不承諾它是全的。如果業務想用來做服務發現和剔除的功能,請千萬牢記。
DNS 服務端和客戶端的行為配合缺一不可,服務端會不會發全部?客戶端能不能收全部?各種差異都會導致解析出來的 ip 可能不一樣。
-
接口
+關注
關注
33文章
8496瀏覽量
150834 -
服務器
+關注
關注
12文章
9017瀏覽量
85182 -
編譯
+關注
關注
0文章
653瀏覽量
32806
原文標題:DNS 做服務發現,是坑嗎 ?
文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論