Пошук уроків, статей та іншого контенту
Міграємо моноліт поступово через фасад і нові компоненти, визначаючи умови безпечного або недоцільного застосування.
Strangler Fig — це патерн поступової заміни моноліту, за якого нова система поетапно перехоплює функціональність старої.
Назва походить від рослини-душителя: вона обвиває дерево, поступово займає його простір, а з часом старе дерево можна видалити. У програмній системі роль такого «обвиття» виконує фасад або маршрутизатор перед монолітом.
Замість схеми:
Клієнт → Новий застосунокна початку з’являється:
Клієнт → Фасад → МонолітПісля міграції окремої функції:
Клієнт → Фасад ─┬→ Новий компонент
└→ МонолітПісля завершення міграції конкретної області:
Клієнт → Фасад → Новий компонентІ лише коли всі області перенесено, фасад та старий моноліт можна спростити або видалити.
Типовий процес складається з таких кроків:
Додати фасад перед монолітом.
Не змінюючи поведінку системи, перенаправити весь трафік через фасад.
Виділити одну функціональну область із чіткою межею.
Реалізувати цю область у новому компоненті.
Налаштувати маршрутизацію потрібних запитів до нового компонента.
Перевірити функціональність, продуктивність і дані.
Поступово збільшувати частку трафіку нового компонента.
Вимкнути стару реалізацію після стабілізації.
Видалити непотрібний код моноліту та тимчасову маршрутизацію.
Фасад може бути:
API Gateway;
reverse proxy;
окремим HTTP-сервісом;
модулем маршрутизації в наявному edge-сервісі;
шаром контролерів перед старими та новими компонентами.
Важливо, щоб фасад не перетворився на новий моноліт. Його відповідальність має бути обмежена маршрутизацією, автентифікацією, спостережуваністю та, за потреби, сумісністю API.
Для першої міграції слід обирати компонент із чіткою межею, а не випадковий набір класів або таблиць.
Добрий кандидат:
має зрозумілий API;
має обмежену кількість залежностей;
може бути перевірений окремими тестами;
не вимагає одночасної зміни всіх даних у системі;
має вимірюваний результат;
не є найкритичнішою функцією бізнесу.
Наприклад, у системі електронної комерції окремими кандидатами можуть бути:
каталог товарів;
пошук;
профіль користувача;
історія замовлень;
розрахунок доставки.
Поганим першим кандидатом зазвичай є функція, яка одночасно:
змінює багато пов’язаних таблиць;
бере участь у складній транзакції;
викликається десятками внутрішніх модулів;
має неявні залежності від глобального стану моноліту;
не має надійного способу перевірки правильності результату.
Фасад може маршрутизувати запити за різними ознаками:
шляхом URL;
HTTP-методом;
версією API;
ідентифікатором клієнта;
часткою трафіку;
feature flag;
регіоном або середовищем.
Наприклад:
GET /api/products/* → новий каталог
POST /api/orders → моноліт
GET /api/users/* → монолітМаршрутизація повинна бути явною. Не варто визначати компонент призначення за непрозорими правилами, які важко перевірити або відкотити.
Нижче наведено спрощений, але runnable-приклад фасаду. Він перенаправляє запити каталогу до нового сервісу, а всі інші запити — до моноліту.
const http = require("node:http");
const PORT = 3000;
const LEGACY_URL = "http://localhost:4000";
const CATALOG_URL = "http://localhost:4001";
function isCatalogRequest(request) {
return request.method === "GET" &&
request.url.startsWith("/api/products");
}
async function proxy(request, response, targetBaseUrl) {
const targetUrl = new URL(request.url, targetBaseUrl);
const headers = new Headers(request.headers);
headers.set("x-forwarded-by", "strangler-facade");
try {
const upstreamResponse = await fetch(targetUrl, {
method: request.method,
headers,
redirect: "manual"
});
response.writeHead(
upstreamResponse.status,
Object.fromEntries(upstreamResponse.headers)
);
const body = Buffer.from(await upstreamResponse.arrayBuffer());
response.end(body);
} catch (error) {
console.error("Помилка проксіювання:", error.message);
response.writeHead(502, {
"content-type": "application/json; charset=utf-8"
});
response.end(JSON.stringify({
error: "upstream_unavailable"
}));
}
}
const server = http.createServer(async (request, response) => {
if (isCatalogRequest(request)) {
// Новий компонент поступово приймає функціональність каталогу.
await proxy(request, response, CATALOG_URL);
return;
}
// Уся ще не мігрована функціональність залишається в моноліті.
await proxy(request, response, LEGACY_URL);
});
server.listen(PORT, () => {
console.log(`Фасад запущено на http://localhost:${PORT}`);
});Для запуску потрібен Node.js із вбудованим fetch:
node facade.jsУ production такий фасад зазвичай реалізують на рівні інфраструктури або використовують готовий reverse proxy. Головна ідея прикладу — не конкретний сервер, а контрольована точка маршрутизації між старою та новою реалізаціями.
Перенаправлення всіх користувачів одразу створює ризик. Краще використовувати поетапне ввімкнення:
0% трафіку — новий компонент розгорнутий, але не використовується.
Внутрішні користувачі або тестове середовище.
Невелика частка production-трафіку.
Окремий регіон, клієнт або tenant.
100% трафіку після перевірки метрик.
Маршрутизацію можна контролювати feature flag. При цьому flag має бути:
централізованим;
швидко змінюваним;
журналювати зміни;
мати безпечне значення за замовчуванням;
дозволяти миттєво повернути трафік до моноліту.
Приклад логіки:
function shouldUseNewCatalog(request) {
const rolloutPercentage = 10;
const clientId = request.headers["x-client-id"] || "anonymous";
// Стабільний розподіл утримує одного клієнта на одному маршруті.
let hash = 0;
for (const character of clientId) {
hash = (hash * 31 + character.charCodeAt(0)) % 100;
}
return hash < rolloutPercentage;
}Важливо використовувати стабільний розподіл. Якщо випадково обирати маршрут для кожного запиту, один користувач може отримати різну поведінку на послідовних запитах.
Поки стара й нова частини працюють одночасно, вони повинні мати сумісні контракти.
Фасад може тимчасово виконувати такі перетворення:
перейменування полів відповіді;
перетворення форматів дат;
адаптація HTTP-кодів;
підтримка старих заголовків;
перетворення старої моделі помилок у нову.
Однак адаптер не повинен приховувати фундаментальну несумісність. Якщо новий компонент має іншу бізнес-семантику, простого перейменування полів недостатньо.
Контракт слід перевіряти за допомогою:
контрактних тестів;
інтеграційних тестів;
перевірки реальних прикладів запитів;
порівняння відповідей старої та нової реалізацій.
Міграція коду зазвичай простіша за міграцію даних. До початку роботи потрібно визначити:
хто є власником кожного набору даних;
де відбувається запис;
де відбувається читання;
як синхронізуються зміни;
як обробляються старі записи;
як виконується зворотний перехід.
Нова реалізація може тимчасово використовувати базу моноліту. Це прискорює початок міграції, але створює сильне зв’язування:
новий компонент залежить від схеми моноліту;
зміна таблиць може зламати обидві системи;
межа між компонентами залишається нечіткою;
міграцію бази даних доведеться виконувати окремо пізніше.
Такий підхід допустимий як проміжний етап, якщо доступ до даних обмежений і заплановано відокремлення.
Нова функціональна область може отримати власне сховище. Тоді потрібно вирішити:
як перенести початкові дані;
як підтримувати актуальність під час переходу;
як уникнути подвійного запису;
який компонент є джерелом істини;
як обробляти помилки синхронізації.
Не слід безконтрольно дозволяти двом компонентам записувати одну й ту саму сутність. Подвійний запис часто створює розбіжності, які важко виправити.
Один із безпечних шляхів — спочатку перенести читання:
Нова реалізація читає дані зі старого сховища.
Перевіряється коректність результатів.
Нова реалізація отримує власне сховище.
Записи поступово переводяться до нового компонента.
Моноліт перестає бути джерелом цієї функціональності.
Це не універсальне правило, але розділення читання та запису часто зменшує ризик першого етапу.
Fallback до моноліту може бути корисним на етапі міграції, але його не можна вважати універсальним рішенням.
Для операцій читання fallback зазвичай безпечніший:
новий компонент не відповів → прочитати з монолітуДля операцій запису це небезпечно:
новий компонент прийняв запит,
фасад не отримав відповідь,
фасад повторив запис у монолітіУ результаті операція може виконатися двічі.
Для записів необхідні:
ідемпотентні ключі;
дедуплікація;
чітка інформація про результат операції;
контроль повторних запитів;
журналювання переходів;
узгоджена стратегія відновлення.
Не можна автоматично повторювати платіж, створення замовлення або іншу невідворотну дію в іншій системі без доведеної ідемпотентності.
Після перемикання трафіку потрібно контролювати не лише доступність сервісу.
Корисні метрики:
частка помилок;
латентність p95 і p99;
кількість тайм-аутів;
частота fallback;
бізнесові показники;
кількість дубльованих або втрачених операцій;
розбіжності між старою та новою відповіддю;
навантаження на базу даних.
Для операцій читання можна тимчасово виконувати тіньові запити до іншої реалізації та порівнювати результати. Тіньовий запит не повинен змінювати стан системи.
Порівнювати потрібно нормалізовані результати. Наприклад, випадковий ідентифікатор, порядок полів або службові часові мітки не повинні створювати хибні розбіжності.
Будь-яке порівняння має враховувати приватність і вартість. Не слід безконтрольно дублювати запити, які дорого виконуються або містять чутливі дані.
Відкат повинен бути продуманий до початку ввімкнення нового компонента.
Мінімальний план містить:
Умови автоматичного або ручного відкату.
Спосіб повернення маршруту до моноліту.
Час, необхідний для перемикання.
Перевірку того, що моноліт ще здатен обробити трафік.
Стратегію для даних, записаних новим компонентом.
Відповідальних за прийняття рішення.
Найпростіший відкат — змінити маршрут на фасаді. Але він працює лише тоді, коли новий компонент не встиг змінити дані у форматі, який моноліт не розуміє.
Тому важливо не видаляти стару реалізацію одразу після перемикання. Її можна залишати вимкненою, але готовою до контрольованого використання протягом періоду спостереження.
Strangler Fig добре підходить, якщо:
система має тривалий життєвий цикл;
повна зупинка недопустима;
моноліт можна поставити за стабільний фасад;
функції мають достатньо чіткі межі;
бізнес готовий до кількох етапів міграції;
є можливість підтримувати стару й нову реалізації паралельно;
команда має спостережуваність і контроль розгортання.
Патерн особливо корисний для систем із постійним production-трафіком, де міграція має виконуватися без великого разового релізу.
Патерн може бути невдалим вибором, якщо:
функціональність настільки зв’язана, що її неможливо розділити;
усі операції залежать від однієї глобальної транзакції;
дані не можна безпечно синхронізувати;
фасад не можна встановити перед клієнтами;
нова і стара системи мають несумісні бізнес-контракти;
підтримка двох реалізацій дорожча за контрольовану одноразову заміну;
моноліт настільки нестабільний, що не може надійно працювати під час міграції;
міграція обмежена коротким часовим вікном, у якому простіше виконати заплановане переключення.
У таких випадках варто розглянути іншу стратегію: локальне переписування модуля, окрему міграцію даних або контрольовану заміну всієї системи. Вибір залежить від ризиків, а не від популярності патерну.
Переміщення класів у новий сервіс не означає виділення функціональної області. Якщо новий компонент постійно викликає внутрішні деталі моноліту, залежність лише змінила форму.
Якщо фасад починає містити правила замовлень, розрахунку цін або авторизації, він перетворюється на ще один моноліт. Фасад має маршрутизувати та адаптувати, а не накопичувати доменну логіку.
Це скасовує головну перевагу патерну — малий контрольований розмір змін. Кожен етап має мати окремий критерій успішності та можливість відкату.
Без метрик неможливо зрозуміти, чи новий компонент справді працює краще. Логи, трасування та метрики потрібно додати до масового перемикання, а не після першого інциденту.
Повторне виконання невідомої операції в іншій системі може створити дублікати або пошкодити дані. Для записів необхідно явно визначити ідемпотентність та стан операції.
Проміжний маршрут і стара реалізація мають бути тимчасовими. Після стабілізації потрібно виконати cleanup:
видалити старі маршрути;
видалити невикористаний код;
прибрати feature flags;
прибрати непотрібні адаптери;
зафіксувати нового власника даних.
Перед перенаправленням production-трафіку до нового компонента варто переконатися, що:
його контракт зафіксований і протестований;
визначений власник даних;
відомо, як обробляються тайм-аути;
є health checks і метрики;
налаштований контрольований rollout;
працює швидкий відкат;
продумані повторні запити;
виконані тести сумісності;
відомі допустимі відмінності між старою та новою реалізаціями;
команда знає, коли і як видаляти старий маршрут.
Strangler Fig — це не просто проксі перед монолітом, а стратегія керованої заміни:
фасад утримує стабільний зовнішній контракт;
нові компоненти перехоплюють функціональність поступово;
маршрутизація дозволяє обмежити область ризику;
feature flags і поетапний rollout дають контроль над трафіком;
міграція даних потребує окремого плану;
fallback безпечний не для всіх операцій;
спостережуваність і відкат мають бути готові заздалегідь;
патерн недоцільний, якщо систему неможливо розділити або підтримка двох реалізацій створює більший ризик, ніж одноразова заміна.