Микросервисы

Микросервисы #

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. Какой способ общения между микросервисами лучше и почему? #

КритерийHTTPKafka
Тип взаимодействияСинхронное (запрос-ответ)Асинхронное (публикация и подписка на события)
Надёжность передачи данныхНет встроенного механизма повторной доставки запросовСообщения сохраняются и доставляются даже при сбоях потребителей
ПроизводительностьОграничено количеством одновременных запросов и доступностью сервисовВысокая производительность при обработке больших объёмов сообщений
Сложность реализацииЛегко реализовать с использованием стандартных библиотек и протоколовТребуется дополнительная настройка брокеров, топиков и клиентов
ЗадержкиМинимальные, так как ответ приходит сразу после запросаВозможны задержки из-за асинхронной обработки сообщений
Независимость сервисовСервисы (отправитель и получатель) зависят от доступности друг другаПолная независимость сервисов: отправитель не знает о состоянии получателя
МасштабируемостьОграничена синхронным взаимодействием и увеличением числа клиентовХорошо масштабируется для работы с высокими нагрузками
Поддержка событийностиНе подходит для событийной архитектурыПредназначен для работы с событиями (обработка, хранение, ретрансляция)
СовместимостьПоддерживается практически всеми языками программирования и библиотекамиТребуется поддержка 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-журнал.