dev./golang

Ch27) 프로파일링으로 성능 개선 하기

minikuma 2024. 11. 25. 22:35
프로파일링이란? 프로그램의 성능 지표를 프로그램이 실행 중에 실시간으로 측정하고 기록하는 것을 말한다. 측정하는 성능 지표는 실행 시간, 메모리 사용량, 함수 호출 시간과 빈도등이 있다. 프로그램 성능이 저하되는 곳을 찾고 원인을 분석하는 데 주목적이 있다.

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 명령어 결과

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판, 공봉식 저