Routing key и шаблоны
Routing key — это публичный контракт вашей шины событий. Его проектируют один раз, а живут с ним годами.
Routing key — строка до 255 байт, которую продюсер прикладывает к сообщению. В topic-обменнике она делится точками на слова, и по этим словам брокер решает, каким очередям сообщение подходит.
Технически в routing key можно положить что угодно: abc, задача-7, пустую строку. Брокеру всё равно. Но ключ — это то, на что подписываются другие команды. Поменяли схему — сломали всех потребителей разом, причём тихо: они просто перестанут получать сообщения, без единого исключения в логах. Поэтому к именованию ключей стоит относиться как к версионированию публичного API.
Как устроено сравнение
В topic-обменнике ключ режется по точкам на слова. Слово — это любая последовательность символов между точками: буквы, цифры, дефисы, подчёркивания. Точка — единственный разделитель.
*в binding key заменяет ровно одно слово;#заменяет ноль или больше слов;- всё остальное сравнивается побайтово, с учётом регистра:
Orders.Createdиorders.created— разные ключи.
Чтобы это перестало быть магией, давайте реализуем алгоритм сравнения на чистом Python и прогоним по нему набор случаев. Логика брокера ровно такая:
def match(pattern_words, key_words):
if not pattern_words:
return not key_words # шаблон кончился — ключ тоже должен кончиться
head = pattern_words[0]
if head == "#":
# '#' съедает 0, 1, 2 ... слов — пробуем все варианты
for i in range(len(key_words) + 1):
if match(pattern_words[1:], key_words[i:]):
return True
return False
if not key_words:
return False # слова кончились, а шаблон требует ещё
if head == "*" or head == key_words[0]:
return match(pattern_words[1:], key_words[1:])
return False
def topic_match(pattern, key):
return match(pattern.split("."), key.split("."))
cases = [
("logs.*.error", "logs.auth.error"),
("logs.*.error", "logs.error"),
("logs.*.error", "logs.auth.db.error"),
("logs.#", "logs.auth.db.error"),
("logs.#", "logs"),
("orders.*.created", "orders.eu.created"),
("#", "anything.at.all"),
]
for pattern, key in cases:
print(f"{pattern} vs {key}: {topic_match(pattern, key)}")Результат:
logs.*.error vs logs.auth.error: True
logs.*.error vs logs.error: False
logs.*.error vs logs.auth.db.error: False
logs.# vs logs.auth.db.error: True
logs.# vs logs: True
orders.*.created vs orders.eu.created: True
# vs anything.at.all: TrueДве строки здесь стоят отдельного внимания. logs.*.error не поймал logs.error — звёздочка требует слово, а не разрешает его отсутствие. И logs.# поймал просто logs — решётка честно матчит ноль слов. Именно на этих двух случаях спотыкаются чаще всего.
Как строить осмысленные ключи
Работающая схема почти всегда выглядит как иерархия от общего к частному, слева направо. Слева — то, по чему будут фильтровать грубо (домен, сервис), справа — детали (тип события, статус).
| Схема | Пример ключа | Полезные подписки |
домен.сущность.событие | orders.order.created | orders.#, *.*.created |
сервис.уровень | auth.error | *.error, auth.# |
домен.регион.событие | payments.eu.refunded | payments.eu.#, *.*.refunded |
сущность.действие.статус | invoice.send.failed | invoice.#, #.failed |
Дальше — правила, которые экономят месяцы боли.
1. Фиксируйте количество сегментов
Если у вас orders.order.created (три слова), пусть все события домена будут трёхсловными: orders.order.paid, orders.shipment.sent. Тогда шаблон orders.*.created работает предсказуемо. Как только в одном месте появится orders.created, а в другом orders.eu.order.created — все существующие подписки начнут дырявить.
2. Не кладите в ключ идентификаторы
Соблазн написать order.4821.created велик, но это тупик: чтобы подписаться на конкретный заказ, придётся создавать binding на каждый ID — а их миллионы. Идентификаторы, суммы, e-mail — это тело сообщения. Ключ — только метаданные маршрутизации, то есть ответ на вопрос «кому это в принципе может быть интересно».
3. Ключ должен дублировать то, что уже есть в теле
В теле события пусть будет поле "event": "order.created", и оно же — в routing key. Потребитель, получив сообщение, не должен зависеть от того, по какому binding'у оно приехало: очередь может быть подписана на десяток шаблонов сразу.
4. Держите словарь событий
Обычный markdown-файл в репозитории, где перечислены все ключи с описанием. Звучит скучно, спасает буквально всех. Без него через год никто не сможет ответить, чем order.done отличается от order.completed — а в проде живут оба.
Как это работает
Topic-маршрутизация в RabbitMQ реализована деревом слов, а не перебором binding'ов и не регулярками. Брокер спускается по дереву, разбирая routing key слово за словом, и собирает множество подошедших очередей. Практические следствия:
- Стоимость растёт от числа binding'ов, а не от числа сообщений. Тысяча binding'ов на обменнике — нормально, десятки тысяч — уже стоит померить.
#в середине шаблона дороже всего.logs.#.errorвалиден, но заставляет брокер перебирать варианты. Работает — да, но обычно это признак, что схема ключей спроектирована неудачно.- Лимит 255 байт. Именно байт, не символов: кириллица в ключе съедает по два байта на букву. Ещё одна причина писать ключи латиницей.
- Сообщение доедет в очередь один раз, даже если подошло сразу трём binding'ам этой очереди.
Частые ошибки
- Ждут, что
logs.*поймаетlogs.app.error. Не поймает: звёздочка — это одно слово, а не «всё остальное». Нуженlogs.#. - Публикуют с шаблоном в ключе.
basic_publish(routing_key="orders.#")отправит сообщение с буквальным ключомorders.#, где решётка — обычный символ. Такое сообщение, скорее всего, не подойдёт ни одному binding'у и молча исчезнет. Шаблоны — только вqueue_bind. - Идентификаторы в ключе. Плодят binding'и, ломают подписки, взрывают память брокера. ID — в теле.
- Переименовали ключ «просто для порядка». Это ломающее изменение контракта, и падает оно не у вас, а у трёх соседних команд. Мигрируют так: продюсер какое-то время публикует событие под старым и новым ключом, потребители переезжают, старый ключ выпиливают.
- Разный регистр и разделители.
Order.Created,order_createdиorder.created— три разных мира. Договоритесь на lower-case + точки и вбейте это в линтер или в общую библиотеку публикации. - Подписались на
#и фильтруют в коде. Тогда вы платите за трафик, за память очереди и за CPU потребителя ради работы, которую брокер сделал бы бесплатно. Если фильтрация в консюмере — норма, значит, ключи спроектированы не под ваши подписки.
Итоги
- Routing key — контракт между командами; проектируйте его как публичный API.
- Иерархия от общего к частному, фиксированное число сегментов, lower-case и точки.
*— ровно одно слово,#— ноль или больше.logs.*иlogs.#— совсем не одно и то же.- Шаблоны допустимы только в binding key; при публикации ключ всегда конкретный.
- Идентификаторы и полезные данные — в теле сообщения, не в ключе.
- Лимит — 255 байт; ключи пишем латиницей.
- Ведите словарь событий: он дешевле, чем археология по логам через год.