Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 8 из 25 ~20 мин +20 XP

Ack, Nack и Reject: гарантии доставки

Гарантии rabbitmq ack начинаются с одного факта: без ручного подтверждения брокер считает сообщение обработанным в момент отправки. Ручные ack превращают чтение из очереди в договор — обработал, отчитайся; не успел — сообщение вернётся и будет доставлено заново. Разберём d.Ack, d.Nack и d.Reject, флаг multiple, судьбу неподтверждённых сообщений при падении воркера и ядовитые циклы, в которых requeue работает против вас.

Содержание урока · 10 разделов

Седьмой урок оставил открытым главный вопрос потребителя: что именно подтверждает d.Ack и что случается, если подтверждения нет. Сообщение в очереди живёт в двух состояниях: ready — ждёт доставки, unacknowledged — выдано воркеру и ждёт вердикта. Пока воркер не подтвердил доставку, брокер хранит сообщение отдельно и никому его больше не отдаст; подтвердил — удаляет навсегда. Вся надёжность системы упирается в момент между этими состояниями: упал воркер с неподтверждённой доставкой, потерялась сеть, завис процесс — брокер возвращает сообщение в очередь и находит ему нового потребителя. Это и есть ручные подтверждения, manual ack: простейший механизм с парой методов и тремя-четырьмя правилами, на которых тем не менее стоят все гарантии доставки курса. Разберём методы по одному, потом проиграем падение воркера, а закончим самой дорогой ошибкой потребителя — бесконечным возвратом ядовитого сообщения.

Manual ack golang: метод d.Ack и флаг multiple#

Базовое подтверждение в manual ack golang выглядит как d.Ack(false): воркер сообщает брокеру, что доставку с этим DeliveryTag обработал, и сообщение можно удалить. Единственный аргумент multiple расширяет действие: d.Ack(true) подтверждает не только текущее, но и все доставки с меньшими тегами на этом канале, которые ещё не подтверждены. Теги растут монотонно в рамках канала, поэтому батч-подтверждение удобнее всего, когда обработки идут по порядку — на пакетных сценариях это экономит десятки сетевых рейсов. Правило канала то же, что и везде: подтверждение отправляется на том же канале, который доставил сообщение; ack чужим каналом даст ошибку неизвестного тега, и канал закроется. Двойное подтверждение одной доставки — та же история: второй ack улетает с тегом, который брокер уже удалил, канал получает PRECONDITION_FAILED и закрывается. Метод возвращает error, но не ждёт встречного кадра: ошибка подтверждения проявится либо в возврате метода при закрытом канале, либо через NotifyClose, — ещё один аргумент держать канал наблюдаемым, как в четвёртом уроке.

Падение воркера: что станет с неподтверждённым#

Проиграем аварию по шагам. Воркер получил доставку — сообщение перешло в unacknowledged. Процесс умер: от паники без recover, от деплоя, от kill -9. Брокер замечает это по закрытию канала или соединения — все неподтверждённые доставки этого потребителя возвращаются в очередь. Возвращённые сообщения встают максимально близко к началу очереди, получают флаг Redelivered=true и уходят первыми же живым потребителям. Из этого следует практика, которую дешевле принять сразу: обработчик должен быть готов к повторной доставке, а бизнес-операция — к дублю. Гарантия, которую даёт ручной ack, называется at-least-once: сообщение доставляется хотя бы один раз, но при сбоях возможны повторы, и от дубликатов защищает дедупликация по MessageId или бизнес-ключу. Противоположная крайность — autoAck=true из седьмого урока: там повторы исключены, зато падение воркера теряет всё выданное. Промежуточных гарантий у очереди нет: либо вы подтверждаете после обработки и миритесь с дублями, либо подтверждаете до и миритесь с потерями.

Воркер с Ack и Nack: три исхода доставки

GO
package main

import (
        "context"
        "errors"
        "log"

        amqp "github.com/rabbitmq/amqp091-go"
)

var errPoison = errors.New("не удается обработать сообщение")

func process(body []byte) error {
        if len(body) == 0 {
                return errPoison
        }
        log.Println("обработано:", string(body))
        return nil
}

func main() {
        conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
        if err != nil {
                log.Fatal(err)
        }
        defer conn.Close()

        ch, err := conn.Channel()
        if err != nil {
                log.Fatal(err)
        }

        _, err = ch.QueueDeclare("tasks", true, false, false, false, nil)
        if err != nil {
                log.Fatal(err)
        }

        ctx, cancel := context.WithCancel(context.Background())
        defer cancel()

        msgs, err := ch.ConsumeWithContext(ctx, "tasks", "ack-worker",
                false, false, false, false, nil)
        if err != nil {
                log.Fatal(err)
        }

        for d := range msgs {
                if err := process(d.Body); err != nil {
                        if errors.Is(err, errPoison) {
                                d.Nack(false, false)
                                continue
                        }
                        d.Nack(false, true)
                        continue
                }
                if err := d.Ack(false); err != nil {
                        log.Println("ack failed:", err)
                }
        }
}

Три исхода в одном цикле: успех — Ack(false), временная ошибка — Nack(false, true) с возвратом в очередь, безнадёжное тело — Nack(false, false) без возврата. Никакого ожидания между доставкой и вердиктом брокер не требует: подтверждение — это отдельный кадр, а не ответ на доставку.

Nack и Reject: три пути неудачной доставки#

Nack и Reject отказываются от доставки, и разница между ними в двух измерениях. d.Reject(requeue bool) — протокольный минимум: отказ от одной доставки, requeue решает, возвращать сообщение в очередь или нет. d.Nack(multiple, requeue bool) — расширение RabbitMQ: то же самое, но с возможностью махнуть сразу на все неподтверждённые доставки с меньшими тегами; для одиночных доставок Nack(false, requeue) эквивалентен Reject(requeue), и в коде их путать не вредно, хотя команда обычно выбирает один глагол. Ветка requeue=true возвращает сообщение, и оно немедленно уходит потребителям снова — с Redelivered=true. Ветка requeue=false удаляет сообщение, если у очереди нет dead letter exchange, и перекладывает его в DLQ, если настроен; разбор DLX и лимитов повторов — в шестнадцатом уроке, здесь достаточно правила: requeue=false без DLX — это стирание сообщения, и оно должно быть осознанным. Третий путь, о котором часто забывают, — отсутствие всякого пути: воркер молча ушёл без ack и nack, и сообщение вернётся само по закрытию канала. Это легальный вариант для обрывов и падений, но не для управления потоком.

Ack, Nack и Reject рядом#

ВызовСмыслКогда применять
d.Ack(false)подтвердить одну доставкуобработка прошла успешно
d.Ack(true)подтвердить все доставки до текущейпакетная обработка с известным порядком
d.Nack(false, true)вернуть одну доставку в очередьвременная ошибка: сеть, недоступная зависимость
d.Nack(false, false)не возвращать: удалить или в DLQсообщение не станет валидным от повтора
d.Reject(requeue)отказ от одной доставкито же, что Nack на одну, выбор — вопрос вкуса команды

Consumer timeout: когда ack приходит слишком поздно#

У ручных подтверждений есть скрытый лимит: брокер ждёт ack на доставку не вечно. Начиная с релизов 3.8.15 у RabbitMQ действует consumer_timeout — по умолчанию тридцать минут на одну доставку; если воркер не подтвердил сообщение за это время, брокер закрывает канал с ошибкой PRECONDITION_FAILED про acknowledgement timeout, а неподтверждённое возвращается в очередь. Ловится это на длинных задачах: обработка видео, тяжёлые импорты, пауза в отладчике на живой очереди. Способы честные: дробить задачу на шаги с промежуточными подтверждениями, выносить прогресс в отдельное хранилище и подтверждать исходное сообщение сразу, либо поднимать consumer_timeout в конфигурации брокера — что осознанно, а не по привычке. Симптом узнаваем: воркер работает, раз в полчаса канал закрывается, сообщения возвращаются — и цикл повторяется. Теперь вы знаете, что это не гонка и не сеть, а лимит протокола, который Broker считает честным для обычных потребителей.

Батч-подтверждение через multiple

GO
var last amqp.Delivery
pending := 0

for d := range msgs {
        if err := process(d.Body); err != nil {
                d.Nack(false, true)
                continue
        }
        last = d
        pending++
        if pending == 10 {
                if err := last.Ack(true); err != nil {
                        log.Println("batch ack failed:", err)
                }
                pending = 0
        }
}

Каждый десятый вызов подтверждает сразу партию: Ack(true) на последней доставке закрывает все неподтверждённые с меньшими тегами. Экономия сетевых рейсов ощутимая, цена тоже: сбой между обработкой и батчем вернёт в очередь до девяти уже сделанных задач — их повторно сделает другой воркер, и это должно быть нормально для вашей бизнес-логики.

Честная цена гарантий at-least-once#

Соберём всю цепочку гарантий, которые к этому уроку уже собраны. Продюсер отправил сообщение,mandatory возвращает немаршрутизируемое — шестой урок; очередь хранит непрочитанное; ручной ack возвращает необработанное — этот урок. Итог называется at-least-once: система гарантирует доставку хотя бы один раз, но не ровно один. Дубли возникают в двух местах: продюсер повторил отправку после обрыва, не узнав, дошло ли; брокер вернул сообщение после сбоя воркера, который уже успел записать результат. Поэтому идемпотентность потребителя — не украшение, а условие работы схемы: повторная обработка заказа не должна создавать второй заказ. Практика: ключ дедупликации в заголовках или MessageId, проверка в обработчике, уникальные ограничения в базе как последний рубеж. Ровно один раз из коробки не обещает ни один брокер сообщений, и любые «exactly-once» маркетинговые формулировки читайте как at-least-once плюс дедупликация на вашей стороне.

Правила подтверждений#

  • Ack после обработки, не до: подтверждение раньше результата — это autoAck под другим именем.
  • К обработке повторов готовятся заранее: дедупликация по MessageId или бизнес-ключу.
  • Ошибки делят на классы: временные — Nack с requeue, невалидные — requeue=false и DLQ.
  • Бесконечный requeue яда — инвалидация воркера; счётчик повторов или DLX ставят до инцидента.
  • Ack только на канале-доставщике; чужой канал или двойной ack закрывают канал PRECONDITION_FAILED.
  • Длинные задачи держат в уме consumer_timeout: тридцать минут на доставку по умолчанию.

Маршрут дальше: девятый урок закрывает вторую половину аварий — падение самого брокера: durable-очередь и DeliveryMode решают, что переживёт рестарт. Возврат неудачных сообщений в систему, а не в никуда, — шестнадцатый урок про dead letter, остановка воркера с дожимом задач — двадцать первый про graceful shutdown. Типовые способы потерять сообщения собраны в двадцать третьем уроке про антипаттерны, а тестируемость обработчика Delivery — в двадцать четвёртом. Похожую механику подтверждений вы могли видеть в потоках Redis — XACK решает ту же задачу в другой терминологии. Подписка и цикл воркера остались в седьмом уроке, карта курса — на странице курса.

Проверка: падение воркера

Воркер с autoAck=false упал, не подтвердив две доставки. Что станет с этими сообщениями?

Проверка: multiple

Воркер вызвал d.Ack(true) на доставке с тегом 10. Что подтвердится?

Что напечатает воркер?

Прочитайте код и выберите, что он выведет:

// в очереди tasks ровно одно сообщение, других нет
first := true
for d := range msgs {
        fmt.Println(d.Redelivered)
        if first {
                first = false
                d.Nack(false, true) // вернуть в очередь
                continue
        }
        d.Ack(false)
}

Соберите цикл с вердиктами

Допишите проверку повтора, возврат при ошибке и подтверждение при успехе.

for d := range msgs { if d. { log.Println("повторная доставка") } if err := process(d.Body); err != nil { d.(false, true) continue } d.(false) }

Соберите обработку доставки по шагам

Расставьте строки в порядке выполнения: соединение, канал, подписка, чтение, обработка, подтверждение.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: Ack, Nack, Reject

Термин

d.Ack(multiple)

нажмите, чтобы перевернуть
Определение

Подтверждение доставки: false — одна, true — все неподтверждённые с меньшими тегами на канале. Отправляется на канале-доставщике.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Ручной ack — договор с брокером: обработал и подтвердил, либо вернёт доставку при закрытии канала.
  • Ack(true) подтверждает партию по тегам одного канала; двойной ack или чужой канал дают PRECONDITION_FAILED.
  • Падение воркера возвращает неподтверждённые доставки в очередь с Redelivered=true — обработчик обязан переживать повторы.
  • Nack и Reject отказываются от доставки: requeue=true возвращает, requeue=false удаляет или уводит в DLQ.
  • Ядовитое сообщение с бесконечным requeue парализует воркер — классы ошибок и DLX ставят заранее.
  • consumer_timeout ограничивает ожидание ack тридцатью минутами по умолчанию — длинные задачи дробят.
  • Итог гарантий — at-least-once: дубли возможны, дедупликация на стороне потребителя обязательна.

Домашнее задание

Запустите воркер из урока и опубликуйте пять сообщений, одно из которых — пустая строка; убедитесь, что яд ушёл с Nack(false, false), а остальные подтвердились. Затем прервите воркер по Ctrl+C посреди обработки и посмотрите в management UI, как сообщения возвращаются и получают Redelivered=true у следующего запуска. В конце соберите батч-подтверждение на десять доставок и оцените по логам, сколько сообщений вернётся при падении посреди партии.

Личные заметкиПусто
Хранятся только в вашем браузере (RabbitMQ · Ack, Nack и Reject: гарантии доставки)

Вопросы и ответы по уроку

Чем Nack отличается от Reject, если на одной доставке они равны?

Только флагом multiple: Nack умеет отказываться от всех неподтверждённых доставок с меньшими тегами, Reject работает строго с одной. Команды обычно выбирают один глагол и держат его во всём коде, чтобы в ревью не гадать, что имелось в виду.

Что делать с задачами, которые обрабатываются дольше consumer_timeout?

Дробить на шаги с промежуточными подтверждениями или выносить прогресс во внешнее хранилище, подтверждая исходную доставку сразу после приёмки. Третьим вариантом остаётся поднять consumer_timeout в конфигурации брокера — но это осознанное решение для конкретных очередей, а не настройка по умолчанию на весь кластер.

Гарантирует ли ручной ack, что сообщение обработано ровно один раз?

Нет: ack даёт at-least-once — хотя бы один раз, с возможными повторами при сбоях. Ровно один раз собирают поверх очереди: дедупликация по ключу в обработчике, уникальные ограничения в базе, идемпотентные операции. Очередь обеспечивает возврат незавершённого, а не отсутствие дублей.

Термины урока

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

Следующая ступень учебного пути

Excelize25 уроков~10 чпрактика25 уроков уже доступны

Excel в Go: чтение и генерация xlsx на excelize

Курс excelize на Go: создание xlsx, чтение, стили, формулы, диаграммы и StreamWriter для больших файлов. 25 уроков с примерами кода и проектом.

  • Создавать и читать xlsx-файлы средствами excelize
  • Работать с листами, ячейками, строками и столбцами
  • Оформлять таблицы: стили, шрифты, границы, форматы чисел и дат
  • Вставлять формулы, диаграммы и изображения