디버깅 중 고루틴(Goroutine)을 찾는 법
고루틴은 Go로 작성된 대부분의 프로그램에서 중요한 부분을 구성합니다. 하지만, 많은 고루틴을 사용하면 프로그램을 디버그하기가 어렵게 되기도 합니다. 이번 포스팅에서는 사용자 지정 데이터를 사용해 고루틴에 레이블을 지정하는 것에 대해 살펴보겠습니다. 이 기능은 현재 EAP 단계에 있는 GoLand 2020.1의 최신 기능 중 하나입니다.
웹 서버에 요청을 하는 애플리케이션을 예로 들어 보겠습니다
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 <- struct{}{} 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 <-done }() 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) }
IDE에서의 사용법
디버거에서 이 코드를 분석하면 makeRequest 고루틴이 무슨 일을 하는지 어떻게 알 수 있을까요? 이런 목록이 표시되고 있을 때 고루틴은 어디에 있는 걸까요?
고루틴 레이블 읽기를 지원하는 새로운 GoLand 기능이 바로 여기서 효력을 발휘합니다.
위 코드를 다음과 같이 조정해 보겠습니다.
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)
이제 디버거에서 동일한 코드를 실행하면 다음과 같은 뷰가 생성됩니다.
보기가 훨씬 더 좋아졌습니다. 이제 설정한 모든 정보를 레이블에서 확인할 수 있습니다. 그리고 무엇보다도, 함수 호출에 의해 백그라운드에서 실행된 다른 고루틴들에도 모두 자동으로 레이블이 추가되므로 동시에 확인할 수 있습니다.
HTTP 핸들러는 함수 시그니처 측면에서 꽤 인기가 많으며, 다른 핸들러 유형과 비교될 수 있습니다. 아래에서 코드를 조정하여 레이블을 설정하는 방법에 대해 살펴보겠습니다.
원래 코드는 m
을 *http.ServeMux(또는 *github.com/gorilla/mux.Router)로 사용하며 다음과 같이 생겼습니다: m.HandleFunc("/", homeHandler)
레이블 지정 코드를 적용하면 다음과 같이 보입니다.
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) }) })
아래와 같이 각 HTTP 요청을 처리하는 고루틴에 레이블을 지정합니다.
요청 객체에 대한 액세스 권한이 있으므로 예제 코드보다 더 복잡한 데이터로 레이블을 채울 수 있습니다.
명령줄에서의 사용법
명령줄에서 직접 Delve를 사용하는 경우 수정 버전 1867862 이상의 Delve가 필요합니다. 이 변경 사항은 다음 릴리스에 포함되며, 현재 v1.4.0 릴리스에는 포함되어 있지 않습니다.
레이블을 보려면 디버그 세션 중 goroutines -l
명령을 호출하면, IDE에 있는 것과 같은 데이터를 볼 수 있습니다.
성능 영향
이제 자연스러운 질문이 생깁니다. 위와 같은 코드를 사용하면 성능에 영향이 있을까요?
답은 “네”입니다. 이러한 레이블을 설정하면 성능에 불이익이 있습니다. 일반적으로 그 영향은 매우 적지만, 그래도 존재합니다. 따라서, 일부 벤치마킹 코드를 사용해 자신의 하드웨어에서 테스트하는 것이 가장 좋습니다.
이 영향을 고려하면 다음 질문이 뒤따릅니다. 성능 영향이 있을 경우 디버깅을 할 때마다 코드를 적용하고 다시 취소해야 함을 의미합니다. 그러면 개발 속도에 영향을 받게 되는데, 더 나아질 수 있는 건가요?
사용자 지정 라이브러리를 사용해 디버깅 레이블링 활성화하기
위 질문에 답하고 성능 영향 없이 디버깅 코드가 컴파일되도록 github.com/dlsniper/debugger 라이브러리를 사용해 makeRequest
코드를 변경하여 다음 함수 호출을 포함시켜 보겠습니다.
func makeRequest(done chan struct{}, c *http.Client, page string, i int64) { defer func() { // Unblock the next request from the queue <-done }() debugger.SetLabels(func() []string { return []string{ "request", "automated", "page", page, "rid", strconv.Itoa(int(i)), } }) >디버거에서 이를 실행하기 전에 하나 더 변경해야 합니다. 실행 구성(run configuration)에서 Go 도구 인수 필드에
-tags debugger
를 추가해야 합니다. 그렇지 않으면 라이브러리가 아무 효과도 없는 생산 코드를 로드하게 됩니다.여기에 표시된 라이브러리는 기존 애플리케이션에서 쉽게 사용할 수 있도록 표준 http.HandlerFunc 시그니처를 지원합니다.
이렇게 생긴 코드로 다시 돌아가서, :
m.HandleFunc("/", homeHandler)
이 핸들러에 레이블을 추가하려면 코드를 다음과 같이 변경할 수 있습니다.
m.HandleFunc("/", debugger.Middleware(homeHandler, func(r *http.Request) []string { return []string{ "request", "real", "path", r.RequestURI, } }))팁:
단일한 함수 또는 메소드에서 debugger.SetLabels 함수에 더 많은 호출을 배치하면 더 쉽게 실행 과정을 추적하고, 필요하지 않은 데이터를 걸러 낼 수 있습니다.팁:
실행 구성을 복제하여 디버거 빌드 태그가 있을 때와 없을 때 모두 해당 코드를 사용할 수 있습니다.참고:
위와 같이 레이블을 설정하면 성능에 약간의 불이익이 발생합니다. 따라서, 성능에 민감하지 않은 환경에서-tags=debugger
빌드 바이너리만 사용하거나, 디버깅 환경에 대한 개선 효과가 성능에 대한 불이익보다 더 나아지도록 해야합니다.오늘은 여기까지입니다. 복잡한 Go 애플리케이션을 디버깅할 때 GoLand를 사용하여 고루틴에 레이블을 추가함으로써 조금이나마 작업을 편하게 하는 방법에 대해 알아보았습니다.
이 게시글의 모든 코드는 github.com/dlsniper/debugger에서 확인할 수 있습니다. 라이브러리를 테스트하는 샘플 코드는 github.com/dlsniper/serverdemo에서 확인할 수 있습니다.
아래의 댓글란, 당사의 이슈 트래커 또는 Twitter에서 여러분의 의견을 남겨주세요.
Florin Pățan의 How to Find Goroutines During Debugging를 번역한 글입니다.
Subscribe to JetBrains Blog updates