0Pricing
PHP Academy · Урок

От монолита к микросервисам

Решите, что разделить и как провести границы сервисов

«От монолита к микросервисам» — бесплатный урок 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";

Выделение модуля

Практичный порядок выделения:

  1. Найдите модуль с небольшим числом входящих зависимостей и чётко определённым владельцем данных.
  2. Сначала скройте его вызовы внутри процесса за интерфейсом в монолите.
  3. Переместите принадлежащие ему данные в собственную схему или базу данных.
  4. Замените реализацию интерфейса сетевым клиентом.
  5. Переключите трафик через фасад и удалите старый код.

Если сначала обернуть вызовы интерфейсом внутри монолита, переход к сети станет менее рискованным.

<?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 — локальная установка не требуется.

Все уроки этого курса

  1. От монолита к микросервисам
  2. Взаимодействие сервисов: REST и gRPC
  3. Шлюзы API и обнаружение сервисов
  4. Отказоустойчивость: размыкатели и повторные попытки
← Назад к PHP Academy