Не без фейлів: як забезпечити стабільність продукту та швидкі релізи
Привіт! Мене звати Вадим Ільченко, я CTO Mate academy. Наш основний продукт — гейміфікована та AI-friendly LMS-платформа, розроблена з нуля. Вона охоплює редактор коду, чати, дашборди, досягнення, стріки, лідерборд, інтеграцію з GitHub та іншими сервісами. Понад 5000 випускників почали роботу в ІТ після навчання на нашій платформі.
На одному з минулих Dev Challenge під час «Ранкової кави» зайшло обговорення теми реліз-менеджменту. Розмова про те, як у кого деплоїться код і з якою частотою, надихнула мене поділитися нашим досвідом спочатку на DOU Architecture Day й тепер у цій колонці.
У Mate academy ми деплоїмось щодня. Далі розповім, як ми побудували цей процес і як забезпечуємо його стабільність.
Проблема
Ключове завдання — швидко доставляти цінність користувачам й не ламати те, що вже працює. Книга Wartime Software чудово описує цю дилему: важливо не лише правильно будувати архітектуру, а й швидко й постійно релізити код.
Мої перші кроки виглядали так: чекаут гілки з master, написання коду, PR в master, мердж або пуш прямо в master без створення гілки, локальний білд, відправка артефакту на сервер і рестарт. Це працює, коли розробників один-два. Але що робити на скейлі? Наприклад, у мене в команді 15 інженерів.
Gitflow чи швидкі релізи
Ми розглядали Gitflow (development/release/feature-гілки, деплой на staging/production). Мінуси: складно пушити хотфікси в прод, мердж-конфлікти, складні міграції та ролбеки. У мого колеги в попередній компанії кодфріз тривав два тижні перед релізом — будь-які зміни блокувались.
Ми обрали інший шлях: чекаут гілки з master, пишемо код, невеликі PR в master, деплой на staging/production прямо з master. Це дозволяє мерджити зміни швидко, конфлікти мінімальні, а код можна доставити в прод за годину. Щоденні релізи дають змогу вже завтра побачити в продакшені код, який сьогодні девелопили.
Надійний CI/CD: білди і тести
При швидких ітераціях дуже важливо тестувати код — ручне тестування тут не підходить. QA не зможе щодня робити регресію, та й фізично це неможливо. Тому використовуємо:
- Unit-тести — для простих функцій та утиліт. Дуже дешеві, покривають основну логіку.
- Інтеграційні тести — перевіряють АРІ-ендпоінти. Тут треба підіймати backend, базу даних та супутні сервіси (Redis, AWS DynamoDB). Дорожчі за unit, але закривають багато випадків: перевірка даних, пермішенів, edge cases, інтеграції з іншими сервісами (емейли, маркетингові івенти тощо). Всі API-ендпоінти повинні покриватися інтеграційними тестами.
- Компонентні тести на UI — мокаємо API-відповідь і перевіряємо рендер та реакцію компонентів на зміни стейту. Відносно дешеві. Пишемо як для складних компонентів (дешборди з даними та візуалізаціями), так і для простих (наприклад, відкриття модалки при кліку на кнопку).
- E2E-тести — піднімається вся інфраструктура «як у продакшені» (FE + backend + база + супутні сервіси). Це важкі та дорогі тести, які робимо для критичного функціоналу та happy path користувача. Бот імітує поведінку юзера, клікає кнопки, перевіряє основний функціонал.
Типізація через TypeScript дозволяє легко рефакторити й розширювати код завдяки статичному аналізу, а ESLint та стайлгайди забезпечують дотримання best practices. Стайлгайди особливо корисні там, де складно автоматизувати перевірки, і служать орієнтиром під час код-рев’ю.
CODEOWNERS вказує команду, відповідальну за конкретний модуль коду. GitHub автоматично тегає команду у код-рев’ю. Це дозволяє не пропустити, коли в «вашу зону відповідальності» накомітять щось.
Під час створення PR запускаються білди на змінених сервісах (TypeScript ловить статичні помилки), лінтери для перевірки стилю, генерація додаткових файлів (наприклад, GraphQL schema) та відповідні тести. Якщо всі тести пройшли і PR апрувнули, його можна мерджити в master.
У master відбувається те ж саме, плюс пакування у реліз-артефакт і автоматичний реліз на development та staging. Раз на день останній «зелений» коміт деплоїться в production автоматично, а розробники отримують лише сповіщення про успіх або помилки.
Сервери налаштовані так: development використовує базу, як у локальної розробки, але з production build коду, що дозволяє швидко перевіряти нові фічі. Staging — копія production без персональних даних, з базою, яка періодично синхронізується з продакшн, для тестування перед релізом. Production — фактичний продакшн сервер.
Стабільний production
Blue/Green deployment
При релізах потрібно оновлювати сервери, щоб нова версія коду запрацювала. Найпростіший спосіб — вимкнути старий сервер і увімкнути новий. Але це створює даунтайм — період, коли сайт недоступний для користувачів. Можна вивісити сторінку «технічні роботи», але кращий варіант — деплой без даунтайму, який називається blue/green deployment.
Як працює підхід:
- Є стара версія коду (Blue) і нова (Green). Поверх них працює load balancer.
- Весь трафік поки що йде на версію Blue.
- Піднімається версія Green, система перевіряє її через health-check (наприклад, ендпоінт, який повертає статус 200, коли сервіс готовий приймати трафік).
- Як тільки health-check успішно пройшов, трафік переключається на версію Green.
- Blue йде у graceful shutdown — сервер перестає приймати нові запити, чекає завершення всіх відкритих і тільки після цього вимикається.
Можна використовувати Canary-релізи: на новий код (Green) йде невелика частина користувачів. Якщо все стабільно і багів немає — деплой продовжується. Якщо метрики погіршуються — автоматичний ролбек.
Feature flags
При маленьких ПРах та щоденних релізах часто потрібно приховати незавершений код у продакшені. Наприклад, фіча вимагає розробки складного UI і його інтеграції в поточну сторінку, або треба зробити редизайн блоку, або необхідна інтеграція нового платіжного провайдера без порушення поточного процесу оплати.
Feature flags допомагають вирішити цю задачу. Як це працює технічно — у системі є список активних та неактивних флагів (boolean true/false), які можна вмикати/вимикати через адмінку без додаткового деплоя. Далі в коді пишемо, у прикладі з «новою» секцією на UI.
if (features.{name}.status === 'ENABLED') {
return <NewAwesomeSection />
}
return <OldAwesomeSection />
Це ж працює і на бекенді:
if (features.{name}.status === 'ENABLED') {
processWithNewLogic();
} else {
processWithOldLogic();
}
У цьому випадку features — це список фіч, який можна зберігати навіть у власній базі даних (найпростіший варіант). Існують також сторонні 3rd-party інструменти, які беруть на себе управління флагами та адмінку.
Можливості, які можна реалізувати:
- Вмикати фічу для певного відсотка користувачів (наприклад, 10% або 50%).
- Автоматично вимикати фічу, якщо зростає кількість помилок після її активації.
Продуктовий аспект
Важливо розуміти успішність релізу. Код сам по собі не має цінності, якщо він не робить продукт кращим і не вирішує проблему користувача.
Для перевірки цього можна запускати A/B тести:
- Половина користувачів бачить «стару» версію, інша половина — «нову».
- Аналізуються як бізнес-метрики, так і технічна ефективність.
- Якщо новий варіант працює гірше чи має баги, лише половина користувачів бачить це, а вплив на продукт мінімальний.
Технічно A/B тести — це ті ж feature flags для користувача (увімкнено чи ні), але з додатковою математикою під капотом для аналізу впливу на бізнес-метрики та технічний перформанс.
Можливість вимкнути флаг без додаткового релізу — дуже дієвий спосіб зменшити ризик покласти продакшн, якщо нова фіча працює не так, як очікувалось.
Ітеративні релізи
Переваги ітеративного підходу можна виділити у два основні пункти:
- Не розробляти всю фічу одразу. Спочатку визначаємо болі користувача, намагаємось їх вирішити і перевіряємо, чи це працює на практиці.
- Поступове впровадження змін. Збираємо фідбек і швидко реагуємо на нього, покращуючи продукт.
В Mate academy ми часто релізимо нові «студентські» фічі поетапно: спершу на одну групу, потім на весь напрям (наприклад, фронтенд), і лише після цього — на всіх користувачів.
Ключовий принцип — Launch and iterate: якщо фіча корисна, це видно одразу і її можна масштабувати, якщо є проблеми, користувачі їх швидко виявлять, і ми можемо швидко пофіксити та ітеруватись далі.
Моніторинг і інциденти
Всі ці перевірки мінімізують ризик, що щось піде не так, але не виключають його повністю. Тому важливо вміти не тільки швидко фіксити проблеми в продакшені, а і вчитись попереджати їх.
У нас була величезна кількість різних інцидентів: прод міг лежати годинами або працювати повільно. Зараз критичних багів значно менше, ніж два роки тому, завдяки вкладеним зусиллям. Хоча проблеми з’являються, і це нормальний процес, важливо як ми на них реагуємо, як працює сервер і що бачать користувачі в цей час.
Швидкий rollback
Гарний підхід з уже згаданої книги — fail fast. Важливо мати можливість якомога швидше відкотитись до робочої версії сайту, а потім вже займатись long-term фіксами. Це може бути вимкнення feature flag’у, ролбек всього продакшена до останнього робочого коміту, відключення сервісів, які перевантажують сайт. Важливо мати можливість робити це швидко.
Наприклад, ми додали можливість тегати всіх учасників через @all в чатах. На етапі тестів і на продакшені спочатку все працювало нормально. Проблеми виникли, коли ми використали анонс фічі з тегом @all в чаті на 20 тис. учасників і всім надійшло сповіщення. Швидкий фікс: ми вимкнули цю фічу для великих груп через feature flag, а вже потім ідентифікували та виправили проблеми з перформансом.
Моніторинг
Моніторинг включає метрики й сповіщення, які допомагають швидко ідентифікувати проблеми:
- Метрики: API latency, кількість помилок, навантаження на базу, трейсинг запитів. Приклад: якщо сайт «тормозить», метрики допомагають зрозуміти, чи проблема у важких обчисленнях на API, чи у збільшеному навантаженні на базу даних.
- Сповіщення: Slack, емейл, СМС або дзвінок (наприклад, через PagerDuty або AWS Chatbot).
Ми також реалізували процес моніторингу даних у базі. Раз на день запускаються запити, щоб перевірити, що аномалії відсутні (наприклад, що всі підписки обробились). Якщо виникає аномалія (наприклад, підписка мала обробитися вчора, але цього не сталося), автоматично приходить сповіщення в Slack.

У нас налаштований алерт на «free storage» у базі даних. Одного ранку спрацював PagerDuty: ми побачили, що вільне місце стрімко зменшується (~20 ГБ за день). При цьому база добре працювала, просто одна міграційна задача зафейлилась і почала накопичувати буферні дані. Якби не моніторинг, сторейдж би закінчився за декілька днів і весь продукт би просто впав через неробочу базу даних.
On-call rotation
Ми впровадили систему on-call, де інженери з досвідом слідкують за стабільністю наших продуктів. Ротація триває тиждень і відбувається приблизно 1 раз на 2 місяці.
On-call інженер реагує на інциденти, отримує дзвінки через PagerDuty, відповідає за ролбеки, дебаг проблем, ідентифікацію, що і де зламалося, та пошук овнерів зламаних модулів для спільної роботи над фіксами.
Переваги для інженерів:
- Вихід із власної «бульбашки» та глибше розуміння інфраструктури.
- Можливість побачити, що роблять інші команди, і челенджити себе під час дебагу.
- Практика прямої відповідальності за продакшн та впливу на те, що делівериться користувачам.
І так, у нас немає окремих девопсів, відповідальних за інфраструктуру. Є люди, які приділяють цьому більше часу, але будь-хто може контріб’ютити до покращення системи. Одна з наших цінностей absolute responsibility проявляється тут дуже чітко.
Постмортеми
Якщо ж інцидент вже стався, важливо якнайшвидше його пофіксити, але потім провести додаткове дослідження: що пішло не так, який корінь проблеми, що ми можемо покращити, щоб ця проблема знову не повторилась.
Варіанти бувають різні. Від «написати додаткові тести на edge-кейси» до «змінити інфраструктуру сервісу, щоб його outage не клав інші сервіси та не було cascading failures».
Система пріоритетів
Щоб реагувати ефективно, важливо чітко виділяти, що є справжнім інцидентом, а що — ні.
- P0 (urgent) — повний outage.
Кидаємо все і йдемо фіксити (+ ролбек до робочого коміту, якщо проблема в коді).
- P1 (high) — не працює певний модуль, але інші operational.
Швидкий фікс та/або ролбек ASAP. Створюємо більш стабільну версію першочергово, вище за всі інші задачі.
- P2 (normal) — звичайний пріоритет.
Може бути баг в продакшені, який ми беремо в спрінт у звичайному режимі.
- P3 (low) — низький пріоритет.
Наприклад, проблема торкається лише
Тільки при P0/P1 створюється інцидент і вживаються всі подальші дії (ролбек, підвищення пріоритету виправлення, залучення on-call, написання постмортему).
Як уникнути повного outage і які failover-стратегії у нас працюють
Наша архітектура — це монолітний «кор» бекенд з окремими сервісами для більш точкових задач. Наприклад, відправка сповіщень чи емейлів, тестування коду, який пишуть студенти на платформі, опрацювання івентів аналітики, синхронізація з CRM, обробка GitHub вебхуків при перевірці домашніх завдань тощо. У такому сетапі ймовірність відмови одного з сервісів досить висока. Важливо, щоб інші сервіси продовжували працювати, і один зламаний модуль не «тягнув за собою» всю систему.
Це явище називається cascading failure. Саме його ми прагнемо уникати через failover-стратегії та ізоляцію сервісів.
Я виділив декілька підходів, які допомагають нам.
Асинхронна взаємодія сервісів
Наприклад, коли юзер пише повідомлення в чаті й натискає Enter, бекенд створює повідомлення і повертає юзеру «OK». Одночасно надсилається івент у message bus, який підхоплює сервіс сповіщень і відправляє пуш. Так, якщо сервіс сповіщень впаде, це не вплине на створене повідомлення в чаті.
Retry стратегія через черги
Ми реалізуємо це за допомогою SQS. Запит потрапляє у чергу і буде повторно оброблений, якщо «приймаючий» сервіс тимчасово недоступний. Раніше ми обробляли GitHub вебхуки напряму, і якщо API фейлило, дані студентів не оброблялись. Після введення черги і retry стратегій запит гарантовано обробляється або повторно ставиться в чергу.
Health-чек і резервна інфраструктура
Сервери постійно перевіряються через health-моніторинг (кожні кілька секунд). Якщо сервер не відповідає, він автоматично відключається від трафіку, а нові запити йдуть на робочі сервери. Необхідно мати резервні сервери; у нас це керується через Kubernetes.
Автоскейлінг
Нові сервери піднімаються автоматично при рості навантаження. Kubernetes робить це «з коробки», для інших сервісів використовується AWS Lambda, які також добре масштабуються.
Concurrency, Throttling і Batching
Обмеження на кількість одночасних запитів (наприклад, сервіс синхронізації з CRM обробляє максимум 10 запитів одночасно). Це хоч і розтягує синхронізацію в часі, але дозволяє бути впевненим, що під час спайків нічого не ляже. Це ж правило працює і при запитах на наше API. Були випадки, коли ми самі себе DDOSили через неоптимізований код на UI, який слав багато запитів. Ми ввели рейт-ліміт і додали моніторинг.
Щодо Batching, так у нас працює внутрішня аналітика. Юзери шлють тисячі івентів за короткий проміжок часу, система їх бетчить під капотом і потім обробляє пачками, а не кожен івент окремо.
Не оверінжинірити
Золоте правило — не ускладнювати собі життя там, де це не потрібно.
Немає коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів