От монолита к микросервисам
Решите, что разделить и как провести границы сервисов
«От монолита к микросервисам» — бесплатный урок PHP Academy на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения PHP Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс PHP Academy содержит 4 уроков всего.
От монолита к микросервисам
Микросервисы — это организационная и эксплуатационная ставка, а не вариант по умолчанию. Разбиение монолита на PHP заменяет простоту внутри процесса сетевыми вызовами, распределёнными данными и сложностью развёртывания. Если делать это по неправильным причинам, всё станет медленнее.
Этот урок посвящён самой сложной части: определению границ сервисов. Если правильно определить границы, остальное будет лишь технической работой; если ошибиться, Вы создадите распределённый монолит — все затраты без какой-либо выгоды.
Зачем разделять (и зачем не стоит)
Уважительные причины для разделения:
- Независимое развёртывание и ответственность команды.
- Независимое масштабирование нагруженных подсистем.
- Изоляция технологий и сред выполнения, а также локализация сбоев.
Плохие причины: «код запутан» (сначала проведите рефакторинг монолита) или желание следовать моде. Если два сервиса всегда должны развёртываться вместе или использовать одну таблицу базы данных, это один сервис в двух ролях.
Ограниченные контексты
Предметно-ориентированное проектирование предлагает самый точный инструмент для определения границ: ограниченный контекст. Внутри контекста термины имеют одно точное значение. «Клиент» в биллинге (получатель счёта) отличается от «клиента» в поддержке (автора обращения).
Каждый ограниченный контекст — подходящий кандидат на роль сервиса. Границы следуют языку бизнеса и тому, как организация действительно взаимодействует, — это закон Конвея в действии.
Высокая связность, низкая связанность
Хорошая граница сервиса оставляет внутри то, что изменяется вместе, а независимо изменяющиеся части выносит наружу. Оценивайте предлагаемое разделение по следующим признакам:
- Скольким функциям требуется одновременно затрагивать два сервиса? (Таких функций должно быть немного.)
- Насколько многочисленны вызовы между сервисами? (Операции должны быть крупными.)
Если реализация одной функции постоянно пересекает границу, значит, граница проведена неправильно.
<?php
// Chatty boundary smell: N network calls to render one view
foreach ($order->lineItems as $item) {
$product = $catalogApi->get($item->productId); // one call PER item!
$names[] = $product['name'];
}
// Coarse-grained: one batch call across the boundary
$ids = array_map(fn($i) => $i->productId, $order->lineItems);
$names = $catalogApi->getMany($ids); // single round-trip
echo count($names) . " products in one call\n";Отдельная база данных для каждого сервиса
Незыблемое правило: каждый сервис владеет своими данными, и ни один другой сервис не обращается к его таблицам. Общие базы данных воссоздают связанность, от которой Вы пытались избавиться разделением: изменение схемы ломает несвязанные сервисы.
Это означает, что запросы между сервисами, которые в монолите выполнялись с помощью SQL JOIN, превращаются в вызовы API или использование реплицированных моделей чтения. Такова цена — и именно в этом смысл.
<?php
// In the monolith: one JOIN across domains
$sql = 'SELECT o.id, c.email
FROM orders o JOIN customers c ON c.id = o.customer_id';
// After split: Orders service holds only the foreign id;
// it asks the Customers service for the rest (or keeps a local read model).
$order = $orderRepo->find($id); // local
$customer = $customersApi->get($order->customerId); // network call
echo $order->id . ' / ' . $customer->email . "\n";Шаблон «душителя»
Никогда не переписывайте монолит одним махом. Шаблон «душителя» позволяет выделять его части постепенно: направьте часть трафика в новый сервис через фасад или прокси, развивайте новый сервис и удаляйте старый путь выполнения только после того, как новый полностью его заменит.
Обратный прокси (или шлюз API) находится перед сервисами и для каждого маршрута решает, обращаться ли к унаследованному монолиту или к новому сервису. Миграция проходит по одной возможности за раз, и каждый этап можно выпустить.
<?php
// Facade routing: peel off one capability at a time
function route(string $path): string {
$migrated = ['/invoices', '/invoices/pdf']; // moved to billing-svc
foreach ($migrated as $prefix) {
if (str_starts_with($path, $prefix)) {
return 'http://billing-svc' . $path;
}
}
return 'http://legacy-monolith' . $path; // everything else, for now
}
echo route('/invoices/pdf'), "\n";
echo route('/users/42'), "\n";Выделение модуля
Практичный порядок выделения:
- Найдите модуль с небольшим числом входящих зависимостей и чётко определённым владельцем данных.
- Сначала скройте его вызовы внутри процесса за интерфейсом в монолите.
- Переместите принадлежащие ему данные в собственную схему или базу данных.
- Замените реализацию интерфейса сетевым клиентом.
- Переключите трафик через фасад и удалите старый код.
Если сначала обернуть вызовы интерфейсом внутри монолита, переход к сети станет менее рискованным.
<?php
// Step 2: hide the implementation behind a port the monolith calls
interface InvoiceService {
public function generate(string $orderId): string; // returns invoice id
}
// Today: local class. Tomorrow: HTTP client to billing-svc.
// The monolith's calling code never changes.
final class LocalInvoiceService implements InvoiceService {
public function generate(string $orderId): string { return 'inv-1'; }
}Согласованность распределённых данных
После разделения данных Вы теряете межсервисные транзакции ACID. Примите согласованность в конечном счёте: сервисы публикуют события о собственных данных, а другие сервисы строят локальные модели чтения на основе этих событий.
Сервис заказов не запрашивает сервис клиентов при каждом обращении — вместо этого он хранит небольшую проекцию, содержащую только нужные ему поля, и обновляет её по событиям CustomerUpdated. Это устраняет зависимость во время выполнения и дополнительный переход с задержкой.
<?php
// Orders service keeps a tiny local projection of customer data
function onCustomerUpdated(PDO $db, array $evt): void {
$db->prepare(
'INSERT INTO customer_read_model (id, email)
VALUES (:id, :email)
ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email'
)->execute(['id' => $evt['id'], 'email' => $evt['email']]);
}Эксплуатационная реальность
Микросервисы переносят сложность из кода в эксплуатацию. До разделения Вам понадобятся:
- Централизованное журналирование и распределённая трассировка (корреляционные идентификаторы между переходами).
- CI/CD для каждого сервиса и независимые версионируемые развёртывания.
- Проверки работоспособности, тайм-ауты, повторные попытки и размыкатели цепи для каждого вызова.
- Контрактные тесты, чтобы поставщик не мог незаметно сломать потребителя.
Если Ваша команда не умеет качественно эксплуатировать один сервис, с десятью будет в десять раз хуже.
Определение подходящего размера сервисов
«Микро» вводит в заблуждение — определяйте размер сервисов по бизнес-возможности и ответственности команды, а не по числу строк кода. При слишком мелком разделении (наносервисы) одна функция порождает лавину сетевых вызовов; при слишком крупном Вы возвращаетесь к монолиту.
Полезное практическое правило: за сервис должна отвечать одна команда, он должен развёртываться самостоятельно и реализовывать основные сценарии без синхронной цепочки через множество соседних сервисов.
Альтернатива: модульный монолит
Прежде чем переходить к распределённой архитектуре, рассмотрите модульный монолит: один развёртываемый компонент, внутренне разделённый на модули с чёткими границами и собственными схемами, которые взаимодействуют только через опубликованные интерфейсы, без доступа к таблицам друг друга.
Вы получаете чистые границы и простой рефакторинг без накладных расходов распределённых систем. Когда модулю действительно понадобится независимое масштабирование или отдельная ответственность, его уже будет удобно выделить с помощью шаблона «душителя». Для большинства команд это правильный первый шаг.
<?php
// Modules talk only through interfaces, never each other's tables.
namespace App\Billing; // owns billing_* tables
interface BillingFacade {
public function invoiceForOrder(string $orderId): string;
}
namespace App\Sales; // owns sales_* tables
final class Checkout {
public function __construct(private \App\Billing\BillingFacade $billing) {}
// Sales never SELECTs from billing_* directly - only via the facade.
}Быстрая проверка
Как распознать неудачное разделение.
Итоги
Как правильно определять границы сервисов:
- Разделяйте ради возможности независимого развёртывания, масштабирования и ответственности, а не потому, что код запутан.
- Согласуйте границы с ограниченными контекстами; стремитесь к высокой связности и низкой связанности.
- Отдельная база данных для каждого сервиса — никаких общих таблиц; заменяйте операции JOIN API или моделями чтения.
- Проводите миграцию постепенно с помощью шаблона «душителя».
- Принимайте согласованность в конечном счёте и вкладывайтесь в эксплуатацию до масштабирования системы.
Далее: как эти сервисы взаимодействуют на практике — REST и gRPC.
Часто задаваемые вопросы
Урок «От монолита к микросервисам» бесплатный?
Да — полный текст урока «От монолита к микросервисам» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс PHP Academy, подпишись на CoddyKit PRO. Курс PHP Academy содержит 4 уроков всего.
Чему я научусь в уроке «От монолита к микросервисам»?
Решите, что разделить и как провести границы сервисов Ты практикуешь PHP Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать PHP Academy?
Предыдущий опыт не требуется. PHP Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.
Сколько времени занимает урок «От монолита к микросервисам»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке PHP Academy?
Да. Каждый урок PHP Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- От монолита к микросервисам
- Взаимодействие сервисов: REST и gRPC
- Шлюзы API и обнаружение сервисов
- Отказоустойчивость: размыкатели и повторные попытки