Содержание урока · 10 разделов
- Headers exchange: маршрутизация по заголовкам
- x-match any и all: привязка по условиям
- x-match на примерах
- Alternate exchange: ловушка для промахов
- Alternate exchange в бою: мониторинг промахов
- Headers или ключ: честный выбор
- Правила headers и alternate exchange
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Direct и topic маршрутизируют строкой ключа, и в большинстве случаев этого хватает. Оставшийся случай выглядит так: маршрут зависит от сочетания атрибутов — формат документа, его тип, версия схемы, приоритет, — и часть атрибутов опциональна. Вшивать это в ключ придётся перечислением комбинаций: report.pdf.v2, report.docx.v2, invoice.pdf.v2 — и с каждым новым атрибутом список растёт умножением. Headers exchange решает задачу иначе: сообщение публикуется с заголовками — картой пар ключ-значение, — а привязка описывает, какие значения она ждёт. Это последний из четырёх типов обменников курса, и он честно редкий; но когда встречается, без него топология превращается в эпос. Встречается он в системах обмена документами, приёмочных шлюзах с версиями схем и в шинах, где правила маршрутизации описывает конфигурация, а не код. Вторая половина урока — про alternate exchange: не тип, а настройка обменника, которая закрывает главный кошмар одиннадцатого урока — сообщения, исчезающие без единой ошибки.
Headers exchange: маршрутизация по заголовкам#
Headers exchange не читает routing key вовсе: он сравнивает заголовки публикации с аргументами привязки. Издатель кладёт атрибуты в поле Headers типа amqp.Table: amqp.Publishing{Headers: amqp.Table{"format": "pdf", "type": "report"}}. Привязка передаёт свои условия аргументами QueueBind — таблицей с обязательным ключом x-match: amqp.Table{"x-match": "all", "format": "pdf", "type": "report"}. Режим x-match решает, считать ли совпадением все заголовки или хотя бы один; это единственный спецключ в таблице, остальные пары — ваши условия. Значения сравниваются с учётом типа: строка "2" и число 2 — разные вещи, и это ловит при миграциях форматов. Обменник объявляется знакомо: ch.ExchangeDeclare("documents", "headers", true, false, false, false, nil) — durable, идемпотентность и PRECONDITION_FAILED при смене типа работают как у direct из одиннадцатого урока, а в management UI тестовая публикация заполняет заголовки таблицей в той же форме.
x-match any и all: привязка по условиям#
Логика x-match — два правила, и их полезно проговорить на примерах. Режим all требует совпадения всех заголовков из привязки: условие x-match all с format pdf и type report пропустит только сообщения, у которых совпали оба; лишние заголовки публикации роли не играют — важен лишь набор, перечисленный в привязке. Режим any требует совпадения хотя бы одного: та же привязка в режиме any возьмёт и «pdf», и «report» по отдельности. Any с единственным заголовком превращается в фильтр по одному признаку — так делают избирательные подписки без topic-словаря. Масок у заголовков нет: совпадение точное, без звёздочек и префиксов; если нужна иерархия значений — это работа для topic из тринадцатого урока. Комбинации получаются выразительными: несколько привязок на одну очередь с разными условиями собирают сложные выборки, а издатель просто публикует заголовки и не знает про фильтры ничего.
Привязка по заголовкам и публикация
GOpackage main
import (
"context"
"log"
amqp "github.com/rabbitmq/amqp091-go"
)
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.ExchangeDeclare("documents", "headers", true, false, false, false, nil)
if err != nil {
log.Fatal(err)
}
if _, err := ch.QueueDeclare("pdf.reports", true, false, false, false, nil); err != nil {
log.Fatal(err)
}
err = ch.QueueBind("pdf.reports", "", "documents", false, amqp.Table{
"x-match": "all",
"format": "pdf",
"type": "report",
})
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
err = ch.PublishWithContext(ctx, "documents", "", false, false,
amqp.Publishing{
ContentType: "application/json",
Headers: amqp.Table{
"format": "pdf",
"type": "report",
"source": "crm",
},
Body: []byte("{}"),
})
if err != nil {
log.Fatal(err)
}
log.Println("опубликовано с заголовками format=pdf, type=report")
}Привязка требует оба заголовка: x-match all, format pdf и type report. У публикации три заголовка — лишний source роли не играет, сравнивается только перечисленный в привязке набор. Routing key пуст и не участвует; в management UI тестовую публикацию с заголовками делают в той же форме, где у topic вводили ключ.
x-match на примерах#
| Привязка | Заголовки публикации | Итог |
|---|---|---|
| all: format=pdf, type=report | format=pdf, type=report | попадание |
| all: format=pdf, type=report | format=pdf, type=report, source=crm | попадание: лишние заголовки не мешают |
| all: format=pdf, type=report | format=pdf | мимо: type отсутствует |
| any: format=pdf, type=report | format=docx, type=report | попадание: совпал один из двух |
| any: format=pdf, type=report | format=docx, type=invoice | мимо: ни одного совпадения |
Кейс: очередь отчётов не получала ничего, хотя публикации шли. В привязке стояло Format с большой буквы, а издатель слал format — ключи заголовков сравниваются с учётом регистра, и совпадение было нулевым. Диагноз виден на странице очереди: привязка есть, сообщений нет; лечится сверкой имён заголовков между издателем и привязкой — и константой в общем пакете, а не строками в двух местах. Вторая цена headers — размер: заголовки едут в каждом сообщении, и таблица из десятка пар прибавляет к доставке сотни байт; класть в заголовки данные, нужные только читателю тела, — типичное злоупотребление. Третий нюанс: типы значений сравниваются честно, и после смены в издателе числового format на строковый привязки со старым типом перестают совпадать — без ошибок, просто мимо.
Alternate exchange: ловушка для промахов#
Alternate exchange — настройка основного обменника: всё, что не нашло очередь, брокер переправляет в запасной. Задаётся аргументом при объявлении: ch.ExchangeDeclare("orders", "direct", true, false, false, false, amqp.Table{"alternate-exchange": "orders.unrouted"}) — и сам запасной объявляется раньше, иначе объявление основного упадёт со ссылкой на несуществующую сущность. Дальше механика простая: публикация с ключом, к которому нет привязок, уходит в orders.unrouted; там обычно стоит fanout и одна очередь-ловушка. Это серверное решение проблемы из warn-врезки одиннадцатого урока: не каждый издатель ловит возвраты через mandatory, а брокер централизованно складывает промахи в место, которое вы контролируете. Правила строгие: сообщение, переправленное в запасной и снова не нашедшее очередь, уничтожается — каскада из alternate в alternate не будет; и NotifyReturn не сработает, если переправка состоялась: брокер считает доставку удавшейся.
Основной обменник с запасным
GOerr = ch.ExchangeDeclare("orders.unrouted", "fanout", true, false, false, false, nil)
if err != nil {
log.Fatal(err)
}
_, err = ch.QueueDeclare("orders.unrouted.log", true, false, false, false, nil)
if err != nil {
log.Fatal(err)
}
err = ch.QueueBind("orders.unrouted.log", "", "orders.unrouted", false, nil)
if err != nil {
log.Fatal(err)
}
err = ch.ExchangeDeclare("orders", "direct", true, false, false, false,
amqp.Table{"alternate-exchange": "orders.unrouted"})
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
err = ch.PublishWithContext(ctx, "orders", "email.typo", false, false,
amqp.Publishing{Body: []byte("проверка маршрута")})
if err != nil {
log.Fatal(err)
}Порядок важен: запасной обменник и его очередь объявляются до основного, потому что аргумент alternate-exchange ссылается на существующую сущность. Сообщение с ключом email.typo не найдёт привязок в orders и окажется в orders.unrouted.log — с телом и заголовками, готовое к разбору.
Alternate exchange в бою: мониторинг промахов#
Ловушка без наблюдения бесполезна: очередь промахов должна звучать. Минимум — глубина и возраст в дашборде, как у любой очереди из двадцать второго урока; правильнее — алерт на каждое сообщение: в здоровой топологии промахов не бывает, и каждая поимка — инцидент с телом, которое видно в management UI и можно переопубликовать после починки. Связка с обязательными возвратами из шестого урока распределяется по уровням: mandatory с NotifyReturn возвращает недоставленное конкретному издателю — вариант там, где публикация и обработка возврата в одних руках; alternate exchange работает централизованно и не требует дисциплины от каждого автора публикаций. DLX из шестнадцатого урока решает зеркальную задачу — куда уходит сообщение, которое потребитель не осилил, — и механически похож: аргумент при объявлении плюс обменник-приёмник. Разница в направлении: alternate ловит недоставленное до потребителя, dead letter — после. И то и другое настраивают аргументами при объявлении — отсюда и путаница: x-dead-letter-exchange живёт на очереди, alternate-exchange — на обменнике.
Headers или ключ: честный выбор#
Headers exchange — редкий гость в продакшн-схемах, и это надо признать прямо: команды выбирают direct, topic и fanout, а заголовками маршрутизируют единицы. Причины понятны: ключ виден в логах и метриках, выглядит в management UI и на ревью, а заголовки прячутся внутри сообщения; словарь ключей читается, таблица условий — нет. Но есть ситуации, где headers выигрывает честно: атрибутов несколько, часть опциональна, их множество комбинаторно растёт; правила маршрутизации меняются чаще кода; нужна маршрутизация без вынесения служебных данных в ключ, например по версиям форматов. Рабочий критерий: если маршрут описывается одним словом или маской — берите ключ; если требует связки трёх-четырёх флагов с опциями — заголовки перестают быть экзотикой. Alternate exchange — другая история: он не выбирается вместо заголовков, а ставится аргументом на любой обменник любого типа, и в боевых схемах на него стоит смотреть как на обязательную страховку.
Заголовки и переправка живут в брокере, а песочнице доступны только fmt, strings, strconv, math, sort, errors и time — без сети: код урока запускается локально против Docker-контейнера из второго урока. Проверьте себя так: опубликуйте сообщение с ключом, к которому нет привязок, и найдите его в очереди-ловушке; затем уберите alternate-exchange из аргументов и сравните — сообщение исчезнет молча. Интерактивы ниже проверят режимы x-match и порядок объявления топологии.
Правила headers и alternate exchange#
- Headers exchange не читает routing key: сравниваются заголовки публикации и условия привязки.
- x-match all требует всех совпадений, any — хотя бы одного; масок у заголовков нет.
- Ключи заголовков чувствительны к регистру, значения сравниваются с учётом типа.
- Alternate exchange переправляет недоставленное в запасной обменник; второго прыжка не будет — промах уничтожается.
- Запасной обменник объявляют до основного: аргумент ссылается на существующую сущность.
- Очередь-ловушка без алерта бесполезна: каждый промах — инцидент, а не архив.
Связи урока: одиннадцатый урок поставил проблему немаршрутизируемых сообщений, шестой про mandatory и NotifyReturn — издательскую половину ответа. Шестнадцатый урок про dead letter построит на той же механике аргументов очередь повторов, пятнадцатый про JSON разберёт тело сообщения, которое headers-маршрутизация не трогает. Durable-семантику ловушки напомнит девятый урок, а fanout из двенадцатого — типичный тип запасного обменника; карта курса — на странице курса.
Проверка: x-match
Привязка с аргументами x-match all, format pdf, type report. Публикация с заголовками format=pdf и type=invoice. Дойдёт ли сообщение до этой очереди?
Проверка: alternate exchange
У обменника orders настроен alternate exchange orders.unrouted. Публикация с ключом, к которому нет привязок в обоих обменниках. Что станет с сообщением?
Где окажется сообщение?
Прочитайте код и выберите, что он выведет:
// основной: exchange "orders" типа direct с аргументом
// {"alternate-exchange": "spare"}
// spare объявлен, очередь spare.log привязана
// в orders привязана очередь main с ключом "email"
err = ch.PublishWithContext(ctx, "orders", "push", false, false,
amqp.Publishing{Body: []byte("a")})
fmt.Println(err == nil)Соберите топологию с ловушкой
Допишите объявление запасного обменника, основного с alternate и привязку ловушки.
Соберите топологию с alternate exchange
Расставьте строки в порядке выполнения: соединение, канал, запасной обменник, очередь, привязка ловушки, основной обменник с аргументом.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: headers и ловушка промахов
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Headers exchange сравнивает заголовки публикации с условиями привязки; routing key не читается.
- x-match all — все совпадения, any — хотя бы одно; масок у заголовков нет.
- Ключи заголовков чувствительны к регистру, значения — к типу: строка и число различаются.
- Alternate exchange переправляет промахи в запасной обменник; каскада из двух переправок не бывает.
- Ловушку объявляют до основного обменника и держат под алертом: промах — инцидент.
- mandatory возвращает промах издателю, alternate складывает его централизованно на брокере.
Домашнее задание
Соберите headers-обменник documents и две очереди: pdf-отчёты с привязкой all по format=pdf и type=report, черновики с any по тем же заголовкам. Публикуйте сообщения с разным набором заголовков и сверяйте попадания с таблицей из урока. Затем повесьте alternate-exchange на обменник из одиннадцатого урока, отправьте сообщение с незнакомым ключом, найдите его в ловушке и решите, какой алерт поставили бы на её глубину.
Личные заметкиПусто
Вопросы и ответы по уроку
Что будет, если у привязки к headers exchange нет ключа x-match?
Брокер применит режим по умолчанию — all: привязка потребует совпадения всех перечисленных заголовков. Полагаться на умолчание не стоит: явный x-match в каждой привязке делает топологию читаемой и спасает от сюрпризов при копировании условий из чужого кода.
Может ли alternate exchange быть topic или direct?
Может: тип запасного не ограничен, просто чаще всего берут fanout, потому что ловушке нужны все промахи без фильтрации. Если промахи нужно делить — например, по типу ошибки — запасной делают topic и привязывают разные очереди с масками; на механику переправки тип не влияет.
Сработает ли NotifyReturn, если сообщение ушло в alternate exchange?
Нет: переправка в запасной обменник считается состоявшейся доставкой, и mandatory не вернёт такое сообщение издателю. Если вы хотите ловить промахи на стороне издателя — не настраивайте alternate; если централизованно — настройте ловушку и не рассчитывайте на возвраты.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Формат сообщений: JSON и структуры
- Ошибки потребителя: retry и dead letter
- Паттерн worker pool на очереди
- RPC над очередями: reply-to и correlation id
- Topic: гибкая маршрутизация по шаблону
- Fanout: рассылка всем подписчикам
- Обменники: direct exchange и routing key
- Prefetch и fair dispatch: QoS потребителя
- Все курсы по Go
Следующая ступень учебного пути
Excel в Go: чтение и генерация xlsx на excelize
Курс excelize на Go: создание xlsx, чтение, стили, формулы, диаграммы и StreamWriter для больших файлов. 25 уроков с примерами кода и проектом.
- Создавать и читать xlsx-файлы средствами excelize
- Работать с листами, ячейками, строками и столбцами
- Оформлять таблицы: стили, шрифты, границы, форматы чисел и дат
- Вставлять формулы, диаграммы и изображения