Goroutines 是大多數用 Go 編寫的程序的重要組成部分。但是,使用大量 goroutines 會使程序難以調試。那怎麼辦?在此博文中,我們將介紹如何使用自定義數據為 goroutine 加上標籤,這是 GoLand 2020.1(現已包含在
EAP[1] 中)的最新功能之一。目錄
- 在 IDE 下使用
- 在命令行下使用
- 性能影響
- 使用自定義庫啟用調試標籤
讓我們以向 Web 服務器發出請求的應用程序為例:
<code>package main
import (
"io"
"io/ioutil"
"math/rand"
"net/http"
"strconv"
"strings"
"time"
)
func fakeTraffic() {
// Wait for the server to start
time.Sleep(1 * time.Second)
pages := []string{"/", "/login", "/logout", "/products", "/product/{productID}", "/basket", "/about"}
activeConns := make(chan struct{}, 10)
c := &http.Client{
Timeout: 10 * time.Second,
}
i := int64(0)
for {
activeConns i++
page := pages[rand.Intn(len(pages))]
// We need to launch this using a closure function to
// ensure that we capture the correct value for the
// two parameters we need: page and i
go func(p string, rid int64) {
makeRequest(activeConns, c, p, rid)
}(page, i)
}
}
func makeRequest(done chan struct{}, c *http.Client, page string, i int64) {
defer func() {
// Unblock the next request from the queue
}()
page = strings.Replace(page, "{productID}", "abc-"+strconv.Itoa(int(i)), -1)
r, err := http.NewRequest(http.MethodGet, "http://localhost:8080"+page, nil)
if err != nil {
return
}
resp, err := c.Do(r)
if err != nil {
return
}
defer resp.Body.Close()
_, _ = io.Copy(ioutil.Discard, resp.Body)
time.Sleep(time.Duration(10+rand.Intn(40)) + time.Millisecond)
}/<code>
在 IDE 下使用
如果我們在調試器(debugger)中分析此代碼,我們如何知道 makeRequest goroutines 在做什麼?當我們看這樣的清單時,這些 goroutine 的執行上下文什麼?
debugger without labels
這就是 GoLand 新版本支持讀取 goroutines 標籤的緣由。
我們調整下上面的代碼:(polaris 注:pprof 是標準庫的 runtime/pprof )
<code>go func(p string, rid int64) {
labels := pprof.Labels("request", "automated", "page", p, "rid", strconv.Itoa(int(rid)))
pprof.Do(context.Background(), labels, func(_ context.Context) {
makeRequest(activeConns, c, p, rid)
})
}(page, i)/<code>
現在,當在調試器中運行相同的代碼時,我們將看到以下視圖:
debugger with labels
看起來好多了。現在,我們可以看到在標籤中設置的所有信息。而且,最重要的是,我們還可以看到通過函數調用在後臺啟動的其他 goroutine,它們都會自動攜帶標籤。
由於 HTTP HandleFunc 這種形式的處理程序非常受歡迎,並且可以與其他處理程序類型進行比較,因此讓我們看一下如何調整下面的代碼以設置標籤。
我們的原始代碼將 m 用作 *http.ServeMux(或 *github.com/gorilla/mux.Router),看起來像這樣:m.HandleFunc("/", homeHandler)。
應用標籤代碼後,它將變為如下所示:
<code>m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
labels := pprof.Labels("path", r.RequestURI, "request", "real")
pprof.Do(context.Background(), labels, func(_ context.Context) {
homeHandler(w, r)
})
})/<code>
這將標記處理每個 HTTP 請求的 goroutine,如下所示。
debugging http middleware with labels
由於可以訪問請求對象,因此可以使用比示例代碼中更復雜的數據填充標籤。
在命令行下使用
如果直接在命令行中使用 Delve,則需要使用 1867862[2] 或更高版本的 Delve。這些更改將包含在下一個版本中,而當前 v1.4.0 版本中未包含。
要查看標籤,請在調試會話期間調用 goroutines -l 命令,以查看到與 IDE 中相同的數據。
debugger dlv from command line with labels
性能影響
隨之而來的自然問題是:使用上述代碼對性能會有影響嗎?
答案是肯定的,設置這些標籤確實會降低性能。通常,它的影響很小,但是仍然會存在,因此最好使用一些基準測試代碼在自己的硬件上進行測試。
考慮到這種影響,就會出現下一個問題:如果涉及性能,則意味著每次需要進行調試時,我都需要應用和撤消代碼。這會影響我的開發速度,這能做得更好嗎?
使用自定義庫啟用調試標籤
要回答上述問題並允許我們的調試代碼在不影響性能的情況下進行編譯,請使用 github.com/dlsniper/debugger[3] 庫並更改我們的 makeRequest 代碼以包括以下函數調用:
<code>func makeRequest(done chan struct{}, c *http.Client, page string, i int64) {
defer func() {
// Unblock the next request from the queue
}()
debugger.SetLabels(func() []string {
return []string{
"request", "automated",
"page", page,
"rid", strconv.Itoa(int(i)),
}
})
\t// ..
}/<code>
在調試器中運行此代碼之前,我們需要進行其他更改。我們需要在運行配置的 Go 工具參數字段中添加 -tags debugger。否則,該庫將加載生產代碼,標籤將不起作用。
debugger - run configuration
此處顯示的庫支持標準的 http.HandlerFunc 簽名,以方便在現有應用程序中使用。
回到我們的代碼,如下所示:m.HandleFunc("/", homeHandler)。
要將標籤添加到這些處理程序,我們可以將代碼更改為如下所示:
<code>m.HandleFunc("/", debugger.Middleware(homeHandler, func(r *http.Request) []string {
return []string{
"request", "real",
"path", r.RequestURI,
}
}))/<code>
專業提示:
在單個函數或方法中對 debugger.SetLabels[4] 函數進行多次調用,可以更輕鬆地跟蹤執行進度並過濾掉不需要的數據。
專業提示:
可以複製運行配置,從而可以在有和沒有調試器構建標記(build tag)的情況下使用代碼。
注意:
如上所示,設置標籤會導致性能下降。因此,僅在對性能要求不高的環境中使用 -tags=debugger 構建的二進制文件,或確保通過改善調試體驗來抵消性能損失。
今天就這樣。我們學習瞭如何使用 GoLand 調試複雜的 Go 應用程序並在 goroutine 中添加標籤,從而使生活變得更輕鬆。
這篇文章中的所有代碼都可以在 github.com/dlsniper/debugger[5] 上找到。用於測試該庫的示例代碼可在 github.com/dlsniper/serverdemo[6] 上找到。
作者:Florin Pățan[7]
原文鏈接:https://blog.jetbrains.com/go/2020/03/03/how-to-find-goroutines-during-debugging/
翻譯:polaris
[1]
EAP: https://blog.jetbrains.com/go/tag/2020-1/
[2]
1867862: https://github.com/go-delve/delve/commit/186786235fc9c2bd9b16c26bb4b0aef60ffb731c
[3]
github.com/dlsniper/debugger: https://github.com/dlsniper/debugger
[4]
debugger.SetLabels: https://pkg.go.dev/github.com/dlsniper/debugger?tab=doc#SetLabels
[5]
github.com/dlsniper/debugger: https://github.com/dlsniper/debugger
[6]
github.com/dlsniper/serverdemo: https://github.com/dlsniper/serverdemo
[7]
Florin Pățan: https://blog.jetbrains.com/go/author/florin-patanjetbrains-com/
閱讀更多 Go語言中文網 的文章