CloudJava: Docker. микросервисы. Kafka.
Реактивный стек. Spring Cloud

Программа

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

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

В этом вводном уроке мы рассмотрим архитектуру приложения, которое будем последовательно разрабатывать на протяжении всего курса, а также теоретические аспекты, необходимые для понимания причин выбора конкретных технологий реализации.
Для начала определимся с отправной точкой в этом, надеюсь, увлекательном путешествии в мир 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-item/{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 сообщение об обработке заказа, содержащее статус заказа после обработки.

Работа с проектом и инструменты
Контакты: Григорий Кислин
E-mail: admin@javaops.ru
ОГРНИП: 317784700063201 | ИНН: 782581076920

Cайт-партнер: topjava.ru
Поделиться:
Москва Санкт-Петербург Киев Минск Харьков Новосибирск Львов Нижний Новгород Алматы Одесса Днепр Краснодар Екатеринбург Самара Ростов-на-Дону Днепропетровск Казань Воронеж Челябинск Пермь Гомель Владивосток Астана Томск Саратов Гродно Уфа Калининград Николаев Запорожье Ярославль Омск Кемерово Белгород Брест Ташкент Херсон Ижевск Чебоксары Караганда Волгоград Балашиха Йошкар-Ола Киров Барнаул Калуга Иркутск Магнитогорск Донецк Монреаль Warszawa Los Angeles Винница Сыктывкар Тюмень Рига Кишинев Бишкек Владимир Красноярск Ульяновск Жуковский Тольятти Тверь Вологда Улан-удэ Сочи Иваново Мариуполь Пенза Краков Сумы Подольск Тула Рязань Хабаровск Helsinki Могилев Haifa Полтава Сургут Новокузнецк Березники San Francisco Иннополис Tel Aviv Ереван Тернополь Ставрополь Кривой рог Северодвинск Витебск Астрахань

AltStyle によって変換されたページ (->オリジナル) /