Вы сказали:
Что такое observability в контексте высоконагруженные сложных комплексных систем?
В контексте высоконагруженных и сложных комплексных систем (особенно с микросервисами, распределёнными транзакциями и интеграциями) observability — это не просто “собирать логи” или “ставить мониторинг”.
Это способность понять, что реально происходит внутри системы, не останавливая её и не лезя в код, — причём быстро и достаточно глубоко, чтобы находить и устранять проблемы до того, как они станут критичными.
Observability — это свойство системы, которое позволяет на основе её внешних сигналов (метрики, логи, трассировки и события) реконструировать внутреннее состояние и причины поведения.
Сильная observability означает, что при инциденте:
Не нужно перебирать гипотезы “вслепую”.
Можно точно ответить на вопросы “что сломалось, где и почему”.
Понимание проблемы занимает минуты, а не часы или дни.
Численные показатели состояния системы, которые можно агрегировать, фильтровать и анализировать:
Инфраструктурные: CPU, RAM, I/O, latency сети.
Прикладные: кол-во успешных и неуспешных запросов, время отклика, количество активных пользователей.
Бизнесовые: количество заказов, средний чек, время прохождения бизнес-операции.
Применение: быстро понять “ухудшилось ли что-то и насколько”.
Текстовые записи событий — в идеале структурированные (JSON) и с единым форматом:
События приложений (ошибки, предупреждения, ключевые действия).
Системные события (перезапуски сервисов, деплои).
Взаимодействие между сервисами.
Применение: разбор деталей “что именно произошло и в каком контексте”.
Прослеживание прохождения запроса через цепочку сервисов:
Показывает какие сервисы участвовали в обработке запроса.
Где были задержки, где возникли ошибки.
Как отдельные вызовы связаны в одну транзакцию.
Применение: быстро локализовать “узкое место” в длинной цепочке интеграций.
Информация о внешних факторах:
Деплои, релизы, конфигурационные изменения.
Изменения в инфраструктуре.
Пики нагрузки из-за маркетинговых акций.
Применение: связывает проблему с возможной причиной “а не совпало ли это с выкладкой новой версии?”.
В распределённых системах (особенно при Java Spring + OpenShift + микрофронты):
Межсервисные зависимости создают “эффект домино”.
Инциденты часто проявляются не там, где находится причина.
Проблемы могут быть междусервисными (например, деградация кэша → задержка в API → таймаут в другом сервисе → сбой бизнес-функции).
Без развитой observability команды поддержки:
Тратят много времени на генерацию и проверку гипотез.
Могут устранять симптомы, не устраняя корень проблемы.
Плохо видят степень влияния проблемы на пользователей и бизнес.
Сценарий: Пользователь жалуется, что заказ оформляется “очень долго”.
🔴 Без observability:
Смотрим логи сервиса заказов — ничего подозрительного.
Смотрим метрики API — вроде норм.
Тратим часы на ручной поиск по разным системам.
🟢 С observability:
В Kibana видим всплеск order.processing_time > 10s
.
В Jaeger видим, что 80% времени тратится на запрос в сервис платежей.
В Grafana видим, что задержка в сервисе платежей началась сразу после деплоя новой версии.
Решение → откат версии → восстановление работы за 15 минут.
Широкий охват — собираем данные со всех слоёв (инфраструктура, приложения, бизнес).
Корреляция данных — метрики, логи и трассировки должны быть связаны (через trace-id, request-id).
Автоматизация реакции — алерты и дашборды.
Ориентация на вопросы, а не только на данные:
Что сломалось?
Где?
Почему?
Насколько критично?
Shift-left — закладывать наблюдаемость ещё на этапе разработки.
Если хочешь, я могу сделать схему observability-архитектуры именно для твоих Java Spring highload сервисов в OpenShift с учётом интеграций и микрофронтов — она поможет сразу понять, какие инструменты и точки интеграции нужны.
Хочешь, я её подготовлю?
Ниже — практическая, пошаговая схема: что именно ставим, где, какие сигналы собираем, как их коррелируем по всей цепочке вызовов, какие SLO/алерты вводим, как не утонуть в объёмах данных и как вывести сопровождение на уровень «минуты до диагноза».
Бизнес/операционные цели
MTTA/MTTR ↓ в 2–3 раза.
Инциденты: обнаружение до жалоб пользователей ≥ 80%.
Диагностика сквозных сценариев (длинные цепочки) ≤ 10 минут.
Управление релизами: чёткая связь «изменение → эффект» (change events).
Технические цели
Сквозная корреляция: trace_id / span_id + log_correlation + exemplars в метриках.
Наборы «золотых сигналов» (latency, traffic, errors, saturation) на уровне сервисов и бизнес-сценариев.
Единый стандарт логов (JSON), унифицированные поля.
Сигналы
Метрики: Prometheus (встроенный в OpenShift) + Thanos (длинное хранение), Grafana для дашбордов.
Логи: Loki (или Elasticsearch, но Loki дешевле по хранению) + графана/коли́брация.
Трейсы: OpenTelemetry → OpenTelemetry Collector → Jaeger (или Tempo). Tail-based sampling на коллекторе.
События/Context: события релизов, конфигураций, фича-флаги, инциденты — в единый event-stream (например, Kafka topic platform.events
) и в Grafana/Jaeger annotations.
Инструменты OpenShift
OpenTelemetry Operator (автоинструментирование Java).
Jaeger Operator (трейсинг).
OpenShift Monitoring (Prometheus/Alertmanager + Thanos).
OpenShift Logging / Loki (или собственный Loki стек).
(Опц.) Service Mesh (OpenShift Service Mesh/ISTIO) — для автоматической пропагации заголовков и сетевых метрик.
Frontend
RUM (OpenTelemetry JS + web-vitals) в микрофронтах: CLS/LCP/FID/INP, ошибки JS, фронтовые спаны → в тот же OTLP.
W3C Trace Context: traceparent
, tracestate
— в каждом межсервисном вызове.
Spring Boot:
OpenTelemetry Java agent (без изменения кода) или SDK + auto-config.
Логи — JSON со вставкой trace_id
, span_id
и service.name
.
Exemplars в метриках (Micrometer ↔ OpenTelemetry), чтобы клик из графика latency в Grafana сразу открывал конкретный трейс.
RUM → Backend: прокидываем traceparent
из браузера в первый backend; отдаём обратно server-timing
для связки.
xml<!-- build.gradle -->
implementation("net.logstash.logback:logstash-logback-encoder:7.4")
<!-- src/main/resources/logback-spring.xml -->
<configuration>
<appender name="JSON" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/app/app.log</file>
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp/>
<pattern>
<pattern>
{
"level":"%level",
"logger":"%logger",
"thread":"%thread",
"message":"%message",
"service.name":"${SPRING_APPLICATION_NAME:-unknown}",
"trace_id":"%X{trace_id:-}",
"span_id":"%X{span_id:-}",
"env":"${ENVIRONMENT:-prod}",
"k8s.namespace":"${K8S_NAMESPACE:-}",
"k8s.pod":"${HOSTNAME:-}"
}
</pattern>
</pattern>
<stackTrace/>
</providers>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/var/log/app/app.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
</appender>
<root level="INFO"><appender-ref ref="JSON"/></root>
</configuration>
В коллекторе или promtail добавьте парсер JSON и отправку в Loki. Обязательно — фильтры PII (маскирование).
kotlin// build.gradle
implementation("io.micrometer:micrometer-registry-prometheus")
implementation("org.springframework.boot:spring-boot-starter-actuator")
// application.yaml
management:
endpoints.web.exposure.include: prometheus,health,info
metrics.tags:
env: ${ENVIRONMENT:prod}
service: ${SPRING_APPLICATION_NAME}
В OpenShift создайте ServiceMonitor
, чтобы Prometheus начал скрейпить /actuator/prometheus
.
Образ контейнера:
nginxENV JAVA_TOOL_OPTIONS="-javaagent:/otel/opentelemetry-javaagent.jar"
ENV OTEL_SERVICE_NAME=${SPRING_APPLICATION_NAME}
ENV OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
ENV OTEL_LOGS_EXPORTER=none
ENV OTEL_RESOURCE_ATTRIBUTES=deployment.environment=prod
Пропагация заголовков в gateway/mesh не должна их «стрипать».
yamlreceivers:
otlp: { protocols: { grpc: {}, http: {} } }
processors:
batch: {}
tail_sampling:
decision_wait: 2s
num_traces: 20000
policies:
- name: errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow
type: latency
latency: { threshold_ms: 1000 }
- name: keypaths
type: string_attribute
string_attribute:
key: "http.target"
values: ["/api/orders", "/api/payments"]
exporters:
otlp/jaeger: { endpoint: "jaeger-collector:4317", tls: { insecure: true } }
logging: {}
prometheusremotewrite:
endpoint: "http://thanos-receive:19291/api/v1/receive"
service:
pipelines:
traces:
receivers: [otlp]
processors: [tail_sampling, batch]
exporters: [otlp/jaeger]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [prometheusremotewrite]
Сэмплируем все ошибки и медленные спаны; обычный трафик — выборочно. Так контролируем объёмы, не теряя важного.
Availability (5xx rate), Latency P95/P99, Saturation (thread pools, JDBC pool, GC pause), Traffic (RPS).
JVM-метрики: heap/GC, JFR события (опц.), connection pool metrics.
Примеры:
Создание заказа: SLO 99% ≤ 3s (end-to-end), ошибка ≤ 0.5%.
Платёж: SLO 99% ≤ 2s, отказ ≤ 0.3%.
Поиск/каталог: SLO 99% ≤ 500ms.
Как считать сквозной SLI
Трейсинг закладывает span на каждую ключевую фазу (A→B→C).
На коллекторе/в беке считаем бизнес-метрику order_e2e_latency_seconds
+ exemplar с trace_id.
Сервисные (PromQL)
rate(http_server_requests_seconds_count{status!~"2.."}[5m]) / rate(http_server_requests_seconds_count[5m]) > 0.02
→ ErrorRateHigh.
histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket[5m])) by (le, service)) > SLO_P99
→ LatencyP99High.
JVM: sum(jvm_gc_pause_seconds_sum[5m]) > 0.5
→ GCStorm.
Пулы: max(jdbc_connections_active) / max(jdbc_connections_max) > 0.9
→ DBPoolSaturated.
Бизнес-алерты
rate(orders_failed_total[5m]) / rate(orders_total[5m]) > 0.01
«Нет заказов 10 минут при трафике > 100 RPS» (аномалия).
Сценарные (Tracing)
Tail-sampling даёт все ошибочные/медленные трейсы → правила в Alertmanager с линками прямо в Jaeger.
Шумогашение
Правила ≥ 5 минут, группировка по сервису/кластеру, multi-window multi-burn rate для SLO.
Платформа (общая площадка микрофронтов)
Ингресс/шлюз: RPS, 4xx/5xx, P95/P99.
Страница «здоровья релизов»: релизные события + изменение ошибок/латенси.
Сервисный шаблон (на каждый бэкенд)
Golden signals + JVM + DB + кэш (hit ratio) + внешние зависимости.
Линки на логи/трейсы по trace_id
(экземплары).
Сквозные сценарии
«Воронка» стадий (order submit → payment auth → fulfillment).
P95/P99 end-to-end, отказоустойчивость по шагам, топ проблемных спанов.
Frontend RUM
LCP/INP/CLS, JS errors, «что изменилось после релиза микрофронта».
Каждый деплой (бэкенд/микрофронт/платформа) публикует событие:
json{
"type":"release",
"service":"orders-ui",
"version":"2025.08.11-14.2",
"commit":"abc1234",
"env":"prod",
"time":"2025-08-11T15:04:05Z"
}
Эти события автоматически аннотируют дашборды (Grafana) и фильтруют трейсы по release.version
.
В алертах добавляем поле changed_within(30m)
→ помогает быстро связать деградацию с релизом.
Метрики: детально 15 дней (Prometheus), агрегаты через Thanos: 180–365 дней.
Трейсы: tail-sample (ошибки/медленные) 7–14 дней; общий поток — 1–5% head-sample 3–7 дней.
Логи: 3–7 дней «горячие» (Loki SSD), 30–90 дней — объектное хранилище (S3-совместимое) с компрессией.
Кардинальность: лимитируем лейблы (user_id
— нельзя, tenant_id
— можно; order_id
— только как лог-поле, не в метриках).
PII/секреты: маскирование в логах/спанах (pre-processors в promtail/otelcol).
RBAC: разделение по namespace/командам; «в проде смотреть можно, править — нельзя».
Логи доступа к данным (кто что смотрел) — для аудита.
Включить OpenTelemetry Java agent на всех 6 сервисах + OTEL Collector.
Привести логи к JSON с trace_id/span_id и отправить в Loki.
Подключить Micrometer→Prometheus, ServiceMonitor на каждый сервис.
Общий инцидент-дашборд: Golden signals всех сервисов + релизные аннотации.
Tail-sampling: ошибки/медленные — сразу видим корень по трейсам.
Микрофронт: внедрить web-vitals + ошибочный JS-логгер → OTLP.
Аудит цепочек вызовов, SLI/SLO черновики.
Развёртывание: OTel Operator, OTel Collector, Jaeger, Loki, Grafana datasources.
Автоинструментирование Java, JSON-логи, Prometheus скрейп.
Базовые дашборды, 10–12 ключевых алертов.
Артефакты: стандарт логов, гайд по trace-context, схема маршрутов.
Бизнес-метрики end-to-end + exemplars.
Tail-sampling политики, алерты по SLO (multi-burn).
Включить RUM + связь фронт↔бек по traceparent.
Артефакты: каталог SLI/SLO, сценарные дашборды, runbooks.
Авто-аннотации релизов, «релизный дашборд».
Канареечные проверки: казусы/регрессии по метрикам.
Автогенерация ссылок в инцидентах: «Открыть трейсы», «Показать последние релизы».
Тюнинг сэмплинга/ретеншнов (стоимость).
Тест на инцидент с имитацией сбоя.
Обучение команд сопровождения: 2-часовые практикумы.
Order latency high
Grafana: «Сквозной — Orders E2E» → spike на P99.
Клик по exemplar → Jaeger: топ медленных спанов.
Посмотреть «external dependencies» (DB/Payments API) — какие коды/таймауты?
Логи с trace_id
: причины (ретраи, ошибки бизнес-валидаций, thread starvation).
Сверка с release annotations последних 30 мин.
Эскалация/откат/фича-флаг → пост-морем с сохранёнными трейсами.
ServiceMesh (если есть):
Включить пропагацию и не удалять traceparent
.
Метрики меша (истинные upstream_latency vs downstream_latency).
Платформа микрофронтов:
Единый слой RUM (SDK), который автоматически добавляет traceparent
в все API-вызовы.
Страница «Здоровье площадки»: latency/ошибки по бандлам, время сборки/доставки, размер/TTFB/LCP.
Развёртывания:
Лейблы Kubernetes: app
, version
, commit
, owner
, tier
— попадают в метрики/трейсы/логи.
EventRouter/ArgoCD-события → аннотации в Grafana.
Фича считается «наблюдаемой», если:
Есть трейс сквозного запроса с именованными спанами по шагам.
Есть бизнес-метрика результата/времени (с экземпларом).
Логи структурированы, ошибки имеют код причины (error_code).
Добавлены алерты и дашборд (линк в MR/тикете).
Изменение публикует release event.
ServiceMonitor (пример)
yamlapiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: orders-sm
labels: { team: orders }
spec:
namespaceSelector: { matchNames: ["prod-orders"] }
selector:
matchLabels: { app: orders }
endpoints:
- port: http
path: /actuator/prometheus
interval: 30s
JaegerInstance (коротко)
yamlapiVersion: jaegertracing.io/v1
kind: Jaeger
metadata: { name: jaeger-prod }
spec:
strategy: production
storage:
type: elasticsearch # или tempo/openshift-logging
ingress: { enabled: true }
Promtail (логи в Loki)
yaml- job_name: kubernetes-pods
pipeline_stages:
- json:
expressions:
level: level
message: message
trace_id: trace_id
span_id: span_id
- labels:
level:
trace_id:
span_id:
relabel_configs:
- action: replace
source_labels: [__meta_kubernetes_pod_label_app]
target_label: service
Кардинальность метрик — строгие ревью тегов/лейблов, линтеры схем.
Стоимость хранения — tail-sampling, downsampling, S3-архивы.
Шум алертов — постепенная настройка порогов, SLO-подход, mute-time для релизов.
PII в логах — маскировка/редакторы, обучение команд.
Dev чек-лист
Автоагент OTel подключён.
traceparent
пробрасывается во все исходящие HTTP/gRPC.
Логи JSON с trace_id
, error_code
.
Бизнес-метрика для ключевой операции + exemplar.
Дашборд/алерт добавлены; релиз публикует event.
Ops чек-лист
Prometheus/Thanos, Loki, Jaeger/Tempo — доступны и резервированы.
Tail-sampling политики применены.
RBAC и retention настроены.
Runbooks в Wiki, ссылки вставлены в алерты.
Схема выше даёт сквозную наблюдаемость: от браузера пользователя и микрофронта — через цепочки микросервисов — до внешних интеграций, с чёткой корреляцией «метрики ↔ трейсы ↔ логи ↔ релизы». Это позволит вашим командам перестать перебирать гипотезы вслепую и находить корни проблем за минуты.
Если хочешь, я подготовлю:
шаблон Grafana-дашбордов (JSON),
готовые ServiceMonitor/Promtail/OTelCollector манифесты под ваши namespace’ы,
пример SLO-пакета (PromQL + Alertmanager).
Скажи, какие два сервиса взять первыми — соберу под них «быстрый старт» прямо в конфигурациях.
Сила логов в формате JSON в том, что они превращают лог из “человеческого текста” в структурированные данные, с которыми можно работать как с таблицей в базе данных: фильтровать, агрегировать, строить дашборды, находить корреляции и связывать их с метриками и трейсам.
Текстовый лог выглядит, например, так:
yaml2025-08-11 20:15:12 INFO OrderService: Заказ #12345 создан за 1.23с, клиент 987
Для человека читаемо, но для машины — это просто строка.
Чтобы отфильтровать по номеру клиента или метрике времени, нужно парсить строку регулярками.
Разные сервисы пишут по-разному, формат не унифицирован → адская боль при поиске.
Тот же лог в JSON:
json{
"timestamp": "2025-08-11T20:15:12Z",
"level": "INFO",
"service": "order-service",
"logger": "OrderService",
"message": "Заказ создан",
"order_id": 12345,
"client_id": 987,
"duration_sec": 1.23,
"trace_id": "8a23f5...",
"span_id": "e1b6c4..."
}
Любая система (Loki, Elasticsearch, Splunk) сразу понимает поля:
level=INFO
— фильтруем по уровню.
order_id=12345
— ищем по конкретному заказу.
trace_id
— находим трейсы и метрики по тому же запросу.
Вместо поиска по тексту с кучей false positive:
sqlclient_id = 987 AND duration_sec > 1.0
— и сразу видим все медленные заказы конкретного клиента.
Если в логах хранится trace_id
:
Из графика в Grafana можно кликнуть на точку пика задержки → открыть именно те логи, которые связаны с этим запросом.
Можно собрать сквозную историю запроса: фронт → API → DB → сторонний сервис.
В микросервисной архитектуре часто 10–50 сервисов.
Если все пишут логи в одном формате JSON с одинаковыми ключами (timestamp
, level
, trace_id
, error_code
, service
), то искать и анализировать становится одинаково удобно везде.
Можно строить сквозные фильтры по всем сервисам сразу.
Структурированные логи легко:
Прогонять через парсеры (promtail, Fluentd, Logstash).
Обогащать (добавлять geo, owner, env).
Маскировать PII.
Отправлять в разные хранилища в зависимости от level
или error_code
.
Добавить новое поле (payment_status
) можно без изменения парсеров.
Старые парсеры просто проигнорируют лишнее поле.
Сценарий: заказ не оформляется, жалобы от пользователей.
Текстовые логи:
Нужно собирать grep по 5–6 сервисам.
Разные форматы, искать дольше 30 минут.
JSON-логи с trace_id:
В Grafana кликаем по trace_id
из алерта.
Получаем все логи по цепочке от фронта до БД.
Видим: задержка в payment-service
на этапе валидации карты.
Вся диагностика за 2–3 минуты.
Если хочешь, я могу сделать шаблон формата JSON-лога для твоих Java Spring сервисов так, чтобы он был:
Унифицирован для всех команд.
Автоматически содержал trace_id
и span_id
для корреляции.
Уже готов для отправки в Loki или Elasticsearch.
Хочешь, я подготовлю этот шаблон?
Окей, давай разберём по шагам, что именно означают JVM-метрики и как по ним понять, что именно жрёт CPU в Java-приложении.
Потому что просто “CPU вырос” — это симптом, а причина может быть в сотне мест: от циклов в коде до утечки потоков или аномального GC.
В Java процессор обычно “ест” три крупных блока:
Источник | Как проявляется | Метрики, на которые смотреть |
---|---|---|
Потоки приложения (application threads) | Много вычислений в коде: парсинг, сортировка, конвертация данных, шифрование, цикл без выхода | Количество активных потоков, cpu.user в ThreadMXBean, hot threads в профайлере |
Garbage Collector (GC) | CPU уходит на сборку мусора (особенно при больших heap или частых minor GC) | jvm_gc_cpu_seconds_total , gc_time_seconds , количество сборок |
JIT-компиляция / class loading | Всплеск при старте или под высокой нагрузкой с большим количеством новых классов | jvm_classes_loaded , JIT compile time (через JFR) |
Нагрузка JVM по CPU:
process_cpu_usage
— доля CPU, которую использует процесс JVM.
system_cpu_usage
— нагрузка на ноду в целом.
Количество потоков:
jvm_threads_live
— текущие активные потоки.
jvm_threads_peak
— пиковое число потоков.
jvm_threads_daemon
— демоны (фоновые).
GC:
jvm_gc_pause_seconds_count
— число пауз GC.
jvm_gc_pause_seconds_sum
— суммарное время пауз.
jvm_gc_memory_allocated_bytes_total
— объём выделенной памяти.
Классы:
jvm_classes_loaded
— загруженные классы.
jvm_classes_unloaded_total
— выгруженные классы.
Heap/Non-heap:
jvm_memory_used_bytes
с area="heap"
и area="nonheap"
.
jvm_memory_max_bytes
.
Если при росте process_cpu_usage
параллельно растёт:
jvm_gc_pause_seconds_count
и *_sum
→ значит, CPU тратится на GC.
jvm_gc_memory_allocated_bytes_total
→ идёт много аллокаций → проверить, что генерирует мусор.
Если GC показатели в норме → значит, это ваш код или внешние вызовы.
При высоком CPU часто растёт jvm_threads_live
или jvm_threads_peak
.
Если потоков стало резко больше:
Возможна утечка потоков (ExecutorService без shutdown).
Бесконечные циклы внутри потоков.
Если потоков много, но CPU не растёт — значит, они в ожидании (IO bound).
Если потоков мало, но CPU высоко → один/несколько потоков загружены под 100% (CPU bound).
В Java есть стандартный способ понять, что выполняется в момент пика CPU:
bashkubectl exec -it <pod> -- jcmd <pid> Thread.print
или
bashkubectl exec -it <pod> -- jstack <pid> | less
Там видно:
Какие потоки работают (RUNNABLE).
Что они делают (стек вызовов).
Какие методы занимают процессор.
Если можно — запускаем Async Profiler или включаем Java Flight Recorder:
bashjcmd <pid> JFR.start name=cpuDump settings=profile duration=60s filename=/tmp/cpu.jfr
Получаем flame graph (покажет, какие методы и классы потребляют CPU).
Симптом | Вероятная причина | Метрики |
---|---|---|
CPU растёт вместе с GC-паузами | Много мелких объектов, частый minor GC | Alloc rate, GC pause time |
CPU растёт, потоков много | Утечка потоков или бурст в пуле потоков | jvm_threads_live |
CPU растёт, потоков мало, но 100% | Алгоритм с высокой вычислительной сложностью | jstack / профилировка |
CPU растёт после релиза | Новый код или библиотека с тяжёлой логикой | release annotations + diff |
Включить экспорт JVM-метрик в Prometheus с детализацией по thread pool’ам.
Настроить алерты:
CPU > 80% + GC time > 20% → возможно GC-проблема.
CPU > 80% + threads > X → возможно утечка потоков.
Логи с thread_name
и trace_id
→ быстрее находить горячие запросы.
Аннотировать релизы в Grafana → видеть, совпал ли рост CPU с деплоем.
Если хочешь, я могу сделать тебе схему “дерева решений” — как по JVM-метрикам и jstack за 5 минут понять, что нагружает CPU,
и прикреплю туда готовые PromQL-запросы для OpenShift, чтобы такие пики отлавливать автоматически.
Хочешь, подготовлю?
Готово. Ниже — «дерево решений» для всплесков CPU в JVM + готовые PromQL-запросы, алерты и команды диагностики прямо в OpenShift. Сохраните это как runbook.
Проверяем процессную CPU JVM:
process_cpu_usage
(Micrometer)
container_cpu_usage_seconds_total
(по pod)
Если pod ↑, а process_cpu_usage
≈ 0 — дело в другом контейнере/сайдкаре.
→ Если рост подтверждён у JVM → к Узлу 1.
Растут:
jvm_gc_pause_seconds_count
, jvm_gc_pause_seconds_sum
,
jvm_gc_memory_allocated_bytes_total
(alloc rate).
Частые minor GC, заметный GC CPU (если есть jvm_gc_cpu_seconds_total
).
Да, это GC → к Узлу 2.
Нет, GC не растёт → к Узлу 3 (ваш код/библиотеки/JNI/шифрование и т.п.).
Alloc rate высокий (например, > 500MB/s для вашего сервиса):
Ищем «мусорогенераторы»: сериализация/десериализация, map/filter на больших коллекциях, преобразования строк, JSON/XML, protobuf.
Действия:
Понизить аллокации (пулы буферов, стримы без боксовки, реюз объектов).
Увеличить heap (если частые minor GC из-за слишком маленьких поколений).
Проверить G1/Parallel/ZGC настройки.
Паузы длинные:
G1: много Mixed
циклов → перегрев старшего поколения.
Действия: увеличить -Xmx
, настроить -XX:MaxGCPauseMillis=200..500
, снизить фрагментацию (off-heap кеши).
Частые Full GC → утечки/большие старые объекты/метаспейс.
Действия: профилировать удерживаемые объекты (heap dump анализ), проверить кэш-политики.
Метаспейс растёт (jvm_memory_used_bytes{area="nonheap",id="Metaspace"}
):
Динамическая генерация классов (прокси, сериализаторы).
Действия: фиксировать точки генерации, обновить библиотеки.
Если после оптимизаций GC всё ещё основной потребитель — снимите JFR и сравните «Object Allocation» и «GC CPU».
jvm_threads_live
, jvm_threads_peak
, jvm_threads_daemon
.
Если потоков резко больше обычного → утечка потоков (Executors, schedulers) или взрыв параллелизма.
Диагностика прямо в pod:
bash# Получить PID JVM в контейнере
kubectl exec -it <pod> -- sh -c 'jcmd | head -n1'
# Стек-профиль: какие потоки RUNNABLE
kubectl exec -it <pod> -- jcmd <pid> Thread.print > /tmp/th.txt
kubectl exec -it <pod> -- jstack <pid> > /tmp/stack.txt
Ищем:
Потоки в состоянии RUNNABLE
и их стеки (верхние кадры показывают «горячий» метод).
Долгие циклы, сортировки, криптографию, компрессию, JSON-парсеры и т.п.
JFR (без рестарта):
bashkubectl exec -it <pod> -- jcmd <pid> JFR.start name=cpu settings=profile duration=60s filename=/tmp/cpu.jfr
kubectl cp <ns>/<pod>:/tmp/cpu.jfr ./cpu.jfr
# открыть в JDK Mission Control локально
async-profiler (если доступен):
bash# CPU 60 секунд, flamegraph
kubectl exec -it <pod> -- ./profiler.sh -d 60 -f /tmp/flame.html <pid>
kubectl cp <ns>/<pod>:/tmp/flame.html ./flame.html
Смотрим верх стека/«площади» на графе — это и есть «кто ест CPU».
Reactor/Netty: спин на event-loop при блокирующих вызовах в NIO-потоках.
→ Проверить, что блокирующее вынесено в boundedElastic / отдельные пулы.
Tomcat/Undertow Jetty: слишком большой maxThreads → лишний контекст-свитчинг.
→ Снизить до разумного, мониторить tomcat_threads_busy
.
Jackson/Gson: горячие поля/рефлексия → включить Afterburner/record-friendly модели, кэшировать ObjectMapper
.
Crypto/Compression: JNI-горячие точки → использовать аппаратные оптимизации, тюнить уровни компрессии.
Процессная CPU JVM (Micrometer)
promqlavg by (pod, service) (process_cpu_usage{service=~"$service", namespace="$ns"}) * 100
CPU pod (cAdvisor/Kubelet)
promqlrate(container_cpu_usage_seconds_total{pod=~"$pod", container!="POD", namespace="$ns"}[5m]) * 100
Сравнение процессной и контейнерной CPU
promql( avg by (pod) (process_cpu_usage{namespace="$ns"}) ) / ( rate(container_cpu_usage_seconds_total{pod=~"$pod", container!="POD", namespace="$ns"}[5m]) )
Если сильно расходится — есть CPU вне JVM (сайдкары, нативные утилиты).
Частота пауз GC
promqlrate(jvm_gc_pause_seconds_count{service=~"$service", namespace="$ns"}[5m])
Суммарное время пауз GC
promqlrate(jvm_gc_pause_seconds_sum{service=~"$service", namespace="$ns"}[5m])
Alloc rate
promqlrate(jvm_gc_memory_allocated_bytes_total{service=~"$service", namespace="$ns"}[5m])
Живые потоки
promqljvm_threads_live{service=~"$service", namespace="$ns"}
Пик потоков
promqlmax_over_time(jvm_threads_live{service=~"$service", namespace="$ns"}[1h])
Heap used vs max
promqlsum by (service) (jvm_memory_used_bytes{area="heap", namespace="$ns"}) / sum by (service) (jvm_memory_max_bytes{area="heap", namespace="$ns"}) * 100
Metaspace
promqljvm_memory_used_bytes{area="nonheap", id="Metaspace", namespace="$ns"}
1) CPU высокая, GC низкий → вероятно код
promql(process_cpu_usage{namespace="$ns"} > 0.8) and (rate(jvm_gc_pause_seconds_sum{namespace="$ns"}[5m]) < 0.05)
→ Alert: HighCPU_NotGC (severity=warning)
2) CPU высокая, GC высоко
promql(process_cpu_usage{namespace="$ns"} > 0.8) and on (pod) (rate(jvm_gc_pause_seconds_sum{namespace="$ns"}[5m]) > 0.2)
→ Alert: HighCPU_GC (severity=warning)
3) Взрыв потоков
promqljvm_threads_live{namespace="$ns"} > (avg_over_time(jvm_threads_live{namespace="$ns"}[1d]) * 1.5)
→ Alert: ThreadLeak_Suspected
4) Высокий alloc rate
promqlrate(jvm_gc_memory_allocated_bytes_total{namespace="$ns"}[5m]) > 2.5e8
(>250MB/s как отправная точка)
→ Alert: HighAllocationRate
5) GC паузы долго для сервиса
promqlrate(jvm_gc_pause_seconds_sum{namespace="$ns"}[15m]) > 0.15
(>9 сек за 1 минуту в среднем)
→ Alert: LongGCPauses
Снимок стеков при пике:
bashkubectl exec -it <pod> -- jcmd <pid> Thread.print | tee /tmp/th.txt
Ищем RUNNABLE и повторяющиеся кадры → это «горячие» места.
JFR 60 секунд:
bashkubectl exec -it <pod> -- jcmd <pid> JFR.start name=cpu settings=profile duration=60s filename=/tmp/cpu.jfr
kubectl cp <ns>/<pod>:/tmp/cpu.jfr ./cpu.jfr
Сравнить с релизами: откройте Grafana дашборд сервиса → включите аннотации релизов → совпадает ли пик CPU со временем выкладки?
Сетевые/IO факторы: если CPU ↑, а RPS не ↑ — ищите «массовую пост-обработку» (JSON/шифрование), ретраи или активные циклы.
Micrometer бинды: thread-pool
(Tomcat/Netty/Executor), http.server
гистограммы (для P95/P99), метрики сериализации при наличии.
Логи JSON с thread_name
, trace_id
, span_id
, release.version
.
Экспорт JFR on-demand (файл-путь и разрешения), readiness-эндпоинт для включения профилирования флагом.
Для Reactor: пулы Schedulers
с метриками (reactor.schedulers.*
) и запрет блокирующих вызовов в event-loop (Hook/BlockHound опционально).
Начать с G1GC (по умолчанию на современных JVM), задать мягкую цель пауз:
ruby-XX:+UseG1GC -XX:MaxGCPauseMillis=300
Для сервисов с экстремальным Throughput и большими паузами рассмотреть ZGC (если JDK 17+ и нагрузка оправдывает).
Следить за Xms == Xmx
(чтобы избежать динамического расширения в проде).
Держать -XX:ActiveProcessorCount
в соответствии с лимитом CPU на pod (если k8s cgroups не распознаются корректно в вашей JDK).
CPU ↑ → смотрим GC метрики.
Если GC ↑ → снижать аллокации / тюнинг heap/GC / анализ объектов.
Если GC норм → потоки + профилинг (JFR/async-profiler) → берём top-stack / flamegraph → фиксим горячие методы.
Сверяем с релизом. Если «после релиза» — катим флаг/откат, делаем постмортем с JFR.
Добавляем целевые метрики, чтобы инцидент в будущем занял минуты, а не часы.
Если дашь названия 1–2 конкретных сервисов (и какие у них веб-стек/GC/пулы), я соберу готовый Grafana-дашборд JSON, Alertmanager rules и манифесты ServiceMonitor под ваши namespace’ы.
Окей, давай по порядку — что такое JFR, что такое профайлинг вообще, и какие бывают профайлеры, особенно для Java/JVM.
Профайлинг (profiling) — это методика измерения и анализа работы программы во время выполнения.
Цель — понять как именно программа использует ресурсы (CPU, память, I/O) и где именно тратится больше всего времени или памяти.
🔍 Главная разница от обычных метрик в том, что профайлинг:
Даёт разрез по коду — вплоть до методов, классов, строк.
Может показать «горячие точки» (hot spots), на которые уходит 80–90% времени.
Помогает найти узкие места, утечки и неэффективные алгоритмы.
Профайлер — это инструмент (программа или библиотека), который собирает данные о выполнении вашего приложения, чтобы потом их можно было проанализировать.
Тип профайлера | Как работает | Плюсы | Минусы |
---|---|---|---|
Сэмплирующий (sampling) | Периодически «останавливает» поток и смотрит стек вызовов | Низкое влияние на производительность, подходит для продакшена | Менее точный по времени, не фиксирует очень быстрые вызовы |
Инструментирующий (instrumentation) | Вставляет код (hooks) в каждую функцию/метод | Очень точный по времени и количеству вызовов | Сильно замедляет выполнение, плохо для продакшена |
Event-based (JFR) | Регистрирует только определённые события JVM | Очень малое влияние на производительность, богатая информация | Ограничен набором событий (но для продакшена чаще всего хватает) |
JFR (Java Flight Recorder) — это встроенный в JVM (с JDK 11+ без лицензии) профайлер от Oracle.
Он собирает события внутри JVM: вызовы методов, GC, аллокации, блокировки потоков, JIT-компиляцию, загрузку классов, I/O, и при этом даёт минимальную нагрузку (обычно <1% CPU).
📌 Особенности JFR:
Работает в продакшене — можно включить на боевом сервисе на 30–60 секунд, чтобы поймать проблему.
Собирает не только CPU, но и:
Метрики GC (паузы, поколения).
Аллокации объектов (кто выделяет память).
Thread dump'ы (что делают потоки).
Class loading/unloading.
Системные события (I/O, сокеты, файловые операции).
Данные сохраняются в .jfr
файле.
Анализировать можно через Java Mission Control (JMC) или сторонние анализаторы.
📌 Пример запуска JFR на работающей JVM:
bash# Узнаём PID процесса
jcmd
# Запускаем запись на 60 секунд с настройками "profile"
jcmd <pid> JFR.start name=cpu settings=profile duration=60s filename=/tmp/cpu.jfr
# Останавливать не нужно — файл сохранится автоматически
CPU pod вырос с 0.5 до 2 vCPU.
Метрики GC норм — значит, не мусорщик.
Запускаем JFR на 60 секунд.
В отчёте JMC видим, что 80% CPU уходит в метод calculateInvoice()
, а внутри — на сортировку списка в 1 млн элементов.
Решение: оптимизировать сортировку или уменьшить объём данных.
Инструмент | Тип | Подходит для продакшена | Примечания |
---|---|---|---|
JFR (Java Flight Recorder) | Event-based | ✅ | Встроен в JVM, минимальная нагрузка, удобен для short-term анализа |
Java Mission Control | Анализатор | ✅ | GUI для чтения JFR-файлов |
Async Profiler | Sampling | ✅ | Очень низкое влияние, даёт flame graphs, CPU/alloc/probe режимы |
YourKit Java Profiler | Instrumentation + sampling | ⚠️ | Коммерческий, удобный GUI, но для продакшена с осторожностью |
VisualVM | Sampling | ⚠️ | Бесплатный, подходит для dev/staging, но не лучший выбор для боевых highload |
Eclipse Memory Analyzer (MAT) | Heap dump анализ | ✅ (off-line) | Для поиска утечек памяти по heap dump |
Perf + async-profiler (Linux) | Sampling | ✅ | Системный уровень, точный, но требует root/dcapabilities |
Задача | Инструмент |
---|---|
CPU резко вырос | JFR или Async Profiler |
Утечка памяти | Heap dump + Eclipse MAT |
GC жрёт CPU | JFR (GC events) или GC logs |
Блокировки потоков | JFR (Thread Dump events) |
Нужен flame graph | Async Profiler или JFR с GUI |
Анализ нагрузки до метода | JFR (Execution Sample) |
Если хочешь, я могу сделать тебе сравнительную таблицу JFR vs Async Profiler vs YourKit с плюсами/минусами именно для highload в продакшене,
и добавить шаблон команды для OpenShift, чтобы можно было запустить JFR/профайлер на pod за один вызов.
Хочешь, сделать?