GoLand

디버깅 중 고루틴(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 요청을 처리하는 고루틴에 레이블을 지정합니다.

레이블을 사용해 http 미들웨어 디버그하기

요청 객체에 대한 액세스 권한이 있으므로 예제 코드보다 더 복잡한 데이터로 레이블을 채울 수 있습니다.

명령줄에서의 사용법

명령줄에서 직접 Delve를 사용하는 경우 수정 버전 1867862 이상의 Delve가 필요합니다. 이 변경 사항은 다음 릴리스에 포함되며, 현재 v1.4.0 릴리스에는 포함되어 있지 않습니다.

레이블을 보려면 디버그 세션 중 goroutines -l 명령을 호출하면, IDE에 있는 것과 같은 데이터를 볼 수 있습니다.

레이블을 사용한 명령줄에서의 디버거 dlv

성능 영향

이제 자연스러운 질문이 생깁니다. 위와 같은 코드를 사용하면 성능에 영향이 있을까요?

답은 “네”입니다. 이러한 레이블을 설정하면 성능에 불이익이 있습니다. 일반적으로 그 영향은 매우 적지만, 그래도 존재합니다. 따라서, 일부 벤치마킹 코드를 사용해 자신의 하드웨어에서 테스트하는 것이 가장 좋습니다.

이 영향을 고려하면 다음 질문이 뒤따릅니다. 성능 영향이 있을 경우 디버깅을 할 때마다 코드를 적용하고 다시 취소해야 함을 의미합니다. 그러면 개발 속도에 영향을 받게 되는데, 더 나아질 수 있는 건가요?

사용자 지정 라이브러리를 사용해 디버깅 레이블링 활성화하기

위 질문에 답하고 성능 영향 없이 디버깅 코드가 컴파일되도록 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ățanHow to Find Goroutines During Debugging를 번역한 글입니다.

image description

Discover more