Go 中的 HTTP debug 技能 瞭解一下

我是一隻可愛的土撥鼠,專注於分享 Go 職場、招聘和求職,解 Gopher 之憂!歡迎關注我。

歡迎大家加入 Go 招聘交流羣,來這裏找志同道合的小夥伴!跟土撥鼠們一起交流學習。

最近小土把一個負責的 HTTP 服務搬上了公司的內部的 k8s 平臺。由於部分 HTTP 服務還未接入 k8s 平臺,所以內部服務之間的交互主要通過內網域名。說到這裏,一天下午小土負責的 A 服務跟下游服務 B 的請求一直報警超時,開始小土使用 tcpdump 定時抓包系統採集了一些 pcap 文件,由於小土對抓包也不是很在行,HTTP 抓包也碰了一鼻子灰。在跟同事交流的同時,想到了 httptrace 包,下面主要介紹一下 httptrace 和問題的定位過程。

HTTP trace 的介紹

Go 官方於 _2016 年 10 月 4 日_發的一篇博文 Introducing HTTP Tracing[1]。翻譯見 Go 的 HTTP tracing。文中主要舉了兩個案例:

  1. 將包含鉤子函數的 httptrace.ClientTrace 放入 request 的 context 中進行跟蹤;

  2. 對 http.Client 進行跟蹤,使用 http.RoundTripper wrapper 來標識當前的請求。

Go1.7 引入了 HTTP trace,可以在 HTTP 客戶端請求過程中收集一些更細粒度的信息,httptrace 包 [2] 提供了 HTTP trace 的支持,收集的信息可用於調試延遲問題,服務監控,編寫自適應系統等。httptrace 包提供了許多鉤子,在 HTTP 往返期間收集各種事件的信息,包括連接的創建、複用、DNS 解析查詢、寫入請求和讀取響應。

httptrace 包

httptrace 包提供了追蹤 HTTP Client 請求事件的機制,主要就是 trace.go 這個文件。trace_test.go 是對trace.go中的函數的單元測試。example_test.go是對 httptrace 的例子測試。

大家可以從這裏查看 trace.go 源碼 [3]

├── example_test.go
├── trace.go
└── trace_test.go

從官網博文例子中也可以看出使用了 ClientTrace 這個 struct 中的一些鉤子函數。下面羅列了 ClientTrace 中的函數列表。

type ClientTrace struct {
 GetConn              func(hostPort string)                             // 在創建連接之前調用
 GotConn              func(GotConnInfo)                                 // 連接成功後調用
 PutIdleConn          func(err error)                                   // 當連接用完時需要放回池子調用
 GotFirstResponseByte func()                                            // 當讀到響應的第一個字節時
 Got100Continue       func()                                            // 當收到的狀態碼是"100",則會調用Got100Continue繼續響應
 Got1xxResponse       func(code int, header textproto.MIMEHeader) error // 爲每個 1xx 信息響應頭調用,Got1xxResponse在最終的非 1xx 響應之前返回。
 DNSStart             func(DNSStartInfo)                                // 當DNS查詢開始時調用
 DNSDone              func(DNSDoneInfo)                                 // 當DNS查詢結束時調用
 ConnectStart         func(network, addr string)                        // 當一個新連接 Dial開始時
 ConnectDone          func(network, addr string, err error)             // 當一個新的連接的Dial完成時
 TLSHandshakeStart    func()                                            // TLS 握手開始時調用,當通過HTTP代理連接到一個HTTPS站點時,握手發生在代理處理完CONNECT請求之後。
 TLSHandshakeDone     func(tls.ConnectionState, error)                  // 在TLS握手後被調用,其中包括握手成功的連接狀態,或者握手失敗的非零錯誤。
 WroteHeaderField     func(key string, value []string)                  // 在傳輸時write完每個請求頭後被調用。
 WroteHeaders         func()                                            // 在傳輸時write完所有請求頭後調用的。
 Wait100Continue      func()                                            // 如果請求時指定了"Expect: 100-continue",且在傳輸過程中已經寫入,但是在寫請求正文之前還在等待 "100 Continue"。
 WroteRequest         func(WroteRequestInfo)                            // 在寫入請求後調用。在重試請求的情況下,它可以被多次調用。

}

很多同學也應該不是很清楚 100 狀態碼的含義,這裏科普一下

100 狀態碼 :HTTP100 狀態碼代表的意思是請繼續請求,即 HTTP 100 Continue 響應狀態。HTTP 100 (Http Status Code 100) 狀態是 HTTP 協議的一種響應碼,是我們請求訪問網站時,服務器端返回的 1xx 請求信息系列響應碼之一。狀態詳細說明:HTTP 100 表示客戶端應當繼續進行請求

引用自 HTTP 狀態碼 100 (Continue) 含義詳解 [4]。

問題定位破案

最後是如何定位到問題的呢?想必大家看了上面的一系列介紹,也應該猜到了。主要在內部封裝的 httpclient 中加入了 clienttrace,並在請求結束後打印了 trace 的 result,通過重新打包部署到 k8s 開發環境,等待問題復現並查看日誌,最後通過觀察 traceresult 發現是域名解析的時候出現了問題,內網域名解析道是空的 ip 信息,便把域名解析的問題同步報告給 k8s 部署平臺負責的同事。

下面是工程中的一些代碼實現。藉此機會小土也準備把 httptrace 工具集成到公共庫中,並可以通過 apollo 平臺配置 httptrace 開關來開啓 debug 定位具體問題。

func (c *HTTPClient) Do (request *http.Request,traceId string, debug bool) (*Response, error) {
  // ... define
  if debug {
  clientTrace, traceResult = clientTrace(traceId)
  request = request.WithContext(httptrace.WithClientTrace(request.Context(), clientTrace))
  }
  // ... retry
  res,err := c.client.Do(request)
  if debug {
  data, _ := json.Marshal(traceResult)
  log.Debugf("[%v] trace info: %s", traceId,string(data))
 }
  // ... parse
}

func clientTrace(traceId string ) (clientTrace *httptrace.ClientTrace,traceResult *TraceResult) {
 traceResult = &TraceResult{
    TraceId:traceId,
  }

 clientTrace := &httptrace.ClientTrace{
  DNSStart: func(info httptrace.DNSStartInfo) {
   currentTime := time.Now().Local().String()
   traceResult.DNS.Start = currentTime
   traceResult.DNS.Host = info.Host
  },
  DNSDone: func(info httptrace.DNSDoneInfo) {
      currentTime := time.Now().Local().String()
   traceResult.DNS.End = currentTime
   traceResult.DNS.Addrs = info.Addrs
   traceResult.DNS.Err = info.Err
  },
    // ... 省略一些代碼
  }
  
 return clientTrace, traceResult
}

小結

這裏對 httptrace 的一些介紹,相信大家也多了一項 debug 技能,也會對 http 從請求到響應有了更深的瞭解。每一個問題都是發現未知的機會,希望 gopher 們在遇到問題時不要害怕,迎難而上,追根到底。

最後希望這篇短文,能給你帶來一些幫助。如有問題可以下方留言討論。

參考資料

[1]

Introducing HTTP Tracing: https://go.dev/blog/http-tracing

[2]

httptrace 包: https://pkg.go.dev/net/http/httptrace

[3]

trace.go 源碼: https://github1s.com/golang/go/blob/HEAD/src/net/http/httptrace/trace.go

[4]

HTTP 狀態碼 100 (Continue) 含義詳解: https://seo.juziseo.com/doc/http_code/100

Go 招聘 Golang 相關求職和招聘,以及面試題、經驗分享,Go 語言其他知識和職場也是值得分享的。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ZWJO4Y1RVqR4d4mJkPFb0Q