Разделение на сервисы
Основы микросервисной архитектуры
Что такое микросервисы
Микросервисы — это архитектурный подход, при котором приложение разбивается на набор небольших, независимо развертываемых сервисов, каждый из которых работает в собственном процессе и взаимодействует через четко определенные API.
Ключевые принципы:
- Разделение по бизнес-возможностям — каждый сервис отвечает за конкретную бизнес-функцию
- Децентрализованное управление — команды самостоятельно принимают технические решения
- Отказоустойчивость — сбой одного сервиса не должен приводить к падению всей системы
- Автоматизация — развертывание, мониторинг и управление должны быть автоматизированы
Преимущества и недостатки
Преимущества:
- Независимое развертывание и масштабирование
- Технологическое разнообразие (polyglot persistence)
- Изоляция отказов
- Лучшая организация команд разработки
- Возможность экспериментов с новыми технологиями
Недостатки:
- Сложность распределенной системы
- Сетевые задержки и ненадежность
- Сложность управления данными
- Операционная сложность
- Сложность отладки и мониторинга
Стратегии разбиения на микросервисы
1. Разбиение по бизнес-функциям
Функциональная декомпозиция — выделение сервисов на основе бизнес-возможностей организации. Каждый сервис инкапсулирует определенную бизнес-функцию.
Принципы выделения:
- Один сервис = одна бизнес-возможность
- Высокая связность внутри сервиса — тесно связанные функции объединяются
- Низкая связность между сервисами — минимум взаимодействий
- Автономность — сервис может функционировать независимо
Пример разбиения e-commerce системы:
// Сервис управления пользователями
@Service
public class UserService {
// Регистрация, аутентификация, профили
public User createUser(CreateUserRequest request) { }
public User authenticateUser(String email, String password) { }
public UserProfile getUserProfile(Long userId) { }
}
// Сервис каталога товаров
@Service
public class ProductCatalogService {
// Управление товарами, категориями, поиск
public Product createProduct(CreateProductRequest request) { }
public List<Product> searchProducts(SearchCriteria criteria) { }
public Category createCategory(CreateCategoryRequest request) { }
}
// Сервис заказов
@Service
public class OrderService {
// Создание заказов, управление статусами
public Order createOrder(CreateOrderRequest request) { }
public Order updateOrderStatus(Long orderId, OrderStatus status) { }
public List<Order> getUserOrders(Long userId) { }
}
2. Разбиение по данным
Разделение по типам данных — каждый сервис управляет определенным типом данных и связанными с ними операциями.
Критерии разбиения:
- Жизненный цикл данных — данные с одинаковым жизненным циклом группируются
- Паттерны доступа — часто используемые вместе данные объединяются
- Консистентность — данные, требующие строгой консистентности, остаются в одном сервисе
3. Разбиение по нагрузке
Масштабирование критичных компонентов — выделение сервисов на основе требований к производительности.
Факторы разбиения:
- Частота использования — часто используемые функции выделяются отдельно
- Ресурсоемкость — CPU/памяти/IO интенсивные операции
- Паттерны масштабирования — различные требования к горизонтальному масштабированию
Bounded Context из Domain-Driven Design
Что такое Bounded Context
Bounded Context — это центральный паттерн в DDD, определяющий границы, в которых конкретная доменная модель действует и применяется. Это семантическая граница, внутри которой все термины имеют четкое и однозначное значение.
Ключевые характеристики:
- Единый язык — внутри контекста все участники используют одну терминологию
- Модельная целостность — доменная модель остается последовательной
- Автономность — контекст может эволюционировать независимо
- Четкие границы — ясно определено, что входит и что не входит в контекст
Выявление Bounded Context
Процесс выявления:
- Event Storming — моделирование доменных событий
- Анализ терминологии — выявление различий в понимании терминов
- Организационные границы — команды, отделы, процессы
- Источники данных — различные системы и базы данных
Пример выявления контекстов в банковской системе:
// Контекст "Управление клиентами"
public class Customer {
private CustomerId id;
private PersonalInfo personalInfo;
private ContactDetails contactDetails;
private KYCStatus kycStatus;
// Бизнес-логика, связанная с клиентом
public void updateKYCStatus(KYCStatus status) { }
public boolean isEligibleForLoan() { }
}
// Контекст "Кредитование" - тот же клиент, но другая модель
public class LoanApplicant {
private ApplicantId id;
private CreditScore creditScore;
private IncomeDetails income;
private LoanHistory loanHistory;
// Бизнес-логика, связанная с кредитованием
public LoanEligibility assessLoanEligibility() { }
public RiskProfile calculateRiskProfile() { }
}
Context Map
Context Map — это документ, описывающий отношения между различными Bounded Context'ами в системе.
Типы отношений:
- Partnership — взаимозависимые команды
- Shared Kernel — общая доменная модель
- Customer-Supplier — upstream/downstream отношения
- Conformist — downstream адаптируется к upstream
- Anticorruption Layer — защитный слой от внешних влияний
// Пример Anticorruption Layer
@Component
public class PaymentGatewayAdapter {
private final ExternalPaymentService externalService;
// Адаптация внешней модели к внутренней
public PaymentResult processPayment(Payment payment) {
ExternalPaymentRequest request = mapToExternalRequest(payment);
ExternalPaymentResponse response = externalService.processPayment(request);
return mapToInternalResult(response);
}
private ExternalPaymentRequest mapToExternalRequest(Payment payment) {
// Преобразование внутренней модели во внешнюю
return ExternalPaymentRequest.builder()
.amount(payment.getAmount().getValue())
.currency(payment.getCurrency().getCode())
.build();
}
}
Database per Service Pattern
Принципы паттерна
Database per Service — каждый микросервис имеет собственную базу данных, к которой другие сервисы не имеют прямого доступа. Это обеспечивает полную автономность сервисов.
Ключевые принципы:
- Инкапсуляция данных — данные скрыты за API сервиса
- Технологическая независимость — каждый сервис выбирает подходящую СУБД
- Независимое развитие — схема БД может изменяться без влияния на другие сервисы
- Масштабируемость — каждая БД масштабируется независимо
Преимущества паттерна
Техническая автономность:
- Выбор подходящей технологии для каждого сервиса
- Независимые схемы и версионирование
- Отсутствие конфликтов блокировок между сервисами
Организационная автономность:
- Команды могут работать независимо
- Отсутствие координации изменений схемы
- Ясная ответственность за данные
Реализация паттерна
// Сервис заказов со своей базой данных
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "customer_id")
private Long customerId; // Ссылка на клиента через ID
@Column(name = "total_amount")
private BigDecimal totalAmount;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@CreationTimestamp
private LocalDateTime createdAt;
}
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByCustomerId(Long customerId);
List<Order> findByStatus(OrderStatus status);
}
// Сервис инвентаря со своей базой данных
@Entity
@Table(name = "inventory")
public class InventoryItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "product_id")
private Long productId;
@Column(name = "available_quantity")
private Integer availableQuantity;
@Column(name = "reserved_quantity")
private Integer reservedQuantity;
// Бизнес-логика инкапсулирована в сервисе
public boolean canReserve(int quantity) {
return availableQuantity >= quantity;
}
}
Вызовы и решения
Проблема: Распределенные транзакции
Решение: Saga Pattern
@Component
public class OrderSaga {
@SagaOrchestrationStart
public void processOrder(OrderCreatedEvent event) {
// 1. Резервируем товар
sagaManager.choreography()
.step("reserve-inventory")
.compensate("release-inventory")
.step("process-payment")
.compensate("refund-payment")
.step("confirm-order")
.compensate("cancel-order")
.execute();
}
@SagaOrchestrationStart
public void handleInventoryReserved(InventoryReservedEvent event) {
// 2. Обрабатываем платеж
paymentService.processPayment(event.getOrderId(), event.getAmount());
}
@SagaOrchestrationStart
public void handlePaymentProcessed(PaymentProcessedEvent event) {
// 3. Подтверждаем заказ
orderService.confirmOrder(event.getOrderId());
}
}
Проблема: Запросы к нескольким сервисам
Решение: API Composition или CQRS
// API Composition - композиция данных на уровне API Gateway
@RestController
public class OrderViewController {
private final OrderService orderService;
private final CustomerService customerService;
private final ProductService productService;
@GetMapping("/orders/{orderId}/details")
public OrderDetailsView getOrderDetails(@PathVariable Long orderId) {
// Параллельные запросы к сервисам
CompletableFuture<Order> orderFuture =
CompletableFuture.supplyAsync(() -> orderService.getOrder(orderId));
CompletableFuture<Customer> customerFuture =
orderFuture.thenCompose(order ->
CompletableFuture.supplyAsync(() ->
customerService.getCustomer(order.getCustomerId())));
CompletableFuture<List<Product>> productsFuture =
orderFuture.thenCompose(order ->
CompletableFuture.supplyAsync(() ->
productService.getProducts(order.getProductIds())));
return CompletableFuture.allOf(orderFuture, customerFuture, productsFuture)
.thenApply(v -> new OrderDetailsView(
orderFuture.join(),
customerFuture.join(),
productsFuture.join()
)).join();
}
}
Data Ownership Models
Общий Data Ownership
Shared Database — несколько сервисов используют одну базу данных. Это антипаттерн в микросервисной архитектуре, но иногда применяется при миграции от монолита.
Характеристики:
- Тесная связность — изменения схемы влияют на все сервисы
- Координация — необходимость согласования изменений
- Производительность — возможны конфликты и блокировки
- Упрощенные запросы — легко получить данные из разных доменов
Проблемы:
// Проблема: несколько сервисов обращаются к одной таблице
@Entity
@Table(name = "users") // Используется и UserService, и OrderService
public class User {
@Id
private Long id;
// Поля для UserService
private String email;
private String password;
// Поля для OrderService
private String shippingAddress;
private String billingAddress;
// Изменение любого поля влияет на оба сервиса
}
Независимый Data Ownership
Dedicated Database — каждый сервис полностью владеет своими данными и предоставляет к ним доступ только через свой API.
Принципы:
- Единственный владелец — только один сервис может изменять данные
- API-первый подход — все операции через публичный API
- Версионирование — изменения API должны быть обратно совместимы
- Кэширование — данные могут кэшироваться в других сервисах
// Правильное разделение данных
@Service
public class CustomerService {
@Autowired
private CustomerRepository customerRepository;
// Единственный источник данных о клиентах
public Customer getCustomer(Long customerId) {
return customerRepository.findById(customerId)
.orElseThrow(() -> new CustomerNotFoundException(customerId));
}
public CustomerProfile getCustomerProfile(Long customerId) {
Customer customer = getCustomer(customerId);
return CustomerProfile.builder()
.id(customer.getId())
.fullName(customer.getFullName())
.email(customer.getEmail())
.build();
}
}
// Другой сервис получает данные через API
@Service
public class OrderService {
@Autowired
private CustomerServiceClient customerServiceClient;
public Order createOrder(CreateOrderRequest request) {
// Проверяем существование клиента через API
CustomerProfile customer = customerServiceClient
.getCustomerProfile(request.getCustomerId());
Order order = Order.builder()
.customerId(customer.getId())
.customerName(customer.getFullName()) // Денормализация
.build();
return orderRepository.save(order);
}
}
Гибридный подход
Event-Driven Data Replication — каждый сервис владеет своими данными, но реплицирует необходимые данные других сервисов через события.
// Публикация событий при изменении данных
@Service
public class CustomerService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public Customer updateCustomer(Long customerId, UpdateCustomerRequest request) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new CustomerNotFoundException(customerId));
customer.updatePersonalInfo(request.getPersonalInfo());
customer = customerRepository.save(customer);
// Публикуем событие об изменении
eventPublisher.publishEvent(new CustomerUpdatedEvent(
customer.getId(),
customer.getFullName(),
customer.getEmail()
));
return customer;
}
}
// Подписка на события в другом сервисе
@Service
public class OrderService {
@EventListener
public void handleCustomerUpdated(CustomerUpdatedEvent event) {
// Обновляем денормализованные данные в заказах
List<Order> orders = orderRepository.findByCustomerId(event.getCustomerId());
orders.forEach(order -> {
order.updateCustomerInfo(event.getFullName(), event.getEmail());
orderRepository.save(order);
});
}
}
Лучшие практики
1. Размер сервиса
Правило двух пицц Amazon — команда, работающая с сервисом, не должна быть больше, чем можно накормить двумя пиццами (5-8 человек).
Критерии размера:
- Когнитивная нагрузка — разработчик должен понимать весь сервис
- Время разработки — новая функция не должна требовать недели работы
- Deployment независимость — сервис развертывается отдельно
2. Коммуникация между сервисами
Синхронная коммуникация:
// REST API с Circuit Breaker
@Component
public class PaymentServiceClient {
@CircuitBreaker(name = "payment-service", fallbackMethod = "fallbackPayment")
@Retry(name = "payment-service")
@TimeLimiter(name = "payment-service")
public CompletableFuture<PaymentResult> processPayment(PaymentRequest request) {
return CompletableFuture.supplyAsync(() -> {
return restTemplate.postForObject(
"/payment/process", request, PaymentResult.class);
});
}
public CompletableFuture<PaymentResult> fallbackPayment(PaymentRequest request, Exception ex) {
return CompletableFuture.completedFuture(
PaymentResult.failed("Payment service unavailable"));
}
}
Асинхронная коммуникация:
// Обработка событий через Message Broker
@Component
public class OrderEventHandler {
@KafkaListener(topics = "order-events", groupId = "inventory-service")
public void handleOrderCreated(OrderCreatedEvent event) {
try {
inventoryService.reserveItems(event.getOrderId(), event.getItems());
// Публикуем событие об успешном резервировании
eventPublisher.publishEvent(new InventoryReservedEvent(
event.getOrderId(), event.getItems()));
} catch (InsufficientStockException e) {
// Публикуем событие об ошибке
eventPublisher.publishEvent(new InventoryReservationFailedEvent(
event.getOrderId(), e.getMessage()));
}
}
}
3. Управление данными
Eventual Consistency — принятие того, что в распределенной системе данные не всегда могут быть консистентными мгновенно.
Стратегии:
- Idempotency — операции должны быть идемпотентными
- Compensation — возможность отмены операций
- Monitoring — отслеживание консистентности данных
// Идемпотентная обработка событий
@Service
public class OrderEventHandler {
@Transactional
public void processOrderCreated(OrderCreatedEvent event) {
// Проверяем, не обработано ли уже это событие
if (eventLogRepository.existsByEventId(event.getEventId())) {
log.info("Event {} already processed", event.getEventId());
return;
}
// Обрабатываем событие
Order order = createOrder(event);
// Записываем в лог обработанных событий
eventLogRepository.save(new EventLog(
event.getEventId(),
event.getEventType(),
LocalDateTime.now()
));
}
}
Эта архитектура требует тщательного планирования и понимания бизнес-доменов, но обеспечивает гибкость, масштабируемость и возможность независимого развития команд разработки.
Коммуникация между сервисами
Основные паттерны коммуникации
Синхронная vs Асинхронная
Синхронная — клиент ждёт ответа от сервера перед продолжением выполнения Асинхронная — клиент отправляет запрос и продолжает работу, не ожидая ответа
Request-Response vs Event-driven
Request-Response — прямой запрос к конкретному сервису с ожиданием ответа Event-driven — сервисы реагируют на события, не зная об отправителе
Request-Response: Client → Service A → Response
Event-driven: Service A → Event Bus → Service B, C, D
Синхронная коммуникация
REST API
REST (Representational State Transfer) — архитектурный стиль для веб-сервисов, основанный на HTTP и принципах:
- Stateless — каждый запрос содержит всю необходимую информацию
- Cacheable — ответы могут кэшироваться
- Uniform interface — стандартные HTTP-методы
- Client-server — разделение клиента и сервера
// Spring Boot REST Controller
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(user);
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody @Valid User user) {
User created = userService.create(user);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
}
// HTTP-клиент с RestTemplate
@Service
public class UserClient {
private final RestTemplate restTemplate;
public User getUser(Long id) {
return restTemplate.getForObject(
"http://user-service/api/users/{id}",
User.class, id);
}
}
Преимущества REST:
- Простота и понятность
- Кэширование HTTP
- Хорошая поддержка в браузерах
- Stateless природа
Недостатки:
- Overhead HTTP-заголовков
- Только текстовые данные
- Нет типобезопасности
gRPC
gRPC — высокопроизводительный RPC-фреймворк от Google, использующий Protocol Buffers и HTTP/2.
// user.proto
syntax = "proto3";
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc CreateUser(CreateUserRequest) returns (User);
rpc ListUsers(ListUsersRequest) returns (stream User);
}
message User {
int64 id = 1;
string name = 2;
string email = 3;
}
message GetUserRequest {
int64 id = 1;
}
// gRPC сервер
@GrpcService
public class UserGrpcService extends UserServiceGrpc.UserServiceImplBase {
@Override
public void getUser(GetUserRequest request,
StreamObserver<User> responseObserver) {
User user = userService.findById(request.getId());
responseObserver.onNext(user);
responseObserver.onCompleted();
}
@Override
public void listUsers(ListUsersRequest request,
StreamObserver<User> responseObserver) {
userService.findAll().forEach(responseObserver::onNext);
responseObserver.onCompleted();
}
}
// gRPC клиент
@Component
public class UserGrpcClient {
private final UserServiceBlockingStub userStub;
public User getUser(Long id) {
return userStub.getUser(
GetUserRequest.newBuilder()
.setId(id)
.build());
}
}
Преимущества gRPC:
- Высокая производительность (бинарный протокол)
- Типобезопасность
- Streaming поддержка
- Кодогенерация из .proto файлов
- HTTP/2 мультиплексирование
Недостатки:
- Сложнее отладка
- Меньше поддержки браузеров
- Dependency на protobuf
Выбор между REST и gRPC
REST выбираем когда:
- Публичные API
- Веб-интерфейсы
- Простые CRUD операции
- Нужна совместимость с браузерами
gRPC выбираем когда:
- Внутренние микросервисы
- Высокие требования к производительности
- Нужны стриминговые операции
- Строгая типизация
Асинхронная коммуникация
Message Brokers
Message Broker — посредник для обмена сообщениями между сервисами
Apache Kafka
Kafka — распределённая потоковая платформа для обработки событий в реальном времени.
Основные концепции:
- Topic — категория сообщений
- Partition — сегмент топика для масштабирования
- Producer — отправляет сообщения
- Consumer — получает сообщения
- Consumer Group — группа потребителей для балансировки нагрузки
// Kafka Producer
@Service
public class OrderEventProducer {
private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
public void publishOrderCreated(OrderEvent event) {
kafkaTemplate.send("order-events", event.getOrderId(), event);
}
}
// Kafka Consumer
@KafkaListener(topics = "order-events", groupId = "notification-service")
public void handleOrderEvent(OrderEvent event) {
switch (event.getType()) {
case ORDER_CREATED:
notificationService.sendOrderConfirmation(event);
break;
case ORDER_SHIPPED:
notificationService.sendShippingNotification(event);
break;
}
}
// Конфигурация
@Configuration
@EnableKafka
public class KafkaConfig {
@Bean
public ProducerFactory<String, OrderEvent> producerFactory() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
return new DefaultKafkaProducerFactory<>(props);
}
}
Преимущества Kafka:
- Высокая пропускная способность
- Горизонтальное масштабирование
- Fault tolerance
- Replay сообщений
- Streaming обработка
Недостатки:
- Сложность настройки
- Потребляет много ресурсов
- Eventual consistency
RabbitMQ
RabbitMQ — message broker на основе AMQP протокола.
Основные концепции:
- Queue — очередь сообщений
- Exchange — маршрутизатор сообщений
- Routing Key — ключ для маршрутизации
- Binding — связь между exchange и queue
// RabbitMQ Producer
@Component
public class OrderEventPublisher {
private final RabbitTemplate rabbitTemplate;
public void publishOrderCreated(OrderEvent event) {
rabbitTemplate.convertAndSend(
"order.exchange",
"order.created",
event);
}
}
// RabbitMQ Consumer
@RabbitListener(queues = "notification.queue")
public void handleOrderEvent(OrderEvent event) {
notificationService.processOrderEvent(event);
}
// Конфигурация
@Configuration
@EnableRabbit
public class RabbitConfig {
@Bean
public TopicExchange orderExchange() {
return new TopicExchange("order.exchange");
}
@Bean
public Queue notificationQueue() {
return QueueBuilder.durable("notification.queue").build();
}
@Bean
public Binding notificationBinding() {
return BindingBuilder
.bind(notificationQueue())
.to(orderExchange())
.with("order.*");
}
}
Преимущества RabbitMQ:
- Простота настройки
- Гибкая маршрутизация
- Управляющий интерфейс
- Поддержка различных протоколов
Недостатки:
- Меньшая производительность чем Kafka
- Сложность масштабирования
- Single point of failure
Kafka vs RabbitMQ
Kafka выбираем для:
- Event streaming
- Высокая пропускная способность
- Event sourcing
- Real-time analytics
RabbitMQ выбираем для:
- Традиционные очереди
- Сложная маршрутизация
- Гарантии доставки
- Простота использования
Паттерны событийной архитектуры
Event Sourcing
Event Sourcing — храним состояние как последовательность событий.
// Событие
public class OrderCreatedEvent {
private String orderId;
private String customerId;
private BigDecimal amount;
private Instant timestamp;
}
// Aggregate
public class Order {
private String orderId;
private OrderStatus status;
private List<OrderItem> items;
// Применение события
public void apply(OrderCreatedEvent event) {
this.orderId = event.getOrderId();
this.status = OrderStatus.CREATED;
// ... другие поля
}
// Восстановление из событий
public static Order fromEvents(List<OrderEvent> events) {
Order order = new Order();
events.forEach(order::apply);
return order;
}
}
CQRS (Command Query Responsibility Segregation)
CQRS — разделение операций чтения и записи.
// Command Side
@Service
public class OrderCommandService {
public void createOrder(CreateOrderCommand command) {
// Валидация и бизнес-логика
OrderCreatedEvent event = new OrderCreatedEvent(command);
eventStore.save(event);
eventPublisher.publish(event);
}
}
// Query Side
@Service
public class OrderQueryService {
public OrderView getOrder(String orderId) {
return orderReadModel.findById(orderId);
}
@EventHandler
public void on(OrderCreatedEvent event) {
// Обновление read model
OrderView view = new OrderView(event);
orderReadModel.save(view);
}
}
Saga Pattern
Saga — управление распределёнными транзакциями через последовательность локальных транзакций.
// Orchestrator Saga
@Component
public class OrderSaga {
@SagaOrchestrationStart
public void handle(OrderCreatedEvent event) {
// Резервирование товара
commandGateway.send(new ReserveInventoryCommand(event.getOrderId()));
}
@SagaOrchestrationHandles
public void handle(InventoryReservedEvent event) {
// Списание средств
commandGateway.send(new ChargePaymentCommand(event.getOrderId()));
}
@SagaOrchestrationHandles
public void handle(PaymentFailedEvent event) {
// Компенсация: отмена резерва
commandGateway.send(new CancelReservationCommand(event.getOrderId()));
}
}
API Gateway
API Gateway — единая точка входа для всех клиентских запросов к микросервисам.
Основные функции:
- Routing — маршрутизация запросов
- Load Balancing — балансировка нагрузки
- Authentication — аутентификация и авторизация
- Rate Limiting — ограничение скорости запросов
- Request/Response Transformation — преобразование данных
- Caching — кэширование ответов
- Monitoring — мониторинг и логирование
// Spring Cloud Gateway
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r.path("/api/users/**")
.filters(f -> f
.addRequestHeader("X-Service", "user-service")
.circuitBreaker(config -> config.setName("user-service"))
.retry(retryConfig -> retryConfig.setRetries(3)))
.uri("lb://user-service"))
.route("order-service", r -> r.path("/api/orders/**")
.filters(f -> f
.addRequestHeader("X-Service", "order-service")
.requestRateLimiter(config -> config
.setRateLimiter(redisRateLimiter())
.setKeyResolver(userKeyResolver())))
.uri("lb://order-service"))
.build();
}
@Bean
public RedisRateLimiter redisRateLimiter() {
return new RedisRateLimiter(10, 20); // 10 req/sec, burst 20
}
}
Backend for Frontend (BFF)
BFF — отдельный API для каждого типа клиента (web, mobile, desktop).
// Web BFF
@RestController
@RequestMapping("/api/web")
public class WebBffController {
@GetMapping("/dashboard")
public DashboardResponse getDashboard() {
// Агрегация данных из множества сервисов
CompletableFuture<User> userFuture =
userService.getCurrentUser();
CompletableFuture<List<Order>> ordersFuture =
orderService.getRecentOrders();
CompletableFuture<List<Notification>> notificationsFuture =
notificationService.getUnreadNotifications();
return CompletableFuture.allOf(userFuture, ordersFuture, notificationsFuture)
.thenApply(v -> new DashboardResponse(
userFuture.join(),
ordersFuture.join(),
notificationsFuture.join()
)).join();
}
}
// Mobile BFF
@RestController
@RequestMapping("/api/mobile")
public class MobileBffController {
@GetMapping("/dashboard")
public MobileDashboardResponse getDashboard() {
// Упрощённые данные для мобильного клиента
User user = userService.getCurrentUser().join();
List<Order> orders = orderService.getRecentOrders(5).join();
return new MobileDashboardResponse(
user.getName(),
orders.size(),
orders.stream()
.filter(order -> order.getStatus() == OrderStatus.PENDING)
.count()
);
}
}
Service Mesh
Service Mesh — инфраструктурный слой для коммуникации между сервисами.
Основные компоненты:
- Data Plane — прокси (Envoy) рядом с каждым сервисом
- Control Plane — управление конфигурацией и политиками
Istio
Istio — популярная Service Mesh платформа.
Основные возможности:
- Traffic Management — маршрутизация и балансировка
- Security — mTLS, политики безопасности
- Observability — метрики, трейсинг, логирование
- Policy Enforcement — контроль доступа
# Istio VirtualService
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: user-service
spec:
hosts:
- user-service
http:
- match:
- headers:
version:
exact: v2
route:
- destination:
host: user-service
subset: v2
weight: 100
- route:
- destination:
host: user-service
subset: v1
weight: 100
---
# Istio DestinationRule
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: user-service
spec:
host: user-service
trafficPolicy:
circuitBreaker:
consecutiveErrors: 5
interval: 30s
baseEjectionTime: 30s
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
Linkerd
Linkerd — легковесная Service Mesh.
Преимущества:
- Простота установки
- Низкий overhead
- Автоматический mTLS
- Встроенная dashboard
# Установка Linkerd
curl -sL https://run.linkerd.io/install | sh
linkerd check --pre
linkerd install | kubectl apply -f -
# Добавление сервиса в mesh
kubectl get deploy user-service -o yaml | linkerd inject - | kubectl apply -f -
Service Mesh vs API Gateway
Service Mesh для:
- Коммуникация между сервисами
- Security между сервисами
- Observability внутри кластера
- Traffic management
API Gateway для:
- Внешние клиенты
- Аутентификация пользователей
- Rate limiting
- Request/Response transformation
Обработка ошибок и устойчивость
Circuit Breaker
Circuit Breaker — защита от каскадных сбоев.
@Component
public class UserServiceClient {
@CircuitBreaker(name = "user-service", fallbackMethod = "fallbackGetUser")
@Retry(name = "user-service")
@TimeLimiter(name = "user-service")
public CompletableFuture<User> getUser(Long id) {
return CompletableFuture.supplyAsync(() ->
restTemplate.getForObject("/users/{id}", User.class, id));
}
public CompletableFuture<User> fallbackGetUser(Long id, Exception ex) {
return CompletableFuture.completedFuture(
User.builder().id(id).name("Unknown").build());
}
}
Timeout и Retry
// Конфигурация Resilience4j
resilience4j:
circuitbreaker:
instances:
user-service:
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 30s
retry:
instances:
user-service:
maxAttempts: 3
waitDuration: 500ms
exponentialBackoffMultiplier: 2
timelimiter:
instances:
user-service:
timeoutDuration: 2s
Лучшие практики
Идемпотентность
Идемпотентность — операция даёт одинаковый результат при повторном выполнении.
@PostMapping("/orders")
public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request,
@RequestHeader("Idempotency-Key") String key) {
// Проверка дубликата
Order existingOrder = orderService.findByIdempotencyKey(key);
if (existingOrder != null) {
return ResponseEntity.ok(existingOrder);
}
Order order = orderService.createOrder(request, key);
return ResponseEntity.status(HttpStatus.CREATED).body(order);
}
Versioning
// URL versioning
@GetMapping("/v1/users/{id}")
public UserV1 getUserV1(@PathVariable Long id) { }
@GetMapping("/v2/users/{id}")
public UserV2 getUserV2(@PathVariable Long id) { }
// Header versioning
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id,
@RequestHeader(value = "API-Version", defaultValue = "1") String version) {
if ("2".equals(version)) {
return ResponseEntity.ok(userService.getUserV2(id));
}
return ResponseEntity.ok(userService.getUserV1(id));
}
Мониторинг и логирование
@RestController
public class OrderController {
@GetMapping("/orders/{id}")
@Timed(name = "order.get", description = "Time taken to get order")
public Order getOrder(@PathVariable Long id) {
log.info("Getting order with id: {}", id);
Order order = orderService.findById(id);
if (order == null) {
log.warn("Order not found: {}", id);
throw new OrderNotFoundException(id);
}
log.info("Order found: {}", order.getId());
return order;
}
}
Вопросы для собеседования
- Когда использовать синхронную, а когда асинхронную коммуникацию?
- Чем отличается Kafka от RabbitMQ?
- Что такое Event Sourcing и когда его применять?
- Различия между API Gateway и Service Mesh?
- Как обеспечить идемпотентность в REST API?
- Паттерны для обработки ошибок в микросервисах?
- Как реализовать распределённые транзакции?
- Стратегии версионирования API?
- Что такое Circuit Breaker и как он работает?
- Как организовать мониторинг межсервисной коммуникации?
Управление согласованностью
Основные проблемы согласованности
ACID vs BASE
ACID (Atomicity, Consistency, Isolation, Durability) — принципы для традиционных баз данных BASE (Basically Available, Soft state, Eventual consistency) — принципы для распределённых систем
В микросервисной архитектуре распределённые транзакции сложны и часто невозможны из-за:
- Независимых баз данных каждого сервиса
- Сетевых задержек и сбоев
- Необходимости высокой доступности
CAP теорема
CAP теорема — в распределённой системе можно гарантировать только 2 из 3 свойств:
- Consistency — все узлы видят одинаковые данные
- Availability — система остаётся доступной
- Partition tolerance — система работает при сетевых разделениях
Микросервисы обычно выбирают AP (доступность + устойчивость к разделениям).
Паттерн Saga
Saga — последовательность локальных транзакций, где каждая транзакция обновляет данные и публикует событие. Если одна транзакция терпит неудачу, выполняются компенсирующие транзакции.
Choreography Saga
Choreography — каждый сервис знает, какие события слушать и какие действия выполнять. Нет центрального координатора.
// Сценарий: Заказ → Оплата → Доставка
// 1. Order Service
@Service
public class OrderService {
@Transactional
public void createOrder(CreateOrderCommand command) {
// Создаём заказ
Order order = new Order(command);
orderRepository.save(order);
// Публикуем событие
OrderCreatedEvent event = new OrderCreatedEvent(
order.getId(),
order.getCustomerId(),
order.getTotalAmount()
);
eventPublisher.publish(event);
}
// Обработка события успешной оплаты
@EventListener
@Transactional
public void handle(PaymentProcessedEvent event) {
Order order = orderRepository.findById(event.getOrderId());
order.markAsPaid();
orderRepository.save(order);
// Публикуем событие для доставки
OrderPaidEvent orderPaidEvent = new OrderPaidEvent(
order.getId(),
order.getShippingAddress()
);
eventPublisher.publish(orderPaidEvent);
}
// Компенсация при неудачной оплате
@EventListener
@Transactional
public void handle(PaymentFailedEvent event) {
Order order = orderRepository.findById(event.getOrderId());
order.cancel("Payment failed");
orderRepository.save(order);
OrderCancelledEvent cancelEvent = new OrderCancelledEvent(
order.getId(),
"Payment failed"
);
eventPublisher.publish(cancelEvent);
}
}
// 2. Payment Service
@Service
public class PaymentService {
@EventListener
@Transactional
public void handle(OrderCreatedEvent event) {
try {
// Обработка платежа
Payment payment = processPayment(
event.getCustomerId(),
event.getAmount()
);
PaymentProcessedEvent successEvent = new PaymentProcessedEvent(
event.getOrderId(),
payment.getId()
);
eventPublisher.publish(successEvent);
} catch (PaymentException e) {
PaymentFailedEvent failEvent = new PaymentFailedEvent(
event.getOrderId(),
e.getMessage()
);
eventPublisher.publish(failEvent);
}
}
// Компенсация при отмене заказа
@EventListener
@Transactional
public void handle(OrderCancelledEvent event) {
Payment payment = paymentRepository.findByOrderId(event.getOrderId());
if (payment != null && payment.getStatus() == PaymentStatus.COMPLETED) {
refundPayment(payment);
PaymentRefundedEvent refundEvent = new PaymentRefundedEvent(
payment.getId(),
payment.getAmount()
);
eventPublisher.publish(refundEvent);
}
}
}
// 3. Shipping Service
@Service
public class ShippingService {
@EventListener
@Transactional
public void handle(OrderPaidEvent event) {
try {
// Создание отгрузки
Shipment shipment = createShipment(
event.getOrderId(),
event.getShippingAddress()
);
ShipmentCreatedEvent shipmentEvent = new ShipmentCreatedEvent(
event.getOrderId(),
shipment.getId()
);
eventPublisher.publish(shipmentEvent);
} catch (ShippingException e) {
ShipmentFailedEvent failEvent = new ShipmentFailedEvent(
event.getOrderId(),
e.getMessage()
);
eventPublisher.publish(failEvent);
}
}
}
Преимущества Choreography:
- Слабая связанность сервисов
- Высокая производительность
- Простая реализация для простых сценариев
Недостатки:
- Сложно отслеживать flow
- Циклические зависимости
- Сложная отладка
Orchestration Saga
Orchestration — центральный координатор (оркестратор) управляет всем процессом Saga.
// Saga Orchestrator
@Component
public class OrderSagaOrchestrator {
private final PaymentService paymentService;
private final ShippingService shippingService;
private final OrderService orderService;
@SagaOrchestrationStart
public void processOrder(OrderCreatedEvent event) {
SagaTransaction saga = SagaTransaction.builder()
.sagaId(UUID.randomUUID().toString())
.orderId(event.getOrderId())
.build();
// Шаг 1: Обработка платежа
try {
PaymentResult paymentResult = paymentService.processPayment(
event.getCustomerId(),
event.getAmount()
);
saga.addCompletedStep("PAYMENT", paymentResult);
processShipping(saga, event);
} catch (PaymentException e) {
compensateOrder(saga, e.getMessage());
}
}
private void processShipping(SagaTransaction saga, OrderCreatedEvent event) {
// Шаг 2: Создание отгрузки
try {
ShipmentResult shipmentResult = shippingService.createShipment(
event.getOrderId(),
event.getShippingAddress()
);
saga.addCompletedStep("SHIPPING", shipmentResult);
completeSaga(saga);
} catch (ShippingException e) {
compensatePayment(saga);
compensateOrder(saga, e.getMessage());
}
}
private void compensatePayment(SagaTransaction saga) {
PaymentResult paymentResult = saga.getStepResult("PAYMENT");
if (paymentResult != null) {
paymentService.refundPayment(paymentResult.getPaymentId());
}
}
private void compensateOrder(SagaTransaction saga, String reason) {
orderService.cancelOrder(saga.getOrderId(), reason);
}
private void completeSaga(SagaTransaction saga) {
orderService.completeOrder(saga.getOrderId());
sagaRepository.markCompleted(saga.getSagaId());
}
}
// Saga Transaction State
@Entity
public class SagaTransaction {
private String sagaId;
private String orderId;
private SagaStatus status;
private Map<String, Object> stepResults;
private List<String> completedSteps;
private List<String> compensatedSteps;
public void addCompletedStep(String stepName, Object result) {
completedSteps.add(stepName);
stepResults.put(stepName, result);
}
public <T> T getStepResult(String stepName) {
return (T) stepResults.get(stepName);
}
}
// Axon Framework Saga пример
@Saga
public class OrderSaga {
@SagaStart
@SagaHandler
public void handle(OrderCreatedEvent event) {
ProcessPaymentCommand paymentCommand = new ProcessPaymentCommand(
event.getOrderId(),
event.getCustomerId(),
event.getAmount()
);
commandGateway.send(paymentCommand);
}
@SagaHandler
public void handle(PaymentProcessedEvent event) {
CreateShipmentCommand shipmentCommand = new CreateShipmentCommand(
event.getOrderId(),
event.getShippingAddress()
);
commandGateway.send(shipmentCommand);
}
@SagaHandler
public void handle(PaymentFailedEvent event) {
CancelOrderCommand cancelCommand = new CancelOrderCommand(
event.getOrderId(),
"Payment failed"
);
commandGateway.send(cancelCommand);
}
@SagaHandler
public void handle(ShipmentFailedEvent event) {
// Компенсация платежа
RefundPaymentCommand refundCommand = new RefundPaymentCommand(
event.getOrderId()
);
commandGateway.send(refundCommand);
// Отмена заказа
CancelOrderCommand cancelCommand = new CancelOrderCommand(
event.getOrderId(),
"Shipping failed"
);
commandGateway.send(cancelCommand);
}
}
Преимущества Orchestration:
- Централизованная логика
- Легко отслеживать состояние
- Простая отладка
- Явное управление компенсацией
Недостатки:
- Центральная точка отказа
- Больше связанности
- Дополнительная сложность
Выбор между Choreography и Orchestration
Choreography выбираем когда:
- Простые бизнес-процессы
- Высокие требования к производительности
- Нужна слабая связанность
Orchestration выбираем когда:
- Сложная бизнес-логика
- Нужен контроль и мониторинг
- Частые изменения процесса
Transaction Outbox Pattern
Transaction Outbox — обеспечивает атомарность между изменением состояния и публикацией события.
Проблема
// Проблемный код - не атомарен
@Transactional
public void createOrder(CreateOrderCommand command) {
// 1. Сохраняем в БД
Order order = new Order(command);
orderRepository.save(order);
// 2. Публикуем событие (может упасть после коммита БД!)
eventPublisher.publish(new OrderCreatedEvent(order));
}
Решение с Outbox
// Outbox Table
@Entity
public class OutboxEvent {
@Id
private String id;
private String aggregateType;
private String aggregateId;
private String eventType;
private String eventData;
private Instant createdAt;
private boolean processed;
}
// Service с Outbox
@Service
public class OrderService {
@Transactional
public void createOrder(CreateOrderCommand command) {
// 1. Сохраняем заказ
Order order = new Order(command);
orderRepository.save(order);
// 2. Сохраняем событие в Outbox (в той же транзакции!)
OutboxEvent outboxEvent = OutboxEvent.builder()
.id(UUID.randomUUID().toString())
.aggregateType("Order")
.aggregateId(order.getId())
.eventType("OrderCreated")
.eventData(objectMapper.writeValueAsString(
new OrderCreatedEvent(order)))
.createdAt(Instant.now())
.processed(false)
.build();
outboxRepository.save(outboxEvent);
}
}
// Polling Publisher
@Component
public class OutboxEventPublisher {
@Scheduled(fixedDelay = 5000) // каждые 5 секунд
@Transactional
public void publishOutboxEvents() {
List<OutboxEvent> unpublishedEvents = outboxRepository
.findByProcessedFalseOrderByCreatedAt(PageRequest.of(0, 100));
for (OutboxEvent event : unpublishedEvents) {
try {
// Публикуем событие
Object eventObject = objectMapper.readValue(
event.getEventData(),
getEventClass(event.getEventType())
);
eventPublisher.publish(eventObject);
// Отмечаем как опубликованное
event.setProcessed(true);
outboxRepository.save(event);
} catch (Exception e) {
log.error("Failed to publish outbox event: {}", event.getId(), e);
// Можно добавить retry логику или DLQ
}
}
}
private Class<?> getEventClass(String eventType) {
return switch (eventType) {
case "OrderCreated" -> OrderCreatedEvent.class;
case "OrderCancelled" -> OrderCancelledEvent.class;
default -> throw new IllegalArgumentException("Unknown event type: " + eventType);
};
}
}
CDC (Change Data Capture) подход
// Debezium connector configuration
{
"name": "order-service-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "postgres",
"database.port": "5432",
"database.user": "order_user",
"database.password": "password",
"database.dbname": "order_db",
"database.server.name": "order-server",
"table.whitelist": "public.outbox_events",
"transforms": "outbox",
"transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter",
"transforms.outbox.route.topic.replacement": "order.events.${routedByValue}"
}
}
// Outbox Event для CDC
@Entity
@Table(name = "outbox_events")
public class OutboxEvent {
@Id
private String id;
@Column(name = "aggregate_type")
private String aggregateType;
@Column(name = "aggregate_id")
private String aggregateId;
@Column(name = "event_type")
private String type; // Debezium использует это поле
@Column(name = "event_data")
private String payload; // Debezium использует это поле
@Column(name = "created_at")
private Instant createdAt;
}
Преимущества CDC:
- Низкая латентность
- Автоматическая публикация
- Не нужен polling
Недостатки:
- Сложность настройки
- Привязка к конкретной БД
- Дополнительная инфраструктура
Eventual Consistency
Eventual Consistency — система достигнет согласованности через некоторое время, но не немедленно.
Стратегии обработки
// Read-your-writes consistency
@Service
public class OrderService {
public Order createOrder(CreateOrderCommand command) {
Order order = new Order(command);
orderRepository.save(order);
// Кэшируем для немедленного чтения
orderCache.put(order.getId(), order);
// Асинхронно публикуем событие
eventPublisher.publishAsync(new OrderCreatedEvent(order));
return order;
}
public Order getOrder(String orderId) {
// Сначала проверяем кэш
Order cached = orderCache.get(orderId);
if (cached != null) {
return cached;
}
return orderRepository.findById(orderId);
}
}
// Версионирование для конфликтов
@Entity
public class Order {
@Id
private String id;
@Version
private Long version;
private OrderStatus status;
// Optimistic locking при обновлении
public void updateStatus(OrderStatus newStatus, Long expectedVersion) {
if (!this.version.equals(expectedVersion)) {
throw new OptimisticLockException(
"Order was modified by another process");
}
this.status = newStatus;
}
}
// Compensating action при конфликтах
@Service
public class OrderEventHandler {
@EventListener
public void handle(PaymentProcessedEvent event) {
try {
Order order = orderService.findById(event.getOrderId());
if (order.getStatus() == OrderStatus.CANCELLED) {
// Заказ уже отменён, компенсируем платёж
paymentService.refundPayment(event.getPaymentId());
return;
}
order.markAsPaid();
orderService.save(order);
} catch (OptimisticLockException e) {
// Retry с exponential backoff
retryTemplate.execute(context -> {
return this.handle(event);
});
}
}
}
Monitoring Eventual Consistency
// Метрики для отслеживания задержек
@Component
public class ConsistencyMetrics {
private final MeterRegistry meterRegistry;
private final Timer consistencyLag;
public ConsistencyMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.consistencyLag = Timer.builder("consistency.lag")
.description("Time between event and consistency achievement")
.register(meterRegistry);
}
@EventListener
public void measureConsistencyLag(OrderCreatedEvent event) {
Timer.Sample sample = Timer.start(meterRegistry);
// Ждём пока все сервисы обработают событие
CompletableFuture.allOf(
waitForPaymentService(event.getOrderId()),
waitForInventoryService(event.getOrderId()),
waitForShippingService(event.getOrderId())
).thenRun(() -> {
sample.stop(consistencyLag);
});
}
}
Compensating Transactions
Compensating Transaction — операция, которая логически отменяет эффект предыдущей транзакции.
Идемпотентность компенсаций
// Компенсирующие операции должны быть идемпотентными
@Service
public class PaymentCompensationService {
public void compensatePayment(String orderId, String reason) {
Payment payment = paymentRepository.findByOrderId(orderId);
if (payment == null) {
// Платёж не найден - уже компенсирован или не создавался
log.info("Payment not found for order {}, skipping compensation", orderId);
return;
}
if (payment.getStatus() == PaymentStatus.REFUNDED) {
// Уже возвращён - идемпотентность
log.info("Payment {} already refunded", payment.getId());
return;
}
if (payment.getStatus() != PaymentStatus.COMPLETED) {
// Нельзя вернуть незавершённый платёж
throw new IllegalStateException(
"Cannot refund payment in status: " + payment.getStatus());
}
// Выполняем возврат
RefundResult result = paymentGateway.refund(
payment.getTransactionId(),
payment.getAmount()
);
payment.refund(result.getRefundId(), reason);
paymentRepository.save(payment);
// Публикуем событие о возврате
eventPublisher.publish(new PaymentRefundedEvent(
payment.getId(),
orderId,
payment.getAmount()
));
}
}
// Semantic Lock для предотвращения параллельных компенсаций
@Service
public class OrderCompensationService {
public void compensateOrder(String orderId, String reason) {
// Семантический лок
boolean lockAcquired = distributedLockService.tryLock(
"order-compensation:" + orderId,
Duration.ofMinutes(5)
);
if (!lockAcquired) {
log.warn("Failed to acquire compensation lock for order {}", orderId);
throw new CompensationLockException("Another compensation in progress");
}
try {
Order order = orderRepository.findById(orderId);
if (order.getStatus() == OrderStatus.CANCELLED) {
// Уже отменён
return;
}
// Компенсирующие действия в обратном порядке
compensateShipping(orderId);
compensatePayment(orderId, reason);
order.cancel(reason);
orderRepository.save(order);
} finally {
distributedLockService.releaseLock("order-compensation:" + orderId);
}
}
}
Compensation Patterns
// Command Pattern для компенсаций
public interface CompensationCommand {
void execute();
boolean canCompensate();
String getDescription();
}
public class RefundPaymentCompensation implements CompensationCommand {
private final String paymentId;
private final PaymentService paymentService;
@Override
public void execute() {
paymentService.refundPayment(paymentId);
}
@Override
public boolean canCompensate() {
Payment payment = paymentService.findById(paymentId);
return payment != null &&
payment.getStatus() == PaymentStatus.COMPLETED;
}
}
// Compensation Manager
@Service
public class CompensationManager {
public void executeCompensations(List<CompensationCommand> compensations) {
// Выполняем компенсации в обратном порядке
Collections.reverse(compensations);
for (CompensationCommand compensation : compensations) {
try {
if (compensation.canCompensate()) {
compensation.execute();
log.info("Executed compensation: {}",
compensation.getDescription());
} else {
log.warn("Skipped compensation: {}",
compensation.getDescription());
}
} catch (Exception e) {
log.error("Failed to execute compensation: {}",
compensation.getDescription(), e);
// Решение: continue, retry, или fail-fast
}
}
}
}
Практические рекомендации
Мониторинг Saga
@Component
public class SagaMonitoring {
// Метрики для Saga
private final Counter sagaStarted;
private final Counter sagaCompleted;
private final Counter sagaFailed;
private final Timer sagaDuration;
@EventListener
public void onSagaStarted(SagaStartedEvent event) {
sagaStarted.increment(
Tags.of("saga.type", event.getSagaType())
);
}
@EventListener
public void onSagaCompleted(SagaCompletedEvent event) {
sagaCompleted.increment(
Tags.of("saga.type", event.getSagaType())
);
Duration duration = Duration.between(
event.getStartTime(),
event.getEndTime()
);
sagaDuration.record(duration);
}
@EventListener
public void onSagaFailed(SagaFailedEvent event) {
sagaFailed.increment(
Tags.of(
"saga.type", event.getSagaType(),
"failure.reason", event.getFailureReason()
)
);
}
}
Debugging и Observability
// Correlation ID для трассировки
@Component
public class SagaTracing {
public void startSaga(String sagaType, String businessKey) {
String correlationId = UUID.randomUUID().toString();
MDC.put("correlationId", correlationId);
MDC.put("sagaType", sagaType);
MDC.put("businessKey", businessKey);
log.info("Starting saga: type={}, businessKey={}", sagaType, businessKey);
}
public void logSagaStep(String stepName, String status) {
log.info("Saga step: name={}, status={}", stepName, status);
}
}
// Health checks для Saga состояния
@Component
public class SagaHealthIndicator implements HealthIndicator {
@Override
public Health health() {
long runningTime = sagaRepository.countByStatusAndCreatedAtBefore(
SagaStatus.RUNNING,
Instant.now().minus(1, ChronoUnit.HOURS)
);
if (runningTime > 10) {
return Health.down()
.withDetail("stuck.sagas", runningTime)
.build();
}
return Health.up()
.withDetail("running.sagas", runningTime)
.build();
}
}
Антипаттерны
Избегайте
- Distributed Transactions (2PC) — блокирующие, не масштабируемые
- Saga без компенсаций — нет возможности откатиться
- Синхронные Saga — увеличивают латентность
- Забытые timeout'ы — зависшие транзакции
- Игнорирование идемпотентности — дублирование операций
// ПЛОХО - синхронная Saga
public void processOrderSync(OrderCreatedEvent event) {
// Блокирующие вызовы
PaymentResult payment = paymentService.processPayment(event);
ShipmentResult shipment = shippingService.createShipment(event);
orderService.completeOrder(event.getOrderId());
}
// ХОРОШО - асинхронная Saga
@SagaHandler
public void handle(OrderCreatedEvent event) {
ProcessPaymentCommand command = new ProcessPaymentCommand(event);
commandGateway.send(command);
}
Вопросы для собеседования
- Чем отличается Choreography от Orchestration Saga?
- Как обеспечить атомарность между изменением данных и публикацией события?
- Что такое Transaction Outbox и когда его использовать?
- Как обрабатывать eventual consistency в пользовательском интерфейсе?
- Какие стратегии компенсации знаете?
- Как мониторить состояние Saga?
- Различия между 2PC и Saga?
- Как обеспечить идемпотентность компенсирующих операций?
- Что делать с зависшими Saga?
- Паттерны для обработки конфликтов в eventual consistency?
Надежность и отказоустойчивость
Основные принципы
Типы отказов
Transient failures — временные сбои (сетевые задержки, временная недоступность) Persistent failures — постоянные сбои (неправильная конфигурация, bugs) Cascading failures — каскадные сбои, когда отказ одного компонента вызывает отказ других
Defensive Programming
Fail Fast — быстро обнаруживать и сообщать об ошибках Fail Safe — продолжать работу в безопасном режиме при сбоях Graceful Degradation — постепенное ухудшение функциональности вместо полного отказа
Circuit Breaker
Circuit Breaker — паттерн защиты от каскадных сбоев, имитирующий электрический автоматический выключатель.
Состояния Circuit Breaker
- CLOSED — нормальная работа, запросы проходят
- OPEN — слишком много ошибок, запросы блокируются
- HALF_OPEN — тестовые запросы для проверки восстановления
CLOSED ──(failures > threshold)──> OPEN
↑ ↓
└──(success)── HALF_OPEN ←──(timeout)
Resilience4j Implementation
Resilience4j — библиотека для построения отказоустойчивых приложений, successor Hystrix.
// Конфигурация
@Configuration
public class CircuitBreakerConfig {
@Bean
public CircuitBreaker userServiceCircuitBreaker() {
return CircuitBreaker.of("user-service",
io.github.resilience4j.circuitbreaker.CircuitBreakerConfig.custom()
.slidingWindowSize(10) // Окно для анализа (10 запросов)
.failureRateThreshold(50) // 50% ошибок = OPEN
.waitDurationInOpenState(Duration.ofSeconds(30)) // Время в OPEN
.minimumNumberOfCalls(5) // Минимум вызовов для анализа
.permittedNumberOfCallsInHalfOpenState(3) // Тестовых запросов в HALF_OPEN
.automaticTransitionFromOpenToHalfOpenEnabled(true)
.build());
}
}
// Использование через аннотации
@Service
public class UserService {
@CircuitBreaker(name = "user-service", fallbackMethod = "fallbackGetUser")
@TimeLimiter(name = "user-service")
@Retry(name = "user-service")
public CompletableFuture<User> getUser(Long userId) {
return CompletableFuture.supplyAsync(() -> {
// Вызов внешнего сервиса
return restTemplate.getForObject(
"/users/{id}", User.class, userId);
});
}
// Fallback method - должен иметь тот же signature + Exception
public CompletableFuture<User> fallbackGetUser(Long userId, Exception ex) {
return CompletableFuture.completedFuture(
User.builder()
.id(userId)
.name("Unknown User")
.email("unknown@example.com")
.build());
}
}
// Программное использование
@Service
public class OrderService {
private final CircuitBreaker circuitBreaker;
public Order getOrder(Long orderId) {
Supplier<Order> decoratedSupplier = CircuitBreaker
.decorateSupplier(circuitBreaker, () -> {
return orderClient.getOrder(orderId);
});
return Try.ofSupplier(decoratedSupplier)
.recover(throwable -> {
log.error("Circuit breaker fallback for order {}", orderId);
return Order.builder()
.id(orderId)
.status(OrderStatus.UNKNOWN)
.build();
})
.get();
}
}
Spring Boot конфигурация
# application.yml
resilience4j:
circuitbreaker:
instances:
user-service:
sliding-window-size: 10
failure-rate-threshold: 50
wait-duration-in-open-state: 30s
minimum-number-of-calls: 5
permitted-number-of-calls-in-half-open-state: 3
automatic-transition-from-open-to-half-open-enabled: true
record-exceptions:
- java.net.ConnectException
- java.net.SocketTimeoutException
ignore-exceptions:
- java.lang.IllegalArgumentException
timelimiter:
instances:
user-service:
timeout-duration: 2s
cancel-running-future: true
retry:
instances:
user-service:
max-attempts: 3
wait-duration: 500ms
exponential-backoff-multiplier: 2
Мониторинг Circuit Breaker
@Component
public class CircuitBreakerMetrics {
@EventListener
public void onCircuitBreakerStateTransition(CircuitBreakerOnStateTransitionEvent event) {
log.info("Circuit breaker {} transitioned from {} to {}",
event.getCircuitBreakerName(),
event.getStateTransition().getFromState(),
event.getStateTransition().getToState());
// Метрики для мониторинга
Metrics.counter("circuit.breaker.state.transition",
"name", event.getCircuitBreakerName(),
"from", event.getStateTransition().getFromState().name(),
"to", event.getStateTransition().getToState().name())
.increment();
}
@EventListener
public void onCircuitBreakerCallNotPermitted(CircuitBreakerOnCallNotPermittedEvent event) {
log.warn("Circuit breaker {} rejected call", event.getCircuitBreakerName());
Metrics.counter("circuit.breaker.calls.rejected",
"name", event.getCircuitBreakerName())
.increment();
}
}
Retry with Backoff
Retry — повторение неудавшихся операций с интеллектуальными стратегиями ожидания.
Стратегии Backoff
Fixed Delay — фиксированная задержка между попытками Exponential Backoff — экспоненциальное увеличение задержки Linear Backoff — линейное увеличение задержки Random Jitter — добавление случайности для избежания thundering herd
// Различные стратегии retry
@Configuration
public class RetryConfig {
// Exponential backoff с jitter
@Bean
public RetryTemplate exponentialRetryTemplate() {
return RetryTemplate.builder()
.maxAttempts(3)
.exponentialBackoff(1000, 2, 10000) // start: 1s, multiplier: 2, max: 10s
.retryOn(ConnectException.class)
.retryOn(SocketTimeoutException.class)
.build();
}
// Fixed delay с jitter
@Bean
public RetryTemplate fixedRetryTemplate() {
return RetryTemplate.builder()
.maxAttempts(5)
.fixedBackoff(2000) // 2 секунды между попытками
.retryOn(TransientException.class)
.build();
}
}
// Resilience4j Retry
@Service
public class PaymentService {
@Retry(name = "payment-gateway")
public PaymentResult processPayment(PaymentRequest request) {
try {
return paymentGateway.charge(request);
} catch (PaymentException e) {
log.warn("Payment attempt failed: {}", e.getMessage());
throw e; // Retry будет перехвачен аннотацией
}
}
// Программная конфигурация retry
private final Retry retry = Retry.of("payment-service",
RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofSeconds(1))
.exponentialBackoffMultiplier(2.0)
.retryOnException(throwable ->
throwable instanceof ConnectException ||
throwable instanceof SocketTimeoutException)
.build());
public PaymentResult processPaymentWithRetry(PaymentRequest request) {
Supplier<PaymentResult> retryableSupplier = Retry.decorateSupplier(
retry, () -> paymentGateway.charge(request));
return retryableSupplier.get();
}
}
Конфигурация Retry
resilience4j:
retry:
instances:
payment-gateway:
max-attempts: 3
wait-duration: 1s
exponential-backoff-multiplier: 2
retry-exceptions:
- java.net.ConnectException
- java.net.SocketTimeoutException
- com.example.TransientException
ignore-exceptions:
- com.example.BusinessException
database:
max-attempts: 5
wait-duration: 500ms
retry-exceptions:
- org.springframework.dao.DataAccessResourceFailureException
Jitter Implementation
// Добавление jitter для избежания thundering herd
@Component
public class JitteredRetry {
private final Random random = new Random();
public <T> T executeWithJitter(Supplier<T> operation, int maxAttempts) {
Exception lastException = null;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return operation.get();
} catch (Exception e) {
lastException = e;
if (attempt == maxAttempts) {
break;
}
// Exponential backoff с jitter
long baseDelay = (long) Math.pow(2, attempt - 1) * 1000; // 1s, 2s, 4s...
long jitter = random.nextLong(baseDelay / 2); // ±50% jitter
long delay = baseDelay + jitter;
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted during retry", ie);
}
}
}
throw new RuntimeException("All retry attempts failed", lastException);
}
}
Bulkhead Pattern
Bulkhead — изоляция ресурсов для предотвращения полного отказа системы. Название от водонепроницаемых перегородок на кораблях.
Thread Pool Isolation
// Изоляция по thread pools
@Configuration
public class BulkheadConfig {
// Separate thread pool для каждого внешнего сервиса
@Bean("userServiceExecutor")
public Executor userServiceExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
executor.setThreadNamePrefix("UserService-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Bean("orderServiceExecutor")
public Executor orderServiceExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(20);
executor.setThreadNamePrefix("OrderService-");
executor.initialize();
return executor;
}
}
@Service
public class ExternalServiceClient {
// Изолированные вызовы
@Async("userServiceExecutor")
public CompletableFuture<User> getUser(Long userId) {
return CompletableFuture.completedFuture(
userServiceClient.getUser(userId));
}
@Async("orderServiceExecutor")
public CompletableFuture<Order> getOrder(Long orderId) {
return CompletableFuture.completedFuture(
orderServiceClient.getOrder(orderId));
}
}
Resilience4j Bulkhead
// Semaphore-based bulkhead
@Service
public class InventoryService {
@Bulkhead(name = "inventory-service", type = Bulkhead.Type.SEMAPHORE)
public Product checkAvailability(String productId) {
return inventoryClient.getProduct(productId);
}
@Bulkhead(name = "inventory-update", type = Bulkhead.Type.THREAD_POOL)
public CompletableFuture<Void> updateInventory(String productId, int quantity) {
return CompletableFuture.runAsync(() ->
inventoryClient.updateQuantity(productId, quantity));
}
}
Connection Pool Isolation
// Separate connection pools для разных типов операций
@Configuration
public class DataSourceConfig {
// Read-only операции
@Bean
@ConfigurationProperties("spring.datasource.read")
public DataSource readOnlyDataSource() {
HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
config.setConnectionTimeout(5000);
config.setReadOnly(true);
return new HikariDataSource(config);
}
// Write операции
@Bean
@ConfigurationProperties("spring.datasource.write")
public DataSource writeDataSource() {
HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
config.setConnectionTimeout(3000);
return new HikariDataSource(config);
}
// Reporting операции (отдельный pool)
@Bean
@ConfigurationProperties("spring.datasource.reporting")
public DataSource reportingDataSource() {
HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(5);
config.setMinimumIdle(1);
config.setConnectionTimeout(10000);
return new HikariDataSource(config);
}
}
Resource Quotas
// Rate limiting по типам операций
@Component
public class ResourceQuotaManager {
private final RateLimiter apiRateLimiter;
private final RateLimiter reportingRateLimiter;
private final RateLimiter healthCheckRateLimiter;
public ResourceQuotaManager() {
// Разные лимиты для разных типов запросов
this.apiRateLimiter = RateLimiter.create(100.0); // 100 RPS для API
this.reportingRateLimiter = RateLimiter.create(10.0); // 10 RPS для отчётов
this.healthCheckRateLimiter = RateLimiter.create(1000.0); // 1000 RPS для health checks
}
public void checkApiQuota() {
if (!apiRateLimiter.tryAcquire()) {
throw new RateLimitExceededException("API rate limit exceeded");
}
}
public void checkReportingQuota() {
if (!reportingRateLimiter.tryAcquire()) {
throw new RateLimitExceededException("Reporting rate limit exceeded");
}
}
}
Timeouts
Timeouts — ограничение времени ожидания операций для предотвращения зависания.
Типы Timeouts
Connection Timeout — время на установление соединения Read Timeout — время ожидания ответа Request Timeout — общее время обработки запроса
// HTTP Client timeouts
@Configuration
public class HttpClientConfig {
@Bean
public RestTemplate restTemplate() {
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory();
// Connection timeout - время на установление соединения
factory.setConnectionRequestTimeout(5000);
// Socket timeout - время ожидания данных
factory.setReadTimeout(30000);
// Connection timeout
factory.setConnectTimeout(10000);
return new RestTemplate(factory);
}
// WebClient с таймаутами
@Bean
public WebClient webClient() {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
.responseTimeout(Duration.ofSeconds(30))
.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(30))
.addHandlerLast(new WriteTimeoutHandler(30)));
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
}
// Resilience4j TimeLimiter
@Service
public class ExternalApiService {
@TimeLimiter(name = "external-api")
public CompletableFuture<String> callExternalApi(String request) {
return CompletableFuture.supplyAsync(() -> {
// Долгая операция
return externalApiClient.call(request);
});
}
// Программная настройка
private final TimeLimiter timeLimiter = TimeLimiter.of(
Duration.ofSeconds(3)); // 3 секунды timeout
public String callWithTimeout(String request) {
Supplier<String> futureSupplier = () -> {
return CompletableFuture
.supplyAsync(() -> externalApiClient.call(request))
.join();
};
Callable<String> restrictedCall = TimeLimiter
.decorateFutureSupplier(timeLimiter, futureSupplier);
try {
return restrictedCall.call();
} catch (Exception e) {
throw new TimeoutException("External API call timed out", e);
}
}
}
Database Timeouts
// Database connection и query timeouts
@Configuration
public class DatabaseConfig {
@Bean
@ConfigurationProperties("spring.datasource")
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
// Connection timeout
config.setConnectionTimeout(5000); // 5s для получения connection из pool
// Socket timeout
config.addDataSourceProperty("socketTimeout", "30000"); // 30s для query execution
// Connection validation timeout
config.setValidationTimeout(3000); // 3s для проверки connection
// Leak detection threshold
config.setLeakDetectionThreshold(60000); // 60s для обнаружения leak
return new HikariDataSource(config);
}
}
// JPA query timeouts
@Repository
public class UserRepository {
@PersistenceContext
private EntityManager entityManager;
public List<User> findUsersWithTimeout(String criteria) {
return entityManager
.createQuery("SELECT u FROM User u WHERE u.name LIKE :criteria", User.class)
.setParameter("criteria", "%" + criteria + "%")
.setHint("javax.persistence.query.timeout", 10000) // 10 seconds
.getResultList();
}
}
Spring Boot Timeouts
# application.yml
spring:
datasource:
hikari:
connection-timeout: 5000
validation-timeout: 3000
leak-detection-threshold: 60000
server:
# Tomcat timeouts
tomcat:
connection-timeout: 20000
resilience4j:
timelimiter:
instances:
external-api:
timeout-duration: 3s
cancel-running-future: true
database:
timeout-duration: 10s
# Feign timeouts
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 30000
Fallback Strategies
Fallback — альтернативное поведение при отказе основной функциональности.
Типы Fallback
Static Fallback — возврат предопределённых значений Cache Fallback — использование кэшированных данных Service Fallback — переключение на альтернативный сервис Degraded Functionality — ограниченная функциональность
// Различные стратегии fallback
@Service
public class ProductService {
private final ProductCache productCache;
private final BackupProductService backupService;
// 1. Static Fallback
@CircuitBreaker(name = "product-service", fallbackMethod = "staticFallback")
public Product getProduct(String productId) {
return productClient.getProduct(productId);
}
public Product staticFallback(String productId, Exception ex) {
return Product.builder()
.id(productId)
.name("Product temporarily unavailable")
.price(BigDecimal.ZERO)
.available(false)
.build();
}
// 2. Cache Fallback
@CircuitBreaker(name = "product-service", fallbackMethod = "cacheFallback")
public Product getProductWithCache(String productId) {
Product product = productClient.getProduct(productId);
productCache.put(productId, product); // Кэшируем успешный результат
return product;
}
public Product cacheFallback(String productId, Exception ex) {
log.warn("Falling back to cache for product {}", productId);
Product cached = productCache.get(productId);
if (cached != null) {
cached.setStale(true); // Помечаем как устаревшие данные
return cached;
}
return staticFallback(productId, ex);
}
// 3. Service Fallback
@CircuitBreaker(name = "product-service", fallbackMethod = "serviceFallback")
public Product getProductWithBackup(String productId) {
return primaryProductService.getProduct(productId);
}
public Product serviceFallback(String productId, Exception ex) {
log.warn("Primary service failed, using backup for product {}", productId);
try {
return backupService.getProduct(productId);
} catch (Exception backupEx) {
log.error("Backup service also failed for product {}", productId);
return cacheFallback(productId, backupEx);
}
}
// 4. Degraded Functionality
@CircuitBreaker(name = "recommendation-service", fallbackMethod = "degradedRecommendations")
public List<Product> getRecommendations(String userId, int limit) {
return recommendationService.getPersonalizedRecommendations(userId, limit);
}
public List<Product> degradedRecommendations(String userId, int limit, Exception ex) {
log.warn("Personalized recommendations failed, using popular products");
// Возвращаем популярные товары вместо персонализированных
return productService.getPopularProducts(limit);
}
}
Async Fallback
// Асинхронные fallback стратегии
@Service
public class AsyncFallbackService {
@CircuitBreaker(name = "async-service", fallbackMethod = "asyncFallback")
@Async
public CompletableFuture<OrderStatus> getOrderStatus(String orderId) {
return CompletableFuture.supplyAsync(() ->
orderStatusClient.getStatus(orderId));
}
// Async fallback method
public CompletableFuture<OrderStatus> asyncFallback(String orderId, Exception ex) {
return CompletableFuture.supplyAsync(() -> {
// Попытка получить из кэша
OrderStatus cached = orderStatusCache.get(orderId);
if (cached != null) {
return cached;
}
// Default status
return OrderStatus.builder()
.orderId(orderId)
.status("UNKNOWN")
.message("Status temporarily unavailable")
.build();
});
}
}
Fallback Chain
// Цепочка fallback методов
@Service
public class UserProfileService {
@CircuitBreaker(name = "user-profile", fallbackMethod = "databaseFallback")
public UserProfile getUserProfile(String userId) {
return externalUserService.getProfile(userId);
}
// Level 1: Попытка из базы данных
public UserProfile databaseFallback(String userId, Exception ex) {
try {
return userRepository.findById(userId)
.map(this::toUserProfile)
.orElse(null);
} catch (Exception dbEx) {
return cacheFallback(userId, dbEx);
}
}
// Level 2: Попытка из кэша
public UserProfile cacheFallback(String userId, Exception ex) {
UserProfile cached = profileCache.get(userId);
if (cached != null) {
return cached;
}
return staticFallback(userId, ex);
}
// Level 3: Статичные данные
public UserProfile staticFallback(String userId, Exception ex) {
return UserProfile.builder()
.userId(userId)
.name("Guest User")
.email("guest@example.com")
.build();
}
}
Комбинирование паттернов
All-in-One Protection
// Полная защита с комбинацией паттернов
@Service
public class OrderProcessingService {
// Комбинирование всех паттернов resilience
@CircuitBreaker(name = "order-processing", fallbackMethod = "fallbackProcessOrder")
@Retry(name = "order-processing")
@TimeLimiter(name = "order-processing")
@Bulkhead(name = "order-processing", type = Bulkhead.Type.THREAD_POOL)
public CompletableFuture<OrderResult> processOrder(OrderRequest request) {
return CompletableFuture.supplyAsync(() -> {
// Валидация с timeout
validateOrderWithTimeout(request);
// Обработка с retry
PaymentResult payment = processPaymentWithRetry(request.getPayment());
// Инвентарь с circuit breaker
InventoryResult inventory = reserveInventoryWithCircuitBreaker(request.getItems());
return OrderResult.builder()
.orderId(UUID.randomUUID().toString())
.paymentId(payment.getId())
.inventoryReservationId(inventory.getReservationId())
.status(OrderStatus.CONFIRMED)
.build();
});
}
public CompletableFuture<OrderResult> fallbackProcessOrder(OrderRequest request, Exception ex) {
log.error("Order processing failed completely", ex);
return CompletableFuture.completedFuture(
OrderResult.builder()
.orderId(UUID.randomUUID().toString())
.status(OrderStatus.FAILED)
.errorMessage("Service temporarily unavailable")
.build());
}
}
Configuration
# Полная конфигурация resilience patterns
resilience4j:
circuitbreaker:
instances:
order-processing:
sliding-window-size: 20
failure-rate-threshold: 50
wait-duration-in-open-state: 60s
retry:
instances:
order-processing:
max-attempts: 3
wait-duration: 1s
exponential-backoff-multiplier: 2
timelimiter:
instances:
order-processing:
timeout-duration: 5s
cancel-running-future: true
bulkhead:
instances:
order-processing:
max-concurrent-calls: 10
order-processing-threadpool:
max-thread-pool-size: 8
core-thread-pool-size: 4
queue-capacity: 20
ratelimiter:
instances:
order-processing:
limit-for-period: 100
limit-refresh-period: 1s
timeout-duration: 0s
Мониторинг и метрики
// Comprehensive monitoring
@Component
public class ResilienceMetrics {
private final MeterRegistry meterRegistry;
public ResilienceMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
// Circuit breaker metrics
CircuitBreakerRegistry.ofDefaults().getEventPublisher()
.onStateTransition(event ->
meterRegistry.counter("circuit.breaker.state.transitions",
"name", event.getCircuitBreakerName(),
"from", event.getStateTransition().getFromState().name(),
"to", event.getStateTransition().getToState().name())
.increment());
// Retry metrics
RetryRegistry.ofDefaults().getEventPublisher()
.onRetry(event ->
meterRegistry.counter("retry.attempts",
"name", event.getName())
.increment());
// Bulkhead metrics
BulkheadRegistry.ofDefaults().getEventPublisher()
.onCallRejected(event ->
meterRegistry.counter("bulkhead.calls.rejected",
"name", event.getBulkheadName())
.increment());
}
}
Лучшие практики
Do's and Don'ts
DO:
- Комбинируйте паттерны для максимальной защиты
- Мони
Регистрация и обнаружение сервисов
Основные концепции
Service Discovery Problem
В микросервисной архитектуре сервисы динамически создаются, уничтожаются и перемещаются. Service Discovery решает проблему поиска и подключения к нужным сервисам.
Основные задачи:
- Service Registration — регистрация сервиса в реестре при запуске
- Service Discovery — поиск доступных экземпляров сервиса
- Health Monitoring — отслеживание состояния сервисов
- Load Balancing — распределение нагрузки между экземплярами
Паттерны Service Discovery
Client-side Discovery — клиент самостоятельно ищет сервисы в реестре Server-side Discovery — поиск происходит на стороне инфраструктуры (Load Balancer, API Gateway)
Client-side: Client → Service Registry → Direct Call to Service
Server-side: Client → Load Balancer → Service Registry → Service
Service Registry Solutions
Netflix Eureka
Eureka — сервис регистрации и обнаружения от Netflix, часть Spring Cloud.
Компоненты:
- Eureka Server — реестр сервисов
- Eureka Client — библиотека для регистрации/обнаружения
// Eureka Server
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
// Конфигурация Eureka Server
# application.yml
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false # Сам сервер не регистрируется
fetch-registry: false # И не получает данные о других сервисах
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
server:
enable-self-preservation: false # Отключаем self-preservation для dev
eviction-interval-timer-in-ms: 10000 # Чистка каждые 10 секунд
// Eureka Client (микросервис)
@SpringBootApplication
@EnableEurekaClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
// Конфигурация клиента
# application.yml
spring:
application:
name: user-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
fetch-registry: true
register-with-eureka: true
instance:
lease-renewal-interval-in-seconds: 30 # Heartbeat каждые 30 сек
lease-expiration-duration-in-seconds: 90 # Удаление через 90 сек без heartbeat
metadata-map:
version: 1.0.0
region: us-east-1
Преимущества Eureka:
- Встроенная интеграция с Spring Cloud
- Self-preservation режим при сетевых проблемах
- Простая настройка и использование
Недостатки:
- Eventual consistency (CP невозможна)
- Netflix больше не развивает активно
- Только HTTP для health checks
HashiCorp Consul
Consul — более мощная система service discovery с дополнительными возможностями.
Особенности:
- Multi-datacenter support — работа в нескольких ДЦ
- Health checks — HTTP, TCP, gRPC, script-based
- Key-Value store — распределённое хранилище конфигураций
- Service mesh ready — интеграция с Consul Connect
# docker-compose.yml для Consul
version: '3.8'
services:
consul:
image: consul:1.15
ports:
- "8500:8500"
- "8600:8600/udp"
command: agent -server -ui -node=server-1 -bootstrap-expect=1 -client=0.0.0.0
// Spring Boot с Consul
@SpringBootApplication
@EnableDiscoveryClient
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
// Конфигурация Consul
# application.yml
spring:
application:
name: order-service
cloud:
consul:
host: localhost
port: 8500
discovery:
enabled: true
service-name: ${spring.application.name}
health-check-path: /actuator/health
health-check-interval: 10s
health-check-timeout: 3s
health-check-critical-timeout: 30s
instance-id: ${spring.application.name}:${random.value}
tags:
- version=1.0.0
- environment=production
metadata:
version: 1.0.0
git-commit: ${git.commit.id:unknown}
Преимущества Consul:
- Сильная консистентность (Raft consensus)
- Богатые health checks
- Multi-datacenter replication
- Service mesh поддержка
Недостатки:
- Более сложная настройка
- Требует больше ресурсов
- Дополнительная инфраструктура
etcd
etcd — распределённое key-value хранилище, часто используется в Kubernetes.
# Kubernetes Service
apiVersion: v1
kind: Service
metadata:
name: user-service
labels:
app: user-service
spec:
ports:
- port: 8080
targetPort: 8080
selector:
app: user-service
---
# Kubernetes Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 3
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: user-service:latest
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "kubernetes"
// Spring Boot с Kubernetes discovery
# application-kubernetes.yml
spring:
cloud:
kubernetes:
discovery:
enabled: true
all-namespaces: false
wait-cache-ready: true
config:
enabled: true
name: user-service-config
Client-side Load Balancing
Spring Cloud LoadBalancer
Spring Cloud LoadBalancer — замена Netflix Ribbon для client-side балансировки.
// Конфигурация Load Balancer
@Configuration
public class LoadBalancerConfig {
// Round Robin (по умолчанию)
@Bean
public ReactorLoadBalancer<ServiceInstance> roundRobinLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RoundRobinLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
name);
}
// Random балансировка
@Bean
public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
name);
}
}
// Использование с RestTemplate
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced // Включает client-side load balancing
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@Service
public class OrderService {
private final RestTemplate restTemplate;
public User getUser(Long userId) {
// Автоматическая балансировка между экземплярами user-service
return restTemplate.getForObject(
"http://user-service/users/{id}",
User.class,
userId);
}
}
// Использование с WebClient
@Configuration
public class WebClientConfig {
@Bean
@LoadBalanced
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
@Bean
public WebClient webClient(WebClient.Builder builder) {
return builder.build();
}
}
@Service
public class PaymentService {
private final WebClient webClient;
public Mono<PaymentResult> processPayment(PaymentRequest request) {
return webClient.post()
.uri("http://payment-service/payments") // Автоматическая балансировка
.bodyValue(request)
.retrieve()
.bodyToMono(PaymentResult.class);
}
}
Кастомные алгоритмы балансировки
// Weighted Round Robin
@Component
public class WeightedRoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private final AtomicInteger position = new AtomicInteger();
private final String serviceId;
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier.get(request).next()
.map(serviceInstances -> processInstanceResponse(supplier, serviceInstances));
}
private Response<ServiceInstance> processInstanceResponse(
ServiceInstanceListSupplier supplier,
List<ServiceInstance> serviceInstances) {
if (serviceInstances.isEmpty()) {
return new EmptyResponse();
}
// Weighted selection based on metadata
List<ServiceInstance> weightedInstances = new ArrayList<>();
for (ServiceInstance instance : serviceInstances) {
String weightStr = instance.getMetadata().get("weight");
int weight = weightStr != null ? Integer.parseInt(weightStr) : 1;
for (int i = 0; i < weight; i++) {
weightedInstances.add(instance);
}
}
int pos = Math.abs(position.incrementAndGet()) % weightedInstances.size();
return new DefaultResponse(weightedInstances.get(pos));
}
}
// Health-aware Load Balancer
@Component
public class HealthAwareLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private final HealthCheckService healthCheckService;
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
return serviceInstanceListSupplier.get(request).next()
.map(this::selectHealthyInstance);
}
private Response<ServiceInstance> selectHealthyInstance(List<ServiceInstance> instances) {
List<ServiceInstance> healthyInstances = instances.stream()
.filter(instance -> healthCheckService.isHealthy(instance))
.collect(Collectors.toList());
if (healthyInstances.isEmpty()) {
// Fallback to all instances if none are healthy
healthyInstances = instances;
}
// Simple round-robin among healthy instances
int index = ThreadLocalRandom.current().nextInt(healthyInstances.size());
return new DefaultResponse(healthyInstances.get(index));
}
}
Sticky Sessions
// Session Affinity Load Balancer
@Component
public class StickySessionLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private final ConcurrentHashMap<String, ServiceInstance> sessionToInstance = new ConcurrentHashMap<>();
private final AtomicInteger roundRobinCounter = new AtomicInteger();
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
String sessionId = extractSessionId(request);
return serviceInstanceListSupplier.get(request).next()
.map(instances -> {
if (sessionId != null) {
// Проверяем, есть ли привязка к сессии
ServiceInstance stickyInstance = sessionToInstance.get(sessionId);
if (stickyInstance != null && instances.contains(stickyInstance)) {
return new DefaultResponse(stickyInstance);
}
}
// Новая сессия или инстанс недоступен
ServiceInstance selectedInstance = selectRoundRobin(instances);
if (sessionId != null) {
sessionToInstance.put(sessionId, selectedInstance);
}
return new DefaultResponse(selectedInstance);
});
}
private String extractSessionId(Request request) {
if (request.getContext() instanceof RequestDataContext) {
RequestDataContext context = (RequestDataContext) request.getContext();
HttpHeaders headers = context.getClientRequest().getHeaders();
return headers.getFirst("X-Session-ID");
}
return null;
}
}
Server-side Load Balancing
API Gateway Load Balancing
// Spring Cloud Gateway
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r.path("/api/users/**")
.filters(f -> f
.loadBalancer() // Включает load balancing
.circuitBreaker(config -> config.setName("user-service")))
.uri("lb://user-service")) // lb:// для load balancing
.route("order-service", r -> r.path("/api/orders/**")
.filters(f -> f.loadBalancer())
.uri("lb://order-service"))
.build();
}
// Кастомная конфигурация load balancer
@Bean
public LoadBalancerClientFactory loadBalancerClientFactory() {
LoadBalancerClientFactory factory = new LoadBalancerClientFactory();
// Конфигурация для конкретного сервиса
factory.setConfigurations(Map.of(
"user-service", UserServiceLoadBalancerConfig.class,
"order-service", OrderServiceLoadBalancerConfig.class
));
return factory;
}
}
// Health check интеграция
@Component
public class HealthCheckLoadBalancerFilter implements LoadBalancerLifecycle<RequestDataContext, ResponseData, ServiceInstance> {
@Override
public boolean supports(Class requestContextClass, Class responseClass, Class serverTypeClass) {
return RequestDataContext.class.isAssignableFrom(requestContextClass);
}
@Override
public void onStartRequest(RequestDataContext requestContext, ServiceInstance serviceInstance) {
// Логирование запроса
log.debug("Routing request to instance: {}:{}",
serviceInstance.getHost(), serviceInstance.getPort());
}
@Override
public void onComplete(CompletionContext<ResponseData, ServiceInstance, RequestDataContext> completionContext) {
ServiceInstance serviceInstance = completionContext.getServiceInstance();
ResponseData responseData = completionContext.getResponseData();
// Обновляем health статус на основе ответа
if (responseData.getHttpStatus().is5xxServerError()) {
healthTracker.recordFailure(serviceInstance);
} else {
healthTracker.recordSuccess(serviceInstance);
}
}
}
Nginx Load Balancing
# nginx.conf
upstream user-service {
# Различные алгоритмы балансировки
least_conn; # или ip_hash, или round_robin (default)
server user-service-1:8080 weight=3;
server user-service-2:8080 weight=2;
server user-service-3:8080 weight=1 backup; # backup сервер
# Health checks (nginx plus)
health_check interval=10s fails=3 passes=2;
}
upstream order-service {
server order-service-1:8080 max_fails=3 fail_timeout=30s;
server order-service-2:8080 max_fails=3 fail_timeout=30s;
}
server {
listen 80;
location /api/users/ {
proxy_pass http://user-service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Timeouts
proxy_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_read_timeout 30s;
}
location /api/orders/ {
proxy_pass http://order-service;
# Sticky sessions
ip_hash;
}
}
Health Checks
Spring Boot Actuator
Spring Boot Actuator — предоставляет готовые health check endpoints и метрики.
// Зависимость
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
// Конфигурация Actuator
# application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,env,prometheus
base-path: /actuator
endpoint:
health:
show-details: always
show-components: always
health:
circuitbreakers:
enabled: true
ratelimiters:
enabled: true
# Информация о приложении
info:
app:
name: ${spring.application.name}
version: @project.version@
description: User management service
build:
version: @project.version@
timestamp: @timestamp@
Кастомные Health Indicators
// Database Health Indicator
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
private final DataSource dataSource;
@Override
public Health health() {
try (Connection connection = dataSource.getConnection()) {
if (connection.isValid(3)) { // 3 секунды timeout
return Health.up()
.withDetail("database", "Available")
.withDetail("connection-pool", getConnectionPoolInfo())
.build();
} else {
return Health.down()
.withDetail("database", "Connection invalid")
.build();
}
} catch (SQLException e) {
return Health.down()
.withDetail("database", "Connection failed")
.withException(e)
.build();
}
}
private Map<String, Object> getConnectionPoolInfo() {
if (dataSource instanceof HikariDataSource) {
HikariDataSource hikari = (HikariDataSource) dataSource;
HikariPoolMXBean pool = hikari.getHikariPoolMXBean();
return Map.of(
"active", pool.getActiveConnections(),
"idle", pool.getIdleConnections(),
"total", pool.getTotalConnections(),
"waiting", pool.getThreadsAwaitingConnection()
);
}
return Map.of("type", dataSource.getClass().getSimpleName());
}
}
// External Service Health Indicator
@Component
public class ExternalServiceHealthIndicator implements HealthIndicator {
private final RestTemplate restTemplate;
private final String externalServiceUrl;
@Override
public Health health() {
try {
ResponseEntity<String> response = restTemplate.getForEntity(
externalServiceUrl + "/health",
String.class);
if (response.getStatusCode().is2xxSuccessful()) {
return Health.up()
.withDetail("external-service", "Available")
.withDetail("response-time", measureResponseTime())
.build();
} else {
return Health.down()
.withDetail("external-service", "Unhealthy")
.withDetail("status-code", response.getStatusCode())
.build();
}
} catch (Exception e) {
return Health.down()
.withDetail("external-service", "Unavailable")
.withException(e)
.build();
}
}
private long measureResponseTime() {
long start = System.currentTimeMillis();
try {
restTemplate.getForEntity(externalServiceUrl + "/ping", String.class);
return System.currentTimeMillis() - start;
} catch (Exception e) {
return -1;
}
}
}
// Business Logic Health Indicator
@Component
public class BusinessHealthIndicator implements HealthIndicator {
private final OrderService orderService;
private final PaymentService paymentService;
@Override
public Health health() {
Health.Builder builder = Health.up();
// Проверка критических бизнес-функций
checkOrderProcessing(builder);
checkPaymentProcessing(builder);
return builder.build();
}
private void checkOrderProcessing(Health.Builder builder) {
try {
long pendingOrders = orderService.countPendingOrders();
if (pendingOrders > 1000) {
builder.down().withDetail("order-processing", "Too many pending orders: " + pendingOrders);
} else {
builder.withDetail("order-processing", "Normal, pending: " + pendingOrders);
}
} catch (Exception e) {
builder.down().withDetail("order-processing", "Failed to check");
}
}
private void checkPaymentProcessing(Health.Builder builder) {
try {
boolean canProcessPayments = paymentService.canProcessPayments();
if (!canProcessPayments) {
builder.down().withDetail("payment-processing", "Payment gateway unavailable");
} else {
builder.withDetail("payment-processing", "Available");
}
} catch (Exception e) {
builder.down().withDetail("payment-processing", "Failed to check");
}
}
}
Reactive Health Indicators
// Reactive Health Indicator для WebFlux
@Component
public class ReactiveExternalServiceHealthIndicator implements ReactiveHealthIndicator {
private final WebClient webClient;
@Override
public Mono<Health> health() {
return webClient.get()
.uri("/external-service/health")
.retrieve()
.toEntity(String.class)
.map(response -> Health.up()
.withDetail("external-service", "Available")
.withDetail("status", response.getStatusCode())
.build())
.onErrorReturn(Health.down()
.withDetail("external-service", "Unavailable")
.build())
.timeout(Duration.ofSeconds(3))
.onErrorReturn(Health.down()
.withDetail("external-service", "Timeout")
.build());
}
}
Интеграция с Service Mesh
Istio Service Discovery
# Istio ServiceEntry для внешних сервисов
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: external-payment-service
spec:
hosts:
- payment.external.com
ports:
- number: 443
name: https
protocol: HTTPS
location: MESH_EXTERNAL
resolution: DNS
---
# DestinationRule для load balancing
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: user-service
spec:
host: user-service
trafficPolicy:
loadBalancer:
consistentHash:
httpCookieName: "session-id" # Sticky sessions
connectionPool:
tcp:
maxConnections: 100
http:
http1MaxPendingRequests: 10
maxRequestsPerConnection: 2
outlierDetection:
consecutiveErrors: 3
interval: 30s
baseEjectionTime: 30s
Consul Connect
// Consul Connect интеграция
# application.yml
spring:
cloud:
consul:
connect:
enabled: true
service-name: ${spring.application.name}
instance-id: ${spring.application.name}:${random.value}
Мониторинг и метрики
Service Discovery Metrics
@Component
public class ServiceDiscoveryMetrics {
private final MeterRegistry meterRegistry;
private final DiscoveryClient discoveryClient;
// Метрики регистрации сервисов
@Scheduled(fixedRate = 30000)
public void recordServiceMetrics() {
List<String> services = discoveryClient.getServices();
Gauge.builder("service.registry.services.count")
.description("Number of registered services")
.register(meterRegistry, services, List::size);
for (String service : services) {
List<ServiceInstance> instances = discoveryClient.getInstances(service);
Gauge.builder("service.registry.instances.count")
.tag("service", service)
.description("Number of instances for service")
.register(meterRegistry, instances, List::size);
// Health статус инстансов
long healthyInstances = instances.stream()
.filter(this::isHealthy)
.count();
Gauge.builder("service.registry.instances.healthy")
.tag("service", service)
.description("Number of healthy instances")
.register(meterRegistry, healthyInstances);
}
}
private boolean isHealthy(ServiceInstance instance) {
try {
ResponseEntity<Map> response = restTemplate.getForEntity(
"http://" + instance.getHost() + ":" + instance.getPort() + "/actuator/health",
Map.class);
return response.getStatusCode().is2xxSuccessful() &&
"UP".equals(((Map<String, Object>) response.getBody()).get("status"));
} catch (Exception e) {
return false;
}
}
}
Load Balancer Metrics
// Метрики load balancing
@Component
public class LoadBalancerMetrics implements LoadBalancerLifecycle<RequestDataContext, ResponseData, ServiceInstance> {
private final MeterRegistry meterRegistry;
@Override
public void onStart(RequestDataContext requestContext) {
Timer.Sample sample = Timer.start(meterRegistry);
requestContext.put("timer.sample", sample);
}
@Override
public void onComplete(CompletionContext<ResponseData, ServiceInstance, RequestDataContext> completionContext) {
Timer.Sample sample = completionContext.getRequestContext().get("timer.sample");
ServiceInstance instance = completionContext.getServiceInstance();
ResponseData response = completionContext.getResponseData();
if (sample != null) {
sample.stop(Timer.builder("loadbalancer.request.duration")
.tag("service", instance.getServiceId())
.tag("instance", instance.getInstanceId())
.tag("status", String.valueOf(response.getHttpStatus().value()))
.register(meterRegistry));
}
// Счётчик запросов по инстансам
Counter.builder("loadbalancer.requests.total")
.tag("service", instance.getServiceId())
.tag("instance", instance.getInstanceId())
.tag("status", String.valueOf(response.getHttpStatus().value()))
.register(meterRegistry)
.increment();
}
}
Лучшие практики
Configuration Management
// Внешняя конфигурация для service discovery
@ConfigurationProperties(prefix = "app.service-discovery")
@Data
public class ServiceDiscoveryProperties {
private boolean enabled = true;
private int heartbeatInterval = 30;
private int registrationTimeout = 60;
private List<String> healthCheckPaths = List.of("/actuator/health");
private Map<String, ServiceConfig> services = new HashMap<>();
@Data
public static class ServiceConfig {
private String name;
private List<String> tags = new ArrayList<>();
private Map<String, String> metadata = new HashMap<>();
private int weight = 1;
private boolean enableHealthCheck = true;
}
}
// Graceful shutdown
@Component
public class ServiceRegistryShutdownHook {
private final DiscoveryClient discoveryClient;
@PreDestroy
public void deregister() {
try {
if (discoveryClient instanceof EurekaDiscoveryClient) {
EurekaDiscoveryClient eurekaClient = (EurekaDiscoveryClient) discoveryClient;
eurekaClient.shutdown();
}
log.info("Service deregistered successfully");
} catch (Exception e) {
log.error("Failed to deregister service", e);
}
}
}
Security Considerations
// Secure service communication
@Configuration
public class SecureServiceDiscoveryConfig {
@Bean
public RestTemplate secureRestTemplate() {
RestTemplate template = new RestTemplate();
// Добавляем аутентификацию
template.getInterceptors().add((request, body, execution) -> {
String token = jwtTokenProvider.generateServiceToken();
request.getHeaders().setBearerAuth(token);
return execution.execute(request, body);
});
return template;
}
// mTLS для service-to-service communication
@Bean
public WebClient secureWebClient() throws Exception {
SslContext sslContext = SslContextBuilder
.forClient()
.keyManager(getKeyManagerFactory())
.trustManager(getTrustManagerFactory())
.build();
HttpClient httpClient = HttpClient.create()
.secure(sslSpec -> sslSpec.sslContext(sslContext));
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
}
Error Handling
// Обработка ошибок service discovery
@Component
public class ServiceDiscoveryErrorHandler {
private final MeterRegistry meterRegistry;
private final AlertService alertService;
@EventListener
public void handleServiceDown(ServiceDownEvent event) {
log.error("Service {} is down: {}", event.getServiceName(), event.getReason());
// Метрики
Counter.builder("service.discovery.errors")
.tag("service", event.getServiceName())
.tag("type", "service_down")
.register(meterRegistry)
.increment();
// Алерты
if (event.isCriticalService()) {
alertService.sendCriticalAlert(
"Critical service down: " + event.getServiceName());
}
}
@EventListener
public void handleRegistryUnavailable(RegistryUnavailableEvent event) {
log.error("Service registry unavailable: {}", event.getRegistryUrl());
// Переключение на backup registry
serviceDiscoveryManager.switchToBackupRegistry();
// Уведомление о проблеме
alertService.sendAlert("Service registry unavailable: " + event.getRegistryUrl());
}
}
// Circuit breaker для service discovery
@Service
public class ResilientServiceDiscovery {
@CircuitBreaker(name = "service-discovery", fallbackMethod = "fallbackGetInstances")
@Retry(name = "service-discovery")
public List<ServiceInstance> getServiceInstances(String serviceName) {
return discoveryClient.getInstances(serviceName);
}
public List<ServiceInstance> fallbackGetInstances(String serviceName, Exception ex) {
log.warn("Service discovery failed for {}, using cached instances", serviceName);
// Возвращаем кэшированные инстансы
List<ServiceInstance> cached = serviceCache.getCachedInstances(serviceName);
if (!cached.isEmpty()) {
return cached;
}
// Последний resort - статически сконфигурированные инстансы
return getStaticInstances(serviceName);
}
private List<ServiceInstance> getStaticInstances(String serviceName) {
return staticServiceConfig.getInstances(serviceName);
}
}
Testing Strategies
// Mock service registry для тестов
@TestConfiguration
public class TestServiceDiscoveryConfig {
@Bean
@Primary
public DiscoveryClient mockDiscoveryClient() {
DiscoveryClient mock = Mockito.mock(DiscoveryClient.class);
// Настройка mock данных
when(mock.getServices()).thenReturn(Arrays.asList("user-service", "order-service"));
when(mock.getInstances("user-service")).thenReturn(Arrays.asList(
createMockInstance("user-service", "localhost", 8081),
createMockInstance("user-service", "localhost", 8082)
));
return mock;
}
private ServiceInstance createMockInstance(String serviceId, String host, int port) {
return new DefaultServiceInstance(
serviceId + "-" + port,
serviceId,
host,
port,
false
);
}
}
// Integration tests
@SpringBootTest
@DirtiesContext
class ServiceDiscoveryIntegrationTest {
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private LoadBalancerClient loadBalancerClient;
@Test
void shouldDiscoverServices() {
List<String> services = discoveryClient.getServices();
assertThat(services).contains("user-service");
List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
assertThat(instances).hasSize(2);
}
@Test
void shouldBalanceLoad() {
Set<ServiceInstance> selectedInstances = new HashSet<>();
// Делаем несколько запросов и проверяем балансировку
for (int i = 0; i < 10; i++) {
ServiceInstance instance = loadBalancerClient.choose("user-service");
selectedInstances.add(instance);
}
// Должны быть выбраны разные инстансы
assertThat(selectedInstances.size()).isGreaterThan(1);
}
}
Антипаттерны и проблемы
Избегайте
- Hardcoded Endpoints — не используйте статические IP/порты
- Missing Health Checks — всегда реализуйте health endpoints
- Ignoring Timeouts — настройте таймауты для всех взаимодействий
- Single Point of Failure — используйте кластеры service registry
- Chatty Health Checks — не делайте health checks слишком частыми
// ПЛОХО - hardcoded endpoints
@Service
public class BadOrderService {
public User getUser(Long userId) {
return restTemplate.getForObject(
"http://192.168.1.100:8080/users/{id}", // Статический IP!
User.class, userId);
}
}
// ХОРОШО - service discovery
@Service
public class GoodOrderService {
public User getUser(Long userId) {
return restTemplate.getForObject(
"http://user-service/users/{id}", // Логическое имя сервиса
User.class, userId);
}
}
// ПЛОХО - игнорирование health check failures
@Component
public class BadHealthIndicator implements HealthIndicator {
@Override
public Health health() {
return Health.up().build(); // Всегда UP!
}
}
// ХОРОШО - реальная проверка здоровья
@Component
public class GoodHealthIndicator implements HealthIndicator {
@Override
public Health health() {
try {
// Проверяем критические зависимости
databaseHealthCheck();
externalServiceHealthCheck();
return Health.up().build();
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.build();
}
}
}
Performance Considerations
// Кэширование результатов service discovery
@Service
public class CachedServiceDiscovery {
private final LoadingCache<String, List<ServiceInstance>> instanceCache;
public CachedServiceDiscovery(DiscoveryClient discoveryClient) {
this.instanceCache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(30, TimeUnit.SECONDS)
.refreshAfterWrite(10, TimeUnit.SECONDS)
.build(serviceName -> discoveryClient.getInstances(serviceName));
}
public List<ServiceInstance> getInstances(String serviceName) {
try {
return instanceCache.get(serviceName);
} catch (Exception e) {
log.error("Failed to get instances for service: {}", serviceName, e);
return Collections.emptyList();
}
}
}
// Async health checks
@Service
public class AsyncHealthChecker {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
private final ConcurrentHashMap<String, Boolean> healthStatus = new ConcurrentHashMap<>();
@Scheduled(fixedRate = 30000)
public void checkAllServices() {
List<ServiceInstance> allInstances = getAllServiceInstances();
List<CompletableFuture<Void>> futures = allInstances.stream()
.map(instance -> CompletableFuture.runAsync(() ->
checkInstanceHealth(instance), executor))
.collect(Collectors.toList());
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.exceptionally(throwable -> {
log.warn("Some health checks failed", throwable);
return null;
});
}
private void checkInstanceHealth(ServiceInstance instance) {
try {
boolean isHealthy = performHealthCheck(instance);
healthStatus.put(getInstanceKey(instance), isHealthy);
} catch (Exception e) {
healthStatus.put(getInstanceKey(instance), false);
}
}
}
Вопросы для собеседования
- Чем отличается client-side от server-side service discovery?
- Какие преимущества и недостатки у Eureka vs Consul?
- Как обеспечить graceful shutdown сервиса при использовании service registry?
- Какие стратегии load balancing знаете и когда их применять?
- Как обрабатывать ситуацию, когда service registry недоступен?
- Что должен включать health check endpoint?
- Как реализовать sticky sessions в микросервисах?
- Различия между health checks и readiness probes?
- Как мониторить эффективность load balancing?
- Стратегии для обновления сервисов без downtime (blue-green, canary)?
- Как обеспечить безопасность service-to-service коммуникации?
- Что такое service mesh и как он связан с service discovery?
Безопасность
OAuth 2.0 и OpenID Connect (OIDC)
OAuth 2.0
OAuth 2.0 — это протокол авторизации, который позволяет приложениям получать ограниченный доступ к ресурсам пользователя без получения его учетных данных.
Основные роли:
- Resource Owner (пользователь) — владелец ресурса
- Client (приложение) — запрашивает доступ к ресурсу
- Authorization Server — выдает токены доступа
- Resource Server — хранит защищенные ресурсы
Потоки авторизации (Grant Types):
- Authorization Code — самый безопасный, для веб-приложений
- Client Credentials — для межсервисного взаимодействия
- Refresh Token — для обновления истекших токенов
// Spring Security OAuth2 Client
@RestController
public class UserController {
@GetMapping("/user")
public String getUser(@AuthenticationPrincipal OAuth2User principal) {
return "Hello " + principal.getAttribute("name");
}
}
OpenID Connect (OIDC)
OIDC — это слой аутентификации поверх OAuth 2.0, который добавляет информацию о пользователе через ID Token.
Отличия от OAuth 2.0:
- OAuth 2.0 = авторизация ("что можно делать")
- OIDC = аутентификация + авторизация ("кто это" + "что можно делать")
ID Token содержит:
sub
— уникальный идентификатор пользователяaud
— аудитория (для кого предназначен токен)iss
— издатель токенаexp
— время истечения
Keycloak
Keycloak — это open-source решение для Identity and Access Management (IAM).
Основные возможности:
- Централизованная аутентификация и авторизация
- Интеграция с LDAP, Active Directory
- Social Login (Google, Facebook, GitHub)
- Fine-grained authorization policies
- Admin Console для управления пользователями
# Docker Compose для Keycloak
version: '3.8'
services:
keycloak:
image: quay.io/keycloak/keycloak:latest
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
ports:
- "8080:8080"
command: start-dev
JWT (JSON Web Tokens)
Структура JWT
JWT состоит из трех частей, разделенных точками: header.payload.signature
Header — содержит тип токена и алгоритм подписи:
{
"alg": "HS256",
"typ": "JWT"
}
Payload — содержит claims (утверждения):
{
"sub": "user123",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622
}
Signature — обеспечивает целостность токена
Валидация JWT
@Service
public class JwtService {
private final String secretKey = "mySecretKey";
public Claims validateToken(String token) {
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
}
public boolean isTokenExpired(String token) {
return validateToken(token).getExpiration().before(new Date());
}
}
Шифрование JWT (JWE)
JWE (JSON Web Encryption) — зашифрованный JWT для защиты конфиденциальных данных.
// Создание зашифрованного JWT
JWEObject jweObject = new JWEObject(
new JWEHeader(JWEAlgorithm.DIR, EncryptionMethod.A128GCM),
new Payload(claims.toJSONString())
);
jweObject.encrypt(new DirectEncrypter(secretKey));
Управление сроком жизни
Access Token — короткий срок жизни (15-30 минут) Refresh Token — длительный срок жизни (дни/недели)
@Component
public class TokenManager {
private final int ACCESS_TOKEN_VALIDITY = 30 * 60; // 30 минут
private final int REFRESH_TOKEN_VALIDITY = 7 * 24 * 60 * 60; // 7 дней
public String generateAccessToken(UserDetails user) {
return Jwts.builder()
.setSubject(user.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_VALIDITY * 1000))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
}
API Gateway и централизованная авторизация
Роль API Gateway
API Gateway выступает единой точкой входа для всех клиентских запросов:
- Аутентификация и авторизация
- Rate limiting
- Request/Response transformation
- Load balancing
- Мониторинг и логирование
Централизованная авторизация
Все проверки безопасности происходят на уровне Gateway, микросервисы доверяют Gateway.
// Spring Cloud Gateway Filter
@Component
public class AuthenticationFilter implements GatewayFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = extractToken(exchange.getRequest());
if (token == null || !jwtService.validateToken(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// Добавляем информацию о пользователе в headers
ServerHttpRequest request = exchange.getRequest().mutate()
.header("X-User-Id", getUserId(token))
.header("X-User-Roles", getUserRoles(token))
.build();
return chain.filter(exchange.mutate().request(request).build());
}
}
Паттерн Token Relay
Gateway перенаправляет токен в downstream сервисы:
# Spring Cloud Gateway configuration
spring:
cloud:
gateway:
routes:
- id: user-service
uri: http://user-service
predicates:
- Path=/users/**
filters:
- TokenRelay
Передача идентификационных данных между сервисами
Способы передачи identity
1. JWT в HTTP Headers
@RestController
public class OrderController {
@Autowired
private PaymentService paymentService;
@PostMapping("/orders")
public ResponseEntity<Order> createOrder(@RequestHeader("Authorization") String token) {
// Извлекаем user info из JWT
String userId = jwtService.extractUserId(token);
// Передаем токен в другой сервис
PaymentResponse payment = paymentService.processPayment(token, orderData);
return ResponseEntity.ok(order);
}
}
2. Service-to-Service токены Для межсервисного взаимодействия используются отдельные токены с Client Credentials flow:
@Service
public class ServiceTokenProvider {
@Value("${oauth2.client-id}")
private String clientId;
@Value("${oauth2.client-secret}")
private String clientSecret;
public String getServiceToken() {
// Получаем токен для сервиса
return webClient.post()
.uri("/oauth/token")
.bodyValue("grant_type=client_credentials&client_id=" + clientId
+ "&client_secret=" + clientSecret)
.retrieve()
.bodyToMono(TokenResponse.class)
.map(TokenResponse::getAccessToken)
.block();
}
}
3. Контекст пользователя (User Context)
// Передача контекста через ThreadLocal или Reactive Context
@Component
public class UserContextHolder {
private static final ThreadLocal<UserContext> contextHolder = new ThreadLocal<>();
public static void setContext(UserContext context) {
contextHolder.set(context);
}
public static UserContext getContext() {
return contextHolder.get();
}
public static void clearContext() {
contextHolder.remove();
}
}
Трассировка пользователя
Для отслеживания пользователя через цепочку сервисов используются:
Correlation ID — уникальный идентификатор запроса:
@RestController
public class BaseController {
@PostMapping("/process")
public ResponseEntity<?> process(@RequestHeader("X-Correlation-ID") String correlationId,
@RequestHeader("X-User-ID") String userId) {
MDC.put("correlationId", correlationId);
MDC.put("userId", userId);
// Логирование будет содержать эти поля
log.info("Processing request for user: {}", userId);
return ResponseEntity.ok().build();
}
}
Distributed Security Context
Для Spring Security в микросервисах:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
Collection<String> roles = jwt.getClaimAsStringList("roles");
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());
});
return converter;
}
}
Ключевые принципы безопасности
Defense in Depth — многоуровневая защита:
- Gateway-level security
- Service-level security
- Data-level security
Principle of Least Privilege — минимальные необходимые права доступа
Zero Trust — не доверяй, проверяй каждый запрос
Token-based Security — stateless аутентификация для масштабируемости
Конфигурация и запуск
Централизованная конфигурация
Spring Cloud Config Server
Spring Cloud Config — это решение для централизованного управления конфигурацией микросервисов. Позволяет хранить конфигурацию в Git репозитории и динамически обновлять её без перезапуска сервисов.
Основные преимущества:
- Версионирование конфигурации через Git
- Разделение конфигурации по окружениям (dev, staging, prod)
- Шифрование чувствительных данных
- Refresh конфигурации без перезапуска
Config Server:
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
# application.yml для Config Server
server:
port: 8888
spring:
cloud:
config:
server:
git:
uri: https://github.com/company/config-repo
search-paths: '{application}'
default-label: main
encrypt:
key: mySecretKey
Config Client:
# bootstrap.yml для микросервиса
spring:
application:
name: user-service
cloud:
config:
uri: http://config-server:8888
fail-fast: true
retry:
max-attempts: 3
management:
endpoints:
web:
exposure:
include: refresh
Структура репозитория конфигурации:
config-repo/
├── user-service/
│ ├── user-service.yml # default profile
│ ├── user-service-dev.yml # dev profile
│ └── user-service-prod.yml # prod profile
└── order-service/
├── order-service.yml
└── order-service-prod.yml
HashiCorp Consul
Consul — это service mesh решение, которое включает в себя service discovery, health checking, KV store для конфигурации и Connect для secure service communication.
Ключевые возможности:
- Service Discovery — автоматическое обнаружение сервисов
- Health Checking — мониторинг состояния сервисов
- KV Store — распределённое хранилище ключ-значение
- Multi-datacenter — поддержка нескольких дата-центров
// Spring Cloud Consul Configuration
@Configuration
@EnableConfigurationProperties
public class ConsulConfig {
@Value("${database.url}")
private String databaseUrl;
@RefreshScope
@Bean
public DataSource dataSource() {
return DataSourceBuilder.create()
.url(databaseUrl)
.build();
}
}
# application.yml для Consul
spring:
application:
name: user-service
cloud:
consul:
host: localhost
port: 8500
discovery:
health-check-interval: 10s
health-check-path: /actuator/health
instance-id: ${spring.application.name}:${random.value}
config:
enabled: true
format: YAML
data-key: data
Consul UI предоставляет веб-интерфейс для управления сервисами и конфигурацией на порту 8500.
Feature Toggles (Feature Flags)
Что такое Feature Toggles
Feature Toggles — это техника разработки ПО, позволяющая включать/выключать функциональность без деплоя кода. Это ключевая практика для Continuous Delivery.
Типы Feature Toggles:
- Release Toggles — для постепенного выката новой функциональности
- Experiment Toggles — для A/B тестирования
- Ops Toggles — для оперативного управления в production
- Permission Toggles — для управления доступом к функциям
Unleash
Unleash — это open-source платформа для управления feature flags с развитой экосистемой SDK.
Архитектура Unleash:
- Unleash Server — центральный сервер для управления флагами
- Unleash UI — веб-интерфейс для управления
- SDK — клиентские библиотеки для различных языков
// Unleash SDK для Java
@Service
public class UserService {
private final Unleash unleash;
public UserService(Unleash unleash) {
this.unleash = unleash;
}
public User createUser(CreateUserRequest request) {
if (unleash.isEnabled("new-user-validation")) {
// Новая логика валидации
return createUserWithAdvancedValidation(request);
} else {
// Старая логика
return createUserWithBasicValidation(request);
}
}
}
Стратегии активации в Unleash:
- gradualRollout — постепенный выкат по проценту пользователей
- userWithId — включение для конкретных пользователей
- remoteAddress — по IP адресу
- applicationHostname — по хосту приложения
FF4J (Feature Flipping for Java)
FF4J — это Java-библиотека для управления feature flags с поддержкой различных хранилищ и мониторинга.
Основные возможности:
- Различные хранилища (In-Memory, JDBC, Redis, MongoDB)
- Веб-консоль для управления
- Аудит и мониторинг использования флагов
- Стратегии активации и групповые операции
@Configuration
public class FF4JConfig {
@Bean
public FF4j ff4j() {
FF4j ff4j = new FF4j();
ff4j.createFeature("premium-features", false);
ff4j.createFeature("beta-ui", false);
// Стратегия по времени
ff4j.getFeature("premium-features")
.addFlippingStrategy(new ReleaseDateFlippingStrategy("2024-12-25-00:00"));
return ff4j;
}
}
@RestController
public class UserController {
@Autowired
private FF4j ff4j;
@GetMapping("/dashboard")
public String getDashboard() {
if (ff4j.check("beta-ui")) {
return "new-dashboard";
}
return "old-dashboard";
}
}
Стратегии деплоя
Blue/Green Deployment
Blue/Green Deployment — это стратегия деплоя, где поддерживаются две идентичные production среды. В любой момент времени только одна из них (например, Blue) обслуживает production трафик.
Процесс Blue/Green:
- Blue среда обслуживает production
- Новая версия деплоится в Green среду
- Тестирование в Green среде
- Переключение трафика на Green
- Blue становится standby для быстрого rollback
Преимущества:
- Мгновенное переключение и rollback
- Возможность тестирования в production-like среде
- Минимальный downtime (только время переключения)
# Kubernetes Blue/Green с Istio
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: user-service
spec:
hosts:
- user-service
http:
- match:
- headers:
version:
exact: v2
route:
- destination:
host: user-service
subset: green
- route:
- destination:
host: user-service
subset: blue
weight: 100
Canary Releases
Canary Release — это техника постепенного выката новой версии, когда небольшая часть трафика направляется на новую версию для тестирования в реальных условиях.
Процесс Canary Release:
- Деплой новой версии рядом со старой
- Направление 5-10% трафика на новую версию
- Мониторинг метрик (error rate, latency, business KPIs)
- Постепенное увеличение трафика (10% → 25% → 50% → 100%)
- Полное переключение или rollback при проблемах
// Canary с Spring Cloud Gateway
@Component
public class CanaryRoutingFilter implements GlobalFilter {
@Value("${canary.percentage:10}")
private int canaryPercentage;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
int random = new Random().nextInt(100);
if (random < canaryPercentage) {
// Направляем на canary версию
exchange.getAttributes().put("version", "canary");
} else {
// Направляем на стабильную версию
exchange.getAttributes().put("version", "stable");
}
return chain.filter(exchange);
}
}
Flagger — это Kubernetes оператор для автоматизации canary deployments:
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: user-service
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: user-service
service:
port: 80
analysis:
interval: 1m
threshold: 5
maxWeight: 50
stepWeight: 5
metrics:
- name: request-success-rate
threshold: 99
- name: request-duration
threshold: 500
Circuit Breaker Dashboard и динамическое обновление конфигурации
Circuit Breaker Pattern
Circuit Breaker — это паттерн для обработки сбоев в распределённых системах. Он предотвращает каскадные сбои, "размыкая цепь" при обнаружении проблем с внешним сервисом.
Состояния Circuit Breaker:
- CLOSED — нормальная работа, запросы проходят
- OPEN — сервис недоступен, запросы сразу завершаются с ошибкой
- HALF_OPEN — тестовый режим, пропускаются ограниченные запросы
Hystrix Dashboard
Hystrix — это библиотека от Netflix для реализации Circuit Breaker паттерна с мощными возможностями мониторинга.
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users/{id}")
@HystrixCommand(
fallbackMethod = "getUserFallback",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000"),
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50")
}
)
public User getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
public User getUserFallback(Long id) {
return new User(id, "Default User", "default@example.com");
}
}
Hystrix Dashboard предоставляет real-time мониторинг:
- Количество запросов и ошибок
- Время отклика
- Состояние circuit breaker
- Использование thread pool
# Hystrix Dashboard configuration
hystrix:
dashboard:
proxy-stream-allow-list: "*"
management:
endpoints:
web:
exposure:
include: hystrix.stream
Resilience4j
Resilience4j — это современная альтернатива Hystrix, легковесная библиотека для fault tolerance.
@Service
public class PaymentService {
private final CircuitBreaker circuitBreaker;
private final WebClient webClient;
public PaymentService() {
this.circuitBreaker = CircuitBreaker.ofDefaults("payment-service");
this.webClient = WebClient.builder().build();
}
public PaymentResponse processPayment(PaymentRequest request) {
Supplier<PaymentResponse> decoratedSupplier = CircuitBreaker
.decorateSupplier(circuitBreaker, () -> callPaymentAPI(request));
return decoratedSupplier.get();
}
}
Динамическое обновление конфигурации
Spring Cloud Config поддерживает обновление конфигурации без перезапуска через /actuator/refresh
endpoint.
@RestController
@RefreshScope // Бин будет пересоздан при обновлении конфигурации
public class ConfigController {
@Value("${app.feature.enabled:false}")
private boolean featureEnabled;
@Value("${app.timeout:5000}")
private int timeoutMs;
@GetMapping("/config")
public Map<String, Object> getConfig() {
return Map.of(
"featureEnabled", featureEnabled,
"timeoutMs", timeoutMs
);
}
}
Spring Cloud Bus для массового обновления всех инстансов:
spring:
cloud:
bus:
enabled: true
config:
server:
bus:
enabled: true
rabbitmq:
host: localhost
port: 5672
После изменения конфигурации в Git:
# Обновить все сервисы одним запросом
curl -X POST http://config-server:8888/actuator/bus-refresh
Мониторинг и Observability
Micrometer интегрируется с различными системами мониторинга:
@Component
public class CustomMetrics {
private final Counter orderCounter;
private final Timer orderProcessingTimer;
public CustomMetrics(MeterRegistry meterRegistry) {
this.orderCounter = Counter.builder("orders.created")
.description("Number of orders created")
.register(meterRegistry);
this.orderProcessingTimer = Timer.builder("order.processing.time")
.description("Order processing time")
.register(meterRegistry);
}
public void recordOrderCreated() {
orderCounter.increment();
}
public void recordProcessingTime(Duration duration) {
orderProcessingTimer.record(duration);
}
}
Ключевые практики
Configuration as Code — вся конфигурация версионируется в Git
Immutable Infrastructure — инфраструктура не изменяется, а пересоздается
Progressive Delivery — постепенный выкат с мониторингом метрик
Observability First — мониторинг и логирование закладываются с самого начала
Graceful Degradation — система продолжает работать в ограниченном режиме при сбоях
Observability (Наблюдаемость)
Три столпа Observability
Observability — это способность понимать внутреннее состояние системы по её внешним выходным данным. В микросервисной архитектуре включает три ключевых компонента:
- Logs — структурированные записи событий с временными метками
- Metrics — числовые измерения производительности системы во времени
- Traces — путь запроса через распределённую систему
Отличие от Monitoring: Monitoring отвечает на вопрос "что сломалось?", а Observability — "почему это сломалось?"
Log Aggregation (Агрегация логов)
Correlation ID
Correlation ID — это уникальный идентификатор, который передается через всю цепочку микросервисов для связывания логов одного пользовательского запроса.
Зачем нужен:
- Связать логи от разных сервисов в рамках одного business flow
- Упростить отладку проблем в распределённой системе
- Обеспечить возможность end-to-end трассировки
@Component
public class CorrelationInterceptor implements HandlerInterceptor {
private static final String CORRELATION_ID_HEADER = "X-Correlation-ID";
private static final String CORRELATION_ID_MDC_KEY = "correlationId";
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String correlationId = request.getHeader(CORRELATION_ID_HEADER);
if (correlationId == null) {
correlationId = UUID.randomUUID().toString();
}
// Добавляем в MDC для автоматического включения в логи
MDC.put(CORRELATION_ID_MDC_KEY, correlationId);
// Добавляем в response для клиента
response.setHeader(CORRELATION_ID_HEADER, correlationId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
MDC.clear(); // Важно! Очищаем MDC после обработки
}
}
// Передача Correlation ID в межсервисные вызовы
@Service
public class UserService {
@Autowired
private WebClient webClient;
public User getUserProfile(Long userId) {
String correlationId = MDC.get("correlationId");
return webClient.get()
.uri("/users/{id}", userId)
.header("X-Correlation-ID", correlationId)
.retrieve()
.bodyToMono(User.class)
.block();
}
}
ELK Stack (Elasticsearch, Logstash, Kibana)
ELK Stack — это набор инструментов для централизованного сбора, обработки и анализа логов.
Компоненты:
- Elasticsearch — распределённая поисковая система для хранения логов
- Logstash — инструмент для сбора, парсинга и трансформации логов
- Kibana — веб-интерфейс для визуализации и анализа данных
# Docker Compose для ELK
version: '3.8'
services:
elasticsearch:
image: elasticsearch:8.8.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
ports:
- "9200:9200"
logstash:
image: logstash:8.8.0
volumes:
- ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
ports:
- "5000:5000"
depends_on:
- elasticsearch
kibana:
image: kibana:8.8.0
ports:
- "5601:5601"
depends_on:
- elasticsearch
# logstash.conf - конфигурация для парсинга JSON логов
input {
beats {
port => 5044
}
}
filter {
if [fields][service] {
mutate {
add_field => { "service_name" => "%{[fields][service]}" }
}
}
# Парсим JSON логи
if [message] =~ /^\{.*\}$/ {
json {
source => "message"
}
}
# Извлекаем correlation ID
if [correlationId] {
mutate {
add_field => { "correlation_id" => "%{correlationId}" }
}
}
}
output {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "microservices-logs-%{+YYYY.MM.dd}"
}
}
EFK Stack (Elasticsearch, Fluentd, Kibana)
Fluentd — альтернатива Logstash, более легковесная и cloud-native. Особенно популярна в Kubernetes.
# Fluentd configuration для Kubernetes
<source>
@type tail
path /var/log/containers/*.log
pos_file /var/log/fluentd-containers.log.pos
tag kubernetes.*
format json
read_from_head true
</source>
<filter kubernetes.**>
@type kubernetes_metadata
</filter>
<match kubernetes.**>
@type elasticsearch
host elasticsearch
port 9200
index_name microservices
type_name _doc
include_tag_key true
tag_key @log_name
</match>
Distributed Tracing (Распределённая трассировка)
OpenTelemetry
OpenTelemetry — это набор инструментов, API и SDK для сбора, обработки и экспорта телеметрических данных (метрик, логов и трейсов). Это CNCF проект, ставший стандартом индустрии.
Основные концепции:
- Trace — представление полного пути запроса через систему
- Span — единица работы в трейсе (например, HTTP запрос, DB запрос)
- Context — механизм передачи трейс-информации между компонентами
// Spring Boot с OpenTelemetry
@RestController
public class OrderController {
private final Tracer tracer = GlobalOpenTelemetry.getTracer("order-service");
@GetMapping("/orders/{id}")
public ResponseEntity<Order> getOrder(@PathVariable Long id) {
Span span = tracer.spanBuilder("get-order")
.setSpanKind(SpanKind.SERVER)
.startSpan();
try (Scope scope = span.makeCurrent()) {
span.setAttribute("order.id", id);
Order order = orderService.findById(id);
span.setStatus(StatusCode.OK);
return ResponseEntity.ok(order);
} catch (Exception e) {
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
}
# application.yml для OpenTelemetry
management:
tracing:
sampling:
probability: 1.0 # 100% sampling для dev
otlp:
tracing:
endpoint: http://jaeger:14268/api/traces
Zipkin
Zipkin — это система распределённой трассировки, разработанная в Twitter. Помогает отслеживать временные затраты в микросервисной архитектуре.
Архитектура Zipkin:
- Reporter — отправляет span данные
- Collector — принимает и валидирует трейсы
- Storage — хранит span данные (In-Memory, MySQL, Cassandra, Elasticsearch)
- Query Service — API для поиска трейсов
- Web UI — интерфейс для анализа трейсов
// Spring Cloud Sleuth с Zipkin
@SpringBootApplication
@EnableZipkinServer // Если запускаем Zipkin server
public class Application {
// Кастомные span теги
@NewSpan("user-validation")
public boolean validateUser(@SpanTag("userId") Long userId) {
// Sleuth автоматически создаст span с тегом userId
return userRepository.existsById(userId);
}
}
# Spring Cloud Sleuth конфигурация
spring:
sleuth:
sampler:
probability: 0.1 # 10% sampling для production
zipkin:
base-url: http://zipkin:9411
web:
skip-pattern: "/health|/metrics|/actuator.*"
Jaeger
Jaeger — это end-to-end система распределённой трассировки от Uber, ныне CNCF проект. Более современная альтернатива Zipkin с лучшей производительностью.
Преимущества Jaeger:
- Высокая производительность (написан на Go)
- Adaptive sampling — умное сэмплирование
- Service dependency analysis
- Root cause analysis
- Интеграция с Prometheus для метрик
# Docker Compose для Jaeger
version: '3.8'
services:
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # Jaeger UI
- "14268:14268" # jaeger.thrift HTTP
- "14250:14250" # gRPC
environment:
- COLLECTOR_OTLP_ENABLED=true
// Jaeger с OpenTelemetry
@Configuration
public class TracingConfig {
@Bean
public OpenTelemetry openTelemetry() {
return OpenTelemetrySdk.builder()
.setTracerProvider(
SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(
JaegerGrpcSpanExporter.builder()
.setEndpoint("http://jaeger:14250")
.build())
.build())
.setResource(Resource.getDefault()
.merge(Resource.create(
Attributes.of(ResourceAttributes.SERVICE_NAME, "order-service"))))
.build())
.build();
}
}
Metrics (Метрики)
Prometheus
Prometheus — это time-series база данных и система мониторинга, разработанная в SoundCloud. Стала стандартом для сбора метрик в cloud-native окружениях.
Архитектура Prometheus:
- Prometheus Server — основной компонент для сбора и хранения метрик
- Pushgateway — для кратковременных jobs
- Exporters — для экспорта метрик из third-party систем
- Alertmanager — для обработки алертов
Типы метрик:
- Counter — монотонно возрастающее значение (количество запросов)
- Gauge — значение, которое может увеличиваться и уменьшаться (CPU usage)
- Histogram — распределение значений по buckets (response time)
- Summary — похож на Histogram, но с quantiles
// Micrometer с Prometheus
@RestController
public class OrderController {
private final Counter orderCounter;
private final Timer orderProcessingTimer;
private final Gauge activeOrdersGauge;
public OrderController(MeterRegistry meterRegistry) {
this.orderCounter = Counter.builder("orders.created.total")
.description("Total number of created orders")
.tag("service", "order-service")
.register(meterRegistry);
this.orderProcessingTimer = Timer.builder("order.processing.duration")
.description("Order processing time")
.register(meterRegistry);
this.activeOrdersGauge = Gauge.builder("orders.active")
.description("Number of active orders")
.register(meterRegistry, this, OrderController::getActiveOrdersCount);
}
@PostMapping("/orders")
public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
return Timer.Sample.start(orderProcessingTimer)
.stop(() -> {
Order order = orderService.create(request);
orderCounter.increment(Tags.of("status", "success"));
return ResponseEntity.ok(order);
});
}
private double getActiveOrdersCount() {
return orderService.getActiveOrdersCount();
}
}
# prometheus.yml конфигурация
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'microservices'
static_configs:
- targets: ['user-service:8080', 'order-service:8081', 'payment-service:8082']
metrics_path: '/actuator/prometheus'
scrape_interval: 5s
- job_name: 'spring-actuator'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['localhost:8080']
rule_files:
- "alert_rules.yml"
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
Grafana
Grafana — это платформа для мониторинга и observability с богатыми возможностями визуализации. Поддерживает множество источников данных.
Ключевые возможности:
- Создание dashboard с различными типами панелей
- Templating — переменные для динамических dashboard
- Alerting — настройка уведомлений
- Plugins — расширение функциональности
- Multi-tenancy — изоляция данных между командами
// Пример dashboard для микросервисов (JSON config)
{
"dashboard": {
"title": "Microservices Overview",
"panels": [
{
"title": "Request Rate",
"type": "graph",
"targets": [
{
"expr": "rate(http_requests_total[5m])",
"legendFormat": "{{service}} - {{method}} {{status}}"
}
]
},
{
"title": "Response Time",
"type": "graph",
"targets": [
{
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "95th percentile"
}
]
}
]
}
}
PromQL запросы для мониторинга микросервисов:
# Частота запросов по сервисам
rate(http_requests_total[5m])
# Процент ошибок
rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) * 100
# 95-й процентиль времени ответа
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
# Доступность сервиса
up{job="microservices"}
# Потребление CPU
rate(process_cpu_seconds_total[5m]) * 100
Alerts and Dashboards (Алерты и дашборды)
Настройка алертов в Prometheus
Alertmanager обрабатывает алерты, отправленные Prometheus сервером, группирует их, убирает дубликаты и отправляет уведомления.
# alert_rules.yml
groups:
- name: microservices_alerts
rules:
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "High error rate on {{ $labels.service }}"
description: "Error rate is {{ $value | humanizePercentage }} for service {{ $labels.service }}"
- alert: HighResponseTime
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "High response time on {{ $labels.service }}"
- alert: ServiceDown
expr: up{job="microservices"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Service {{ $labels.instance }} is down"
# alertmanager.yml
global:
smtp_smarthost: 'smtp.gmail.com:587'
smtp_from: 'alerts@company.com'
route:
group_by: ['alertname', 'service']
group_wait: 30s
group_interval: 5m
repeat_interval: 12h
receiver: 'web.hook'
routes:
- match:
severity: critical
receiver: 'critical-alerts'
receivers:
- name: 'web.hook'
webhook_configs:
- url: 'http://slack-webhook/alerts'
- name: 'critical-alerts'
email_configs:
- to: 'devops@company.com'
subject: 'Critical Alert: {{ .GroupLabels.alertname }}'
body: |
{{ range .Alerts }}
Alert: {{ .Annotations.summary }}
Description: {{ .Annotations.description }}
{{ end }}
slack_configs:
- api_url: 'https://hooks.slack.com/services/...'
channel: '#alerts'
title: 'Critical Alert'
SLI/SLO/SLA
Service Level Indicators (SLI) — метрики, которые измеряют уровень сервиса:
- Availability (доступность):
up{job="service"} == 1
- Latency (задержка):
http_request_duration_seconds
- Error rate (процент ошибок):
rate(http_requests_total{status=~"5.."}[5m])
Service Level Objectives (SLO) — целевые значения SLI:
- 99.9% availability
- 95% запросов быстрее 200ms
- < 1% error rate
Service Level Agreements (SLA) — договорные обязательства с клиентами
# Пример SLO как Prometheus rules
- record: sli:availability:rate5m
expr: up{job="user-service"}
- record: sli:latency:rate5m
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{job="user-service"}[5m]))
- record: sli:error_rate:rate5m
expr: rate(http_requests_total{job="user-service",status=~"5.."}[5m]) / rate(http_requests_total{job="user-service"}[5m])
- alert: SLOAvailabilityBreach
expr: sli:availability:rate5m < 0.999
for: 5m
labels:
severity: critical
Best Practices для Observability
Golden Signals (4 ключевые метрики):
- Latency — время отклика
- Traffic — количество запросов
- Errors — процент ошибок
- Saturation — загруженность ресурсов
RED Method (для request-driven систем):
- Rate — частота запросов
- Errors — количество ошибок
- Duration — время выполнения
USE Method (для ресурсов):
- Utilization — использование ресурса
- Saturation — очередь запросов к ресурсу
- Errors — ошибки ресурса
Structured Logging — логи в JSON формате для лучшего парсинга:
@Slf4j
@RestController
public class UserController {
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody CreateUserRequest request) {
String correlationId = MDC.get("correlationId");
log.info("Creating user",
kv("correlationId", correlationId),
kv("userId", request.getEmail()),
kv("action", "user_creation_started"));
try {
User user = userService.create(request);
log.info("User created successfully",
kv("correlationId", correlationId),
kv("userId", user.getId()),
kv("action", "user_creation_completed"));
return ResponseEntity.ok(user);
} catch (Exception e) {
log.error("Failed to create user",
kv("correlationId", correlationId),
kv("error", e.getMessage()),
kv("action", "user_creation_failed"));
throw e;
}
}
}
Доставка и инфраструктура
CI/CD пайплайны для микросервисов
Особенности CI/CD в микросервисной архитектуре
Continuous Integration/Continuous Deployment в микросервисах имеет свои специфические вызовы по сравнению с монолитом:
Основные принципы:
- Independent Deployment — каждый сервис деплоится независимо
- Service-specific Pipelines — отдельный пайплайн для каждого сервиса
- Shared Infrastructure — общие инструменты и практики
- Contract Testing — проверка совместимости API между сервисами
Структура monorepo vs polyrepo:
- Monorepo — все сервисы в одном репозитории, упрощает code sharing
- Polyrepo — каждый сервис в отдельном репо, больше автономии команд
Pipeline структура для микросервиса
# .github/workflows/ci-cd.yml для GitHub Actions
name: User Service CI/CD
on:
push:
branches: [main, develop]
paths: ['user-service/**'] # Запуск только при изменениях в user-service
pull_request:
branches: [main]
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
user-service-changed: ${{ steps.changes.outputs.user-service }}
steps:
- uses: actions/checkout@v3
- uses: dorny/paths-filter@v2
id: changes
with:
filters: |
user-service:
- 'user-service/**'
build-and-test:
needs: detect-changes
if: needs.detect-changes.outputs.user-service-changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Cache Maven dependencies
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
- name: Run tests
run: |
cd user-service
mvn clean test
- name: Build Docker image
run: |
cd user-service
docker build -t user-service:${{ github.sha }} .
- name: Run security scan
uses: anchore/scan-action@v3
with:
image: user-service:${{ github.sha }}
deploy-staging:
needs: build-and-test
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
steps:
- name: Deploy to staging
run: |
kubectl set image deployment/user-service user-service=user-service:${{ github.sha }} -n staging
kubectl rollout status deployment/user-service -n staging
deploy-production:
needs: build-and-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Deploy to production
run: |
helm upgrade user-service ./helm/user-service \
--set image.tag=${{ github.sha }} \
--namespace production
Multi-service orchestration
Deployment Orchestration — координация деплоя нескольких связанных сервисов:
# GitLab CI pipeline с зависимостями
stages:
- build
- test
- deploy-staging
- contract-tests
- deploy-production
variables:
DOCKER_REGISTRY: "registry.company.com"
# Общий template для микросервисов
.service-template: &service-template
stage: build
script:
- cd $SERVICE_DIR
- mvn clean package
- docker build -t $DOCKER_REGISTRY/$SERVICE_NAME:$CI_COMMIT_SHA .
- docker push $DOCKER_REGISTRY/$SERVICE_NAME:$CI_COMMIT_SHA
user-service-build:
<<: *service-template
variables:
SERVICE_DIR: "user-service"
SERVICE_NAME: "user-service"
order-service-build:
<<: *service-template
variables:
SERVICE_DIR: "order-service"
SERVICE_NAME: "order-service"
contract-tests:
stage: contract-tests
script:
# Pact contract testing
- mvn test -Dtest=ContractTest
dependencies:
- user-service-build
- order-service-build
Feature Branches и Environment Management
Environment Strategy для микросервисов:
- Feature Environments — динамически создаваемые окружения для feature branches
- Staging — стабильное окружение для интеграционных тестов
- Production — production окружение с blue/green или canary deployment
# Script для создания feature environment
#!/bin/bash
BRANCH_NAME=$(echo $CI_COMMIT_REF_NAME | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')
NAMESPACE="feature-$BRANCH_NAME"
# Создаем namespace
kubectl create namespace $NAMESPACE --dry-run=client -o yaml | kubectl apply -f -
# Деплоим сервисы с уникальными именами
helm install user-service-$BRANCH_NAME ./helm/user-service \
--namespace $NAMESPACE \
--set image.tag=$CI_COMMIT_SHA \
--set ingress.host="$BRANCH_NAME-user.dev.company.com"
Управление версиями и API-совместимость
Semantic Versioning (SemVer)
SemVer — это стандарт версионирования в формате MAJOR.MINOR.PATCH (например, 2.1.3):
- MAJOR — breaking changes, несовместимые изменения API
- MINOR — новая функциональность, обратно совместимая
- PATCH — bug fixes, обратно совместимые
В контексте микросервисов:
- Версионирование API endpoints
- Версионирование Docker образов
- Версионирование Helm charts
- Database schema versioning
// API versioning через URL path
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
@GetMapping("/{id}")
public UserV1 getUser(@PathVariable Long id) {
return userService.getUserV1(id);
}
}
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
@GetMapping("/{id}")
public UserV2 getUser(@PathVariable Long id) {
// V2 включает дополнительные поля
return userService.getUserV2(id);
}
}
API Evolution Strategies
Стратегии эволюции API без breaking changes:
1. Additive Changes — добавление новых полей:
// V1: базовая структура
public class UserV1 {
private Long id;
private String name;
private String email;
}
// V2: добавляем новые поля с default values
public class UserV2 {
private Long id;
private String name;
private String email;
private String phoneNumber; // новое поле
private LocalDateTime lastLoginAt; // новое поле
}
2. Header-based Versioning:
@RestController
public class UserController {
@GetMapping("/users/{id}")
public ResponseEntity<?> getUser(@PathVariable Long id,
@RequestHeader(value = "API-Version", defaultValue = "1") String version) {
switch (version) {
case "1":
return ResponseEntity.ok(userService.getUserV1(id));
case "2":
return ResponseEntity.ok(userService.getUserV2(id));
default:
return ResponseEntity.badRequest().body("Unsupported API version");
}
}
}
3. Content Negotiation:
@GetMapping(value = "/users/{id}", produces = "application/vnd.company.user-v1+json")
public UserV1 getUserV1(@PathVariable Long id) {
return userService.getUserV1(id);
}
@GetMapping(value = "/users/{id}", produces = "application/vnd.company.user-v2+json")
public UserV2 getUserV2(@PathVariable Long id) {
return userService.getUserV2(id);
}
Contract Testing с Pact
Pact — инструмент для contract testing между consumer и provider сервисами:
// Consumer test (Order Service)
@ExtendWith(PactConsumerTestExt.class)
class UserServiceContractTest {
@Pact(consumer = "order-service", provider = "user-service")
public RequestResponsePact getUserPact(PactDslWithProvider builder) {
return builder
.given("user exists")
.uponReceiving("get user by id")
.path("/api/v1/users/123")
.method("GET")
.willRespondWith()
.status(200)
.headers(Map.of("Content-Type", "application/json"))
.body(newJsonBody(body -> {
body.numberType("id", 123);
body.stringType("name", "John Doe");
body.stringType("email", "john@example.com");
}).build())
.toPact();
}
@Test
void testGetUser(MockServer mockServer) {
// Test consumer logic with mock
UserClient userClient = new UserClient(mockServer.getUrl());
User user = userClient.getUser(123L);
assertThat(user.getId()).isEqualTo(123L);
assertThat(user.getName()).isEqualTo("John Doe");
}
}
Database Schema Evolution
Flyway/Liquibase для управления изменениями схемы БД:
-- V1__Create_users_table.sql
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE
);
-- V2__Add_phone_to_users.sql
ALTER TABLE users ADD COLUMN phone_number VARCHAR(20);
-- V3__Add_last_login_to_users.sql
ALTER TABLE users ADD COLUMN last_login_at TIMESTAMP;
# application.yml для Flyway
spring:
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
validate-on-migrate: true
Контейнеризация и запуск
Docker для микросервисов
Docker обеспечивает изоляцию, портабельность и консистентность окружения для микросервисов.
Multi-stage Dockerfile для Java приложений:
# Multi-stage build для уменьшения размера образа
FROM maven:3.8.4-openjdk-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:resolve
COPY src ./src
RUN mvn clean package -DskipTests
FROM openjdk:17-jre-slim
WORKDIR /app
# Создаем пользователя для безопасности
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Копируем JAR из builder stage
COPY --from=builder /app/target/*.jar app.jar
# Настраиваем JVM для контейнера
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
# Healthcheck для container orchestration
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
USER appuser
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
Docker Compose для локальной разработки:
# docker-compose.yml
version: '3.8'
services:
user-service:
build: ./user-service
ports:
- "8081:8080"
environment:
- SPRING_PROFILES_ACTIVE=docker
- DATABASE_URL=jdbc:postgresql://postgres:5432/userdb
depends_on:
- postgres
- redis
networks:
- microservices-network
order-service:
build: ./order-service
ports:
- "8082:8080"
environment:
- USER_SERVICE_URL=http://user-service:8080
depends_on:
- user-service
networks:
- microservices-network
postgres:
image: postgres:13
environment:
POSTGRES_DB: userdb
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- microservices-network
redis:
image: redis:6-alpine
networks:
- microservices-network
networks:
microservices-network:
driver: bridge
volumes:
postgres_data:
Kubernetes основы
Kubernetes — это платформа оркестрации контейнеров, которая автоматизирует деплой, масштабирование и управление контейнеризованными приложениями.
Основные ресурсы Kubernetes:
- Pod — минимальная единица деплоя, содержит один или несколько контейнеров
- Deployment — управляет репликацией Pods и rolling updates
- Service — обеспечивает сетевой доступ к Pods
- Ingress — управляет внешним доступом к сервисам
- ConfigMap/Secret — управление конфигурацией и секретами
# deployment.yaml для микросервиса
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
labels:
app: user-service
version: v1
spec:
replicas: 3
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
version: v1
spec:
containers:
- name: user-service
image: user-service:1.2.3
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "kubernetes"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
selector:
app: user-service
ports:
- port: 80
targetPort: 8080
protocol: TCP
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: user-service-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: api.company.com
http:
paths:
- path: /users
pathType: Prefix
backend:
service:
name: user-service
port:
number: 80
Kubernetes Patterns для микросервисов
Sidecar Pattern — дополнительный контейнер в Pod для вспомогательных функций:
# Pod с sidecar для логирования
apiVersion: v1
kind: Pod
metadata:
name: user-service-with-sidecar
spec:
containers:
- name: user-service
image: user-service:latest
volumeMounts:
- name: logs
mountPath: /app/logs
- name: log-shipper # Sidecar контейнер
image: fluent/fluent-bit:latest
volumeMounts:
- name: logs
mountPath: /app/logs
readOnly: true
- name: fluent-bit-config
mountPath: /fluent-bit/etc
volumes:
- name: logs
emptyDir: {}
- name: fluent-bit-config
configMap:
name: fluent-bit-config
Helm - пакетный менеджер для Kubernetes
Helm — это пакетный менеджер для Kubernetes, который упрощает управление сложными приложениями через templates и charts.
Структура Helm Chart:
user-service/
├── Chart.yaml # Метаданные chart
├── values.yaml # Значения по умолчанию
├── templates/
│ ├── deployment.yaml # Template для Deployment
│ ├── service.yaml # Template для Service
│ ├── ingress.yaml # Template для Ingress
│ └── configmap.yaml # Template для ConfigMap
└── charts/ # Зависимости (subcharts)
# Chart.yaml
apiVersion: v2
name: user-service
description: User Service Helm Chart
version: 1.0.0
appVersion: "1.2.3"
dependencies:
- name: postgresql
version: 11.6.12
repository: https://charts.bitnami.com/bitnami
# values.yaml
replicaCount: 3
image:
repository: user-service
tag: "1.2.3"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
targetPort: 8080
ingress:
enabled: true
host: api.company.com
path: /users
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
postgresql:
enabled: true
postgresqlUsername: user_service
postgresqlDatabase: users
# templates/deployment.yaml с Helm templating
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "user-service.fullname" . }}
labels:
{{- include "user-service.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "user-service.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "user-service.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: {{ .Values.service.targetPort }}
env:
- name: DATABASE_URL
value: "jdbc:postgresql://{{ include "user-service.fullname" . }}-postgresql:5432/{{ .Values.postgresql.postgresqlDatabase }}"
resources:
{{- toYaml .Values.resources | nindent 12 }}
Helm Commands:
# Установка chart
helm install user-service ./user-service --namespace production
# Обновление с новыми значениями
helm upgrade user-service ./user-service \
--set image.tag=1.3.0 \
--set replicaCount=5
# Откат к предыдущей версии
helm rollback user-service 1
# Удаление
helm uninstall user-service
GitOps с ArgoCD
GitOps — подход к управлению инфраструктурой, где Git является единственным источником истины для декларативной инфраструктуры и приложений.
# ArgoCD Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: user-service
namespace: argocd
spec:
project: microservices
source:
repoURL: https://github.com/company/k8s-manifests
targetRevision: HEAD
path: user-service
helm:
valueFiles:
- values-production.yaml
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Best Practices для доставки микросервисов
Immutable Infrastructure — инфраструктура не изменяется, а заменяется:
- Использование тегов Docker образов вместо latest
- Versioned Helm charts
- Infrastructure as Code (Terraform, Pulumi)
Progressive Delivery — безопасный выкат новых версий:
- Feature flags для управления функциональностью
- Canary deployments для постепенного выката
- Automated rollback при обнаружении проблем
Security by Design:
- Container image scanning
- Kubernetes RBAC
- Network policies
- Secret management (Vault, Kubernetes Secrets)
Monitoring and Observability:
- Health checks на всех уровнях
- Distributed tracing
- Centralized logging
- Custom metrics для business logic
Тестирование микросервисов
Пирамида тестирования в микросервисах
Test Pyramid в микросервисной архитектуре отличается от монолитной и включает дополнительные слои:
Структура пирамиды (снизу вверх):
- Unit Tests — быстрые, изолированные тесты бизнес-логики
- Integration Tests — тестирование взаимодействия с внешними системами
- Contract Tests — проверка совместимости API между сервисами
- Component Tests — тестирование сервиса как черного ящика
- End-to-End Tests — минимальное количество, только critical user journeys
Ключевые принципы:
- Больше быстрых и дешевых тестов внизу пирамиды
- Меньше медленных и дорогих тестов вверху
- Contract tests заменяют многие интеграционные тесты
- Избегание shared test environments
Contract Testing
Что такое Contract Testing
Contract Testing — это техника тестирования, которая проверяет совместимость интерфейсов между сервисами без необходимости запуска всех зависимых сервисов.
Основные концепции:
- Consumer — сервис, который потребляет API
- Provider — сервис, который предоставляет API
- Contract — формальное описание взаимодействия между Consumer и Provider
- Pact — файл, содержащий контракт (expectations)
Преимущества:
- Быстрое выявление breaking changes
- Независимое тестирование сервисов
- Документирование API взаимодействий
- Возможность parallel development
Spring Cloud Contract
Spring Cloud Contract — это фреймворк от Pivotal для contract-driven development, особенно удобный для Spring Boot приложений.
Consumer-Driven Contracts (CDC) подход:
- Consumer определяет ожидания (contract)
- Provider реализует API согласно контракту
- Автоматическая генерация тестов для Provider
- Автоматическая генерация stubs для Consumer
// Contract definition (Groovy DSL)
// src/test/resources/contracts/user_service/should_return_user_by_id.groovy
Contract.make {
description "Should return user by ID"
request {
method GET()
url value(consumer(regex('/users/[0-9]+')), producer('/users/123'))
headers {
contentType(applicationJson())
}
}
response {
status OK()
headers {
contentType(applicationJson())
}
body([
id: value(producer(123), consumer(anyPositiveInt())),
name: value(producer("John Doe"), consumer(anyNonEmptyString())),
email: value(producer("john@example.com"), consumer(anyEmail())),
createdAt: value(producer("2024-01-15T10:30:00Z"), consumer(anyIso8601WithOffset()))
])
}
}
// Provider test (автоматически генерируется)
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureStubRunner(
ids = "com.company:user-service:+:stubs:8080",
stubsMode = StubRunnerProperties.StubsMode.LOCAL
)
class UserServiceContractTest {
@Autowired
private MockMvc mockMvc;
@Test
void should_return_user_by_id() throws Exception {
mockMvc.perform(get("/users/123")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(123))
.andExpect(jsonPath("$.name").value("John Doe"))
.andExpect(jsonPath("$.email").value("john@example.com"));
}
}
// Consumer test с WireMock stubs
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureWireMock(port = 8080)
class OrderServiceTest {
@Autowired
private OrderService orderService;
@Test
void should_create_order_for_existing_user() {
// Stub от Spring Cloud Contract автоматически настроен
stubFor(get(urlEqualTo("/users/123"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"id\":123,\"name\":\"John Doe\",\"email\":\"john@example.com\"}")));
CreateOrderRequest request = new CreateOrderRequest(123L, "Product A", 100.0);
Order order = orderService.createOrder(request);
assertThat(order.getUserId()).isEqualTo(123L);
assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING);
}
}
Pact Framework
Pact — это популярный open-source инструмент для contract testing, поддерживающий множество языков программирования.
Workflow Pact:
- Consumer создает Pact (описание взаимодействия)
- Pact публикуется в Pact Broker
- Provider загружает Pact и верифицирует его
- Results публикуются обратно в Broker
// Consumer test (Order Service)
@ExtendWith(PactConsumerTestExt.class)
class OrderServicePactTest {
@Pact(consumer = "order-service", provider = "user-service")
public RequestResponsePact getUserPact(PactDslWithProvider builder) {
return builder
.given("user with ID 123 exists")
.uponReceiving("get user by ID")
.path("/api/v1/users/123")
.method("GET")
.headers("Accept", "application/json")
.willRespondWith()
.status(200)
.headers(Map.of("Content-Type", "application/json"))
.body(newJsonBody(body -> {
body.numberType("id", 123);
body.stringType("name", "John Doe");
body.stringMatcher("email", ".*@.*", "john@example.com");
body.stringType("status", "ACTIVE");
}).build())
.toPact();
}
@Test
@PactTestFor(pactMethod = "getUserPact")
void testGetUser(MockServer mockServer) {
UserServiceClient client = new UserServiceClient(mockServer.getUrl());
User user = client.getUser(123L);
assertThat(user.getId()).isEqualTo(123L);
assertThat(user.getName()).isEqualTo("John Doe");
assertThat(user.getEmail()).isEqualTo("john@example.com");
}
}
// Provider verification (User Service)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Provider("user-service")
@PactFolder("pacts")
class UserServiceProviderPactTest {
@LocalServerPort
private int port;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", port));
}
@State("user with ID 123 exists")
void userExists() {
// Подготавливаем данные для теста
User user = new User(123L, "John Doe", "john@example.com", UserStatus.ACTIVE);
userRepository.save(user);
}
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void verifyPact(PactVerificationContext context) {
context.verifyInteraction();
}
}
Pact Broker для управления контрактами:
# docker-compose.yml для Pact Broker
version: '3.8'
services:
pact-broker:
image: pactfoundation/pact-broker:latest
ports:
- "9292:9292"
environment:
PACT_BROKER_DATABASE_URL: "postgres://postgres:password@postgres/pactbroker"
depends_on:
- postgres
postgres:
image: postgres:13
environment:
POSTGRES_DB: pactbroker
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
Integration Testing
Testcontainers
Testcontainers — это Java библиотека, которая позволяет запускать Docker контейнеры в JUnit тестах. Идеально подходит для интеграционного тестирования с реальными внешними зависимостями.
Основные возможности:
- Запуск любых Docker образов в тестах
- Автоматическое управление lifecycle контейнеров
- Поддержка популярных технологий (PostgreSQL, Redis, Kafka, etc.)
- Network isolation между тестами
// Интеграционный тест с PostgreSQL
@SpringBootTest
@Testcontainers
class UserRepositoryIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private UserRepository userRepository;
@Test
void should_save_and_find_user() {
User user = new User("John Doe", "john@example.com");
User saved = userRepository.save(user);
Optional<User> found = userRepository.findById(saved.getId());
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("John Doe");
}
}
// Тест с несколькими контейнерами
@SpringBootTest
@Testcontainers
class OrderServiceIntegrationTest {
@Container
static Network network = Network.newNetwork();
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
.withNetwork(network)
.withNetworkAliases("postgres");
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:6-alpine")
.withNetwork(network)
.withNetworkAliases("redis")
.withExposedPorts(6379);
@Container
static MockServerContainer mockServer = new MockServerContainer(DockerImageName.parse("mockserver/mockserver"))
.withNetwork(network)
.withNetworkAliases("mockserver");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
registry.add("spring.redis.host", redis::getHost);
registry.add("spring.redis.port", redis::getFirstMappedPort);
registry.add("external.user-service.url",
() -> "http://" + mockServer.getHost() + ":" + mockServer.getServerPort());
}
@Test
void should_create_order_with_user_validation() {
// Setup mock for user service
MockServerClient client = new MockServerClient(mockServer.getHost(), mockServer.getServerPort());
client.when(request().withPath("/users/123"))
.respond(response().withStatusCode(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"id\":123,\"name\":\"John Doe\"}"));
// Test order creation
CreateOrderRequest request = new CreateOrderRequest(123L, "Product A", 100.0);
Order order = orderService.createOrder(request);
assertThat(order.getUserId()).isEqualTo(123L);
}
}
WireMock
WireMock — это библиотека для создания HTTP моков и стабов. Позволяет имитировать внешние API для изолированного тестирования.
Основные возможности:
- HTTP/HTTPS мокирование
- Request matching по URL, headers, body
- Response templating
- Stateful behavior
- Fault injection для тестирования устойчивости
// Standalone WireMock server
@SpringBootTest
class PaymentServiceTest {
private WireMockServer wireMockServer;
@BeforeEach
void setUp() {
wireMockServer = new WireMockServer(8089);
wireMockServer.start();
configureFor("localhost", 8089);
}
@AfterEach
void tearDown() {
wireMockServer.stop();
}
@Test
void should_process_payment_successfully() {
// Setup mock response
stubFor(post(urlEqualTo("/api/payments"))
.withHeader("Content-Type", equalTo("application/json"))
.withRequestBody(matchingJsonPath("$.amount", equalTo("100.0")))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"transactionId\":\"tx-123\",\"status\":\"SUCCESS\"}")
.withFixedDelay(100))); // Simulate network latency
PaymentRequest request = new PaymentRequest("100.0", "USD", "card-token");
PaymentResponse response = paymentService.processPayment(request);
assertThat(response.getTransactionId()).isEqualTo("tx-123");
assertThat(response.getStatus()).isEqualTo("SUCCESS");
// Verify the request was made
verify(postRequestedFor(urlEqualTo("/api/payments"))
.withHeader("Authorization", containing("Bearer")));
}
@Test
void should_handle_payment_service_timeout() {
// Test timeout scenario
stubFor(post(urlEqualTo("/api/payments"))
.willReturn(aResponse()
.withFixedDelay(5000))); // 5 second delay
PaymentRequest request = new PaymentRequest("100.0", "USD", "card-token");
assertThatThrownBy(() -> paymentService.processPayment(request))
.isInstanceOf(PaymentTimeoutException.class);
}
}
// WireMock с Spring Boot интеграцией
@SpringBootTest
@AutoConfigureWireMock(port = 0) // Random port
class UserServiceClientTest {
@Value("${wiremock.server.port}")
private int wiremockPort;
@Test
void should_handle_various_response_scenarios() {
// Success scenario
stubFor(get(urlEqualTo("/users/1"))
.willReturn(okJson("{\"id\":1,\"name\":\"John\"}")));
// Not found scenario
stubFor(get(urlEqualTo("/users/999"))
.willReturn(aResponse().withStatus(404)));
// Server error scenario
stubFor(get(urlEqualTo("/users/500"))
.willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
// Circuit breaker scenario - multiple failures
stubFor(get(urlMatching("/users/.*"))
.inScenario("Circuit Breaker")
.whenScenarioStateIs(STARTED)
.willReturn(aResponse().withStatus(500))
.willSetStateTo("FAILED_ONCE"));
stubFor(get(urlMatching("/users/.*"))
.inScenario("Circuit Breaker")
.whenScenarioStateIs("FAILED_ONCE")
.willReturn(aResponse().withStatus(500))
.willSetStateTo("FAILED_TWICE"));
}
}
Component Testing
Component Tests тестируют сервис как черный ящик, используя его публичный API, но с замокированными внешними зависимостями.
// Component test с полным Spring контекстом
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@AutoConfigureWireMock(port = 0)
class OrderServiceComponentTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13");
@Autowired
private TestRestTemplate restTemplate;
@LocalServerPort
private int port;
@Test
void should_create_order_end_to_end() {
// Mock external user service
stubFor(get(urlEqualTo("/users/123"))
.willReturn(okJson("{\"id\":123,\"name\":\"John Doe\",\"status\":\"ACTIVE\"}")));
// Mock external payment service
stubFor(post(urlEqualTo("/payments"))
.willReturn(okJson("{\"transactionId\":\"tx-123\",\"status\":\"SUCCESS\"}")));
// Create order via HTTP API
CreateOrderRequest request = new CreateOrderRequest(123L, "Product A", 100.0);
ResponseEntity<Order> response = restTemplate.postForEntity(
"http://localhost:" + port + "/orders",
request,
Order.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody().getUserId()).isEqualTo(123L);
assertThat(response.getBody().getStatus()).isEqualTo(OrderStatus.CONFIRMED);
// Verify external calls were made
verify(getRequestedFor(urlEqualTo("/users/123")));
verify(postRequestedFor(urlEqualTo("/payments")));
}
}
Chaos Engineering
Что такое Chaos Engineering
Chaos Engineering — это дисциплина экспериментирования с распределенными системами для повышения уверенности в способности системы выдерживать турбулентные условия в production.
Принципы Chaos Engineering:
- Гипотеза о стационарном состоянии — определить нормальное поведение
- Вариация реальных событий — имитировать реальные сбои
- Запуск экспериментов в production — реальные условия дают реальные результаты
- Автоматизация — постоянное выполнение экспериментов
- Минимизация blast radius — ограничение влияния экспериментов
Chaos Monkey for Spring Boot
Chaos Monkey for Spring Boot — это инструмент для внедрения хаоса в Spring Boot приложения на уровне кода.
Типы атак (Assaults):
- Latency Assault — добавление задержек
- Exception Assault — выброс исключений
- AppKiller Assault — завершение приложения
- Memory Assault — потребление памяти
# application.yml конфигурация Chaos Monkey
chaos:
monkey:
enabled: true
watcher:
component: true
controller: true
restController: true
service: true
repository: true
assaults:
level: 5 # 1-1000, вероятность атаки
latencyRangeStart: 1000 # мин задержка (мс)
latencyRangeEnd: 3000 # макс задержка (мс)
latencyActive: true
exceptionsActive: true
killApplicationActive: false
memoryActive: false
settings:
watchedCustomServices:
- com.company.orderservice
// Кастомный Chaos Monkey watcher
@Component
@ConditionalOnProperty("chaos.monkey.enabled")
public class ChaosMonkeyUserService {
@Autowired
private ChaosMonkeyRequestScope chaosMonkeyRequestScope;
private final UserRepository userRepository;
public ChaosMonkeyUserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User findById(Long id) {
// Chaos Monkey может внедрить хаос здесь
chaosMonkeyRequestScope.callChaosMonkey();
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
}
}
// Кастомные Assault для специфических сценариев
@Component
public class CustomDatabaseAssault implements Assault {
private final DataSource dataSource;
@Override
public boolean isActive() {
return true; // Активен ли этот assault
}
@Override
public void attack() {
try {
// Имитируем проблемы с БД
Thread.sleep(ThreadLocalRandom.current().nextInt(2000, 5000));
if (ThreadLocalRandom.current().nextDouble() < 0.3) {
throw new DataAccessException("Chaos Monkey: Database connection failed") {};
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Chaos Engineering инструменты
Litmus — Cloud-Native Chaos Engineering платформа для Kubernetes:
# Litmus ChaosExperiment для pod deletion
apiVersion: litmuschaos.io/v1alpha1
kind: ChaosExperiment
metadata:
name: pod-delete
spec:
definition:
scope: Namespaced
permissions:
- apiGroups: [""]
resources: ["pods"]
verbs: ["create","delete","get","list","patch","update"]
image: "litmuschaos/go-runner:latest"
args:
- -c
- ./experiments -name pod-delete
command:
- /bin/bash
env:
- name: TOTAL_CHAOS_DURATION
value: '15'
- name: CHAOS_INTERVAL
value: '5'
- name: FORCE
value: 'true'
Gremlin — коммерческая платформа для Chaos Engineering:
- Network failures (latency, packet loss, DNS failures)
- Resource exhaustion (CPU, memory, disk)
- State failures (process killer, shutdown)
Circuit Breaker Testing с Chaos
// Тестирование Circuit Breaker с хаосом
@SpringBootTest
@EnableChaosMonkey
class CircuitBreakerChaosTest {
@Autowired
private PaymentService paymentService;
@Autowired
private ChaosMonkeyProperties chaosMonkeyProperties;
@Test
void should_open_circuit_breaker_under_failures() {
// Включаем агрессивные failures
chaosMonkeyProperties.getAssaults().setLevel(800); // 80% failures
chaosMonkeyProperties.getAssaults().setExceptionsActive(true);
// Делаем несколько вызовов, чтобы открыть circuit breaker
IntStream.range(0, 10).forEach(i -> {
try {
paymentService.processPayment(new PaymentRequest("100", "USD", "token"));
} catch (Exception e) {
// Ожидаем исключения из-за Chaos Monkey
}
});
// Проверяем, что circuit breaker открылся
assertThatThrownBy(() -> paymentService.processPayment(new PaymentRequest("100", "USD", "token")))
.isInstanceOf(CircuitBreakerOpenException.class);
}
}
Мониторинг Chaos Experiments
// Metrics для отслеживания chaos experiments
@Component
public class ChaosMetrics {
private final Counter chaosAttacksCounter;
private final Timer chaosRecoveryTimer;
public ChaosMetrics(MeterRegistry meterRegistry) {
this.chaosAttacksCounter = Counter.builder("chaos.attacks.total")
.description("Total number of chaos attacks")
.tag("type", "chaos")
.register(meterRegistry);
this.chaosRecoveryTimer = Timer.builder("chaos.recovery.time")
.description("Time to recover from chaos attack")
.register(meterRegistry);
}
public void recordChaosAttack(String assaultType) {
chaosAttacksCounter.increment(Tags.of("assault", assaultType));
}
public void recordRecoveryTime(Duration recoveryTime) {
chaosRecoveryTimer.record(recoveryTime);
}
}
Best Practices для тестирования микросервисов
Test Data Management:
- Используйте Database per Test для изоляции
- Test Data Builders для создания тестовых данных
- Cleanup стратегии для интеграционных тестов
Performance Testing:
- Load testing отдельных микросервисов
- Contract performance testing
- Distributed system performance testing
Security Testing:
- Authentication/Authorization testing
- API security testing (OWASP)
- Container security scanning
Monitoring Test Execution:
- Test metrics и reporting
- Flaky test detection
- Test execution time monitoring