go
2026년 3월 16일· 12분 읽기

Grafana Pyroscope로 Go 애플리케이션 Continuous Profiling 시작하기

#golang
#profiling
#pyroscope
#grafana
#continuous-profiling
#flame-graph
#observability
#performance
#pprof
#고랭
#프로파일링

1. 들어가며

Go에서 성능 분석을 할 때 net/http/pprofruntime/pprof를 주로 사용한다. 개발 환경에서 특정 시점의 CPU 사용량이나 메모리 할당을 스냅샷으로 확인하기에는 충분하지만, 프로덕션 환경에서는 몇 가지 한계가 있다.

  • 문제가 발생한 시점에 수동으로 프로파일을 수집해야 한다
  • 수집한 프로파일은 로컬 파일로만 남아 시간 경과에 따른 비교가 어렵다
  • 여러 인스턴스에서 분산된 프로파일 데이터를 중앙에서 관리할 수 없다

Continuous Profiling은 이러한 한계를 해결한다. 프로덕션에서 상시 낮은 오버헤드로 프로파일 데이터를 수집하고, 중앙 저장소에 보관하여 언제든 과거 데이터를 조회할 수 있다.

이 글에서는 Continuous Profiling 플랫폼인 Grafana Pyroscope를 Go 애플리케이션에 연동하는 방법을 실습한다. **Push 모드(SDK)**와 Pull 모드(Alloy) 두 가지 수집 방식을 모두 다루며, Flame Graph로 성능 병목을 분석하는 과정까지 살펴본다.

이 글에서 사용한 전체 코드는 GitHub에서 확인할 수 있다.

2. Continuous Profiling 개요

2.1 전통적 프로파일링 vs Continuous Profiling

구분전통적 프로파일링Continuous Profiling
수집 시점개발/디버깅 시 수동 실행프로덕션에서 상시 자동 수집
오버헤드높음 (개발 환경에서만 사용)낮음 (~2-5% CPU)
데이터 범위특정 시점 스냅샷시간 경과에 따른 연속 데이터
분석 방식사후 분석 (reactive)사전 예방적 분석 (proactive)
저장로컬 파일중앙 집중 DB (장기 보관)

전통적 프로파일링은 문제가 발생한 후 수동으로 데이터를 수집하는 반면, Continuous Profiling은 항상 데이터를 수집하므로 문제 발생 시점의 프로파일을 즉시 확인할 수 있다.

2.2 프로파일 유형 (Go 기준)

Go에서 수집할 수 있는 주요 프로파일 유형은 다음과 같다.

프로파일 유형설명활성화 방법
CPU함수별 CPU 사용 시간기본 활성화
Alloc (Objects/Space)메모리 할당 횟수/크기기본 활성화
Inuse (Objects/Space)현재 사용 중인 메모리기본 활성화
Goroutine활성 고루틴 수 및 스택선택 활성화
Mutex (Count/Duration)뮤텍스 경합 횟수/시간runtime.SetMutexProfileFraction()
Block (Count/Duration)블로킹 대기 횟수/시간runtime.SetBlockProfileRate()

Mutex와 Block 프로파일은 기본 비활성화이므로, 명시적으로 활성화해야 한다. Push 모드에서는 SDK 초기화 전에, Pull 모드에서는 애플리케이션 시작 시 설정한다.

3. Grafana Pyroscope 아키텍처

3.1 핵심 컴포넌트

Pyroscope는 다음과 같은 마이크로서비스 컴포넌트로 구성되며, Monolithic 모드에서는 단일 프로세스로 실행된다.

flowchart LR
    Client["Client\n(SDK / Alloy)"]
    Dist["Distributor"]
    Ing["Ingester"]
    Store["Object Storage"]
    QF["Query Frontend"]
    Q["Querier"]
    SG["Store Gateway"]
    UI["Grafana UI"]

    Client --> Dist --> Ing --> Store
    UI --> QF --> Q --> Ing
    Q --> SG --> Store
컴포넌트역할
Distributor클라이언트로부터 프로파일 데이터 수신 및 라우팅
Ingester메모리에 임시 저장 후 Object Storage에 쓰기
Querier프로파일 데이터 조회 및 병합
Query Frontend쿼리 캐싱 및 최적화
Store Gateway장기 저장소(Object Storage) 접근

3.2 데이터 수집 방식: Push vs Pull

Pyroscope는 두 가지 방식으로 프로파일 데이터를 수집할 수 있다. 데이터가 Pyroscope 서버에 도달한 이후의 저장, 조회, Flame Graph 분석은 어떤 수집 방식을 사용하든 완전히 동일하다. 차이는 수집 경로뿐이다.

flowchart TD
    subgraph push["Push 모드 (SDK)"]
        App1["Go App\n+ pyroscope-go SDK"] -->|"직접 전송"| PS1["Pyroscope Server"]
    end

    subgraph pull["Pull 모드 (Alloy)"]
        App2["Go App\n+ pprof 엔드포인트"] <-->|"주기적 스크래핑"| Alloy["Grafana Alloy"]
        Alloy -->|"전송"| PS2["Pyroscope Server"]
    end
기준Push (SDK)Pull (Alloy)
코드 변경SDK 추가 필요변경 없음 (pprof만 노출)
인프라추가 없음Alloy 설치 필요
Profiling LabelsTagWrapper로 세밀한 label 태깅 가능pprof 기본 label만 사용
기존 pprof 활용별도 공존 설정 필요그대로 활용
K8s 환경Pod마다 SDK 설정Alloy DaemonSet으로 일괄 수집
추천 상황새 프로젝트, 세밀한 분석 필요기존 서비스, 코드 변경 어려운 경우

실무 팁: Kubernetes 환경에서 이미 pprof를 노출하는 서비스가 많다면 Pull 모드가 효율적이다. 반면 엔드포인트별 프로파일링 같은 세밀한 분석이 필요하면 Push 모드의 TagWrapper가 유리하다.

4. 로컬 환경 구축

Docker Compose로 Pyroscope 서버, Grafana, 그리고 Push/Pull 모드 샘플 애플리케이션을 한 번에 실행할 수 있다.

4.1 Docker Compose 구성

services:
  # --- 공통 인프라 ---
  pyroscope:
    image: grafana/pyroscope:latest
    ports:
      - "4040:4040"
    networks:
      - pyroscope-net

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
    volumes:
      - ./grafana/provisioning:/etc/grafana/provisioning
    depends_on:
      - pyroscope
    networks:
      - pyroscope-net

  # --- Push 모드 ---
  app-http:
    build:
      context: .
      dockerfile: http-server/Dockerfile
    ports:
      - "8080:8080"
    depends_on:
      - pyroscope
    environment:
      - PYROSCOPE_SERVER=http://pyroscope:4040
      - PORT=8080
    networks:
      - pyroscope-net

  # --- Pull 모드 ---
  app-pull:
    build:
      context: .
      dockerfile: pull-server/Dockerfile
    ports:
      - "6060:6060"
    environment:
      - PORT=6060
    networks:
      - pyroscope-net

  alloy:
    image: grafana/alloy:latest
    volumes:
      - ./alloy/config.alloy:/etc/alloy/config.alloy
    command: ["run", "/etc/alloy/config.alloy"]
    depends_on:
      - pyroscope
      - app-pull
    networks:
      - pyroscope-net

networks:
  pyroscope-net:
    driver: bridge
> docker compose up -d

4.2 Grafana 데이터소스 연결

Grafana 프로비저닝 설정으로 Pyroscope 데이터소스를 자동 등록할 수 있다.

# grafana/provisioning/datasources/pyroscope.yml
apiVersion: 1

datasources:
  - name: Pyroscope
    type: grafana-pyroscope-datasource
    url: http://pyroscope:4040
    isDefault: true
    editable: true

4.3 접속 URL

서비스URL설명
Pyroscopehttp://localhost:4040Pyroscope UI
Grafanahttp://localhost:3000Grafana 대시보드 (익명 접속)
App (Push)http://localhost:8080Echo HTTP 서버 (Push 모드)
App (Pull)http://localhost:6060pprof 서버 (Pull 모드)

Grafana에서 Explore 메뉴 → Pyroscope 데이터소스를 선택하면 수집된 프로파일 데이터를 Flame Graph로 확인할 수 있다. Push 모드 앱은 echo.server, Pull 모드 앱은 pull.golang.app으로 표시된다.

5. 데이터 수집

5.1 Push 모드: SDK 연동

Push 모드는 애플리케이션에 Pyroscope Go SDK를 추가하여, 프로파일 데이터를 Pyroscope 서버로 직접 전송하는 방식이다.

5.1.1 SDK 설치 및 기본 설정

> go get github.com/grafana/pyroscope-go

pyroscope.Start()로 프로파일러를 초기화하면, 애플리케이션이 실행되는 동안 설정된 프로파일 유형의 데이터를 Pyroscope 서버로 지속 전송한다.

package main

import (
	"log"
	"os"
	"runtime"

	"github.com/grafana/pyroscope-go"
)

func main() {
	// 뮤텍스/블로킹 프로파일은 기본 비활성화이므로 명시적으로 활성화
	runtime.SetMutexProfileFraction(5)
	runtime.SetBlockProfileRate(5)

	profiler, err := pyroscope.Start(pyroscope.Config{
		ApplicationName: "simple.golang.app",       // Pyroscope UI에서 표시되는 이름
		ServerAddress:   "http://localhost:4040",    // Pyroscope 서버 주소
		Logger:          pyroscope.StandardLogger,
		Tags:            map[string]string{"hostname": os.Getenv("HOSTNAME")},
		ProfileTypes: []pyroscope.ProfileType{
			pyroscope.ProfileCPU,           // CPU 프로파일
			pyroscope.ProfileAllocObjects,  // 메모리 할당 횟수
			pyroscope.ProfileAllocSpace,    // 메모리 할당 크기
			pyroscope.ProfileInuseObjects,  // 현재 사용 중인 객체 수
			pyroscope.ProfileInuseSpace,    // 현재 사용 중인 메모리 크기
			pyroscope.ProfileGoroutines,    // 고루틴
			pyroscope.ProfileMutexCount,    // 뮤텍스 경합 횟수
			pyroscope.ProfileMutexDuration, // 뮤텍스 경합 시간
			pyroscope.ProfileBlockCount,    // 블로킹 횟수
			pyroscope.ProfileBlockDuration, // 블로킹 시간
		},
	})
	if err != nil {
		log.Fatalf("pyroscope 시작 실패: %v", err)
	}
	defer profiler.Stop() // 종료 시 마지막 프로파일 데이터 전송
}

5.1.2 주요 설정 항목

필드설명기본값
ApplicationNamePyroscope UI에서 표시되는 애플리케이션 이름(필수)
ServerAddressPyroscope 서버 URL(필수)
Tags프로파일 데이터에 추가할 메타데이터 태그nil
ProfileTypes수집할 프로파일 유형 목록CPU + Alloc + Inuse
Logger로깅 인터페이스nil
DisableGCRunsGC 실행 비활성화 (CPU 오버헤드 감소)false

5.1.3 Profiling Labels (TagWrapper)

참고: Profiling Labels는 Push 모드에서만 사용할 수 있다. Pull 모드에서는 pprof가 제공하는 기본 스택 트레이스만 수집되므로, 커스텀 label 태깅이 불가능하다. 이것이 Push/Pull 모드의 가장 큰 기능적 차이다.

Pyroscope의 TagWrapper를 사용하면 특정 코드 경로에 label을 태깅할 수 있다. 태깅된 프로파일 데이터는 Flame Graph에서 label 기준으로 필터링할 수 있어서, "어떤 엔드포인트가 CPU를 많이 쓰는가?"와 같은 질문에 답할 수 있다.

pyroscope.TagWrapper(ctx,
	pyroscope.Labels("workload", "cpu"),
	func(c context.Context) {
		cpuWork() // 이 블록의 프로파일 데이터에 workload=cpu label 태깅
	})

5.1.4 엔드포인트별 프로파일링

Echo HTTP 서버에서 각 핸들러를 TagWrapper로 감싸면 엔드포인트별 성능을 개별적으로 분석할 수 있다.

func handleSlow(c echo.Context) error {
	start := time.Now()

	pyroscope.TagWrapper(c.Request().Context(),
		pyroscope.Labels("endpoint", "/slow"),
		func(ctx context.Context) {
			fibonacci(38) // CPU 집약적 연산
		})

	return c.JSON(http.StatusOK, response{
		Message: "slow response (CPU intensive)",
		Elapsed: time.Since(start).String(),
	})
}

func handleMemory(c echo.Context) error {
	start := time.Now()

	pyroscope.TagWrapper(c.Request().Context(),
		pyroscope.Labels("endpoint", "/memory"),
		func(ctx context.Context) {
			allocateMemory() // 대량 메모리 할당
		})

	return c.JSON(http.StatusOK, response{
		Message: "memory response (heap allocation)",
		Elapsed: time.Since(start).String(),
	})
}

Grafana에서 Pyroscope 데이터소스를 조회하면 endpoint label로 /slow/memory 요청의 프로파일을 각각 필터링할 수 있다.

아래는 Push 모드(echo.server)의 CPU 프로파일 Flame Graph이다. main.fibonacci가 CPU 시간의 대부분을 차지하는 것을 한눈에 확인할 수 있다.

Push 모드 CPU Flame Graph

메모리 프로파일에서는 main.allocateMemory의 메모리 할당 패턴을 확인할 수 있다.

Push 모드 Memory Flame Graph

5.2 Pull 모드: Alloy 연동

Pull 모드는 애플리케이션 코드를 변경하지 않고, 기존 net/http/pprof 엔드포인트를 Grafana Alloy가 주기적으로 스크래핑하는 방식이다. Prometheus의 Pull 방식과 동일한 개념이다.

5.2.1 애플리케이션 측 설정

Pull 모드에서 애플리케이션은 pprof 엔드포인트만 노출하면 된다. Pyroscope SDK를 추가할 필요가 없다.

import (
	"net/http"
	_ "net/http/pprof" // /debug/pprof/* 엔드포인트 자동 등록
)

func main() {
	http.ListenAndServe(":6060", nil)
}

5.2.2 Grafana Alloy 설정

Alloy는 Grafana에서 만든 텔레메트리 수집기(collector)로, Pyroscope의 Pull 모드 수집을 담당한다. config.alloy 파일에 스크래핑 대상을 정의한다.

// config.alloy
pyroscope.scrape "default" {
  targets = [
    {"__address__" = "app-pull:6060", "service_name" = "pull.golang.app"},
  ]

  scrape_interval = "15s"  // 15초마다 스크래핑

  profiling_config {
    profile.process_cpu { enabled = true }           // CPU 프로파일
    profile.memory {                                  // 메모리 프로파일
      enabled = true
      path    = "/debug/pprof/allocs"
    }
    profile.goroutine { enabled = true }              // 고루틴 프로파일
    profile.mutex { enabled = true }                  // 뮤텍스 프로파일
    profile.block { enabled = true }                  // 블로킹 프로파일
  }

  forward_to = [pyroscope.write.endpoint.receiver]    // 수집 데이터 전송 대상
}

pyroscope.write "endpoint" {
  endpoint {
    url = "http://pyroscope:4040"                     // Pyroscope 서버 주소
  }
}

Alloy가 15초마다 pprof 엔드포인트를 스크래핑하므로, 부하 생성 후 잠시 기다리면 Grafana에서 pull.golang.app 애플리케이션으로 프로파일 데이터를 조회할 수 있다.

아래는 Pull 모드(pull.golang.app)의 CPU 프로파일이다. Push 모드와 동일하게 main.fibonacci가 CPU 병목으로 표시되지만, TagWrapper 기반 label 필터링은 사용할 수 없다.

Pull 모드 CPU Flame Graph

5.3 부하 테스트

Push/Pull 모드 모두 동일한 엔드포인트로 부하를 생성할 수 있다.

# --- Push 모드 (http://localhost:8080) ---
> curl http://localhost:8080/fast       # 빠른 응답 (기준선)
> curl http://localhost:8080/slow       # CPU 부하
> curl http://localhost:8080/memory     # 메모리 부하

# --- Pull 모드 (http://localhost:6060) ---
> curl http://localhost:6060/fast       # 빠른 응답
> curl http://localhost:6060/slow       # CPU 부하
> curl http://localhost:6060/memory     # 메모리 부하

# Pull 모드 pprof 엔드포인트 직접 확인
> curl http://localhost:6060/debug/pprof/

6. Grafana Profiles Drilldown

어떤 수집 방식을 사용하든 Pyroscope 서버에 저장된 프로파일 데이터는 동일한 방식으로 분석할 수 있다. 부하 생성 후 Grafana의 Drilldown > Profiles 메뉴에서 수집된 프로파일 데이터를 확인할 수 있다. Profiles Drilldown은 서비스 목록 → 프로파일 유형 → Flame Graph → Labels 순서로 점진적으로 분석 범위를 좁혀갈 수 있다.

6.1 All Services (서비스 목록)

첫 화면에서는 Pyroscope에 등록된 모든 서비스의 프로파일 데이터를 Grid 뷰로 보여준다.

Grafana Profiles Drilldown - All Services

서비스명설명수집 방식
echo.serverEcho HTTP 서버 (엔드포인트별 프로파일링)Push (SDK)
pull.golang.apppprof 엔드포인트를 노출하는 서버Pull (Alloy)
pyroscopePyroscope 서버 자체의 프로파일Push (자체 수집)
simple.golang.app기본 SDK 연동 예제Push (SDK)

상단의 Profile type 드롭다운에서 process_cpu/cpu, memory 등 프로파일 유형을 전환할 수 있고, 서비스 이름으로 검색 필터링도 가능하다.

6.2 Profile Types (프로파일 유형별 현황)

서비스를 선택하면 해당 서비스에서 수집 중인 모든 프로파일 유형을 한눈에 볼 수 있다. 아래는 echo.server의 Profile Types 화면이다.

Profile Types - echo.server

CPU, memory, goroutine, mutex, block 등 각 프로파일 유형의 시계열 그래프가 표시되어, 어떤 리소스에 이상이 있는지 빠르게 파악할 수 있다. 각 카드의 Flame graph 링크를 클릭하면 해당 프로파일 유형의 상세 Flame Graph로 이동한다.

6.3 Flame Graph (상세 분석)

특정 프로파일 유형을 선택하면 Flame Graph와 함께 심볼 테이블이 표시된다. 심볼 테이블에서는 각 함수의 Self time과 Total time을 정렬하여 성능 병목 함수를 빠르게 식별할 수 있다.

Flame Graph - echo.server CPU

Flame Graph는 프로파일링 데이터를 스택 트레이스 기반으로 시각화한 그래프다.

  • 가로축: 전체 시간 대비 해당 함수가 차지하는 비율 (넓을수록 많은 리소스 사용)
  • 세로축: 함수 호출 계층 (위에서 아래로 호출이 깊어짐)
  • 루트 노드: 전체 애플리케이션 시간의 100%
[              root (100%)                ]
[     funcA (60%)      ][   funcB (40%)   ]
[  funcC (30%) ][ funcD (30%) ]

Flame Graph를 분석할 때 주의할 점은 다음과 같다.

  • 넓은 블록 = 성능 병목 후보 (해당 함수에서 많은 시간 소비)
  • 깊은 스택 = 호출 체인이 깊음 (반드시 문제를 의미하지는 않음)
  • Self time vs Total time: 자기 자신의 실행 시간 vs 하위 함수를 포함한 전체 시간

주요 분석 기능은 다음과 같다.

  • 시간 범위 선택: 특정 시간 구간의 프로파일만 분석
  • 함수 클릭: 해당 함수 중심으로 필터링하여 상세 확인
  • Labels 필터링: endpoint=/slow 등으로 특정 코드 경로만 분석 (Push 모드에서 label 태깅한 경우)

6.4 Labels (label별 분류)

Labels 탭에서는 프로파일 데이터를 label 기준으로 그룹화하여 볼 수 있다. Push 모드에서 TagWrapper로 태깅한 label(예: hostname, pyroscope_spy)별로 시계열을 분리하여 비교할 수 있다.

Labels - echo.server

6.5 Diff Flame Graph (비교 분석)

Diff flame graph 탭에서는 두 시간 구간의 프로파일을 나란히 비교할 수 있다. Baseline과 Comparison 구간을 각각 선택하면, 변경 전후의 성능 차이를 색상으로 시각화한다 (빨간색=증가, 초록색=감소).

Diff Flame Graph

7. 실전 팁

7.1 프로덕션 적용 시 주의사항

  • 오버헤드 관리: Pyroscope SDK의 CPU 오버헤드는 약 2-5%이다. DisableGCRuns: true 옵션으로 GC 관련 오버헤드를 줄일 수 있다
  • 프로파일 유형 선택: 모든 프로파일을 활성화하면 오버헤드가 늘어나므로, CPU와 메모리 프로파일만 기본 활성화하고 필요 시 Mutex/Block을 추가하는 것을 권장한다
  • SetMutexProfileFractionSetBlockProfileRate: 값이 작을수록 더 많은 이벤트를 기록한다. 프로덕션에서는 5 이상의 값으로 오버헤드를 조절한다

7.2 기존 pprof 코드와의 공존

Pyroscope Go SDK는 내부적으로 runtime/pprof를 사용한다. 기존에 net/http/pprof를 사용하고 있다면 Pyroscope SDK와 함께 사용할 수 있다.

import _ "net/http/pprof" // 기존 pprof HTTP 엔드포인트 유지

// Pyroscope SDK 추가 - 동일한 프로파일 데이터를 Pyroscope 서버로도 전송
profiler, _ := pyroscope.Start(pyroscope.Config{...})
defer profiler.Stop()

기존 pprof 엔드포인트는 즉석 디버깅용으로 유지하면서, Pyroscope로 상시 프로파일링 데이터를 수집하는 하이브리드 구성이 가능하다.

7.3 Push/Pull 모드 마이그레이션

이미 Pull 모드로 운영 중인 서비스에 Push 모드를 추가하거나, 그 반대도 가능하다.

  • Pull → Push 전환: SDK를 추가하고, Alloy 설정에서 해당 타겟을 제거한다. TagWrapper로 세밀한 label 태깅이 필요해진 경우에 전환한다.
  • Push + Pull 공존: SDK로 Push하면서 pprof 엔드포인트도 노출할 수 있다. 다만 Alloy가 같은 서비스를 스크래핑하면 데이터가 중복되므로, 하나의 수집 방식만 활성화하는 것을 권장한다.

8. 마무리

이 글에서는 Grafana Pyroscope를 활용한 Go 애플리케이션 Continuous Profiling을 다루었다.

  • Continuous Profiling은 프로덕션에서 상시 프로파일을 수집하여, 전통적 pprof의 "문제 발생 후 수동 수집" 한계를 해결한다
  • **Push 모드(SDK)**는 pyroscope.Start() 한 줄로 연동할 수 있으며, TagWrapper로 엔드포인트별 세밀한 분석이 가능하다
  • **Pull 모드(Alloy)**는 코드 변경 없이 기존 pprof 엔드포인트를 활용하여, 특히 K8s 환경에서 DaemonSet으로 여러 서비스를 일괄 수집하기에 유리하다
  • Flame Graph를 통해 성능 병목을 시각적으로 빠르게 파악하고, 비교/Diff 뷰로 변경 전후의 성능 차이를 확인할 수 있다

전체 코드는 GitHub에서 확인할 수 있다.

9. 참고

관련 글