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

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

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

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

Headers и альтернативный exchange

Когда маршрутизация должна смотреть на атрибуты сообщения, а не на строку ключа, приходит headers exchange: сравнение идёт по заголовкам с режимами any и all. Вторая половина урока — про alternate exchange: ловушку для промахов, которая собирает всё, что не нашло очередь. Разберём x-match, таблицы аргументов, связку с mandatory из шестого урока и почему редкий тип обменника всё равно стоит знать.

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

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 из тринадцатого урока. Комбинации получаются выразительными: несколько привязок на одну очередь с разными условиями собирают сложные выборки, а издатель просто публикует заголовки и не знает про фильтры ничего.

Привязка по заголовкам и публикация

GO
package 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=reportformat=pdf, type=reportпопадание
all: format=pdf, type=reportformat=pdf, type=report, source=crmпопадание: лишние заголовки не мешают
all: format=pdf, type=reportformat=pdfмимо: type отсутствует
any: format=pdf, type=reportformat=docx, type=reportпопадание: совпал один из двух
any: format=pdf, type=reportformat=docx, type=invoiceмимо: ни одного совпадения

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 не сработает, если переправка состоялась: брокер считает доставку удавшейся.

Основной обменник с запасным

GO
err = 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 — другая история: он не выбирается вместо заголовков, а ставится аргументом на любой обменник любого типа, и в боевых схемах на него стоит смотреть как на обязательную страховку.

Правила 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 и привязку ловушки.

err := ch.("spare", "fanout", true, false, false, false, nil) if err != nil { log.Fatal(err) } err = ch.ExchangeDeclare("orders", "direct", true, false, false, false, amqp.Table{"": "spare"}) if err != nil { log.Fatal(err) } err = ch.QueueBind(, "", "spare", false, nil)

Соберите топологию с alternate exchange

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

Строки

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

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

Карточки: headers и ловушка промахов

Термин

headers exchange

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

Обменник, который маршрутизирует по заголовкам сообщения; routing key не читает.

1 / 6

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

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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 на обменник из одиннадцатого урока, отправьте сообщение с незнакомым ключом, найдите его в ловушке и решите, какой алерт поставили бы на её глубину.

Личные заметкиПусто
Хранятся только в вашем браузере (RabbitMQ · Headers и альтернативный exchange)

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

Что будет, если у привязки к headers exchange нет ключа x-match?

Брокер применит режим по умолчанию — all: привязка потребует совпадения всех перечисленных заголовков. Полагаться на умолчание не стоит: явный x-match в каждой привязке делает топологию читаемой и спасает от сюрпризов при копировании условий из чужого кода.

Может ли alternate exchange быть topic или direct?

Может: тип запасного не ограничен, просто чаще всего берут fanout, потому что ловушке нужны все промахи без фильтрации. Если промахи нужно делить — например, по типу ошибки — запасной делают topic и привязывают разные очереди с масками; на механику переправки тип не влияет.

Сработает ли NotifyReturn, если сообщение ушло в alternate exchange?

Нет: переправка в запасной обменник считается состоявшейся доставкой, и mandatory не вернёт такое сообщение издателю. Если вы хотите ловить промахи на стороне издателя — не настраивайте alternate; если централизованно — настройте ловушку и не рассчитывайте на возвраты.

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

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

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

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

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

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

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