Экспресс-курс · No. 18
Когда одной части системы нужно, чтобы другая что-то сделала, простой путь — позвать её и ждать ответа. Мощный путь — бросить сообщение в очередь и идти дальше, дав другой стороне забрать его, когда она готова. Этот один сдвиг — от «позвать и ждать» к «оставить сообщение и продолжить» — это как системы остаются быстрыми, переживают сбои и масштабируются.
Только суть · Один образ на идею · Выучи слова
Чтобы увидеть, зачем существуют очереди, сперва почувствуй боль простой альтернативы: один сервис зовёт другой и ждёт его. Это работает до того мига, когда становится по-настоящему важно.
Прямой вызов связывает два сервиса
Позвонить кому-то и оставаться на линии, пока он не закончит задачу, — нельзя положить трубку, нельзя делать что-то ещё, а если он не берёт, ты застрял.
Очевидный способ для сервиса A заставить сервис B что-то сделать — синхронный вызов: A спрашивает, потом ждёт, удерживая всё, пока B не ответит. Это туго связывает (couples) их — скорость A теперь заложник скорости B, и A не может продолжить, пока B не готов. Для быстрого ответа это нормально. Для медленной или тяжёлой работы A проводит время замороженным, в ожидании кого-то другого.
Если вызываемый лежит, вызывающий ломается
Звонить на склад подтвердить заказ и, раз никто не отвечает, отказываться брать любые новые заказы вообще — одна закрытая дверь клинит весь магазин.
Глубже лежит проблема сбоя. Если B лежит или перегружен, когда A зовёт, запрос A тоже падает — сбой распространяется (propagates) прямо вверх по цепочке. Один медленный или сломанный сервис может застопорить всё, что от него зависит, а всплеск трафика к B тянет A за собой вниз. Тугая связь значит, что слабость расходится, и система доступна ровно настолько, насколько её самая шаткая часть.
Некоторая работа не должна блокировать пользователя
На кассе ты платишь и уходишь с чеком — ты не стоишь у кассы, пока они пакуют, отгружают и шлют тебе письмо. Медленные части случаются после того, как ты ушёл.
Куча работы не обязана завершиться до того, как пользователь получит ответ, — отправить подтверждающее письмо, сгенерировать счёт, уменьшить фото. Заставлять пользователя ждать всего этого медленно и бессмысленно. Ты хочешь принять запрос, ответить быстро и дать медленным частям случиться после. Прямой вызов так не может; он заставляет всех ждать всего.
Прямой вызов связывает два сервиса во времени: вызывающий ждёт и наследует медлительность и сбои вызываемого. Для медленной или несрочной работы это ловушка.
Лекарство — перестать звать и начать слать сообщения. Очередь — это буфер посередине, что даёт одной стороне оставить работу, а другой забрать её, расцепляя их во времени.
Брось сообщение и иди дальше
Оставить голосовое вместо ожидания на линии: ты говоришь, что нужно, кладёшь трубку и занимаешься своим днём — они разберутся, когда смогут.
Вместо того чтобы звать B и ждать, A пишет сообщение (message), описывающее работу, и бросает его в очередь (queue) — линию, где сообщения ждут обработки. A теперь свободен продолжить немедленно; ему всё равно, когда B до него доберётся. Сообщение — это маленький пакет «вот что надо сделать», переданный, а не выполненный на месте. Эта передача и есть вся идея.
Очередь — это буфер во времени
Лоток входящих на столе: работа копится в нём и обрабатывается ровно, так что внезапный наплыв бумаг не захлёстывает человека — он лишь делает лоток выше на время.
Очередь сидит между двумя сторонами как буфер, держа сообщения, пока их не обработают. Это расцепляет их во времени (decouples in time): отправитель и получатель больше не обязаны быть быстрыми, доступными или даже работающими в один момент. Работу, произведённую всплеском, можно потреблять ровно. Очередь впитывает несоответствие между тем, как быстро работа приходит, и тем, как быстро её можно сделать.
Асинхронно значит не ждать результата
Отправить письмо, а не вести разговор, — ты шлёшь его и продолжаешь, веря, что его прочтут и по нему сработают позже, без того чтобы ты стоял там.
Это асинхронная (asynchronous) работа: отправитель не ждёт результата. Он выстреливает сообщение и продолжает, а исход случается позже, в стороне. Ты отдаёшь мгновенный ответ прямого вызова, а взамен перестаёшь быть заблокированным. Для всего, что не требует немедленного ответа, этот размен — фундамент быстрых, устойчивых систем.
Очередь — это буфер между отправителем и получателем. Брось сообщение и иди дальше — работа расцеплена во времени, сделана асинхронно, когда другая сторона готова.
Три роли заставляют очередь работать, и их именование снимает бо́льшую часть жаргона. Одна сторона делает сообщения, одна их обрабатывает, а что-то посередине держит их безопасно.
Producer делает работу; consumer её выполняет
Кухня: официанты накалывают тикеты заказов, а повара снимают их один за одним, чтобы готовить. Две стороны никогда не говорят напрямую — рейка тикетов это весь интерфейс.
Сторона, что создаёт сообщения, — producer (производитель, или publisher); сторона, что их обрабатывает, — consumer (потребитель, или worker). Producer бросает тикет; consumer берёт один, делает работу и идёт к следующему. Они не знают и не ждут друг друга — они знают только очередь. Это чистое разделение и позволяет каждой стороне строиться, масштабироваться и меняться самой по себе.
Брокер держит сообщения безопасно
Почта между отправителем и получателем: она берёт твоё письмо, хранит его в безопасности и держит, пока получатель не заберёт, — чтобы ничто не потерялось, если его нет.
Посередине сидит брокер (broker) — софт вроде RabbitMQ или Apache Kafka, что принимает сообщения, надёжно их хранит и доставляет consumer'ам. Это почта системы. Его надёжность — в этом суть: даже если все consumer'ы лежат, брокер держит сообщения в безопасности, пока кто-то не готов обработать, так что работа припаркована, а не потеряна.
Добавь consumer'ов, чтобы идти быстрее
Когда тикеты заказов копятся, ты ставишь больше поваров на линию — каждый берёт следующий тикет, и завал расчищается быстрее, не меняя того, как заказы приходят.
Поскольку consumer'ы просто тянут следующее сообщение, можно гонять многих параллельно на одной очереди. Завал растёт? Добавь больше worker'ов, и они делят нагрузку автоматически — каждый хватает разное сообщение. Это одна из больших сил паттерна: ты масштабируешь медленную часть независимо, добавляя consumer'ов, не трогая producer'ов или саму очередь.
Producer'ы создают сообщения, consumer'ы их обрабатывают, а брокер держит их безопасно посередине. Нужно больше пропускной? Добавь consumer'ов — они делят очередь автоматически.
Есть две очень разные вещи, чем может быть сообщение: приказ что-то сделать или объявление, что что-то случилось. Второе тихо меняет то, как проектируют целые системы.
Команда велит одному worker'у сделать задачу
Рабочий наряд, вручённый одному конкретному отделу: «уменьши эту картинку». Он называет работу, ждёт, что ровно одна команда её сделает, и на этом всё.
Сообщение-команда (command) — это прямая инструкция, нацеленная на одного consumer'а: «отправь это письмо», «обработай этот платёж». Это очередь, используемая как список дел — producer знает, что должно случиться, и передаёт задачу, чтобы её сделали раз, тем, кто её вытянет. Это простейшее использование очереди: сгрузить конкретную работу, чтобы сделать её позже, ровно один раз.
Событие объявляет, что что-то случилось
Объявление на доске для всей компании: «только что зарегистрировался новый клиент». Оно никому не велит, что делать, — кто заинтересован, реагирует по-своему.
Событие (event) — другое: оно констатирует факт о прошлом — «заказ размещён», «пользователь зарегистрирован» — не говоря, кто что должен делать. Producer просто объявляет его и не знает и не заботится, кто слушает. Может, никто не среагирует; может, пять сервисов. Это переворачивает отношение: вместо того чтобы командовать конкретному worker'у, ты вещаешь, что что-то истинно, и даёшь заинтересованным сторонам решать.
Pub/sub: одно событие, много реакций
Газета, напечатанная раз и доставленная каждому подписчику, каждый из которых читает её по своим причинам — болельщик, инвестор, разгадыватель кроссвордов — из одного и того же выпуска.
Эта модель вещания — publish/subscribe (pub/sub): producer публикует событие в топик (topic), а каждый заинтересованный consumer подписывается и получает свою копию. Одно событие «заказ размещён» может запустить сервис писем, сервис аналитики и сервис отгрузки разом — это fan-out (веерная рассылка). Публикатор даже не знает, что они существуют. Новые реакции можно добавить позже, вообще не трогая публикатора.
События расцепляют, кто кого знает
Добавление нового подписчика к газете ничего не меняет в том, как газета пишется, — публикатору никогда не надо знать, кто читает.
В этом глубокая мощь событийного (event-driven) дизайна: producer события полностью расцеплен со всеми, кто на него реагирует. Чтобы добавить новую фичу, что откликается на «заказ размещён», ты просто подписываешь нового consumer'а — никаких изменений в сервисе заказов. Системы, построенные так, растут добавлением слушателей, а не правкой того, что слушают. Так большие системы развиваются без того, чтобы всё зависело от всего.
Команда велит одному consumer'у сделать задачу. Событие объявляет факт и даёт любому реагировать — а pub/sub веером раскидывает одно событие многим, расцепляя, кто кого знает.
Очереди добавляют движущихся частей, так что они должны это оправдать. Они оправдывают, покупая три вещи, что трудно получить иначе: устойчивость, гладкую обработку всплесков и независимое масштабирование.
Устойчивость: лежащий consumer просто задерживает работу
Если кухня закрывается на час, тикеты заказов просто ждут на рейке — когда она открывается, повара прорабатывают завал. Ни один заказ не потерян.
Поскольку брокер держит сообщения, падение consumer'а не теряет работу — сообщения ждут, пока он не вернётся, потом обрабатываются. Сравни с прямым вызовом, где лежащий сервис означает прямой сбой. Очередь превращает «сервис лежит» из жёсткой ошибки во временную задержку. Эта устойчивость (resilience) — работа переживает сбой и возобновляется — часто единственная самая большая причина, почему команды тянутся к очереди.
Выравнивание нагрузки: впитать всплески
Водохранилище между паводком и городом: волна вливается в водохранилище, что выпускает воду ровным, безопасным темпом ниже по течению.
Когда наплыв запросов приходит разом — распродажа, вирусный момент — очередь даёт выровнять нагрузку (level the load): всплеск наполняет очередь, а consumer'ы осушают её своим ровным темпом. Медленная нижестоящая система никогда не видит всплеска, только управляемый, ровный поток. Давление завала называется backpressure (обратное давление). Без очереди тот же всплеск ударил бы по системе напрямую и, вероятно, опрокинул бы её.
Масштабируй и развивай каждую сторону саму по себе
Можно нанять больше поваров, сменить оборудование кухни или добавить новую станцию — всё без изменения того, как официанты берут заказы, ведь они касаются только рейки тикетов.
Расцепление значит, что каждая сторона может меняться независимо. Масштабируй consumer'ов вверх или вниз под спрос; перепиши consumer на другом языке; добавь совсем новый consumer, реагирующий на существующие события, — всё, не трогая producer'ов. Очередь — это стабильный шов между частями системы, а стабильные швы — вот что даёт большой системе расти и меняться по частям, а не всё разом.
Очереди покупают устойчивость (работа переживает падение), выравнивание нагрузки (всплески впитываются) и независимое масштабирование — выгоды, что прямой вызов попросту не даст.
Асинхронные сообщения решают реальные проблемы и создают новые. Ни одна не критична, но притворяться, что их нет, — вот как очередные системы получают тонкие, бесящие баги.
Сообщения могут прийти дважды
Почта, на всякий случай, иногда доставляет копию письма, в доставке которого не уверена, — лучше дважды, чем никогда, но теперь ты можешь сработать по одной инструкции дважды.
Большинство брокеров гарантируют доставку at-least-once (хотя бы один раз): они скорее пошлют сообщение дважды, чем рискнут потерять его, так что consumer может изредка получить одно сообщение больше раза. Если «списать с клиента» сработает дважды — это реальная проблема. Лекарство — идемпотентность (idempotency): спроектировать работу так, чтобы сделать её дважды имело тот же эффект, что и раз (сперва проверь «уже списано?»). Предполагай дубликаты; делай их безвредными.
Порядок не гарантирован
Два письма, отправленные подряд, могут прийти в любом порядке, — так что если шаг два появляется раньше шага один, получатель в замешательстве.
При многих сообщениях и многих параллельных consumer'ах сообщения не обязательно обрабатываются в порядке отправки. Если «обнови адрес» и «удали аккаунт» приходят не по порядку, получаешь бессмыслицу. Некоторые системы сохраняют порядок внутри категории ценой усилий; часто дешевле спроектировать работу, что не зависит от строгого порядка. Так или иначе, никогда не предполагай порядок, если специально его не устроил.
Результат согласован в итоге
Новость, расходящаяся по офису: пару мгновений одни знают, а другие нет, пока слух не дойдёт до всех и они снова не согласятся.
Поскольку реакции случаются асинхронно, система согласована в итоге (eventually consistent): сразу после события разные части могут кратко расходиться — заказ существует, но аналитика его ещё не сосчитала. Она нагоняет за мгновения, но «мгновенно согласовано везде» ушло. Ты проектируешь вокруг этого зазора, показывая пользователям разумные промежуточные состояния, а не предполагая, что каждая часть обновляется в один миг.
Отравленному сообщению нужно куда-то деться
Письмо, по которому никто не может сработать — смазанное, невозможное, — нельзя бесконечно перепосылать, клиня линию. Его откладывают в особый лоток, чтобы кто-то посмотрел.
Некоторые сообщения нельзя обработать успешно никогда — кривые или для данных, которых уже нет. Оставленный сам по себе, consumer перепосылает их вечно и блокирует очередь. Стандартный ответ — dead-letter queue (очередь мёртвых писем): после пары неудачных попыток сообщение отодвигается в отдельную очередь на разбор, чтобы перестать отравлять поток. Планируй сообщения, что падают, иначе одно плохое сообщение застопорит всё за ним.
Асинхронные сообщения несут дубликаты (лечи идемпотентностью), негарантированный порядок, согласованность в итоге и падающие сообщения (используй dead-letter queue). Планируй все четыре.
Очередь — силовой инструмент, а не дефолт. Навык — тянуться к ней, когда расцепление правда окупается, и оставлять прямой вызов, когда простота стоит больше.
Используй очередь, когда проблема — это ожидание
Ты берёшь номерок и уходишь за медленной услугой, что заберёшь позже, — но для быстрого «да или нет» ты просто спрашиваешь у стойки, ведь брать талон было бы глупее, чем подождать.
Тянись к очереди, когда работа медленная, всплесковая или несрочная, когда хочешь, чтобы вызывающий остался быстрым, или когда несколько сервисов должны среагировать на одно. Оставь прямой вызов, когда нужен немедленный ответ, чтобы продолжить, — поиск цены, проверка прав, — потому что там простота и мгновенный результат стоят больше расцепления. Подбирай инструмент под то, является ли ожидание реальной проблемой.
Не тянись к ней рефлексом
Установить целую почтовую комнату с системой сортировки, чтобы передать записку человеку за соседним столом, — машинерия теперь стоит больше проблемы, что решает.
Очереди добавляют реальную сложность: брокер, что надо гонять, дубликаты и порядок, что надо обрабатывать, согласованность в итоге, что надо проектировать, и тяжелее проследить запрос, что теперь скачет через сообщения. Для простой системы с быстрым вызовом между двумя частями эти накладные не стоят того. Добавляй очередь, когда конкретная выгода — устойчивость, масштаб, расцепление — оправдывает цену, а не потому, что «событийное» звучит продвинуто.
- Проблема ли ожидание — работа медленная, всплесковая или несрочная настолько, чтобы расцеплять? - Команда или событие — один worker делает задачу или много сервисов реагируют на факт? - Идемпотентна ли работа — безопасна для двойного запуска, раз сообщения могут прийти дважды? - Предполагает ли она порядок, и устроил ли я его или спроектировал так, чтобы не зависеть? - Может ли система терпеть краткую согласованность-в-итоге? - Куда деваются упавшие сообщения — есть ли dead-letter queue?
- synchronous / asynchronous — позвать и ждать против отправить и продолжить. - coupling / decoupling — связаны во времени против свободны друг от друга. - queue / message / broker — буфер, единица работы, софт, что их держит. - producer / consumer — сторона, что делает сообщения, сторона, что их обрабатывает. - command / event — сделай эту задачу против это случилось. - pub/sub / topic / fan-out — вещать одно событие многим подписчикам. - at-least-once / идемпотентность / dead-letter queue / eventual consistency — асинхронные опасности и их лекарства.
- Ты ставишь в очередь медленную, всплесковую, несрочную работу и держишь прямые вызовы для мгновенных ответов. - Твои consumer'ы идемпотентны, так что дубликат сообщения не вредит.
- Ты не предполагаешь порядок, если явно его не устроил. - Ты проектируешь под согласованность в итоге, а не ждёшь мгновенного согласия. - Упавшие сообщения садятся в dead-letter queue, а не клинят линию.
Очереди намеренны: тянись к одной, когда расцепление во времени покупает устойчивость, масштаб или fan-out, — и оставляй прямой вызов, когда мгновенный ответ важнее всего этого.