Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке Мікросервісна архітектура?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Мікросервісна архітектура** - це підхід, де застосунок розбивається на незалежні сервіси, кожен з власною базою даних і комунікацією через API або черги повідомлень. Головний компроміс: отримуєш ізоляцію збоїв і незалежні деплойменти, але додаєш складність розподіленої системи. **Ключове правило:** один сервіс, одна база даних.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Мікросервісна архітектура** - це підхід до побудови застосунків, де система розбивається на невеликі незалежні сервіси, кожен з яких відповідає за окрему бізнес-функцію і має власну базу даних. ## Теорія ### TL;DR - Кожен сервіс робить одну річ і деплоїться незалежно від інших - Сервіси спілкуються через HTTP/REST, gRPC або черги повідомлень - Kafka або RabbitMQ - Кожен сервіс має власну базу даних - спільна БД між сервісами є антипаттерном - Найскладніше не написати сервіси, а впоратись зі збоями в розподіленій системі - Закон Конвея: твої мікросервіси відображатимуть структуру твоєї організації, хочеш ти цього чи ні ### Приклад ```javascript // 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: Спільна база даних між сервісами** ```javascript // НЕПРАВИЛЬНО: два сервіси підключені до однієї бази даних // UserService читає з таблиці orders - це домен OrderService const orders = await db.query( 'SELECT * FROM orders WHERE user_id = ?', [userId] ); // UserService тепер залежить від схеми OrderService // Будь-яка міграція в OrderService може зламати UserService непомітно ``` **Помилка 2: Довгий синхронний ланцюжок викликів** ```javascript // НЕПРАВИЛЬНО: 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 і обробка збоїв між сервісами. Ці витрати окупаються тільки тоді, коли координація в моноліті стає справжнім вузьким місцем, а не як проактивний архітектурний вибір. ## Приклади ### Виокремлення сервісу з моноліту ```javascript // ДО: Усе в одному 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 ```javascript 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 для розподілених транзакцій ```javascript // 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, без спільного координатора транзакцій. Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.