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, EMACs, Nano 등의 표준 Linux 텍스트 편집기에 관한 기본 지식

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

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

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

초급 중급 고급

자습형 환경 설정

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

Screenshot from 2016-02-10 12:45:26.png

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

그런 다음 Google Cloud 리소스를 사용할 수 있도록 Cloud 콘솔에서 결제를 사용 설정해야 합니다.

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

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

Google Cloud Shell

Google Cloud를 노트북에서 원격으로 실행할 수 있지만, 이 Codelab에서는 설정을 간소화하기 위해 클라우드에서 실행되는 명령줄 환경인 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 콘솔의 왼쪽 탐색 메뉴에서 'Profiler'를 클릭하여 Profiler UI로 이동합니다.

또는 Cloud Console 검색창을 사용하여 프로파일러 UI로 이동할 수 있습니다. 'Stackdriver Profiler'를 입력하고 검색된 항목을 선택하면 됩니다. 어느 방법을 사용하든지 아래와 같이 '표시할 데이터 없음' 메시지가 표시된 Profiler UI가 표시됩니다. 새 프로젝트이므로 아직 수집된 프로파일링 데이터가 없습니다.

이제 프로파일링할 항목을 가져올 시간입니다.

GitHub에서 제공되는 간단한 합성 Go 애플리케이션을 사용합니다. 아직 열려 있는 Cloud Shell 터미널에서 (프로파일러 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, 힙, 스레드 프로필을 수집합니다. 여기 코드는 뮤텍스('경합'이라고도 함) 프로필의 수집을 사용 설정합니다.

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

$ go run main.go

프로그램이 실행되면 프로파일링 에이전트가 구성된 5가지 유형의 프로필을 주기적으로 수집합니다. 수집은 시간이 지남에 따라 무작위로 이루어지므로 (각 유형의 평균 비율은 분당 프로필 1개) 각 유형이 수집되는 데 최대 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'를 입력하여 종료하면 됩니다.

이제 데이터를 수집했으니 자세히 살펴보겠습니다. Google에서는 프로덕션에서 발생하는 다양한 종류의 성능 문제를 시뮬레이션하는 합성 앱 (소스는 GitHub에서 제공)을 사용하고 있습니다.

CPU 집약적 코드

CPU 프로필 유형을 선택합니다. UI가 로드되면 load 함수의 4개 리프 블록이 플레임 그래프에 표시되며, 이 블록은 전체 CPU 소비를 나타냅니다.

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

main.go

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

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

프로필 데이터 필터를 사용하여 표시를 추가로 세분화할 수 있습니다. 예를 들어 필터 문자열로 'baz'를 지정하는 '스택 표시' 필터를 추가해 보세요. load() 호출 경로 4개 중 2개만 표시되는 아래 스크린샷과 같은 화면이 표시됩니다. 이 두 경로만 이름에 문자열 'baz'가 있는 함수를 통과합니다. 이러한 필터링은 더 큰 프로그램의 일부에만 집중하려는 경우에 유용합니다 (예: 프로그램의 일부만 소유한 경우).

메모리 집약적 코드

이제 '힙' 프로필 유형으로 전환합니다. 이전 실험에서 만든 필터를 삭제해야 합니다. 이제 alloc에 의해 호출되는 allocImpl가 앱에서 메모리의 기본 소비자로 표시되는 플레임 그래프가 표시됩니다.

플레임 그래프 위의 요약 표는 앱에서 사용된 총 메모리 양이 평균 ~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 집약적 코드

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

Flame 그래프 위의 요약에서 wait 함수에서 호출 스택이 증가하는 goroutine이 100개 있음을 확인할 수 있습니다. 이러한 대기를 시작하는 코드가 다음과 같다는 점을 고려하면 이는 정확합니다.

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'를 지정하여 명시적으로 사용 설정해야 합니다. 이 컬렉션은 특정 잠금이 고루틴 A에 의해 잠금 해제될 때 다른 고루틴 B가 잠금이 잠금 해제되기를 기다린 횟수를 '경합' 측정항목 아래에 기록하여 작동합니다. 또한 차단된 goroutine이 잠금을 기다린 시간을 '지연' 측정항목에 기록합니다. 이 예시에는 하나의 경합 스택이 있으며 잠금의 총 대기 시간은 11.03초입니다.

이 프로필을 생성하는 코드는 뮤텍스를 두고 경쟁하는 4개의 고루틴으로 구성됩니다.

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 일반 라이선스에 따라 사용이 허가되었습니다.