Откуда берется распределенный монолит
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Распределённый монолит: происхождение и природа
Распределённый монолит (Distributed Monolith) — это архитектурный антипаттерн, где система выглядит как микросервисы, но функционирует как тесно связанный монолит. Это наихудшее из обоих миров: вся сложность распределённых систем без её преимуществ.
Как он возникает
1. Неправильное разделение на микросервисы
Обычно история развития выглядит так:
┌─────────────────────────────────────────────────┐
│ Этап 1: Монолитная архитектура │
│ - Один процесс, одна БД │
│ - Просто масштабировать (горизонтально) │
│ - Быстрое разработка │
└─────────────────────────────────────────────────┘
↓ рост проекта
┌─────────────────────────────────────────────────┐
│ Этап 2: Попытка перейти на микросервисы │
│ - Команда слышала о масштабируемости │
│ - Начали делить монолит "по слоям" (User, │
│ Order, Payment сервисы) │
│ - Забыли про слабую связанность │
└─────────────────────────────────────────────────┘
↓ результат
┌─────────────────────────────────────────────────┐
│ Распределённый монолит │
│ - Много сервисов, много RPC вызовов │
│ - Синхронные зависимости повсюду │
│ - Сложность отладки, низкая надёжность │
└─────────────────────────────────────────────────┘
2. Синхронные зависимости между сервисами
Вот типичный случай:
# UserService
@app.post("/users")
def create_user(user_data):
user = User.create(**user_data)
# 🔴 СИНХРОННЫЙ вызов к другому сервису!
response = requests.post(
"http://payment-service/setup-account",
json={"user_id": user.id}
)
# Если payment-service упал, упадёт и этот сервис
if response.status_code != 200:
raise Exception("Payment setup failed")
return user
Это создаёт цепь зависимостей, которая превращает всю систему в монолит:
- UserService зависит от PaymentService
- PaymentService зависит от NotificationService
- NotificationService зависит от EmailService
- И так далее...
Результат: если упадёт любой сервис в цепи, упадёт вся система.
3. Недостаточная обработка отказов
# ❌ Плохо: нет обработки timeout/retry
def get_user_with_orders(user_id):
user = db.get_user(user_id)
# Если OrderService медленный, зависнет весь запрос
orders = requests.get(f"http://order-service/{user_id}").json()
return {"user": user, "orders": orders}
# ✅ Хорошо: timeout, retry, fallback
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential())
def get_orders_with_retry(user_id, timeout=2):
try:
return requests.get(
f"http://order-service/{user_id}",
timeout=timeout
).json()
except requests.Timeout:
# Fallback: вернуть пустой список
return []
4. Общая база данных
Много "микросервисов" оказываются в одной БД:
-- Одна БД для всех!
CREATE TABLE users (...);
CREATE TABLE orders (...);
CREATE TABLE payments (...);
CREATE TABLE notifications (...);
Это полностью уничтожает независимость сервисов:
- Изменение схемы в одной таблице может сломать другой сервис
- Блокировки в БД влияют на всех
- Невозможно использовать разные БД (PostgreSQL, MongoDB)
- Невозможно масштабировать отдельные сервисы
Признаки распределённого монолита
- 🔴 Синхронные REST/RPC вызовы между сервисами
- 🔴 Одна общая БД для всех микросервисов
- 🔴 Общие библиотеки, которые обновляются вместе
- 🔴 Если упадёт один сервис, система деградирует
- 🔴 Логирование и трейсинг разбросаны по сервисам
- 🔴 Версионирование API не продумано
- 🔴 Невозможно развёртывать сервисы независимо
Как правильно делать микросервисы
1. Слабая связанность через async
# ✅ Асинхронная коммуникация
from celery import shared_task
@shared_task
def setup_payment_account(user_id):
"""Это выполнится в фоне, независимо от создания user"""
payment_service.setup_account(user_id)
# В UserService:
@app.post("/users")
def create_user(user_data):
user = User.create(**user_data)
# Просто отправляем задачу, не ждём результата
setup_payment_account.delay(user.id)
return user
2. Event-driven архитектура
# Используем message queue (RabbitMQ, Kafka)
from kombu import Connection, Exchange, Queue
# UserService публикует событие
user_created_event = {
"event_type": "user.created",
"user_id": user.id,
"email": user.email,
"timestamp": datetime.now().isoformat()
}
with Connection("amqp://localhost") as conn:
exchange = Exchange("users", type="topic")
exchange.publish(user_created_event, routing_key="user.created")
# PaymentService и NotificationService слушают независимо
# Если одного нет — user все равно создан
3. Database per service
UserService
└─ PostgreSQL (users table)
OrderService
└─ PostgreSQL (orders table)
PaymentService
└─ MongoDB (payments, transactions)
NotificationService
└─ Redis (message queue)
Каждый сервис полностью независим в плане хранилища.
4. Обработка отказов
# Circuit Breaker паттерн
from circuitbreaker import circuit
@circuit(failure_threshold=5, recovery_timeout=60)
def call_payment_service(user_id):
return requests.post("http://payment-service/setup", json={"user_id": user_id})
try:
call_payment_service(user_id)
except CircuitBreakerListener:
# Payment service недоступен, но мы продолжаем работу
log.warning(f"Payment service unavailable, using fallback")
# Используем retry/fallback стратегию
Почему распределённый монолит появляется
- Неправильное разделение ответственности: делят по слоям (User, Order), а не по bounded contexts
- Отсутствие async стека: нет Celery, RabbitMQ, Kafka
- Коммуникация по умолчанию синхронная: привычка из монолита (прямой вызов функций)
- Недоумение о независимости: не понимают разницы между монолитом и микросервисами
- Спешка: "быстро разделим и готово" — без планирования
Выводы
Распределённый монолит — результат копирования архитектурного паттерна без понимания его суути. Правильные микросервисы требуют:
- Независимой разработки: каждый сервис развёртывается отдельно
- Асинхронной коммуникации: через события, не RPC
- Слабой связанности: максимум повторения кода, минус координация
- Отказоустойчивости: circuit breakers, timeouts, fallbacks
- Dedicated databases: каждый сервис хранит свои данные
Если этого нет — это не микросервисы, а распределённый монолит.