有同學通過zipkin
發現dns
解析偶爾會花費40ms(預期是1ms以內),並且猜測和alpine
映象有關係。
第一反應不太可能是alpine
映象的問題(alpine
映象使用這麼頻繁,如果有問題應該早就修復了),下面針對這個問題進行分析。
首先我們瞭解下golang
中如何進行dns解析的。直接看程式碼,關鍵函式goLookupIPCNAMEOrder
// src/net/dnsclient_unix.go
func (r *Resolver) goLookupIPCNAMEOrder(ctx context.Context, network, name string, order hostLookupOrder) (addrs []IPAddr, cname dnsmessage.Name, err error) {
// 省略檢查程式碼
// 讀取/etc/resolv.conf,防止讀取頻繁,5秒鐘生效一次
resolvConf.tryUpdate("/etc/resolv.conf")
// ...
// 預設解析ipv4和ipv6
qtypes := []dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeAAAA}
// 【關鍵】根據network的不同,4結尾的只解析ipv4,6結尾的只解析ipv6
switch ipVersion(network) {
case '4':
qtypes = []dnsmessage.Type{dnsmessage.TypeA}
case '6':
qtypes = []dnsmessage.Type{dnsmessage.TypeAAAA}
}
// ...
// 判斷/etc/resolv.conf裡面的single-request和single-request-reopen引數,如果設定的話,就是序列請求,否者是並行請求
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關鍵字了麼?沒有設定single-request就是併發解析
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
}
}
// 下面程式碼也很重要
var lastErr error
// len(namelist) = len(search domain) + 1
// 遍歷nameserver,resolv.conf中可以配置多個nameserver,比如下面的配置namelist長度就是4:
// nameserver 169.254.20.10
// nameserver 172.16.0.10
// search meipian-test.svc.cluster.local svc.cluster.local cluster.local
for _, fqdn := range conf.nameList(name) {
// ...
// 遍歷解析型別,這裡就是ipv4和ipv6
for _, qtype := range qtypes {
// ....
}
}
// ...
return addrs, cname, nil
}
通過以上程式碼我們可以得出以下結論:
go實現了dns
解析
Dns
解析跟是不是alpine
映象沒有關係,因為go
中dns解析
是自己實現的,不依賴於系統呼叫。go build tag
也證明了這一點
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
內建解析器會讀取配置檔案
go
程式會讀取並解析/etc/resolv.conf
檔案,並且標準選項都有實現,包括single-request
和single-request-reopen option
設定。
// src/net/dnsconfig_unix.go
case s == "single-request" || s == "single-request-reopen":
// Linux option:
// http://man7.org/linux/man-pages/man5/resolv.conf.5.html
// "By default, glibc performs IPv4 and IPv6 lookups in parallel [...]
// This option disables the behavior and makes glibc
// perform the IPv6 and IPv4 requests sequentially."
conf.singleRequest = true
single-request引數是有效的
如果設定了single request
選項,dns
解析的時候是序列的
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}
}
}
如果沒有設定single-request
選項,dns解析
是並行的(真實情況是並行和序列結合的)。
if conf.singleRequest {
// ...
} 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
}
}
解析過程和配置相關
dns
解析策略、次數和ndots
、search domain
和nameserver
配置強相關:
預設情況下
dns
查詢會同時解析IPv4
和IPv6
地址(不論容器是否支援IPv6
)ndots
和待解析的域名決定要不要優先使用search domain
,通俗一點說,如果你的域名請求引數中,點的個數
比配置的ndots
小,則會優先拼接search domain
後去解析,比如有如下配置:search meipian-test.svc.cluster.local svc.cluster.local cluster.local options ndots:3
如果現在解析的域名是
www.baidu.com
,ndots
配置的是3
,待解析域名中的點數(2)比 ndots 小,所以會優先拼接搜尋域名去解析,解析順序如下:- www.baidu.com.meipian-test.svc.cluster.local.
- www.baidu.com.svc.cluster.local.
- www.baidu.com.cluster.local.
- www.baidu.com.
如果配置檔案中
ndots
等於2
,則解析順序如下:serach domain
和nameserver
決定了dns
最多查詢的次數,即查詢次數等於搜素域的數量+1
乘以dnsserver的數量
。比如有以下配置:nameserver 169.254.20.10 nameserver 172.16.0.10 search meipian-test.svc.cluster.local svc.cluster.local cluster.local options ndots:3
當我們解析
www.baidu.com
域名時,解析順序如下:解析域名 查詢型別 dns server www.baidu.com.meipian-test.svc.cluster.local. A 169.254.20.10 www.baidu.com.meipian-test.svc.cluster.local. A 172.16.0.10 www.baidu.com.meipian-test.svc.cluster.local. AAAA 169.254.20.10 www.baidu.com.meipian-test.svc.cluster.local. AAAA 172.16.0.10 www.baidu.com.svc.cluster.local. A 169.254.20.10 www.baidu.com.svc.cluster.local. A 172.16.0.10 www.baidu.com.svc.cluster.local. AAAA 169.254.20.10 www.baidu.com.svc.cluster.local. AAAA 172.16.0.10 www.baidu.com.cluster.local. A 169.254.20.10 www.baidu.com.cluster.local. A 172.16.0.10 www.baidu.com.cluster.local. AAAA 169.254.20.10 www.baidu.com.cluster.local. AAAA 172.16.0.10 www.baidu.com. A 169.254.20.10 www.baidu.com. A 172.16.0.10 www.baidu.com. AAAA 169.254.20.10 www.baidu.com. AAAA 172.16.0.10 一共16次,是不是很恐怖?當然只有在最壞的情況(比如域名確實不存在時)才會有這麼多次請求。
⚠️ 序列和並行請求是如何結合的?
並行是指同一個域名的去同一個dns server解析不同的型別時是並行的,不同的域名之間還是序列的。
把請求放在時間線上就像下面這樣:
上圖話的是最壞的情況,實際上過程中只要有一次解析成功就返回了。
內建解析器引數預設值
ndots: 1,
timeout: 5 * time.Second, // dns解析超時時間為5秒,有點太長了
attempts: 2, // 解析失敗,重試兩次
defaultNS = []string{"127.0.0.1:53", "[::1]:53"} // 預設dns server
search:os.Hostname //
其中需要注意的就是timeout
,建議在resolv.conf
上加上這個引數,並且寫個較小的值。因為dns
解析預設是udp
請求(不可靠),如果發生丟包情況就會等5s。
上面說到go
使用的是內建解析器,其實並不是所有情況都是這樣的。
兩種解析器
golang
有兩種域名解析方法:內建go
解析器和基於cgo
的系統解析器。
// src/net/cgo_stub.go
//go:build !cgo || netgo
// +build !cgo netgo
func init() { netGo = true }
// src/net/conf_netcgo.go
//go:build netcgo
// +build netcgo
func init() { netCgo = true }
預設情況下用的是內建解析,如果你想指定使用cgo
解析器,可以build
的時候指定。
export GODEBUG=netdns=go # force pure Go resolver
export GODEBUG=netdns=cgo # force cgo resolver
內建解析器解析策略
當goos=linux
下使用的是 hostLookupFilesDNS
,也就是說,hosts
解析優先dns
解析(go1.17.5)。
const (
// hostLookupCgo means defer to cgo.
hostLookupCgo hostLookupOrder = iota
hostLookupFilesDNS // files first
hostLookupDNSFiles // dns first
hostLookupFiles // only files
hostLookupDNS // only DNS
)
var lookupOrderName = map[hostLookupOrder]string{
hostLookupCgo: "cgo",
hostLookupFilesDNS: "files,dns",
hostLookupDNSFiles: "dns,files",
hostLookupFiles: "files",
hostLookupDNS: "dns",
}
根據作業系統的不同,使用的解析策略也會略有不同,比如android
平臺就會強制使用cgo
// src/net/conf.go
fallbackOrder := hostLookupCgo
// ...
if c.forceCgoLookupHost || c.resolv.unknownOpt || c.goos == "android" {
return fallbackOrder
}
在go1.17
之前是沒有辦法禁用ipv6
解析的。1.17
之後go
提供了一些方式
// 預設是IPv4和IPv6都解析
qtypes := []dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeAAAA}
// 根據network的不同可以只解析ipv4或者只解析ipv6
switch ipVersion(network) {
case '4':
qtypes = []dnsmessage.Type{dnsmessage.TypeA}
case '6':
qtypes = []dnsmessage.Type{dnsmessage.TypeAAAA}
}
// ipVersion returns the provided network's IP version: '4', '6' or 0
// if network does not end in a '4' or '6' byte.
func ipVersion(network string) byte {
if network == "" {
return 0
}
n := network[len(network)-1]
if n != '4' && n != '6' {
n = 0
}
return n
}
所以想要禁用IPv6
解析的話就很容易了,我們只需要在建立連線的時候指定network
型別。以http
為例,重寫Transport
的DialContext
方法,將原來的network
(預設是tcp
)強制寫成tcp4
。
&http.Client{
Transport: &http.Transport{
// ....
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
// 強制使用ipv4解析
return zeroDialer.DialContext(ctx, "tcp4", addr)
},
}
}
go
預設使用內建dns
解析器,不依賴作業系統,跟基礎映象無關go
內建解析器會讀取/etc/resov.conf
配置,標準配置都有實現,手動修改配置5秒後生效Go1.17
之後可以禁用ipv6
解析go
內建解析器解析過程預設是並行和序列結合的- 相同域名的不同請求型別是並行的
- 不同域名之間是序列的
優化建議
修改
ndots
為合適的值k8s
中如何配置的dnsPolicy
是ClusterFist
,預設ndots會是
5`- 如果微服務之前請求使用的是
service name
,那麼不需要修改(拼接搜尋域名之後是可以成功解析的) - 如果微服務之間請求使用的是域名(或者說拼接搜尋域名之後一定解析不到的情況下),需要將
ndots
設定成合適值,目標是把原始域名放在前面解析(拼接搜尋域名放在後面)
- 如果微服務之前請求使用的是
修改
timeout
為合適的值go
預設是5s
,因為udp
請求的不可靠性,一旦遇到丟包情況,就會讓程式等到天荒地老禁用
Ipv6
解析開啟single-request
對於
go
內建解析器而言single-request
和single-request-reopen
是同一個意思,這決定了不同解析請求(A
或者AAAA
)是併發還是序列,預設是並行。如果禁用了IPv6
,就沒有併發解析的必要了,建議開始single-request
優化效果
dns解析只有有效的A記錄查詢了,世界突然安靜了。
本作品採用《CC 協議》,轉載必須註明作者和本文連結