hello大家好,我是小樓。
最近踩了個DNS解析的小坑,雖然問題解決了,但排查過程比較曲折,最後還是有一點沒有想通,整個過程分享給大家。
背景
最近負責的服務要置換機器。置換機器可能很多小夥伴不知道是幹啥,因為大家平時接觸不到,我簡單解釋一下什麼是機器置換以及為什麼需要機器置換。
機器置換通俗地講就是更換機器,把服務從一臺機器遷移到另一臺上去。
為什麼要機器置換呢? 表面原因可能是機器硬體故障、或者機器過了保修期。
有些小夥伴可能就想問,我在公司也負責了很多服務,為啥從來沒有置換過機器呢?原因可能是用了容器,沒有直接部署在物理機上,置換機器的任務被轉移給了雲平臺的運維人員;還可能是你們有專門的運維幫忙做了這件事,對開發人員來說幾乎是透明的。
我負責的服務為啥要置換呢?因為機器過保了。服務為啥部署在物理機上呢?因為它是個基礎服務,和一般服務不太一樣,有一些限制,只能在物理機上部署。為啥沒有運維人員幫忙呢?因為公司很多基礎服務是自運維,開發者既做開發又是運維。
說完機器置換,再來聊聊這個基礎服務,它是一個Go寫的服務,不停地傳送HTTP請求,記住這點就好,其他不重要。
這個服務在置換機器後,HTTP請求的耗時慢了不少,如下圖,黃色為老機器,藍色為新機器,指標的值就是HTTP請求的耗時(毫秒),大概1.5倍的差距。這就是今天要分享的問題,接下來說說我的排查過程。
問題排查
這種情況,先去看了機器的各項指標,如CPU、網路情況等等,看看是否有異常,確認是否被其他指標影響了。但看了一圈下來,發現新機器的各項指標甚至還優於老機器。
接著去詢問了提供機器的同學,看看機器是否有異常,結果也是沒有。
既然HTTP請求變慢,就想到看看是請求的哪個環節變慢了,用如下的命令來測試下,域名我用百度的域名來代替:
curl -o /dev/null -s -w %{time_namelookup}::%{time_connect}::%{time_total}"\n" http://www.baidu.com
這裡的各個引數代表含義(還有一些其他引數也可用):
- time_total 總時間,按秒計。精確到小數點後三位。
- time_namelookup DNS解析時間,從請求開始到DNS解析完畢所用時間。
- time_connect 連線時間,從開始到建立TCP連線完成所用時間,包括前邊DNS解析時間,如果需要單純的得到連線時間,用這個time_connect時間減去前邊time_namelookup時間。以下同理,不再贅述。
- time_appconnect 連線建立完成時間,如SSL/SSH等建立連線或者完成三次握手時間。
- time_pretransfer 從開始到準備傳輸的時間。
- time_redirect 重定向時間,包括到最後一次傳輸前的幾次重定向的DNS解析,連線,預傳輸,傳輸時間。
- time_starttransfer 開始傳輸時間。在client發出請求之後,Web 伺服器返回資料的第一個位元組所用的時間
這樣能看到域名解析、連線、傳輸各個階段的耗時情況,新老機器對比,如果有一項特別高,那麼這項肯定有問題
- 新機器:0.001484::0.001743::0.007489
- 老機器:0.000681::0.000912::0.002475
簡單計算一下:
- 新機器:DNS解析耗時0.001484秒,連線建立耗時0.000258秒,總耗時0.007489秒
- 老機器:DNS解析耗時0.000681秒,連線建立耗時0.000231秒,總耗時0.002475秒
雖然從這次的測試資料來看,新機器DNS解析似乎慢了一點,但你仔細看這個數值,幾乎對請求的總體耗時沒啥影響,而且多測試幾次,發現這兩臺機器的DNS解析其實差不多。
但還是不放心,驗證DNS是否存在問題,再用dig命令去試一下
dig www.baidu.com
執行時,明顯感覺到了卡頓,確定是DNS有問題了。
問題解決
一開始,我去網上搜尋了一下DNS慢的相關文章,找到了一篇文章《記一次Go net庫DNS問題排查》,但稍微驗證了下,和我的case沒啥關係,文章是好文章,所以也貼個連結,感興趣可以讀讀。
《記一次Go net庫DNS問題排查》https://juejin.cn/post/6948469896007122974
接著就去找了網路組的同學,網路組的同學稍微看了一眼就知道原因了,說新機器沒有安裝DNSmasq,這又是個啥?不要慌,先去網上查下再接話。
DNSmasq 提供 DNS 快取和 DHCP 服務功能。作為域名解析伺服器(DNS),DNSmasq可以通過快取 DNS 請求來提高對訪問過的網址的連線速度。作為DHCP 伺服器,DNSmasq 可以用於為區域網電腦分配內網ip地址和提供路由。DNS和DHCP兩個功能可以同時或分別單獨實現。DNSmasq輕量且易配置,適用於個人使用者或少於50臺主機的網路。此外它還自帶了一個 PXE 伺服器。
簡單來說,這裡它扮演的是一個DNS快取的角色,提高DNS的查詢速度。
說到這裡,插播一個小知識,我一直以為DNS會被作業系統快取,不知道你們有沒有這樣的錯覺,但實際上,Linux下如果沒有特殊處理,每一次DNS解析都要查詢DNS伺服器。很好證明,可以用tcpdump抓DNS的包試試,我當時也試了下,每次都會去遠端拿DNS解析結果。這個結論在《TCP/IP詳解卷1》中也能找到相關的描述:
只有Windows和比較新的Linux系統可以在客戶端快取DNS,而且Linux系統是需要手動開啟的,所以預設情況下都要去遠端獲取DNS快取。
言歸正傳,網路組同學說要麼裝一個DNSmasq,要麼改下DNS伺服器的配置,也就是/etc/resolv.conf
檔案,由於機器上已經有服務了,所以選擇了改配置這種比較安全的方式。
沒改之前,/etc/resolv.conf 的第一行是127.0.0.1,也就是將本地也作為DNS伺服器,但實際上本地沒有開啟DNS服務,網路組同學說,去掉第一行配置或者安裝DNSmasq都可以。
先是去掉了127.0.0.1的配置,結果耗時不變!
隨後加上127.0.0.1的配置,又安裝了DNSmasq後,耗時就降下去了。
整個解決的過程,程式沒有重啟,唯一的變數是安裝了DNSmasq,所以這一定是DNS的鍋了。
問題反思
雖然問題解決了,但我還有幾個疑問:
- 為什麼配置了127.0.0.1的DNS server,但沒有開啟DNSmasq呢?
- 為什麼去掉127.0.0.1配置會無效呢?
第1個問題比較好搞清楚,問了下系統部的同學,他說本來是應該開啟DNSmasq的,但出了一點點小差錯,結果只配置了127.0.0.1。
再看第2個問題,DNS本地快取和遠端查詢差距這麼大嗎?據網路組同學說DNS server是公司內自建的,內網傳輸,實際並不慢,用dig也好測試,使用第2、3行的DNS server測試下,發現dig的速度都很快。
dig www.baidu.com @host
為什麼有了127.0.0.1的配置就變得很慢呢?下面就從我的幾個猜測入手,一個個證明,但在猜測之前,我們先了解一下Go程式解析DNS的流程。
Go的DNS解析流程
Go的DNS解析分為兩種:
- cgo方式,呼叫c語言標準庫的實現
- 純Go程式碼實現
由於要適配各個平臺,所以又有了各個平臺的實現。
這部分程式碼位於net
包下,想要跟蹤也很簡單,寫個建立連線的程式碼,一步步debug,找到域名解析的地方。
我直接告訴你從lookup_unix.go
檔案的lookupIP
方法看起,當然這只是Unix系統,包括Mac和Linux,不過Mac不走純Go的程式碼,它被強制走到cgo了,在Linux上沒有特殊配置是走純Go實現的DNS解析,以下程式碼以Linux為例:
func (r *Resolver) lookupIP(ctx context.Context, network, host string) (addrs []IPAddr, err error) {
// ①強制走純Go的DNS解析器
if r.preferGo() {
return r.goLookupIP(ctx, host)
}
// ②根據解析順序解析
order := systemConf().hostLookupOrder(r, host)
if order == hostLookupCgo {
if addrs, err, ok := cgoLookupIP(ctx, network, host); ok {
return addrs, err
}
// cgo not available (or netgo); fall back to Go's DNS resolver
// ③如果cgo搞不定,降級到先檔案再DNS
order = hostLookupFilesDNS
}
ips, _, err := r.goLookupIPCNAMEOrder(ctx, host, order)
return ips, err
}
這裡order有如下幾種
hostLookupCgo hostLookupOrder = iota // cgo
hostLookupFilesDNS // 檔案優先
hostLookupDNSFiles // DNS優先
hostLookupFiles // 只查檔案
hostLookupDNS // 只查DNS
這裡的檔案也就是/etc/hosts
,goLookupIP 最終也呼叫了 goLookupIPCNAMEOrder,但goLookupIPCNAMEOrder這個方法的程式碼太長,所以我這裡只講一下大致的流程:
- 如果需要先查詢hosts檔案,則先查,查到直接返回
- 讀取/etc/resolv.conf檔案,拿出DNS server的配置,並且每5秒更新一次
- 構造DNS請求並向伺服器傳送,UDP讀取的超時時間預設為5秒,可在/etc/resolv.conf檔案中配置,同一個域名的不同型別(如ipv4和ipv6)的查詢可配置為並行或序列
- 向DNS server傳送請求採用的是輪詢機制,如果其中一個server請求出錯,則順延至下一個,重試次數預設為2,可在/etc/resolv.conf檔案中配置
- 最後解析查詢結果並返回,如果結果為空,且配置了hosts檔案兜底,則查詢一次檔案
好了,流程簡單介紹到這裡,接下來驗證我的幾個猜想。
猜想一:Go是否只在程式啟動時讀取一次/etc/resolv.conf檔案
這個猜想的依據是,如果查詢DNS時拿到了127.0.0.1的DNS server,且本地未開啟DNS服務時,可能會慢,且配置檔案如果修改了,Go程式如果只在初始化時讀一次檔案,那自然改配置檔案無效。
但事實並非如此,上面也說了,Go在讀取DNS配置檔案時是惰性地每隔5秒更新一次
func (conf *resolverConfig) tryUpdate(name string) {
// 初始化,只做一次
conf.initOnce.Do(conf.init)
// ...
now := time.Now()
if conf.lastChecked.After(now.Add(-5 * time.Second)) {
return
}
conf.lastChecked = now
// ...
dnsConf := dnsReadConfig(name)
conf.mu.Lock()
conf.dnsConfig = dnsConf
conf.mu.Unlock()
}
而且我做了個實驗,寫了個DNS解析的測試程式碼,放在有127.0.0.1配置但未開啟DNSmasq的伺服器上跑,抓127.0.0.1 53埠(DNS預設埠)的包,發現是有流量的,然後修改/etc/resolv.conf配置,去掉127.0.0.1,發現抓不到127.0.0.1 53埠的流量了,這證明和程式碼邏輯一致,本猜想不成立。
猜想二:DNS查詢遠端比本地慢很多
這個很好證明,還是用上面的程式
- 放在無127.0.0.1配置的伺服器上跑
- 放在有127.0.0.1配置且開啟DNSmasq的伺服器上跑
結果兩者耗時差不多,甚至他們和在有127.0.0.1配置但未開啟DNSmasq的伺服器上的耗時也基本一致。
這說明無論怎樣查詢DNS都不慢。
猜想三:是否是併發太高導致
為什麼我會有這個猜想呢,一是線上的QPS大概是50左右,和上面測試的場景不太一樣,二是我在上面的程式碼中看到了鎖,是不是併發高了之後,鎖帶來的開銷變大導致?
我寫了個100併發的程式碼,去查詢DNS,結果發現這段程式碼在如下三種場景,耗時都差不多
- 無127.0.0.1配置的伺服器
- 有127.0.0.1配置且開啟DNSmasq的伺服器
- 有127.0.0.1配置且未開啟DNSmasq的伺服器
同時我也去問了網路組的同學,他說DNS server能抗住百萬QPS,服務端沒有壓力。
最後
寫到最後,我emo了~雖然問題解決了,但為什麼當時DNS查詢慢還是不知道,如果你看了文章知道其中哪裡有問題,或者有什麼比較好的排查方法,歡迎來探討,反正我是查不下去了。
最後再說一句,寫文章很辛苦,需要點鼓勵,來個點贊
、在看
、關注
吧,我們下期再見。
搜尋關注微信公眾號"捉蟲大師",後端技術分享,架構設計、效能優化、原始碼閱讀、問題排查、踩坑實踐。