Даже самая идеальная микросервисная архитектура может упасть. В статье обсудим зарубежный материал, где автор рассказывает о проблеме Context Collapse.
024 открытий73 показов
В феврале на Medium вышла статья Context Collapse: The Silent Microservices Killer. Перевели ее для вас, так как тема довольно редкая и интересная. Ниже предлагаем обсудить проблему контекста и как он может упасть.
Что вообще такое context collapse
Есть вероятность, что вы слышите этот термин в первый раз. Немного лирики: в мире микросервисов есть два понятия: технический контекст и доменный контекст (Bounded Context).
Технический контекст
Технический контекст — это информация, которая передается между микросервисами для выполнения запросов или операций. В нем лежит большое количество данных: идентификаторы запросов (например, TraceID в трассировке), пользовательские сессии, метаданные или параметры аутентификации.
Без технического контекста микросервисы не могут:
- отслеживать, откуда пришел запрос и куда он направляется,
- сохранять согласованность данных между сервисами,
- выявлять сбои (например, через логи или трассировку).
Доменный контекст (Bounded Context)
Доменный контекст происходит из методологии Domain-Driven Design (DDD) — это про четкое разделение бизнес-логики между микросервисами. У каждого сервиса должна быть своя «ограниченная область» (Bounded Context), где термины, данные и правила имеют уникальное значение.
Например, в интернет-магазине слово «заказ» может означать разные вещи для сервиса оплаты (финансовая транзакция) и сервиса доставки (отправка покупателю). Если границы контекста не определены четко, возникает путаница: сервисы начинают дублировать логику или интерпретировать данные по-разному.
Без доменного контекста:
- код будет дублироваться и появится избыточность,
- усложнится взаимодействие между сервисами,
- систему будет сложнее поддерживать.
Вернемся к коллапсу контекста
Автор статьи просит нас представить следующую картину: вы сделали новую архитектуру, она масштабируется, разделяется, деплои проходят гладко, CI/CD пайплайны цветут и пахнут — другими словами, не архитектура, а мечта. Все работает как часы, поэтому вы сидите, попивая кофе и раскладывая пасьянс.
Вдруг приходит пользователь и сообщает, что платеж был проведен дважды. На панели мониторинга появляются задержки, и логи здесь вообще бесполезны. Вы начинаете разбираться и спустя несколько часов споров с терминалом, находите причину: один из сервисов забыл, кто вообще такой этот пользователь, прямо во время проведения транзакции.
Тра-та-та, это и есть контекстный коллапс. Как называет его автор — тихий убийца микросервисов в 2025 году. Поймать этот баг с помощью breakpoints нельзя, при этом он будет уничтожать вашу производительность и код в целом.
Что на самом деле происходит
Представьте, что микросервисы — это эстафетный забег: каждый сервис передает «эстафетную палочку» — идентификаторы пользователей, сессионные данные, намерения — следующему участнику. Контекстный коллапс случается, когда эта палочка выпадает на бегу.
Сервис теряет важное состояние, например:
- «Это корзина Пети»
- “Этот платеж уже прошел”
И начинается хаос: дублирующиеся API-запросы, потерянные транзакции или, что еще хуже, повреждение данных.
В 2025 году появляется все больше слишком фрагментированных архитектур и гипермасштабируемых систем. Ваши запросы могут проходить через 10, 20 и даже 30 сервисов, но если на каком-то этапе отвалился контекст, то это конец.
В статье автор не делает акцента на том, какой именно это контекст — технический или доменный. На самом деле может произойти все что угодно: это может быть как потеря технического контекста, так и нарушение доменных границ (когда сервисы лезут не в свое дело). Оба случая приводят к хаосу: данные становятся несогласованными, ошибки множатся, а разработчики теряют контроль над системой.
Как понять, что контекст вот-вот может «коллапснуться»
Вы наверняка были в такой ситуации: вы на 100% уверены, что система должна работать, но она не работает, хотя ошибок в коде никаких, казалось бы, нет. Вот основные признаки коллапса, с которыми вы можете столкнуться:
- Всплеск пинга: сервисы запрашивают данные, которые должны бы уже знать, создавая огромное количество лишних вызовов и перегружая API.
- Дублирующиеся действия: повторные списания, задвоенные отправки писем, неожиданные повторные заказы. Пользователи возмущены, рейтинг падает.
- Мистические баги: логи говорят «всё в порядке», но результат явно неверный. Код не видит проблему, а отладка превращается в кошмар.
Давайте рассмотрим на примере. Клиент решает перевести деньги с одного счёта на другой. Сервис транзакций отправляет запрос в модуль проверки лимитов, затем передаёт его в систему обработки платежей. Но из-за потери контекста на одном из этапов модуль проверки лимитов теряет информацию о сессии клиента и воспринимает его как нового пользователя. В результате:
- Лимиты не распознаются, и транзакция блокируется, даже если у пользователя достаточно денег.
- Либо, наоборот, проверка пропускается, и клиенту позволяют перевести больше лимита.
- Сервис обработки платежей не получает подтверждение проверки и отправляет повторный запрос, а это может привести к двойному списанию средств.
Клиент идет громить службу поддержки, она — разработчиков, а они размахивают руками, потому что в логах все отлично.
Опрос CNCF за 2024 год показал, что 68% команд, которые используют микросервисы, сталкиваются с необъяснимыми проблемами производительности. И всему виной в том числе контекстный коллапс.
5 решений, как бороться с контекстным коллапсом
Да, контекстный коллапс — крайне неприятный баг. Главное — чтобы все сервисы работали слаженно и «знали» друг друга.
1. Правильно передавайте контекст
Вам нужно передавать критические важные данные — идентификаторы пользователей, состояние транзакций, метаданные запроса — в каждом шаге цепочки. Стоит использовать OpenTelemetry, чтобы распространять контекст по сервисам.
Вот пример реализации middleware автором на Go:
package main import ( "context" "net/http" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) func contextMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tracer := otel.Tracer("my-service") ctx, span := tracer.Start(r.Context(), "handle-request") defer span.End() // Извлекаем контекст (например, ID) из заголовков userID := r.Header.Get("X-User-ID") span.SetAttributes(attribute.String("user_id", userID)) // Передаем контекст дальше r = r.WithContext(ctx) next.ServeHTTP(w, r) }) } func main() { mux := http.NewServeMux() mux.HandleFunc("/process", func(w http.ResponseWriter, r *http.Request) { span := trace.SpanFromContext(r.Context()) userID := span.SpanContext().TraceID().String() // Получаем доступ к контексту w.Write([]byte("Processing for user: " + userID)) }) handler := contextMiddleware(mux) http.ListenAndServe(":8080", handler) }
2. Кэшируйте с умом
Нет смысла заставлять сервисы угадывать, если они просто могут запомнить. Быстрый кэш, например, с Redis, может хранить временный контекст — например, токены сеанса или состояния запросов, — поэтому сервисы не будут постоянно спрашивать «кто это?» Вот фрагмент кода на Питоне, где используется Redis для хранения и извлечения контекста:
import redis import json from flask import Flask, request app = Flask(__name__) cache = redis.Redis(host='localhost', port=6379, db=0) @app.route('/checkout', methods=['POST']) def checkout(): request_id = request.headers.get('X-Request-ID') user_data = { 'user_id': request.json.get('user_id'), 'cart': request.json.get('cart') } # Сохранение контекста в Redis с TTL 5 минут cache.setex(request_id, 300, json.dumps(user_data)) # Передача данных downstream-сервису return {"message": "Checkout started", "request_id": request_id} @app.route('/payment', methods=['POST']) def payment(): request_id = request.headers.get('X-Request-ID') cached = cache.get(request_id) if cached: user_data = json.loads(cached) return {"message": f"Payment for {user_data['user_id']} processed"} return {"error": "Context lost"}, 400 if __name__ == "__main__": app.run(port=5000)
Это позволяет поддерживать контекст во всех вызовах, не перегружая вашу базу данных. А еще TTL (time-to-live) Redis сам за собой убирает — устаревшие данные не скрываются.
3. Используйте Event-Driven Architecture
Микросервисы без сохранения состояния выглядят отлично, пока не контекст не коллапснется. Событийная архитектура идет от обратного: вместо того чтобы надеяться, что службы все запомнят, регистрируйте каждый шаг как событие. Если служба все-таки забудет, воспроизведите поток. Вот пример на Node.js с Kafka:
const { Kafka } = require('kafkajs'); const kafka = new Kafka({ clientId: 'order-service', brokers: ['localhost:9092'] }); const producer = kafka.producer(); const consumer = kafka.consumer({ groupId: 'payment-group' }); async function logEvent(event) { await producer.connect(); await producer.send({ topic: 'order-events', messages: [{ value: JSON.stringify(event) }] }); await producer.disconnect(); } async function processPayment() { await consumer.connect(); await consumer.subscribe({ topic: 'order-events', fromBeginning: true }); await consumer.run({ eachMessage: async ({ message }) => { const event = JSON.parse(message.value); if (event.type === 'checkout_started') { console.log(`Processing payment for user: ${event.user_id}`); // Обработчик платежа } } }); } // Запуск события logEvent({ type: 'checkout_started', user_id: 'user123', cart: ['item1'] }); // Запуск обработки событий processPayment();
Здесь также можно использовать RabbitMQ. В общем, больше никаких отговорок со стороны сервиса из разряда «я забыл».
4. Проверяйте логи
Когда контекст теряется, логи — ваше место преступления. Настройте Grafana Loki или Datadog для поиска «потерянных контекстов». Вот пример с Grafana:
#docker-compose.yml version: '3' services: app: image: my-microservice logging: driver: loki options: loki-url: "http://localhost:3100/loki/api/v1/push" loki-labels: "service=app,env=prod" loki: image: grafana/loki:latest ports: - "3100:3100"
Тэгните логи с помощью request_id или user_id, а затем вызовите Loki:
{service="app"} |~ "context lost"
5. Тестируйте на коллапсы
Профилактика лучше, чем лечение. Добавьте хаос-тестирование с помощью, например, Chaos Mesh, чтобы моделировать потери контекста.
Хаос-тестирование — это метод преднамеренного введения сбоев в систему, чтобы проверить, насколько она устойчива и надежна. Обычное тестирование и мониторинг могут выявить проблемы, но хаос-тестирование (Chaos Engineering) помогает увидеть, как система поведет себя в случае неожиданных отказов.
Прервите mid-requests к сервисам — сможет ли система восстановится в таком случае? Если нет, значит, ваша передача контекста недостаточно надежна.
Эти решения не универсальны. Начните с правильного распространения контекста для быстрых улучшений, затем добавьте кэширование или событийную архитектуру для масштабируемости и постоянно аудируйте систему.