Микросервисы на C#: архитектура и практика продвинутого уровня
Переход от монолита к микросервисной архитектуре на платформе .NET — это не просто техническое решение, а стратегический шаг к масштабируемости и отказоустойчивости. Однако на продвинутом уровне разработчики сталкиваются с нетривиальными проблемами: распределенные транзакции, согласованность данных и сложная оркестрация. В этой статье мы разберем архитектурные паттерны, которые помогут построить надежную микросервисную экосистему на C#, и рассмотрим практические примеры кода.
1. Паттерн Saga: управление распределенными транзакциями
В микросервисах традиционные ACID-транзакции невозможны. Паттерн Saga решает проблему согласованности данных, разбивая глобальную операцию на последовательность локальных транзакций с компенсирующими действиями.
Хореография vs. Оркестрация:
- Хореография — каждый сервис публикует события и реагирует на них. Подходит для простых сценариев.
- Оркестрация — центральный координатор (Orchestrator) управляет шагами. Рекомендуется для сложных бизнес-процессов.
Пример реализации оркестратора на C# с использованием MassTransit:
public class OrderSaga : MassTransitStateMachine<OrderSagaState>
{
public State Submitted { get; set; }
public State PaymentPending { get; set; }
public State Completed { get; set; }
public Event<OrderSubmitted> OrderSubmitted { get; set; }
public Event<PaymentProcessed> PaymentProcessed { get; set; }
public Event<PaymentFailed> PaymentFailed { get; set; }
public OrderSaga()
{
InstanceState(x => x.CurrentState);
Initially(
When(OrderSubmitted)
.Then(context => context.Saga.OrderId = context.Message.OrderId)
.TransitionTo(Submitted)
.Publish(context => new ProcessPayment
{
OrderId = context.Saga.OrderId,
Amount = context.Message.Amount
})
);
During(Submitted,
When(PaymentProcessed)
.TransitionTo(Completed)
.Finalize(),
When(PaymentFailed)
.Then(context => Console.WriteLine("Компенсация: отмена заказа"))
.TransitionTo(Final)
);
}
}
Этот код демонстрирует, как оркестратор управляет жизненным циклом заказа: при успешной оплате сага завершается, при ошибке — запускается компенсация.
2. CQRS и Event Sourcing: разделение ответственности
Паттерн CQRS (Command Query Responsibility Segregation) разделяет команды (запись) и запросы (чтение) на разные модели. В сочетании с Event Sourcing (ES) он обеспечивает полное аудирование и гибкость.
Архитектура CQRS+ES:
- Команды — изменяют состояние через события.
- События — хранятся в Event Store (например, EventStoreDB).
- Проекции — строят read-модели для быстрых запросов.
Пример команды и обработчика:
public class CreateOrderCommand : ICommand
{
public Guid OrderId { get; set; }
public decimal Amount { get; set; }
}
public class CreateOrderHandler : ICommandHandler<CreateOrderCommand>
{
private readonly IEventStore _eventStore;
public async Task Handle(CreateOrderCommand command)
{
var orderCreated = new OrderCreatedEvent
{
OrderId = command.OrderId,
Amount = command.Amount,
Timestamp = DateTime.UtcNow
};
await _eventStore.AppendEventAsync(
$"order-{command.OrderId}",
orderCreated
);
}
}
Read-модель строится отдельно. Например, для получения списка заказов:
public class OrderReadModel
{
public Guid Id { get; set; }
public decimal TotalAmount { get; set; }
public string Status { get; set; }
}
public class OrderQueryService
{
private readonly IDbConnection _readDb;
public async Task<IEnumerable<OrderReadModel>> GetOrdersAsync()
{
const string sql = "SELECT Id, TotalAmount, Status FROM Orders";
return await _readDb.QueryAsync<OrderReadModel>(sql);
}
}
Такой подход позволяет оптимизировать чтение под конкретные сценарии, не затрагивая модель записи.
3. Продвинутая коммуникация: брокеры сообщений и Resiliency
Синхронное взаимодействие (HTTP/gRPC) в микросервисах создает жесткие связи. На продвинутом уровне предпочтение отдается асинхронной коммуникации через брокеры сообщений (RabbitMQ, Kafka).
Паттерн Outbox: Гарантирует доставку сообщений при сохранении данных в БД.
public class OrderService
{
private readonly AppDbContext _db;
private readonly IMessageBus _bus;
public async Task CreateOrderAsync(Order order)
{
// 1. Сохраняем заказ и сообщение в одной транзакции
using var transaction = await _db.Database.BeginTransactionAsync();
_db.Orders.Add(order);
_db.OutboxMessages.Add(new OutboxMessage
{
Type = "OrderCreated",
Payload = JsonSerializer.Serialize(order),
CreatedAt = DateTime.UtcNow
});
await _db.SaveChangesAsync();
await transaction.CommitAsync();
// 2. Отдельный процесс отправляет сообщения из Outbox
}
}
Resiliency с Polly: Обработка временных сбоев.
var retryPolicy = Policy
.Handle<HttpRequestException>()
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
var circuitBreaker = Policy
.Handle<TimeoutException>()
.CircuitBreakerAsync(5, TimeSpan.FromMinutes(1));
await retryPolicy.WrapAsync(circuitBreaker)
.ExecuteAsync(() => _paymentService.ProcessPaymentAsync(order));
Эти техники критически важны для production-систем, где отказ одного сервиса не должен парализовать всю систему.
4. Мониторинг и Observability в .NET микросервисах
Без централизованного мониторинга микросервисная архитектура превращается в "черный ящик". Продвинутые команды используют OpenTelemetry для сбора метрик, трейсов и логов.
Настройка OpenTelemetry в ASP