Що таке Мікросервісна архітектура?
Мікросервісна архітектура - це підхід до побудови застосунків, де система розбивається на невеликі незалежні сервіси, кожен з яких відповідає за окрему бізнес-функцію і має власну базу даних.
Теорія
TL;DR
- Кожен сервіс робить одну річ і деплоїться незалежно від інших
- Сервіси спілкуються через HTTP/REST, gRPC або черги повідомлень - Kafka або RabbitMQ
- Кожен сервіс має власну базу даних - спільна БД між сервісами є антипаттерном
- Найскладніше не написати сервіси, а впоратись зі збоями в розподіленій системі
- Закон Конвея: твої мікросервіси відображатимуть структуру твоєї організації, хочеш ти цього чи ні
Приклад
// Order Service - відповідає тільки за замовлення
// Запускається на порту 3001, має власну PostgreSQL
app.post('/orders', async (req, res) => {
const { userId, productId, quantity } = req.body;
// Пишемо тільки у ВЛАСНУ базу даних
const order = await OrderDB.create({ userId, productId, quantity });
// Повідомляємо Inventory Service через чергу, не через пряме звернення до БД
await kafka.send('order.created', { orderId: order.id, productId, quantity });
res.json({ orderId: order.id });
});Order Service ніколи не лізе напряму в базу Inventory Service. Він публікує подію, а Inventory Service реагує на неї. Ось у чому суть декомпозиції.
Моноліт проти мікросервісів
Моноліт запускає все в одному процесі. Той самий код обробляє користувачів, продукти, замовлення та платежі. Деплоїш усе разом. Якщо в модулі платежів є баг - перезбираєш весь застосунок. Якщо одна команда зламала збірку - всі стоять.
Мікросервіси розбивають цей моноліт по бізнес-межах. User Service, Order Service і Payment Service - це окремі процеси, окремі деплойменти, окремі бази даних. Падіння Payment Service не тягне за собою Orders. Деплоймент User Service не вимагає узгодження з трьома іншими командами.
Компроміс реальний. Ти отримуєш ізоляцію та незалежне масштабування, але додаєш мережеві затримки, розподілений трейсинг і eventual consistency до кожної взаємодії, яка раніше була простим локальним викликом функції.
Комунікація між сервісами
Дві основні моделі:
Синхронна (REST, gRPC): сервіс A викликає сервіс B і чекає відповіді. Просто зрозуміти, але якщо B повільний або недоступний - A теж зависає.
Асинхронна (Kafka, RabbitMQ, SQS): сервіс A публікує подію і продовжує роботу. Сервіс B обробляє її коли готовий. Вища стійкість, але з eventual consistency - ти не можеш одразу підтвердити результат на downstream-сервісі.
gRPC швидший за REST для внутрішніх викликів між сервісами (бінарний протокол, HTTP/2). Багато команд використовують REST для публічного API і gRPC для внутрішньої комунікації.
Окрема база даних для кожного сервісу
Це не рекомендація - це правило. Якщо два сервіси використовують спільну БД, вони не є по-справжньому незалежними. Зміна схеми в одному сервісі може зламати інший. Їх не можна масштабувати окремо. Не можна замінити технологію бази даних одного сервісу без впливу на код іншої команди.
На практиці: Order Service має власну PostgreSQL, Inventory Service - MongoDB, Payment Service - окремий інстанс PostgreSQL з суворими транзакційними гарантіями. Кожна команда повністю контролює свою схему.
Побічний ефект: ACID-транзакції між сервісами неможливі. Ця проблема вирішується паттерном Saga - кожен сервіс виконує локальну транзакцію і публікує подію, а компенсуючі транзакції відкочують зміни при збої.
Ключові інфраструктурні патерни
API Gateway: єдина точка входу для зовнішніх клієнтів. Маршрутизує запити, обробляє автентифікацію, rate limiting і агрегацію. Популярні варіанти: Kong, AWS API Gateway, NGINX.
Service Discovery: як сервіс A знає, де запущений сервіс B? Consul або Kubernetes вирішують це автоматично. Сервіси реєструються самостійно, і інші знаходять їх за ім'ям, а не за захардкодженою IP-адресою.
Circuit Breaker: коли сервіс B постійно падає, Circuit Breaker зупиняє сервіс A від нескінченних запитів до нього. Після певного порогу збоїв він відкриває ланцюг і повертає fallback-відповідь миттєво. Resilience4j - стандарт для Java, opossum - для Node.js.
Коли мікросервіси мають сенс
Чесна відповідь: не з самого початку. Більшість успішних мікросервісних систем виокремлювали з працюючого моноліту, а не проектували мікросервісами одразу. Команди, які я бачив, переходили до мікросервісів ще до того як моноліт став реальною проблемою, витрачали місяці на інфраструктурні питання замість фіч.
Мікросервіси окупаються коли:
- Декілька команд хочуть деплоїтись незалежно без координації релізів
- Різні частини системи мають різні вимоги до масштабування (автентифікація проти обробки відео)
- Потрібна різна технологічна база: ML-пайплайн на Python, основний API на Go, фронтенд на Node
- Моноліт дійсно став важко змінювати і повільно збирати
Вони додають зайву складність якщо команда маленька, продукт ще шукає свою форму, або ніхто в команді не мав досвіду з розподіленими системами в продакшені.
Типові помилки
Помилка 1: Спільна база даних між сервісами
// НЕПРАВИЛЬНО: два сервіси підключені до однієї бази даних
// UserService читає з таблиці orders - це домен OrderService
const orders = await db.query(
'SELECT * FROM orders WHERE user_id = ?', [userId]
);
// UserService тепер залежить від схеми OrderService
// Будь-яка міграція в OrderService може зламати UserService непомітноПомилка 2: Довгий синхронний ланцюжок викликів
// НЕПРАВИЛЬНО: A викликає B, B викликає C, C викликає D
const user = await userService.getUser(userId); // 100ms
const product = await productService.getProduct(id); // 100ms
const price = await pricingService.getPrice(id); // 100ms
// Якщо pricingService недоступний - весь запит на замовлення падає
// Затримки складаються: мінімум 300ms, і це щасливий шляхВикористовуй черги повідомлень замість ланцюжків синхронних викликів.
Помилка 3: API без версіонування
Якщо оновлюєш API сервісу B без версіонування - всі клієнти, які його викликають, ламаються на наступному деплойменті. Паралельна робота /v1/orders і /v2/orders дає командам змогу мігрувати у власному темпі без синхронізованих релізів.
Помилка 4: Ігнорування розподіленого трейсингу
Коли маєш десять сервісів, відлагодження одного повільного запиту вимагає його трекінгу через усі ці сервіси. Налаштуй Jaeger, Zipkin або Datadog APM з першого дня. Додавати це ретроспективно після інцидентів набагато болючіше.
Де зустрічається
- Netflix: понад 700 мікросервісів. Один з перших на такому масштабі. Створили Hystrix, Eureka і Ribbon, бо таких інструментів тоді просто не існувало.
- Amazon: на початку 2000-х Джефф Безос видав внутрішній мандат - всі команди повинні передавати дані через API. Саме це організаційне обмеження зробило AWS можливим.
- Uber: перейшов від моноліту до мікросервісів під час глобального масштабування. Пізніше зіткнувся з проблемами координації сотень сервісів і перейшов до domain-oriented microservices.
- Kubernetes + Docker: стандартний шар деплойменту сьогодні. Кожен сервіс запускається в контейнері, Kubernetes обробляє масштабування, health checks і service discovery.
Питання для підготовки до співбесіди
Q: Що таке паттерн Saga і коли його застосовують?
A: Saga - це послідовність локальних транзакцій, де кожен крок публікує подію, що запускає наступний сервіс. Якщо крок падає, компенсуючі транзакції відкочують попередні кроки. Застосовується коли бізнес-операція охоплює декілька сервісів і потребує можливості відкоту, якого не дасть розподілена ACID-транзакція.
Q: Чим мікросервіси відрізняються від SOA (Service-Oriented Architecture)?
A: SOA покладається на центральну Enterprise Service Bus (ESB) і важкі протоколи типу SOAP. Мікросервіси використовують легкі HTTP або gRPC API, уникають центрального брокера, і кожен сервіс менший за обсягом з власною базою даних. SOA-сервіси були великими, мікросервіси мають бути достатньо маленькими щоб одна команда могла їх повністю контролювати.
Q: Чи можна використовувати одну спільну базу для всіх сервісів заради простоти?
A: Технічно так, але тоді сервіси не є незалежними. Будь-яка зміна схеми - це скоординований деплоймент. Неможливо масштабувати зберігання окремо, а повільний запит одного сервісу деградує інші. Правило «база на сервіс» існує саме щоб це зв'язування не виникло.
Q: Як обробляти автентифікацію між сервісами?
A: API Gateway перевіряє JWT один раз і передає ідентифікатор користувача до downstream-сервісів через заголовки запиту. Кожен сервіс довіряє шлюзу і не повторює логіку автентифікації. Деякі команди також використовують окремий Auth Service, який видає короткочасні токени для міжсервісних викликів.
Q: Моноліт добре працює, команда 5 розробників. Чи варто переходити на мікросервіси?
A: Ні. Операційні витрати реальні: окремі CI/CD пайплайни, розподілений трейсинг, eventual consistency і обробка збоїв між сервісами. Ці витрати окупаються тільки тоді, коли координація в моноліті стає справжнім вузьким місцем, а не як проактивний архітектурний вибір.
Приклади
Виокремлення сервісу з моноліту
// ДО: Усе в одному Express-застосунку
app.post('/orders', async (req, res) => {
const user = await db.users.findById(req.body.userId);
const product = await db.products.findById(req.body.productId);
const order = await db.orders.create({ userId: user.id, productId: product.id });
await db.inventory.decrement(req.body.productId);
res.json(order);
});
// ПІСЛЯ: Order Service обробляє тільки замовлення
// Inventory живе в окремому сервісі
app.post('/orders', async (req, res) => {
// Отримуємо користувача від User Service через HTTP
const userRes = await fetch(`http://user-service/users/${req.body.userId}`);
const user = await userRes.json();
// Пишемо тільки у ВЛАСНУ базу
const order = await orderDB.create({ userId: user.id, productId: req.body.productId });
// Публікуємо подію - Inventory Service сам обробить декремент
await kafka.publish('order.created', {
productId: req.body.productId,
quantity: req.body.quantity
});
res.json(order);
});Після розбиття команда Order деплоїться незалежно. Для відвантаження фічі не потрібні Inventory чи Product команди в кімнаті. Кожен сервіс масштабується окремо: якщо зростає кількість замовлень, масштабуєш тільки Order Service.
Паттерн Circuit Breaker з fallback
const CircuitBreaker = require('opossum');
async function callPaymentService(payload) {
const res = await fetch('http://payment-service/pay', {
method: 'POST',
body: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' }
});
return res.json();
}
const paymentBreaker = new CircuitBreaker(callPaymentService, {
timeout: 3000, // помилка якщо відповідь довше 3с
errorThresholdPercentage: 50, // відкрити ланцюг після 50% збоїв
resetTimeout: 30000 // повторна спроба через 30с
});
paymentBreaker.fallback(() => ({
status: 'pending',
message: 'Платіж поставлено в чергу на повторну спробу'
}));
app.post('/checkout', async (req, res) => {
const result = await paymentBreaker.fire(req.body);
res.json(result);
});Коли Payment Service недоступний, ланцюг відкривається після досягнення порогу. Подальші запити миттєво отримують fallback замість того щоб чекати 3 секунди на таймаут кожен. Замовлення продовжують оброблятись, платіж буде повторений пізніше.
Паттерн Saga для розподілених транзакцій
// Order Service - запускає сагу
async function createOrderSaga({ userId, productId, quantity, orderId }) {
// Крок 1: запитуємо резервування товару
await kafka.send('inventory.reserve', { productId, quantity, orderId });
// Крок 2 спрацює після події 'inventory.reserved' (Inventory Service)
// Крок 3 спрацює після події 'payment.charged' (Payment Service)
}
// Inventory Service реагує на запит резервування
kafka.on('inventory.reserve', async ({ productId, quantity, orderId }) => {
const available = await inventoryDB.checkStock(productId, quantity);
if (available) {
await inventoryDB.reserve(productId, quantity);
await kafka.send('inventory.reserved', { orderId });
} else {
// Компенсуюча подія - Order Service скасує замовлення
await kafka.send('inventory.reserve.failed', { orderId, reason: 'Out of stock' });
}
});
// Order Service обробляє збій і компенсує
kafka.on('inventory.reserve.failed', async ({ orderId, reason }) => {
await orderDB.cancel(orderId, reason);
await kafka.send('order.cancelled', { orderId });
});Кожен сервіс виконує свою локальну роботу і публікує подію. Сага обробляє ланцюжок відкоту при збої. Без розподіченого блокування, без двофазного commit, без спільного координатора транзакцій.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.