Содержание урока · 10 разделов
- Manual ack golang: метод d.Ack и флаг multiple
- Падение воркера: что станет с неподтверждённым
- Nack и Reject: три пути неудачной доставки
- Ack, Nack и Reject рядом
- Consumer timeout: когда ack приходит слишком поздно
- Честная цена гарантий at-least-once
- Правила подтверждений
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Седьмой урок оставил открытым главный вопрос потребителя: что именно подтверждает 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: три исхода доставки
GOpackage 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 на одну, выбор — вопрос вкуса команды |
Кейс: воркер читал JSON, на ошибке разбора делал Nack(false, true) — вернуть в очередь. Одно битое сообщение от заказчика с выгрузкой разрушило вечер: возврат ставит его в начало очереди, воркер тут же получает его снова, снова не может разобрать, снова возвращает. Процесс крутится на полном ядре, полезные сообщения стоят за ядом, лог растёт мегабайтами в минуту. Лечение двухслойное: разбор ошибок на классы — невалидное сообщение уходит с requeue=false в DLQ, а временные сбои возвращаются; иногда добавляют счётчик повторов в заголовках, чтобы третий раз стал последним. Урок шестнадцатый собирает это в готовый паттерн retry с dead letter.
Consumer timeout: когда ack приходит слишком поздно#
У ручных подтверждений есть скрытый лимит: брокер ждёт ack на доставку не вечно. Начиная с релизов 3.8.15 у RabbitMQ действует consumer_timeout — по умолчанию тридцать минут на одну доставку; если воркер не подтвердил сообщение за это время, брокер закрывает канал с ошибкой PRECONDITION_FAILED про acknowledgement timeout, а неподтверждённое возвращается в очередь. Ловится это на длинных задачах: обработка видео, тяжёлые импорты, пауза в отладчике на живой очереди. Способы честные: дробить задачу на шаги с промежуточными подтверждениями, выносить прогресс в отдельное хранилище и подтверждать исходное сообщение сразу, либо поднимать consumer_timeout в конфигурации брокера — что осознанно, а не по привычке. Симптом узнаваем: воркер работает, раз в полчаса канал закрывается, сообщения возвращаются — и цикл повторяется. Теперь вы знаете, что это не гонка и не сеть, а лимит протокола, который Broker считает честным для обычных потребителей.
Батч-подтверждение через multiple
GOvar 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 плюс дедупликация на вашей стороне.
Подтверждения — протокольный обмен с брокером, в браузерной песочнице ему взяться неоткуда: там работают только fmt, strings, strconv, math, sort, errors и time, без сети. Воркер этого урока запускается локально против Docker-брокера из второго урока; чтобы увидеть возврат, добавьте в очередь битое сообщение продюсером из шестого урока и понаблюдайте Redelivered. Интерактивы ниже проверят аргументы методов и судьбу неподтверждённой доставки.
Правила подтверждений#
- 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)
}Соберите цикл с вердиктами
Допишите проверку повтора, возврат при ошибке и подтверждение при успехе.
Соберите обработку доставки по шагам
Расставьте строки в порядке выполнения: соединение, канал, подписка, чтение, обработка, подтверждение.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: Ack, Nack, Reject
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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 у следующего запуска. В конце соберите батч-подтверждение на десять доставок и оцените по логам, сколько сообщений вернётся при падении посреди партии.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем Nack отличается от Reject, если на одной доставке они равны?
Только флагом multiple: Nack умеет отказываться от всех неподтверждённых доставок с меньшими тегами, Reject работает строго с одной. Команды обычно выбирают один глагол и держат его во всём коде, чтобы в ревью не гадать, что имелось в виду.
Что делать с задачами, которые обрабатываются дольше consumer_timeout?
Дробить на шаги с промежуточными подтверждениями или выносить прогресс во внешнее хранилище, подтверждая исходную доставку сразу после приёмки. Третьим вариантом остаётся поднять consumer_timeout в конфигурации брокера — но это осознанное решение для конкретных очередей, а не настройка по умолчанию на весь кластер.
Гарантирует ли ручной ack, что сообщение обработано ровно один раз?
Нет: ack даёт at-least-once — хотя бы один раз, с возможными повторами при сбоях. Ровно один раз собирают поверх очереди: дедупликация по ключу в обработчике, уникальные ограничения в базе, идемпотентные операции. Очередь обеспечивает возврат незавершённого, а не отсутствие дублей.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Excel в Go: чтение и генерация xlsx на excelize
Курс excelize на Go: создание xlsx, чтение, стили, формулы, диаграммы и StreamWriter для больших файлов. 25 уроков с примерами кода и проектом.
- Создавать и читать xlsx-файлы средствами excelize
- Работать с листами, ячейками, строками и столбцами
- Оформлять таблицы: стили, шрифты, границы, форматы чисел и дат
- Вставлять формулы, диаграммы и изображения