Микросервисы #
1. Монолит vs Микросервис #
- Монолит - это подход, где всё приложение разрабатывается как единое целое. Все функции и компоненты связаны друг с другом, работают в одном проекте и на одном сервере
- Микросервис - это подход, где приложение разбивается на независимые сервисы, каждый из которых отвечает за отдельную задачу
Плюсы и минусы монолитов?
Плюсы:
- Не надо настраивать взаимодействие(Kafka например)
- Нет проблем с транзакциями в БД в отличии от микросервисов
- Выше производительность
- Простое развертывание для девопса
- Проще тестировать, т.к. монолит проще поднять нежели 10-ки микросервисов
Минусы:
- Если упадет монолит, то упадут все его части
- Долгая сборка тестов
- При увеличении нагрузки, приходится масштабировать всё приложение целиком, даже если проблема только в одной его части
- Сложность добавления нового функционала, т.к. надо понимать как компоненты работают, чтобы где то не упало что то
- Сложно добавлять новые технологии, потому что все взаимосвязано
2. Плюсы и минусы микросервисов #
Плюсы:
- Проще влиться в проект - не надо понимать весь проект
- Проще протестировать конкретный микросервис
- Отказоустойчивость – если упал какой-нибудь сервис, остальные продолжат работать
- Легче пробовать новые технологии, переписав один сервис, а не весь монолит
- Можно масштабировать только те сервисы, которые под нагрузкой, не трогая остальные
Минусы:
- Медленнее монолита
- Интеграционные тесты сложнее, т.к нужно проверить, как взаимодействуют все сервисы вместе.
- Порог вхождения разработчиков выше, чем в монолитной архитектуре(надо разбираться в брокерах сообщений)
- Сложнее работать с транзакциями если они в разных микросервисах
3. Паттерн Saga в микросервисной архитектуре #
Saga - это паттерн для управления распределёнными транзакциями в микросервисах. Он разбивает глобальную транзакцию на последовательность локальных транзакций, каждая из которых выполняется в рамках одного сервиса. Решает проблему распределённых транзакций (которые отрабатывают в разных системах), чтобы при ошибке в одной из транзакций был rollback во всех
Плюсы
- Гибкость - позволяет работать с распределёнными транзакциями без блокировок баз данных
- Отказоустойчивость - при сбоях можно выполнить компенсационное действие вместо отката всей транзакции
- Независимость сервисов - нет необходимости в общей базе данных, каждый сервис может использовать собственное хранилище
- Подходит для долгих операций - локальные транзакции могут быть выполнены асинхронно и растянуты во времени
Минусы
- Сложность реализации - необходимо тщательно продумать порядок шагов и компенсационные действия
- Отсутствие атомарности - результаты промежуточных шагов видны другим сервисам, даже если последующие шаги завершатся сбоем
- Неявная согласованность - данные могут быть несогласованными в течение выполнения всей цепочки
- Задержки - асинхронные процессы могут увеличить время завершения транзакции
- Необходимость оркестрации - требует использования системы оркестрации или управления событиями
Какие способы координации транзакций в Saga?
- Хореография - микросервисы сами обмениваются событиями между собой. Каждый сервис реагирует на события другого и выполняет свою локальную транзакцию, создавая новое событие для следующего сервиса
- Оркестрация - есть один “управляющий” сервис, который следит за процессом. Он говорит каждому микросервису что делать и следит за успешностью каждого шага
Какие типы транзакций существуют в Saga
- Компенсируемая - это обычная транзакция, которую можно откатить в случае ошибки на следующих шагах
- Компенсирующая - запускается в случае возникновения ошибки в компенсируемой транзакции, которая откатывает все предыдущие транзакции
- Поворотная - это транзакция, после которой нельзя ничего откатить
- Повторяемая - это транзакция, которую можно выполнить снова, если возникли временные проблемы
4. CAP теорема #
CAP теорема (теорема Брюера) гласит, в любой реализации распределённых вычислений возможно обеспечить не более двух из трёх следующих свойств:
- Consistency (согласованность) - данные во всех сервисах в один момент времени не противоречат друг другу
- Availability (доступность) - любой запрос к системе завершается откликом, однако без гарантии, что ответы всех узлов системы совпадают
- Partition Tolerance (устойчивость к фрагментации сети) - cистема продолжает работать, даже если между узлами пропала связь или сеть разделилась на части
5. Паттерн Circuit Breaker #
Circuit Breaker переводится как автоматический выключатель (автомат защиты, предохранитель). Он мониторит вызовы к внешнему сервису и при обнаружении большого количества неудачных попыток временно “блокирует” вызов к сервису, предотвращая падение всей системы. Circuit Breaker может находиться в трёх состояниях
- Закрытое (цепь замкнута) - всё нормально, запросы проходят к сервису
- Открытое (цепь разомкнута) - если ошибки становятся слишком частыми, Circuit Breaker перестаёт направлять запросы к проблемному сервису
- Полуоткрытое - после некоторого времени Circuit Breaker позволяет нескольким запросам к внешнему сервису пройти. Если всё нормально - Circuit Breaker переходит в закрытое состояние. Если нет - остаётся в открытом состоянии
Проводя аналогию с электрической цепью, Circuit Breaker - это автоматический ключ, размыкающий (открытое состояние) и замыкающий цепь (закрытое состояние). Полуоткрытое состояние можно описать как замкнутый ключ с плохим контактом
6. Паттерн API Gateway #
API Gateway - это шлюз для API. Он анализирует запросы и решает, какой микросервис должен их обработать. Может объединять информацию от разных микросервисов в один ответ, а также:
- управлять нагрузкой
- кэшировать ответы (уменьшение времени отклика)
- интегрироваться с Identity Provider’ами
7. HTTP vs Kafka. Какой способ общения между микросервисами лучше и почему? #
| Критерий | HTTP | Kafka |
|---|---|---|
| Тип взаимодействия | Синхронное (запрос-ответ) | Асинхронное (публикация и подписка на события) |
| Надёжность передачи данных | Нет встроенного механизма повторной доставки запросов | Сообщения сохраняются и доставляются даже при сбоях потребителей |
| Производительность | Ограничено количеством одновременных запросов и доступностью сервисов | Высокая производительность при обработке больших объёмов сообщений |
| Сложность реализации | Легко реализовать с использованием стандартных библиотек и протоколов | Требуется дополнительная настройка брокеров, топиков и клиентов |
| Задержки | Минимальные, так как ответ приходит сразу после запроса | Возможны задержки из-за асинхронной обработки сообщений |
| Независимость сервисов | Сервисы (отправитель и получатель) зависят от доступности друг друга | Полная независимость сервисов: отправитель не знает о состоянии получателя |
| Масштабируемость | Ограничена синхронным взаимодействием и увеличением числа клиентов | Хорошо масштабируется для работы с высокими нагрузками |
| Поддержка событийности | Не подходит для событийной архитектуры | Предназначен для работы с событиями (обработка, хранение, ретрансляция) |
| Совместимость | Поддерживается практически всеми языками программирования и библиотеками | Требуется поддержка Kafka-клиентов для различных языков |
| Устойчивость к сбоям | Если один сервис недоступен, запрос не может быть обработан | Сообщения сохраняются в очереди, пока не будут обработаны |
Использовать HTTP, если:
- нужно немедленное подтверждение результата (например, авторизация, выполнение команды)
- система проста и не требует сложной обработки сообщений
Использовать Kafka, если:
- требуется высокая производительность и масштабируемость
- сервисы должны быть независимыми и работать асинхронно
- важна надёжность передачи данных и минимизация влияния сбоев
Идеальный подход - гибридное использование:
- для взаимодействия в реальном времени применять HTTP
- для событийной модели и обработки больших объёмов данных - Kafka
8. Шаблоны проектирования для микросервисов (паттерны) #
| Паттерн | Категория | Краткое описание | Когда применять |
|---|---|---|---|
| API Gateway | Интеграция | Единая точка входа, маршрутизация запросов к микросервисам | Когда нужно скрыть внутренние URL и объединить контракты |
| Service Discovery | Инфраструктура | Автоматическая регистрация и поиск сервисов в кластере | При динамическом масштабировании и частых перезапусках |
| Circuit Breaker | Надёжность | Останавливает вызовы к недоступному сервису, даёт ему восстановиться | Для хрупких удалённых зависимостей с переменной доступностью |
| Retry | Надёжность | Повторная попытка выполнения неудачных запросов | При кратковременных сетевых или инфраструктурных сбоях |
| Bulkhead | Надёжность | Изоляция ресурсов (пулы потоков, соединений) для каждого сервиса | Чтобы сбой одного функционала не «затопил» весь сервис |
| Saga | Транзакции | Управление распределёнными транзакциями через последовательность локальных операций и компенсации | Когда нужны данные в нескольких сервисах с откатом изменений |
| CQRS | Моделирование данных | Разделение команд (write) и запросов (read) на разные модели и сервисы | При высоких требованиях к производительности чтения |
| Strangler Fig | Развёртывание | Постепенный перенос функциональности старой системы в микросервисы | Миграция монолита на микросервисы шаг за шагом |
| Sidecar | Инфраструктура | Соседний процесс рядом с сервисом, предоставляющий кросс-функциональные возможности (логирование, прокси) | Для единообразной маршрутизации, безопасности, телеметрии |
| Ambassador | Инфраструктура | Прокси/агент, выступающий «послом» сервиса, осуществляющий внешнюю интеграцию | Когда нужно вынести внешние зависимости за границы сервиса |
9. Что такое Transactional Outbox #
Transactional Outbox - это паттерн, который обеспечивает надёжную доставку событий между сервисами. Он заключается в том, что бизнес-данные и событие записываются в одной транзакции в БД, а потом отдельный процесс публикует событие в брокер. Таким образом, мы избегаем рассинхронизации между базой и очередью.
Проблема, которую он решает
@Transactional
public void placeOrder(Order order) {
orderRepository.save(order); // 1 Сохранили заказ в БД
eventBus.publish(new OrderCreatedEvent(order)); // 2 Отправили событие в Kafka
}
Если между 1 и 2 произойдёт сбой (например, сеть упала, брокер недоступен, приложение умерло) - заказ сохранится, но событие не уйдёт, и другие сервисы (например, payment-service) о нём никогда не узнают
Если наоборот - сообщение ушло, а транзакция завершилась откатом, то получатель узнает о заказе, которого в базе нет.
Решение через паттерн:
// сохраняем событие в одной транзакции с сущностью
@Transactional
public void createOrder() {
orderRepository.save(order);
outboxRepository.save(
new OutboxEvent("OrderCreated",json(order))
);
}
// в шедулере вычитываем события с нужным статусом и отправляем в брокер
@Scheduled(fixedDelay = 1000)
public void publish() {
var events = outboxRepository.findUnprocessed();
for (var event : events) {
kafka.send(
event.type(),
event.payload()
);
event.markProcessed();
outboxRepository.save(event);
}
}
Важный нюанс - если после отправки в брокер новый статус не установится в БД (outboxRepository.save(event) не отработает), то при следующем вызове произойдёт повторная отправка этого же события, а значит, на консьюмере должно быть осуществлена дедупликация сообщений (идемпотентность)
Преимущества паттерна
- Атомарность - данные и событие фиксируются одной транзакцией
- Надёжность - не потеряешь события даже при сбое
- Простота - всё хранится в одной БД
- Совместимо с любым брокером - Kafka, RabbitMQ, SQS, etc
- Легко отлаживать - можно посмотреть outbox-таблицу
Недостатки
- Таблица
outboxрастёт - нужно чистить - События публикуются не мгновенно (нужно планировать задержку)
- Нужно следить за идемпотентностью на стороне потребителей
В Enterprise системах часто используют готовые CDC (Change Data Capture) системы, например Debezium, который перед отправкой в Kafka читает не таблицу в Postgresql, а WAL-журнал.