Вводная часть

Назначение и задачи проекта

В этом вводном уроке мы рассмотрим архитектуру приложения, которое будем последовательно разрабатывать на протяжении всего курса, а также теоретические аспекты, необходимые для понимания причин выбора конкретных технологий реализации.
Для начала определимся с отправной точкой в этом, надеюсь, увлекательном путешествии в мир Spring Cloud и микросервисов. Итак, мы выступаем в роли команды, которая пишет бэкенд для сервиса заказов еды на вынос.
За нами закреплена реализация следующих задач:
  • управление меню нашего онлайн-кафе
  • регистрация, аутентификация и авторизация клиентов и сотрудников кафе
  • управление заказами зарегистрированных пользователей
  • организация возможности пользователям оставить отзыв о блюде
  • предоставление фронтенду информации о блюдах и пользовательских отзывах
Заказчик настаивает на применении микросервисной архитектуры, так как он уверен в успехе проекта и хочет, чтобы каждый элемент системы был:
  • максимально независим от остальных
  • легко масштабировался горизонтально за счет поднятия новых инстансов сервиса
  • имел возможность изменять конфигурацию без перезапуска сервиса
  • был устойчив к возможным сбоям
  • предоставлял возможность отслеживать свое текущее состояние

Манифест от Heroku “12-факторное приложение”

Перед началом работы тим-лид команды предлагает изучить опыт разработчиков Heroku, которые обобщили его в своем манифесте “12-факторное приложение”. Вкратце, этот опыт включает в себя 12 принципов, которых следует придерживаться при разработке систем с требованиями, подобными тем, что нашей команде предъявляет заказчик:
  1. Одна кодовая база - одно приложение. Если несколько приложений используют общий код, то его можно выделить в отдельную библиотеку, и объявить ее как зависимость. В программировании применяется принцип DRY (Don’t Repeat Yourself), однако при проектировании микросервисов разработчикам иногда целесообразно отходить от этого правила в части касающейся моделей состояния, передаваемых по сети или Data Transfer Objects (DTOs) - они адаптируются под каждый микросервис, и в некоторых случаях могут быть полностью идентичными, в некоторых - нет. В библиотеку обычно выносится бизнес-логика, а не DTOs.
  2. Явное объявление и изолирование зависимостей. Приложение не должно зависеть от неявно существующих и доступных системе пакетов. Java разработчики для управлением зависимостями в большинстве случаев используют Maven или Gradle, где явно прописывают все необходимое для запуска и корректной работы своего приложения. Под неявными зависимостями подразумевается, например, не прописанная в манифесте зависимостей (build.gradle или pom.xml) утилита curl, по каким-либо причинам необходимая для работы приложения: на машине разработчика она может присутствовать, но в среде выполнения - нет.
  3. Разделение конфигурации приложения и кода. Иногда приложения хранят конфигурацию как константы в коде. Это нарушение методологии двенадцати факторов, которая требует строгого разделения конфигурации и кода. Конфигурация может существенно различаться между развёртываниями, код не должен различаться. При этом такие конфигурационные параметры, как логины, пароли, адреса БД, ключи API сторонних сервисов и т.п. рекомендуется хранить в переменных окружения, а не в конфигурационных файлах. Переменные окружения также считаются частью конфигурации приложения, их легко изменить между развёртываниями, не изменяя код, они являются независимым от языка и операционной системы стандартом, они не публикуются в репозиторий. Проверить правильность соблюдения этого пункта можно так: оцените, можете ли вы опубликовать свой код в открытый доступ без компрометации персональных учетных данных.
  4. Сторонние службы (базы данных, кэши и т.д.) рассматриваются как подключаемые ресурсы, данные для подключения которых должны храниться в конфигурации. При разработке нашего приложения данные для подключения к сторонним службам в dev-среде для простоты мы будем хранить в конфигурационных файлах, а для prod-среды конфигурационные файлы будут ссылаться на переменные окружения. Например: spring.datasource.username=${DB_USERNAME}
  5. Строгое разделение стадий сборки, релиза и выполнения. Согласно методологии двенадцати факторов приложение проходит несколько этапов жизненного цикла:
    1. Сборка - преобразование кода в исполняемый пакет (сборку), включая загрузку зависимостей, компиляцию файлов и ресурсов. Сборка инициируется разработчиком приложения всякий раз, когда разворачивается новый код. Тесты, которые пишет разработчик, входят в стадию сборки.
    2. Релиз - объединение полученной на предыдущем этапе сборки с конфигурацией для конкретной среды исполнения. Полученный релиз содержит сборку и конфигурацию и готов к немедленному запуску в среде выполнения. Каждый релиз неизменяем и должен иметь уникальный идентификатор, а также храниться в центральном репозитории. Если говорить о тестовом стенде, то для него готовится отдельный релиз на основе сборки, то есть собранный проект объединяется с конфигурациями для стенда. На нем может проводится нагрузочное тестирование, интеграционное тестирование уже развернутых микросервисов и т.п.
    3. Выполнение - запуск собранного релиза в соответствующей ему среде выполнения. Запуск может происходить автоматически в таких случаях, как перезагрузка сервера или перезапуск упавшего процесса менеджером процессов.
    Строгое разделение в данном случае подразумевает, что мы не можем внести изменения в код приложения, находящегося на стадии выполнения, так как в этом случае нарушится синхронизация с предыдущими стадиями сборки и релиза.
  6. Приложение или запускаемый в рамках него процесс не должны хранить внутри себя состояние. Любые данные, требующие сохранения должны храниться в сторонней службе.
  7. Приложение должно быть полностью самодостаточным и не должно полагаться на наличие стороннего веб-сервера во время выполнения. В случае со Spring Boot приложениями разработчики из коробки получают по умолчанию Tomcat при использовании блокирующего Spring Web, и Netty при использовании реактивного Spring WebFlux.
  8. Приложение должно иметь возможность быть запущенным как несколько процессов на различных физических машинах (масштабироваться). Отсутствие хранящихся в памяти приложения и доступных всем процессам разделяемых данных гарантирует, что масштабирование является простой и надежной операцией. Разделяемые данные и общее состояние должны храниться в сторонних службах, например, распределенный кэш Redis.
  9. Процессы приложения являются утилизируемыми. Это означает, что они могут быть запущены и остановлены в любой момент. При этом время запуска процесса должно стремиться к минимуму, а его завершение должно быть корректным - текущие запросы должны быть отработаны, а новые не должны приниматься.
  10. Необходимо поддерживать различные окружения (dev/staging/prod) максимально похожими. Это подразумевает также использование одинаковых сторонних служб в различных окружениях.
  11. Логирование должно рассматриваться как поток событий. Приложение никогда не занимается маршрутизацией и хранением своего потока вывода. Приложение не должно записывать логи в файл и управлять файлами логов. Вместо этого каждый выполняющийся процесс записывает без буферизации свой поток событий в стандартный вывод stdout. Это позволяет агрегировать события от разных процессов, включая как процессы приложения, так и сторонние службы.
  12. Задачи администрирования и управления должны выполняться с помощью разовых процессов и подчиняться тем же правилам, что и регулярные процессы:
    • Они запускаются на уровне релиза, используя те же кодовую базу и конфигурацию, что и любой другой выполняющийся в этом релизе процесс.
    • Код администрирования должен поставляться вместе с кодом приложения, чтобы избежать проблем синхронизации.
    • Зависимости должны быть объявлены в основном манифесте зависимостей, чтобы процесс мог быть выполнен при обычном развертывании.
    • Конфигурация задачи должна находиться в переменных окружения, чтобы ее можно было выполнить в разных окружениях.

Паттерны проектирования микросервисной архитектуры

Также тим-лид настаивает, чтобы каждый член команды ознакомился с рядом паттернов, применяемых при проектировании микросервисной архитектуры:
  1. Service Discovery - позволяет микросервисам общаться между собой, не зная точных IP адресов инстансов.
  2. API Gateway - выступает в качестве входной точки в систему микросервисов, маршрутизируя входящие со стороны клиентов запросы. Именно в Gateway логичнее всего реализовывать сквозной функционал: безопасность, ограничение количества входящих запросов, кэширование сессий и т.п.
  3. Externalized Configuration - обеспечивает возможность разделить кодовую базу и конфигурации микросервисов.
  4. Distributed Tracing - обеспечивает возможность отслеживания жизненного пути входящего запроса, проходящего через несколько микросервисов.
  5. Log Aggregation - агрегирует логи от всех микросервисов и перенаправляет их в систему централизованного анализа логов.
  6. Circuit Breaker - предотвращает каскадное падение сервисов в случае, когда микросервисы используют синхронное взаимодействие между собой (блокирующие запросы) и один из сервисов по какой-либо причине не отвечает в течение длительного периода времени или регулярно возвращает ошибку 5ХХ.
  7. Transactional Outbox - используется для обеспечения гарантированной записи сообщения в БД и отправки сообщения в брокер сообщений.
Каждый из этих паттернов и бОльшая часть принципов 12-факторного приложения будут применены в ходе реализации поставленной заказчиком задачи. На текущий момент команда лишь знакомится с ними, а детальное их обсуждение состоится при разработке соответствующих компонентов системы.

Схема и стек приложения

Команда архитекторов завершила свою работу и предоставила следующую схему микросервисов: Структура проекта
Тим-лид предоставил ряд рекомендаций: использовать
  • Spring Boot 3 в качестве основного фреймворка для написания микросервисов
  • Spring Cloud Config Server для хранения конфигураций
  • Spring Cloud Netflix - Eureka в качестве Service Discovery
  • PostgreSQL 16 в качестве баз данных для микросервисов (у каждого микросервиса своя база)
  • Spring Cloud Gateway в качестве входной точки в приложение
  • Apache Kafka в качестве брокера сообщений для обеспечения асинхронного взаимодействия микросервисов
  • Redis для кэширования данных
  • Keycloak + OIDC/OAuth2 для аутентификации/авторизации

Назначение и API микросервисов

Основными микросервисами, отвечающими за реализацию бизнес-логики, станут:

  1. Menu Service
  2. Orders Service
  3. Review Service
  4. Menu Aggregate Service
  5. Order Dispatch Service

Menu Service

Предоставляет REST API для CRUD-операций с меню:
  • POST /v1/menu-items - создать блюдо, информация о блюде передается в теле запроса. Доступно для сотрудников, информация о сотруднике передается в токене доступа.
  • DELETE /v1/menu-items/{id} - удалить блюдо. Доступно для сотрудников, информация о сотруднике передается в токене доступа
  • PATCH /v1/menu-items/{id} - обновить блюдо, параметры обновления передаются в теле запроса. Доступно для сотрудников, информация о сотруднике передается в токене доступа
  • GET /v1/menu-items/{id} - получить блюдо. Доступно всем пользователям
  • GET /v1/menu-items?category={category}&sort={sort} - получить список блюд из выбранной категории, отсортированный или по алфавиту (AZ, ZA), или по цене (PRICE_ASC, PRICE_DESC), или по дате создания (DATE_ASC, DATE_DESC). Доступно всем пользователям
Данные хранятся в реляционной базе PostgreSQL 16.

Orders Service

Предоставляет REST API для создания и просмотра совершенных пользователем заказов:
  • POST /v1/menu-orders - создать заказ, информация о заказе передается в теле запроса. Доступно для зарегистрированных клиентов онлайн кафе, информация о пользователе передается в токене доступа
  • GET /v1/menu-orders?sort={sort}&from={from}&size={size} - получить пагинированный список (используется offset пагинация) заказов пользователя, отсортированный по дате создания (DATE_ASC, DATE_DESC). Информация о пользователе передается в токене доступа.

Отправляет в топик Kafka v1.public.orders_outbox сообщения о создании заказа после того, как заказ был сохранен. Для обеспечения гарантированной отправки сообщения применяется паттерн Transactional Outbox, для чего используется Kafka Connect (система, позволяющая надежно вычитывать данные из внешних систем в Kafka и отправлять данные из топиков Kafka во внешние системы) и Debezium Postgres Connector (опенсорсный коннектор, который постоянно вычитывает изменения в БД и отправляет их в топики Kafka). Вычитывает из топика Kafka v1.orders_dispatch сообщения о том, что заказ был обработан и передан на исполнение или отклонен, обновляет статус заказа в Postgres.
Дополнительно: Kafka Connect на примере Debezium PostgresConnector.

Данные хранятся в реляционной базе PostgreSQL 16.

Review Service

Предоставляет REST API для CRUD-операций с отзывами пользователей о блюдах:
  • POST /v1/reviews - создать отзыв, информация об отзыве передается в теле запроса. Информация о пользователе, создающем отзыв, передается в токене доступа, доступно для зарегистрированных пользователей. Рейтинг блюда должен быть от 1 до 5.
  • GET /v1/reviews/{id} - получить отзыв по ID. Доступно всем пользователям
  • GET /v1/reviews/my?sortBy={sort}&from={from}&size={size} получить пагинированный список отзывов конкретного пользователя. Список отсортирован по дате создания: DATE_ASC, DATE_DESC. Информация о пользователе передается в токене доступа. (доступно для зарегистрированных пользователей)
  • GET /v1/reviews/menu-items/{id}?sort={sort}&from={from}&size={size} - получить пагинированный список отзывов к конкретному блюду. Список отсортирован по дате создания: DATE_ASC, DATE_DESC. Доступно всем пользователям. Также в ответе передается информация о рейтинге и средней оценке блюда.
  • POST /v1/reviews/ratings - получить рейтинги и средние оценки блюд, идентификаторы которых передаются в теле запроса. Рейтинг блюда рассчитывается согласно доверительному интервалу биномиального распределения по методу Уилсона (Wilson Score Confidence Interval). За основу взяты материалы из статей How to Build a 5 Star Rating System with Wilson Interval и How Not To Sort By Average Rating.
Данные хранятся в реляционной базе PostgreSQL 16.

Menu Aggregate Service

Предоставляет REST API для агрегированных запросов блюд с отзывами или с рейтингами:
  • GET /v1/menu-aggregate/{menuId}?sort={sort}&from={from}&size={size} - получить информацию о блюде с отзывами. Список отзывов пагинирован и отсортирован по дате создания: DATE_ASC, DATE_DESC.
  • GET /v1/menu-aggregate?category={category}&sort={sort} - получить информацию о блюдах из указанной категории. Блюда отсортированы или по алфавиту (AZ, ZA), или по цене (PRICE_ASC, PRICE_DESC), или по дате создания (DATE_ASC, DATE_DESC), или по рейтингу (RATE_ASC, RATE_DESC). Информация по каждому блюду включает в себя рейтинг блюда, полученный из Review Service.
Не хранит данные в базе, а получает их из сервисов Menu Service и Orders Service с помощью неблокирующих вызовов, используя Spring WebFlux.

Order Dispatch Service

Реализует обработку заказов
  • Вычитывает из топика Kafka v1.public.orders_outbox сообщения о создании заказа, далее происходит обработка заказа. На текущий момент логика заключается в логировании события.
  • После обработки заказа отправляет в топик Kafka v1.orders_dispatch сообщение об обработке заказа, содержащее статус заказа после обработки.
Работа с проектом и инструменты