Зачем 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:
- Continuous profiling в проде → собрал
default.pgo - Положил в репо рядом с
main.go go buildподхватывает PGO профиль- 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). На 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% незаметен, но и
проблем найти нечего. Просто
pprofad-hoc хватает. - Compliance запрещает читать stack trace production data (PII в strings), не используй sampling string captures.
- Embedded, IoT, нет ресурсов на eBPF agent.