Пошук уроків, статей та іншого контенту
Порівняєте монолітну та мікросервісну архітектури, їхні переваги, недоліки й критерії вибору.
Архітектура визначає, як частини застосунку організовані, взаємодіють між собою та розгортаються.
У цьому уроці розглянемо два підходи:
монолітну архітектуру;
мікросервісну архітектуру.
Обидва підходи можуть бути правильними. Вибір залежить від розміру системи, команди, вимог до масштабування та складності продукту.
Моноліт — це застосунок, який розгортається як одна одиниця.
Усі основні частини системи працюють в одному процесі:
HTTP API;
бізнес-логіка користувачів;
робота із замовленнями;
доступ до бази даних;
автентифікація.
Це не означає, що весь код має бути в одному файлі. Моноліт можна поділити на модулі та зберегти чіткі межі між частинами системи.
У NestJS модулі дають змогу організувати моноліт за функціональними областями:
src/
├── users/
│ ├── users.module.ts
│ ├── users.controller.ts
│ └── users.service.ts
├── orders/
│ ├── orders.module.ts
│ ├── orders.controller.ts
│ └── orders.service.ts
├── app.module.ts
└── main.tsУ такому випадку users і orders є частинами одного застосунку. Вони можуть мати окрему бізнес-логіку, але запускаються та розгортаються разом.
Після створення стандартного проєкту NestJS командою nest new app файл src/main.ts можна замінити таким прикладом:
import 'reflect-metadata';
import {
Controller,
Get,
Injectable,
Module,
} from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
@Injectable()
class UsersService {
findAll() {
return [
{ id: 1, name: 'Олена' },
{ id: 2, name: 'Андрій' },
];
}
}
@Controller('users')
class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.findAll();
}
}
@Module({
controllers: [UsersController],
providers: [UsersService],
})
class UsersModule {}
@Injectable()
class OrdersService {
findAll() {
return [
{ id: 101, userId: 1, total: 1250 },
{ id: 102, userId: 2, total: 800 },
];
}
}
@Controller('orders')
class OrdersController {
constructor(private readonly ordersService: OrdersService) {}
@Get()
findAll() {
return this.ordersService.findAll();
}
}
@Module({
controllers: [OrdersController],
providers: [OrdersService],
})
class OrdersModule {}
@Module({
imports: [UsersModule, OrdersModule],
})
class AppModule {}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
console.log('Застосунок запущено на http://localhost:3000');
}
bootstrap();У цьому прикладі:
UsersModule відповідає за користувачів;
OrdersModule відповідає за замовлення;
обидва модулі працюють в одному NestJS-застосунку;
застосунок запускається одним процесом;
API доступне через один сервер.
Після запуску можна звернутися до таких маршрутів:
GET http://localhost:3000/users
GET http://localhost:3000/ordersЦе модульний моноліт: застосунок є монолітом на рівні розгортання, але код поділений на логічні модулі.
Усі частини системи знаходяться в одному проєкті. Розробнику не потрібно перемикатися між кількома репозиторіями або запускати багато сервісів.
Для запуску зазвичай достатньо:
одного процесу NestJS;
однієї бази даних;
одного набору змінних середовища.
Запит проходить усередині одного процесу. Його можна відстежувати звичайним debugger, а помилки легше відтворити.
Застосунок збирається та розгортається як одна одиниця. Не потрібно координувати незалежні релізи кількох сервісів.
Виклик іншого сервісу відбувається без мережі:
const user = this.usersService.findById(userId);Це зазвичай простіше та швидше, ніж HTTP-запит або повідомлення через брокер.
З часом кодова база може стати великою. Якщо межі модулів не контролювати, різні частини системи почнуть сильно залежати одна від одної.
Навіть невелика зміна в одному модулі може вимагати повторного розгортання всього застосунку.
Якщо велике навантаження має лише функція замовлень, зазвичай масштабують весь моноліт, а не тільки цю функцію.
Проблема в одному компоненті може вплинути на весь процес. Наприклад, необроблена помилка або надмірне використання пам’яті можуть зробити недоступними всі маршрути.
Мікросервісна архітектура розділяє систему на кілька незалежних сервісів.
Кожен сервіс зазвичай:
відповідає за окрему бізнес-область;
запускається у власному процесі;
може мати власний життєвий цикл;
розгортається незалежно;
спілкується з іншими сервісами через мережу.
Наприклад, інтернет-магазин можна поділити на:
users-service;
orders-service;
payments-service;
notifications-service.
Схематично це виглядає так:
Клієнт
│
▼
API Gateway
│
├── Users Service
├── Orders Service
├── Payments Service
└── Notifications ServiceУ NestJS для створення мікросервісів можна використовувати пакет @nestjs/microservices. Сервіси можуть взаємодіяти через різні транспортні механізми, наприклад TCP, Redis, NATS або Kafka. Конкретний вибір залежить від вимог системи.
Головна ідея не в самому транспорті, а в тому, що виклик іншого сервісу відбувається через мережеву взаємодію:
orders-service ── запит ──> users-serviceТака взаємодія складніша, ніж виклик класу в моноліті, оскільки мережа може бути повільною або недоступною.
Команді не потрібно розгортати всю систему через зміну лише одного сервісу.
Можна збільшити кількість екземплярів сервісу, який отримує найбільше навантаження.
Наприклад:
users-service працює в одному екземплярі;
orders-service — у трьох;
notifications-service — у п’яти.
Кожен сервіс має чітку бізнес-область. Це допомагає розподіляти роботу між командами.
За потреби різні сервіси можуть використовувати різні технології. Наприклад, NestJS для API та інший інструмент для обробки фонових задач.
Однак така свобода також збільшує складність системи.
Потрібно керувати:
кількома процесами;
конфігурацією сервісів;
мережевою взаємодією;
логами;
моніторингом;
розгортанням;
перевіркою доступності сервісів.
У моноліті виклик методу зазвичай або виконується, або завершується помилкою всередині процесу.
У мікросервісах додатково можливі:
тайм-аут;
втрата мережевого з’єднання;
тимчасова недоступність сервісу;
повторне виконання запиту;
часткове виконання операції.
Один користувацький запит може пройти через кілька сервісів. Для пошуку проблеми потрібні узгоджені логи та ідентифікатор запиту.
Якщо різні сервіси мають окремі бази даних, одна бізнес-операція може охоплювати кілька сховищ.
Наприклад, створення замовлення може включати:
створення замовлення;
резервування товару;
оплату;
відправлення повідомлення.
Якщо один із кроків не виконався, систему потрібно спроєктувати так, щоб вона могла коректно обробити цю ситуацію.
Для мікросервісів часто потрібні додаткові інструменти та процеси:
автоматизоване розгортання;
централізовані логи;
моніторинг;
перевірки доступності;
керування секретами;
тестове середовище для кількох сервісів.
Моноліт:
Один проєкт → один процес → одне розгортанняМікросервіси:
Кілька проєктів або застосунків → кілька процесів → незалежні розгортанняМоноліт використовує виклики класів і методів у межах одного процесу.
Мікросервіси використовують мережеві протоколи, повідомлення або віддалені виклики.
Моноліт зазвичай масштабується цілком.
Мікросервіси можна масштабувати окремо.
У моноліті простіше контролювати помилки всередині одного процесу.
У мікросервісах потрібно враховувати часткові відмови: один сервіс може бути недоступним, тоді як інші продовжують працювати.
Моноліт добре підходить для невеликої команди, яка працює з єдиною кодовою базою.
Мікросервіси можуть бути корисними для кількох команд, які незалежно розвивають різні бізнес-області.
продукт новий і вимоги ще змінюються;
команда невелика;
домен ще недостатньо зрозумілий;
потрібно швидко створити першу версію;
система має помірне навантаження;
немає потреби в незалежному масштабуванні частин;
команда не має досвіду експлуатації розподілених систем.
Для багатьох проєктів хорошим початком є модульний моноліт. Він зберігає простоту одного застосунку, але встановлює логічні межі між частинами системи.
різні частини системи мають суттєво різні вимоги до масштабування;
кілька команд повинні працювати незалежно;
окремі компоненти мають різні цикли релізів;
потрібна ізоляція окремих частин системи;
бізнес-області добре зрозумілі та мають чіткі межі;
команда готова підтримувати складнішу інфраструктуру.
Розмір проєкту сам по собі не є достатньою причиною для переходу на мікросервіси. Великий, але добре організований моноліт іноді простіший і надійніший за набір погано розділених сервісів.
Моноліт і мікросервіси не обов’язково є взаємовиключними варіантами назавжди.
Практичний шлях може виглядати так:
Створити модульний моноліт.
Визначити стабільні межі між бізнес-областями.
Виміряти навантаження та знайти реальні проблеми.
Винести лише ту частину, для якої це дає конкретну користь.
Налаштувати взаємодію між старою та новою частинами системи.
Поступово перевірити незалежне розгортання і масштабування.
Важливо не розділяти систему на сервіси лише за назвами папок. Мікросервіс повинен мати практичну причину для існування та чітку відповідальність.
Моноліт — не помилка. Це простий і часто ефективний підхід для початку проєкту.
Проблемою стає не сам моноліт, а відсутність структури, тестів і меж між модулями.
Якщо бізнес-логіка ще постійно змінюється, межі сервісів можуть бути визначені неправильно. У результаті сервіси почнуть часто звертатися один до одного та стануть залежними.
Поділ на окремі сервіси controllers-service, database-service і validation-service зазвичай не є корисним бізнес-поділом.
Краще групувати код навколо бізнес-областей:
користувачі;
замовлення;
платежі;
сповіщення.
Віддалений виклик не є таким самим, як виклик локального методу. Він може завершитися тайм-аутом або помилкою, тому це потрібно враховувати під час проєктування.
Якщо всі сервіси безпосередньо змінюють усі таблиці, незалежність сервісів стає формальною. Зміна однієї таблиці може зламати кілька сервісів.
Мікросервіси не є автоматично кращими, а моноліт не є застарілим. Важливо оцінювати конкретні вимоги, обмеження та можливості команди.
Моноліт — це застосунок, який розгортається як одна одиниця.
Мікросервісна архітектура складається з незалежних сервісів, які взаємодіють через мережу.
Моноліт простіший у розробленні, тестуванні, налагодженні та розгортанні.
Мікросервіси дають незалежне масштабування та розгортання, але збільшують інфраструктурну складність.
NestJS добре підходить для створення модульних монолітів і мікросервісів.
Для нового проєкту часто варто почати з модульного моноліту.
Переходити до мікросервісів потрібно через конкретні потреби, а не лише через популярність цього підходу.