Содержание урока · 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 |
| вечный requeue | CPU горит, глубина не убывает | DLX, лимит попыток, парковка в DLQ |
| очередь-база данных | миллионы сообщений лежат годами | TTL, перенос истории в хранилище |
Кейс: очередь писем копилась, пока воркеры справлялись; в один день внешний SMTP-шлюз завис, издатели продолжали писать, а через сутки брокер упёрся в дисковый аларм. По алармам памяти и диска брокер блокирует соединения издателей: публикации встали у всех сервисов на этом брокере, включая ни в чём не виноватые vhost, — потребители при этом работали, и картина выглядела как загадочная паралич половины системы. Восстановление было ручным: чистить очередь, поднимать воркеров, разбираться со шлюзом. Профилактика стоит копейки: у каждой очереди лимит — x-max-length или x-message-ttl из пятого урока — и осознанный выбор overflow. Классическая очередь по умолчанию работает drop-head и молча выбрасывает старейшие сообщения; режим reject-publish отклоняет новые публикации, и при подтверждениях издатель узнаёт об отказе. Тренд глубины, который ловит проблему за сутки до аларма диска, — наблюдаемость из двадцать второго урока.
Вечный requeue и ядовитые истории#
Вечный requeue растёт из искреннего желания ничего не терять: воркер получает битый JSON, зовёт Nack с requeue=true, сообщение возвращается в очередь, воркер берёт его снова — круг замыкается. CPU горит, логи пухнут, а глубина не убывает: яд не лечится повторением. consumer timeout из восьмого урока в тридцать минут прервёт цикл только для одной доставки — очередь из тысячи ядовитых сообщений обеспечит бесконечную работу. Лечение — маршрут из шестнадцатого урока: nack без requeue в exchange, чья очередь с TTL и DLX назад, лимит попыток и парковка в DLQ для разбора. Счётчик попыток при этом ведёт брокер сам: заголовок x-death заполняется при каждом dead-lettering с полем count, читать его можно из d.Headers без собственной мутации и перепубликации. Симптом паттерна на дежурстве характерный: глубина стоит на месте, скорость раздач высока, потребители заняты — сообщения крутятся по кругу, не приближаясь к успеху.
Аудит топологии: маленький линтер
GOfunc 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-очередь. Ответ не думали на любой из них — найден баг раньше продакшена, и это самая дешёвая победа из всех, что даёт тема очередей.
Аудит использует сеть и management API — он выполняется локально против контейнера из второго урока, где /api/queues отдаст живой список очередей. В песочнице открыты fmt, strings, strconv, math, sort, errors и time: эмуляцию drop-head на срезе и логику лимитов можно отрепетировать программой, как в задании ниже. Интерактивы на странице закрепят страховки публикации и поведение классической очереди с лимитом.
Чек-лист антипаттернов для ревью#
- Канал на горутину: у каждой горутины свой 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])Соберите страховки публикации
Допишите режим подтверждений, ловца возвратов и персистентность.
Соберите защищённую публикацию
Расставьте строки: режим подтверждений, потоки уведомлений, публикация с обязательным адресатом, проверка ошибки, вердикт брокера, реакция на отказ.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: антипаттерны очередей
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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 для разбора.
Личные заметкиПусто
Вопросы и ответы по уроку
Когда допустим fire-and-forget?
Когда потеря сообщения не имеет последствий: потоковые метрики, деградирующий трейлинг, события с коротким TTL. Даже там стоит проверять ошибку Publish и включать mandatory — возврат unroutable дёшев. Подтверждения имеют цену в задержке, поэтому их оставляют там, где потеря конвертируется в деньги: платежи, уведомления, заказы.
Почему бы не включить подтверждения на всём?
Скорость публикаций падает, растут очереди ожидания вердиктов и число горутин, которые за ними следят; при всплесках это превращается в собственное узкое место. Практичная граница: подтверждения там, где потеря недопустима, и осознанный бюджет потерь там, где сообщение воспроизводимо из источника. Выбор фиксируют в ревью, а не оставляют на усмотрение автора каждого файла.
Как считать попытки, если воркер перепубликует сам, а не через DLX?
Тогда брокер не ведёт x-death — счётчик становится вашим: читайте собственный заголовок x-retry-count, инкрементируйте и публикуйте копию с новым значением. Путь через DLX честнее: счётчик и причина ведутся брокером, а приложение только читает заголовок. Оба варианта легальны, но смешивать их в одной цепочке не стоит — легко получить два независимых счётчика попыток и вечный круг между ними.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Excel в Go: чтение и генерация xlsx на excelize
Курс excelize на Go: создание xlsx, чтение, стили, формулы, диаграммы и StreamWriter для больших файлов. 25 уроков с примерами кода и проектом.
- Создавать и читать xlsx-файлы средствами excelize
- Работать с листами, ячейками, строками и столбцами
- Оформлять таблицы: стили, шрифты, границы, форматы чисел и дат
- Вставлять формулы, диаграммы и изображения