Перетяжка, Карта дня
Перетяжка, Карта дня
Перетяжка, Карта дня

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

Даже самая идеальная микросервисная архитектура может упасть. В статье обсудим зарубежный материал, где автор рассказывает о проблеме Context Collapse.

841 открытий6К показов
Context Collapse: как микросервисы могут сойти с ума

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

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