Stackdriver Profiler로 프로덕션 성능 분석

클라이언트 앱과 프런트엔드 웹 개발자는 일반적으로 코드 성능 향상을 위해 Android 스튜디오 CPU 프로파일러Chrome에 포함된 프로파일링 도구와 같은 도구를 사용하지만, 백엔드 서비스에서 작업하는 소프트웨어에는 거의 액세스할 수 없거나 이러한 기술을 활용하지 못하고 있습니다. Stackdriver Profiler는 코드가 Google Cloud Platform에서 실행되는지 아니면 다른 곳에서 실행되더라도 서비스 개발자에게 동일한 기능을 제공합니다.

이 도구는 프로덕션 애플리케이션에서 CPU 사용량 및 메모리 할당 정보를 수집합니다. 해당 정보의 출처인 애플리케이션의 소스 코드를 식별하여 애플리케이션에서 가장 많은 리소스를 사용하고 있는 부분을 파악할 수 있게 해주고, 코드의 성능 특성을 알려줍니다. 도구에서 사용하는 수집 기법의 오버헤드가 낮으므로 프로덕션 환경에서 지속적으로 사용하기에 적합합니다.

이 Codelab에서는 Go 프로그램에 Stackdriver Profiler를 설정하는 방법을 알아보고 이 도구에서 제공할 수 있는 애플리케이션 성능에 대한 유용한 정보를 알아봅니다.

학습할 내용

  • Stackdriver Profiler로 프로파일링하기 위한 Go 프로그램을 구성하는 방법
  • Stackdriver Profiler로 성능 데이터를 수집, 보기, 분석하는 방법

필요한 항목

  • Google Cloud Platform 프로젝트
  • Chrome 또는 Firefox와 같은 브라우저
  • Vim, EMAC, Nano와 같은 표준 Linux 텍스트 편집기에 대한 지식

본 튜토리얼을 어떻게 사용하실 계획인가요?

읽기만 할 계획입니다 읽은 다음 연습 활동을 완료할 계획입니다

Google Cloud Platform 사용 경험을 평가해 주세요.

초급 중급 고급

자습형 환경 설정

Google 계정 (Gmail 또는 Google 앱)이 아직 없다면 계정을 만들어야 합니다. Google Cloud Platform Console(console.cloud.google.com)에 로그인하여 새 프로젝트를 만듭니다.

2016-02-10 12:45:26.png 스크린샷

모든 Google Cloud 프로젝트에서 고유한 이름인 프로젝트 ID를 기억하세요(위의 이름은 이미 사용되었으므로 사용할 수 없습니다). 이 ID는 나중에 이 Codelab에서 PROJECT_ID라고 부릅니다.

다음으로 Google Cloud 리소스를 사용하려면 Cloud Console에서 결제를 사용 설정해야 합니다.

이 codelab을 실행하는 과정에는 많은 비용이 들지 않지만 더 많은 리소스를 사용하려고 하거나 실행 중일 경우 비용이 더 들 수 있습니다(이 문서 마지막의 '삭제' 섹션 참조).

Google Cloud Platform의 신규 사용자는 $300 무료 체험판을 사용할 수 있습니다.

Google Cloud Shell

노트북에서 원격으로 작동할 수 있지만, 이 Codelab에서 설정하는 과정을 더 간소화하기 위해 Cloud에서 실행되는 명령줄 환경인 Google Cloud Shell을 사용할 것입니다.

Google Cloud Shell 활성화

GCP 콘솔에서 오른쪽 상단 툴바의 Cloud Shell 아이콘을 클릭합니다.

그런 다음 'Cloud Shell 시작'을 클릭합니다.

환경을 프로비저닝하고 연결하는 데 몇 분 정도 소요됩니다.

가상 머신은 필요한 모든 개발 도구와 함께 로드됩니다. 영구적인 5GB 홈 디렉토리를 제공하고 Google Cloud에서 실행되므로 네트워크 성능과 인증이 크게 개선됩니다. 이 실습에서 대부분의 작업은 브라우저나 Google Chromebook만을 이용하여 수행할 수 있습니다.

Cloud Shell에 연결되면 인증이 이미 완료되어 있고 프로젝트가 이미 PROJECT_ID로 설정되어 있음을 확인할 수 있습니다.

Cloud Shell에서 다음 명령어를 실행하여 인증되었는지 확인합니다.

gcloud auth list

명령어 결과

Credentialed accounts:
 - <myaccount>@<mydomain>.com (active)
gcloud config list project

명령어 결과

[core]
project = <PROJECT_ID>

또는 다음 명령어로 설정할 수 있습니다.

gcloud config set project <PROJECT_ID>

명령어 결과

Updated property [core/project].

Cloud Console에서 왼쪽 탐색 메뉴에서 'Profiler&quot'를 클릭하여 Profiler UI로 이동합니다.

또는 Cloud Console 검색창을 사용하여 Profiler UI로 이동할 수 있습니다. 즉, 'Stackdriver Profiler'를 입력한 다음 찾은 항목을 선택하면 됩니다. 어느 쪽이든 아래와 같이 '표시할 데이터 없음' 메시지가 포함된 프로파일러 UI가 표시됩니다. 이 프로젝트는 새로 만들어졌으므로 아직 프로파일링 데이터가 수집되지 않습니다.

이제 프로필을 만들 시간이에요.

GitHub에서 제공하는 간단한 합성 Go 애플리케이션을 사용합니다. Cloud Shell 터미널에서 아직 열려 있고 '표시할 데이터가 없습니다.' 메시지는 Profiler UI에 계속 표시하는 동안 다음 명령어를 실행합니다.

$ go get -u github.com/GoogleCloudPlatform/golang-samples/profiler/...

그런 다음 애플리케이션 디렉터리로 전환합니다.

$ cd ~/gopath/src/github.com/GoogleCloudPlatform/golang-samples/profiler/hotapp

디렉터리에는 프로파일링 에이전트가 사용 설정된 합성 앱인 "main.go" 파일이 포함됩니다.

main.go

...
import (
        ...
        "cloud.google.com/go/profiler"
)
...
func main() {
        err := profiler.Start(profiler.Config{
                Service:        "hotapp-service",
                DebugLogging:   true,
                MutexProfiling: true,
        })
        if err != nil {
                log.Fatalf("failed to start the profiler: %v", err)
        }
        ...
}

프로파일링 에이전트는 기본적으로 CPU, 힙 및 스레드 프로필을 수집합니다. 이 코드는 mutex (content_contention"라고도 함) 프로필의 수집을 사용 설정합니다.

이제 프로그램을 실행합니다.

$ go run main.go

프로그램이 실행되면 프로파일링 에이전트는 주기적으로 구성된 5가지 유형의 프로필을 수집합니다. 데이터 수집은 시간 경과에 따라 무작위로 지정되며 (유형당 분당 평균 비율), 각 유형을 수집하는 데 최대 3분이 걸릴 수 있습니다. 프로그램에서 언제 프로필을 만들지 알려줍니다. 메시지는 위의 구성에서 DebugLogging 플래그로 사용 설정되고, 그렇지 않으면 에이전트가 자동으로 실행됩니다.

$ go run main.go
2018/03/28 15:10:24 profiler has started
2018/03/28 15:10:57 successfully created profile THREADS
2018/03/28 15:10:57 start uploading profile
2018/03/28 15:11:19 successfully created profile CONTENTION
2018/03/28 15:11:30 start uploading profile
2018/03/28 15:11:40 successfully created profile CPU
2018/03/28 15:11:51 start uploading profile
2018/03/28 15:11:53 successfully created profile CONTENTION
2018/03/28 15:12:03 start uploading profile
2018/03/28 15:12:04 successfully created profile HEAP
2018/03/28 15:12:04 start uploading profile
2018/03/28 15:12:04 successfully created profile THREADS
2018/03/28 15:12:04 start uploading profile
2018/03/28 15:12:25 successfully created profile HEAP
2018/03/28 15:12:25 start uploading profile
2018/03/28 15:12:37 successfully created profile CPU
...

첫 번째 프로필이 수집되면 곧 UI가 자동으로 업데이트됩니다. 그 이후에는 자동 업데이트되지 않으므로 새 데이터를 보려면 프로파일러 UI를 수동으로 새로고침해야 합니다. 시간 간격 선택 도구에서 현재 버튼을 두 번 클릭하면 다음 작업이 실행됩니다.

UI가 새로고침되면 다음과 같이 표시됩니다.

프로필 유형 선택기에는 사용 가능한 5개의 프로필 유형이 표시됩니다.

이제 각 프로필 유형과 중요한 UI 기능을 검토한 후 몇 가지 실험을 진행해 보겠습니다. 이 단계에서는 Cloud Shell 터미널이 더 이상 필요하지 않으므로 CTRL-C를 누르고 "exit"를 입력하면 종료됩니다.

이제 일부 데이터를 수집했으므로 자세히 살펴보겠습니다. 여기서는 합성 앱 (GitHub에서 사용 가능)을 사용하여 프로덕션 환경의 다양한 종류의 일반적인 성능 문제를 시뮬레이션합니다.

CPU를 많이 사용하는 코드

CPU 프로필 유형을 선택합니다. UI가 로드되면 Flame 그래프에서 load CPU의 리프 블록 4개가 표시되어 모든 CPU 소비를 합산합니다.

이 함수는 타이트 루프를 실행하여 많은 CPU 주기를 소비하도록 특별히 작성되었습니다.

main.go

func load() {
        for i := 0; i < (1 << 20); i++ {
        }
}

이 함수는 네 가지 호출 경로(busyloop → {foo1, foo2} → {bar, baz} → load)를 통해 busyloop()에서 간접적으로 호출됩니다. 함수 상자의 너비는 특정 호출 경로의 상대적 비용을 나타냅니다. 이 경우 4개 경로의 비용은 거의 같습니다. 실제 프로그램에서는 실적 측면에서 가장 중요한 통화 경로를 최적화하는 데 집중하려고 합니다. 큰 상자와 함께 비용이 더 많이 드는 경로를 시각적으로 강조하는 불꽃 그래프는 이러한 경로를 쉽게 식별할 수 있게 해줍니다.

프로필 데이터 필터를 사용하여 디스플레이를 미세 조정할 수 있습니다. 예를 들어 "baz"를 필터 문자열로 지정하는 '스택 표시' 필터를 추가해 보세요. 아래와 같은 스크린샷이 표시됩니다. load()의 호출 경로 4개 중 2개만 표시됩니다. 이 두 경로는 이름에 'baz" 문자열이 있는 함수를 통과하는 유일한 경로입니다. 이러한 필터링은 대규모 프로그램의 하위 부분에 초점을 맞추고 싶을 때 유용합니다 (예: 앱의 일부만 소유하는 경우).

메모리 집약적인 코드

이제 "Heap" 프로필 유형으로 전환하세요. 이전 실험에서 만든 필터를 모두 삭제해야 합니다. 이제 alloc라는 allocImpl이 앱에서 기본 메모리의 소비자로 표시되는 Flame 그래프가 표시됩니다.

Flame 그래프 위의 요약 표는 앱에서 사용된 총 메모리의 평균이 약 57.4MiB임을 나타내며 대부분 allocImpl 함수에 의해 할당됩니다. 이 함수를 구현하면 다음과 같은 결과가 발생합니다.

main.go

func allocImpl() {
        // Allocate 64 MiB in 64 KiB chunks
        for i := 0; i < 64*16; i++ {
                mem = append(mem, make([]byte, 64*1024))
        }
}

함수는 한 번 실행되며, 작은 청크로 64MiB를 할당한 후 이 청크에 대한 포인터를 전역 변수에 저장하여 가비지 컬렉션이 방지됩니다. 프로파일러에 의해 표시되는 메모리의 양은 64MiB와 약간 다릅니다. Go 힙 프로파일러는 통계 도구이므로 측정값은 오버헤드가 낮지만 바이트 정확도가 높지는 않습니다. 이와 같이 10% 정도 차이가 나더라도 놀라지 마세요.

IO 집약적 코드

프로필 유형 선택기에서 "Threads"를 선택하면 디스플레이가 대부분의 너비가 waitwaitImpl 함수에서 사용하는 Flame 그래프로 전환됩니다.

Flame 그래프의 요약에서 wait 함수에서 호출 스택을 늘리는 100개의 goroutine이 있음을 알 수 있습니다. 이러한 대기를 시작하는 코드는 다음과 같으므로 정확히 맞습니다.

main.go

func main() {
        ...
        // Simulate some waiting goroutines.
        for i := 0; i < 100; i++ {
                go wait()
        }

이 프로필 유형은 프로그램에서 예상치 못한 시간 (예: I/O)을 소비하는지 여부를 파악하는 데 유용합니다. 이러한 호출 스택은 대체로 CPU 프로파일러의 많은 부분을 소비하지 않기 때문에 CPU 프로파일러가 샘플링되지 않습니다. 예를 들어 스레드 프로필과 함께 '스택 숨기기' 필터를 사용하는 것이 좋습니다. 예를 들어 gopark, 호출로 끝나는 모든 스택은 유휴 고루틴이거나 I/O에서 대기하는 스택보다 덜 흥미로운 경우가 많습니다.

스레드 프로필 유형은 스레드가 오랫동안 프로그램의 다른 부분에서 소유한 뮤텍스를 기다리는 지점을 식별하는 데 도움이 될 수 있지만 다음과 같은 프로필 유형이 더 유용합니다.

경합이 많은 코드

경합 프로필 유형은 프로그램에서 가장 많이 잠그는 잠금을 나타냅니다. 이 프로필 유형은 Go 프로그램에서 사용할 수 있지만 에이전트 구성 코드에서 \';MutexProfiling: true"를 지정하여 명시적으로 사용 설정해야 합니다. 컬렉션은 특정 루틴이 goroutine A에 의해 잠금 해제되었을 때 도어락이 잠금 해제될 때까지 기다리는 다른 goroutine B를 기록한 횟수를 기록("Contentions" 측정항목 아래에 표시)하여 작동합니다. 또한 차단된 goroutine이 잠금을 대기한 시간("Delay" 측정항목 아래에) 이 예에서는 단일 경합 스택이 있고 잠금 총 대기 시간은 11.03초입니다.

이 프로필을 생성하는 코드는 뮤텍스를 두고 싸우는 4개의 goroutine으로 구성됩니다.

main.go

func contention(d time.Duration) {
        contentionImpl(d)
}

func contentionImpl(d time.Duration) {
        for {
                mu.Lock()
                time.Sleep(d)
                mu.Unlock()
        }
}
...
func main() {
        ...
        for i := 0; i < 4; i++ {
                go contention(time.Duration(i) * 50 * time.Millisecond)
        }
}

이 실습에서는 Go 프로그램을 구성하여 Stackdriver Profiler에 사용하는 방법을 배웠습니다. 또한 이 도구를 사용하여 실적 데이터를 수집, 확인, 분석하는 방법도 배웠습니다. 이제 새로운 기술을 Google Cloud Platform에서 실행하는 실제 서비스에 적용할 수 있습니다.

지금까지 Stackdriver Profiler를 구성하고 사용하는 방법을 배웠습니다.

자세히 알아보기

라이선스

이 작업물은 Creative Commons Attribution 2.0 일반 라이선스에 따라 사용이 허가되었습니다.