Skip to content

필사 모드: Pyroscope로 Kubernetes 애플리케이션 Continuous Profiling 구축하기

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며

Metrics, Logs, Traces에 이은 관측성의 네 번째 기둥이 **Continuous Profiling**입니다. CPU는 충분한데 레이턴시가 높다면? 메모리 사용량이 서서히 증가한다면? 전통적인 모니터링으로는 원인을 찾기 어렵습니다.

**Pyroscope**는 Grafana 생태계에 통합된 continuous profiling 도구로, 코드 레벨에서 성능 병목을 지속적으로 추적합니다.

Continuous Profiling이란?

기존 프로파일링은 "문제가 발생한 후" 수동으로 실행했지만, Continuous Profiling은 **항상 켜져 있는** 프로파일러입니다.

| 항목 | 전통적 프로파일링 | Continuous Profiling |

| --------- | ----------------- | -------------------- |

| 실행 시점 | 문제 발생 후 수동 | 상시 자동 수집 |

| 오버헤드 | 높음 (10-50%) | 낮음 (2-5%) |

| 히스토리 | 없음 | 시계열 저장 |

| 환경 | 개발/스테이징 | 프로덕션 |

| 비교 분석 | 어려움 | 시간대별 비교 가능 |

Pyroscope 아키텍처

Pyroscope는 두 가지 컴포넌트로 구성됩니다:

- **Pyroscope Server**: 프로파일 데이터를 수집, 저장, 쿼리

- **Agent/SDK**: 애플리케이션에서 프로파일 데이터를 수집하여 서버로 전송

Kubernetes에 Pyroscope 설치

Helm으로 Pyroscope 설치

helm repo add grafana https://grafana.github.io/helm-charts

helm repo update

helm install pyroscope grafana/pyroscope \

--namespace monitoring \

--create-namespace \

--set pyroscope.extraArgs.storage.backend=filesystem \

--set persistence.enabled=true \

--set persistence.size=50Gi

Grafana Alloy로 eBPF 프로파일링

Grafana Alloy(구 Grafana Agent)를 사용하면 코드 수정 없이 eBPF로 프로파일을 수집할 수 있습니다.

alloy-config.yaml

apiVersion: v1

kind: ConfigMap

metadata:

name: alloy-config

namespace: monitoring

data:

config.alloy: |

pyroscope.ebpf "instance" {

forward_to = [pyroscope.write.endpoint.receiver]

targets_only = false

default_target = {"service_name" = "unspecified"}

targets = discovery.kubernetes.pods.targets

}

discovery.kubernetes "pods" {

role = "pod"

}

pyroscope.write "endpoint" {

endpoint {

url = "http://pyroscope:4040"

}

}

alloy-daemonset.yaml

apiVersion: apps/v1

kind: DaemonSet

metadata:

name: alloy

namespace: monitoring

spec:

selector:

matchLabels:

app: alloy

template:

metadata:

labels:

app: alloy

spec:

hostPID: true # eBPF에 필수

containers:

- name: alloy

image: grafana/alloy:latest

args:

- run

- /etc/alloy/config.alloy

securityContext:

privileged: true

runAsUser: 0

volumeMounts:

- name: config

mountPath: /etc/alloy

- name: sys-kernel

mountPath: /sys/kernel

volumes:

- name: config

configMap:

name: alloy-config

- name: sys-kernel

hostPath:

path: /sys/kernel

언어별 SDK 연동

Python (FastAPI)

pip install pyroscope-io

pyroscope.configure(

application_name="order-service",

server_address="http://pyroscope:4040",

tags={

"region": "ap-northeast-2",

"env": "production",

},

)

from fastapi import FastAPI

app = FastAPI()

@app.get("/orders/{order_id}")

async def get_order(order_id: str):

이 함수의 CPU/메모리 사용이 자동으로 프로파일됨

order = await db.fetch_order(order_id)

return order

Go

package main

"github.com/grafana/pyroscope-go"

"net/http"

)

func main() {

pyroscope.Start(pyroscope.Config{

ApplicationName: "api-gateway",

ServerAddress: "http://pyroscope:4040",

ProfileTypes: []pyroscope.ProfileType{

pyroscope.ProfileCPU,

pyroscope.ProfileAllocObjects,

pyroscope.ProfileAllocSpace,

pyroscope.ProfileInuseObjects,

pyroscope.ProfileInuseSpace,

pyroscope.ProfileGoroutines,

pyroscope.ProfileMutexCount,

pyroscope.ProfileMutexDuration,

pyroscope.ProfileBlockCount,

pyroscope.ProfileBlockDuration,

},

Tags: map[string]string{

"env": "production",

},

})

http.HandleFunc("/health", healthHandler)

http.ListenAndServe(":8080", nil)

}

Java (Spring Boot)

// build.gradle

// implementation 'io.pyroscope:agent:0.13.1'

// application.yml

// pyroscope:

// application-name: user-service

// server-address: http://pyroscope:4040

// format: jfr

@SpringBootApplication

public class UserServiceApplication {

public static void main(String[] args) {

PyroscopeAgent.start(

new Config.Builder()

.setApplicationName("user-service")

.setServerAddress("http://pyroscope:4040")

.setProfilingEvent(EventType.ITIMER)

.setFormat(Format.JFR)

.build()

);

SpringApplication.run(UserServiceApplication.class, args);

}

}

Flame Graph 분석

Flame Graph 읽는 법

Flame Graph에서:

- **X축**: 샘플 비율 (넓을수록 해당 함수에서 더 많은 시간 소비)

- **Y축**: 호출 스택 깊이 (아래에서 위로)

- **색상**: 무작위 (구분용)

Pyroscope CLI로 프로파일 조회

pyroscope query \

--app-name order-service \

--from "now-1h" \

--until "now" \

--profile-type cpu

병목 진단 패턴

**CPU 병목 예시:**

main.handleRequest (40%)

└── db.QueryRow (35%)

└── net/http.(*conn).readRequest (30%)

└── crypto/tls.(*Conn).Read (25%)

해석: TLS 핸드셰이크가 CPU의 25%를 차지 → 커넥션 풀링으로 해결

**메모리 누수 예시:**

runtime.mallocgc (60%)

└── encoding/json.(*Decoder).Decode (45%)

└── main.processLargePayload (40%)

해석: JSON 디코딩에서 대량의 메모리 할당 → 스트리밍 파서로 전환

Grafana 대시보드 연동

Grafana에 Pyroscope 데이터 소스 추가

Settings > Data Sources > Add > Grafana Pyroscope

URL: http://pyroscope:4040

유용한 Grafana 패널 설정

{

"targets": [

{

"datasource": { "type": "grafana-pyroscope-datasource" },

"profileTypeId": "process_cpu:cpu:nanoseconds:cpu:nanoseconds",

"labelSelector": "{service_name=\"order-service\"}",

"queryType": "profile"

}

]

}

Traces와 Profiling 연동

Tempo(분산 추적)와 Pyroscope를 연동하면, 느린 trace에서 바로 해당 시점의 프로파일을 확인할 수 있습니다.

Tempo 설정에 Pyroscope 연동 추가

tempo.yaml

overrides:

defaults:

profiles:

pyroscope:

url: http://pyroscope:4040

실전 성능 개선 사례

사례 1: N+1 쿼리 발견

프로파일에서 db.fetch_order가 80%의 CPU를 차지

Flame Graph로 확인하면 같은 쿼리가 반복 호출

Before: N+1

async def get_orders_with_items(user_id):

orders = await db.fetch_orders(user_id)

for order in orders:

order.items = await db.fetch_items(order.id) # N번 호출!

return orders

After: JOIN으로 1번 조회

async def get_orders_with_items(user_id):

return await db.fetch_orders_with_items(user_id) # 1번 호출

사례 2: 메모리 누수 추적

// 프로파일에서 runtime.mallocgc가 지속적으로 증가

// inuse_space 프로파일로 확인

// Before: 버퍼를 매번 새로 할당

func processRequest(data []byte) {

buf := make([]byte, 1024*1024) // 매 요청마다 1MB 할당

// ...

}

// After: sync.Pool로 버퍼 재사용

var bufPool = sync.Pool{

New: func() interface{} {

buf := make([]byte, 1024*1024)

return &buf

},

}

func processRequest(data []byte) {

buf := bufPool.Get().(*[]byte)

defer bufPool.Put(buf)

// ...

}

프로파일 비교 (Diff View)

배포 전후 프로파일 비교

pyroscope diff \

--app-name order-service \

--left-from "2026-03-02T10:00:00Z" \

--left-until "2026-03-02T11:00:00Z" \

--right-from "2026-03-03T10:00:00Z" \

--right-until "2026-03-03T11:00:00Z" \

--profile-type cpu

Grafana에서도 Explore 패널에서 두 시간대의 프로파일을 오버레이하여 비교할 수 있습니다. 빨간색은 증가, 녹색은 감소를 나타냅니다.

정리

Continuous Profiling은 관측성의 마지막 퍼즐입니다:

- **Metrics**: "무엇이" 느린지 알려줌

- **Logs**: "무슨 일이" 일어났는지 알려줌

- **Traces**: "어디서" 느린지 알려줌

- **Profiles**: "왜" 느린지 알려줌 (코드 레벨)

Pyroscope + Grafana 조합으로 프로덕션 환경의 성능 문제를 코드 레벨까지 추적하세요.

**Q1. Continuous Profiling이 전통적 프로파일링과 다른 점은?**

항상 켜져 있어 프로덕션에서 지속적으로 수집하며, 오버헤드가 2-5%로 낮고, 시간대별 비교가 가능합니다.

**Q2. eBPF 프로파일링의 장점은?**

코드 수정 없이 커널 레벨에서 모든 프로세스의 프로파일을 수집할 수 있습니다.

**Q3. Flame Graph에서 X축이 의미하는 것은?**

해당 함수(와 하위 함수)에서 소비한 시간의 비율입니다. 넓을수록 더 많은 시간을 사용합니다.

**Q4. Grafana Alloy DaemonSet에서 hostPID: true가 필요한 이유는?**

eBPF가 호스트의 프로세스 정보에 접근하려면 호스트 PID 네임스페이스가 필요합니다.

**Q5. 메모리 누수를 추적하는 데 적합한 프로파일 타입은?**

inuse_space (현재 사용 중인 메모리) 프로파일로 시간에 따라 증가하는 할당을 추적합니다.

**Q6. Diff View의 활용 사례는?**

배포 전후의 프로파일을 비교하여 새 코드가 성능에 미친 영향을 확인합니다.

**Q7. Traces와 Profiling을 연동하면 어떤 이점이 있나요?**

느린 trace에서 해당 시점의 코드 레벨 프로파일로 바로 이동하여 병목 원인을 정확히 파악할 수 있습니다.

현재 단락 (1/231)

Metrics, Logs, Traces에 이은 관측성의 네 번째 기둥이 **Continuous Profiling**입니다. CPU는 충분한데 레이턴시가 높다면? 메모리 사용량이 서...

작성 글자: 0원문 글자: 6,171작성 단락: 0/231