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

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

Ну и прежде чем описывать подводные камни такого решения, уточню, на что конкретно вы подписываетесь, когда слишком рано переходите на микросервисы:

Слабое место Как проявляется Последствия для разработчика
Сложность развёртывания Необходимость оркестрировать более 5 сервисов для работы одной функции Лишние часы работы над каждым релизом
Хрупкость локальной разработки Расползание Docker, сломанные скрипты, платформо-зависимость Медленный онбординг, частые сбои
Дубликация CI/CD Несколько пайплайнов с повторяющейся логикой Дополнительная плата за каждый сервис
Связывание сервисов «Разделённые» сервисы, тесно связанные общим состоянием Медленное внесение изменений, дополнительные хлопоты по координации
Издержки наблюдаемости Распределённая трассировка, логирование, мониторинг Требуются недели для должной настройки всех инструментов
Фрагментация наборов тестов Тесты разбросаны по сервисам Ненадёжные тесты, низкая уверенность
Далее я расскажу, почему микросервисы на ранних этапах зачастую дают обратный эффект, когда они действительно помогают, и как структурировать системы стартапа для повышения скорости его роста и выживаемости.

▍ Монолит вам не враг



Если вы создаёте продукт SaaS, даже простая обёртка базы данных SQL в конечном итоге может привнести много внутренней сложности, связанной с её бизнес-логикой. Вы также можете прибегать к различным интеграциям и фоновым задачам, которые позволяют преобразовывать один набор данных в другой.

Со временем ваше приложение может обрасти ненужными функциями и стать беспорядочным. Монолиты же отличаются тем, что продолжают работать — даже в беспорядочном виде они позволяют команде акцентировать внимание на самом важном:

  • выживание;
  • создание ценности для клиента

Самое большое преимущество монолитов в простоте их развёртывания. Как правило, подобные проекты создаются на базе существующих фреймворков — это может быть Django для Python, ASP.Net для C#, Nest.js для Node.js и так далее. Придерживаясь монолитной архитектуры, вы получаете значительное преимущество перед вычурными микросервисами — обширную поддержку со стороны опенсорсного сообщества и мейнтейнеров проекта, которые изначально создавали эти фреймворки для обеспечения работы в виде единого процесса, монолитного приложения.

Расскажу одну историю. Как-то в стартапе, занимавшемся недвижимостью, я вёл команду фронтенда и периодически консультировал ребят из бэкенда в выборе технологий. Так вот, наше приложение создавалось на Laravel и претерпело интересную эволюцию. Начиналось оно с небольшой информационной таблицы для риелторов, но в итоге разрослось в куда более масштабную систему.

Со временем приложение превратилось в многофункциональный программный пакет, который обрабатывал сотни гигабайт документов и интегрировался с десятками сторонних сервисов. При этом он всё также опирался на откровенно простой стек PHP, функционирующий на сервере Apache.

Наша команда активно налегала на лучшие практики, рекомендованные сообществом Laravel. И это себя оправдало. Мы смогли значительно масштабировать возможности приложения, продолжая соответствовать задачам и ожиданиям бизнеса.

Интересно, что нам ни разу не пришлось разделять систему на микросервисы или применять более сложные паттерны инфраструктуры. Таким образом мы избежали большого объёма непредвиденной сложности. Простота архитектуры дала нам важный рычаг для решения задач. И это отражает опыт других людей. К примеру, в статье компании Basecamp «Majestic Monolith» авторы расписывают, почему на начальных этапах простота подобна суперсиле.

Люди нередко сетуют, что сложно делать монолиты масштабируемыми, но подобные сложности могут быть вызваны неудачной модульной структурой ПО.

Вывод: грамотно структурированный монолит позволяет команде сосредоточиться на поставке продукта, а не тушении «пожаров».

▍ Но разве микросервисы не являются «лучшей практикой»?


Многие инженеры сразу начинают с микросервисов, считая, что «это правильный путь». Да, при больших масштабах они помогают. Но в стартапе аналогичная сложность ведёт к торможению разработки. Микросервисы оправдывают себя, только когда у вас есть реальные узкие места, препятствующие масштабированию, большие команды или независимо развивающиеся направления.

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

Вывод: микросервисы — это инструмент для масштабирования, а не стартовый шаблон.

▍ Когда микросервисы не к месту (особенно на ранних этапах)


В одной из начинающих команд, которую я консультировал, решение разделить сервисы создало больше хлопот по координации разных отделов, нежели принесло технической пользы.

Архитектура определяла не только форму кода, но также схему планирования, оценки и поставки продукта. Причём эти организационные издержки на первых порах легко упустить из виду, а потом становится слишком поздно.

Диаграмма: издержки координации растут линейно с количеством сервисов и экспоненциально при добавлении продакт-менеджеров, дедлайнов и несогласованных таймлайнов

Вот несколько наиболее распространённых анти-паттернов, которые могут просочиться в систему на ранних этапах.

▍ 1. Произвольные границы сервисов



В теории мы часто видим предложения разделять ПО на основе бизнес-логики — пользовательский сервис, сервис продукции, сервис заказов и так далее. Такой подход часто заимствуется из принципов предметно-ориентированного дизайна (Domain-Driven Design, DDD) или чистой архитектуры (Clean Architecture), которые имеют смысл в больших масштабах. В случае же начинающих продуктов, ещё не достигших стабильности и надёжности, они могут привести к преждевременному окостенению структуры.

В итоге вы получаете:

  • общие базы данных;
  • вызовы между сервисами в простых рабочих потоках;
  • связывание под видом «разделения».

Ещё в одном проекте я наблюдал, как команда разделила пользователя, аутентификацию и авторизацию в отдельные сервисы. Это усложнило развёртывание и затруднило координацию сервисов при любой операции API, какую бы разработчики ни создавали.

В реальности бизнес-логика не отображается непосредственно в границы сервиса. Преждевременное разделение может сделать систему более хрупкой и часто замедляет внесение изменений.

Вместо этого нужно точечно изолировать узкие места, опираясь на реальный план масштабирования, а не теоретическую элегантность.

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

Вывод: разделяйте систему не по теории, а по фактическим узким местам.

▍ 2. Расползание репозитория и инфраструктуры


При разработке приложения обычно важно следующее:

  • согласованность стиля кода (линтинг);
  • инфраструктура тестирования, включая проверку интеграции;
  • конфигурация локальной среды;
  • документация;
  • конфигурация CI/CD.

При работе с микросервисами всё это нужно умножать на количество сервисов. Если ваш проект имеет структуру монолита, вы можете упростить себе жизнь, наладив централизованную конфигурацию CI/CD (при работе с GitHub Actions или GitLab CI). Некоторые команды разделяют микросервисы по разным репозиториям, что сильно усложняет сохранение согласованности кода и одинаковых конфигураций без привлечения дополнительных инструментов, а значит и усилий.


Для команды из трёх человек это перебор. Переключение между репозиториями и дополнительные инструменты увеличивают время разработки каждой функции.

▍ Устранение проблем за счёт использования единого репозитория и языка программирования


Исправить эту проблему можно по-разному. В молодых проектах самое важное — это хранить весь код в едином репозитории. Такой подход гарантирует присутствие в продакшене одной версии программы, а также упростит координацию код-ревью и взаимодействие внутри небольших команд.

В случае проектов Node.js я настоятельно рекомендую использовать инструмент nx или turborepo, так как они:

  • упрощают настройку CI/CD в подпроектах;
  • поддерживают кэширование сборки на основе графа зависимостей;
  • Позволяют рассматривать внутренние сервисы как библиотеки TypeScript (с помощью импортов ES6).

Эти инструменты экономят время, которое в противном случае тратится на связующий код или очередное продумывание оркестрации. Но есть у них и существенные недостатки:

  • сложные деревья зависимостей могут быстро разрастаться;
  • настройка производительности CI имеет свои особенности;
  • иногда для сокращения времени сборки требуется более быстрый инструментарий (например, bun).

Итак: инструменты вроде nx или turborepo в небольших командах повышают скорость работы с единым репозиторием — если только вы готовы вкладываться в поддержку их чистоты.

При разработке микросервисов на базе Go на ранних стадиях будет удобно трудиться в едином пространстве, используя директиву replace в go.mod. В конечном итоге по мере расширения ПО можно будет без лишних усилий выделить модули Go в отдельные репозитории.

Вывод: единый репозиторий с общей инфраструктурой экономит ваше время, а также обеспечивает согласованность и душевный покой.

▍ 3. Проблемы в локальной среде ведут к утрате скорости


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

Молодые проекты часто страдают от:

  • недостатка документации;
  • устаревших зависимостей;
  • применения техник, опирающихся на конкретную ОС (например, конфигурации под Linux).

Из своего опыта могу сказать, что проекты, которые мне доставались от других команд, часто создавались под одну операционную систему. Некоторые разработчики предпочитали писать под macOS и никогда не озадачивались поддержкой своих скриптов оболочки под Windows. В командах, с которыми я сотрудничал, бывали инженеры, работавшие на машинах с Windows. И им часто приходилось переписывать скрипты оболочки или капитально перестраивать процесс налаживания работы локальной среды.

Со временем мы стандартизировали настройку среды для ОС разработчиков, чтобы уменьшить трения внутри команды. Это стало небольшим вложением, которое позволило сэкономить немало часов работы с приходом каждого нового инженера. Было нелегко, но мы осознали всю важность того, чтобы код запускался на ноутбуке любого нового разработчика, какую бы систему тот ни использовал.

Приведу пример ещё одного проекта, где инженер создал хрупкую конфигурацию микросервиса, который поток обработки контейнеров Docker смонтировал в локальную файловую систему. Естественно, когда ваш ПК работает под Linux, вы платите очень мало за выполнение процессов в виде контейнеров.

Но вот приход в команду нового фронтенд-разработчика, у которого на рабочем ноутбуке стояла Windows, привёл к кошмару. Ему приходилось запускать аж десять контейнеров, чтобы просто увидеть свой UI. При этом всё постоянно ломалось — то тома, то сеть, то проблемы с совместимостью контейнеров — и сама конфигурация была очень плохо задокументирована. Всё это создало немало трений в процессе онбординга.

В итоге мы вместе придумали прокси на Node.js, который имитировал конфигурацию nginx/Docker без контейнеров. Да, элегантности этому решению не хватало, зато оно позволило новому разработчику, наконец, включиться в общую работу.


Вывод: если ваше приложение работает только в одной ОС, то от провала продуктивности вас отделяет лишь один ноутбук.

Совет: в идеале старайтесь делать git clone <repo> && make up, чтобы проект запускался локально. Если это невозможно, необходимо вести актуальный файл README с инструкциями для Windows/macOS/Linux. Сегодня существуют языки программирования и наборы инструментов, которые плохо работают под Windows (например, OCaml), но популярный программный стек прекрасно себя чувствует в любой из распространённых операционных систем. Ограничение конфигурации лишь одной ОС зачастую говорит о недостаточном продумывании процесса разработки.

▍ 4. Несоответствие технологий


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

  • Node.js и Python: такая комбинация отлично подходит для быстрого внесения доработок, но усложняет управление сборкой артефактов, версиями зависимостей, а также обеспечение согласованности среды выполнения.
  • Go: компилируется в статические бинарные файлы, обеспечивает быструю сборку и низкие операционные издержки. Более гармонично подходит в случаях, когда требуется разделение.

Очень важно выбрать подходящий технологический стек с самого начала. Если вы ориентированы на быстродействие, обратите внимание на JVM с её экосистемой и возможностью масштабного развёртывания и выполнения артефактов в архитектурах на базе микросервисов. Если же вас интересует частое внесение доработок и оперативное прототипирование без масштабирования инфраструктуры развёртывания, то вполне подойдёт что-то вроде Python.

Довольно часто команды осознают, что выбрали совсем неподходящую технологию, которая изначально такой не казалась. В итоге им приходится вкладываться в переписывание бэкенда на другом языке (как в этом случае, когда инженерам пришлось выкручиваться из ситуации со старой кодовой базой на Python 2 и в итоге переносить её на Go).

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

Иногда вы подходите к точке, когда нужно расширить функциональность с привлечением машинного обучения или ETL-инструментов. В таком случае вы просто отдельно создаёте МО-инфраструктуру на Python, поскольку он имеет богатую экосистему предметно-ориентированных библиотек, которой не может похвастаться ни один другой язык. Но такие решения нужно принимать, когда в штате есть достаточно специалистов. В противном случае небольшая команда погрузится в пучину сложности совмещения нескольких программных стеков.

Вывод: сопоставляйте используемые технологии со своими возможностями, а не амбициями.

▍ 5. Скрытая сложность: коммуникация и мониторинг


Микросервисы несут с собой незримый набор потребностей:

  • обнаружение сервиса;
  • версионирование API;
  • повторы, защитные механизмы вроде circuit breaker и fallback;
  • распределённая трассировка;
  • централизованное логирование и предупреждение.

В монолите баг может раскрываться простой трассировкой стека. В распределённой же системе это подразумевает вопрос «Почему сервис А падает, когда развёртывание сервиса В отстаёт от развёртывания С на 30 секунд?» Здесь вам придётся прилично вложиться в набор технологий для наблюдаемости. Чтобы сделать всё «правильно», потребуется определённым образом снарядить ваше приложение. Это может подразумевать интеграцию OpenTelemetry для поддержки трассировки или привлечение инструментов облачного провайдера вроде AWS XRay, если вы реализуете крупную бессерверную систему. Но в этот момент вам придётся полностью сместить акцент с кода приложения на создание сложной инфраструктуры мониторинга, которая обеспечит уверенность в должном функционировании системы в продакшене.

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

Совет: уясните, что распределённые системы требуют вложений, поэтому их использование влечёт за собой целый класс новых инженерных задач.

▍ Когда микросервисы оправданы


Несмотря на упомянутые сложности, в некоторых случаях разделение на уровне сервисов оказывается очень кстати. Это может быть:

  • Изоляция рабочей нагрузки: стандартным примером будет следование лучшим практикам AWS по использованию уведомлений S3 — когда изображение загружается в S3, активируется процесс изменения его размера/оптического распознавания символов и тому подобное. Польза в том, что это позволяет выделить непонятные библиотеки обработки данных в собственный изолированный сервис и закрепить за его API только обработку изображений и генерацию вывода на основе загруженных данных. В итоге вышестоящие клиенты, загружающие данные в S3, не будут связаны с этим сервисом, плюс уменьшатся издержки, связанные с его оснащением, так как он будет относительно прост.
  • Расходящиеся потребности в масштабировании: представьте, что создаёте ИИ-продукт. Одна из его частей (веб API), которая активирует рабочие нагрузки для модели МО и выдаёт результаты, не требует много ресурсов, так как взаимодействует в основном с базой данных. Но вот модель МО выполняется на GPU, является тяжеловесной и требует использования особых машин с поддержкой GPU и дополнительных настроек. Разделив эти части приложения в отдельные сервисы, работающие на разных машинах, вы сможете масштабировать их независимо.
  • Различные требования к среде выполнения: предположим, у вас есть легаси-код, написанный на C++. В таком случае перед вами 2 варианта — магически перевести его на язык основной программы или найти способы интегрировать его в неё. В зависимости от сложности легаси-приложения, вам может потребоваться писать связующий код, а также реализовывать дополнительные сетевые механизмы и протоколы для взаимодействия с этим сервисом. Но суть в том, что из-за несовместимости сред выполнения вам наверняка придётся выделить это приложение в отдельный сервис. Более того, вы также можете написать основное приложение на C++, но из-за различий в конфигурациях компилятора и зависимостях вам не удастся легко скомпилировать всё это вместе в едином исполняемом файле.

Крупные компании сталкивались с аналогичными проблемами. К примеру, команда разработки в Uber описала свой переход к предметно-ориентированной архитектуре микросервисов — и не из-за её теоретической чистоты, а в результате возникновения реальной сложности взаимодействия между командами и достижения пределов масштабирования. Их статья является хорошим примером того, как микросервисы могут работать, когда у вас достаточно зрелая организация, и есть достаточный операционный ресурс для их поддержки.

В одном проекте, который тоже касался работы с недвижимостью, нам от предыдущей команды достался код Python, который выполнял аналитику, загружая полученные данные в БД MS-SQL. В той ситуации мы поняли, что будет излишне затратно и бессмысленно создавать поверх такого механизма приложение на Django. В имевшемся коде использовались другие зависимости среды выполнения, и он был достаточно изолирован, поэтому мы держали его отдельно и возвращались к нему, только когда что-то шло не так. Такой подход сработал даже для нашей небольшой команды, поскольку этот сервис аналитики редко требовал изменений или обслуживания.

Вывод: используйте микросервисы при наличии расходящихся в своей сути рабочих процессов, а не просто, потому что они сулят чистоту.

▍ Практические советы для стартапов


Если вы готовите свой первый продукт, вот вам несколько рекомендаций:

  • Начинайте с монолита. Выберите распространённый фреймворк и сосредоточьтесь на реализации функциональности. Любого известного фреймворка с лихвой хватит для создания API или сайта и обслуживания пользователей. Не гонитесь за хайпом. Пусть это будет рутинный путь, зато впоследствии вы сами себе скажете «спасибо».
  • Один репозиторий. Не озадачивайтесь разделением кода на несколько репозиториев. Я работал с основателями компаний, которые хотели разделить репозиторий для уменьшения риска кражи подрядчиками их интеллектуальной собственности — обоснованное беспокойство. Но на практике это обеспечивало больше напряжения, нежели безопасности — замедление сборок, фрагментирование цикла CI/CD и слабая видимость процессов среди команд. В итоге такая неэффективная защита интеллектуальной собственности просто не стоила связанных с ней операционных хлопот, особенно притом, что внутри единого репозитория управляться с нужными механизмами доступа было проще. В начинающих проектах ясность и скорость важнее теоретического прироста безопасности.
  • Простейшая локальная конфигурация. Используйте make up. Если этого недостаточно, тщательно продумывайте все шаги, записывайте видео с экрана и добавляйте скриншоты. Если ваш код будет выполнять начинающий разработчик, он наверняка столкнётся с проблемой, и вам придётся объяснять, как её решить. Я на личном опыте понял, что документирование всех возможных проблем в каждой операционной системе избавляет от необходимости тратить время на объяснение, почему определённые моменты в локальном сеттинге не работают.
  • На начальных этапах вкладывайтесь в CI/CD. Даже если это простой HTML-код, который несложно скопировать (scp) на сервер вручную, этот процесс можно автоматизировать с помощью систем контроля версий и CI/CD. Когда конфигурация должным образом автоматизирована, вы просто забываете об инфраструктуре непрерывной интеграции и сосредотачиваетесь на функциональности. Я видел немало команд и основателей компаний, которые при работе со сторонними подрядчиками зачастую экономят на CI/CD, что приводит к негодованию разработчиков, которым надоедает деплоить всё вручную.
  • Разделяйте точечно. Разделяйте программу, только когда это точно решит острую проблему. В противном случае лучше вкладываться в модульность и тесты внутри монолита — это можно реализовать быстрее и легче.

Ну и самое главное: оптимизируйте систему под скорость разработки.

Скорость подобна кислороду для вашего стартапа. Преждевременное внедрение архитектуры микросервисов приводит к постепенной утечке этого кислорода, пока в конечном итоге стартап просто не задохнётся.

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

▍ Если вы склонитесь к микросервисам


Я сталкивался с проектами, которые переходили на микросервисы раньше, чем следовало бы, и вот несколько рекомендаций на подобный случай:

  • Оценивайте свой технологический стек, на который опирается архитектура микросервисов. Вкладывайтесь в развитие инструментов для разработчика. Когда вы разделяете проект на сервисы, вам приходится автоматизировать не только их стек, но также конфигурацию локальной и продакшен среды. В определённых проектах мне приходилось создавать отдельный CLI, который выполнял административные задачи в едином репозитории. В одном случае у нас было 15-20 микросервисов, и для локального деплоя мне пришлось создавать инструмент командной строки, чтобы тот динамически генерировал файлы docker-compose.yml, которые бы позволили обычному разработчику легко запускать программу одной командой.
  • Используйте надёжные протоколы коммуникации между сервисами. Если это асинхронный обмен сообщениями, проследите, чтобы их схемы были согласованы и стандартизованы. Если это REST, обратитесь к документации OpenAPI. В клиентах, отвечающих за межсервисную коммуникацию, нужно реализовать много нештатных операций: повторы с экспоненциальным увеличением периода ожидания, таймауты. Типичный голый клиент gRPC требует ручного налаживания всех этих дополнительных механизмов для исключения спорадических ошибок.
  • Проследите, чтобы ваша конфигурация модульных, интеграционных и сквозных тестов была стабильна и при дополнительных разделениях кодовой базы на отдельные сервисы хорошо масштабировалась.
  • В небольших проектах, где используется разделение на уровне микросервисов, для согласованной организации кода взаимодействия и механизмов наблюдения лучше задействовать общую библиотеку с типичными вспомогательными функциями. При этом важно сохранять минимальный размер этой библиотеки. Любое серьёзное изменение приведёт к повторной компиляции всех зависимых сервисов — даже если оно их не касается.

  • Налаживайте механизмы наблюдения на ранних этапах. Добавляйте структурированные логи JSON и создавайте соответствующие ID для отладки приложения после его развёртывания. Даже простейшие вспомогательные функции, выдающие богатую информацию для журналов (при условии снаряжения приложения правильными инструментами логирования/трассировки) часто экономят время, выявляя подозрительные пользовательские потоки.

Подытожим. Если вы всё равно собираетесь переходить на микросервисы, то должны заранее учитывать сопутствующие издержки в плане дополнительных усилий по развёртыванию и обслуживанию, необходимые для создания конфигурации, с которой сможет работать любой инженер в вашей команде.

Вывод: если вы решили перейти на сложную архитектуру, приложите усилия, чтобы сделать её управляемой.

▍ Заключение


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

Первым делом — выживание. Масштабирование потом. Используйте простейшую систему, которая работает, и оправдывайте каждый привносимый в неё уровень сложности.

Telegram-канал со скидками, розыгрышами призов и новостями IT ?

Комментарии (1)


  1. Dhwtj
    18.05.2025 10:00

    Недешёвый инструмент управления сложностью и чуть чуть масштабируемостью