Разделение на сервисы

Основы микросервисной архитектуры

Что такое микросервисы

Микросервисы — это архитектурный подход, при котором приложение разбивается на набор небольших, независимо развертываемых сервисов, каждый из которых работает в собственном процессе и взаимодействует через четко определенные 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

Процесс выявления:

  1. Event Storming — моделирование доменных событий
  2. Анализ терминологии — выявление различий в понимании терминов
  3. Организационные границы — команды, отделы, процессы
  4. Источники данных — различные системы и базы данных

Пример выявления контекстов в банковской системе:

// Контекст "Управление клиентами"
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;
    }
}

Вопросы для собеседования

  1. Когда использовать синхронную, а когда асинхронную коммуникацию?
  2. Чем отличается Kafka от RabbitMQ?
  3. Что такое Event Sourcing и когда его применять?
  4. Различия между API Gateway и Service Mesh?
  5. Как обеспечить идемпотентность в REST API?
  6. Паттерны для обработки ошибок в микросервисах?
  7. Как реализовать распределённые транзакции?
  8. Стратегии версионирования API?
  9. Что такое Circuit Breaker и как он работает?
  10. Как организовать мониторинг межсервисной коммуникации?

Управление согласованностью

Основные проблемы согласованности

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();
    }
}

Антипаттерны

Избегайте

  1. Distributed Transactions (2PC) — блокирующие, не масштабируемые
  2. Saga без компенсаций — нет возможности откатиться
  3. Синхронные Saga — увеличивают латентность
  4. Забытые timeout'ы — зависшие транзакции
  5. Игнорирование идемпотентности — дублирование операций
// ПЛОХО - синхронная 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);
}

Вопросы для собеседования

  1. Чем отличается Choreography от Orchestration Saga?
  2. Как обеспечить атомарность между изменением данных и публикацией события?
  3. Что такое Transaction Outbox и когда его использовать?
  4. Как обрабатывать eventual consistency в пользовательском интерфейсе?
  5. Какие стратегии компенсации знаете?
  6. Как мониторить состояние Saga?
  7. Различия между 2PC и Saga?
  8. Как обеспечить идемпотентность компенсирующих операций?
  9. Что делать с зависшими Saga?
  10. Паттерны для обработки конфликтов в eventual consistency?

Надежность и отказоустойчивость

Основные принципы

Типы отказов

Transient failures — временные сбои (сетевые задержки, временная недоступность) Persistent failures — постоянные сбои (неправильная конфигурация, bugs) Cascading failures — каскадные сбои, когда отказ одного компонента вызывает отказ других

Defensive Programming

Fail Fast — быстро обнаруживать и сообщать об ошибках Fail Safe — продолжать работу в безопасном режиме при сбоях Graceful Degradation — постепенное ухудшение функциональности вместо полного отказа

Circuit Breaker

Circuit Breaker — паттерн защиты от каскадных сбоев, имитирующий электрический автоматический выключатель.

Состояния Circuit Breaker

  1. CLOSED — нормальная работа, запросы проходят
  2. OPEN — слишком много ошибок, запросы блокируются
  3. 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);
    }
}

Антипаттерны и проблемы

Избегайте

  1. Hardcoded Endpoints — не используйте статические IP/порты
  2. Missing Health Checks — всегда реализуйте health endpoints
  3. Ignoring Timeouts — настройте таймауты для всех взаимодействий
  4. Single Point of Failure — используйте кластеры service registry
  5. 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);
        }
    }
}

Вопросы для собеседования

  1. Чем отличается client-side от server-side service discovery?
  2. Какие преимущества и недостатки у Eureka vs Consul?
  3. Как обеспечить graceful shutdown сервиса при использовании service registry?
  4. Какие стратегии load balancing знаете и когда их применять?
  5. Как обрабатывать ситуацию, когда service registry недоступен?
  6. Что должен включать health check endpoint?
  7. Как реализовать sticky sessions в микросервисах?
  8. Различия между health checks и readiness probes?
  9. Как мониторить эффективность load balancing?
  10. Стратегии для обновления сервисов без downtime (blue-green, canary)?
  11. Как обеспечить безопасность service-to-service коммуникации?
  12. Что такое 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:

  1. Blue среда обслуживает production
  2. Новая версия деплоится в Green среду
  3. Тестирование в Green среде
  4. Переключение трафика на Green
  5. 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:

  1. Деплой новой версии рядом со старой
  2. Направление 5-10% трафика на новую версию
  3. Мониторинг метрик (error rate, latency, business KPIs)
  4. Постепенное увеличение трафика (10% → 25% → 50% → 100%)
  5. Полное переключение или 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 — это способность понимать внутреннее состояние системы по её внешним выходным данным. В микросервисной архитектуре включает три ключевых компонента:

  1. Logs — структурированные записи событий с временными метками
  2. Metrics — числовые измерения производительности системы во времени
  3. 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 ключевые метрики):

  1. Latency — время отклика
  2. Traffic — количество запросов
  3. Errors — процент ошибок
  4. 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 в микросервисной архитектуре отличается от монолитной и включает дополнительные слои:

Структура пирамиды (снизу вверх):

  1. Unit Tests — быстрые, изолированные тесты бизнес-логики
  2. Integration Tests — тестирование взаимодействия с внешними системами
  3. Contract Tests — проверка совместимости API между сервисами
  4. Component Tests — тестирование сервиса как черного ящика
  5. 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) подход:

  1. Consumer определяет ожидания (contract)
  2. Provider реализует API согласно контракту
  3. Автоматическая генерация тестов для Provider
  4. Автоматическая генерация 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:

  1. Consumer создает Pact (описание взаимодействия)
  2. Pact публикуется в Pact Broker
  3. Provider загружает Pact и верифицирует его
  4. 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:

  1. Гипотеза о стационарном состоянии — определить нормальное поведение
  2. Вариация реальных событий — имитировать реальные сбои
  3. Запуск экспериментов в production — реальные условия дают реальные результаты
  4. Автоматизация — постоянное выполнение экспериментов
  5. Минимизация 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