# Continuous profiling: Pyroscope, eBPF, flame graphs в проде _Observability и мониторинг · LinuxLab Knowledge Base_ **TL;DR:** Continuous profiling - always-on CPU/memory profiler в проде через eBPF. 1-2% overhead. Flame graphs показывают hot path. Pyroscope (Grafana), Parca, Polar Signals. Замена ad-hoc perf для production debug. ## Зачем continuous profiling Классический profiling, Brendan Gregg-style: SSH на хост, `perf record`, `perf script | flamegraph.pl`. Hassle: - Реактивно (после инцидента, обычно поздно) - Один хост, один процесс - Snapshot, не trends - Требует root, sudo, debug symbols on disk - Overhead **в момент** замера ~5-10% CPU **Continuous profiling**, always-on profiler в каждом узле, sample ~100 Hz, оверхед **1-2% CPU**, шлёт в central storage. Появилось ~2020 (Google "Continuous Profiling" paper, Pyroscope). Что даёт: - **Постоянные flame graphs**, открыл Grafana, видишь где CPU тратится прямо сейчас, или 2 недели назад - **Diff между версиями**, flame graph до/после релиза, увидеть регрессию - **Correlation с метриками**, alert «p99 latency растёт» → сразу видишь stack где это - **Root-cause за минуты**, не за дни В 2025, production-tool в Cloudflare, Polar Signals, Coinbase, Pinterest. Самые заметные runtime регрессии находят профайлингом. ## Как работает eBPF profiler Старый perf: kernel-ring-buffer, perf_event_open, sample на каждом CPU через PMU (timer interrupt). Stack walk через frame pointers или DWARF. **eBPF profiler** ([[ebpf-basics|eBPF]]), eBPF программа атачится к perf event (sample 100 Hz): ``` int profile(struct bpf_perf_event_data *ctx) { u32 pid = bpf_get_current_pid_tgid() >> 32; u64 *count = bpf_map_lookup_elem(&counts, &pid); bpf_get_stack(ctx, stack, sizeof(stack), 0); // kernel stack bpf_get_stack(ctx, stack, sizeof(stack), USER); // user stack // aggregate in BPF map } ``` Преимущества: - **Frame-pointer-free** stack walking (DWARF unwinding в eBPF, в последних ядрах) - **Per-cgroup filtering**, profiles per Kubernetes pod без знания PID - **Symbolization on-host**, eBPF читает /proc/PID/maps, находит ELF, extract symbols - **CO-RE** ([[bpf-co-re|BPF Compile Once Run Everywhere]]), один binary на разные ядра ## Flame graphs, как читать Каждый rectangle, функция. **Width = доля CPU time**. Y-axis call stack (parent внизу, children сверху). ``` ┌─────────────────────────────────────────────┐ │ main() │ ├─────────────────┬───────────────────────────┤ │ serve_request │ gc_collect │ ├──────────┬──────┼─────────────┬─────────────┤ │ parse │ db │ sweep_old │ allocate │ ├──┬───────┼──────┼─────────────┼─────────────┤ │j │ regex │ scan │ │ │ └──┴───────┴──────┴─────────────┴─────────────┘ ``` «`regex` занимает 25% CPU всего процесса». Если плато широкое и плоское, leaf hot. Если высокое и узкое, глубокий call stack без plateau, обычно нормально. Flame graph **самплируется**, не traces, точные числа неточны (±5%), но пропорции верные. ## Pyroscope architecture ``` ┌───────────────┐ pull profiles ┌──────────────┐ │ Pyroscope │ ─────────────────► │ target │ │ server │ │ (with eBPF │ │ ┌───────────┐ │ │ profiler │ │ │ TSDB-like │ │ │ agent) │ │ │ profiles │ │ └──────────────┘ │ │ store │ │ ┌──────────────┐ │ └───────────┘ │ ◄───── push ────── │ target │ │ ┌───────────┐ │ │ (with SDK │ │ │ Querier │ │ ◄─── Grafana │ push agent) │ │ └───────────┘ │ UI └──────────────┘ └───────────────┘ ``` Storage похож на [[prometheus-basics|Prometheus TSDB]]: profile per (service, instance) per timestamp. Query, flame graph за range с filter по labels. Два режима ingest: - **Pull eBPF agent** (DaemonSet), single agent профилирует всё на хосте без SDK. Best для k8s. - **Push SDK**, runtime-specific (Java, Go, Python). Точнее для managed runtimes (Java JIT-frames eBPF не видит). ## Labels, каждый profile тегирован Как и Prometheus, labels определяют series: ``` service.name=checkout pod=checkout-7f8b9c-q2lx9 namespace=prod region=us-east-1 version=1.4.2 ``` Можно фильтровать в UI: «flame graph для `version=1.4.2 vs 1.4.1`», «diff between `region=us-east vs us-west`». Это и есть мощь, slice по label, найти аномалию. Cardinality правила те же: `pod`, десятки тысяч, OK с retention. `request_id`, никогда. ## Profile types Pyroscope/Parca собирают разные типы: | Type | Что показывает | |------|----------------| | `process_cpu` | on-CPU sampling - где время CPU | | `goroutine` (Go) | живые goroutines - leak detection | | `inuse_space` (Go heap) | currently alloc'd memory | | `alloc_space` (Go heap) | total allocated since start | | `block` (Go) | blocking on chan/mutex | | `mutex` (Go) | contention | | `wall_clock` | wall-time (включая sleep, IO wait) - что блокирует | | `lock_contention` (Java) | JFR-based | Для Java auto-magic через [[opentelemetry|OpenTelemetry profiling]] + JFR (Java Flight Recorder). ## Pyroscope vs Parca vs Polar Signals | Tool | Backend | Storage | Маркетинг | |------|---------|---------|-----------| | **Pyroscope** | Grafana Labs (acq 2023) | own TSDB-like | OSS, integrated с Grafana | | **Parca** | Polar Signals | own | OSS, Parquet-on-S3 | | **Polar Signals Cloud** | hosted | proprietary | $$ paid | | **GProfiler** | Granulate (acq Intel) | own | автоматическая ARM/x86 | Pyroscope, recommended для самохоста с Grafana stack. Parca, если хочется `parquet`-friendly query через DuckDB/SQL. ## Continuous profiling vs traditional perf | Свойство | perf record | continuous profiler | |----------|-------------|---------------------| | When | ad-hoc (после инцидента) | always-on | | Overhead | 5-10% во время записи | 1-2% постоянно | | Storage | local file (perf.data) | central, retention | | Symbolization | locally | on-agent + central | | Multi-host | нет | да, per-pod | | Diff между датами | вручную через folded | UI feature | | Stack accuracy | DWARF + perf | eBPF + DWARF/frame-pointers | ## CPU profiling vs memory Для CPU bottleneck, `process_cpu` тип, sample @100Hz, hot path. Для memory leak, `alloc_space` (Go heap-profiler) или Java JFR `Allocation*` events. Видно **где аллоцируется мусор**, что после GC растёт. Memory profiling overhead выше (~3-5%), не always-on в проде. Включай по запросу или при `memory.high` triggered ([[cgroups-v2-deep|cgroup PSI]]). ## Profile-Guided Optimization (PGO) Go 1.21+, Rust nightly: компилятор берёт production profile как hint, оптимизирует hot code (better inlining, branch prediction). Workflow: 1. Continuous profiling в проде → собрал `default.pgo` 2. Положил в репо рядом с `main.go` 3. `go build` подхватывает PGO профиль 4. Speedup 2-15% на типичный Go-сервис Pyroscope есть `pyroscope-pgo` exporter для этого. ## Когда что-то пошло не так - **Stack «[unknown]»**, нет debug symbols или frame pointers отсутствуют (gcc default `-fomit-frame-pointer`). Build с `-fno-omit-frame-pointer` или enable DWARF unwinding в profiler. - **Java stack frames пустые**, JIT compilation, нужен JFR-based instead of pure eBPF. - **High cardinality**, каждый pod = новый series. Лимит retention или drop pod label, оставь deployment. - **Profiles не приходят**, eBPF agent требует `CAP_SYS_ADMIN` + `CAP_BPF` ([bpf-co-re](/kb/bpf-co-re.md)). На k8s, `securityContext.privileged` или подходящие capabilities. - **Дельта между версиями странная**, разные [[cgroups-v2-deep|cgroup PSI throttling]] изменили execution time, не код. Контролируй test environment. - **Async/await stacks разорваны**, Go goroutine, Rust async. Pyroscope Go SDK + ebpf делают это, Python asyncio, partially. ## Когда НЕ использовать - **Маленький сервис < 10K rps**, overhead 1-2% незаметен, но и проблем найти нечего. Просто `pprof` ad-hoc хватает. - **Compliance** запрещает читать stack trace **production data** (PII в strings), не используй sampling string captures. - **Embedded**, IoT, нет ресурсов на eBPF agent. ## Команды ```bash pyroscope agent --target-pid=$(pgrep -f myapp) --tag=service=myapp ``` Pyroscope eBPF agent на конкретный PID - быстрая проверка ```bash curl -s 'http://pyroscope:4040/render?query=process_cpu{service="checkout"}&from=now-1h&until=now&format=collapsed' | head -20 ``` Сырые folded stacks (формат для FlameGraph.pl) для query 'CPU usage of checkout' ```bash go tool pprof -http=:8080 'http://pyroscope:4040/render?query=process_cpu{...}&format=pprof' ``` Открыть Pyroscope профиль в `go tool pprof` локально ```bash perf record -F 99 -g -p $(pgrep myapp) -- sleep 30 ``` Классический perf для сравнения с eBPF-based - 99Hz, 30 сек ```bash parca-agent --node=$NODE --kubernetes --metadata-external-labels=cluster=prod ``` Parca agent на k8s node - DaemonSet-style deploy ```bash go test -cpuprofile=cpu.prof ./... && go tool pprof -http=: cpu.prof ``` On-demand pprof в Go - локально для bench, не для prod ```bash OTEL_EXPORTER_OTLP_PROFILES_ENDPOINT=http://collector:4318 java -javaagent:opentelemetry-javaagent.jar -jar app.jar ``` OTel Java agent с profiling-enabled (experimental в OTel 1.30+) ## См. также - [eBPF - программируемый kernel](/kb/ebpf-basics.md) - [BPF CO-RE - Compile Once Run Everywhere](/kb/bpf-co-re.md) - [cgroups v2 - unified hierarchy, PSI, eBPF control](/kb/cgroups-v2-deep.md) - [Prometheus: scrape, TSDB, PromQL и production-pitfalls](/kb/prometheus-basics.md) - [OpenTelemetry: signals, OTLP, Collector pipeline](/kb/opentelemetry.md)