Расскажи про доставку сообщений в распределенных системах

«Расскажи про доставку сообщений в распределенных системах» — вопрос из категории Распределенные системы, который задают на 33% собеседований Data Инженер. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

При проектировании микросервисных и event-driven архитектур я сталкивался с необходимостью выбора модели доставки сообщений. Это фундаментальный выбор, влияющий на отказоустойчивость и консистентность всей системы.

Основные модели гарантий доставки:

  1. At-most-once (максимум один раз): Сообщение может быть потеряно. Это модель по умолчанию для многих систем, когда скорость важнее надежности. Использовал, например, для отправки метрик или логов, где потеря отдельных данных некритична.
  2. At-least-once (минимум один раз): Гарантируется, что сообщение будет доставлено, но возможны дубликаты из-за повторных отправок при сбоях. Это самая распространенная модель в моих проектах. Для обработки дублей на стороне потребителя необходимо реализовывать идемпотентность.
  3. Exactly-once (ровно один раз): Сложная модель, требующая координации между продюсером, брокером и консьюмером. На практике она часто реализуется как идемпотентный продюсер + транзакции + идемпотентный консьюмер.

Пример из практики с Apache Kafka: Мы строили финансовый пайплайн, где дублирование транзакций было недопустимо. Использовали комбинацию настроек в Kafka для достижения семантики exactly-once в рамках одного приложения.

// Конфигурация продюсера на Java
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker:9092");
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true"); // Включаем идемпотентность
props.put(ProducerConfig.ACKS_CONFIG, "all"); // Ждем подтверждения от всех реплик
props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "my-transactional-id"); // Для транзакций

KafkaProducer<String, String> producer = new KafkaProducer<>(props);

producer.initTransactions(); // Инициализация транзакций

try {
    producer.beginTransaction();
    // Отправка нескольких сообщений в рамках одной транзакции
    producer.send(new ProducerRecord<>("orders", "order-123", "{...}"));
    producer.send(new ProducerRecord<>("audit-log", "audit-123", "{...}"));
    producer.commitTransaction(); // Атомарный коммит всех отправленных сообщений
} catch (Exception e) {
    producer.abortTransaction(); // Откат всей пачки в случае ошибки
    throw e;
}

С какими проблемами сталкивался:

  • Сетевые задержки и таймауты: Приводили к ложным ретраям и дублям. Решение — тщательная настройка таймаутов и использование экспоненциального отката (exponential backoff).
  • Нарушение порядка доставки при ретраях: В Kafka порядок гарантируется в пределах одного партишна. При использовании max.in.flight.requests.per.connection=1 (для at-least-once) порядок сохраняется, но падает производительность.
  • Распределенный консенсус: Для управления метаданными (например, офсетами консьюмеров или состоянием транзакций) брокеры используют Apache ZooKeeper (или KRaft в новых версиях Kafka), который реализует алгоритмы консенсуса, такие как ZAB.