Мониторинг и Observability
1. Введение
Представьте: 3 часа ночи, на ваш пейджер приходит алерт — latency API выросла в 10 раз. Вы открываете дашборд и видите... ничего подозрительного. Метрики в норме. Логи молчат. Куда копать?
Именно в такие моменты становится ясно: разница между мониторингом и observability — это разница между системой, которая говорит вам "что-то сломалось", и системой, которая позволяет ответить на вопрос "почему это сломалось".
Мониторинг — это практика наблюдения за заранее известными метриками и состояниями системы. Вы знаете, что хотите измерить, и настраиваете алерты.
Observability (наблюдаемость) — это свойство системы, которое позволяет понять её внутреннее состояние, исходя исключительно из внешних выходных данных. Observable система позволяет задавать произвольные вопросы о своём поведении — даже те, которые вы не предусмотрели при проектировании.
Почему это важно с точки зрения System Design?
Без мониторинга и observability невозможно:
- Обнаруживать деградацию производительности до того, как её заметят пользователи
- Понимать реальное поведение системы в production
- Принимать data-driven решения об оптимизации
- Выполнять SLA/SLO перед бизнесом и клиентами
- Проводить post-mortem анализ инцидентов
Кто использует подобные системы?
- Google — разработала Monarch (внутренняя система метрик), Dapper (прообраз distributed tracing), Borg monitoring
- Netflix — Atlas (система метрик), открытая observability платформа
- Uber — M3 (система метрик на базе Prometheus), Jaeger (open-source distributed tracing)
- Lyft — создала Envoy с встроенным observability, активно использует Prometheus
- Airbnb — Chronos, собственные решения на базе open-source стека
- Cloudflare — обрабатывает петабайты телеметрии в реальном времени
2. Функциональные требования
Core features (must-have)
- Сбор метрик — агрегация числовых измерений (счётчики, гистограммы, gauges) со всех сервисов
- Централизованное логирование — сбор, хранение и поиск структурированных логов
- Distributed Tracing — отслеживание запроса через цепочку микросервисов
- Алертинг — уведомления при нарушении пороговых значений
- Визуализация — дашборды с графиками в реальном времени
- Retention — хранение телеметрии с заданным TTL
Extended features (nice-to-have)
- Аномалий детекция — ML-based выявление аномалий без заданных порогов
- Корреляция сигналов — автоматическая связка метрик, логов и трейсов одного инцидента
- SLO tracking — автоматический расчёт error budget
- Профилирование в production — continuous profiling (как Google pprof)
- Synthetic monitoring — искусственные транзакции для проактивной проверки
- Инфраструктурный мониторинг — состояние хостов, Kubernetes-кластеров, сетей
3. Нефункциональные требования
Ожидаемая нагрузка
Рассмотрим систему мониторинга для крупной компании уровня Uber/Airbnb:
- Инфраструктура: 10 000 микросервисов, 50 000 инстансов
- Метрики: каждый инстанс генерирует ~1 000 метрик каждые 15 секунд
- Логи: каждый инстанс генерирует ~1 000 строк/сек
- Трейсы: 500 000 RPS на входящий трафик, sampling rate 1%
Latency requirements
- Метрики: задержка до появления на дашборде — не более 30 секунд
- Логи: задержка поиска по свежим логам — не более 60 секунд
- Трейсы: задержка доступа к завершённому трейсу — не более 30 секунд
- Алерты: уведомление после нарушения порога — не более 60 секунд
Availability target
- 99.9% uptime (8.7 часов простоя в год) — мониторинг сам по себе не является критическим путём для бизнеса, но инциденты без мониторинга обнаруживаются значительно позже
- Graceful degradation: при падении системы мониторинга основные сервисы должны продолжать работу
Consistency requirements
- Eventual consistency приемлема для метрик и логов
- Для алертингового pipeline важна at-least-once delivery (лучше лишнее уведомление, чем пропущенный инцидент)
4. Back-of-the-Envelope Estimation
Метрики (Metrics)
Инстансов: 50 000
Метрик на инстанс: 1 000
Scrape interval: 15 сек
Точек данных в секунду:
50 000 × 1 000 / 15 ≈ 3 300 000 точек/сек = 3.3M samples/sec
Размер одного sample (timestamp + value + labels):
~50 байт
Пропускная способность (ingestion):
3.3M × 50 = 165 MB/sec ≈ 165 MB/s
Хранение (retention 90 дней):
165 MB/s × 86400 × 90 ≈ 1.28 PB (до сжатия)
Prometheus с delta encoding + XOR compression:
Compression ratio ≈ 10x
Итого: ~128 TB
Логи (Logs)
Инстансов: 50 000
Строк/сек на инстанс: 1 000
Средний размер строки: 200 байт
Пропускная способность:
50 000 × 1 000 × 200 = 10 GB/sec
Хранение (retention 30 дней):
10 GB/s × 86400 × 30 = 25.9 PB (до сжатия)
С gzip compression (ratio ≈ 10x для текстовых логов):
Итого: ~2.6 PB
Трейсы (Traces)
Входящий RPS: 500 000
Sampling rate: 1% = 5 000 трейсов/сек
Span'ов на трейс: ~10 (среднее число сервисов в цепочке)
Размер span: ~1 KB
Пропускная способность:
5 000 × 10 × 1 KB = 50 MB/sec
Хранение (retention 7 дней):
50 MB/s × 86400 × 7 = 30 TB
5. High-Level Design
Три столпа Observability
┌─────────────────────────────────────────────────────────────┐
│ Три столпа Observability │
├─────────────────┬──────────────────┬────────────────────────┤
│ METRICS │ LOGS │ TRACES │
│ │ │ │
│ "Что происходит"│"Почему произошло"│"Где произошло" │
│ │ │ │
│ Числовые агре- │ Текстовые собы- │ Путь запроса через │
│ гаты во времени │ тия с контекстом │ распределённую систему │
│ │ │ │
│ Prometheus │ ELK, Loki │ Jaeger, Zipkin │
│ Grafana │ Fluentd │ OpenTelemetry │
└─────────────────┴──────────────────┴────────────────────────┘
Архитектура системы мониторинга
┌─────────────────── Источники данных ──────────────────────┐
│ │
│ [Service A] [Service B] [Service C] [Database] │
│ │ │ │ │ │
│ Metrics Metrics Metrics Metrics │
│ Logs Logs Logs Logs │
│ Traces Traces Traces │
└────────────────────────────┬──────────────────────────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Prometheus │ │ Fluent Bit │ │ OTel │
│ (scraper) │ │ (log agent) │ │ Collector │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Thanos / │ │ Loki / │ │ Jaeger / │
│ Cortex │ │ Elasticsearch│ │ Tempo │
│ (long-term) │ │ (storage) │ │ (storage) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└──────────────────┼──────────────────┘
│
▼
┌──────────────────┐
│ Grafana │
│ (Visualization) │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ AlertManager │
│ (Routing) │
└────────┬─────────┘
│
┌────────┼────────┐
▼ ▼ ▼
[PagerDuty] [Slack] [Email]
Data Flow
- Сбор метрик: Prometheus в режиме pull scrape'ит
/metricsэндпоинты каждые 15 сек - Сбор логов: Fluent Bit как DaemonSet в Kubernetes собирает stdout/stderr и пересылает в Loki/Elasticsearch
- Сбор трейсов: OpenTelemetry SDK в приложениях отправляет трейсы в OTel Collector → Jaeger
- Хранение: Thanos добавляет long-term хранилище поверх Prometheus (S3 compatible)
- Визуализация: Grafana подключается ко всем источникам через datasource plugins
- Алертинг: Prometheus Rules → AlertManager → notification channels
6. API Design
Metrics Query API (Prometheus HTTP API)
# Instant query GET /api/v1/query Query params: query: string (PromQL expression) time: unix timestamp Response: { "status": "success", "data": { "resultType": "vector", "result": [ { "metric": {"__name__": "http_requests_total", "job": "api-server"}, "value": [1714564800, "1234"] } ] } } # Range query GET /api/v1/query_range Query params: query: string start: unix timestamp end: unix timestamp step: duration (e.g., "15s", "1m")
Logs Query API (Loki)
# Query logs GET /loki/api/v1/query_range Query params: query: string (LogQL expression) start: nanosecond timestamp end: nanosecond timestamp limit: int (max log lines) direction: "forward" | "backward" Response: { "status": "success", "data": { "resultType": "streams", "result": [ { "stream": {"app": "api-server", "level": "error"}, "values": [ ["1714564800000000000", "ERROR: connection refused to db"] ] } ] } }
Traces Query API (Jaeger)
# Find traces GET /api/traces Query params: service: string operation: string tags: string (JSON) start: microseconds end: microseconds minDuration: string (e.g., "100ms") limit: int # Get specific trace GET /api/traces/{traceID} Response: { "data": [ { "traceID": "abc123", "spans": [ { "traceID": "abc123", "spanID": "def456", "operationName": "HTTP GET /api/users", "startTime": 1714564800000, "duration": 45000, "tags": [{"key": "http.status_code", "value": 200}], "logs": [], "references": [] } ] } ] }
Alerting API (AlertManager)
# Create silence (подавление алертов) POST /api/v2/silences Body: { "matchers": [ {"name": "alertname", "value": "HighLatency", "isRegex": false}, {"name": "service", "value": "payment-service", "isRegex": false} ], "startsAt": "2024-05-01T00:00:00Z", "endsAt": "2024-05-01T04:00:00Z", "comment": "Planned maintenance window" } # Get active alerts GET /api/v2/alerts Response: List of active alerts with labels, annotations, fingerprint
7. Data Model
Метрики — модель данных
Prometheus использует time series модель:
Каждая time series уникально идентифицируется:
metric_name + labels (key-value пары)
Примеры:
http_requests_total{method="GET", status="200", service="api"}
http_requests_total{method="POST", status="500", service="api"}
Это две разные time series!
Физическое хранение (TSDB):
┌──────────────────────────────────────────────────┐
│ Series │
│ ┌────────────────┬──────────────────────────┐ │
│ │ Labels (sorted)│ Chunks (compressed data) │ │
│ │ __name__=http..│ [t0,v0][t1,v1]...[tn,vn] │ │
│ └────────────────┴──────────────────────────┘ │
│ │
│ Inverted Index: label → [series IDs] │
└──────────────────────────────────────────────────┘
Четыре типа метрик Prometheus
from prometheus_client import Counter, Gauge, Histogram, Summary # Counter — только растёт (запросы, ошибки, байты) requests_total = Counter( 'http_requests_total', 'Total HTTP requests', ['method', 'status', 'path'] ) requests_total.labels(method='GET', status='200', path='/api/users').inc() # Gauge — может расти и падать (CPU, memory, connections) memory_usage = Gauge( 'memory_usage_bytes', 'Current memory usage in bytes' ) memory_usage.set(get_memory_usage()) # Histogram — распределение значений (latency, size) request_duration = Histogram( 'http_request_duration_seconds', 'HTTP request duration', ['method', 'path'], buckets=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10] ) # Summary — процентили (аналог Histogram, но вычисляет на клиенте) # Используйте Histogram вместо Summary для масштабируемости!
Логи — структурированная модель
// Плохо: неструктурированные логи "2024-05-01 10:30:00 ERROR Failed to process payment for user 12345" // Хорошо: структурированные JSON логи { "timestamp": "2024-05-01T10:30:00.123Z", "level": "ERROR", "service": "payment-service", "version": "v2.3.1", "trace_id": "abc123def456", // ← связь с трейсом! "span_id": "789ghi", "user_id": "12345", "event": "payment_failed", "error": "connection refused", "duration_ms": 5023, "host": "payment-pod-xyz" }
Трейсы — модель данных
Trace (единый запрос через систему):
TraceID: 1a2b3c4d
│
├─ Span: API Gateway (0ms - 150ms)
│ SpanID: aaaa
│ Tags: http.method=GET, http.url=/checkout
│ Logs: [120ms] "Routing to checkout service"
│
├─ Span: Checkout Service (5ms - 130ms)
│ SpanID: bbbb, ParentSpanID: aaaa
│ Tags: service.name=checkout
│
│ ├─ Span: DB Query (10ms - 45ms)
│ │ SpanID: cccc, ParentSpanID: bbbb
│ │ Tags: db.type=postgresql, db.statement=SELECT...
│ │
│ └─ Span: Payment Service Call (50ms - 120ms)
│ SpanID: dddd, ParentSpanID: bbbb
│ Tags: http.status_code=200
Схема хранения трейсов (Jaeger + Cassandra)
-- Таблица трейсов CREATE TABLE traces ( trace_id UUID, span_id bigint, parent_id bigint, operation text, service text, start_time timestamp, duration bigint, -- microseconds tags map<text, text>, logs list<frozen<log_entry>>, PRIMARY KEY (trace_id, span_id) ) WITH CLUSTERING ORDER BY (span_id ASC); -- Индекс для поиска по сервису CREATE TABLE service_operation_index ( service text, operation text, start_time timestamp, trace_id UUID, PRIMARY KEY ((service, operation), start_time, trace_id) ) WITH CLUSTERING ORDER BY (start_time DESC);
8. Deep Dive в ключевые компоненты
8.1 Prometheus: архитектура и TSDB
Prometheus — это система мониторинга с pull-моделью: она сама опрашивает (scrape) целевые сервисы, а не получает данные от них. Это ключевое архитектурное решение.
Преимущества pull-модели:
- Prometheus всегда знает, какие сервисы "молчат" (network partition vs. service down)
- Проще отлаживать — можно вручную вызвать
/metricsэндпоинт - Нет перегрузки от "storms" push-агентов при деградации
Недостатки pull-модели:
- Проблемы с ephemeral jobs (short-lived tasks) — решается через Pushgateway
- Сложнее при dynamic service discovery за NAT
TSDB: как Prometheus хранит данные
Prometheus использует собственную Time Series Database (TSDB), оптимизированную для записи и чтения метрик.
Disk layout:
/data/prometheus/
├── chunks_head/ # WAL (Write-Ahead Log) для recovery
├── 01ABCDEF.../ # Block (2 часа данных)
│ ├── chunks/ # Compressed time series data
│ │ └── 000001 # Chunk file (128 MB max)
│ ├── index # Inverted index (labels → series IDs)
│ ├── meta.json # Block metadata
│ └── tombstones # Deleted series markers
└── 01GHIJKL.../ # Older block
Алгоритм сжатия данных (Gorilla compression):
Facebook разработал алгоритм, который использует delta-of-delta кодирование для timestamps и XOR для значений:
# Псевдокод Gorilla compression для time series def compress_timestamps(timestamps): """ Delta-of-delta encoding для временных меток. Если данные приходят регулярно (каждые 15 сек), большинство delta-of-delta == 0 → отличное сжатие. """ result = [] prev_t = timestamps[0] prev_delta = 0 for t in timestamps[1:]: delta = t - prev_t dod = delta - prev_delta # delta-of-delta if dod == 0: result.append('0') # 1 бит! elif -63 <= dod <= 64: result.append('10' + encode(dod, 7)) # 9 бит elif -255 <= dod <= 256: result.append('110' + encode(dod, 9)) # 12 бит else: result.append('1110' + encode(dod, 12)) # 16 бит prev_t = t prev_delta = delta return result def compress_values(values): """ XOR encoding для float64 значений. """ result = [] prev_val = values[0] prev_leading = 0 prev_trailing = 0 for val in values[1:]: xor = float_xor(val, prev_val) if xor == 0: result.append('0') # 1 бит — значение не изменилось! else: leading = count_leading_zeros(xor) trailing = count_trailing_zeros(xor) if leading == prev_leading and trailing == prev_trailing: # Используем те же leading/trailing zeros result.append('10' + encode_meaningful_bits(xor)) else: result.append('11' + encode_with_metadata(xor)) prev_val = val prev_leading = leading prev_trailing = trailing return result
Результат: Prometheus сжимает данные примерно до 1.37 байта на sample при теоретическом минимуме около 1 байта (vs 16 байт для наивного хранения timestamp+value).
8.2 Distributed Tracing: OpenTelemetry и Jaeger
Distributed tracing позволяет восстановить полный путь запроса через десятки микросервисов. Ключевая проблема — context propagation: каждый сервис должен передать trace context следующему.
W3C Trace Context (стандарт)
# HTTP заголовки для propagation traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 ^ ^ ^ ^ | trace-id (128 bit hex) span-id flags version tracestate: vendor1=value1,vendor2=value2
Как работает sampling
Полный трейсинг 100% запросов слишком дорог. Стратегии sampling:
class TracingSampler: """ Head-based sampling: решение принимается на входе. Быстро, но может пропустить важные "редкие" ошибки. """ def should_sample(self, trace_id: str, rate: float = 0.01) -> bool: # Детерминированный sampling по trace_id # Гарантирует что все spans одного trace либо собираются, либо нет hash_value = int(trace_id[:16], 16) / (16**16) return hash_value < rate class TailBasedSampler: """ Tail-based sampling: решение принимается после завершения трейса. Позволяет всегда сохранять трейсы с ошибками или высокой латенси. Реализует: Jaeger Adaptive Sampling, OpenTelemetry Collector Tail Sampler. """ def __init__(self): self.buffer = {} # В памяти храним spans до решения о sampling self.decision_wait = 30 # секунд ждём завершения трейса def should_sample_trace(self, trace: Trace) -> bool: # Всегда сохраняем трейсы с ошибками if any(span.has_error for span in trace.spans): return True # Всегда сохраняем медленные трейсы (> P99) if trace.duration_ms > self.p99_threshold: return True # Random sampling для остальных return random.random() < 0.001 # 0.1%
Instrumentation: OpenTelemetry SDK
from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.trace.export import BatchSpanProcessor # Настройка провайдера provider = TracerProvider() exporter = OTLPSpanExporter(endpoint="http://otel-collector:4317") provider.add_span_processor(BatchSpanProcessor(exporter)) trace.set_tracer_provider(provider) tracer = trace.get_tracer("payment-service") # Использование в коде async def process_payment(user_id: int, amount: float): with tracer.start_as_current_span("process_payment") as span: span.set_attribute("user.id", user_id) span.set_attribute("payment.amount", amount) try: # Вложенный span для вызова базы данных with tracer.start_as_current_span("db.check_balance") as db_span: db_span.set_attribute("db.system", "postgresql") db_span.set_attribute("db.operation", "SELECT") balance = await db.get_balance(user_id) if balance < amount: span.set_attribute("payment.result", "insufficient_funds") span.set_status(trace.Status(trace.StatusCode.ERROR)) raise InsufficientFundsError() # HTTP вызов к платёжному провайдеру with tracer.start_as_current_span("stripe.charge") as http_span: http_span.set_attribute("http.method", "POST") result = await stripe_client.charge(amount) http_span.set_attribute("http.status_code", result.status_code) span.set_attribute("payment.result", "success") return result except Exception as e: span.record_exception(e) span.set_status(trace.Status(trace.StatusCode.ERROR, str(e))) raise
8.3 Alerting: от метрик к уведомлениям
Эффективный алертинг — искусство. Слишком много алертов → alert fatigue → игнорирование. Слишком мало → пропущенные инциденты.
Анатомия хорошего алерта (Google SRE)
# prometheus/rules/api_alerts.yml groups: - name: api.rules rules: # ПЛОХОЙ алерт: CPU > 80% # Почему плохой? CPU сам по себе не причиняет вред пользователям. - alert: HighCPU expr: cpu_usage > 0.8 for: 5m severity: warning # ХОРОШИЙ алерт: высокий error rate # Symptom-based: напрямую отражает проблему пользователя - alert: HighErrorRate expr: | sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) > 0.01 for: 5m labels: severity: critical team: backend annotations: summary: "High error rate on {{ $labels.service }}" description: | Error rate is {{ $value | humanizePercentage }} (threshold: 1%). Runbook: https://wiki/runbooks/high-error-rate dashboard: "https://grafana/d/abc123" # Burnrate alert (SLO-based) — наиболее продвинутый подход # Алертит когда расходуется error budget слишком быстро - alert: SLOBurnRateHigh expr: | ( sum(rate(http_requests_total{status=~"5.."}[1h])) / sum(rate(http_requests_total[1h])) ) > (14.4 * 0.001) # 14.4x burn rate for 1h window for: 2m labels: severity: critical slo: api_availability annotations: summary: "SLO burn rate too high — 1 hour window" description: | At current burn rate, error budget will be exhausted in {{ $value | humanizeDuration }}.
AlertManager: routing и deduplication
# alertmanager/config.yml global: resolve_timeout: 5m slack_api_url: 'https://hooks.slack.com/...' route: group_by: ['alertname', 'service'] group_wait: 30s # Ждём перед первой нотификацией (батчинг) group_interval: 5m # Интервал между повторными нотификациями repeat_interval: 4h # Повтор если алерт не resolved receiver: 'default' routes: # Критические алерты — только PagerDuty в рабочее время - match: severity: critical receiver: 'pagerduty' continue: true # Продолжить matching других routes # Production DB — senior DBA on-call - match: team: database env: production receiver: 'dba-oncall' # Silence maintenance windows - match: alertname: 'DeploymentInProgress' receiver: 'null' receivers: - name: 'pagerduty' pagerduty_configs: - routing_key: '<key>' severity: '{{ if eq .CommonLabels.severity "critical" }}critical{{ else }}warning{{ end }}' - name: 'slack' slack_configs: - channel: '#alerts-{{ .CommonLabels.team }}' title: '[{{ .Status | toUpper }}] {{ .CommonLabels.alertname }}' text: '{{ range .Alerts }}{{ .Annotations.description }}{{ end }}' - name: 'null' # /dev/null для некритических алертов inhibit_rules: # Если сервис полностью недоступен — подавляем все downstream алерты - source_match: alertname: 'ServiceDown' target_match: team: backend equal: ['service']
9. Масштабирование
Масштабирование Prometheus: Thanos
Одиночный Prometheus не масштабируется горизонтально — каждый инстанс хранит данные локально. Thanos решает эту проблему:
┌─────────────────────────────────────────────────┐
│ Thanos │
│ │
│ [Prometheus-1] [Prometheus-2] [Prometheus-3] │
│ │ │ │ │
│ [Sidecar] [Sidecar] [Sidecar] │
│ │ │ │ │
│ └───────────────┼───────────────┘ │
│ │ │
│ [Thanos Store] │
│ │ │
│ [Object Storage] │
│ (S3 / GCS / Azure) │
│ │
│ [Thanos Query] ← единая точка запросов │
│ [Thanos Compact] ← downsampling старых данных │
│ [Thanos Ruler] ← глобальные recording rules │
└─────────────────────────────────────────────────┘
Thanos Sidecar выгружает завершённые TSDB blocks в object storage каждые 2 часа. Thanos Query federate запросы через все Prometheus инстансы и object storage, дедуплицируя данные от HA пар.
Масштабирование логов: Loki vs Elasticsearch
Elasticsearch (традиционный подход):
+ Полнотекстный поиск, мощные агрегации
+ Mature ecosystem (Kibana, APM)
- Высокое потребление памяти (JVM heap)
- Индексирует всё → дорого при высоком cardinality
- Сложное горизонтальное масштабирование
Loki (modern, "Prometheus for logs"):
+ Индексирует только labels (как Prometheus)
+ Дешёвый ingest (parquet на S3 ≈ $0.023/GB)
+ Нативная интеграция с Grafana
- Полнотекстный поиск медленнее (grep по chunks)
- Менее мощные агрегации
- Меньший community vs ELK
Выбор:
- Compliance/audit/security logs → Elasticsearch (нужен rich search)
- Application logs в cloud-native среде → Loki (дёшево и интегрировано)
Шардирование и партиционирование
Метрики (Cortex — горизонтально масштабируемый Prometheus)
Cortex использует consistent hashing для распределения series:
hash(metric_labels) % num_ingesters → ingester_id
Каждый ingester обрабатывает свой диапазон hash space.
Replication factor = 3 (как в Cassandra).
При записи: quorum write (2 из 3)
При чтении: quorum read (2 из 3)
Логи (Elasticsearch sharding)
{ "settings": { "number_of_shards": 5, // Партиционирование по дням "number_of_replicas": 1, "index.lifecycle.name": "logs-ilm-policy" }, "mappings": { "properties": { "timestamp": {"type": "date"}, "level": {"type": "keyword"}, "service": {"type": "keyword"}, "message": {"type": "text", "index": false} // Не индексируем для экономии } } }
Index Lifecycle Management (ILM):
Hot (NVMe SSD, 7 дней)
→ Warm (HDD, 23 дней, force merge + compression)
→ Cold (S3 Glacier, 340 дней, read-only)
→ Delete (365+ дней)
Кэширование
Thanos Query Frontend — кэшируем результаты PromQL:
L1: In-memory cache (per query-frontend pod)
L2: Memcached (shared across pods)
Cache key = hash(query + time_range + step)
TTL = max(5 min, step_duration)
Особенность: alignment запросов по step
Без alignment: query(now) != query(now + 1sec) → cache miss
С alignment: все запросы округляются до step → cache hit rate ≈ 90%
10. Trade-offs и альтернативы
Pull vs Push модель сбора метрик
| Аспект | Pull (Prometheus) | Push (Graphite, InfluxDB) |
|---|---|---|
| Service discovery | Нативный, через SD | Сервис должен знать адрес |
| Short-lived jobs | Проблема (Pushgateway) | Нативно |
| Firewall/NAT | Проблема | Нативно |
| Debug | Просто (curl /metrics) | Сложнее |
| Overload control | Prometheus контролирует tempo | Может быть перегружен |
| Multi-tenancy | Сложно | Проще |
Вердикт: Prometheus pull-модель лучше для majority use cases в cloud-native средах. Push модель предпочтительна для IoT, serverless функций и short-lived jobs.
Агент для логов: Fluentd vs Fluent Bit vs Logstash
Logstash (ELK stack, Java):
+ Мощные трансформации, rich plugin ecosystem
- Высокое потребление ресурсов (JVM)
- Для инстансов с 512 MB RAM — нет
Fluentd (Ruby + C):
+ Хорошая экосистема плагинов
+ Умеренное потребление
- Тяжелее Fluent Bit
Fluent Bit (C, для DaemonSet):
+ Минимальное потребление (~650KB RAM, <1% CPU)
+ Рекомендован CNCF для Kubernetes
- Меньше плагинов, чем у Fluentd
Рекомендация: Fluent Bit как DaemonSet на каждом узле Kubernetes
→ Fluentd/Logstash как aggregator-слой (опционально)
OpenTelemetry vs Vendor-specific SDK
Vendor lock-in:
DataDog SDK → работает только с DataDog
AWS X-Ray SDK → работает только с X-Ray
OpenTelemetry:
+ Vendor-neutral, CNCF graduated project
+ Один SDK для всех backends (Jaeger, Zipkin, DataDog, etc.)
+ Стандарт в индустрии (2024+)
- Более молодой, некоторые features ещё в beta
Вердикт: Инвестируйте в OpenTelemetry — это будущее.
11. Отказоустойчивость
Single Points of Failure
Компонент | Риск | Решение
---------------------|---------------------------|------------------------
Prometheus | Потеря метрик при падении | HA pair (2 инстанса) + Thanos
AlertManager | Дублированные алерты | Cluster mode (gossip protocol)
Elasticsearch | Потеря данных | Replication factor ≥ 2
Grafana | Нет дашбордов | Multiple instances + LB
OTel Collector | Потеря трейсов | Agent на каждом поде + buffer
Prometheus HA
# Два идентичных Prometheus с одинаковым конфигом # Thanos Querier дедуплицирует данные при запросе # prometheus-a.yml global: external_labels: replica: 'a' # Единственное отличие! cluster: 'prod-eu-west' # prometheus-b.yml global: external_labels: replica: 'b' cluster: 'prod-eu-west'
Disaster Recovery
Стратегия резервного копирования:
Метрики (Thanos):
- Непрерывная загрузка в S3 (каждые 2 часа)
- S3 Versioning включён
- Cross-region replication (us-east-1 → eu-west-1)
- RPO: 2 часа (максимальная потеря данных)
- RTO: 30 минут (время восстановления нового инстанса)
Логи (Elasticsearch):
- Snapshot в S3 каждые 6 часов
- ILM автоматически управляет retention
- Cross-cluster replication для критических индексов
- RPO: 6 часов
- RTO: 2-4 часа (время восстановления зависит от объёма)
Grafana:
- Дашборды хранятся в Git (Infrastructure as Code)
- Grafana provisioning из ConfigMap в Kubernetes
- RTO: 5 минут (helm install)
Защита от alert storms
class AlertRateLimiter: """ При масштабном инциденте сотни сервисов могут одновременно отправить алерты. AlertManager группирует их, но нужна дополнительная защита. """ def __init__(self, max_per_minute: int = 10): self.max_per_minute = max_per_minute self.sent_count = 0 self.window_start = time.time() def should_send(self, alert: Alert) -> bool: now = time.time() # Сброс счётчика каждую минуту if now - self.window_start > 60: self.sent_count = 0 self.window_start = now if self.sent_count >= self.max_per_minute: # Логируем подавленные алерты для аудита logger.warning(f"Rate limited alert: {alert.name}") return False self.sent_count += 1 return True
12. SRE практики: Error Budgets и Toil
SLI, SLO, SLA
Это фундамент SRE — без чёткого определения "что значит работающий сервис" невозможно строить разумный мониторинг.
SLI (Service Level Indicator):
Метрика, описывающая аспект качества сервиса.
Примеры:
- Availability SLI = успешные запросы / все запросы
- Latency SLI = % запросов, выполненных за < 200ms
- Throughput SLI = запросов/сек
SLO (Service Level Objective):
Целевое значение SLI за период времени.
Примеры:
- 99.9% запросов успешны за rolling 30-дневное окно
- 95% запросов выполняются за < 200ms за rolling 7-дневное окно
SLA (Service Level Agreement):
Юридический контракт с клиентом, основанный на SLO.
SLA = SLO - buffer (SLO жёстче, чтобы было время реагировать)
Если SLO = 99.9%, то SLA = 99.5%
Error Budget
class ErrorBudget: """ Error Budget = допустимый объём ошибок за период. Если SLO = 99.9% за 30 дней: Error budget = 0.1% × 30 × 24 × 60 = 43.2 минуты downtime Пока есть error budget — можно деплоить. Когда budget исчерпан — только hotfixes, feature freeze. """ def __init__(self, slo: float, window_days: int = 30): self.slo = slo self.window_seconds = window_days * 24 * 3600 self.total_budget_seconds = (1 - slo) * self.window_seconds def remaining_budget(self, error_rate_series: list) -> float: """Вычислить оставшийся error budget в секундах.""" consumed = sum( rate * interval for rate, interval in error_rate_series ) return self.total_budget_seconds - consumed def burn_rate(self, current_error_rate: float) -> float: """ Burn rate: как быстро расходуется error budget. Burn rate 1 = нормальный темп. Burn rate 14.4 = budget закончится за 1/14.4 = 2 дня вместо 30. """ return current_error_rate / (1 - self.slo)
PromQL для SLO мониторинга
# Текущий availability SLI (за 30 дней) sli_availability_30d = sum(increase(http_requests_total{status!~"5.."}[30d])) / sum(increase(http_requests_total[30d])) # Оставшийся error budget (в процентах) error_budget_remaining = 1 - ( (1 - sli_availability_30d) / (1 - 0.999) ) # Burn rate за последний час burn_rate_1h = ( sum(rate(http_requests_total{status=~"5.."}[1h])) / sum(rate(http_requests_total[1h])) ) / (1 - 0.999) # Multi-window burn rate alerting (Google SRE Book рекомендация) # Алертить при burn rate > 14.4 за 1 час (budget закончится за 2 дня) # И при burn rate > 6 за 6 часов (для менее срочных случаев)
Toil: что это и как с ним бороться
Toil (рутина) — это ручная, повторяющаяся, автоматизируемая работа, которая не приносит долгосрочной ценности.
Примеры toil в мониторинге:
- Ручное создание silences при каждом деплойменте
- Ручное масштабирование storage при росте данных
- Ручной анализ логов при каждом инциденте
Как автоматизировать:
# Автоматическое создание silence при деплойменте import requests from datetime import datetime, timedelta class AlertManagerClient: def create_deployment_silence( self, service: str, duration_minutes: int = 30, deployed_by: str = "CI/CD" ): """ Автоматически подавляет не-критические алерты во время деплоя. Вызывается из CI/CD pipeline. """ silence = { "matchers": [ {"name": "service", "value": service, "isRegex": False}, {"name": "severity", "value": "warning", "isRegex": False} ], "startsAt": datetime.utcnow().isoformat() + "Z", "endsAt": (datetime.utcnow() + timedelta(minutes=duration_minutes)).isoformat() + "Z", "comment": f"Deployment silence created by {deployed_by}", "createdBy": "automation" } response = requests.post( f"{self.alertmanager_url}/api/v2/silences", json=silence ) return response.json()["silenceID"]
13. Мониторинг платформы мониторинга
Да, системе мониторинга тоже нужен мониторинг. Это называется "meta-monitoring".
Ключевые метрики
Prometheus:
prometheus_tsdb_head_series → количество активных time series
prometheus_tsdb_compactions_total → активность TSDB
prometheus_rule_evaluation_duration → время вычисления правил
prometheus_target_scrape_pool_size → количество targets
up{job="..."} → доступность targets
AlertManager:
alertmanager_alerts_received_total → входящие алерты
alertmanager_notifications_total → отправленные уведомления
alertmanager_notification_latency → latency нотификаций
Elasticsearch:
elasticsearch_cluster_health_status → 0=red, 1=yellow, 2=green
elasticsearch_jvm_mem_heap_used_bytes → JVM heap (критично!)
elasticsearch_indices_indexing_index_current → backpressure
Jaeger:
jaeger_collector_spans_received_total → входящий поток spans
jaeger_collector_spans_dropped_total → dropped spans (!)
SLI/SLO для платформы мониторинга
# Метрики SLO для нашей observability платформы SLOs: - name: "Metrics Ingestion Availability" target: 99.5% # Не 99.9% — сам мониторинг не критический путь sli: "% времени когда Prometheus успешно scrape'ит targets" - name: "Log Search Latency" target: "P95 < 10 seconds" sli: "Время ответа на Loki/Elasticsearch запрос" - name: "Alert Delivery" target: "99.9% алертов доставлены за < 5 минут" sli: "alertmanager_notification_latency < 300s"
Alerting правила для мониторинга
- alert: PrometheusTargetDown expr: up == 0 for: 5m annotations: summary: "Prometheus target {{ $labels.job }} is down" - alert: AlertManagerDown expr: absent(up{job="alertmanager"}) for: 1m severity: critical annotations: summary: "AlertManager is down — we're flying blind!" - alert: ElasticsearchClusterRed expr: elasticsearch_cluster_health_status == 0 for: 2m severity: critical - alert: JaegerSpansDropping expr: rate(jaeger_collector_spans_dropped_total[5m]) > 0 severity: warning annotations: description: "Jaeger dropping {{ $value }} spans/sec. Check storage."
14. Вопросы для самопроверки
-
В чём разница между мониторингом и observability? Почему в микросервисной архитектуре observability важнее, чем в монолите?
-
Объясните три столпа observability (metrics, logs, traces). В каком случае вы бы начали расследование инцидента с каждого из них?
-
Prometheus использует pull-модель. Каковы преимущества и недостатки этого подхода по сравнению с push-моделью? Как решается проблема short-lived jobs?
-
Что такое Cardinality проблема в Prometheus? Приведите пример метрики с высокой cardinality и объясните, почему это опасно.
Подсказка: http_requests_total{user_id="12345"} — хорошо или плохо? -
Опишите tail-based sampling для distributed tracing. Чем он лучше head-based sampling? Какие ресурсные impliсации это несёт?
-
Рассчитайте error budget: Если ваш SLO = 99.95% за 30 дней, и в прошлом месяце было 3 инцидента общей длительностью 15 минут — сколько error budget осталось?
-
Что такое burn rate алерты? Почему они лучше, чем алерты по абсолютному значению error rate?
-
Вам нужно масштабировать систему логирования с 10 GB/день до 10 TB/день. Какие компоненты станут узким местом первыми? Как будете масштабировать?
-
Distributed tracing требует instrumentation всех сервисов. Как организовать rollout трейсинга в компании с 200 микросервисами, написанными на разных языках?
-
Ваша система мониторинга падает. Какие механизмы вы заложили, чтобы узнать об этом? Опишите концепцию "meta-monitoring" и "dead man's switch".
15. Дополнительные ресурсы
Papers и академические работы
- "Dapper, a Large-Scale Distributed Systems Tracing Infrastructure" (Google, 2010) — оригинальная paper, заложившая основы distributed tracing: research.google/pubs/dapper
- "Gorilla: A Fast, Scalable, In-Memory Time Series Database" (Facebook, 2015) — алгоритм сжатия, используемый в Prometheus: vldb.org
- "The Site Reliability Workbook" (Google, 2018) — практическое руководство по SRE, SLO, Error Budgets
Официальная документация
- Prometheus: prometheus.io/docs — особенно разделы про TSDB internals и best practices
- OpenTelemetry: opentelemetry.io — стандарт для instrumentation
- Grafana Loki: grafana.com/docs/loki — документация по LogQL
- Jaeger: jaegertracing.io/docs — архитектура и настройка
Блоги инженерных команд
- Uber Engineering: "M3 and Prometheus, Monitoring at Scale" — как Uber перешёл с proprietary системы на M3+Prometheus
- Netflix Tech Blog: "Edgar: Solving Mysteries Faster with Observability" — Netflix observability platform
- Cloudflare Blog: "How we analyze Cloudflare's dataplane performance" — обработка петабайт телеметрии
- Grafana Blog: "Grafana Loki: Like Prometheus, but for Logs" — дизайн и архитектура Loki
- Google SRE Blog: "Alerting on SLOs like Prometheus and Grafana can" — multi-window burn rate алертинг
Talks (конференции)
- KubeCon 2023: "OpenTelemetry: The State of the Art" — текущее состояние OpenTelemetry
- PromCon: ежегодная конференция, все доклады на YouTube
- SREcon: "Error Budget Policies at Google" — как Google использует error budgets на практике
- Observability Con (Grafana): практические кейсы внедрения observability
Инструменты для практики
# Локальный стенд для экспериментов с полным стеком docker-compose up -d prometheus grafana alertmanager jaeger loki # Репозиторий с примерами: grafana/intro-to-mltp # (Metrics, Logs, Traces, Profiles — all in one demo) git clone https://github.com/grafana/intro-to-mltp
Итоги главы
В этой главе мы рассмотрели полный стек observability — от теоретических основ до практических деталей реализации:
Ключевые выводы:
-
Observability vs Monitoring — observability позволяет задавать произвольные вопросы о системе, мониторинг — только заранее предусмотренные.
-
Три столпа — метрики, логи и трейсы дополняют друг друга: метрики показывают "что", логи — "почему", трейсы — "где именно".
-
Prometheus + Thanos — де-факто стандарт для метрик в cloud-native среде с возможностью масштабирования до глобального уровня.
-
OpenTelemetry — инвестируйте в vendor-neutral instrumentation, чтобы не оказаться в lock-in.
-
Symptom-based alerting — алертируйте на симптомы (высокий error rate, высокая latency), а не причины (CPU, память). Пользователей не волнует CPU.
-
Error Budgets — переводят абстрактную надёжность в конкретный "бюджет", который инженеры и бизнес могут совместно расходовать на риски и скорость разработки.
-
Масштаб — при объёме 10M+ samples/sec выбор хранилища, sampling стратегии и retention policy имеет огромное финансовое и операционное значение.