Адский эксперимент: личный сайт на нищих микросервисах — Habr

Элементы, расположенные близко друг к другу, воспринимаются как связанные, а удалённые — как отдельные группы Закон Фиттса Чем ближе и крупнее элемент, тем быстрее пользователь сможет с ним взаимодействовать Закон Хика Чем больше элементов меню, кнопок или опций вы показываете пользователю, тем дольше он будет думать и тем выше вероятность ошибки Законы и правила Бена Шнейдермана 1) Стремитесь к единообразию; 2) Удовлетворяйте потребности как опытных, так и начинающих пользователей; 3) Предлагайте информативную обратную связь; 4) Дизайн диалогов должен сообщать о завершении действий; 5) Обеспечивайте простую обработку ошибок; 6) Разрешайте легко отменять действия; 7) Давайте пользователям чувство контроля; 8) Сокращайте кратковременную память Когда мы начинаем непосредственно создавать дизайн клиентской части в Figma мы должны пользоваться каждым принципом, правилом и законом из этих таблиц. 2.5 Проектирование БД Базы данных бывают реляционные SQL и нереляционные NoSQL. У нерялиционных есть также еще свои виды для решения определенных задач: документоориетированные, ключ-значение, колоночные, графовые, векторные (для ИИ и нейросетей) и другие. Какая база данных подойдет для хранения данных контента? Что такое контент? Это коллекция документов. Из определения уже понятно, что хорошей идеей будет использование документоориетированной базы данных MongoDB. А для микросервиса аутентификации? Данные пользователя (хэш пароля, email, токены обновления) отлично ложатся на документную модель. Каждый пользователь — один документ в коллекции. Получается тоже MongoDB. Ну а как для уведомлений? У разных типов уведомлений (email, push, in-app) могут быть разные данные. В реляционной БД пришлось бы делать несколько таблиц или столбец с JSON. В MongoDB это решается естественно. Ну и комментарии. Mon..! Нет! Комментарии часто образуют древовидные структуры (ответы на ответы). Операции «добавить комментарий -> увеличить счетчик комментариев в посте» должны быть атомарными. MongoDB поддерживает многодокументные транзакции, но они дорогие с точки зрения производительности по сравнению с транзакциями в реляционных БД. В реляционной БД это стандартная и быстрая операция. Для комментариев выбираем PostgreSQL. Как будет выглядеть модель такой базы данных?А вот так: В реляционных базах данных мы привыкли к трем видам связей: • one-to-one (один-ко-одному) • one-to-many (один-ко-многим) • many-to-many (многие-ко-многим) • [здесь кое-чего не хватает] А это что за вид связи? Это 4-ый вид связи, про который многие забывают. Он называется Self-referencing. Это когда таблица ссылается сама на себя. Такой вид связи идеален для создания иерархических структур. Система комментариев и есть иерархическая структура. Вам, наверно, интересно, а как это выглядит в базе данных. Что же, я приведу реальную выгрузку данных из базы с вложенными комментариями: { "id": 112, "project_id": "68dc3c354a3d786189d88077", "author_id": "68d5a8b16b11bde9670809f2", "author_email": "admin@admin.com", "comment_text": "Вложенный комментарий уровня 2 (первый)", "created_at": "2025-10-01T15:50:13.704689", "parent_comment_id": 111, "likes": 0, "dislikes": 0 }, { "id": 111, "project_id": "68dc3c354a3d786189d88077", "author_id": "68d5a8b16b11bde9670809f2", "author_email": "admin@admin.com", "comment_text": "Вложенный комментарий уровня 1 (второй)", "created_at": "2025-10-01T15:49:57.661543", "parent_comment_id": 109, "likes": 0, "dislikes": 0 }, { "id": 110, "project_id": "68dc3c354a3d786189d88077", "author_id": "68d5a8b16b11bde9670809f2", "author_email": "admin@admin.com", "comment_text": "Вложенный комментарий уровня 1 (первый)", "created_at": "2025-10-01T15:49:48.908385", "parent_comment_id": 109, "likes": 0, "dislikes": 0 }, { "id": 109, "project_id": "68dc3c354a3d786189d88077", "author_id": "68d5a8b16b11bde9670809f2", "author_email": "admin@admin.com", "comment_text": "Отличный проект!", "created_at": "2025-10-01T15:48:51.064197", "parent_comment_id": null, "likes": 0, "dislikes": 0 } ] Страшно? Да как бы ни так. Современный JavaScript без каких либо проблем обработает такую выгрузку и расположит все как положено. В подтверждении своих слов мы с вами чуть-чуть забежим вперед. В рамках проекта мы создадим кастомную админку на Quasar (это фреймворк на основе Vue 3), в которой будет возможность править комментарии. Так вот, как эта самая выгрузка будет выглядеть в нашей админке: Красиво? А ведь это те самые запутанные данные из PostgreSQL. 2.6 Проектирование API Я сразу отойду от классического определения API. По сути дела, API – это контракт, определяющий, как две системы должны взаимодействовать друг с другом. API могут быть: • stateful – с сохранением состояния • stateless – без сохранения состояния Stateful API сохраняет состояние клиента на сервере между запросами, требуя поддержания сессии. Stateless API обрабатывает каждый запрос полностью независимо, без хранения состояния клиента на сервере. Существует несколько подходов API: • RPC-протоколы (SOAP, gRPC) для удаленного вызова процедур • REST-архитектура для работы с ресурсами через HTTP • GraphQL — язык запросов для гибкого получения данных SOAP используется в корпоративном сегменте – там, где важна безопасность и всё построено на транзакциях. REST доминирует при создании публичных (открытых) API. gRPC популярен для внутренней коммуникации сервисов. GraphQL решает проблемы получения данных по множественным параметрам запроса (например, надо указать цвет, год, габариты, вес и другие параметры) в сложных клиентских приложениях. В нашем случае мы будем использовать REST для коммуникации между фронтендом и API шлюзом, а также между Admin Service и Admin GUI. REST хорош тем, что он очень прост и понятен. В качестве спецификации он использует OpenAPI/Swagger. Внутри бэкенда для коммуникации API шлюза с другими микросервисами будет использоваться gRPC. 3. Пока все хорошо: разработка клиентской части приложения 3.1 Создание ассетов с помощью ИИ В настоящее время у любого появилась уникальная возможность создавать графические элементы интерфейса с помощью искусственного интеллекта (ИИ) абсолютно бесплатно. Например, я хочу, чтобы у меня было анимированное небо на заднем фоне, а на переднем средневековый монастырь. И то, и то другое можно без проблем генерировать с помощью ИИ. Возьмем, например, нейросеть GigaChat – по текстовому запросу (промту) можно получить готовый и качественный ассет: Тоже самое и с облаками — ввести нужный промпт и получить ассет. Также нейросети отлично решают вопрос с генерацией favicon – это иконки на вкладке вашего сайта, а также на всех возможных устройствах. 3.2 Создание UX/UI сайта с помощью Figma Как по мне, онлайн-программа Figma представляет один из лучших функционалов для создания UX/UI. Если вы не знакомы с этим приложением — изучить его можно за 1 день. Вооружившись принципами и законами UX/UI создадим дизайн сайта: Как видно мы поместили сгенерированные ИИ ассеты на общий макет. Когда дизайн готов можно приступать к вёрстке на HTML, CSS. 3.3 Разработка основного фронтенда Frontend с помощью Vue.js Современный фронтенд можно классифицировать по принципу рендеринга и архитектуре на две фундаментальные модели: Серверный рендеринг (Server-Side Rendering, SSR): В этой модели пользовательский интерфейс генерируется на стороне сервера. Бэкенд выполняет код, извлекает данные из базы, вставляет их в HTML-шаблон и отправляет клиенту полностью готовую к отображению HTML-страницу. Классическими примерами фреймворков, использующих этот подход, являются Django и Flask. Клиентский рендеринг (Client-Side Rendering, CSR): В этой парадигме сервер отправляет клиенту минимальный HTML-каркас и JavaScript-бандл (как правило, это SPA — Single Page Application). Браузер загружает и исполняет JavaScript, который затем самостоятельно управляет DOM, динамически запрашивает данные у бэкенда (обычно через JSON API) и рендерит интерфейс непосредственно на стороне клиента. Такое приложение является независимым от бэкенда. Яркие примеры — приложения, созданные на React, Angular или Vue.js. Также существует комплексное решение, где сочетаются SSR и CSR – это фреймворк Nuxt.js. В проекте использован CSR, мы создадим SPA на Vue.js. Почему Vue.js? Концепция, которая лежит в основе данного фреймворка, мне лично очень нравится. Приложение на Vue.js собирают из компонентов, в каждом из которых инкапсулированы HTML-шаблон (template), логика (script) и стиль (style). Каждый компонент помещают во vue-файл. Теперь надо обратиться к нашему дизайну на Figma и произвести разбивку нашего SPA на компоненты со связями между компонентами. В моем случае декомпозиция выглядит так: Мне кажется, что даже и не надо описывать каждый компонент — по его названию сразу понятно, что это такое и какую функцию он выполняет. Для каждого представления (View) я решил выбрать абсолютно разные способы реализации: AboutMeView – карусель ProjectsView – карточки со всплывающими модальными окнами с динамическим контентом CertificatesView – галерея PublicationView – таблица ContactsView – анимированные 3D-ссылки Здесь я использовал как внешние библиотеки для Vue.js, так и встроенные функции, а местами чистый JavaScript. В итоге, если все собрать получилось вот это: Напоминаю, что это SPA. Никакой перезагрузки страницы не происходит, но тем не менее в URL-строке браузера появляются адреса вида /about. Дело в том, что Vue Router автоматически переключает компоненты, бесшовно, без перезагрузки страницы, что резко улучшает UX. Также у нас динамические переключается язык и темы (с темной на светлую и наоборот). Еще один важный нюанс — это то, как наш SPA будет ходить на бэкенд. В языке JavaScript есть встроенные решения: XMLHttpRequest — он считается устаревшим, но до сих пор используется fetch — современный подход, основанный на Promise’ах Но мы не будем использовать ни то, не другое. Мы будем использовать стороннюю библиотеку axios, так как axios предоставляет более удобный API: автоматическая трансформация JSON-ответов, перехватчики для глобальной обработки запросов, отмена запросов и встроенная защита от XSRF-атак. SPA необходимо оптимизировать. Это достигается помощью ленивой загрузке маршрутов, асинхронных компонентов, сборщика пакетов Vite. Полный код фронтенда Frontend доступен здесь . 3.4 Разработка Admin GUI с помощью Quasar Я скажу так: если вы знаете Vue, то вы автоматически знаете Quasar. Quasar – это тот же самый Vue с уже готовыми компонентами из коробки. Ваша задача просто собрать эти компоненты воедино. Quasar – это не про уникальность и авторский дизайн, это про функциональность. Нам не нужна красивая графика в Admin GUI. Мы её соберем из компонентов. В итоге получается это: Долго ли собрать такую панель? С Copilot её можно собрать за пару вечеров. Полный код Admin GUI доступен здесь . 4. Погружение в ад: разработка серверной части приложения 4.1 Обзор основных концепций Перед тем, как мы приступим к реализации микросервисов, мы с вами должны понять основные концепции, которые будут реализованы в этих микросервисах. Я постараюсь вас не душить, буду предельно краток и лаконичен. Важный момент: мы будем говорить как это всё выглядит на языке Python. Следующие темы относятся только к языку Python. В других языках это реализуется иначе. 4.1.1 Асинхронное программирование Если есть асинхронное программирование, значит есть синхронное. Что это означает? Синхронное означает последовательное, то есть каждая последующая строчка кода ждёт пока выполнится предыдущая. А что если одна строчка кода выполняется очень долго? Это называется блокировкой. А что именно блокируется? Когда вы запускаете Python-код создаётся Python-процесс. Этому процессу выделяется свой объем оперативной памяти и процессорного времени. Внутри этого процесса находится много всего: PID, файловые дескрипторы и в том числе основной поток. Именно этот поток и блокируется. Блокировку необходимо увидеть каждому программисту и прочувствовать её. Давайте напишем синхронный код и сделаем блокировку. Все библиотеки Python можно разделить на блокирующие и неблокируюшие. Стандартная библиотека time является блокируюшей и там есть блокирующая функция sleep from time import sleep, time def start_timer(seconds: float) -> None: sleep(seconds) return def main() -> None: start = time() sleep(3) # Блокирует основной поток на 3 с. sleep(4) # Блокирует основной поток на 4 с. sleep(5) # Блокирует основной поток на 5 с. end = time() print(f"Код выполнился за время {end — start:.2f} с.") if __name__ == "__main__": main() Код выполнился за 12 секунд.А теперь давайте сделаем тоже самое, только с применением асинхронного программирования. import asyncio from time import time async def start_timer(seconds: float) -> None: await asyncio.sleep(seconds) return async def main() -> None: start = time() tasks = [ asyncio.create_task(start_timer(3)), asyncio.create_task(start_timer(4)), asyncio.create_task(start_timer(5)), ] await asyncio.gather(*tasks) end = time() print(f"Код выполнился за время {end — start:.2f} с.") if __name__ == "__main__": asyncio.run(main()) Код выполнился за 5 секунд. Но что здесь происходит? Здесь мы запускаем асинхронный цикл. Туда мы помещает корутину main. Внутри main мы создаем задачи — это такие обёртки вокруг корутины start_timer. Задача нужна для того, чтобы корутины выполнялись конкурентно. То есть каждая корутина стремится выполниться как можно скорее. У нас в основном потоке есть только асинхронный цикл и всё. Есть ли блокировка. Да, есть! Блокируется основной поток этим самым асинхронным циклом, но он эффективно используется за счет переключения между корутинами при операциях ввода-вывода. Тоже самое будет происходить в наших микросервисах. Самое главное условие — использование неблокирующих библиотек и клиентов: • неблокирующий клиент FastAPI для HTTP-запросов • небокирующие сессии SQLAlchemy 2 • неблокирущий клиента Pymongo • неблокирующий клиент Redis • неблокирующший клиент aiokafka Всё это помещается в асинхронный цикл событий asyncio и выполняется конкурентно. Асинхронное программирование — это огромная тема. Вам надо хорошо знать, что такое CPU bound – операции и IO Bound операции. Существует прекрасная книга на эту тему, который должен изучить как учебник любой Python-программист — М. Фаулер: "Python Concurrency with asyncio" 4.1.2 Использование Redis для кэширования и ограничения запросов На уровне железа есть разные компоненты компьютера для хранения данных. Это могут быть SSD, оперативная память (RAM), flash-карта. Нам надо как можно быстрее получить данные. Что мы выберем? Оперативная память работает быстрее всего (если не брать L1/L2/L3 кэши процессора). У неё очень высокая скорость записи и чтения данных. Но она довольно дорогая и объем её небольшой. А зачем нам нужна эта скорость? Это улучшает опыт пользователя — чем он быстрее получит данные, тем лучше. Мы поняли, что нам нужна оперативная память, а как именно хранить в ней данные? Существует решение – in-memory NoSQL база данных типа «ключ-значение» Redis. Зачем нам лезть каждый раз в основную базу данных MongoDB или PostgreSQL, когда можно извлечь один раз данные и поместить их в Redis в оперативной памяти!? Эта операция называется кэширование — от английского слова «cache», которое переводится как «тайник». Именно в тайник мы помещаем данные и оттуда извлекаем. Всё бы хорошо, но есть одно «но». Что если данные часто меняются? Каждые 20 секунд. Надо ли нам класть данные в кэш? Нет, это замедлит работу сервиса. Поэтому кэширование применяем только для данных, которые редко меняются. Для данных, которые часто меняются либо кэш не используется, либо можно использовать короткое время жизни кэша (TTL — анг. Time to live). Правда, есть одно важное замечание — «инвалидация кэша». Смысл этой концепции в том, что когда у нас происходит какая-то CUD-операция (Create, Update, Delete) – мы можем сбросить кэш. Обнулить его. Это будет использовано в нашем проекте Еще Redis используется для хранения количества запросов для каждого IP-адреса с целью их ограничения. Зачем это нужно? Это элемент информационной безопасности — чтобы защититься от DoS-атак (отказ от обслуживания). За тем, что происходит в Redis, можно наблюдать. Существует инструмент Redis Insight. Он запускается отдельным контейнером и мы можем полностью манипулировать данными в Redis. Это нам пригодится в будущем, чтобы убедиться, что данные загружаются действительно из Redis, а не из базы данных. 4.1.3 Асинхронная коммуникация Apache Kafka Мы уже с вами выяснили, что между микросервисами должна быть минимальная связанность. В идеале, один микросервис ничего не должен знать о другом микросервисе. Именно для этого применяют события. Apache Kafka обладает невероятной производительностью. Здесь есть всё, что нужно для комфортной работы — гарантированная доставка сообщений, механизм предотвращение дублирования сообщений, горизонтальное масштабирование партиций в топиках и еще огромное число полезных функций. Apache Kafka – это огромная тема. В 2025 году вышла потрясающая книга, после прочтения которой у вас не останется ни одного вопроса по этой технологии — Anatoly Zelenin, Alexander Kropp: “Apache Kafka in Action: From basics to production” 4.1.4 Elastic Stack У нас каждый микросерсис имеет собственные логи. В продакшене будет очень сложно лезть в каждый контейнер и смотреть, что там происходит. Надо как-то агрегировать все логи, сохранять их, анализировать, визуализировать. Именно эту функцию выполняет Elastic Stack. Раньше эта технология называлась ELK Stack. ELK – это сокращение трех технологий: • E -Elasticsearch – нереляционная база данных для полнотекстового поиска • L – Logstash – инструмент для извлечения данных, обработки их и фильтрации с последующей передачей в Elasticsearch. • K – Kibana – это графический интерфейс для Elasticsearch, у которого огромное количество возможностей: визуализация, построение дашбордов, диаграмм, графиков. В последнюю версию Kibana завези ИИ, который может находить аномалии и коллизии данных. Проблема была именно с Logstash. Инструмент классный, но иногда избыточный. Он прекрасно справляется со своей задачей и по сей день. И сегодня он очень распространён. Но иногда хочется чего-то полегче. Поэтому придумали легковесные решения — Beats. Их много видов. В нашем проекте будет использоваться Filebeat. Этот инструмент может извлекать логи отовсюду: из txt-файла с логами, из консоли, из Docker-контейнера и самое главное, что нам надо — из топика Apache Kafka. Filebeat требует конфигурации и отдельного Docker-файла. Вы можете посмотреть в моём проекте как правильно написать конфигурацию Filebeat . И опять я скажу, что Elastic Stack – это огромная тема. Что почитать на эту тему? Существует прекрасная книга 2025 года — Шривастава Анураг: “Elasticsearch для разработчиков: индексирование, анализ, поиск и агрегирование данных. 2-е изд.” Не утихают споры по коммерческому использованию Elasticsearch выше 7 версии. Действительно, где-то в районе 2021 года из-за пертурбаций с лицензией Elasticsearch многие продуктовые компании перешли к эксплуатации OpenSearch. В нашем случае проект некоммерческий — поэтому спокойно пользуемся самой последней версией Elasticsearch. 4.2 Разработка Admin Service Представьте: 50 микросервисов и 50 админок — это и есть тот самый Admin Hell. Чтобы не свести администраторов с ума, мы создали единую админ-панель как центр управления полетами (запомните эту фразу — она нам пригодится в будущем). Однако здесь мы пошли на эту архитектурную авантюру. Admin Service в нашем эксперименте — это антипаттерн во плоти. Нарушения DDD и микросервисных принципов: • Прямой CRUD к MongoDB и PostgreSQL других сервисов • Отбирает функции доменных сервисов (хеширование паролей, генерация токенов) • Нарушение границ контекстов (отправка писем, работа с файлами) • Создание распределенного монолита Что внутри этого Admin Service: • Прямые манипуляции с чужими БД • Бизнес-логика, принадлежащая Auth/Notification Service • Функции преобразования файлов, нарушающие isolation • Kafka-продюсер, дублирующий функционал специализированных сервисов Почему мы так сделали? Это выбор для эксперимента: показать, как НЕ надо строить микросервисы. В продакшене каждая из этих функций должна жить в своем доменном сервисе. Полный код Admin Service доступен здесь . 4.3 Разработка Content Service Давайте рассмотрим Content Service — идеальный пример того, как микросервис выполняет функцию «Сбегай мне за пивом». Этот gRPC-сервис технически безупречен: асинхронная работа с MongoDB, строгая типизация через Pydantic, полноценный health-check. Но архитектурно он «нищий» — лишен какой-либо бизнес-логики. Что делает этот сервис на самом деле? Отдает готовые данные проектов, технологий, сертификатов и т.п. Поддерживает сортировку и мультиязычность. И… всё. Никакой обработки, никаких решений, никакой доменной логики. Content Service стал просто прокси к базе данных. Вся бизнес-логика работы с контентом — валидация, преобразование, бизнес-правила — живет в других сервисах (в основном в том самом Admin Service). Наш микросервис не управляет своим доменом, а лишь обслуживает запросы к хранилищу. Полный код Content Service доступен здесь . 4.4 Разработка Auth Service На фоне «нищего» Content Service наш Auth Service выглядит образцом доменной полноты — но это лишь иллюзия. Формально здесь есть вся необходимая бизнес-логика: JWT-токены, bcrypt-хеширование, Kafka-интеграция для уведомлений. Однако в рамках нашего эксперимента этот сервис оказался лишённым важнейшего права — управлять своим доменом полностью. Что здесь не так: критическая функция бана пользователей вынесена в Admin Service, нарушен принцип единственной ответственности — бан должен быть частью домена аутентификации, Admin Service может напрямую манипулировать состоянием пользователей, обходя бизнес-логику Auth Service. Когда администратор банит пользователя через Admin Service, он нарушает инкапсуляцию домена аутентификации. Auth Service не участвует в этом процессе. Технически Auth Service идеален — асинхронные операции, безопасное хеширование, полноценная работа с токенами. Но архитектурно он урезан в правах: все церемонии соблюдены, но реальная власть находится в другом месте. Полный код Auth Service доступен здесь . 4.5 Разработка Notification Service Notification Service лишь притворяется самодостаточным. Да, он грамотно обрабатывает события из Kafka и отправляет письма, но его суверенитет нарушен тем же монстром — Admin Service. Admin Service имеет прямой доступ к БД notification_db. Самый архитектурно чистый сервис в нашем стеке оказывается таким же «нищим», как и Content Service. Разница лишь в том, что Content Service изначально был пустым, а Notification Service ограблен. Теперь наш эксперимент действительно «адский»: мы доказали, что достаточно одной архитектурной ошибки (прямой доступ к БД), чтобы обесценить все правильные решения в микросервисной системе. Полный код Notification Service доступен здесь . 4.6 Разработка API Шлюза (API Gateway) API Gateway в нашем эксперименте оказался тем редким случаем, где мы следовали лучшим практикам. Кэширование на уровне шлюза — это действительно оправданный паттерн, позволяющий разгрузить бэкенд-сервисы и мгновенно отдавать закэшированные данные. Когда фронтенд запрашивает контент, шлюз сначала проверяет Redis. И тот выдает оттуда кэш, если он там есть. Полный код API Gateway доступен здесь . 5. Ложное благополучие: тестирование и обеспечение качества Ирония нашего эксперимента в том, что все модульные и интеграционные тесты успешно проходят. Pytest с гордостью сообщает о 100% покрытии, MongoDB-моки корректно работают, Kafka-консьюмеры исправно обрабатывают сообщения. Технически сервисы безупречны — но не архитектурно. 5.1 Тестирование API с помощью Postman Давайте протестируем самого главного смутьяна Admin Service: Отлично, все тесты прошли! Но что-то не так. Успешное тестирование !== правильная архитектура (специально использую символ строгого неравенства из JavaScript). Мы можем иметь идеальные тесты с полным покрытием, но при этом создавать распределённый монолит, где сервисы бессмысленны, а данные текут через дыры в архитектуре. 5.2 Модульные и интеграционные тесты с помощью pytest Несмотря на все нарушения DDD и микросервисных принципов, тестовая культура в проекте осталась на высоте. Каждый сервис, будь то «нищий» Content Service или «ограбленный» Notification Service, покрыт полноценными тестами — и это создаёт иллюзию качества. Мы используем pytest с фикстурами для изолированного тестирования компонентов. Например, Auth Service тестируется с моками MongoDB, где каждый тестовый случай проверяет корректность хеширования паролей, генерации JWT-токенов и работы с refresh-токенами. Интеграционные тесты через TestClient FastAPI и асинхронные pytest-плагины гарантируют, что API-эндпоинты возвращают ожидаемые структуры данных. Даже прямой доступ Admin Service к чужим базам данных не мешает тестам проходить — ведь мы мокаем все сторонние зависимости, создавая изолированную тестовую среду. Интересно, что наши тесты доказывают: технически сервисы работают безупречно. Content Service корректно отдаёт данные из MongoDB, Notification Service отправляет письма через SMTP, а Auth Service валидирует токены. Проблема в том, что тесты проверяют корректность кода, а не архитектурную целостность. Мы можем иметь 100% покрытие и при этом архитектурный ад — тесты просто не умеют проверять соблюдение DDD-принципов и границ контекстов. Это важный урок: успешное тестирование не равно успешной архитектуре, и наш эксперимент наглядно это демонстрирует. 5.3 О контрактном и сквозном тестировании В нашем эксперименте мы сознательно отказались от реализации контрактного тестирования (contract testing) и сквозного тестирования (end-to-end testing). Контрактное тестирование позволило бы нам формализовать взаимодействия между микросервисами и гарантировать, что изменения в одном сервисе не сломают его потребителей. Но в мире, где Admin Service напрямую лезет в чужие базы данных, эти контракты теряют смысл — зачем проверять API, если половина взаимодействий происходит в обход них? Цена отсутствия E2E-тестов оказалась выше, чем мы предполагали. Без сквозных тестов, которые проходят через весь стек приложения — от интерфейса админки до сохранения данных в MongoDB — мы не смогли обнаружить системные проблемы, вызванные нашими архитектурными компромиссами. Все модульные и интеграционные тесты проходили, но в продакшене могли всплыть тонкие баги, связанные именно с нарушением границ контекстов и прямым доступом к данным. Эти тесты служат системой раннего предупреждения об архитектурных проблемах, которые модульные тесты просто не в состоянии отследить. В нашем случае их отсутствие позволило антипаттернам беспрепятственно укорениться в системе. 6. Ад продолжается: Внедрение и развертывание Если вы думали, что архитектурные компромиссы остаются на уровне кода, вы сильно ошибались. Наш «адский эксперимент» перешёл на новый уровень, когда мы начали развёртывание. Все те архитектурные грехи, которые казались безобидными в разработке, превратились в настоящий кошмар при деплое. 6.1 Docker Compose Docker Compose как зеркало архитектурных проблем… Наш docker-compose.yaml разросся до невероятных размеров, отражая все скрытые зависимости между сервисами. Admin Service требовал доступ к 4 разным базам данных, порядок запуска контейнеров напоминал запуск спутника на Марс. Помните мою фразу про «центр управления полетами». Тем не менее всё прекрасно работает. Docker Desktop не знает какая мина замедленного действия заложена в приложении (об этом в разделе 7): Посмотреть docker-compose.yaml можно здесь . 6.2 Kubernetes Наше путешествие по кругам микросервисного ада достигло логического финала — развертывания в Kubernetes. Казалось бы, что может быть лучше для микросервисов, чем их родная среда оркестрации? Но как показал эксперимент, Kubernetes лишь многократно усилил все наши архитектурные промахи. Наш docker-compose.yaml, и без того напоминавший спагетти, превратился в целую пачку манифестов, где каждый новый Deployment добавлял в систему новые точки отказа. Kubernetes не спасает от плохой архитектуры — он лишь делает ее недостатки более явными и дорогостоящими. Наш кластер стал живым воплощением принципа GIGO (Garbage in, garbage out). Но самое смешное, что всё исправно и работает: Посмотреть полную реализацию Kubernetes можно здесь . 7. Вечные страдания: эксплуатация и техническая поддержка Если вы думали, что боль заканчивается после деплоя, вы ошибались — она только начинается. Наш «адский эксперимент» перешёл в вечные страдания, когда мы начали эксплуатацию системы. OOMKiller стал нашим постоянным спутником. Казалось бы, мы выделили сервисам достаточное количество оперативной памяти, но оказалось, что архитектурные антипаттерны потребляют ресурсы с аппетитом голодного кота с помойки. Не верите? А вот вам нотариально заверенный скриншот: 8. Важнейшие выводы, которые я сделал Вывод №1: Микросервисы — это про границы контекстов, а не про технологии.Мы идеально настроили gRPC, Kafka и Kubernetes, но полностью провалили главное — разделение доменной ответственности. Получилась распределённая монолитная система, где сервисы формально независимы, но фактически сросшиеся через общие базы данных. Вывод №2: Прямой доступ к одной БД с разных сервисов — не надо так.Сначала даёт ощущение скорости разработки, а потом превращает систему в кошмар поддержки. OOMKiller был не причиной, а следствием — он лишь наказывал нас за архитектурные косяки. Вывод №3: «Бедные сервисы» убивают микросервисную архитектуру на корню.Сервис без доменной логики — это просто дорогой прокси к базе данных. Content Service, лишённый бизнес-правил, стал бесполезным звеном в цепочке, потребляющим ресурсы и добавляющим сложности. Вывод №4: Тесты не видят архитектурных проблем.Можно иметь 100% покрытие и при этом идеально протестированную архитектурную катастрофу. Тесты проверяют код, а не соблюдение DDD-принципов. Вывод №5: Kubernetes не спасает от плохой архитектуры — он её лишь дороже делает.Оркестратор многократно усиливает все архитектурные ошибки, превращая их из теоретических проблем в реальные финансовые потери на инфраструктуре. Вывод №6: Админка — это UI, а не раздутый огромный сервис.Admin Service не должен быть свалкой всей бизнес-логики системы. Вывод №7: Архитектурные компромиссы имеют свойство накапливаться.Одна маленькая уступка («пусть пока Admin Service лезет прямо в БД») тянет за собой шлейф проблем, которые проявляются только на этапе эксплуатации. Ну и в завершении статьи я вам покажу правильный системный дизайн: 9. Эпилог История коммерческой разработки полна примеров, где провалы архитектуры приводили к катастрофическим последствиям — наш «адский эксперимент» оказался удивительно точным воспроизведением реальных антипаттернов. Healthcare.gov в 2013 году повторил нашу ошибку с монолитной архитектурой и прямыми подключениями к БД, рухнув при первых же 1100 пользователях и потребовав $2.1 млрд на переделку. Knight Capital в 2012 году наглядно показал, к чему ведет архитектурный хаос — их торговые системы с 8 версиями одного кода, работавшими одновременно, сожгли $460 млн за 45 минут. Даже гиганты вроде Uber и Twitter прошли через мучительный переход от монолитов, где Twitter с его знаменитым «Fail Whale» и Uber с репозиторием на 8 млн строк кодом демонстрируют, как нарушение границ контекстов и отказ от модульности превращают систему в неподдерживаемого зомби. А ведь сервисы всех этих компаний создавали большие профессиональные команды, где были свои архитекторы и senior-специалисты. Наш проект, по сути, стал испытательным полигоном, где мы повторили все эти ошибки — от «бедных сервисов» до прямого доступа к чужим базам данных, — чтобы на практике показать, почему архитектурные принципы существуют не просто как теория, а как суровая необходимость, проверенная миллиардными потерями. Полный код проекта доступен здесь . Теги: Source: https://habr.com/ru/articles/964450/