Вольный перевод https://nexo.sh/posts/microservices-for-startups/
Раннее разделение на микросервисы может снизить скорость работы вашей команды
В стартапе важны скорость разработки, скорость внедрения новых функций. Архитектура проекта и организационная структура команды влияют на это непосредственно. Микросервисная архитектура как чрезмерное проектирование на начальном этапе может привести к тому, что сами микросервисы будут технически незавершены, их развертывание на машинах разработчиков - проблематичным, а сложность настройки будет деморализовывать команду.
Раннее внедрение микросервисов
Болевая точка | Что в действительности происходит | Негативные последствия |
---|---|---|
Сложность развертывания | Внесение изменений в 5 и более сервисов для реализации одной функции | Увеличение времени релиза |
Сложность настройки среды разработки | Объемные Docker, сломанные скрипты, трюки для настройки под разные операционные системы | Медленная адаптация новых разработчиков, частые поломки среды разработки |
Дублирование CI/CD | Несколько дублирующих процессов | Дополнительная работа на каждый сервис |
Межсервисная связность | “Несвязанные” сервисы связаны общим состоянием | Медленные изменения, затраты на координацию |
Сложный мониторинг | Распределенная отладка, логирование, мониторинг | Долгая настройка |
Фрагментированные тесты | Тесты разбросаны по сервисам | Хрупкие тесты, низкое доверие к тестам |
Монолиты - не враги
В разработке SAAS-продукта даже простая обертка вокруг SQL может увеличить внутреннюю сложность приложения и потребовать дополнительных интеграций.
Со временем в приложении часто появляются ненужные функции, код раздувается и становится запутанным, но монолиты в таких ситуациях проявляют свои лучшие качества - они продолжают работать. Монолит, даже структурно сложный, позволяет вашей команде сфокусироваться на том, что важно для стартапа:
- Оставаться на плаву
- Выпускать полезные для пользователей функции
Громадное преимущество монолита - это простота в развертывании. Обычно такие проекты построены вокруг проверенных фреймворков (например, Django для Python, ASP.Net для C#, Nest.js для Node.js, и т.д). Разрабатывая монолитное приложение вы получаете широкую поддержку сообщества, которое создавало и проектировало эти платформы для применения в первую очередь в монолитах.
В одном стартапе в сфере недвижимости я руководил командой фронтендеров, и иногда консультировался с командой бекенда на предмет выбора технологий. Наше приложение на базе Laravel выросло из небольшой панели управления для агентов недвижимости в большую сложную систему.
Со временем оно превратилось в многофункциональный сервис, обрабатывающий сотни гигабайт документов и интегрированный во множество сторонних сервисов, работая на простом стеке PHP + Apache.
Команда опиралась на лучшие практики Laravel сообщества, и мы смогли значительно масштабировать приложение, по прежнему удовлетворяя потребности и ожидания бизнеса.
Нам никогда не приходилось разделять систему на микросервисы или внедрять более сложные инфраструктурные шаблоны. Простая архитектура позволила избежать многих проблем. Вспомните статью Basecamp про “Величественный монолит”, в которой объясняется почему простота сама по себе является суперсилой.
Монолиты считаются сложно масштабируемыми, но причиной этому часто является именно плохая внутренняя структура приложения, а не его монолитность.
Вывод: Хорошо структурированный монолит позволяет сосредоточиться на ценности продукта.
Но ведь микросервисы - это “лучшие практики”?
Многие разработчики, внедряя микросервисы на раннем этапе, считают, что это - правильный путь, основанный на “лучших практиках”. Микросервисы действительно помогают в масштабировании, однако в стартапах их сложность вставляет палки в колеса.
Микросервисы окупаются только при наличии реальных проблем с масштабированием. Но пока такие проблемы отсутвуют, микросервисы становятся слишком затратными, дублируя инфраструктуру, усложняя настройки, затягивая итерации. Например, компания Segment в конечном счете отказалась от микросервисов именно по этим причинам.
Вывод: Микросервисы - инструмент масштабирования, а не стартовый шаблон приложения.
Как именно микросервисы вредят (особенно на ранних стадиях)
В одной организации, которую я консультировал, решение построить архитектуру вокруг микросервисов привело к увеличению затрат на управление, а не к техническому выигрышу. Архитектура определяла не только код, но еще и то, как мы планировали, оценивали работу и развертывали приложение.
Диаграмма: затраты на координацию росли линейно с количеством сервисов и экспонинцеально при подключении продуктового менеджера и при попытках вписаться в дедлайн.
Вот наиболее характерные анти-паттерны, появляющиеся на ранней стадии развития приложения при внедрении микросервисов
1. Проблема с разделением ответственности
Теория популяризует разделения бизнес-логики на домены: сервис пользователей, сервис продуктов, сервис заказов и т.д. Это идеи предметно-ориентированного проектирования (Domain-Driven Design) или чистой архитектуры (Clean Architecture), которые имеют смысл при масштабировании, но на ранней стадии разработки они усложняют стуктуру приложения, прежде чем сам программный продукт станет стабильным и верифицированным. В этоге вы получаете:
- Базы данных с общим доступом из разных сервисов
- Сквозные вызовы API разных сервисов для упрощения логики
- Связность, замаскированная под “разделение”
В одном проекте я наблюдал, как команда разработчиков формировала разные сервисы для управления пользователями, аутентификацей и авторизацей. Это приводило к проблемам в развертывании и координации для любого API, которые они разрабатывали.
На самом деле бизнес логика в общем случае не делится по границам сервисов. Преждевременное разделение делает систему более хрубкой и сложной для изменений.
Гораздо полезнее - искать и устранять конкретные проблемы с маштабированием приложения.
В некоторых случаях мы эмулировали разделение логики приложения на уровне внутренней структуры без реального физического разделения на уровне сервисов. Это позволяло принять взвешенное решение, прежде чем реализовывать привязку к новой инфраструктуре.
Вывод: Делите приложение на сервисы не на основании теории, а на основании анализа существующих проблем с масштабированием.
2. Неконтроллируемый рост количества репозиториев и сложности инфраструктуры
Во время работы над приложение обычно важные следующие вещи:
- Согласованный стиль кода (linting)
- Тестирование инфраструктуры и интеграционные тесты
- Настройка приложения на машине разработчика
- Документация
- Настройка CI/CD
Работая с микросервисами, вы вынуждены внедрять это для каждого сервиса в отдельности. Пока приложение находится в единственном репозитории, вы можете упростить свою жизнь, используя централизованную конфигурацию CI/CD. Некоторые команды разделяют микросервисы в отдельные репозитории, что значительно усложняет поддержку согласованности кода и настроек.
Для команды из трех человек это непросто. Переключение контекста между репозиториями и инструментами увеличивает время разработки каждой функции.
Как улучшить ситуацию, используя единый репозиторий и единый язык программирования
Существует несколько способов нивелировать эти проблемы. Для проектов на ранней стадии важно использовать единый репозиторий для программного кода. Это гарантирует, что в продакшене может существовать только одна единственная версия кода, это упрощает проверку кода и координацию для небольшой команды.
Для проектов на Node.js я рекомендую использовать такие инструменты работы с единым репозиторием как nx или turborepo. Оба инструмента:
- Упрощают конфигурацию CI/CD в подпроектах
- Поддерживают кэширование сборки на основе графов зависимостей
- Позволяют работать с внутренними сервисами как с библиотеками TypeScript (через импорт ES6)
Эти инструменты экономят время, потраченное на написание связывающего кода и переработу оркестрации. Но тем неменее это все-таки компромисс:
- Сложность дерева зависимостей может рости быстро
- Оптимизация производительности CI - не тривиальна
- Возможно, вам понадобится более быстрый инструмент (например, bun) для сокращения времени сборки
Для микросервисов на основе GO неплохая идея - использовать единое рабочее пространство (в терминах GO) с заменой диркетив в go.mod. По мере масштабирования приложения модули всегда можно будет вынести в отдельные репозитории.
Вывод: единый репозиторий с объединенной инфраструктурой экономит ваше время, дает согласованный и адекватный код.
3. Проблемы с развертыванием на машинах разработчиков = проблемы со скроростью разработки
Если запуск приложения на машине разработчика занимает три часа, требует настройки кастомных скриптов и докера, со скоростью разработки проблемы.
Проекты на ранней стадии развития зачастую страдают от:
- Неполной документации
- Устаревших зависимостей
- Трюков для настройки под определенные операционные системы (например, поддерживают только Linux)
Исходя из моего опыта, многие проекты, которые я наблюдал, зачастую разрабатывались под единственную операционную систему. Некоторые разработчики предпочитали сборку на maxOS и никогда не беспокоились о поддержке сборки на Windows. В моих прошлых проектах инженеры предпочитали использовать машины на Windows, и часто это требовало переписывания скриптов или полного понимания и анализа процесса запуска в локальном окружении. После стандартизации настройки среды сборки для всех разработчиков мы смогли упростить работу и сэкономили время на адаптацию новых разработчиков.
Еще в одном проекте, который создавался разработчиком “в одни руки”, процесс настройки среды запуска микросервисов на основе Docker-контейнеров использовал локальную файловую систему. Конечно, это эффективно, когда ваш компьютер работает под управлением Linux.
Но подключение нового фронтэндера на старом ноутбуке с Windows превратилось в страшное дело. Им пришлось развернуть 10 контейнеров, чтобы просто запустить на компьютере фронтэндера пользовательский интерфейс. Ничего не работало: диски, сеть, совместимость контейнеров.
В итоге мы сделали прокси на базе Node.js для имитиации веб-сервера без контейнеров. Это не было элегантным решением, но позволило сотруднику начать работу.
Вывод: если ваше приложение развертывается только на единственной операционной системе, то вы - на расстоянии одного ноутбука от катастрофы.
Совет: В идеале, ваш проект должен запускаться всего лишь с помощью git clone && make. Если это не возможно, то поддерживайте актуальный README с инструкцией для Windows/macOS/Linux. В настоящее время существуют некоторые языки программирования, имеющие плохую поддержку Windows (например, OCaml), но современный широко распространенный стек разработки обычно хорошо работает в любой операционной системе. Ограничение одной операционной системой часто является симпотомом недостаточной инвестиции в DX.
4. Несовместимость технологий
Помимо архетиктуры, ваш технический стек также влияет на то, насколько болезненными становятся микросервисы: далеко не каждый язык хорошо вписывается в микросервисную архитектуру.
- Node.js и Python: Отлично подходит для быстрой разработки, но управление артефактами сборки, версиями зависимостей, согласованностью рантайма между различными сервисами быстро становится проблематичным.
- Go: Компилируемый язык, быстрая сборка и высокая эффективность. Естесственным образом подходит, если требуется разделение на сервисы.
Важно выбрать правильный технический стек на самом раннем этапе. Если вам важна производительность, стоит обратить внимание на JVM и ее экосистему, а так же возможность развертывания для поддержки масштабирования и запуска в микросервисной архитектуре. Если вам важна скорость разработки без масштабирования инфрастуктуры, то что-то вроде Python - неплохой выбор.
Зачастую команды поздно осознают, какие проблемы они получили, выбрав определенную технологию, и вынуждены переписывать бекенд на другой язык. Например, вот эта команда была вынуждена переписать систему с Python 2 на Go.
Однако, если вам действительно необходимо, вы можете объединить разные языки в одной системе с помощью таких протоколов как gRPC или с помощью асинхронного обмена сообщениями. Например, если вам необходимо дополнить свою систему функциями машинного обучения или ETL, то вам наверняка понадобится внедрить Python из-за его уникальных библиотек. Для этого нужны будут дополнительные разработчики с соответствующей компетенцией, иначе ваша небольшая команда завязнет в борьбе со сложностью объединения нескольких стеков разработки.
Вывод: Выбирайте технологию в соответствии с вашими ограничениями, а не с вашими амбициями.
5. Скрытая сложность: Коммуникации и мониторинг
Микросервисы требуют:
- Service discovery
- Версионирования API
- Повторные запросы, circuit breakers, фаллбеки
- Распределенную отладку
- Централизированное логирование и алерты
В монолите описать ошибку можно простой трассировкой стека вызовов. В распределенной системе появляются проблемы “почему сервис A падает, если сервис B вдруг подвисает на 30 секунд?” И вы должны потратить время на построение адекватной системы отслеживания ошибок. Чтобы сделать это “правильно” необходимо определенным образом настроить ваши приложения, например интегрировать OpenTelemetry для поддержки трассировки, или использовать сервисы, предоставляемыми ваши облачным провайдером, например, AWS XRay, если вы используете сложную serverless-систему. Но для этого вам необходимо сместить фокус с разработки новых функций на создание сложной системы мониторинга инфраструктуры, которая позволит вам понять, действительно ли авше приложение корректно функционирует в продакшене.
Конечно, ситема мониторинга нужна и для монолита, но здесь ее построить гораздо проще с точки зрения количества сервисов, которые необходимо поддерживать в согласованном состоянии.
Совет: Поймите, что распределенность системы - не бесплатная опция. Она нужна для решения определенного класса инженерных задач.
Когда микросервисы действительно имеют смысл
Несмотря на упомянутые сложности с микросервисами, в некоторых случаях их применение действительно полезно:
- Workload Isolation: распространенный пример - использование уведомлений AWS S3 — когда изображение загружается в S3 и запускаются процессы ресайза, OCR, и т.д. Почему это удобно: мы можем изолировать специфичные библиотеки обработки изображений в отдельный сервис, разработать для него специализированный API, ориентированный исключительно на обработку изображений. Клиенты, загружающие данные в S3, не связаны с этим сервисом, и при использовании такого сервиса меньше накладные расходы из-за его относительной простоты.
- Разные потребности в масштабировании: — Представьте, что разрабатываете приложение на базе ИИ. Одна часть системы (web API) запускает процессы машинного обучения ML и показывает последние результаты. Эта часть не требует больших ресурсов, легковесная, так как просто взаимодействует с БД. С другой стороны ML-модель, требующая GPUs, действительно тяжелая и требует специальных машин с поддержкой GPU и дополнительной настройкой. Разделяя эти части в отдельные сервисы, исполняемые на разных машинах, вы можете масштабировать их независимо.
- Невозможность совместить разные части в виде одного приложения: — Допустим у вас есть устаревшая часть кода, написанная на C++. У вас два варианта — неким магическим способом преобразовать его в ваш оснвной язык разработки или найти способ интегрировать его с основными приложением. В зависимости от сложности легаси-кода, вам придется написать обертку, реализующий сетевые протоколы, позволяющие взаимодействовать с этим сервисом и выделить эту часть в отдельный сервис из-за невозможности скомпилировать все в виде единого приложения.
Крупные организации столкнулись с аналогичными проблемами. Например, команда Uber описала свой переход к доменно-ориентированной микросервисной архитектуре — не из теоретической чистоты, а в ответ на реальную сложность и ограниченность масштабирования. Их статья - хороший пример, как микросервисымогут работать, когда у ваша организация достаточно зрелая и есть ресурсы на поддержание инфраструктуры.
Еще в одном проекте в нише недвижимости от прошлой команды нам остался код на Python, реализующий аналитику на основе данных в MS-SQL, и мы решили, что было бы слишком затратно реализовывать вокруг него приложение на Django. Код имел обособленные зависимости и был неплохо изолирован, таким образом мы оставили его в качестве отдельного приложения и только иногда меняли его, когда что-то работало не так, как ожидается. Это неплохо работало для нашей небольшой команды, потому что сервис аналитики редко требовал модификации и поддержки.
Вывод: Используйте микросервисы, когда рабочие процессы изолированы, а не только потому что они кажутся “чистыми”.
Практические советы для стартапов
Если вы работаете над первой версией приложения:
- Начните с монолита. Выберите распространненый фреймворк и сфокусируйтесь на разработке функциональности. Большинство популярных фреймворков позволяет реализовать API или вебсайт, и начать обслуживать посетителей. Не поддавайтесь хайпу и придерживайтесь прагматичного подхода.
- Единственный репозиторий. Не делите код на репозитории раньше времени. Я знаком с основателями, которые хотели поделить код на репозитории, чтобы снизить риск доступа подрядчиков к интеллектуальной собственности. Это действительно серьезная проблема, но такое решение на практике дает больше проблем, чем безопасности делает приложение хрупким, фрагментирует CI/CD, и замедляет взаимодействие между командами. Незначительная защита интеллектуальной собственности не стоитла таких затрат, особенно когда можно обеспечить контроль внутри одного репозитория. Для команда на ранних стадиях развития продукта ясность и скорость имеют большее значение, чем теоретический выигрыш в безопасности.
- Принципиально простое развертывание приложения. Заставьте сборку работать. Если это требует более чем одно действие, напишите конкретную инструкцию, снимите видео, добавьте скриншотов. Если ваш код будет выполнять интерн или стажер, они столкнутся с препятсвием и вам все равно придется тратить время и объяснять им, как устранить проблему. Документирование каждой возможной проблемы для каждой операционной системы сильно экономит время на развертывание локальной среды.
- Настройте CI/CD на ранних этапах. Даже простой HTML, который вы вручную загружаете на сервер с помощью scp, можно автоматизировать и подключить CI/CD для загрузки его из репозитария. Когда развертывание автоматизировано, вы можете забыть о неи и сфокусироваться на разработке. Я часто наблюдал, когда многие команды, работая с аутсорсингом, экономили на CI/CD, это приводило к тому, что команда была деморализована и раздражена ручными процессами развертывания.
- Разделяйте точечно. Выделяйте сервисы только тогда, когда это действительно решает проблему. В противном случае инвестируйте в модульность и тесты внутри монолита - это быстрее и проще в обслуживании.
Самое главное: Оптимизируйте скорость разработки.
Скорость - это кислород вашего стартапа. Преждевременное разделение на микросервисы медленно высасывает кислород — до тех пор пока однажды вы не сможете дышать.
Вывод: Начните с простого, оставайтесь прагматичными, и разделяйте на микросервисы только того, когда это необходимо.
Если вы выберете микросервисную архитектуру
У меня были проекты, гдя микросервисы были внедрены слишком рано, и ниже рекомендации, что делать в таком случае:
- Оцените технический стек, который поддерживает вашу инфраструктуру на основе микросервисов. Инвестируйте в инструменты для разработки. Когда ваша система разделена на сервисы, необходимо думать об автоматизации стека, автоматизации конфигурации настроек продакшена и локальной разработки. В некоторых проектах мне пришлось создать отдельные CLI-скрипты, которые выполняли административные задачи в монорепозитории. Один проект содержал 15-20 развертываний микросервисов, и для локальной среды мне пришлось создать скрипт, генерирующий docker-compose.yml файла динамически, чтобы добиться развертывания на машине разработчика всего одной командой.
- Обеспечьте надежные протоколы связи для коммуникаций между сервисами. Если это асинхронный обмен сообщениями, убедитесь, что структура сообщений согласована и стандартизирована. Если это REST, обеспечьте документацию на базе OpenAPI. Клиенты межсервисной коммуникации должны обеспечивать множество вещей, которые не идут "из коробки": повторные запросы с экпонинциальной задержкой, таймауты. Типичный клиент gRPC с минимальным набором функций требует контроля проблем, связанных с сетевыми задержками.
- Убедитесь, что настройка юнит-тестов, интеграционных тестов, и сквозного тестирования - стабильна и масштабируется в зависимости от количества уровней разделения, которые вы внедрили в свою кодовую базу.
- На небольших проектах вы скорее всего будете использовать общую библиотеку для поддержки мониторинга и взаимодействия между сервисами. Важно сохранять компактность общей библиотеки. Любое изменение библиотеки приведет в пересборке всех зависимых сервисов, даже если они не связаны между собой.
- Внедрите мониторинг как можно раньше. Добавьте логи в виде структурированного JSON, связанные между собой через общие идентификаторы для отладки. Даже простые механизм журналирования, сохраняющие подробную информацию (до тех пор пока вы еще не внедрили надлежащие инструменты логронивая и трассировки) часто экономят время при разборе проблемных ситуаций.
Подводя итог: если вы все же решили использовать микросервисы, вам следуюет заранее понимать, какие затраты придется заплатить в виде дополнительного времени на разработку и обслуживание, чтобы сделать настройку работоспособной для каждого инженера в команде.
Вывод: Если осознанно усложняете систему, сделайте сложность управляемой.