Антипаттерн: архитектура Netflix для проекта на 200 пользователей
Команда запускает продукт для двухсот пользователей — внутренний сервис, нишевый SaaS, MVP под пилот с одним клиентом. А архитектура на старте выглядит так, будто готовится выдержать миллионы запросов в секунду: десяток микросервисов, очередь сообщений между ними, у каждого сервиса своя специализированная база. На словах это звучит как «делаем по-взрослому, сразу правильно». На практике — это архитектура, скопированная с разборов инфраструктуры крупных технологических компаний, наложенная на нагрузку, которую один процесс на одном сервере обслуживает не напрягаясь. Разберём, почему так происходит, во что это выливается операционно и как понять, какая сложность архитектуре действительно нужна.
Содержание
Как выглядит этот антипаттерн на практике
Типичный набор для проекта, который решил расти «по образцу большого технологического бизнеса» ещё до появления первого реального пользователя: приложение разбито на полдюжины-десяток отдельных сервисов (авторизация, профили, уведомления, каталог, биллинг, аналитика — каждый в своём репозитории и со своим циклом деплоя), между ними стоит брокер сообщений вроде Kafka или RabbitMQ, у каждого сервиса — своя, «подходящая под задачу» база: реляционная для одного, документная для другого, поисковый движок для третьего, отдельное хранилище для кеша. Сверху — оркестрация в Kubernetes, API-шлюз, service mesh, распределённый трейсинг, отдельный стек логов и метрик для каждого компонента.
Условно это выглядит как docker-compose.yml, разросшийся до состояния, когда его страшно открывать:
services:
api-gateway:
build: ./gateway
auth-service:
build: ./auth
auth-db:
image: postgres:16
user-service:
build: ./users
user-db:
image: mongo:7
notification-service:
build: ./notifications
search-service:
build: ./search
search-db:
image: elasticsearch:8.15
cache:
image: redis:7
broker:
image: bitnami/kafka:3
zookeeper:
image: bitnami/zookeeper:3
tracing:
image: jaegertracing/all-in-one
Тринадцать сервисов, три разных СУБД, брокер сообщений со своим координатором и отдельный стек трейсинга — и всё это под нагрузку, которую двести активных пользователей создают одним-двумя запросами в секунду в пиковые минуты. Для сравнения: такую нагрузку без всякого напряжения держит один процесс на среднем VPS с одной базой данных и кешем в памяти того же процесса.
Откуда берётся этот антипаттерн
Причина почти никогда не в реальной технической необходимости — она в том, откуда команда берёт образец для подражания:
- Cargo cult из докладов и статей о больших системах. Доклад или технический блог рассказывает, как крупная компания решает свои проблемы масштаба — и решение переносится один в один, без сверки с тем, есть ли у собственного проекта хоть отдалённо похожая нагрузка.
- Резюме-driven development. Настроить очередь сообщений, поднять кластер оркестрации и развести данные по специализированным хранилищам — заметно интереснее в портфолио, чем поддерживать один аккуратный монолит с одной базой.
- Страх «не выдержим роста», не подкреплённый расчётом. Опасение обосновано, но решение принимается без единой цифры: какая нагрузка есть сейчас, с каким запасом её тянет простая архитектура, при каком росте вообще возникнет необходимость что-то менять.
- Путаница между «современно» и «нужно именно здесь». Микросервисы, очереди и полиглотное хранение данных — рабочие, проверенные подходы под конкретный класс задач (независимые команды, которым нужно деплоить порознь; объёмы данных, которые не помещаются в одну СУБД), а не универсальный стандарт «хорошей» архитектуры по умолчанию — тот же механизм подмены лежит в основе мифа о том, что микросервисы правильны, а монолит устарел и в мифе, что Kubernetes нужен любому проекту.
- Продавливание вендорами. Managed-очереди, managed-кластеры оркестрации и специализированные СУБД продаются как «стандарт индустрии», хотя это опции для конкретного масштаба задач, а не стартовая точка для любого проекта.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверРезко возросшая операционная сложность без реальной необходимости
Каждый дополнительный компонент архитектуры — это не абстрактная «сложность», а вполне конкретный список обязанностей, который ложится на команду, обслуживающую двести пользователей:
- Больше точек отказа. Один процесс с одной базой может отказать одним способом — упасть или перестать отвечать. Десяток сервисов с брокером и тремя СУБД отказывают десятками разных способов: упал один сервис — посыпались зависимые от него вызовы; отстала очередь — накопился бэклог необработанных сообщений; недоступна одна из специализированных баз — сломалась только часть функциональности, и найти какая именно, стало отдельной задачей.
- Сетевые вызовы вместо вызовов функций. Внутри монолита обращение к другому модулю — вызов функции в памяти, который либо отработал, либо упал с понятной трассировкой. Между сервисами то же самое обращение — сетевой запрос со своими таймаутами и повторными попытками. Нужно закладывать retry-логику, circuit breaker, разбираться, что делать при частичной недоступности — и всё ради вызова, который в монолите занял бы одну строку кода.
- Согласованность данных между базами. Пока данные лежат в одной СУБД, транзакция гарантирует согласованность из коробки. Как только данные одной бизнес-операции размазаны по нескольким хранилищам, простая транзакция превращается в задачу распределённой согласованности — паттерн saga, компенсирующие операции, обработка ситуации «одна часть данных записалась, а другая нет».
- Деплой и версионирование. Вместо выкладки одного приложения — координация версий десятка сервисов и миграции схем в нескольких базах одновременно.
- Мониторинг и логи размазаны по компонентам. Отладка — это уже не просмотр логов одного процесса, а сопоставление логов брокера, нескольких сервисов и нескольких СУБД по времени и по идентификатору запроса, который нужно протащить через всю цепочку вызовов.
- Инфраструктура для управления инфраструктурой. Оркестрация десятка сервисов сама требует конфигурации, мониторинга и обновлений — команда обслуживает не только продукт, но и платформу, на которой он работает.
Ни один из этих пунктов не обусловлен нагрузкой в двести пользователей — все они появляются просто потому, что количество компонентов выросло, а не потому, что какой-то из компонентов реально понадобился под трафик или объём данных.
Команда тратит время на инфраструктуру, а не на продукт
У небольшой команды — часто это два-три разработчика на проект такого масштаба — время конечно и делится между двумя вещами: тем, что двигает продукт вперёд, и тем, что нужно, чтобы система вообще продолжала работать. Каждый лишний компонент архитектуры смещает этот баланс во вторую сторону.
Разработчику приходится разбираться не в бизнес-логике, а в том, почему сообщение застряло в очереди и не дошло до потребителя; почему одна из специализированных баз не приняла бэкап по расписанию; почему манифест в оркестраторе перестал применяться после обновления версии платформы. Ни одна из этих задач не добавляет пользователю ни одной новой возможности продукта — это исключительно плата за поддержание архитектуры, которая сама себя усложнила.
На горизонте нескольких месяцев это заметно по тому, куда уходит бэклог: вместо задач вида «добавить фильтр в каталог» или «ускорить форму регистрации» там накапливаются задачи вида «обновить версию брокера сообщений», «разобраться, почему упала одна из трёх баз», «поправить манифест деплоя после того, как в кластере что-то сломалось». Для продукта с двумя сотнями пользователей, где важнее всего скорость итераций и обратная связь от первых клиентов, это прямой урон: за то же время разработчика конкурирует не другая фича, а инфраструктурная сложность, которую команда создала себе сама.
Чужой масштаб решает чужие проблемы, а не ваши
Ключевая ошибка антипаттерна — не в том, что микросервисы, очереди или полиглотное хранение данных плохи сами по себе. Они хорошо решают конкретные проблемы определённого масштаба: у крупной компании счёт пользователей идёт на миллионы, над продуктом одновременно работают десятки команд, которым нужно выкладывать свои части системы независимо не толкаясь, а объём данных физически не помещается ни в одну СУБД на одном сервере.
У проекта на двести пользователей ни одной из этих проблем нет:
- Нагрузка не подходит даже близко к пределу одного процесса на одном сервере.
- Над кодом работает одна небольшая команда, которой не нужно независимо деплоить части системы, потому что деплоят её люди, сидящие в одном чате.
- Объём данных — это база, которая свободно помещается в оперативную память современного сервера, а не проблема, которую нужно решать распределённым хранением.
Архитектурное решение, которое решает проблему независимого масштабирования команд из пятидесяти инженеров, ничего не даёт команде из трёх человек — она и так координируется словами в чате быстрее, чем через любую систему очередей. Решение, которое разносит данные по специализированным хранилищам ради обработки петабайтов, не ускоряет работу с базой, которая весит сотни мегабайт. Скопированная архитектура переносит не пользу, а издержки: тот же принцип соразмерности разбирался применительно к отказоустойчивости в антипаттерне про кластер из трёх серверов под сайт-визитку — там резервирование строили не под реальный риск простоя, а «на всякий случай»; здесь та же логика применена не к отказоустойчивости, а ко всей архитектуре целиком.
Принцип соразмерности: расти по факту, а не заранее
Здоровый подход — architecture as needed, а не architecture as imagined: начинать с минимально достаточной сложности и усложнять только тогда, когда появляется измеримая причина, а не заранее «про запас».
Для проекта на двести пользователей это чаще всего означает:
- Один монолитный процесс с чистым внутренним разделением на модули — авторизацию, профили, каталог и уведомления можно выделить в отдельные пакеты или слои внутри одного приложения, не превращая их в отдельные сетевые сервисы. Границы модулей от этого не хуже, а рефакторинг в отдельный сервис при реальной необходимости остаётся возможным в будущем.
- Одна СУБД, использованная по максимуму. Современный PostgreSQL закрывает подавляющее большинство задач небольшого проекта без отдельных специализированных хранилищ:
jsonb— для полуструктурированных данных вместо документной базы, встроенный полнотекстовый поиск или расширение вродеpg_trgm— вместо отдельного поискового движка на первое время, простая таблица задач с обработкой черезSELECT ... FOR UPDATE SKIP LOCKED— вместо полноценного брокера сообщений там, где нужна лишь фоновая очередь заданий, а не потоковая обработка событий в реальном времени. - Кеш в памяти процесса или один Redis — вместо отдельного кластера кеширования с собственной репликацией.
- Вертикальный рост первым, горизонтальный — когда он реально понадобится. Прежде чем делить систему на части, стоит выяснить, сколько нагрузки тянет один сервер с увеличенным числом ядер и памяти — для двухсот пользователей этот потолок обычно даже не виден.
- Простой деплой одного артефакта — вместо оркестрации кластера сервисов:
systemd-юнит или один контейнер, который перезапускается по обновлению образа, справляется с задачей выкатки без отдельной платформы оркестрации.
Ориентир по тому, когда конкретный компонент действительно оправдан:
| Компонент | Когда есть смысл добавлять |
|---|---|
| Отдельный микросервис | Есть отдельная команда, которой физически нужно деплоить независимо, или часть системы требует принципиально другого масштабирования, чем остальная |
| Специализированная СУБД (документная, поисковая, колоночная) | Задача, для которой она заточена, реально не решается штатными средствами основной СУБД на измеримом объёме данных |
| Брокер сообщений | Нужна гарантированная доставка событий между независимыми потребителями с разной скоростью обработки, а не просто фоновая обработка задач одним воркером |
| Оркестрация кластера контейнеров | Число инстансов и сервисов уже физически не управляется вручную или простым скриптом деплоя |
| Distributed tracing и отдельный стек логов на компонент | Запрос реально проходит через несколько независимых сервисов, и без трассировки его путь не восстановить |
Решение об усложнении принимается по измеримому сигналу — CPU или память упираются в потолок под нормальной нагрузкой, конкретная задача не решается штатными средствами текущей СУБД, команда выросла настолько, что общий деплой стал узким местом для нескольких групп разработчиков одновременно. Если такого сигнала нет — компонент не нужен, сколько бы статей о нём ни было написано. Это тот же вопрос соразмерности, что и при выборе конфигурации сервера на старте — тема разобрана в материале про то, сколько ресурсов реально нужно стартапу на старте.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Разве не дешевле сразу заложить сложную архитектуру, чтобы потом не переделывать при росте?
Обычно наоборот. Переделка простой, хорошо структурированной монолитной системы в распределённую при реальном росте — понятная, локализованная задача с ясным критерием «когда пора». Поддержка избыточно сложной архитектуры с первого дня — это постоянные издержки прямо сейчас, ради роста, который может не наступить в предполагаемом виде или вообще не наступить.
А если инвесторы или заказчик ожидают увидеть «современный технологический стек»?
Современность архитектуры измеряется не количеством сервисов и очередей, а тем, что продукт работает стабильно, быстро развивается и не проседает по деньгам на инфраструктуру, которая не нужна. Хорошо структурированный монолит с чистыми внутренними границами — это тоже осознанное архитектурное решение, а не «недоделанная» система.
Можно ли начать с малого, но заранее оставить возможность вырасти в микросервисы?
Да, и это правильная стратегия — если код внутри монолита организован модулями с чёткими границами и явными интерфейсами между ними, выделение любого модуля в отдельный сервис при реальной необходимости становится техническим рефакторингом, а не переписыванием системы с нуля.
Как понять, что проекту на двести пользователей уже пора что-то усложнять?
По измеримым сигналам, а не по ощущениям: сервер стабильно упирается в CPU или память под обычной нагрузкой, конкретная функция объективно не тянется штатными средствами текущей базы данных, или команда выросла настолько, что общий деплой одного артефакта стал мешать нескольким группам разработчиков работать параллельно. Без такого сигнала — вопрос преждевременный.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →