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

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

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

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

Антипаттерны: как теряют сообщения

Самые дорогие ошибки rabbitmq — потерянные сообщения, и чаще всего за ними стоит антипаттерн очередей, переживший код-ревью: fire-and-forget, общий канал на все горутины, безлимитная очередь или вечный requeue. Разберём пять историй потерь с симптомами, по которым они всплывают на дежурстве, и лечением, которое уже есть в предыдущих уроках курса.

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

Потерянное письмо, пропавший заказ, тишина вместо уведомления — расследования почти всегда приводят не к мистике брокера, а к договору, который код не выполнил. Хорошая новость: каталог этих договоров короткий, и почти каждый антипаттерн имеет готовое лечение из предыдущих уроков — собирать заново не придётся, придётся применять. Вторая хорошая новость: у каждого антипаттерна есть опознаваемый симптом, и дежурный по нему ставит диагноз быстрее, чем по логам приложения. В этом уроке пять историй: канал как расходник и общий канал, fire-and-forget, безлимитная очередь, вечный requeue и два дизайн-нарушения — очередь вместо базы данных и утечка reply-очередей. По каждой — как ломается, как проявляется на дежурстве и чем лечится. В конце — чек-лист для код-ревью и маленький аудит топологии, который находит подозрительные очереди в живом брокере.

Ошибки rabbitmq: канал как расходник и общий канал#

Первая пара ошибок rabbitmq зеркальная, и обе стороны связаны с каналом. Расходник: канал открывают на каждую операцию — publish открыл канал, закрыл, следующий publish снова. Каждый Channel — это фреймы открытия, согласование лимитов и закрытие: тысячи каналов в минуту переводят брокера на перекладывание накладных расходов, съедают дескрипторы, а при пиках пикируют соединение целиком. Вторая сторона — общий канал: один amqp.Channel отдают всем горутинам, потому что так экономнее. Контракт библиотеки из четвёртого урока гласит, что канал не рассчитан на конкурентную работу: ack, nack и публикации из разных горутин перемешивают кадры, брокер закрывает канал за нарушение протокола, а вместе с ним умирают все его неподтверждённые доставки. Симптом общий на обе стороны: необъяснимые закрытия каналов с кодами 406 и 504 под нагрузкой, которые не воспроизводятся локально на одном запросе. Лечение — дисциплина: канал на горутину с собственным окном Qos и тегом, а общий канал живёт только там, где объявляют топологию в одном месте.

Fire-and-forget: публикация без обратной связи#

Fire-and-forget выглядит безобидно: Publish вернул nil, логика двинулась дальше. Но nil из шестого урока означает лишь «сообщение принято библиотекой», а между этим моментом и доставкой лежит длинный путь. Публикация без DeliveryMode в non-durable очередь исчезает при рестарте брокера — даже плановом. Публикация с опечаткой в routing key и без mandatory выбрасывается молча: адресата нет, а издатель ничего не узнал. Наконец, без режима подтверждений никто не скажет, что брокер вернул отказ при переполнении или внутреннем сбое. Каждая из трёх дыр закрывается инструментом из предыдущих глав: durable и persistent из девятого урока, mandatory с NotifyReturn из шестого, publisher confirms через Confirm и NotifyPublish. Честная цена: подтверждения добавляют кадры и задержку, поэтому их включают на критичных маршрутах — платежи, уведомления, — а для потоковых счётчиков оставляют лёгкий путь и осознанный бюджет потерь, зафиксированный в коде, а не в голове.

Публикация: плохо и лучше

GO
// ПЛОХО: fire-and-forget
_ = ch.PublishWithContext(ctx, "", "orders", false, false, amqp.Publishing{
        Body: []byte(body), // transient, без mandatory, без подтверждений
})

// ЛУЧШЕ: страховки на месте
if err := ch.Confirm(false); err != nil { // режим подтверждений
        log.Fatal(err)
}
confirms := ch.NotifyPublish(make(chan amqp.Confirmation, 16))
returned := ch.NotifyReturn(make(chan amqp.Return, 16))

go func() {
        for c := range confirms {
                if !c.Ack { // брокер отказал: повтор или парковка
                        log.Println("отказ брокера:", c.DeliveryTag)
                }
        }
}()

err = ch.PublishWithContext(ctx, "", "orders", true, false, amqp.Publishing{
        Body:         []byte(body),
        DeliveryMode: amqp.Persistent,
})
if err != nil {
        log.Println("publish:", err)
}

Каждая строка закрывает свою дыру: Confirm включает режим подтверждений, NotifyPublish даёт поток вердиктов брокера, NotifyReturn ловит unroutable при mandatory, а DeliveryMode делает сообщение персистентным. Читателя подтверждений регистрируют до публикации: иначе первый вердикт уйдёт в пустой канал. Цена видна в коде: три дополнительных вызова на публикацию — поэтому такой путь включают на критичных маршрутах, а не на всех подряд.

Антипаттерн, симптом и лечение#

АнтипаттернСимптом на дежурствеЛечение
общий канал или канал-расходникзакрытия 406 и 504 под нагрузкойканал на горутину, общий — для декларации
fire-and-forgetсообщения исчезают без следов в логахmandatory, confirms, durable и persistent
безлимитная очередьдисковый аларм, встали чужие издателиx-max-length или TTL, осознанный overflow
вечный requeueCPU горит, глубина не убываетDLX, лимит попыток, парковка в DLQ
очередь-база данныхмиллионы сообщений лежат годамиTTL, перенос истории в хранилище

Вечный requeue и ядовитые истории#

Вечный requeue растёт из искреннего желания ничего не терять: воркер получает битый JSON, зовёт Nack с requeue=true, сообщение возвращается в очередь, воркер берёт его снова — круг замыкается. CPU горит, логи пухнут, а глубина не убывает: яд не лечится повторением. consumer timeout из восьмого урока в тридцать минут прервёт цикл только для одной доставки — очередь из тысячи ядовитых сообщений обеспечит бесконечную работу. Лечение — маршрут из шестнадцатого урока: nack без requeue в exchange, чья очередь с TTL и DLX назад, лимит попыток и парковка в DLQ для разбора. Счётчик попыток при этом ведёт брокер сам: заголовок x-death заполняется при каждом dead-lettering с полем count, читать его можно из d.Headers без собственной мутации и перепубликации. Симптом паттерна на дежурстве характерный: глубина стоит на месте, скорость раздач высока, потребители заняты — сообщения крутятся по кругу, не приближаясь к успеху.

Аудит топологии: маленький линтер

GO
func audit() error {
        req, err := http.NewRequest(http.MethodGet,
                "http://localhost:15672/api/queues", nil)
        if err != nil {
                return err
        }
        req.SetBasicAuth("guest", "guest")

        resp, err := http.DefaultClient.Do(req)
        if err != nil {
                return err
        }
        defer resp.Body.Close()

        var rows []map[string]any
        if err := json.NewDecoder(resp.Body).Decode(&rows); err != nil {
                return err
        }

        for _, r := range rows {
                name, _ := r["name"].(string)
                ready, _ := r["messages"].(float64)
                cons, _ := r["consumers"].(float64)
                args, _ := r["arguments"].(map[string]any)
                _, limited := args["x-max-length"]

                if cons == 0 || !limited || ready > 1000 {
                        fmt.Printf("подозрительно: %s ready=%.0f consumers=%.0f max-length=%v
",
                                name, ready, cons, args["x-max-length"])
                }
        }
        return nil
}

Аудит берёт список очередей из management API из второго урока и печатает кандидатов по трём признакам антипаттернов: нет потребителей, нет лимита, глубина за тысячей. Числа из JSON приходят как float64 — отсюда приведения и вывод без дробной части. Критерии расширят под свои SLA, а сам скрипт запускают по расписанию и отправляют отчёт в общий канал: подозрительная очередь появляется в поле зрения до инцидента.

Антипаттерны очередей на код-ревью#

Пятая пара антипаттернов живёт на границе дизайна, и ловят их не на дежурстве, а на ревью. Очередь-база данных: в очередь складывают историю за годы и потом удивляются памяти и медленному management UI — очередь про пропускную способность, а не про архив, история живёт в хранилище с индексами и запросами. Утечка reply-очередей в RPC из восемнадцатого урока: клиент создаёт reply-to очередь на каждый запрос и забывает QueueDelete — через месяц у брокера тысячи эксклюзивных пустышек, а листинги в UI невозможно читать. Родственный случай — очередь на каждого пользователя в промышленных масштабах: объявления стоят дороже, чем кажется, а брокер ограничивает их количество. На ревью помогает пять вопросов: кто делает ack, что случится с ошибкой публикации, где DLX, ограничена ли очередь, кто закрывает reply-очередь. Ответ не думали на любой из них — найден баг раньше продакшена, и это самая дешёвая победа из всех, что даёт тема очередей.

Чек-лист антипаттернов для ревью#

  • Канал на горутину: у каждой горутины свой amqp-канал с окном и тегом, общий — только для объявления топологии.
  • Важные публикации с обязательным адресатом: mandatory плюс NotifyReturn, подтверждения на критичных маршрутах.
  • Персистентность там, где потеря недопустима: durable-очередь и DeliveryMode из девятого урока.
  • У каждой очереди лимит: x-max-length или TTL, а поведение переполнения выбрано осознанно.
  • Ядовитые сообщения уходят в DLX: Nack с requeue=true на непредсказуемом входе — прямой путь к кругу.
  • Reply-очереди RPC закрываются или переиспользуются: очередь на каждый запрос — утечка из восемнадцатого урока.
  • Аудит /api/queues входит в регламент: очереди без потребителей и лимитов видны в отчёте, а не в чате инцидента.

Лечение ссылается на соседей: правило канала сформулировал четвёртый урок про connection и channel, механика nil и mandatory — в шестом про публикацию, круг requeue и consumer timeout — в восьмом про ack, а флаги очереди — в пятом. Цепочку повторов с DLX собирает шестнадцатый урок, reply-очереди RPC — восемнадцатый, а сигналы, по которым антипаттерны видны заранее, — двадцать второй про наблюдаемость. Карта курса — на странице курса.

Проверка: общий канал с мьютексом

Один amqp.Channel отдали десяти горутинам-воркерам и обернули все операции мьютексом. Что из этого верно?

Проверка: дисковый аларм

Очередь без ограничений заполнила диск, сработал дисковый аларм брокера. Что произойдёт с публикациями?

Что выбросит drop-head?

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

q := []string{}
maxLen := 3
for _, m := range []string{"a", "b", "c", "d"} {
        q = append(q, m)
        if len(q) > maxLen {
                q = q[1:] // drop-head, как max-length у классической очереди
        }
}
fmt.Println(q[0])

Соберите страховки публикации

Допишите режим подтверждений, ловца возвратов и персистентность.

err := ch.(false) // включаем режим подтверждений returned := ch.(make(chan amqp.Return, 16)) err = ch.PublishWithContext(ctx, "", "orders", , false, amqp.Publishing{ Body: body, DeliveryMode: amqp., })

Соберите защищённую публикацию

Расставьте строки: режим подтверждений, потоки уведомлений, публикация с обязательным адресатом, проверка ошибки, вердикт брокера, реакция на отказ.

Строки

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

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

Карточки: антипаттерны очередей

Термин

fire-and-forget

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

Публикация без обратной связи: nil не значит доставлено. Лечат mandatory с NotifyReturn, publisher confirms и персистентностью.

1 / 6

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

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

Вывод

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

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

  • Канал не конкурентен: свой канал на горутину, общий — только для декларации топологии.
  • Fire-and-forget закрывается связкой durable, persistent, mandatory и publisher confirms на критичных маршрутах.
  • У каждой очереди лимит x-max-length или TTL, а поведение переполнения выбирают осознанно.
  • По алармам памяти и диска брокер блокирует издателей: безлимитная очередь топит весь брокер, не только свою очередь.
  • Вечный requeue лечится DLX и лимитом попыток; счётчик x-death ведёт брокер сам.
  • Очередь-база данных и утечка reply-очередей ловятся пятью вопросами на код-ревью.
  • Аудит /api/queues по трём признакам — нет потребителей, нет лимита, большая глубина — находит проблемы до инцидента.

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

Прогоните аудит из урока против контейнера из второго урока и найдите среди своих очередей кандидатов: без лимитов, без потребителей, с растущей глубиной. Возьмите одну из них и почините по чек-листу: добавьте x-max-length или TTL, включите mandatory с NotifyReturn на издателе из шестого урока и подтверждения на критичном маршруте. Затем воспроизведите вечный requeue битым сообщением, убедитесь, что заголовок x-death растёт при каждом dead-lettering, а после третьей попытки сообщение оседает в DLQ для разбора.

Личные заметкиПусто
Хранятся только в вашем браузере (RabbitMQ · Антипаттерны: как теряют сообщения)

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

Когда допустим fire-and-forget?

Когда потеря сообщения не имеет последствий: потоковые метрики, деградирующий трейлинг, события с коротким TTL. Даже там стоит проверять ошибку Publish и включать mandatory — возврат unroutable дёшев. Подтверждения имеют цену в задержке, поэтому их оставляют там, где потеря конвертируется в деньги: платежи, уведомления, заказы.

Почему бы не включить подтверждения на всём?

Скорость публикаций падает, растут очереди ожидания вердиктов и число горутин, которые за ними следят; при всплесках это превращается в собственное узкое место. Практичная граница: подтверждения там, где потеря недопустима, и осознанный бюджет потерь там, где сообщение воспроизводимо из источника. Выбор фиксируют в ревью, а не оставляют на усмотрение автора каждого файла.

Как считать попытки, если воркер перепубликует сам, а не через DLX?

Тогда брокер не ведёт x-death — счётчик становится вашим: читайте собственный заголовок x-retry-count, инкрементируйте и публикуйте копию с новым значением. Путь через DLX честнее: счётчик и причина ведутся брокером, а приложение только читает заголовок. Оба варианта легальны, но смешивать их в одной цепочке не стоит — легко получить два независимых счётчика попыток и вечный круг между ними.

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

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

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

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

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

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

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