Блог AST-SoftPro
Надёжность системы: мониторинг, алертинг, логирование
Надёжность системы: мониторинг, алертинг, логирование
Надёжная система — это не система, которая никогда не ломается. Это система, которая быстро обнаруживает проблемы, уведомляет команду и восстанавливается с минимальным влиянием на пользователей. В этой статье разберём три кита надёжности: мониторинг, алертинг и логирование.
Три столпа наблюдаемости
Наблюдаемость (observability) — способность понять внутреннее состояние системы по внешним данным. Три столпа:
-
Метрики — числовые показатели (RPS, latency, error rate)
-
Логи — текстовые записи о событиях
-
Трейсы — путь запроса через систему
Метрики: что измерять
Без метрик вы не знаете, что происходит в системе. Основные метрики — RED и USE.
RED-метрики (для сервисов):
-
Rate — количество запросов в секунду
-
Errors — количество ошибок
-
Duration — время выполнения запроса
USE-метрики (для ресурсов):
-
Utilization — насколько ресурс загружен
-
Saturation — насколько ресурс близок к пределу
-
Errors — ошибки ресурса
Собираем метрики на Python:
from prometheus_client import Counter, Histogram, Gauge, start_http_server
import time
# Запуск Prometheus exporter
start_http_server(8000)
# Счётчик запросов
REQUEST_COUNT = Counter(
"http_requests_total",
"Total HTTP requests",
["method", "endpoint", "status"],
)
# Гистограмма времени выполнения
REQUEST_DURATION = Histogram(
"http_request_duration_seconds",
"HTTP request duration",
["method", "endpoint"],
buckets=[0.01, 0.05, 0.1, 0.5, 1.0, 5.0],
)
# Счётчик ошибок
ERROR_COUNT = Counter(
"http_errors_total",
"Total HTTP errors",
["method", "endpoint", "error_type"],
)
# Использование памяти воркеров
WORKER_MEMORY = Gauge(
"worker_memory_bytes",
"Worker memory usage",
["worker_id"],
)
# Middleware для FastAPI
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
app = FastAPI()
class MetricsMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
start = time.time()
try:
response = await call_next(request)
duration = time.time() - start
REQUEST_COUNT.labels(
method=request.method,
endpoint=request.url.path,
status=response.status_code,
).inc()
REQUEST_DURATION.labels(
method=request.method,
endpoint=request.url.path,
).observe(duration)
return response
except Exception as e:
duration = time.time() - start
ERROR_COUNT.labels(
method=request.method,
endpoint=request.url.path,
error_type=type(e).__name__,
).inc()
raise
app.add_middleware(MetricsMiddleware)
Grafana: визуализация метрик
Grafana — стандартный инструмент для визуализации метрик Prometheus. Дашборды позволяют видеть состояние системы в реальном времени.
Ключевые дашборды:
-
Overview — общая картина: RPS, ошибки, latency по всем сервисам
-
Детали сервиса — метрики конкретного сервиса
-
Infrastructure — CPU, память, диск, сеть
-
Business — заказы, пользователи, конверсия
Пример алерта в Grafana:
# Alert: High Error Rate
Condition: http_errors_total / http_requests_total > 0.05
For: 5m
Severity: critical
Логирование: структурированные логи
Структурированные логи (в JSON-формате) — основа отладки. Они позволяют фильтровать, агрегировать и искать по логам.
Структурированное логирование на Python:
import logging
import json
from logging import Handler
class JsonFormatter(logging.Formatter):
def format(self, record):
log_entry = {
"timestamp": self.formatTime(record),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
"module": record.module,
"function": record.funcName,
"line": record.lineno,
}
if hasattr(record, "request_id"):
log_entry["request_id"] = record.request_id
if hasattr(record, "user_id"):
log_entry["user_id"] = record.user_id
if record.exc_info:
log_entry["exception"] = self.formatException(record.exc_info)
return json.dumps(log_entry, ensure_ascii=False)
# Настройка логгера
logger = logging.getLogger("shop")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logger.addHandler(handler)
# Использование
logger.info("Order created", extra={"request_id": "abc-123", "user_id": 42})
logger.error("Payment failed", extra={"order_id": 123, "error": "timeout"})
Уровни логирования:
| Уровень | Когда использовать | Пример |
|---|---|---|
| DEBUG | Детальная отладка | Значения переменных |
| INFO | Нормальная работа | Заказ создан, пользователь вошёл |
| WARNING | Необычное, но не критичное | Кэш пропущен, медленный запрос |
| ERROR | Ошибка в операции | Платёж не прошёл, email не отправлен |
| КРИТИЧЕСКИЙ | Сбой системы | База данных недоступна |
Circuit Breaker: защита от каскадных сбоев
Circuit breaker — паттерн, который предотвращает повторные запросы к неработающему сервису. Если сервис не отвечает, circuit breaker «открывает» и перестаёт отправлять запросы на заданное время.
Реализация circuit breaker на Python:
import time
from enum import Enum
class CircuitState(Enum):
CLOSED = "closed" # Нормальная работа
OPEN = "open" # Сервис недоступен, запросы блокируются
HALF_OPEN = "half_open" # Проверка: один запрос для проверки
class CircuitBreaker:
def __init__(self, name: str, failure_threshold: int = 5,
recovery_timeout: int = 60, half_open_max: int = 1):
self.name = name
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.half_open_max = half_open_max
self.state = CircuitState.CLOSED
self.failure_count = 0
self.success_count = 0
self.last_failure_time = None
self.half_open_requests = 0
def can_execute(self) -> bool:
if self.state == CircuitState.CLOSED:
return True
if self.state == CircuitState.OPEN:
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = CircuitState.HALF_OPEN
self.half_open_requests = 0
return True
return False
# HALF_OPEN
if self.half_open_requests < self.half_open_max:
self.half_open_requests += 1
return True
return False
def record_success(self):
if self.state == CircuitState.HALF_OPEN:
self.success_count += 1
if self.success_count >= self.half_open_max:
self.state = CircuitState.CLOSED
self.failure_count = 0
self.success_count = 0
else:
self.failure_count = 0
def record_failure(self):
self.failure_count += 1
self.last_failure_time = time.time()
if self.state == CircuitState.HALF_OPEN:
self.state = CircuitState.OPEN
elif self.failure_count >= self.failure_threshold:
self.state = CircuitState.OPEN
logger.warning(
f"Circuit breaker {self.name} OPEN "
f"(failures: {self.failure_count})"
)
# Использование
payment_circuit = CircuitBreaker("payment-service")
@app.post("/orders/pay")
def pay_order(order_id: int):
if not payment_circuit.can_execute():n raise HTTPException(
503, "Payment service temporarily unavailable"
)
try:
result = payment_service.charge(order_id)
payment_circuit.record_success()
return result
except Exception as e:
payment_circuit.record_failure()
raise HTTPException(502, "Payment processing failed")
Retry с exponential backoff
Повторные попытки с экспоненциальной задержкой — стандартный подход для временных сбоев.
import time
import random
def retry_with_backoff(func, max_retries: int = 3, base_delay: float = 1.0):
"""Retry с экспоненциальной задержкой и джиттером"""
for attempt in range(max_retries):
try:
return func()
except Exception as e:
if attempt == max_retries - 1:
raise
# Exponential backoff + jitter
delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
logger.warning(
f"Retry {attempt + 1}/{max_retries} "
f"after {delay:.1f}s: {e}"
)
time.sleep(delay)
# Использование
result = retry_with_backoff(
lambda: payment_service.charge(order_id),
max_retries=3,
base_delay=2.0,
)
Алертинг: когда будить команду
Алертинг — это уведомления о проблемах. Правильный алертинг — когда команда получает только важные уведомления.
Правила алертинга:
-
Алерт = действие. Если алерт не требует действия — это не алерт, а дашборд.
-
P0 — немедленно. Сбой сервиса, потеря данных, недоступность базы.
-
P1 — в течение часа. Деградация производительности, рост ошибок.
-
P2 — завтра. Предупреждения о приближении лимитов.
Примеры алертов:
| Алерт | Условие | Уровень |
|---|---|---|
| Сервис недоступен | HTTP 5xx > 50% за 5 минут | P0 |
| Высокая latency | P95 > 2 секунд за 10 минут | P1 |
| Очередь растёт | Длина очереди > 1000 за 15 минут | P1 |
| Диск заполняется | Использование диска > 85% | P2 |
| Память Redis | Использование > 90% | P2 |
Частые ошибки
-
Отсутствие мониторинга. Деплой без дашбордов — это полёт вслепую. Мониторинг настраивается до продакшена.
-
Слишком много алертов. 100 алертов в день — это не мониторинг, это шум. Команда перестает реагировать.
-
Логи без request_id. Без request_id невозможно отследить запрос через несколько сервисов.
-
Circuit breaker без half-open. Без half-open состояния circuit breaker никогда не восстановится автоматически.
-
Retry без backoff. Мгновенные повторные попытки создают лавину запросов к и без того перегруженному сервису.
Заключение
Надёжность системы — это не один инструмент, а комплекс мер: метрики для понимания состояния, логи для отладки, алерты для быстрого реагирования, circuit breaker и retry для устойчивости к сбоям.
Начинайте с простого: Prometheus + Grafana для метрик, структурированные логи в JSON, базовые алерты. Усложняйте по мере роста системы.
Ключевые моменты:
-
Три столпа наблюдаемости: метрики (Prometheus), логи (JSON), трейсы (OpenTelemetry).
-
RED-метрики для сервисов, USE-метрики для ресурсов.
-
Circuit breaker защищает от каскадных сбоев — обязательный паттерн для микросервисов.
-
Retry с exponential backoff — для временных сбоев, без джиттера.
-
Алерт = действие. Если алерт не требует реакции — это дашборд, а не алерт.