Повідомлення, якими мікросервіси обмінюються через брокер повідомлень (message broker), можна поділити щонайменше на дві категорії: команди (commands) та події (events). Брокер у вміст не заглядає: і команда, і подія для нього — просто повідомлення, тобто тіло (JSON, Avro, Protobuf тощо) і заголовки. Проте це різні концепції, і їх варто явно розрізняти, коли проєктуєте взаємодію між мікросервісами.
Розберімо, що таке команди й події та які ключові відмінності між ними.
Що таке команди (Commands)
Уявімо систему з кількох мікросервісів. Мікросервіс Audit приймає від інших команду створити й зберегти запис аудиту (audit log entry). Так ми можемо відстежувати, хто з користувачів, що і коли зробив у системі. Ось високорівнева діаграма:
CreateAuditLogEntry у чергу, а виконує її лише Audit
Коли мікросервісу Profile чи Order потрібно створити запис аудиту, він надсилає в брокер команду з потрібними даними. Мікросервіс Audit отримує цю команду і створює запис. Зазвичай команди надсилають у чергу (queue). Це модель point-to-point: кожне повідомлення з черги забирає лише один споживач.
Спробуймо виділити важливі характеристики команд:
- Команду можуть надсилати кілька відправників. І
Profile, іOrderнадсилають у брокер команду, коли їм потрібно створити запис аудиту. - Команда має лише одного споживача. Команду
CreateAuditLogEntryобробляє тільки мікросервісAudit. Ідеться про логічного споживача:Auditможе працювати в кількох екземплярах, які конкурують за повідомлення з однієї черги (патерн Competing Consumers), але кожне повідомлення отримає лише один із них. Водночас «один споживач» не означає «рівно одна обробка»: більшість брокерів гарантують доставку at-least-once, тож та сама команда може прийти повторно, і обробник має бути ідемпотентним. Якщо ж ту саму команду обробляють кілька різних мікросервісів, це, найімовірніше, свідчить про проблему в архітектурі. - Мета команди — викликати певну поведінку.
ProfileіOrderнадсилають команду, щобAuditстворив запис аудиту. - Контракт команди належить споживачу. Лише мікросервіс
Auditвизначає схему команди й відповідає за неї. Відправники залежать від цього контракту: якщоAuditзмінить схему, їм доведеться оновитися. - Команда зазвичай містить усі дані, потрібні для виконання операції. Ці дані має надати відправник.
- Назва команди — дієслово в наказовому способі:
CreateAuditLogEntry,SendEmail,ReserveStock. Один мікросервіс каже іншому, що зробити, тож назва звучить як розпорядження.
Ще одна важлива властивість команди: споживач може її відхилити. Наприклад, відправник помилково передав некоректні дані. Тоді споживач валідує команду, відхиляє її, і стан системи не змінюється. Але через брокер відправник не отримує відповіді одразу. Повідомити його про відмову можна лише окремим повідомленням — відповіддю (reply) у чергу відправника або подією на кшталт AuditLogEntryRejected. Якщо відправнику потрібно дізнатися результат негайно, простіше реалізувати команду синхронно — через HTTP або gRPC.
Що таке події (Events)
Тепер погляньмо на іншу діаграму, щоб зрозуміти, що таке подія:
ArticlePublished у топік, і кожен підписник отримує власну копію
У нашому прикладі мікросервіс Article публікує подію ArticlePublished. Мікросервіс Notification отримує цю подію і надсилає email-сповіщення всім підписникам автора. Мікросервіс Analytics отримує ту саму подію, щоб оновити статистику.
Тепер виділимо важливі характеристики подій:
- Подію публікує лише один видавець. Якщо ту саму подію публікують кілька різних мікросервісів, це може свідчити про порушення принципу єдиної відповідальності на рівні архітектури.
- Подія може мати нуль, одного або багато споживачів. Будь-яка кількість мікросервісів може підписатися на подію, щоб відреагувати на неї. Видавець не знає, хто саме підписаний, — і не повинен знати.
- Подія повідомляє решту системи, що щось сталося. Наприклад,
ArticlePublishedз’являється вже після того, як статтю опублікували. Споживачу лишається прийняти цей факт і відреагувати. - Контракт події належить видавцю. Мікросервіс
Articleвизначає схему подіїArticlePublishedі відповідає за неї. Підписники залежать від цього контракту, тобто залежність спрямована навпаки, ніж у команди. - Подія містить дані про те, що сталося, але не дані, потрібні конкретному споживачу. Наприклад,
ArticlePublishedнесе ідентифікатори статті та її автора, але не список email-адрес, на які треба надіслати сповіщення. Ці дані належать мікросервісуNotification: він сам знаходить підписників автора заauthorId. Скільки даних класти в подію — окреме проєктне рішення. Це може бути мінімальне сповіщення лише з ідентифікаторами (event notification) або подія з повним станом сутності (event-carried state transfer), яка позбавляє споживачів зворотних запитів до видавця. - Подія описує факт у минулому, тому її назва — дієслово в минулому часі:
ArticlePublished,ArticleDeleted…
Підсумок
Зведімо відмінності між командами та подіями в одну таблицю:
| Команда | Подія | |
|---|---|---|
| Хто надсилає | Один або кілька відправників | Лише один видавець |
| Хто отримує | Рівно один споживач | Нуль, один або багато підписників |
| Канал у брокері | Черга, point-to-point | Топік, publish-subscribe |
| Мета | Викликати певну поведінку в конкретного споживача | Повідомити всіх зацікавлених, що щось сталося |
| Хто про кого знає | Відправник знає, хто виконає команду | Видавець не знає своїх підписників |
| Власник контракту | Споживач | Видавець |
| Дані | Усе, що потрібно для виконання дії | Дані про сам факт |
| Стан системи | Споживач вирішує, виконати команду і змінити стан чи відхилити її | Стан уже змінився, споживачу лишається прийняти факт |
| Назва | Дієслово в наказовому способі | Дієслово в минулому часі |
Як працювати з командами й подіями в Azure Service Bus і що робити з ідемпотентністю, retry та dead-letter, розбираємо на курсі «Microsoft Azure для розробників».
Це переклад моєї англомовної статті «Commands vs Events in Microservices», опублікованої на Medium. В українській версії додано, через які канали брокера зазвичай передають команди й події, і уточнено кілька формулювань.