프로파일링이란? 프로그램의 성능 지표를 프로그램이 실행 중에 실시간으로 측정하고 기록하는 것을 말한다. 측정하는 성능 지표는 실행 시간, 메모리 사용량, 함수 호출 시간과 빈도등이 있다. 프로그램 성능이 저하되는 곳을 찾고 원인을 분석하는 데 주목적이 있다.
27.1 특정 구간 프로파일링
Go 언어에서 프로파일링 데이터를 수집하는 방법이 몇 가지 있는데, 그중에서 특정 구간을 프로파일링 하는 방법을 살펴보자. 이 방법은 성능 개선이 필요한 특정 함수나 구간을 조사할 때 좋은 방법이다.
- 성능 측정을 시작하는 곳에 pprof.StartCPUProfile() 함수를 호출
- 성능 측정을 끝내는 곳에 pprof.StopCPUProfile() 함수를 호출
- 결과 저장
package main
import (
"fmt"
"log"
"os"
"runtime/pprof"
"time"
)
func Fib(n int) int {
if n == 0 {
return 0
} else if n == 1 {
return 1
} else {
return Fib(n - 1) + Fib(n - 2)
}
}
func main() {
// (1) 프로파일링 결과를 저장할 파일 생성
f, err := os.Create("cpu.prof")
if err != nil {
log.Fatal(err)
}
defer f.Close()
// (2) 프로파일링 시작
pprof.StartCPUProfile(f)
// (3) 프로그램 종료 전에 프로파일링 종료
defer pprof.StopCPUProfile()
fmt.Println(Fib(50))
// 10초 대기
time.Sleep(10 * time.Second)
}
프로그램 종료 후에 cpu.prof 파일 생성을 확인한 뒤 pprof 툴을 활용하여 프로그래밍 결과를 분석한다.
go tool pprof cpu.prof
top 명령어를 실행해 보자.
top 명령어는 자원 점유율이 높은 순으로 출력한다. 결과를 보면 Fib() 함수가 전체 중 97.76%를 차지하고 있음을 알 수 있다. 이를 통해 어떤 함수가 전체 성능에 가장 많은 영향을 주고 있는지 알 수 있게 된다. 즉, Fib() 함수를 개선하게 되면 전체 성능이 개선될 수 있음을 의미한다.
번외로 책에서 소개한 Graphviz라는 툴을 설치하여 go 프로파일링을 할 수 있다. Graphviz는 Windows, Linux, macOS에 설치 가능하다. 설치 방법은 책을 참고하면 된다.
이제 프로파일링을 통해 Fib() 함수를 개선하게 되면 전체 성능이 개선될 수 있음을 알게 되었다. 이제 함수를 고쳐 보자.
package main
import (
"fmt"
"log"
"os"
"runtime/pprof"
"time"
)
// (1) 피보나치 결과를 저장할 맵
var fibMap [65535]int
func Fib(n int)int {
// (2) 이미 계산한 경우는 바로 반환
f := fibMap[n]
if f > 0 {
return f
}
if n == 0 {
return 0
} else if n == 1 {
f = 1
} else {
f = Fib(n - 1) + Fib(n - 2)
}
// (3) 결과를 저장
fibMap[n] = f
return f
}
func main() {
f, err := os.Create("cpu.prof")
if err != nil {
log.Fatal(err)
}
defer f.Close()
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
fmt.Println(Fib(50))
// 10초 대기
time.Sleep(10 * time.Second)
}
Fib() 함수 개선 아이디어는 결과를 Map에 저장한 뒤 이미 계산된 결과가 존재하는 경우 계산하지 않고 Map에 있는 값을 바로 반환하도록 구현하였다. 이렇게 중간 값을 저장하는 방식을 다이내믹 프로그래밍 중 메모이징 방식이라고 한다.
top을 해 보면, Fib() 함수가 자원 사용률의 상위에서 없어진 거를 알 수 있다. 그만큼 해당 함수는 개선되었으며, 전체적인 성능도 향상된 것을 알 수 있다.
27.2 서버에서 프로파일링
일반적으로 웹 서버와 같이 계속 실행되는 서버 프로그램에서는 특정 구간만 프로파일링을 하기 어렵다. 이번에는 계속 실행되는 프로그램을 프로파일링하는 방법을 살펴보자.
package main
import (
"math/rand"
"net/http"
_ "net/http/pprof" // (1) 웹 프로파일링을 실행
"time"
)
func main() {
http.HandleFunc("/log", logHandler)
http.ListenAndServe(":8080", nil)
}
func logHandler(w http.ResponseWriter, r *http.Request) {
ch := make(chan int)
go func() {
time.Sleep(time.Duration(rand.Intn(400)) * time.Millisecond)
ch <- http.StatusOK
}()
select {
case status := <-ch:
w.WriteHeader(status)
case <-time.After(200 * time.Millisecond):
w.WriteHeader(http.StatusRequestTimeout)
}
}
(1) net/http/pprof 패키지를 임포트하면 자동으로 웹 프로파일링을 실행된다. 이 패키지는 임포트 하면 자동으로 실행되기 때문에 빈칸 지시자(_)를 통해 임포트가 사라지지 않게 해 준다.
빌드를 하고 실행하면 웹 서버가 실행된다. 웹 브라우저에 http://localhost:8080/log에 접속하면 "200 Status OK" 또는 "400 Bad Request"가 랜덤 하게 발생되는 것을 볼 수 있다. 이제 웹 브라우저에 http://localhost:8080/debug/pprof를 입력하면 아래와 같은 프로파일링 화면을 볼 수 있다.
net/http/pprof 패키지를 임포트 했기 때문에 이 프로파일링 페이지가 자동으로 제공된다. 메뉴를 하나씩 살펴보자.
alloc | 프로그램에서 할당된 모든 메모리와 어디에서 할당되었는지 소스 위치를 통해 알 수 있게 해준다. |
block | Mutex나 WaitGroup, 채널 등과 같은 멀티 스레드 환경에서 현재 대기 상태인 객체를 보여준다. |
cmdine | 프로그램이 실행되었을 때, 어떤 인수로 실행되었는지 보여준다. |
goroutine | 현재 실행되고 있는 모든 고루틴을 보여준다. |
heap | 현재 메모리가 할당된 객체를 보여준다. |
mutex | 현재 대기중인 Mutex 콜스택을 보여준다. |
profile | CPU 프로파일링 시작하고 그 결과를 파일로 다운로드 받을 수 있다. |
threadcreate | 새로운 OS 스레드를 생성한 콜스택을 보여준다. |
trace | 트레이스 정보 수집을 시작한 뒤 그 결과를 파일로 다운로드받을 수 있다. |
27.2.1 Hey로 부하 테스트하기
Hey 프로그램을 활용해 웹 서버에 부하 테스트를 진행해 보자. Hey는 웹 서버에 요청을 반복 전송해 웹 서버 성능을 테스트할 때 사용한다. Hey는 https://github.com/rakyll/hey에서 다운로드하면 된다. 부하는 아래 명령어와 같이 줄 수 있다.
# macOS, Linux 계열
hey http://localhost:8080/log
실행 이후 http://localhost:8080/debug/pprof를 다시 접속하면 allocs나 goroutine 등 여러 수치를 볼 수 있다.
27.2.2 프로파일링 결과 분석
프로파일링은 http://localhost:8080/debug/pprof의 profile을 클릭해 프로파일링을 실행한다. 그런 뒤 hey를 다시 실행해 부하를 주고, 30초 뒤에 profile 결과를 다운로드하면 된다. 다운로드된 파일을 아래와 같은 명령어로 수행한다.
go tool pprof profile
profile은 파일명이고 다른 파일명으로 한 경우 해당 파일명으로 명령어를 작성하면 된다.
pprof 프롬프트에 top 5 명령어를 입력하면 가장 많은 CPU 실행시간을 차지한 상위 5개의 함수를 볼 수 있다. 추가로 web 명령어를 하게 되면 그래프 형태로도 볼 수 있다.
프로파일링과 부하 테스트를 통해 CPU와 같은 하드웨어 자원이 어떻게 사용되는 지 알 수 있고, 사용자의 프로그램의 개선 포인트를 발견하는데 도움을 준다.
References
- Tucker의 Go 언어 프로그래밍 2판, 공봉식 저