← Назад к вопросам

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

1.0 Junior🔥 121 комментариев
#Apache Kafka и потоковая обработка

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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

Доставка сообщений — одна из самых сложных проблем в распределенных системах. Вот подробный обзор гарантий доставки, их имплементации и trade-offs.

Три типа гарантий доставки

1. At-Most-Once (максимум один раз)

Сообщение доставляется 0 или 1 раз, но может быть потеряно.

Производитель -> Шина -> Потребитель
     |             |         |
   Отправил        Передал   Обработал
                                |
                            Сбой: сообщение потеряно

Когда использовать:

  • Логи неполной информации (потеря одного лога не критична)
  • Метрики, которые усредняются (потеря одного значения не страшна)
  • Аналитика, где 99% точность достаточна

Недостатки:

  • Потеря данных при сбоях
  • Не подходит для критичных операций

2. At-Least-Once (минимум один раз)

Сообщение доставляется 1 или более раз. Может быть дубликат, но не потеряется.

Производитель -> Шина -> Потребитель
                  |          |
             Persists    Ack получен
                          |
                      Но потребитель сбойнулся
                          |
                    Перепослать -> Дубликат

Как это работает:

  1. Производитель посылает сообщение и ждет ack
  2. Если ack не пришел, отправляет повторно
  3. Потребитель может получить дубликат

Когда использовать:

  • Финансовые транзакции (дубликаты можно дедупликировать)
  • Критичные события
  • Charge в платежных системах

Реализация в Kafka:

from kafka import KafkaProducer

producer = KafkaProducer(
    bootstrap_servers=['localhost:9092'],
    acks='all',  # Ждем ack от всех репликас
    retries=3,   # Переотправляем при сбое
    compression_type='snappy'
)

future = producer.send('orders', b'{"order_id": 123}')
try:
    record_metadata = future.get(timeout=10)
    print(f"Сообщение отправлено на partition {record_metadata.partition}")
except KafkaError as e:
    print(f"Ошибка отправки: {e}")

3. Exactly-Once (ровно один раз)

Сообщение доставляется ровно один раз — это идеал, но очень дорого в реализации.

Производитель -> Шина -> Потребитель
    |             |          |
  Отправил    Persists   Обработал & Сохранил offset
                               |
                          Атомарно: результат и offset
                          в один транзакции

Когда использовать:

  • Денежные переводы (ошибка = потеря денег)
  • Критичные счетчики (количество билетов)
  • Системы с SLA в 99.99%+

Стоимость:

  • Транзакции требуют синхронизации
  • Снижает throughput на 50-90%
  • Усложняет архитектуру

Реализация в Kafka (Transactional Writes):

from kafka import KafkaProducer
from kafka.errors import KafkaError

producer = KafkaProducer(
    bootstrap_servers=['localhost:9092'],
    transactional_id='my-producer-1',
    acks='all'
)

try:
    with producer.transaction():
        future = producer.send('orders', b'{"order_id": 123}')
        # Если здесь ошибка — вся транзакция откатывается
        record_metadata = future.get(timeout=10)
        # Сообщение видно потребителям только после commit
except KafkaError as e:
    # Транзакция откатывается автоматически
    print(f"Ошибка в транзакции: {e}")
finally:
    producer.close()

Дедупликация для достижения Exactly-Once

Если система гарантирует at-least-once, можно реализовать exactly-once через дедупликацию:

-- Пример: таблица с идемпотентными операциями
CREATE TABLE processed_messages (
    message_id UUID PRIMARY KEY,
    body JSON NOT NULL,
    processed_at TIMESTAMP NOT NULL
);

-- Потребитель:
-- 1. Проверяет, есть ли message_id
-- 2. Если нет — обрабатывает и вставляет
-- 3. Если есть — пропускает

INSERT INTO processed_messages (message_id, body, processed_at)
VALUES (
    '550e8400-e29b-41d4-a716-446655440000',
    '{"order_id": 123}',
    NOW()
)
ON CONFLICT (message_id) DO NOTHING;

Trade-offs между гарантиями

ГарантияПотеря данныхДубликатыLatencyThroughputСложность
At-most-onceДаНетНизкаяВысокийНизкая
At-least-onceНетВозможныСредняяСреднийСредняя
Exactly-onceНетНетВысокаяНизкийВысокая

Факторы, влияющие на доставку

1. Сетевые сбои

Производитель отправил -> Сетевая ошибка -> Потребитель не получил
Решение: retry с exponential backoff

2. Сбой потребителя

Потребитель получил -> Обработал -> Упал перед сохранением offset
Решение: сохранить offset ПОСЛЕ обработки

3. Раздел (partition) недоступен

Кафка: у нас 3 репликаса, 2 упали
Вопрос: отправлять ли дальше?
- min.insync.replicas=3 (at-least-once, может быть unavailable)
- min.insync.replicas=1 (быстро, но может потеря)

Best Practices

1. Выбирайте правильную гарантию

# Для аналитики: at-most-once
producer = KafkaProducer(
    acks=0,      # Не ждем ack
    retries=0    # Без retries
)

# Для критичных данных: exactly-once
producer = KafkaProducer(
    acks='all',
    retries=3,
    transactional_id='my-producer'
)

2. Идемпотентная обработка Всегда обрабатывайте как at-least-once, даже если система гарантирует exactly-once. В distributed systems всегда может быть ошибка.

3. Мониторинг и алертинг

metrics = {
    'messages_sent': 0,
    'messages_failed': 0,
    'messages_duplicated': 0,
    'latency_p99_ms': 0
}

# Track все три метрики

4. Graceful shutdown

import signal

def shutdown_handler(signum, frame):
    # Завершить все pending messages
    producer.flush(timeout=30)
    producer.close()
    consumer.close()

signal.signal(signal.SIGTERM, shutdown_handler)

Заключение

Выбор гарантии доставки зависит от:

  • Критичности данных (можно ли потерять?)
  • Требуемого SLA (99% или 99.99%?)
  • Бюджета (at-least-once дешевле, exactly-once дороже)
  • Latency требований (exactly-once медленнее)

Реальность: большинство систем используют at-least-once + дедупликация, это баланс между надежностью и производительностью.

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