Пошук уроків, статей та іншого контенту
Розгляне публікацію подій у теми, підписки споживачів і незалежне отримання повідомлень кількома сервісами.
Pub/Sub (Publish/Subscribe, публікація/підписка) — це модель обміну повідомленнями, у якій відправник події не звертається безпосередньо до конкретного отримувача.
Замість цього:
видавець (publisher) публікує повідомлення в тему (topic);
споживачі (subscribers) підписуються на тему;
система доставки передає повідомлення всім відповідним підпискам.
Схематично модель виглядає так:
Publisher
|
v
Topic
/ \
v v
Підписка A Підписка B
| |
Сервіс email Сервіс аналітикиВидавець не знає:
які саме сервіси отримають подію;
скільки таких сервісів існує;
коли вони оброблять повідомлення;
як вони реалізують свою бізнес-логіку.
Це створює слабке зв’язування між компонентами системи.
Publisher створює та публікує повідомлення.
Наприклад, сервіс замовлень може опублікувати подію:
{
"type": "order.created",
"orderId": "order-123",
"userId": "user-42",
"total": 1499
}Видавець не повинен викликати сервіс електронної пошти або аналітики напряму. Він лише повідомляє, що замовлення створено.
Topic — логічний канал, у який публікуються повідомлення певного типу або призначення.
Приклади тем:
orders
payments
user-events
notifications
Тему можна розглядати як потік подій, пов’язаних із певною областю системи.
Subscription визначає, як конкретний споживач отримує повідомлення з теми.
Одна тема може мати кілька незалежних підписок:
Topic: orders
Subscription: email-service
Subscription: analytics-service
Subscription: warehouse-serviceКожна підписка має власний стан доставки. Якщо сервіс аналітики тимчасово недоступний, це не повинно блокувати сервіс електронної пошти.
Subscriber або consumer читає повідомлення з підписки та виконує певну дію.
Наприклад:
сервіс електронної пошти надсилає лист клієнту;
сервіс аналітики записує подію;
сервіс складу резервує товар.
Одна й та сама подія може бути оброблена кількома сервісами незалежно.
Розглянемо подію order.created.
Сервіс замовлень
|
| публікує order.created
v
orders
/ \
v v
email-sub analytics-sub
| |
Email-сервіс АналітикаСервіс замовлень створює замовлення.
Він публікує подію в тему orders.
Pub/Sub-система створює доступне повідомлення для кожної підписки.
Email-сервіс отримує власну копію повідомлення.
Аналітичний сервіс отримує власну копію повідомлення.
Сервіси підтверджують успішну обробку незалежно один від одного.
Це називають fan-out — одна опублікована подія розповсюджується до кількох споживачів.
Нижче наведено спрощену реалізацію Pub/Sub на JavaScript. Вона працює в пам’яті та демонструє основну модель: одна тема, кілька підписок і незалежна обробка повідомлень.
class PubSub {
constructor() {
this.topics = new Map();
}
createTopic(topicName) {
if (!this.topics.has(topicName)) {
this.topics.set(topicName, new Map());
}
}
subscribe(topicName, subscriptionName, handler) {
this.createTopic(topicName);
const subscriptions = this.topics.get(topicName);
if (subscriptions.has(subscriptionName)) {
throw new Error(`Підписка "${subscriptionName}" уже існує`);
}
subscriptions.set(subscriptionName, {
handler,
queue: [],
processing: false
});
}
publish(topicName, message) {
const subscriptions = this.topics.get(topicName);
if (!subscriptions) {
throw new Error(`Тема "${topicName}" не існує`);
}
for (const subscription of subscriptions.values()) {
subscription.queue.push(message);
this.processQueue(subscription);
}
}
async processQueue(subscription) {
if (subscription.processing) {
return;
}
subscription.processing = true;
while (subscription.queue.length > 0) {
const message = subscription.queue[0];
try {
await subscription.handler(message);
// Видаляємо повідомлення лише після успішної обробки.
subscription.queue.shift();
} catch (error) {
console.error("Помилка обробки повідомлення:", error.message);
// Зупиняємо цю підписку, щоб не втратити повідомлення.
break;
}
}
subscription.processing = false;
}
}
const broker = new PubSub();
broker.subscribe("orders", "email-service", async (event) => {
console.log(
`[email-service] Надсилаємо лист для замовлення ${event.orderId}`
);
});
broker.subscribe("orders", "analytics-service", async (event) => {
console.log(
`[analytics-service] Записуємо подію ${event.type} для замовлення ${event.orderId}`
);
});
broker.subscribe("orders", "warehouse-service", async (event) => {
console.log(
`[warehouse-service] Резервуємо товари для замовлення ${event.orderId}`
);
});
broker.publish("orders", {
type: "order.created",
orderId: "order-123",
userId: "user-42",
total: 1499
});Очікуваний результат:
[email-service] Надсилаємо лист для замовлення order-123
[analytics-service] Записуємо подію order.created для замовлення order-123
[warehouse-service] Резервуємо товари для замовлення order-123У цьому прикладі кожна підписка отримує повідомлення окремо. Обробка в email-service не споживає повідомлення для analytics-service.
Різні підписки можуть мати:
різну швидкість обробки;
різний час доступності;
різну логіку повторних спроб;
різний термін зберігання повідомлень;
різні права доступу.
Наприклад, аналітичний сервіс може обробляти події із затримкою, а сервіс надсилання листів — майже одразу. Це не обов’язково впливає один на одного.
Важливо розрізняти дві моделі.
Кожна підписка отримує копію кожного повідомлення:
Topic
|
+-- Subscription A -> Consumer A
|
+-- Subscription B -> Consumer BЦя модель використовується, коли всі сервіси повинні побачити кожну подію.
Іноді один сервіс має кілька екземплярів для масштабування:
Subscription
|
+-- Consumer instance 1
+-- Consumer instance 2
+-- Consumer instance 3У такому разі повідомлення зазвичай розподіляються між екземплярами, а не доставляються кожному з них. Це дозволяє обробляти чергу паралельно.
Отже:
різні підписки реалізують fan-out;
кілька споживачів однієї підписки реалізують горизонтальне масштабування обробки.
Після отримання повідомлення споживач зазвичай має підтвердити його обробку. Таке підтвердження часто називають acknowledgment, або скорочено ack.
Типовий життєвий цикл:
Повідомлення доступне
|
v
Споживач отримав повідомлення
|
v
Успішна обробка?
/ \
так ні
| |
ack повторна спробаПовідомлення не варто видаляти одразу після отримання. Якщо сервіс завершиться з помилкою під час обробки, повідомлення може бути втрачено.
Безпечніша послідовність:
отримати повідомлення;
виконати бізнес-операцію;
підтвердити повідомлення після успішного завершення.
Якщо сталася помилка, система може:
повторно доставити повідомлення;
тимчасово відкласти його;
перемістити його в окрему чергу помилкових повідомлень після кількох невдалих спроб.
Pub/Sub-системи часто гарантують доставку at-least-once — щонайменше один раз.
Це означає, що повідомлення не повинно загубитися, але в окремих ситуаціях воно може бути доставлене повторно. Наприклад:
споживач виконав операцію;
сервіс завершився до надсилання ack;
система вважає повідомлення необробленим;
повідомлення доставляється повторно.
Тому обробник має бути ідемпотентним: повторна обробка тієї самої події не повинна створювати неправильний результат.
Наприклад, замість безумовного створення запису можна перевіряти унікальний ідентифікатор події:
const processedEvents = new Set();
async function handleOrderCreated(event) {
if (processedEvents.has(event.eventId)) {
console.log("Подію вже оброблено:", event.eventId);
return;
}
// Виконуємо операцію лише один раз.
console.log("Створюємо запис для замовлення:", event.orderId);
processedEvents.add(event.eventId);
}
handleOrderCreated({
eventId: "event-001",
type: "order.created",
orderId: "order-123"
});
handleOrderCreated({
eventId: "event-001",
type: "order.created",
orderId: "order-123"
});У реальній системі набір оброблених ідентифікаторів зберігають у зовнішній базі даних або іншому надійному сховищі. Зберігати його лише в пам’яті процесу недостатньо після перезапуску сервісу.
Подія зазвичай містить не лише дані предметної області, а й метадані:
{
"eventId": "event-001",
"type": "order.created",
"occurredAt": "2026-09-02T10:30:00Z",
"version": 1,
"data": {
"orderId": "order-123",
"userId": "user-42",
"total": 1499
}
}Корисні поля:
eventId — унікальний ідентифікатор події;
type — тип події;
occurredAt — час виникнення;
version — версія схеми події;
data — корисне навантаження.
Є два поширені підходи до вмісту події:
передати всі потрібні дані в повідомленні;
передати лише ідентифікатор, а решту даних споживач отримає окремо.
Повідомлення з усіма потрібними даними зменшує кількість додаткових запитів, але може бути більшим. Повідомлення лише з ідентифікатором є компактнішим, проте споживач стає залежним від доступності сервісу, у якого потрібно отримати деталі.
Publisher не залежить від конкретних subscriber-ів. Новий сервіс можна додати, створивши для нього підписку, не змінюючи код видавця.
Кожну підписку можна масштабувати відповідно до її навантаження. Сервіс аналітики та сервіс сповіщень можуть мати різну кількість екземплярів.
Publisher не мусить чекати завершення всіх операцій. Подію можна прийняти та обробити пізніше.
Тимчасова помилка одного споживача не обов’язково зупиняє інших споживачів.
До наявної теми можна під’єднати новий сервіс, не додаючи нових прямих викликів у всі наявні компоненти.
Pub/Sub не усуває складність, а переносить її в іншу частину системи.
Потрібно враховувати:
можливість повторної доставки;
затримки між публікацією та обробкою;
порядок повідомлень;
помилки споживачів;
моніторинг черг і підписок;
сумісність версій формату подій;
контроль зростання кількості повідомлень.
У такій архітектурі дані можуть стати узгодженими не миттєво. Наприклад, замовлення вже створене, але запис в аналітиці з’явиться через кілька секунд. Це називають eventual consistency, або зрештою узгодженістю.
Pub/Sub добре підходить, коли:
одну подію мають обробити кілька незалежних сервісів;
обробку можна виконати асинхронно;
видавець не повинен знати всіх споживачів;
сервіси потрібно масштабувати незалежно;
тимчасові затримки обробки прийнятні.
Приклади:
після створення замовлення надіслати лист, оновити аналітику та запустити резервування товару;
після реєстрації користувача створити профіль і надіслати вітальне повідомлення;
після успішної оплати оновити баланс, сформувати чек і записати подію для аудиту.
Прямий синхронний виклик може бути кращим, якщо видавець повинен негайно отримати результат конкретної операції. Pub/Sub призначений передусім для подій і асинхронної взаємодії.
Якщо кілька сервісів повинні отримати кожну подію, їм потрібні окремі підписки. Кілька екземплярів однієї підписки не створюють копію повідомлення для кожного екземпляра.
Не можна припускати, що кожне повідомлення буде доставлено рівно один раз. Обробник повинен коректно переживати повторну доставку.
Якщо надіслати ack до виконання бізнес-операції, збій процесу може призвести до втрати повідомлення.
Publisher не повинен знати, які дії виконують усі споживачі. Його відповідальність — сформувати коректну подію та опублікувати її.
Зміна структури повідомлення без версії може зламати старих споживачів. Формат події варто змінювати сумісно або явно збільшувати його версію.
Постійна повторна доставка некоректного повідомлення може блокувати чергу. Для таких випадків потрібні обмеження кількості спроб і окрема черга проблемних повідомлень.
Pub/Sub розділяє видавця та споживачів через теми й підписки.
Publisher публікує подію в topic, не звертаючись напряму до сервісів.
Кожна окрема subscription може отримати власну копію повідомлення.
Різні підписки дозволяють незалежно обробляти одну подію кількома сервісами.
Кілька споживачів однієї підписки використовують для паралельного масштабування.
Повідомлення слід підтверджувати після успішної обробки.
Через можливу повторну доставку обробники мають бути ідемпотентними.
Pub/Sub забезпечує слабке зв’язування та асинхронність, але додає затримки й потребу керувати помилками, повторними спробами та версіями подій.