Серия «Архитектура и производительность» открывается первой, но самой важной статьёй — выбором способа, которым ваш проект будет устроен внутри. Всё, что мы писали до этого в серии про веб-без фреймворков — JavaScript, CSS, Fetch API, Web Components, потоки и офлайн-режим — работало для клиента, то есть для браузера пользователя. А теперь настал черёд сервера: как организовать код, базу данных и взаимодействие между разными частями приложения.
Этот вопрос стоит перед каждым разработчиком, который вырос из простого сайта в реальную систему. С одной стороны — классический монолит: всё лежит в одном месте, подключается через include, общается с базой данных напрямую. С другой — микросервисы: десятки отдельных процессов, каждый отвечает за свой кусок, соединены сетью.
Почему одни люди выбирают первое, другие — второе? Где грань между «всё ещё удобно держать вместе» и «пора разделять»? Разберёмся на простых примерах и реальных задачах. Без жаргона, без абстрактных схем — только конкретика.
Часть I. Что такое монолит и почему он работает на старте
1. Простыми словами о структуре
Классический веб-монолит можно представить себе как многоквартирный дом: вся инфраструктура находится под одной крышей. Код лежит в одном дереве каталогов, скрипт запускается одним процессом (например, PHP-FPM), все таблицы базы данных — в одной MySQL-базе. Каждый пользовательский запрос проходит через единственную точку входа (index.php), которая распределяет работу по внутренним файлам.
Вот минимальный пример монолита на PHP:
<?php
// index.php — главный вход во всё приложение
include 'includes/config.php'; // параметры подключения к базе
include 'includes/router.php'; // маршрутизация (какой обработчик вызвать)
include 'includes/auth.php'; // функция проверки логина/пароля
include 'includes/user.php'; // операции с пользователями
$router = new Router(); // создаём объект маршрутизатора
$router->dispatch($_SERVER['REQUEST_URI']); // разбираем URL и отдаём ответЗдесь нет ничего сложного. Всё в одном файле, зависимости очевидны. Чтобы изменить форму профиля пользователя, вы правите файл user.php и сразу видите результат. Чтобы добавить новый раздел, вы пишете обработку в includes/ и ссылку в шаблон.
Когда вы запускаете этот скрипт на локальном сервере (php -S localhost:8080) или ставите на продакшн Nginx+PHP-FPM, сервер просто читает файлы с диска и выполняет их. Всё работает, пока приложение небольшое.
Это тот самый подход, на котором построено большинство ранних версий WordPress, Joomla, Drupal. Весь движок — один большой zip-архив, который вы закачиваете на хостинг. Нет ни микросервисов, ни очередей — просто набор PHP-файлов, которые вызывают друг друга.
Часть II. Точки роста — где начинается боль монолита
2. Когда монолит перестает быть удобным
Монолит отлично работает, пока вы один. Пока вы можете проверить весь сайт за пять минут, не задумываясь. Но когда система растёт, появляются проблемы, которые нельзя игнорировать:
А) Команда больше пяти человек. Вы наняли троих программистов: один занимается карточками товаров, второй — платёжной системой, третий — чатом поддержки. Все пишут в разные файлы одного монолита. Что происходит? Они начинают ломать друг друга. Утро понедельника: обновляется база данных, слетают стили, никто не знает, кто за это отвечал. В монолите любой файл может коснуться любого другого. Ошибку найти трудно, протестировать — почти невозможно.
Б) Одну часть хочется масштабировать больше других. Представьте интернет-магазин. Картинки товаров грузят гигабайты трафика, пользователи сидят в профиле полдня, а отправка платежей занимает доли секунды. В монолите вы не можете «перегрузить» только картинки: если вы запустите два процесса вместо одного, они оба будут жрать память под всю базу, включая профиль пользователей. Чтобы разгрузить картинки, придётся масштабировать весь сайт — а значит, платить вдвое больше ресурсов впустую.
В) Языки и технологии разные. Вам нужен быстрый расчёт скидок на Python, красивый фронтенд на JavaScript, а база — на PostgreSQL. Монолит требует общего языка программирования. Или вы берёте Python везде (и пишите PHP-подобным синтаксисом), или делаете костыли, где Python говорит с PHP через текстовые файлы. И то, и другое — боль.
Пример такой ситуации — популярный стартап, который начал с монолита на Rails: сначала всё шло гладко, потом команда выросла до двадцати человек, и каждый релиз превращался в испытание. Вот типичный сценарий: вы изменили таблицу в БД для новой функции, забыли добавить колонку в старый модуль, и весь сайт упал с ошибкой SQL, хотя проблема была не там. В монолите изменения касаются всего — даже если вы тронули только один файл.
Чтобы понять, почему так происходит и как правильно строить границу между компонентами, давайте посмотрим, какие преимущества даёт разделение системы на отдельные части.
Часть III. Преимущества разделения: что даёт микросервисный подход
3. Независимый деплой: одно обновление — без ожидания всего
Главное преимущество сервиса, отделённого от остального приложения, заключается в том, что его можно обновлять отдельно. Если вы сделали новую функцию оплаты и проверили её локально, вы можете перезапустить только платёжный модуль, не останавливая весь сайт. Пользователи этого не заметят, и если что-то пойдёт не так, упадёт только функция оплаты — остальные страницы продолжат работать.
Для сравнения, вот как выглядит архитектура небольших сервисов в проекте:
{
"services": {
"frontend": {
"port": 3000,
"framework": "Vanilla JS (ES Modules)",
"description": "Обработка запросов пользователя"
},
"auth-service": {
"port": 3001,
"language": "PHP 8.2+",
"description": "Логин, регистрация, сессии"
},
"catalog-service": {
"port": 3002,
"language": "PHP 8.2+",
"description": "Карточки товаров, категории"
},
"notification-service": {
"port": 9090,
"language": "Node.js / PHP",
"description": "Уведомления, рассылки"
}
}
}
Каждый блок здесь живёт своей жизнью. Вы можете переписать auth-service на Go, не касаясь фронтенда, или полностью заменить логику уведомлений. Деплой каждого блока осуществляется независимо, поэтому шанс сломать соседний компонент практически исключён.
4. Масштабирование под конкретную нагрузку
Если ваша главная проблема — нагрузка на картинки или большие файлы, вы можете запустить десять копий каталога (catalog-service), чтобы они держали эту тяжесть, а сервис авторизации оставить в единственном экземпляре. Система будет эффективнее расходовать ресурсы CPU, RAM и сети. При этом каждая служба может говорить со своим языком, используя собственный стек технологий, и объединяться в одну большую систему только в момент обработки пользовательского запроса.
Это особенно полезно для проектов типа WordPress-платформы: один VPS тянет сайт и админку, но при резком скачке трафика (например, статья попала в рекомендацию соцсети) вы не хотите замедляться. С микросервисами вы легко увеличите количество обработчиков контента, не затрагивая платёжный модуль.
С другой стороны, микросервисы привносят свои ограничения и сложности. О том, за что приходится платить и какие ловушки ждут при распределении архитектуры, расскажем дальше.
Часть IV. Минусы микросервисов: за что приходится платить
5. Новая головная боль: всё стало сетевым вызовом
В монолите вы вызываете функцию прямо в памяти: $user = getUserById(123). Результат приходит мгновенно. В микросервисной архитектуре этого же пользователя нужно запросить по сети: отправить HTTP-запрос к http://auth-service:3001/users/123, дождаться ответа, разобрать JSON. Что может пойти не так?
- Сеть может быть медленной. Даже на внутренней машине задержка составляет 2–10 миллисекунд. На разных серверах — до сотни. Если один запрос собирает данные из пяти сервисов вместо одного, общее время увеличивается многократно.
- Сервис может не ответить. Если
auth-serviceзавис или перезапускается, все запросы пользователей к нему падают с таймаутами. В монолите такой ошибки просто нет: память своя, процессы свои. В распределённой системе вы обязаны обработать отказ безопасно — вернуть пользователю понятную ошибку или попробовать снова. - Нужно дублировать логику безопасности. В монолите все данные доступны из единой точки входа, где вы ставите один nonce и проверяете сессию. В микросервисах каждая конечная точка должна сама проверять токен, иначе злоумышленник может обратиться напрямую к базе или каталогу, минуя авторизацию.
Вот простой пример того, как выглядит обращение к другому сервису из фронтенда (в стиле нашего проекта с Vanilla JS):
// frontend/index.js — запрос к auth-service
async function getUserProfile(userId) {
try {
const response = await fetch(`https://api.example.test/auth-service/users/${userId}`);
if (!response.ok) {
throw new Error(`Ошибка получения профиля: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Не удалось связаться с сервером:', error.message);
// Можно предложить пользователю повторить попытку или использовать кэш (см. статью про сервис-воркеры)
}
}Ещё один нюанс: мониторинг. В монолите достаточно посмотреть логи одного файла. Когда сервисов десять, а каждый пишет в свою базу или свой файл логов — искать причину сбоя становится настоящим квестом. Об этом подробнее в статье про логирование и метрики.
Давайте теперь разберёмся, как правильно определить границы между отдельными службами, чтобы архитектура была удобной и поддерживаемой.
Часть V. Границы сервисов: как определить, где заканчивается один и начинается другой
6. Как выбрать правильное разделение
Главная ошибка новичков — деление системы по техническим признакам вместо функциональных. Например, создают один сервис только для работы с базой данных, другой — для генерации PDF, третий — для обработки видео. Получается набор мелких инструментов, которым тяжело договориться между собой. В результате каждый запрос идёт через пять-шесть промежуточных сервисов, прежде чем вернуться к клиенту.
Правильный способ — деление по предметной области (domain). Каждый сервис должен закрывать одну цель бизнеса, а не техническую задачу.
На примере интернет-магазина:
- Catalog Service управляет товарами, категориями и поиском. Никаких данных о пользователях.
- User Service хранит профили, пароли, историю заказов. Никаких товаров.
- Payment Service принимает оплату и формирует чеки. Никаких пользователей.
- Notification Service отправляет письма и push-уведомления. Он реагирует только на события, ничего лишнего.
Такой подход называется Domain-Driven Design (DDD) и помогает избежать путаницы: каждый разработчик знает, что принадлежит именно его службе. Изменения внутри границ безопасны, а взаимодействие между ними — чётко оформлено.
Конкретный пример структуры каталогов сервисов:
/services
/catalog # товары, фильтры
/users # авторизация, профили
/payments # транзакции
/notifications # уведомления, email
Фронтенд обращается к ним последовательно: сначала загружает каталог, затем профиль пользователя (если залогинен), затем предлагает оплатить. Каждая ветка независима.
Осталось разобраться с главным вопросом: когда вообще стоит переходить к такому разделению. Ниже разберём случаи, когда монолит лучше сохранить.
Часть VI. Когда НЕ надо делить — честный взгляд на маленькие проекты
7. Почему монолит иногда лучше не трогать
Если у вас один разработчик, небольшая команда или простая задача — не усложняйте. Распределённая архитектура добавляет большие издержки: настраивать сеть, следить за доступностью каждого сервиса, делать отладку межпроцессного взаимодействия — это часы работы в неделю, которые можно было бы потратить на фичи. Для небольших продуктов (лендинги, блоги, простые CMS вроде WordPress на одном VPS) монолит — идеальный выбор.
Вот несколько случаев, когда оставаться на монолите — мудрое решение:
| Ситуация | Что делать |
|---|---|
| Вы работаете в соло | Оставьте всё в includes/. Одна база данных, один index.php. |
| Задача — протестировать идею (MVP) | Не тратьте время на настройку микросервисов до появления первых клиентов. |
| Приложение простое (сайт-визитка, портфолио, блог) | Используйте готовую CMS. Она уже монолит, и прекрасно справляется. |
| Нет DevOps-специалиста или времени на инфраструктуру | Микросервисы требуют оркестрации, балансировки, очереди сообщений. Без команды это сложно. |
Если же вы решите расширить монолит, начать с Docker-контейнеров — простой вариант изоляции, где вы запускаете всё на одном VPS, но каждый процесс (база, веб, очереди) живёт отдельно. Это уже следующий уровень после чистого монолита (мы обсудим контейнеры в статье №11 этой серии).
Как службы узнают друг о друге? Есть два основных пути: прямые вызовы через HTTP/RPC и обмен через очереди сообщений.
HTTP-RPC (вызов напрямую): Самый простой способ. Сервис A посылает REST-запрос сервису B и ждёт ответа. Используется повсеместно, потому что это понятно каждому разработчику. Недостаток — связь жёсткая: если B не отвечает, A тоже падает. Подходит для большинства задач, где нужна немедленная реакция. Пример: получить информацию о продукте перед оплатой.
Очереди сообщений (асинхронный обмен): Более надёжный, но сложный путь. Вместо прямого вызова A кладёт сообщение в брокер (RabbitMQ, Kafka, SQS), а B забирает его, когда готов. Гарантируется, что ни одно событие не потеряется: даже если сервис B временно недоступен, сообщения накопятся в очереди и будут обработаны позже. Недостаток — сложнее писать и поддерживать: нужно знать протоколы брокеров, настраивать retry policies и dead-letter queues. Идеально для тяжёлых фоновых задач: отправка email, обработка видео, генерация PDF.
Мы подробно обсудим и RPC, и очереди в следующих статьях. Сейчас запомните главное правило: начинайте с простого (REST-вызовы), добавляйте очереди только тогда, когда прямое взаимодействие действительно начинает тормозить систему.
Чек-лист: готовность архитектуры
- Попробуйте считать строчки кода (LOC). Если ваш проект — до нескольких тысяч строк монолитного PHP/JS/CSS-файлового дерева — оставайтесь в нём. Микросервисы начнут перегружать систему.
- Проверьте команду. Если над проектом работает один-два человека — монолит идеален. Если более пяти и каждый занят своей функцией — пора выделять сервисы.
- Разделите функциональные блоки по бизнес-логике. Каталог, оплата, пользовательские аккаунты — это естественные границы. Техническое деление (база, логгер, API) — плохая идея.
- Решите стиль коммуникации заранее. Прямой вызов — для простых задач, очередь — для сложных фоновых процессов (см. статью про очереди сообщений).
- Подумайте о масштабируемости. Если вам нужно масштабировать только один участок, попробуйте сначала улучшить код внутри монолита: вынесите тяжелый модуль в отдельную библиотеку, используйте кэширование Redis (разберём в следующей статье).
- Не бойтесь начинать с монолита. Многие огромные платформы начинались именно так. Переход к микросервисам — это естественный этап взросления системы, а не обязательное условие с самого первого дня.
- Используйте Docker-контейнеры даже для монолита. Это поможет вам подготовиться к будущему разделению без потери гибкости (подробнее в финальной статье про контейнеризацию и оркестрацию).
- Сохраняйте простоту там, где она возможна. Одностраничное приложение на Vanilla JS, которое подключается к одному серверному скрипту, — это не откат в прошлое, а осознанная оптимизация.
👤 Зарегистрируйтесь, чтобы добавить материал в закладки.
