Главная Веб-разработка Context Collapse: как микросервисы могут сойти с ума

Context Collapse: как микросервисы могут сойти с ума

от admin

Даже самая идеальная микросервисная архитектура может упасть. В статье обсудим зарубежный материал, где автор рассказывает о проблеме 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.
  • Дублирующиеся действия: повторные списания, задвоенные отправки писем, неожиданные повторные заказы. Пользователи возмущены, рейтинг падает.
  • Мистические баги: логи говорят «всё в порядке», но результат явно неверный. Код не видит проблему, а отладка превращается в кошмар.

Давайте рассмотрим на примере. Клиент решает перевести деньги с одного счёта на другой. Сервис транзакций отправляет запрос в модуль проверки лимитов, затем передаёт его в систему обработки платежей. Но из-за потери контекста на одном из этапов модуль проверки лимитов теряет информацию о сессии клиента и воспринимает его как нового пользователя. В результате:

  • Лимиты не распознаются, и транзакция блокируется, даже если у пользователя достаточно денег.
  • Либо, наоборот, проверка пропускается, и клиенту позволяют перевести больше лимита.
  • Сервис обработки платежей не получает подтверждение проверки и отправляет повторный запрос, а это может привести к двойному списанию средств.
Читать также:
Популярная ИИ-библиотека на Python Ultralytics заразилась криптомайнером — Tproger

Клиент идет громить службу поддержки, она — разработчиков, а они размахивают руками, потому что в логах все отлично.

Опрос 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 к сервисам — сможет ли система восстановится в таком случае? Если нет, значит, ваша передача контекста недостаточно надежна.

Эти решения не универсальны. Начните с правильного распространения контекста для быстрых улучшений, затем добавьте кэширование или событийную архитектуру для масштабируемости и постоянно аудируйте систему.

Похожие статьи