Масштабирование монолита до 1 млн строк: уроки от тимлида до CTO

Обложка: Масштабирование монолита до 1 млн строк: уроки от тимлида до CTO

Что можно узнать из монолита на миллион строк кода? Джек Кинселла проработал 7 лет техлидом и CTO в MorphMarket — маркетплейсе на стеке Django / React (TS) / React Native с командой из 20 человек. За это время кодовая база выросла до 1 000 000 строк, а Джек прошёл путь от первого инженера до технического директора.

Он опубликовал 113 уроков, которые вынес из этого опыта. Мы отобрали самые ценные, сгруппировали по темам и адаптировали для русскоязычной аудитории. Каждый урок — практический: его можно взять и применить в своём проекте уже сегодня.

Ключевые выводы

— Кэширование — последняя мера. Сначала исправьте модель данных, индексы и N+1-запросы.
— E2E-тесты на happy path дают в 10 раз больше отдачи, чем юнит-тесты на отдельные методы.
— Доведите Sentry до inbox zero — это единственный самый мощный шаг для качества продукта.
— Не обвиняйте конкретного разработчика за баг в проде: это всегда каскад из 3+ ошибок системы.
— Деплой за 2 минуты вместо 28 — достижимо, и это меняет культуру принятия решений.

Про архитектуру и масштабирование

Используйте сервисные объекты вместо раздувания моделей

В фреймворках, где модели строятся вокруг существительных (Product, User, Auction), логика неизбежно скапливается в этих классах. Со временем каждый из них превращается в монстра на тысячи строк. Решение — выносить процессы в отдельные сервисные объекты: PlaceAuctionBidService, ProductBillingService. Суть — перестать мыслить существительными и начать мыслить глаголами.

Одна и та же вещь, реализованная дважды — провал технического лидерства

Если в кодовой базе есть два разных клиента к одному платёжному провайдеру, три файла с функциями для работы с датами или дублирующиеся UI-компоненты — это симптом отсутствия ответственного за архитектуру. Создавайте очевидные, легко находимые места для общего кода (common/datetime.py, common/geo.py) и отмечайте переиспользуемый код на ревью.

Полиморфные связи в БД — почти всегда ошибка

ORM-код для полиморфных отношений становится крайне запутанным, и вы теряете защиту через foreign key constraints. Проще добавить nullable FK для каждой связанной модели и написать абстракции на уровне запросов.

Дефолтные фильтры и сортировки в ORM — всегда ошибка

Скрытые дефолты вроде order_by('-id') или filter(is_deleted=False) становятся миной замедленного действия, когда в команде 20+ человек. Новый разработчик не знает про неявное поведение — и получает баг. А скрытый ORDER BY портит аналитические запросы, потому что ORM добавляет ключ сортировки в SELECT.

Вся инфраструктура — в коде

Когда Джек пришёл в MorphMarket, все 27 крон-задач были настроены вручную на продакшн-сервере. Большинство отсутствовали на staging. Перенос в код (а затем конфигурация AWS/Heroku/CloudFlare/Sentry через Terraform) сделал все окружения похожими друг на друга — и, что важно, сделал инфраструктуру доступной для чтения AI-агентам.

Про тестирование

E2E > интеграционные > юнит-тесты

Пользователям важно, чтобы система работала целиком. Один E2E-тест на happy path каждой ключевой фичи (регистрация, покупка, создание листинга) даёт на порядок больше уверенности, чем десятки юнит-тестов. Детали дорабатывайте интеграционными и юнит-тестами — они быстрее и проще в поддержке.

Автоматические тесты для шаблонных фич

В MorphMarket написали систему, которая автоматически тестирует около 300 админ-страниц: парсит имя страницы, находит фабрику, создаёт запись в БД и проверяет, что страница открывается. Тот же подход можно применить к любому CRUD-эндпоинту без выделенного теста.

Изолируйте тесты полностью

Хрупкость тестов убивает мотивацию команды. Основные ловушки:

  • Всё, что связано со временем — замораживайте или мокайте
  • Фиксируйте seed для всех источников случайности — в каждой библиотеке
  • Запретите реальные HTTP-запросы в не-E2E тестах (используйте VCR-подобные библиотеки)
  • Отдельный Redis для тестов с автоочисткой перед каждым тестом
  • Тестовая среда не должна читать ни одной переменной из вашего .env

Мокайте только внешние объекты

Мок — полезный инструмент, но только для внешних зависимостей. Мокать внутренние методы тестируемого класса — антипаттерн. Если можно переписать тест без мока без потери скорости — так и сделайте.

CI должен проверять не только ошибки, но и раздражающие мелочи

В MorphMarket CI также ловит новые warning-и в тестах, рассинхронизацию SDK с бэкендом и забытые миграции в Django. Чем раньше замечена мелочь, тем дешевле её исправить.

Про отладку и мониторинг

Sentry до inbox zero — самый мощный шаг к качеству

Когда Джек пришёл в компанию, Sentry показывал 1 000 ошибок в час. После агрессивной фильтрации (игнор старых браузеров, расширений, пометка JavaScript-ошибок уникальным идентификатором) и починки реальных багов — меньше 1 ошибки в час. Результат: каждая новая ошибка стала событием, на которое команда реагирует немедленно.

Dead Man Switch — мониторьте то, что должно происходить

Исключения шумят и легко ловятся. А вот отсутствие события — нет. Если крон-система перестала работать или бэкапы БД не снимаются — вы узнаете об этом только через heartbeat-мониторы. Настройте их.

Username в каждой строке лога

По умолчанию в логах — только IP-адрес. Но IP ротируется, создаёт лишнее перенаправление при дебаге и не показывает картину при использовании нескольких устройств. Поместите текущий запрос в thread-local и добавьте username в каждую запись.

Трипвайр-алерты на 20 самых важных эндпоинтов

Деградация производительности подкрадывается незаметно — инкрементальные фичи постепенно ломают индексы и кэши. Установите порог в несколько стандартных отклонений от нормы на ключевых эндпоинтах. Команда узнает о проблеме в течение часов после деплоя, а не через неделю жалоб.

Громко падайте при ошибках

Не глушите исключения except: pass. Паттерн от Джека: крашиться локально, мягко отказывать в проде — но всегда отправлять ошибку в Sentry/логи. Чем раньше заметили — тем меньше ущерб.

Про работу в команде

Не обвиняйте одного человека за баг в проде

Любой инцидент — это каскад минимум трёх провалов: автор не заметил, ревьюер пропустил, QA не покрыло, тесты не проверили, архитектура допустила класс ошибки. Обвинение одного человека мешает команде видеть системные причины.

Все разработчики должны быть немного фулстек

В MorphMarket убрали строгое деление на фронтенд/бэкенд/ops. Переобучение заняло год, но окупилось: фронтендер добавляет поле в БД сам, а не ждёт бэкендера — это сокращает время доставки фич.

Пятницы технического долга

Разумный компромисс между CEO, который хочет фичи, и инженерами, которым нужна чистота кода: с понедельника по четверг приоритеты задаёт продукт, а по пятницам — CTO. Бонус: инженеры отдыхают на глубоких задачах перед выходными.

Запретите длинные PR

В MorphMarket был период, когда PR висели по 3 месяца и накапливали тысячи изменений. Их невозможно ревьюить, они конфликтуют с master и создают огромный деплой-риск. Новое правило: мерж в master каждые 1–2 дня, максимум — раз в неделю. Побочный эффект: старшие разработчики дают фидбэк раньше, и драматические переписывания случаются реже.

Поощряйте общение в удалённых командах

Джек начал с ежедневных постов о прогрессе, потом стал публично задавать вопросы (даже на которые знал ответ), делиться изменениями в коде и просить мнения. Это постепенно снизило барьер и сделало общение нормой, а не исключением.

Про производительность

N+1-запросы — враг номер один в ORM

Проблема проявляется на крупных эндпоинтах, которые постепенно расширяют разные авторы: новые фильтры и параметры создают спорадические N+1, которые возникают только при определённом сочетании фильтров и объёме данных. Настройте APM (например, New Relic) для автоматического обнаружения N+1 и обучите команду ловить их на ревью.

Кэширование — последняя мера

Код кэширования чудовищно сложен в поддержке. Сначала исправьте модель данных, допишите индексы, уберите N+1, оптимизируйте алгоритмы. И только после этого — кэш. Джек рассказывает о случаях, когда наивный кэш работал на малых объёмах, но убивал Redis (он однопоточный!) при росте данных на два порядка.

Таймауты на всех уровнях — иммунная система сервиса

Веб-запрос должен завершиться за 25 секунд — иначе убивается. Да, для длинных задач придётся использовать фоновые задачи, но без таймаутов одна тормозящая внешняя API может съесть все потоки сервера и уронить всё.

Выгружайте работу в фоновые задачи

Отправка email, push-уведомления, обработка вебхуков от платёжных систем — всё это должно уходить в очередь. Веб-процесс обязан оставаться быстрым.

ORDER BY id вместо ORDER BY created

Если поле created неизменяемо и совпадает по порядку с id — сортируйте по id. Индекс на id уже есть бесплатно в каждой таблице.

Удаляйте ненужные данные

В MorphMarket некоторые таблицы копили 10 лет данных — логи IP-адресов, записи push-уведомлений, сообщения девятилетней давности. Скрипт очистки делает эти таблицы компактнее и быстрее.

Про безопасность и надёжность

Начните с модели угроз: что может получить атакующий?

Для MorphMarket основные вектора: захват аккаунтов (фикс — обязательный 2FA), спам-фишинг (капча на регистрации + фильтры сообщений), DDoS (Cloudflare + rate limit на nginx + кэш ключевых эндпоинтов). Универсального подхода нет — каждый сервис должен понимать свою специфику.

CHECK-ограничения в БД — лучше предотвратить, чем чинить

Раньше в MorphMarket крон-задача раз в сутки проверяла целостность данных и слала алерты. Всё заменили на CHECK constraints на уровне БД — некоторые проверяют до 16 операций над разными колонками. Невалидные данные просто не могут быть записаны.

Вебхуки — рассадник race conditions

Действие в коде может вызвать вебхук, который прилетит быстрее, чем данные запишутся в БД. Получатель вебхука прочитает устаревшее состояние и может затереть свежие данные. Решение: откладывайте действия, вызывающие вебхуки, до on_commit в БД.

Логируйте намерение перед побочным эффектом — особенно для платежей

Перед списанием с карты или возвратом пишите в лог: "bill user XYZ $100 for Y". Простой Write-Ahead Log спасал MorphMarket в нескольких инцидентах.

Про деплой

Деплой должен быть быстрым — действительно быстрым

MorphMarket сократил деплой с 28 минут до 2. Как: разделили фронтенд и бэкенд, распараллелили, перешли на Rust-based компиляцию JS, вынесли сборку на мощные машины (MacBook M-series вместо commodity Heroku), убрали из слага всё ненужное, почистили старые ассеты из S3.

Smoke-тест после каждого деплоя в прод

Автоматическая проверка ключевых страниц через Playwright после каждого деплоя. Если последние 150 деплоев были скучными — соблазн перестать проверять вручную огромен. Автоматический smoke-тест не устаёт и не теряет бдительность.

Не деплойте ничего рискованного в пятницу

Баг может проявиться вечером или в субботу, когда никого нет. Мелкие хотфиксы и изменения в админке — можно. Всё остальное — до четверга.

Плейбук для инцидентов при деплое

Короткие инструкции: как убить залипшие соединения к БД, точечно почистить кэш (без flush, который разлогинит всех), форсировать обновление ассетов. Обученная команда сокращает даунтайм в разы.

Про карьерный рост — от тимлида до CTO

Научитесь говорить о производительности сотрудников

Джек признаётся, что панически боялся этих разговоров, потому что не любит конфликты. Но реальность проста: лучше открыто сказать человеку, где он не дотягивает, чем копить раздражение и уволить неожиданно. Большинство людей способны расти под менторингом — Джек это недооценивал.

Документ ожиданий от сотрудника

Если написать "мы ценим, когда вы решаете проблемы в #bug-discussion до того, как они дойдут до CTO", люди действительно начнут так делать. Мотивированный сотрудник хочет ясности. Он не может прочитать ваши мысли.

Одно крупное изменение за раз

Внедряете E2E-тесты? Доведите до стабильности, прежде чем браться за линтеры. Если система сделана плохо — команда не будет ей доверять и не будет использовать. Плюс есть предел того, сколько изменений люди могут усвоить одновременно.

CLI-инструменты + AI = максимальный рычаг

AI отлично работает с текстом. CLI-инструменты тоже. Дайте AI доступ к продакшн-логам, отчётам об ошибках, read-only реплике БД, CI-результатам и REPL — и получите огромный множитель продуктивности. Джек называет это "наблюдаемость через CLI — ваш главный усилитель для AI".

FAQ

Почему монолит, а не микросервисы?

Монолит на миллион строк с 20 разработчиками — рабочая модель, если соблюдать дисциплину. Микросервисы добавляют сетевую сложность, и для команды такого размера overhead координации перевешивает выгоды. Джек не утверждает, что микросервисы плохи — он утверждает, что переход оправдан только при конкретной боли, а не как дефолтный выбор.

С чего начать, если в проекте уже бардак?

С Sentry до inbox zero и таймаутов на веб-запросы. Первое даёт видимость реальных проблем, второе предотвращает каскадные отказы. Дальше — по статье: N+1, изоляция тестов, сервисные объекты.

Насколько эти уроки применимы за пределами Django?

Джек 17 лет работал с Rails, Laravel, Express и другими фреймворками — и подчёркивает, что большинство уроков переносимы. N+1, дефолтные сортировки, race conditions в вебхуках, инфраструктура как код — всё это не привязано к конкретному стеку.

Правда ли, что E2E-тесты лучше юнит-тестов?

Не лучше, а дают больше отдачи на первом этапе. Один E2E-тест на happy path покрывает весь стек. Юнит-тесты нужны для деталей и граничных случаев, но начинать покрытие лучше сверху вниз.

Выводы

113 уроков Джека Кинселлы — это не теория из книг, а выжимка из 7 лет ежедневной работы над монолитом, который обслуживает реальных пользователей. Главные принципы:

  • Предотвращайте, а не чините — CHECK constraints, таймауты, линтеры
  • Делайте невидимое видимым — логи, мониторинг, Sentry до inbox zero
  • Инвестируйте в скорость обратной связи — быстрый деплой, быстрый CI, короткие PR
  • Доверяйте людям и системе, а не контролю — fullstack-навыки, плейбуки, документ ожиданий

Оригинальная статья: Scaling a Monolith to 1M LOC: 113 Pragmatic Lessons from Tech Lead to CTO (Semicolon & Sons, Jack Kinsella).

Если знаете коллегу, который борется с растущим монолитом — отправьте ему эту статью. А в комментариях расскажите, какой урок оказался самым неожиданным для вас.