Введение

Классический мониторинг отвечает на вопрос «что сломалось»: метрики показывают рост CPU, логи — стек ошибок, трейсы — медленный сервис. Но когда нужно ответить «почему именно этот сервис стал медленным», почти все инструменты пасуют. Вы видите, что контейнер потребляет 2 ядра CPU, но не видите, какая строка кода их загружает.

Coroot — open-source observability-платформа, которая из метрик, логов и трейсов делает выводы о том, что чинить. Её ключевая особенность — непрерывное профилирование без изменений кода: eBPF-профилировщик снимает CPU-профили всех процессов на ноде, а языковые профилировщики (Go, Java) добавляют память и блокировки. В результате флеймграф показывает нагрузку до точной строки кода, а предустановленные инспекции находят типовые проблемы — утечки памяти, лишние аллокации, блокировки.

Coroot ставится в любой Kubernetes-кластер. В этой статье мы развернём Coroot через официальный coroot-operator (Community Edition), а затем задеплоим четыре намеренно «сломанных» приложения — на Nuxt (Node.js), Python, Go и Java — и посмотрим, как их проблемы всплывают в профилировании.

Coroot vs Pyroscope vs Parca vs Pixie vs Perforator

Метрика

Coroot (Community Edition)

Grafana Pyroscope

Parca

Pixie

Perforator (Yandex)

Профилирование

eBPF CPU + Go (heap/pprof) + Java (async-profiler)

языковые SDK, Grafana Alloy, OTLP; eBPF через Alloy/OTel

eBPF + pprof

eBPF-автоинструментация k8s, CPU-профили

eBPF kernel + userspace, CPU, sPGO/AutoFDO

Нужны ли изменения кода

Нет (eBPF CPU + Go heap), только для blocking/mutex/goroutine — опционально pprof

Да — SDK/агент (eBPF только через Alloy/OTel)

Нет (eBPF)

Нет (eBPF)

Нет (eBPF)

Метрики + логи + трейсы

есть, в одном UI

нет (только профили)

нет (только профили)

частично (eBPF-метрики, запросы и трейсы)

нет (только профили)

Автодиагностика (инспекции)

есть, 80%+ типовых проблем

нет

нет

частично (готовые PxL-скрипты)

нет

SLO-алертинг

есть

нет

нет

нет

нет

Service Map

есть

нет

нет

частично (по eBPF-трафику)

нет

Хранилище профилей

ClickHouse

S3-совместимое

object storage

локально в кластере (краткосрочное)

ClickHouse (метаданные профилей) + PostgreSQL (метаданные бинарей) + S3-совместимое (сырые профили)

Self-hosted

есть

есть

есть

есть

есть

Два профилировщика с eBPF-сбором: Pixie — open-source eBPF-автоинструментация для Kubernetes (метрики, запросы и CPU-профили без изменений в подах); Perforator от Yandex — production-ready continuous profiling для больших датацентров (десятки тысяч нод), вдохновлённый Google-Wide Profiling, с размоткой стека без frame pointers и sPGO-профилями для PGO-сборки.

Два способа профилирования: eBPF и user-space

Coroot собирает профили двумя дополняющими способами: eBPF покрывает CPU всех процессов на ноде без изменений в коде, а языковые профилировщики добирают память и блокировки конкретных рантаймов. Оба механизма живут внутри агентов Coroot и обращаются к чужому процессу снаружи:

  • Go heap — coroot-node-agent читает runtime.MemProfile из памяти процесса (/proc/<pid>/mem), в приложение ничего не подключается

  • Go pprof — coroot-cluster-agent скрейпит стандартный /debug/pprof (CPU/blocking/mutex), который нужно лишь экспортировать и пометить аннотациями

  • Java — coroot-node-agent находит HotSpot JVM и динамически подгружает libasync-profiler.so через JVM Attach API (CPU/alloc/lock)

  • Python — eBPF-инструментирование резолвит Python-фреймы через Pyroscope eBPF-профайлер, включённый в coroot-node-agent

Предварительные требования

Для развёртывания демо-окружения понадобятся:

  • установленный и настроенный Kubernetes-кластер.

  • kubectl и Helm >= 3;

Часть 1. Разворачиваем Coroot в Kubernetes

Архитектура

Coroot в кластере состоит из нескольких компонентов, которые разворачивает coroot-operator:

  • coroot — сам сервер: UI, API, инспекции

  • coroot-node-agent — DaemonSet на каждой ноде: eBPF CPU-профилировщик (плюс Go heap-профайлер, Python-инструментирование и Java через async-profiler), метрики, логи, трейсы

  • coroot-cluster-agent — Deployment: кластерная телеметрия + pprof-скрейп Go-приложений

  • Prometheus — хранилище метрик

  • ClickHouse — хранилище логов, трейсов и профилей (+ clickhouse-keeper для координации)

Шаг 1. Установка Coroot в кластер

Coroot ставится вручную через Helm. Сначала создаём namespace и Secret с паролем администратора:

kubectl create namespace coroot

kubectl -n coroot create secret generic coroot-admin-secret \
  --from-literal=admin-password=<пароль-админа>

Затем создаём coroot-values.yaml.

Файл coroot-values.yaml:

metricsRefreshInterval: "30s"

# Retention: все данные Coroot хранятся не дольше 1 часа
cacheTTL: "1h"      # TTL метрического кэша Coroot
tracesTTL: "1h"     # TTL таблиц трейсов в ClickHouse
logsTTL: "1h"       # TTL таблиц логов в ClickHouse
profilesTTL: "1h"   # TTL таблиц профилей в ClickHouse

authBootstrapAdminPasswordSecret:
  name: coroot-admin-secret
  key: admin-password

ingress:
  className: traefik
  host: coroot_url
  path: /

clickhouse:
  shards: 1
  replicas: 1
  keeper:
    replicas: 1
  storage:
    size: "20Gi"

prometheus:
  retention: "1h"
  storage:
    size: "10Gi"

storage:
  size: "10Gi"

# Java-профилирование: node-agent динамически подгружает async-profiler в HotSpot JVM
nodeAgent:
  env:
    - name: ENABLE_JAVA_ASYNC_PROFILER
      value: "true"

Устанавливаем оператор и сам Coroot:

# Оператор Coroot (управляет Coroot CR, node-agent, cluster-agent, Prometheus, ClickHouse)
helm install coroot-operator oci://ghcr.io/coroot/charts/coroot-operator \
  --version 0.9.10 -n coroot

# Coroot CE: чарт рендерит Coroot CR (spec — из coroot-values.yaml)
helm install coroot oci://ghcr.io/coroot/charts/coroot-ce \
  --version 0.3.3 -n coroot -f coroot-values.yaml

Шаг 2. Coroot CR и retention 1 час

Helm-чарт coroot-ce рендерит Custom Resource Coroot, которым управляет оператор. Что здесь важно:

  • Retention ограничен 1 часом в трёх местах: TTL таблиц ClickHouse (logsTTL/tracesTTL/profilesTTL), метрический кэш (cacheTTL) и retention встроенного Prometheus (prometheus.retention: "1h").

  • Нюанс по Prometheus: данные хранятся двухчасовыми блоками, поэтому при prometheus.retention: "1h" фактический горизонт метрик лежит в диапазоне ~2–4 часа.

Шаг 3. Проверяем

В UI входим с логином admin и паролем администратора, заданным на Шаге 1 (admin-password). Оператор уже сконфигурировал Prometheus и ClickHouse и создал проект default, поэтому ничего настраивать не нужно — сразу переходим к приложениям.

Страница Applications
Страница Applications

Страница Applications в целом понятна, но отметим пару нюансов.

shortage в колонке CPU (подчёркнуто красным) — не «процент загрузки», а нехватка процессорного времени: сколько времени процессы ждали CPU, но не получали его. Причины — троттлинг по лимиту CPU либо конкуренция с другими контейнерами на ноде.

Latency против Net — это разные вещи. Latency — время ответа самого приложения на запросы клиентов (сколько ждут вызывающие стороны). Net — сетевой round-trip time на TCP-уровне между приложением и сервисами, от которых оно зависит: время обработки приложением в него не входит, измеряется только сетевая компонента. Поэтому demo-golang имеет Latency 5ms, но Net <0.1ms — приложение быстрое, сеть не задерживает.

Incidents

Incidents
Incidents

В Coroot управление инцидентами (Incidents) построено вокруг концепции SLO-based alerting (оповещений на основе целей уровня обслуживания). Вместо сотен разрозненных алертов на каждое событие процессора Coroot создаёт единый инцидент, только когда приложение действительно «страдает» с точки зрения пользователя.

Alerts

Alerts
Alerts

Встроенная система алертинга на базе преднастроенных алертов автоматически выявляет проблемы в приложениях и инфраструктуре Kubernetes и показывает первопричины сбоев.

Для отправки алертов наружу необходимо зайти Project Settings → Integrations: Slack, Microsoft Teams, PagerDuty, Opsgenie, webhook. Маршрутизация по категориям приложений и типам событий: Incidents, Deployments, Alerts. Источники алертов: инспекции, новые паттерны в логах, Kubernetes-события, кастомный PromQL.

Service Map

Service Map
Service Map

Service Map — автоматическая визуализация связей между компонентами, без ручной настройки и без изменений кода.

Шаг 4. OpenTelemetry Collector

Скорее всего, у вас уже установлен OpenTelemetry Collector, поэтому конфигурируем отправку трейсов через него — он принимает трейсы от всех четырёх приложений по OTLP/HTTP (порт 4318), батчит их и пересылает в Coroot. Конфигурация — в otel-collector-values.yaml в корне репозитория (используется config чарта open-telemetry/opentelemetry-collector, который сливается с дефолтным конфигом: ненужные дефолтные ресиверы jaeger/zipkin/prometheus и pipelines logs/metrics явно обнулены через null, остаётся только HTTP-ресивер трейсов).

Файл otel-collector-values.yaml:

mode: deployment

# Короткое и предсказуемое имя ресурсов (иначе будет <release>-opentelemetry-collector).
fullnameOverride: otel-collector

image:
  repository: otel/opentelemetry-collector-contrib

config:
  receivers:
    otlp:
      protocols:
        http:
          endpoint: 0.0.0.0:4318

  exporters:
    otlp_http/coroot:
      endpoint: "http://coroot-coroot.coroot:8080"

  service:
    pipelines:
      # Дефолтные pipelines logs/metrics не нужны — глушим.
      logs: null
      metrics: null
      # traces сливается с дефолтным, поэтому список receivers переопределяем
      # целиком (без jaeger/zipkin) и меняем exporters на coroot.
      traces:
        receivers: [otlp]
        processors: [batch]
        exporters: [otlp_http/coroot]

Устанавливаем коллектор:

helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
helm install otel-collector open-telemetry/opentelemetry-collector \
  --version 0.173.1 -n otel --create-namespace -f otel-collector-values.yaml

Коллектор слушает OTLP/HTTP на 4318 в namespace otel. Приложения обращаются к нему по адресу http://otel-collector.otel:4318/v1/traces, а сам коллектор пересылает батчи в Coroot на внутренний сервис coroot-coroot.coroot:8080.

Обзор Tracing с установленными 4 demo приложениями. В разделе трассировок пять вкладок:

OVERVIEW
OVERVIEW

OVERVIEW — HeatMap распределения запросов во времени со статусами и длительностью. По тепловой карте сразу видны аномалии; выделив область, можно посмотреть входящие в неё трейсы.

TRACES
TRACES

TRACES — просмотр отдельных трейсов по выделенной области: путь запроса по сервисам, спаны и их длительности.

ERROR CAUSES — автоматически анализирует все затронутые запросы в выделенной области и находит спаны с ошибками — одного ли типа все ошибки или происходят разные сбои одновременно.

LATENCY EXPLORER
LATENCY EXPLORER

LATENCY EXPLORER — сравнивает длительность операций с остальными запросами; задержка визуализируется как latency-флеймграф, замедлившиеся операции подсвечиваются красным.

COMPARE ATTRIBUTES
COMPARE ATTRIBUTES

COMPARE ATTRIBUTES — сравнение атрибутов трасс внутри выделенной области с остальными запросами, полезно при разном поведении для конкретных клиентов, браузеров или feature flag.

Часть 2. Четыре «сломанных» приложения

Чтобы продемонстрировать профилирование, задеплоим четыре приложения с намеренно внесёнными проблемами. Исходники — в каталоге apps, деплой — Helm-чартом chart. Вместе с приложениями чарт поднимает генераторы нагрузки — по одному Kubernetes Job на каждое включённое приложение.

Обзор приложения и SLO

При открытии приложения Coroot показывает SLO (Service Level Objectives) — целевые показатели надёжности сервиса. По умолчанию отслеживаются два SLO: Availability (99% запросов должны быть обслужены без ошибок) и Latency (99% запросов должны обслуживаться быстрее 500 мс). Coroot считает SLI по eBPF-метрикам на уровне приложения и показывает фактическое соблюдение объектива, latency в виде гистограммы с фиксированными бакетами (5 мс — 10 с) и остаток error budget.

Шаг 1. Nuxt (Node.js)

Приложение на Nuxt 3 с единственным API-эндпоинтом /api/cpu, который считает наивный Фибоначчи (fib(35) — ~30 млн рекурсивных вызовов). Экспоненциальная сложность мгновенно видна в CPU-профиле.

Ключевой момент — символизация JS-фреймов. eBPF-профилировщик снимает нативные стектрейсы, но без perf-map названия JS-функций не резолвятся. Node.js умеет генерировать perf-map сам, если запустить его с флагами.

Файл chart/values.yaml (фрагмент):

nuxt:
  env:
    NODE_OPTIONS: "--perf-basic-prof-only-functions --interpreted-frames-native-stack"

Файл apps/nuxt/Dockerfile (фрагмент):

ENV NODE_OPTIONS="--perf-basic-prof-only-functions --interpreted-frames-native-stack"

С этими флагами во флеймграфе будут реальные имена функций fib/fib, а не анонимные адреса.

Минимальная рабочая версия — 18.19+ / 20.10+ / 21.1+.

Трейсы подключаются через OpenTelemetry: Nitro-плагин запускает NodeSDK с HttpInstrumentation, который на каждый запрос создаёт server-span, а в обработчике добавляется вложенный span fib.

Файл apps/nuxt/server/plugins/otel.ts (фрагмент):

export default defineNitroPlugin(() => {
  const sdk = new NodeSDK({
    instrumentations: [new HttpInstrumentation()],
  })
  sdk.start()
})

Экспорт — в OpenTelemetry Collector через OTLP.

Файл chart/values.yaml (фрагмент):

nuxt:
  env:
    OTEL_SERVICE_NAME: "demo-nuxt"
    OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "http://otel-collector.otel:4318/v1/traces"
    OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: "http/protobuf"

Что видно в Coroot

Обзор и SLO приложения demo-nuxt
Обзор и SLO приложения demo-nuxt

На overview-slo видно соблюдение двух SLO (Availability и Latency), остаток error budget и гистограмму latency с фиксированными бакетами.

CPU shortage у demo-nuxt
CPU shortage у demo-nuxt

Инспекция CPU показывает две проверки: Node CPU utilization — ok (загрузка ноды ниже порога 80%), и Container CPU utilization — high CPU utilization of 1 container: контейнер demo-nuxt превышает 80% своего CPU-лимита.

Флеймграф CPU demo-nuxt
Флеймграф CPU demo-nuxt

Флеймграф показывает, что почти всё CPU-время (~100%) уходит в event loop Node.js: стек идёт через uv__io_poll → uv__read → uv__stream_io, затем через microtask-очередь попадает в Nitro/Nuxt. Далее профиль делится на двух потребителей CPU: ~33% — обработчик /api/cpu (api/cpu.get.mjs), где почти все сэмплы приходятся на рекурсивный fib (api/cpu.get.mjs:924 — наивные числа Фибоначчи); остальное — обвязка Nitro (nitro.mjs:1645) и накладные расходы V8/libuv. Узкое место — CPU-bound рекурсия fib, а не HTTP-цикл.

Tracing demo-nuxt
Tracing demo-nuxt

На вкладке Tracing у demo-nuxt — server-span на каждый запрос и вложенный span на вычисление. Из аномалии открывается флеймграф, из медленного span'а — логи и профили.

Шаг 2. Python

Python-приложение на стандартном http.server с эндпоинтом /cpu: наивный fib(30) плюс busy-loop с math.sqrt. eBPF-профилировщик Coroot снимает CPU-профиль Python-процесса без каких-либо агентов и изменений кода, а Pyroscope eBPF-профайлер резолвит Python-фреймы, так что во флеймграфе виден именно naive_fib.

Трейсы — автоинструментация OpenTelemetry: приложение запускается через opentelemetry-instrument, который сам инструментирует http.server и экспортирует server-span'ы в OpenTelemetry Collector через OTLP. В app.py обработчик дополнительно оборачивается во вложенный span через trace.get_tracer(...).

Файл apps/python/app.py (фрагмент):

from opentelemetry import trace

tracer = trace.get_tracer("demo-python")

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        with tracer.start_as_current_span(self.path):
            ...

Файл apps/python/Dockerfile (фрагмент):

CMD ["opentelemetry-instrument", "--traces_exporter", "otlp_proto_http", "--metrics_exporter", "none", "--logs_exporter", "none", "python", "app.py"]

Файл chart/values.yaml — тот же фрагмент, что для Nuxt, только OTEL_SERVICE_NAME: "demo-python".

Что видно в Coroot

Обзор и SLO приложения demo-python
Обзор и SLO приложения demo-python

На overview-slo Availability SLO в статусе OK (доля успешных запросов ≥ 99%), а Latency SLO нарушен: error budget burn rate 100.0x в течение 1 часа, так как доля запросов быстрее 500 мс ниже 99% — вызов /cpu с наивным fib(30) и busy-loop выходит за предел в 500 мс.

CPU shortage у demo-python
CPU shortage у demo-python

В инспекции CPU те же две проверки: Node CPU utilization — ok (загрузка ноды ниже порога 80%), и Container CPU utilization — high CPU utilization of 1 container: контейнер demo-python превышает 80% своего CPU-лимита.

Флеймграф CPU demo-python
Флеймграф CPU demo-python

Флеймграф Profiling за выбранный интервал покажет CPU в naive_fib и в busy-loop с math.sqrt; режим Comparison подсветит красным функции, которые стали есть больше CPU относительно прошлого интервала.

Tracing demo-python
Tracing demo-python

На вкладке Tracing у demo-python — server-span от автоинструментации http.server и вложенный span на вычисление. По аномалии можно перейти во флеймграф, по медленному span'у — в логи и профили.

Шаг 3. Golang

Go-приложение с тремя проблемами сразу:

  • утечка памяти — фоновый цикл каждую секунду добавляет 1 MiB в слайс, который никогда не освобождается (лимит пода 2 GiB). Генератор нагрузки ещё дергает /leak, поэтому растут и горутины — под упирается в память и перезапускается

  • утечка горутин — эндпоинт /leak запускает горутину, которая блокируется навсегда

  • CPU-нагрузка — эндпоинт /cpu с бесполезным циклом на 5 млн итераций

Для Go Coroot собирает профили по трём каналам, которые частично пересекаются:

Список типов профилей вкладки Profiling для demo-golang
Список типов профилей вкладки Profiling для demo-golang

Тип профиля

eBPF (node-agent)

Go heap-профайлер (node-agent, /proc/<pid>/mem)

pprof-скрейп (cluster-agent, /debug/pprof)

CPU

есть, все процессы, любой язык

нет

есть, /debug/pprof/profile

heap / Memory

нет

есть, Go Memory (alloc/inuse × space/objects)

есть, Memory (те же данные, дубль)

blocking

нет

нет

есть, /debug/pprof/block

mutex

нет

нет

есть, /debug/pprof/mutex

goroutine

нет

нет

есть, /debug/pprof/goroutine

Выбор профилей зависит от задачи:

Нужны профили

Что делать

Изменения в коде

heap / CPU

ничего: Go Memory собирает node-agent без дополнительной настройки, CPU — eBPF

не нужны

blocking / mutex / goroutine

eBPF их не даёт — нужен pprof-скрейп (/debug/pprof), а --go-heap-profiler=disabled уберёт дублирующие Go Memory

import _ "net/http/pprof" + аннотации пода

Для heap и CPU Go-приложение не требует ни правки кода, ни profile-scrape — оба канала работают извне (eBPF + чтение /proc/<pid>/mem). pprof-скрейп нужен только ради профилей, которых нет в eBPF: blocking, mutex и goroutine.

Файл chart/values.yaml (фрагмент):

golang:
  podAnnotations:
    coroot.com/profile-scrape: "true"
    coroot.com/profile-port: "8080"

Сам код подключает net/http/pprof одной строкой:

import _ "net/http/pprof"

Трейсы — ручное инструментирование OpenTelemetry (автоинструментации для Go в Coroot нет): маршрутизатор оборачивается в otelhttp.NewHandler, а OTLP-экспортер читает endpoint из env и шлёт спаны в OpenTelemetry Collector. Каждый входящий запрос на /cpu и /leak становится трейсом в Coroot.

Файл apps/golang/main.go (фрагмент):

exporter, err := otlptrace.New(ctx, otlptracehttp.NewClient())
// ...
handler := otelhttp.NewHandler(http.DefaultServeMux, "http-server")

Привязка минимального Go к версии opentelemetry-go:

Версия opentelemetry-go

Минимальный Go

v1.38.0

Go 1.23

v1.42.0

Go 1.24

v1.46.0 (используется в проекте)

Go 1.25

v1.47.0+

Go 1.26

Файл apps/golang/Dockerfile (фрагмент):

FROM golang:1.25-alpine AS build

Файл chart/values.yaml (фрагмент):

golang:
  env:
    OTEL_SERVICE_NAME: "demo-golang"
    OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "http://otel-collector.otel:4318/v1/traces"
    OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: "http/protobuf"

Memory-профиль показывает устойчивый рост alloc_space: куча растёт на ~1 MiB/сек за счёт фонового growLeak. Флеймграф memory-профиля указывает точное место — main.growLeak, где происходит append в leakBuf.

Что видно в Coroot

Обзор и SLO приложения demo-golang
Обзор и SLO приложения demo-golang

На overview-slo видно, как рост памяти и утечка горутин деградируют соблюдение двух SLO (Availability и Latency); вызовы /cpu и /leak уходят за objective 500 мс.

Флеймграф memory-профиля demo-golang
Флеймграф memory-профиля demo-golang

Горутины-утечки видны косвенно: число горутин растёт (/healthz отдаёт runtime.NumGoroutine()), а Coroot связывает это с ростом потребления и деградацией SLO.

Tracing demo-golang
Tracing demo-golang

На вкладке Tracing — server-span на каждый запрос (otelhttp.NewHandler). HeatMap и выделение области — как в Шаге 1. Из CPU-аномалии открывается флеймграф, из медленного span'а — логи и профили.

Шаг 4. Java

Java-профилирование включается флагом ENABLE_JAVA_ASYNC_PROFILER=true в coroot-values.yaml (см. Шаг 1). Java-агент для профилирования не нужен — coroot-node-agent сам находит HotSpot JVM по libjvm.so в /proc/<pid>/maps и подгружает async-profiler через JVM Attach API. (Java-агент в apps/java/Dockerfile — это OpenTelemetry-инструментация для трейсов, к профилированию отношения не имеет.)

Java-приложение на встроенном com.sun.net.httpserver с тремя эндпоинтами:

  • /cpu — наивный fib(35) плюс цикл на 5 млн итераций

  • /alloc — фоновая аллокация массивов (видна в Memory-профиле как alloc_space/alloc_objects)

  • /lock — два потока намеренно конкурируют за один монитор (synchronized + sleep) и создают Lock-профиль

Профилирование работает и без JVM-флагов — async-profiler подгружается в JVM динамически (через JVM Attach API). Но без них флеймграф деградирует: dynamical attach не гарантирует, что у уже скомпилированного JIT-кода есть подходящие символы и frame pointer, поэтому горячие сэмплы не резолвятся в имена методов и падают в [unknown] (а naiveFib может вообще исчезнуть как отдельный фрейм, будучи заинлайненным в cpuBurn). Флаги ниже минимизируют потери и оставляют профиль «до точной строки кода»:

Файл apps/java/Dockerfile (фрагмент):

ENTRYPOINT ["java", \
  "-javaagent:/app/opentelemetry-javaagent.jar", \
  "-XX:+UnlockDiagnosticVMOptions", \
  "-XX:+DebugNonSafepoints", \
  "-XX:+PreserveFramePointer", \
  "-XX:TieredStopAtLevel=1", \
  "-XX:CompileCommand=dontinline,DemoJava.naiveFib", \
  "-cp", "/app", "DemoJava"]
  • -XX:+UnlockDiagnosticVMOptions разблокирует диагностические опции — без него JVM не примет -XX:+DebugNonSafepoints.

  • -XX:+DebugNonSafepoints заставляет JIT сохранять debug-информацию и в несейфпоинтах — без этого часть JIT-фреймов не резолвится в [unknown].

  • -XX:+PreserveFramePointer сохраняет RBP для раскрутки стека — без этого async-profiler (и eBPF-профилировщик) не могут пройтись по нативному стеку, и сэмплы уходят в [unknown].

  • -XX:TieredStopAtLevel=1 оставляет только C1-компиляцию: простой машинный код легче маппится обратно в методы, чем агрессивно оптимизированный C2.

  • -XX:CompileCommand=dontinline,DemoJava.naiveFib запрещает инлайнить naiveFib — иначе метода не будет видно отдельным фреймом.

Для demo-java Coroot показывает сразу несколько типов профилей: async-profiler отдаёт CPU, память и блокировки, а приставка «Java» отличает их от CPU-профиля eBPF-профилировщика. На вкладке Profiling в капле выбора типа профиля шесть позиций:

Список типов профилей вкладки Profiling для demo-java
Список типов профилей вкладки Profiling для demo-java

Тип профиля

eBPF (node-agent)

async-profiler (node-agent, JVM Attach API)

CPU (eBPF)

есть, все процессы, любой язык

нет

Java CPU

нет

есть, событие cpu

Java Lock (contentions)

нет

есть, событие lock (число контеншенов монитора)

Java Lock (delay)

нет

есть, событие lock (время ожидания монитора)

Java Memory (alloc_objects)

нет

есть, событие alloc (число объектов)

Java Memory (alloc_space)

нет

есть, событие alloc (байты)

Выбор профилей зависит от задачи:

Нужны профили

Что делать

Изменения в коде

CPU (eBPF)

ничего: eBPF-профилировщик снимает CPU всех процессов без дополнительной настройки

не нужны

Java CPU / Memory / Lock

включить ENABLE_JAVA_ASYNC_PROFILER=true в coroot-values.yaml — node-agent сам найдёт JVM и подгрузит async-profiler

не нужны; JVM-флаги (PreserveFramePointer и др.) опционально улучшают символизацию

Для всех типов профилей Java правка кода не нужна: CPU снимает eBPF-профилировщик, а всё с префиксом «Java» — async-profiler, который node-agent динамически подгружает в HotSpot JVM. JVM-флаги из предыдущего абзаца не обязательны — они лишь минимизируют [unknown] и оставляют профиль «до точной строки кода».

Трейсы — автоматическая инструментация через OpenTelemetry Java-агент: jar скачивается в образе и подключается флагом -javaagent, так что менять код не нужно — спаны HTTP-запросов генерируются автоматически и уходят в OpenTelemetry Collector через OTLP.

Файл apps/java/Dockerfile (фрагмент):

RUN wget -q -O /opentelemetry-javaagent.jar \
      https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar

Файл chart/values.yaml — тот же фрагмент, что для Nuxt, только OTEL_SERVICE_NAME: "demo-java".

Что видно в Coroot

Обзор и SLO приложения demo-java
Обзор и SLO приложения demo-java

На overview-slo Availability SLO в статусе OK (доля успешных запросов ≥ 99%), а Latency SLO нарушен: error budget burn rate 65.9x в течение 1 часа, так как доля запросов быстрее 500 мс ниже 99% — вызов /cpu с наивным fib(35) уходит за objective 500 мс, а /alloc даёт рост потребления памяти.

CPU shortage у demo-java
CPU shortage у demo-java

Инспекция CPU фиксирует то же самое: Node CPU utilization — ok (загрузка ноды ниже порога 80%), и Container CPU utilization — high CPU utilization of 1 container: контейнер demo-java превышает 80% своего CPU-лимита.

JVM-профиль demo-java
JVM-профиль demo-java

Инспекция JVM выводит две проверки: JVM availability — ok (число недоступных инстансов JVM не превышает 0), и JVM safepoints — ok (время остановки приложения на safepoint-операциях не превышает 50 мс).

Tracing demo-java
Tracing demo-java

На вкладке Tracing — server-span на каждый запрос (OTel Java-агент, без изменений кода). Аномалия CPU ведёт во флеймграф, медленный span — в логи и профили.

Флеймграф CPU (eBPF) demo-java
Флеймграф CPU (eBPF) demo-java

CPU (eBPF) — нативный CPU-профиль с eBPF-профайлера поверх всех процессов ноды: почти всё время уходит в naiveFib (рекурсия с экспоненциальной сложностью), как и у Python/Node.js.

Флеймграф Java Lock (contentions) demo-java
Флеймграф Java Lock (contentions) demo-java

Java Lock (contentions) — число контеншенов монитора: два потока конкурируют за один synchronized-блок на эндпоинте /lock.

Флеймграф Java Memory (alloc_objects) demo-java
Флеймграф Java Memory (alloc_objects) demo-java

Java Memory (alloc_objects) — рост числа аллоцированных объектов по стеку аллокаций в DemoJava.allocate (эндпоинт /alloc). Парный профиль Java Memory (alloc_space) показывает тот же стек в байтах.

На вкладке Profiling async-profiler отдаёт те же типы профилей: CPU (почти всё время в naiveFib), Memory (рост alloc_space/alloc_objects по стеку аллокаций в DemoJava.allocate) и Lock (время ожидания монитора и число контеншенов на synchronized-блоке).

Масштабирование и обновление

Реплики и ClickHouse

Для продакшена имеет смысл clickhouse.shards/replicas: 2 и keeper.replicas: 3 (по умолчанию), а также несколько реплик Coroot (replicas: 2), для чего потребуется вынести конфигурацию из SQLite в PostgreSQL (postgres.* в CR).

Заключение

Coroot отвечает на вопрос «почему медленно» — на него классический мониторинг не отвечает. Непрерывное eBPF-профилирование снимает CPU-профили без изменений кода, языковые профилировщики добавляют память и блокировки, а предустановленные инспекции находят типовые проблемы. Всё это — с метриками, логами и трейсами в одном UI.

Ключевые особенности:

  • Без инструментирования — eBPF снимает CPU-профили всех процессов без изменений в коде

  • Флеймграф до строки кода — CPU и память по клику, сравнение с базовой линией

  • Предустановленные инспекции — находят ~80% типовых проблем автоматически

  • Все сигналы в одном месте — метрики, логи, трейсы и профили связаны между собой

  • Один Helm-чарт — coroot-operator управляет всем стеком

Полезные ссылки: