진단

진단 (Diagnostics)

Go 프로그램이 제대로 도는지, 어디가 느린지 궁금할 때 쓰는 도구가 많아요. 이 페이지는 Go 생태계가 제공하는 진단 API와 도구를 한곳에 정리하고, 특정 문제에 어떤 도구를 골라야 하는지 판단하는 데 도움을 줍니다. 진단 솔루션은 크게 네 묶음으로 나뉘어요.

  • 프로파일링(Profiling). 프로그램의 복잡도와 비용을 분석합니다. 메모리 사용량이나 빈번하게 호출되는 함수 같은 것들로, 프로그램의 어느 부분이 비싼지 찾아내죠.
  • 트레이싱(Tracing). 코드에 계측을 넣어 호출이나 사용자 요청의 수명 주기 전체에서 발생하는 지연(latency)을 분석합니다. 트레이스는 시스템 전체 지연에 각 구성 요소가 얼마나 기여하는지 보여줘요. 트레이스는 여러 Go 프로세스에 걸칠 수도 있습니다.
  • 디버깅(Debugging). Go 프로그램을 잠시 멈추고 그 실행 상태를 살펴봅니다. 프로그램 상태와 흐름을 검증할 수 있죠.
  • 런타임 통계 및 이벤트. 런타임 통계와 이벤트를 수집·분석하면 Go 프로그램의 건강 상태를 높은 수준에서 볼 수 있습니다. 지표의 급등/급락을 통해 처리량, 활용도, 성능 변화를 찾아내요.

참고: 일부 진단 도구는 서로 간섭할 수 있습니다. 예를 들어 정밀한 메모리 프로파일링은 CPU 프로파일을 왜곡하고, 고루틴 블로킹 프로파일링은 스케줄러 트레이스에 영향을 줍니다. 더 정확한 정보를 얻으려면 도구를 분리해서 사용하세요.

출처: Go 공식 문서

프로파일링 (Profiling)

프로파일링은 비싸거나 자주 호출되는 코드 섹션을 찾는 데 유용해요. Go 런타임은 pprof 시각화 도구가 기대하는 형식의 프로파일링 데이터를 제공합니다. 프로파일링 데이터는 테스트 중에는 go test로, 또는 net/http/pprof 패키지가 제공하는 엔드포인트로 수집할 수 있어요. 사용자는 프로파일링 데이터를 모은 뒤 pprof 도구로 핵심 코드 경로를 걸러내고 시각화해야 합니다. runtime/pprof 패키지가 제공하는 미리 정의된 프로파일은 다음과 같아요.

  • cpu. 프로그램이 CPU 사이클을 활발히 소비하며 시간을 보내는 곳을 알아냅니다(잠자거나 I/O를 기다리는 시간과 반대되는 의미).
  • heap. 힙 메모리 할당 샘플을 보고합니다. 현재·과거 메모리 사용량을 모니터링하고 메모리 누수를 확인하는 데 쓰죠.
  • threadcreate. 새 OS 스레드를 만드는 데 이른 코드 섹션을 보고합니다.
  • goroutine. 현재 모든 고루틴의 스택 트레이스를 보고합니다.
  • block. 고루틴이 동기화 프리미티브(타이머 채널 포함)를 기다리며 블로킹되는 지점을 보여줍니다. block 프로파일은 기본적으로 켜져 있지 않아요. runtime.SetBlockProfileRate로 활성화합니다.
  • mutex. 락 경합을 보고합니다. CPU가 뮤텍스 경합 때문에 충분히 사용되지 않는다고 생각될 때 쓰는 프로파일이죠. 기본적으로 꺼져 있으며, runtime.SetMutexProfileFraction로 켭니다.

Go 프로그램을 프로파일링하는 데 쓸 수 있는 다른 프로파일러도 있나요? Linux에서는 perf 도구로 Go 프로그램을 프로파일링할 수 있어요. perf는 cgo/SWIG 코드와 커널까지 프로파일링하고 언와인딩할 수 있어서, 네이티브/커널 성능 병목을 파악하는 데 유용합니다. macOS에서는 Instruments 스위트로 Go 프로그램을 프로파일링할 수 있어요.

운영 중인 서비스도 프로파일링할 수 있나요? 네, 운영에서 프로그램을 프로파일링하는 건 안전합니다. 다만 일부 프로파일(예: CPU 프로파일)을 켜면 비용이 늘어나요. 성능이 떨어질 걸 각오해야 하죠. 성능 패널티는 운영에서 키기 전에 프로파일러의 오버헤드를 측정해 예상할 수 있습니다. 운영 서비스를 주기적으로 프로파일링하는 것도 고려해 볼 만해요. 특히 단일 프로세스의 복제본이 여럿 있는 시스템이라면, 주기적으로 무작위 복제본 하나를 고르는 게 안전한 선택입니다. 운영 프로세스를 골라 Y초마다 X초 동안 프로파일링하고 결과를 저장해 시각화·분석한 뒤, 이걸 주기적으로 반복하는 방식이죠. 결과는 수동/자동으로 검토해 문제를 찾을 수 있어요. 프로파일 수집은 서로 간섭할 수 있으니 한 번에 프로파일 하나만 수집하는 걸 권장합니다.

프로파일링 데이터를 시각화하는 가장 좋은 방법은 무엇인가요? Go 도구는 go tool pprof로 프로파일 데이터의 텍스트, 그래프, callgrind 시각화를 제공합니다. Profiling Go programs 글에서 실제 사용을 볼 수 있어요.

  • 가장 비싼 호출을 텍스트로 나열합니다.
  • 가장 비싼 호출을 그래프로 시각화합니다.
  • Weblist 뷰는 소스의 비싼 부분을 HTML 페이지에 줄 단위로 보여줍니다. 다음 예시에서 530ms가 runtime.concatstrings에서 소비되며, 각 줄의 비용이 목록으로 제시됩니다.
  • 가장 비싼 호출을 weblist로 시각화합니다.

프로파일 데이터를 시각화하는 또 다른 방법은 플레임 그래프(flame graph) 예요. 플레임 그래프는 특정 조상 경로를 따라 이동할 수 있어서, 특정 코드 섹션을 확대/축소할 수 있습니다. 상위 pprof가 플레임 그래프를 지원합니다.

내장 프로파일에만 국한되나요? 아닙니다. 런타임이 제공하는 것 외에도 Go 사용자는 pprof.Profile로 자신만의 커스텀 프로파일을 만들고 기존 도구로 검사할 수 있어요.

프로파일러 핸들러(/debug/pprof/...)를 다른 경로나 포트로 서빙할 수 있나요? 네. net/http/pprof 패키지는 기본적으로 기본 mux에 핸들러를 등록하지만, 패키지가 노출한 핸들러를 사용해 직접 등록할 수도 있습니다. 예를 들어 다음 예시는 pprof.Profile 핸들러를 :7777/custom_debug_path/profile에 서빙합니다.

package main

import (
	"log"
	"net/http"
	"net/http/pprof"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/custom_debug_path/profile", pprof.Profile)
	log.Fatal(http.ListenAndServe(":7777", mux))
}

트레이싱 (Tracing)

트레이싱은 호출 체인의 수명 주기 전체에서 지연을 분석하도록 코드에 계측을 넣는 방법입니다. Go는 Go 노드당 최소한의 트레이싱 백엔드로 golang.org/x/net/trace 패키지를, 간단한 대시보드를 갖춘 최소한의 계측 라이브러리를 제공해요. Go는 또한 일정 구간 안의 런타임 이벤트를 트레이스하는 실행 트레이서도 제공합니다. 트레이싱을 쓰면 다음을 할 수 있어요.

  • Go 프로세스 안에서 애플리케이션 지연을 계측하고 분석한다.
  • 긴 호출 체인에서 특정 호출의 비용을 측정한다.
  • 활용도와 성능 개선 지점을 알아낸다. 병목은 트레이싱 데이터가 없으면 항상 뻔한 게 아니거든요.

단일 시스템(monolith)에서는 프로그램의 구성 요소에서 진단 데이터를 모으기가 상대적으로 쉬워요. 모든 모듈이 한 프로세스 안에 살면서 로그, 오류, 기타 진단 정보를 보고하는 공통 리소스를 공유하니까요. 하지만 시스템이 단일 프로세스를 넘어 분산되기 시작하면, 프론트엔드 웹 서버에서 시작된 호출이 사용자에게 응답이 돌아갈 때까지 모든 백엔드를 거쳐가는 걸 따라가기가 어려워집니다. 이때 분산 트레이싱(distributed tracing) 이 크게 활약해요. 분산 트레이싱으로 운영 시스템을 계측하고 분석할 수 있거든요.

분산 트레이싱은 사용자 요청의 수명 주기 전체에서 지연을 분석하도록 코드를 계측하는 방법이에요. 시스템이 분산되어 있고 기존의 프로파일링·디버깅 도구가 확장되지 않을 때, 사용자 요청과 RPC의 성능을 분석하기 위해 분산 트레이싱 도구를 쓰고 싶을 거예요. 분산 트레이싱을 쓰면 다음이 가능해요.

  • 큰 시스템에서 애플리케이션 지연을 계측하고 프로파일링한다.
  • 사용자 요청의 수명 주기 안의 모든 RPC를 추적하고, 운영에서만 보이는 통합 문제를 발견한다.
  • 우리 시스템에 적용할 수 있는 성능 개선을 알아낸다. 트레이싱 데이터를 모으기 전에는 많은 병목이 뻔하지 않으니까요.

Go 생태계는 트레이싱 시스템마다 다양한 분산 트레이싱 라이브러리와, 백엔드에 무관한 라이브러리를 제공합니다.

모든 함수 호출을 자동으로 가로채서 트레이스를 만들 방법이 있나요? Go는 모든 함수 호출을 자동으로 가로채 트레이스 스팬을 만드는 방법을 제공하지 않아요. 스팬을 만들고 끝내고 주석을 다는 건 수동으로 코드를 계측해야 합니다.

Go 라이브러리에서 트레이스 헤더를 어떻게 전파해야 하나요? 트레이스 식별자와 태그를 context.Context 안에서 전파할 수 있어요. 업계에는 아직 표준적인 트레이스 키나 트레이스 헤더의 공통 표현이 없습니다. 각 트레이스 공급자가 자기 Go 라이브러리에 전파 유틸리티를 제공할 책임이 있죠.

표준 라이브러리나 런타임의 어떤 저수준 이벤트를 트레이스에 포함할 수 있나요? 표준 라이브러리와 런타임은 저수준 내부 이벤트를 알려주는 여러 API를 노출하려고 노력 중이에요. 예를 들어 httptrace.ClientTrace는 나가는 요청의 수명 주기에서 저수준 이벤트를 따라가는 API를 제공합니다. 런타임 실행 트레이서에서 저수준 런타임 이벤트를 가져와 사용자가 자기 사용자 이벤트를 정의·기록하도록 하는 작업도 진행 중이에요.

디버깅 (Debugging)

디버깅은 프로그램이 오작동하는 이유를 찾아내는 과정입니다. 디버거를 쓰면 프로그램의 실행 흐름과 현재 상태를 이해할 수 있어요. 디버깅에는 여러 스타일이 있지만, 이 섹션에서는 프로그램에 디버거를 붙이는 것과 코어 덤프 디버깅에만 초점을 맞춥니다. Go 사용자는 주로 다음 디버거를 씁니다.

  • Delve. Go 프로그래밍 언어용 디버거예요. Go의 런타임 개념과 내장 타입을 지원하며, Go 프로그램을 위한 완성도 높고 신뢰할 수 있는 디버거가 되려고 노력하고 있습니다.
  • GDB. Go는 표준 Go 컴파일러와 Gccgo를 통해 GDB를 지원합니다. 다만 스택 관리, 스레딩, 런타임에는 GDB가 기대하는 실행 모델과 충분히 달라 디버거를 혼란시킬 수 있는 측면이 있어요 (gccgo로 컴파일해도 마찬가지). GDB로 Go 프로그램을 디버깅할 수는 있지만, 이상적이지 않고 혼란을 줄 수 있습니다.

디버거는 Go 프로그램과 얼마나 잘 작동하나요? gc 컴파일러는 함수 인라이닝과 변수 레지스터화 같은 최적화를 수행합니다. 이런 최적화는 때때로 디버거로 디버깅하는 걸 더 어렵게 만들어요. 최적화된 바이너리에 대해 생성되는 DWARF 정보의 품질을 개선하려는 노력이 진행 중입니다. 개선이 나오기 전까지는, 디버깅할 코드를 빌드할 때 최적화를 끄는 걸 권장합니다. 다음 명령은 컴파일러 최적화 없이 패키지를 빌드해요.

$ go build -gcflags=all="-N -l"

개선 노력의 일환으로 Go 1.10은 새 컴파일러 플래그 -dwarflocationlists를 도입했습니다. 이 플래그는 컴파일러가 위치 목록(location list)을 추가하게 해서, 최적화된 바이너리에서 디버거가 일할 수 있게 도와줘요. 다음 명령은 최적화를 유지하되 DWARF 위치 목록을 포함해 빌드합니다.

$ go build -gcflags="-dwarflocationlists=true"

추천하는 디버거 사용자 인터페이스는 무엇인가요? delve와 gdb 모두 CLI를 제공하지만, 대부분의 에디터 통합과 IDE가 디버깅 전용 사용자 인터페이스를 제공합니다.

Go 프로그램으로 사후(postmortem) 디버깅이 가능한가요? 코어 덤프 파일은 실행 중인 프로세스의 메모리 덤프와 그 프로세스 상태를 담은 파일이에요. 주로 프로그램의 사후 디버깅과, 실행 중 상태를 이해하는 데 쓰이죠. 이 두 경우 모두 코어 덤프 디버깅을 운영 서비스의 사후 분석에 좋은 진단 도구로 만듭니다. Go 프로그램에서 코어 파일을 얻어 delve나 gdb로 디버깅할 수 있어요. 단계별 가이드는 core dump debugging 페이지를 참고하세요.

런타임 통계 및 이벤트 (Runtime statistics and events)

런타임은 내부 이벤트의 통계와 보고를 제공해, 사용자가 런타임 수준에서 성능·활용 문제를 진단할 수 있게 해줍니다. 이런 통계를 모니터링하면 Go 프로그램의 전반적인 건강 상태와 성능을 더 잘 이해할 수 있어요. 자주 모니터링하는 통계와 상태는 다음과 같아요.

  • runtime.ReadMemStats는 힙 할당과 가비지 컬렉션 관련 지표를 보고합니다. 메모리 통계는 프로세스가 메모리 리소스를 얼마나 소비하는지, 메모리를 잘 활용하는지, 메모리 누수를 잡아내는지 모니터링하는 데 유용해요.
  • debug.ReadGCStats는 가비지 컬렉션에 관한 통계를 읽습니다. 리소스가 GC 일시 중지에 얼마나 쓰이는지 보는 데 유용하죠. GC 일시 중지의 타임라인과 일시 중지 시간 백분위도 보고합니다.
  • debug.Stack은 현재 스택 트레이스를 반환합니다. 스택 트레이스는 현재 몇 개의 고루틴이 돌고 있는지, 무엇을 하고 있는지, 블로킹됐는지를 보는 데 유용해요.
  • debug.WriteHeapDump는 모든 고루틴의 실행을 중단하고 힙을 파일로 덤프하게 해줍니다. 힙 덤프는 주어진 시점의 Go 프로세스 메모리 스냅샷으로, 할당된 모든 객체와 고루틴, 파이널라이저 등을 담고 있어요.
  • runtime.NumGoroutine은 현재 고루틴 수를 반환합니다. 이 값을 모니터링해 고루틴이 충분히 활용되는지 보거나 고루틴 누수를 감지할 수 있어요.

실행 트레이서 (Execution tracer)

Go는 광범위한 런타임 이벤트를 포착하는 런타임 실행 트레이서를 함께 제공해요. 스케줄링, 시스템 콜, 가비지 컬렉션, 힙 크기 등 여러 이벤트를 런타임이 수집하고, go tool trace로 시각화할 수 있습니다. 실행 트레이서는 지연과 활용 문제를 감지하는 도구예요. CPU가 얼마나 잘 활용되는지, 네트워킹이나 시스템 콜이 고루틴 선점의 원인일 때가 언제인지 살펴볼 수 있죠. 트레이서는 다음에 유용해요.

  • 고루틴이 어떻게 실행되는지 이해한다.
  • GC 실행 같은 핵심 런타임 이벤트 일부를 이해한다.
  • 병렬화가 부실한 실행을 찾아낸다.

다만 과도한 메모리나 CPU 사용의 원인 같은 핫스팟을 찾는 데는 그다지 좋지 않아요. 그런 건 프로파일링 도구를 먼저 쓰세요. 위에서 go tool trace 시각화는 실행이 정상적으로 시작됐다가 직렬화되는 모습을 보여줍니다. 공유 리소스에 대한 락 경합이 있어 병목을 만드는 걸 암시하죠. 런타임 트레이스를 수집·분석하려면 go tool trace를 참고하세요.

GODEBUG

GODEBUG 환경 변수를 적절히 설정하면 런타임도 이벤트와 정보를 내보냅니다.

  • GODEBUG=gctrace=1은 각 컬렉션에서 수집된 메모리 양과 일시 중지 길이를 요약해 가비지 컬렉터 이벤트를 출력합니다.
  • GODEBUG=inittrace=1은 완료된 패키지 초기화 작업의 실행 시간과 메모리 할당 정보 요약을 출력합니다.
  • GODEBUG=schedtrace=X는 X밀리초마다 스케줄링 이벤트를 출력합니다.

GODEBUG 환경 변수는 표준 라이브러리와 런타임에서 명령어 세트 확장을 비활성화하는 데도 쓸 수 있어요.

  • GODEBUG=cpu.all=off는 모든 선택적 명령어 세트 확장을 비활성화합니다.
  • GODEBUG=cpu.<extension>=off는 지정한 명령어 세트 확장의 명령어 사용을 비활성화합니다. 여기서 <extension>sse41이나 avx 같은 명령어 세트 확장의 소문자 이름입니다.

더 알아보기 (Learn more)