- Authors
- Name
- はじめに
- Continuous Profilingとは?
- Pyroscope アーキテクチャ
- 言語別SDK連携
- Flame Graph 分析
- Grafana ダッシュボード連携
- 実践的なパフォーマンス改善事例
- プロファイル比較(Diff View)
- まとめ
はじめに
Metrics、Logs、Tracesに続くオブザーバビリティの第4の柱がContinuous Profilingです。CPUは十分なのにレイテンシが高い場合は?メモリ使用量が徐々に増加している場合は?従来のモニタリングでは原因を特定するのが困難です。
PyroscopeはGrafanaエコシステムに統合されたcontinuous profilingツールで、コードレベルでパフォーマンスのボトルネックを継続的に追跡します。
Continuous Profilingとは?
従来のプロファイリングは「問題が発生した後」に手動で実行していましたが、Continuous Profilingは常にオンのプロファイラーです。
| 項目 | 従来のプロファイリング | Continuous Profiling |
|---|---|---|
| 実行時点 | 問題発生後に手動 | 常時自動収集 |
| オーバーヘッド | 高い(10-50%) | 低い(2-5%) |
| 履歴 | なし | 時系列で保存 |
| 環境 | 開発/ステージング | プロダクション |
| 比較分析 | 困難 | 時間帯別の比較が可能 |
Pyroscope アーキテクチャ
Pyroscopeは2つのコンポーネントで構成されます:
- 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
import pyroscope
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
import (
"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
import io.pyroscope.javaagent.PyroscopeAgent;
import io.pyroscope.javaagent.config.Config;
@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がCPUの80%を占有
# 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パネルで2つの時間帯のプロファイルをオーバーレイして比較できます。赤色は増加、緑色は減少を示します。
まとめ
Continuous Profilingはオブザーバビリティの最後のピースです:
- Metrics: 「何が」遅いかを教えてくれる
- Logs: 「何が起きた」かを教えてくれる
- Traces: 「どこで」遅いかを教えてくれる
- Profiles: 「なぜ」遅いかを教えてくれる(コードレベル)
Pyroscope + Grafanaの組み合わせで、プロダクション環境のパフォーマンス問題をコードレベルまで追跡しましょう。
クイズ:Continuous Profiling 理解度チェック(7問)
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から該当時点のコードレベルのプロファイルに直接移動でき、ボトルネックの原因を正確に特定できます。