Форум программистов, компьютерный форум, киберфорум
UnmanagedCoder
Войти
Регистрация
Восстановить пароль

Введение в Dapr для разработчиков .NET

Запись от UnmanagedCoder размещена 18.03.2025 в 14:46
Показов 1911 Комментарии 0

Нажмите на изображение для увеличения
Название: 1d469389-49aa-4f0c-8f63-a7027eb014c6.jpg
Просмотров: 73
Размер:	205.8 Кб
ID:	10449
Разработка распределенных систем никогда не была настолько востребованной и одновременно такой сложной. Если вы .NET разработчик, то наверняка сталкивались с необходимостью жонглировать обнаружением сервисов, управлением состоянием, обменом сообщениями и интеграцией с разнообразными инфраструктурными API. Бизнес-логика тонет под горами служебного кода, а решения становятся тесно связанными с конкретными технологиями. А что если существует лучший способ?

Dapr (Distributed Application Runtime) — это открытый проект, который берет на себя сложные инфраструктурные задачи, позволяя сосредоточиться на самом важном: бизнес-логике вашего приложения. По сути, это набор строительных блоков, обеспечивающих стандартизированный подход к решению типичных проблем в микросервисной архитектуре. Представьте, что вы разрабатываете микросервисную систему без Dapr. Вероятно, ваш код будет выглядеть примерно так:

C#
1
2
3
4
5
6
7
8
9
10
// Традиционный подход — прямые зависимости от инфраструктуры
builder.Services.AddStackExchangeRedisCache(options => { 
    options.Configuration = "redis:6379"; 
});
 
builder.Services.AddSingleton<IMessageBroker>(provider => 
    new KafkaMessageBroker("kafka:9092"));
 
builder.Services.AddSingleton<ISecretManager>(provider =>
    new AzureKeyVaultClient(new Uri("https://myvault.vault.azure.net")));
Этот код жестко привязывает приложение к Redis для кеширования, Kafka для обмена сообщениями и Azure Key Vault для хранения секретов. Представьте, что завтра ваша команда решит перейти на другой брокер сообщений или облачный провайдер — придется переписывать значительную часть кода. С Dapr же картина меняется кардинально:

C#
1
2
3
4
5
6
7
8
9
10
11
12
// Подход с Dapr — простые, согласованные API
builder.Services.AddDaprClient();
 
// Пример использования в коде:
// Управление состоянием (может быть Redis, Cosmos DB и т.д.)
await daprClient.SaveStateAsync("statestore", "customer-123", customerData);
 
// Обмен сообщениями (может быть Kafka, RabbitMQ и т.д.)
await daprClient.PublishEventAsync("pubsub", "orders", orderData);
 
// Секреты (может быть Azure Key Vault, HashiCorp Vault и т.д.)
var secret = await daprClient.GetSecretAsync("secretstore", "api-keys");
Различие заметно невооруженным глазом: вместо жесткой привязки к конкретным технологиям, вы работаете с абстрактными "строительными блоками" через единообразный API. При этом базовые провайдеры можно заменять без изменения кода — просто обновив конфигурационные файлы компонентов Dapr. Это ключевое преимущество Dapr: он создаёт уровень абстракции, который делает ваш код инфраструктурно-агностичным. Но Dapr — это нечто большее, чем просто набор абстракций. Это полноценная среда выполнения, которая решает множество сложных проблем в распределенных системах. Например, он добавляет возможность отслеживания запросов между сервисами, автоматическое шифрование трафика с использованием mTLS, обнаружение сервисов и балансировку нагрузки. Все эти функции вам бы пришлось реализовывать самостоятельно либо использовать отдельные решения для каждой проблемы. При этом Dapr не навязывает вам какой-то конкретный язык программирования или фреймворк. Будь то .NET, Java, Python или Go — Dapr работает с любым стеком технологий. Хотя эта статья ориентирована на разработчиков .NET, концепции Dapr универсальны. Интересная деталь: Dapr первоначально разрабатывался в Microsoft, но сейчас является выпускным проектом (graduated project) в рамках Cloud Native Computing Foundation (CNCF), где находится в хорошей компании с такими проектами, как Kubernetes, Prometheus и Envoy.

Какие проблемы помогает решить Dapr в мире микросервисов? По мере роста распределенной системы, разработчики неизбежно сталкиваются с набором стандартных челленджей. И вот тут Dapr выступает как швейцарский нож, предоставляя набор готовых строительных блоков для их решения. Одна из наиболее частых проблем — обмен сообщениями. Традиционно каждый брокер сообщений требует специального кода для интеграции: RabbitMQ, Kafka, Azure Service Bus — все они имеют свои SDK и особенности. Что происходит, когда нужно сменить одного провайдера на другого? Перепись кода, риски возникновения багов, дополнительное время на тестирование. Dapr выравнивает игровое поле, предлагая единый API для работы с различными системами обмена сообщениями.

Второй болезненный вопрос — управление состоянием. В микросервисных архитектурах часто возникает необходимость сохранения данных между запросами. Redis, MongoDB, Azure Cosmos DB, SQL Server — список потенциальных хранилищ огромен, и у каждого свой API. С Dapr этот вопрос решается через унифицированный интерфейс сохранения и извлечения состояния.

Вы когда-нибудь сталкивались с проблемами обнаружения сервисов? Где именно находится сервис обработки платежей? Как обработать ситуацию, когда этот сервис временно недоступен? Самописным решением или через интеграцию с Consul, Kubernetes API или другой системой? Dapr нивелирует и эту проблему, предоставляя стандартный способ обращения к другим сервисам, автоматически добавляя отказоустойчивость, трассировку запросов и шифрование. Вопрос секретов тоже традиционно причиняет головную боль. API-ключи, пароли, сертификаты — все это нужно хранить безопасно и иметь к ним доступ из кода. Azure Key Vault, HashiCorp Vault, AWS Secrets Manager — у каждого облачного провайдера есть своё решение. А Dapr? Он предоставляет единый интерфейс для получения секретов, независимо от того, где они хранятся.

Возможно, вы заметили паттерн? Dapr систематически устраняет жесткие зависимости между вашим кодом и инфраструктурными сервисами. Вместо этого он предлагает промежуточный слой абстракции, который дает гибкость для замены компонентов без изменения кода.

А теперь представьте, что вы разрабатываете систему с десятками микросервисов, каждому из которых требуется доступ к состоянию, обмену сообщениями и секретам. Без Dapr каждый сервис реализует свою версию интеграции с инфраструктурой — это огромные трудозатраты на разработку и поддержку. С Dapr же код становится проще, понятнее и, что немаловажно, переиспользуемым между сервисами. Это создаёт еще одно существенное преимущество — упрощение перехода между средами разработки, тестирования и промышленной эксплуатации. Тот же код работает везде, просто с разной конфигурацией компонентов Dapr. Локальная разработка с Redis переходит в продакшн с Azure Cosmos DB без изменения строчки кода.

Архитектура и компоненты Dapr



Как же Dapr удается быть столь гибким и универсальным? Секрет кроется в его архитектуре, основанной на паттерне "сайдкар" (sidecar). Вместо того чтобы встраиваться в ваше приложение напрямую, Dapr работает как отдельный процесс рядом с ним. В этой модели каждое приложение получает собственный экземпляр Dapr, который выступает посредником между вашим кодом и внешним миром. Представьте Dapr как помощника, который берёт на себя рутинные операции, пока вы занимаетесь творческой частью — бизнес-логикой.

Ваше приложение взаимодействует с Dapr через HTTP или gRPC, используя простой и понятный API. А дальше Dapr берёт на себя коммуникацию с инфраструктурными сервисами. Такое разделение дает целый ряд преимуществ:
Языковая независимость: Dapr работает с любым языком программирования, включая все варианты .NET.
Кросс-катинг функции: Безопасность, наблюдаемость и отказоустойчивость решаются на уровне Dapr, а не вашего кода.
Абстракция инфраструктуры: Приложение остается независимым от конкретных технологий.
Упрощение разработки: Чистый, поддерживаемый код, сфокусированный на бизнес-логике.
Готовность к продакшену: Встроенные механизмы, улучшающие надежность в рабочем окружении.

Давайте взглянем на архитектуру визуально. В классической модели сайдкара каждый микросервис имеет собственный экземпляр Dapr, который запускается в том же пространстве (на том же хосте, в том же pod'е Kubernetes и т.д.):

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
┌────────────────┐  ┌────────────────┐  ┌────────────────┐
│  Микросервис A │  │  Микросервис B │  │  Микросервис C │
└───────┬────────┘  └───────┬────────┘  └───────┬────────┘
        │                   │                   │
        ▼                   ▼                   ▼
┌────────────────┐  ┌────────────────┐  ┌────────────────┐
│  Dapr Сайдкар  │  │  Dapr Сайдкар  │  │  Dapr Сайдкар  │
└───────┬────────┘  └───────┬────────┘  └───────┬────────┘
        │                   │                   │
        ▼                   ▼                   ▼
┌─────────────────────────────────────────────────────────┐
│      Инфраструктурные сервисы (Redis, Kafka, и т.д.)    │
└─────────────────────────────────────────────────────────┘
Но что конкретно умеет делать Dapr? Для этого обратимся к концепции "строительных блоков" (building blocks). Каждый блок решает определённую распространенную проблему в микросервисных архитектурах.

1. Вызов сервисов (Service Invocation): Позволяет надежно взаимодействовать между сервисами с автоматическим обнаружением, балансировкой нагрузки и повторными попытками. Именно здесь Dapr проявляет свою магию, решая сразу несколько сложных задач:
Обнаружение сервисов: Поиск местоположения сервисов.
Отказоустойчивая коммуникация: Корректная обработка временных сбоев.
Балансировка нагрузки: Распределение запросов между несколькими экземплярами.
Наблюдаемость: Отслеживание запросов через границы сервисов.
Безопасность: Шифрование трафика между сервисами.
2. Управление состоянием (State Management): Предоставляет унифицированный способ хранения и извлечения состояния с функциями контроля параллелизма и транзакций. Вместо прямого взаимодействия с Redis или Cosmos DB, вы обращаетесь к абстракции Dapr, которая берет на себя детали реализации.
3. Публикация/подписка (Pub/Sub): Реализует асинхронный обмен сообщениями между сервисами, обеспечивая слабую связность и событийно-ориентированную архитектуру. Это один из самых мощных блоков Dapr, который существенно упрощает коммуникацию в распределенной системе.
4. Рабочие процессы (Workflows): Позволяет определять долгоживущие, постоянные процессы, охватывающие несколько микросервисов. Это особенно полезно для сложных бизнес-процессов, которые должны сохранять своё состояние даже при перезапуске сервисов.
5. Привязки (Bindings): Соединяет приложения с внешними системами, либо для запуска приложения от внешних событий, либо для вызова внешних сервисов. Эта абстракция позволяет взаимодействовать с различными внешними системами через единообразный интерфейс.
6. Акторы (Actors): Реализует паттерн виртуального актора, облегчая создание сервисов с сохранением состояния и инкапсулированным поведением. Модель акторов особенно эффективна для сценариев с большим количеством независимых объектов, имеющих собственное состояние.
7. Секреты (Secrets): Предлагает безопасный доступ к конфиденциальной конфигурации, такой как строки подключения и API-ключи, из различных хранилищ секретов.
8. Конфигурация (Configuration): Централизует настройки приложений с поддержкой динамических обновлений для нескольких сервисов.
9. Распределенные блокировки (Distributed Lock): Обеспечивает взаимоисключающий доступ к общим ресурсам в распределенной среде.
10. Криптография (Cryptography): Предоставляет операции шифрования и дешифрования, управляя ключами.
11. Задачи (Jobs): Позволяет планировать и оркестрировать задачи (например, пакетную обработку).
12. Беседа (Conversation): Дает возможность отправлять запросы в различные модели крупных языковых систем (LLM), с кешированием запросов и обфускацией персональных данных.

Вот краткий обзор строительных блоков Dapr и наиболее популярных сервисов, с которыми они взаимодействуют:
Управление состоянием: Redis, MongoDB, PostgreSQL, Azure Cosmos DB.
Обмен сообщениями: Kafka, RabbitMQ, Azure Service Bus, Redis Streams.
Секреты: Azure Key Vault, HashiCorp Vault, AWS Secrets Manager.
Привязки: Azure Blob Storage, AWS S3, MQTT, Cron, SendGrid.

Каждый из этих блоков реализован через компоненты, которые настраиваются через YAML-файлы. Эти файлы определяют, какие конкретно технологии и как именно Dapr будет использовать при работе. Рассмотрим детальнее наиболее используемые строительные блоки Dapr, которые решают повседневные задачи микросервисной архитектуры.

От дизайнера интерфейсов: какие проблемы могут возникнуть у разработчиков при переходе с Net 2.5 на Net 3.5?
Приветствую почтенную публику! Коротко о себе: опыт в разработке интерфейсов с 1994 года. Работал со многими командами разработчиков. С 1986 по...

Введение в ASP.NET MVC 5. 2 глава
Здравствуйте! Делаю по этой книге на 2 главе, при запуске выводит ошибку: Делал все как в книге, в visual studio ошибки не высвечиваются. Что не...

Опрос-исследование .NET разработчиков
Здравствуйте. Провожу исследование для выявления причин успеха .NET-разработчиков, хочу систематизировать это в некую методологию. Если вы...

Сообщество программистов и разработчиков ПО под .NET в Киеве
Назрел вот такой вопрос: А не создать ли нам мааааленькое такое сообщество программистов и разработчиков ПО под .NET? А то как-то прямо-таки...


Вызов сервисов



Блок вызова сервисов — это чрезвычайно мощная абстракция, которая существенно упрощает коммуникацию между микросервисами. Представьте стандартный микросервисный ландшафт, где один сервис должен вызвать другой. Без Dapr процесс выглядит примерно так:
1. Определить местоположение целевого сервиса (с помощью Consul, Kubernetes DNS и т.д.).
2. Создать HTTP-клиент с настройками повторных попыток, таймаутов.
3. Сформировать запрос и обработать различные сценарии отказов.
4. Обеспечить трассировку между сервисами.
5. Конфигурировать TLS и авторизацию.

С Dapr этот процесс сводится к вызову API через сайдкар:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Клиентское приложение делает запрос
using Dapr.Client;
 
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
 
var app = builder.Build();
 
app.MapGet("/checkout/{itemId}", async (int itemId, DaprClient daprClient) =>
{
    // Создание данных заказа
    var orderData = new OrderData(itemId, DateTime.UtcNow);
 
    // Вызов сервиса обработки заказов
    var result = await daprClient.InvokeMethodAsync<OrderData, string>(
        "order-processor",
        "process-order",
        orderData);
 
    return Results.Ok(new { Message = $"Заказ {itemId} обработан: {result}" });
});
 
await app.RunAsync();
 
public record OrderData(int ItemId, DateTime OrderedAt);
А вот как выглядит соответствующий сервис, обрабатывающий запрос:

C#
1
2
3
4
5
6
7
8
9
10
11
12
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
 
app.MapPost("/process-order", (OrderData order) =>
{
    Console.WriteLine($"Обработка заказа {order.ItemId} от {order.OrderedAt}");
    return $"Подтверждение заказа {order.ItemId}: #{Guid.NewGuid().ToString()[..8]}";
});
 
await app.RunAsync();
 
public record OrderData(int ItemId, DateTime OrderedAt);
Что происходит при таком взаимодействии:
1. Сервис checkout вызывает InvokeMethodAsync через DaprClient.
2. Сайдкар Dapr для сервиса checkout получает этот запрос.
3. Сайдкар ищет местоположение сервиса order-processor.
4. Запрос пересылается сайдкару Dapr сервиса order-processor.
5. Сайдкар order-processor пересылает запрос самому сервису.
6. Ответ следует по обратному пути.

На каждом этапе Dapr автоматически обеспечивает обнаружение сервисов, шифрование данных, повторные попытки и распределенную трассировку без дополнительного кода.

Обмен сообщениями



Блок публикации и подписки (pub/sub) обеспечивает асинхронный обмен сообщениями между сервисами с гарантией доставки сообщений (at-least-once). Этот паттерн необходим для создания устойчивых, слабосвязанных микросервисов, способных:
  • Обрабатывать операции асинхронно, не блокируя пользователя.
  • Продолжать работу, когда downstream-сервисы недоступны.
  • Масштабироваться независимо в зависимости от нагрузки.

Dapr использует файлы компонентной конфигурации для определения брокера сообщений. Вот пример компонента Redis pub/sub:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: order-events
spec:
  type: pubsub.redis
  version: v1
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    value: ''
Ключевые элементы конфигурации:
metadata.name: Имя компонента (order-events), на которое ссылается приложение.
spec.type: Тип компонента (pubsub.redis).
spec.metadata: Настройки, специфичные для типа компонента.

Файл размещается в директории components, где Dapr может его обнаружить. Локально это обычно ./components/ относительно приложения. А вот как выглядит публикация событий в Dapr:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Сервис-издатель
using Dapr.Client;
 
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
 
var app = builder.Build();
 
app.MapPost("/create-order", async (OrderRequest request, DaprClient daprClient) =>
{
    var orderEvent = new OrderCreatedEvent(
        request.OrderId,
        request.CustomerId,
        request.Items,
        DateTime.UtcNow
    );
 
    // Публикация события в топик "orders"
    await daprClient.PublishEventAsync("order-events", "orders", orderEvent);
 
    return Results.Accepted();
});
 
await app.RunAsync();
 
public record OrderRequest(Guid OrderId, string CustomerId, List<string> Items);
public record OrderCreatedEvent(Guid OrderId, string CustomerId, List<string> Items, DateTime CreatedAt);
И подписка на эти события:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Сервис-подписчик
using Dapr;
 
var builder = WebApplication.CreateBuilder(args);
 
// Добавление обработки событий Dapr
builder.Services.AddDapr();
builder.Services.AddControllers();
 
var app = builder.Build();
 
// Обязательно для Dapr pub/sub
app.UseCloudEvents();
app.MapSubscribeHandler();
 
// Подписка на топик "orders"
app.MapPost("/events/orders", [Topic("order-events", "orders")] async (OrderCreatedEvent orderEvent) =>
{
    Console.WriteLine($"Обработка заказа {orderEvent.OrderId} для клиента {orderEvent.CustomerId}");
    await ProcessOrderAsync(orderEvent);
    return Results.Ok();
});
 
await app.RunAsync();
 
async Task ProcessOrderAsync(OrderCreatedEvent orderEvent)
{
    // Обработка заказа
    await Task.Delay(100); // Имитация работы
}
 
public record OrderCreatedEvent(Guid OrderId, string CustomerId, List<string> Items, DateTime CreatedAt);
При такой реализации:
  • Издатель использует Dapr для отправки событий в топик.
  • Dapr обрабатывает взаимодействие с брокером сообщений (Kafka, Redis, и др.).
  • Подписчик декорирует конечные точки атрибутами [Topic].
  • Dapr доставляет сообщения соответствующим подписчикам.

Важно отметить, что name, определённое в файле компонента (order-events), должно точно соответствовать первому параметру в PublishEventAsync("order-events", ...) и [Topic("order-events", ...)]. Если эти имена не совпадают, сообщения не будут корректно передаваться между сервисами.

Одно из наиболее привлекательных свойств Dapr — это модульная структура компонентов. Хотите перейти с Redis на RabbitMQ? Просто замените spec.type на pubsub.rabbitmq и обновите раздел metadata. Код приложения останется неизменным, демонстрируя впечатляющую гибкость Dapr.

Интеграция с .NET-приложениями



Одно из ключевых преимуществ Dapr — это простота его интеграции с экосистемой .NET. Microsoft активно участвовала в разработке Dapr, что отразилось на качестве инструментария для .NET разработчиков. Давайте рассмотрим, как начать работу с Dapr в .NET-проектах. Для начала необходимо установить инструментарий Dapr. Если вы работаете на Windows, проще всего воспользоваться PowerShell:

PowerShell
1
powershell -Command "iwr -useb https://raw.githubusercontent.com/dapr/cli/master/install/install.ps1 | iex"
Для Linux и macOS подойдет следующий скрипт:

Bash
1
wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash
После установки CLI вы можете инициализировать Dapr в локальной среде разработки:

Bash
1
dapr init
Эта команда развернет необходимые компоненты Dapr, включая Redis для хранения состояния и обмена сообщениями по умолчанию. Процесс занимает всего пару минут, после чего вы будете готовы к работе с Dapr.
Теперь добавим необходимые пакеты NuGet в ваш проект. Для большинства случаев достаточно установить базовый SDK:

Bash
1
dotnet add package Dapr.Client
Если вы работаете с ASP.NET Core и хотите использовать атрибуты для подписки на события или другие возможности интеграции, добавьте также:

Bash
1
dotnet add package Dapr.AspNetCore
Что происходит, когда вы добавляете эти пакеты? Dapr.Client предоставляет класс DaprClient, который является основным инструментом для взаимодействия с Dapr из .NET-кода. Через него можно обращаться ко всем строительным блокам Dapr. Dapr.AspNetCore добавляет интеграцию с конвейером ASP.NET Core, включая атрибуты для обработки событий, маршрутизацию подписок и другие удобства. Простейшая интеграция Dapr с ASP.NET Core приложением выглядит следующим образом:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var builder = WebApplication.CreateBuilder(args);
 
// Регистрация DaprClient в DI-контейнере
builder.Services.AddDaprClient();
 
// Для приложений, которые будут получать события Pub/Sub
builder.Services.AddControllers().AddDapr();
 
var app = builder.Build();
 
// Настройка middleware для Pub/Sub
app.UseCloudEvents();
app.MapSubscribeHandler();
 
// Остальная конфигурация приложения
app.MapControllers();
// или
app.MapGet("/...", ...);
 
app.Run();
После такой настройки вы можете использовать DaprClient через внедрение зависимостей (dependency injection) в контроллерах, сервисах или минимальных API-эндпоинтах:

C#
1
2
3
4
5
6
7
8
9
app.MapGet("/customers/{id}", async (string id, DaprClient daprClient) =>
{
    // Получение состояния из хранилища Dapr
    var customer = await daprClient.GetStateAsync<Customer>("statestore", id);
    if (customer == null)
        return Results.NotFound();
        
    return Results.Ok(customer);
});
Чтобы запустить приложение с Dapr, используйте команду:

Bash
1
dapr run --app-id myapp --app-port 5000 --dapr-http-port 3500 -- dotnet run
Где:
--app-id — уникальный идентификатор вашего приложения,
--app-port — порт, на котором работает ваше приложение,
--dapr-http-port — порт для HTTP API Dapr,
-- — разделитель между параметрами Dapr и командой запуска приложения.

Можно также упростить запуск, создав профиль в Properties/launchSettings.json:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
  "profiles": {
    "MyApp.WithDapr": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "dapr": {
        "enabled": true,
        "appId": "myapp",
        "appPort": 5000,
        "httpPort": 3500
      }
    }
  }
}
При такой конфигурации Visual Studio предложит вам запускать приложение с Dapr или без него.

С .NET 8 и Visual Studio 2022, отладка Dapr-приложений стала еще проще. Visual Studio автоматически подхватывает настройки Dapr и запускает сайдкар вместе с основным приложением, когда вы нажимаете F5. Это работает не только для одного приложения, но и для нескольких проектов в решении, что позволяет отлаживать взаимодействие между микросервисами. Что касается работы в контейнерной среде, Dapr отлично интегрируется с Docker и Kubernetes. Для Docker Compose вы можете добавить сайдкар Dapr к вашему сервису:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
version: '3.4'
services:
  myservice:
    image: myservice
    build:
      context: .
      dockerfile: MyService/Dockerfile
    ports:
      - "5000:80"
 
  myservice-dapr:
    image: "daprio/daprd:latest"
    command: ["./daprd", "-app-id", "myservice", "-app-port", "80"]
    network_mode: "service:myservice"
    volumes:
      - "./components:/components"
    depends_on:
      - myservice
Теперь о нюансах интеграции с .NET Aspire, новой облачной платформой Microsoft для создания распределенных приложений. Dapr и Aspire отлично дополняют друг друга. Aspire фокусируется на оркестрации .NET-специфичных приложений, в то время как Dapr предоставляет языково-агностичные строительные блоки. Вот пример интеграции Dapr с .NET Aspire:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using CommunityToolkit.Aspire.Hosting.Dapr;
 
// Program.cs в проекте Aspire AppHost
var builder = DistributedApplication.CreateBuilder(args);
 
// Добавление сервиса Aspire и настройка Dapr
var orderService = builder.AddProject<Projects.OrderService>("orderservice")
.WithDaprSidecar(new DaprSidecarOptions
{
    AppId = "order-api",
    Config = "./dapr/config.yaml",
    ResourcesPaths = ["./dapr/components"]
});
 
// Добавление еще одного сервиса, который будет взаимодействовать с order-service через Dapr
var checkoutService = builder.AddProject<Projects.CheckoutService>("checkoutservice")
.WithDaprSidecar(new DaprSidecarOptions
{
    AppId = "checkout-api",
    Config = "./dapr/config.yaml",
    ResourcesPaths = ["./dapr/components"]
})
// Ссылка на order-service по его Dapr app ID
.WithReference(orderService);
 
builder.Build().Run();
Обратите внимание, что здесь используется пакет CommunityToolkit.Aspire.Hosting.Dapr, который является официальной интеграцией Dapr для .NET Aspire. Ранее использовавшаяся библиотека Aspire.Hosting.Dapr сейчас считается устаревшей. Одно из преимуществ такой интеграции — возможность наблюдать поток сообщений между сервисами в панели мониторинга Aspire. Вы можете видеть в реальном времени, как сервисы обмениваются данными через Dapr, что существенно упрощает отладку распределенных приложений.

В целом, интеграция Dapr с .NET-приложениями практически бесшовна. Вы можете начать с минимальных изменений в коде, постепенно добавляя возможности Dapr по мере необходимости. API остается консистентным и интуитивно понятным, что делает кривую обучения достаточно пологой даже для разработчиков, только начинающих работать с микросервисами. Особенно ценно то, что Dapr не заставляет вас полностью переписывать существующие приложения. Вы можете постепенно внедрять отдельные строительные блоки, например, начать с хранения состояния, затем добавить публикацию/подписку и т.д. Это делает Dapr идеальным выбором для эволюционного перехода от монолита к микросервисам или для улучшения существующей микросервисной инфраструктуры.

Пример: приложение заказа товаров на микросервисах



Давайте создадим простое, но функциональное микросервисное приложение с использованием Dapr, которое продемонстрирует ключевые возможности этой технологии.
Представим, что мы разрабатываем систему электронной коммерции с двумя микросервисами:
Catalog — управление товарами и их наличием.
Order — обработка заказов и оплаты.

Начнем с создания базовой структуры проекта:

Bash
1
2
3
4
5
mkdir dapr-shop-demo
cd dapr-shop-demo
mkdir components
mkdir catalog-service
mkdir order-service
Теперь определим компоненты Dapr, которые будем использовать. Создадим файл components/statestore.yaml для хранения состояния:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
spec:
  type: state.redis
  version: v1
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    value: ""
  - name: actorStateStore
    value: "true"
И файл components/pubsub.yaml для обмена сообщениями:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: shopevents
spec:
  type: pubsub.redis
  version: v1
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    value: ""
Теперь создадим сервис каталога. В папке catalog-service инициализируем новый проект ASP.NET Core:

Bash
1
2
3
4
5
cd catalog-service
dotnet new webapi -n Catalog.API
cd Catalog.API
dotnet add package Dapr.AspNetCore
dotnet add package Dapr.Extensions.Configuration
Создадим базовую модель продукта:

C#
1
2
3
4
5
6
7
8
9
public class Product
{
    public string Id { get; set; } = Guid.NewGuid().ToString();
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
    public int StockLevel { get; set; }
    public DateTime LastUpdated { get; set; } = DateTime.UtcNow;
}
Теперь определим контроллер для каталога товаров:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
using Dapr.Client;
using Microsoft.AspNetCore.Mvc;
 
[ApiController]
[Route("[controller]")]
public class ProductsController : ControllerBase
{
    private readonly DaprClient _daprClient;
    private readonly ILogger<ProductsController> _logger;
    private const string StateStoreName = "statestore";
    private const string PubSubName = "shopevents";
 
    public ProductsController(DaprClient daprClient, ILogger<ProductsController> logger)
    {
        _daprClient = daprClient;
        _logger = logger;
    }
 
    [HttpGet]
    public async Task<IActionResult> GetAll()
    {
        try
        {
            var products = new List<Product>();
            // В реальной системе здесь был бы запрос к базе данных
            // Для демо добавим пару товаров
            products.Add(new Product 
            { 
                Id = "p1", 
                Name = "Смартфон XYZ", 
                Description = "Флагманская модель 2025 года",
                Price = 79999,
                StockLevel = 15
            });
            
            products.Add(new Product 
            { 
                Id = "p2", 
                Name = "Ноутбук ABC", 
                Description = "Мощный ноутбук для работы и игр",
                Price = 129999,
                StockLevel = 8
            });
            
            return Ok(products);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Ошибка при получении списка товаров");
            return StatusCode(500, "Внутренняя ошибка сервера");
        }
    }
 
    [HttpGet("{id}")]
    public async Task<IActionResult> GetById(string id)
    {
        try
        {
            // В реальном приложении получаем из базы данных
            // Для демо используем Dapr state store
            var product = await _daprClient.GetStateAsync<Product>(StateStoreName, $"product-{id}");
            
            if (product == null)
                return NotFound();
                
            return Ok(product);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Ошибка при получении товара {ProductId}", id);
            return StatusCode(500, "Внутренняя ошибка сервера");
        }
    }
 
    [HttpPost]
    public async Task<IActionResult> Create(Product product)
    {
        try
        {
            if (string.IsNullOrEmpty(product.Id))
                product.Id = Guid.NewGuid().ToString();
                
            product.LastUpdated = DateTime.UtcNow;
            
            // Сохраняем товар в хранилище состояния Dapr
            await _daprClient.SaveStateAsync(StateStoreName, $"product-{product.Id}", product);
            
            // Публикуем событие о создании нового товара
            await _daprClient.PublishEventAsync(PubSubName, "product-created", product);
            
            return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Ошибка при создании товара");
            return StatusCode(500, "Внутренняя ошибка сервера");
        }
    }
 
    [HttpPut("{id}")]
    public async Task<IActionResult> Update(string id, Product product)
    {
        try
        {
            var existingProduct = await _daprClient.GetStateAsync<Product>(StateStoreName, $"product-{id}");
            
            if (existingProduct == null)
                return NotFound();
                
            product.Id = id;
            product.LastUpdated = DateTime.UtcNow;
            
            await _daprClient.SaveStateAsync(StateStoreName, $"product-{id}", product);
            
            // Публикуем событие об обновлении товара
            await _daprClient.PublishEventAsync(PubSubName, "product-updated", product);
            
            return Ok(product);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Ошибка при обновлении товара {ProductId}", id);
            return StatusCode(500, "Внутренняя ошибка сервера");
        }
    }
}
Обратите внимание, что мы используем хранилище состояний Dapr для сохранения информации о товарах и механизм публикации событий для оповещения о создании и обновлении товаров.
Обновим файл Program.cs для настройки Dapr:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var builder = WebApplication.CreateBuilder(args);
 
// Добавляем контроллеры и Dapr
builder.Services.AddControllers().AddDapr();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
 
// Регистрируем DaprClient
builder.Services.AddDaprClient();
 
var app = builder.Build();
 
// Настраиваем промежуточное ПО
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
 
app.UseHttpsRedirection();
app.UseAuthorization();
 
// Добавляем промежуточное ПО Dapr
app.UseCloudEvents();
app.MapControllers();
app.MapSubscribeHandler();
 
app.Run();
Теперь перейдем к созданию сервиса заказов. В папке order-service создадим новый проект:

Bash
1
2
3
4
5
cd ../order-service
dotnet new webapi -n Order.API
cd Order.API
dotnet add package Dapr.AspNetCore
dotnet add package Dapr.Extensions.Configuration
Определим модель заказа:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class OrderItem
{
    public string ProductId { get; set; }
    public string ProductName { get; set; }
    public decimal UnitPrice { get; set; }
    public int Quantity { get; set; }
}
 
public class Order
{
    public string Id { get; set; } = Guid.NewGuid().ToString();
    public string CustomerId { get; set; }
    public List<OrderItem> Items { get; set; } = new List<OrderItem>();
    public decimal TotalAmount => Items.Sum(i => i.UnitPrice * i.Quantity);
    public string Status { get; set; } = "Created";
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    public DateTime? CompletedAt { get; set; }
}
И создадим контроллер с методами для создания и отслеживания заказов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
using Dapr;
using Dapr.Client;
using Microsoft.AspNetCore.Mvc;
 
[ApiController]
[Route("[controller]")]
public class OrdersController : ControllerBase
{
    private readonly DaprClient _daprClient;
    private readonly ILogger<OrdersController> _logger;
    private const string StateStoreName = "statestore";
    private const string PubSubName = "shopevents";
 
    public OrdersController(DaprClient daprClient, ILogger<OrdersController> logger)
    {
        _daprClient = daprClient;
        _logger = logger;
    }
 
    [HttpPost]
    public async Task<IActionResult> CreateOrder(Order order)
    {
        try
        {
            if (string.IsNullOrEmpty(order.CustomerId))
                return BadRequest("CustomerId is required");
 
            // Проверяем наличие товаров через вызов сервиса каталога
            foreach (var item in order.Items)
            {
                try
                {
                    // Используем Dapr для вызова сервиса каталога
                    var product = await _daprClient.InvokeMethodAsync<object>(
                        HttpMethod.Get,
                        "catalog-service",
                        $"products/{item.ProductId}");
                        
                    if (product == null)
                        return BadRequest($"Product {item.ProductId} not found");
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "Не удалось проверить наличие товара {ProductId}", item.ProductId);
                    // В реальном приложении здесь был бы механизм Circuit Breaker
                }
            }
 
            // Сохраняем заказ в хранилище состояния
            await _daprClient.SaveStateAsync(StateStoreName, $"order-{order.Id}", order);
            
            // Публикуем событие о создании заказа
            await _daprClient.PublishEventAsync(PubSubName, "order-created", order);
            
            // В реальном приложении здесь бы запускался процесс обработки заказа
            
            return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Ошибка при создании заказа");
            return StatusCode(500, "Внутренняя ошибка сервера");
        }
    }
 
    [HttpGet("{id}")]
    public async Task<IActionResult> GetOrder(string id)
    {
        try
        {
            var order = await _daprClient.GetStateAsync<Order>(StateStoreName, $"order-{id}");
            
            if (order == null)
                return NotFound();
                
            return Ok(order);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Ошибка при получении заказа {OrderId}", id);
            return StatusCode(500, "Внутренняя ошибка сервера");
        }
    }
    
    [Topic(PubSubName, "product-updated")]
    [HttpPost("product-updated")]
    public async Task<IActionResult> HandleProductUpdate(Product product)
    {
        _logger.LogInformation("Получено уведомление об обновлении товара: {ProductId}", product.Id);
        // Здесь можно обновить информацию о товаре в активных заказах
        return Ok();
    }
}
Добавим класс Product.cs для обработки событий об обновлении товаров:

C#
1
2
3
4
5
6
7
8
9
public class Product
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
    public int StockLevel { get; set; }
    public DateTime LastUpdated { get; set; }
}
Обновим Program.cs для сервиса заказов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var builder = WebApplication.CreateBuilder(args);
 
builder.Services.AddControllers().AddDapr();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
 
// Регистрируем DaprClient
builder.Services.AddDaprClient();
 
var app = builder.Build();
 
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
 
app.UseHttpsRedirection();
app.UseAuthorization();
 
// Настраиваем Dapr
app.UseCloudEvents();
app.MapControllers();
app.MapSubscribeHandler();
 
app.Run();
Теперь, когда наши сервисы готовы, запустим их с поддержкой Dapr. Создадим файлы dapr.json в каждой директории сервиса для упрощения запуска:

В catalog-service/dapr.json:
JSON
1
2
3
4
5
{
  "id": "catalog-service",
  "port": 5001,
  "componentsPath": "../components"
}
В order-service/dapr.json:
JSON
1
2
3
4
5
{
  "id": "order-service",
  "port": 5002,
  "componentsPath": "../components"
}
Теперь мы можем запустить оба сервиса с Dapr в отдельных терминалах:

Bash
1
2
3
4
5
# В директории catalog-service
dapr run --app-id catalog-service --app-port 5001 --dapr-http-port 3501 -- dotnet run
 
# В директории order-service
dapr run --app-id order-service --app-port 5002 --dapr-http-port 3502 -- dotnet run
В этом базовом примере мы создали два микросервиса, которые:
1. Используют хранилище состояний Dapr (state store) для сохранения информации о товарах и заказах.
2. Обмениваются событиями через механизм публикации/подписки.
3. Непосредственно вызывают API друг друга через сервисные вызовы Dapr.

В нашем примере мы использовали Redis в качестве бэкенда для хранения состояния и обмена сообщениями. В реальном проекте эти компоненты можно заменить на более подходящие для конкретного сценария, например, MongoDB для состояния и Kafka для обмена сообщениями, просто изменив конфигурацию в YAML-файлах компонентов.

Реализация паттерна Circuit Breaker в нашей системе — важный шаг для повышения её устойчивости. Вместо немедленного отказа при недоступности сервиса каталога, мы можем реализовать более изящное решение с помощью Polly и Dapr. Добавим в сервис заказов пакет Polly:

Bash
1
dotnet add package Polly
И модифицируем контроллер заказов для использования Circuit Breaker:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class OrdersController : ControllerBase
{
    // Существующие поля...
    
    // Добавим политику Circuit Breaker
    private static readonly AsyncCircuitBreakerPolicy _circuitBreakerPolicy = 
        Policy.Handle<Exception>()
              .CircuitBreakerAsync(
                  exceptionsAllowedBeforeBreaking: 3,
                  durationOfBreak: TimeSpan.FromSeconds(30),
                  onBreak: (ex, timespan) => {
                      // Лог о размыкании цепи
                      Console.WriteLine($"Circuit broken for {timespan.TotalSeconds}s due to: {ex.Message}");
                  },
                  onReset: () => {
                      Console.WriteLine("Circuit reset");
                  });
    
    // Модифицируем метод создания заказа
    [HttpPost]
    public async Task<IActionResult> CreateOrder(Order order)
    {
        try
        {
            if (string.IsNullOrEmpty(order.CustomerId))
                return BadRequest("CustomerId is required");
 
            // Проверяем наличие товаров с использованием Circuit Breaker
            foreach (var item in order.Items)
            {
                try
                {
                    // Оборачиваем вызов в политику Circuit Breaker
                    await _circuitBreakerPolicy.ExecuteAsync(async () => {
                        var product = await _daprClient.InvokeMethodAsync<object>(
                            HttpMethod.Get,
                            "catalog-service",
                            $"products/{item.ProductId}");
                            
                        if (product == null)
                            throw new Exception($"Product {item.ProductId} not found");
                    });
                }
                catch (BrokenCircuitException)
                {
                    _logger.LogWarning("Circuit is open, using fallback for product {ProductId}", item.ProductId);
                    // Используем резервные данные или кешированную информацию
                    // В реальном приложении здесь могла бы быть логика получения данных из кеша
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error checking product {ProductId}", item.ProductId);
                    return BadRequest($"Failed to validate product {item.ProductId}");
                }
            }
 
            // Остальная логика создания заказа...
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error creating order");
            return StatusCode(500, "Internal server error");
        }
    }
    
    // Остальные методы...
}
Для более полной демонстрации возможностей Dapr, добавим обработку долгоживущих рабочих процессов. Представим, что после создания заказа должен запуститься процесс обработки, который включает несколько шагов: проверку оплаты, резервирование товаров на складе и подготовку к отправке. Создадим новый контроллер для управления этим процессом:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
[ApiController]
[Route("[controller]")]
public class OrderProcessingController : ControllerBase
{
    private readonly DaprClient _daprClient;
    private readonly ILogger<OrderProcessingController> _logger;
 
    public OrderProcessingController(DaprClient daprClient, ILogger<OrderProcessingController> logger)
    {
        _daprClient = daprClient;
        _logger = logger;
    }
 
    [Topic("shopevents", "order-created")]
    [HttpPost("handle-new-order")]
    public async Task<IActionResult> HandleNewOrder(Order order)
    {
        _logger.LogInformation("Received new order: {OrderId}", order.Id);
        
        try {
            // Начинаем процесс обработки заказа
            // В реальном приложении здесь мог бы быть запуск Dapr Workflow
            
            // Имитация обработки платежа
            await ProcessPayment(order);
            
            // Резервирование товаров на складе
            await ReserveInventory(order);
            
            // Подготовка к отправке
            await PrepareForShipment(order);
            
            // Обновляем статус заказа
            order.Status = "Processed";
            order.CompletedAt = DateTime.UtcNow;
            
            await _daprClient.SaveStateAsync("statestore", $"order-{order.Id}", order);
            
            // Публикуем событие о завершении обработки
            await _daprClient.PublishEventAsync("shopevents", "order-processed", order);
            
            return Ok();
        }
        catch (Exception ex) {
            _logger.LogError(ex, "Error processing order {OrderId}", order.Id);
            
            // Обновляем статус заказа в случае ошибки
            order.Status = "Processing Failed";
            await _daprClient.SaveStateAsync("statestore", $"order-{order.Id}", order);
            
            return StatusCode(500, "Order processing failed");
        }
    }
    
    private async Task ProcessPayment(Order order)
    {
        _logger.LogInformation("Processing payment for order {OrderId}", order.Id);
        // Имитация задержки обработки платежа
        await Task.Delay(500);
    }
    
    private async Task ReserveInventory(Order order)
    {
        _logger.LogInformation("Reserving inventory for order {OrderId}", order.Id);
        
        // Для каждого товара в заказе обновляем его запас в каталоге
        foreach (var item in order.Items)
        {
            // Используем Dapr резильентное API для вызова сервиса каталога
            try
            {
                await _daprClient.InvokeMethodAsync(
                    HttpMethod.Put,
                    "catalog-service",
                    $"products/{item.ProductId}/reserve",
                    new { Quantity = item.Quantity });
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to reserve inventory for product {ProductId}", item.ProductId);
                throw; // Пробрасываем ошибку для отмены всего процесса
            }
        }
    }
    
    private async Task PrepareForShipment(Order order)
    {
        _logger.LogInformation("Preparing order {OrderId} for shipment", order.Id);
        // Имитация подготовки к отправке
        await Task.Delay(700);
    }
}
Для отслеживания и анализа работы наших микросервисов, настроим мониторинг и логирование с использованием Dapr. Dapr включает встроенную поддержку распределенной трассировки через OpenTelemetry, что позволяет отслеживать запросы, проходящие через различные сервисы. Обновим Program.cs в обоих сервисах для включения трассировки:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var builder = WebApplication.CreateBuilder(args);
 
// Существующая настройка...
 
// Настраиваем логирование
builder.Logging.AddConsole();
 
// Включаем трассировку Dapr
builder.Services.AddOpenTelemetryTracing(builder => builder
    .AddAspNetCoreInstrumentation()
    .AddHttpClientInstrumentation()
    .AddZipkinExporter(options =>
    {
        options.Endpoint = new Uri("http://localhost:9411/api/v2/spans");
    }));
 
// Остальная настройка...
Для запуска этой конфигурации нам понадобится Zipkin. Добавим его запуск через Docker:

Bash
1
docker run -d -p 9411:9411 openzipkin/zipkin
Теперь, когда мы настроили мониторинг, давайте улучшим обработку ошибок и добавим распределенные транзакции для операций, затрагивающих несколько сервисов. Для демонстрации распределенных транзакций, модифицируем процесс создания заказа:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
[HttpPost]
public async Task<IActionResult> CreateOrder(Order order)
{
    try
    {
        if (string.IsNullOrEmpty(order.CustomerId))
            return BadRequest("CustomerId is required");
 
        // Начинаем распределенную транзакцию
        var transactionId = Guid.NewGuid().ToString();
        
        _logger.LogInformation("Starting transaction {TransactionId} for order creation", transactionId);
        
        // Проверяем и резервируем товары в рамках транзакции
        foreach (var item in order.Items)
        {
            try
            {
                // Добавляем транзакционный контекст к вызову
                var metadata = new Dictionary<string, string>
                {
                    ["Transaction-Id"] = transactionId
                };
                
                var product = await _daprClient.InvokeMethodAsync<Product>(
                    HttpMethod.Get,
                    "catalog-service",
                    $"products/{item.ProductId}",
                    null,
                    metadata);
                    
                if (product == null)
                    return BadRequest($"Product {item.ProductId} not found");
                    
                // Обновляем информацию о товаре в заказе
                item.ProductName = product.Name;
                item.UnitPrice = product.Price;
                
                // Резервируем товар (уменьшаем доступное количество)
                await _daprClient.InvokeMethodAsync(
                    HttpMethod.Put,
                    "catalog-service",
                    $"products/{item.ProductId}/reserve",
                    new { Quantity = item.Quantity },
                    metadata);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in transaction {TransactionId} for product {ProductId}", 
                                transactionId, item.ProductId);
                                
                // В случае ошибки, отменяем транзакцию
                await AbortTransaction(transactionId, order);
                return BadRequest($"Failed to process product {item.ProductId}: {ex.Message}");
            }
        }
        
        // Если все прошло успешно, создаем заказ
        await _daprClient.SaveStateAsync("statestore", $"order-{order.Id}", order);
        
        // Публикуем событие с указанием транзакции
        var metadata = new Dictionary<string, string>
        {
            ["Transaction-Id"] = transactionId
        };
        
        await _daprClient.PublishEventAsync("shopevents", "order-created", order, metadata);
        
        _logger.LogInformation("Completed transaction {TransactionId} for order {OrderId}", 
                              transactionId, order.Id);
        
        return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Unexpected error creating order");
        return StatusCode(500, "Internal server error");
    }
}
 
private async Task AbortTransaction(string transactionId, Order order)
{
    _logger.LogWarning("Aborting transaction {TransactionId} for order {OrderId}", 
                      transactionId, order.Id);
    
    // Публикуем событие отмены транзакции
    var metadata = new Dictionary<string, string>
    {
        ["Transaction-Id"] = transactionId
    };
    
    await _daprClient.PublishEventAsync("shopevents", "transaction-aborted", 
                                      new { TransactionId = transactionId, Order = order }, 
                                      metadata);
}
В сервисе каталога нам потребуется обработчик для отмены транзакции:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Topic("shopevents", "transaction-aborted")]
[HttpPost("handle-transaction-abort")]
public async Task<IActionResult> HandleTransactionAbort([FromBody] TransactionAbortEvent abortEvent)
{
    _logger.LogWarning("Processing transaction abort: {TransactionId}", abortEvent.TransactionId);
    
    // В реальной системе здесь был бы код для освобождения зарезервированных ресурсов
    
    return Ok();
}
 
public class TransactionAbortEvent
{
    public string TransactionId { get; set; }
    public object Order { get; set; }
}
Такая реализация демонстрирует ключевое преимущество Dapr: возможность создавать надежные распределенные системы с понятным кодом, сосредоточенным на бизнес-логике. Dapr берет на себя сложности совместимости, устойчивости к сбоям и отслеживания операций между сервисами.

Сравнение с альтернативами



Выбор технологии для построения микросервисной архитектуры — это всегда компромисс между простотой использования, функциональностью, производительностью и сопровождаемостью. Dapr предлагает свой подход к решению распространённых проблем микросервисов, но он не единственный игрок на этом поле. Сравним его с популярными альтернативами, чтобы лучше понять его сильные и слабые стороны.

Netflix OSS, один из первых широко используемых наборов инструментов для микросервисов, включает такие компоненты как Eureka (обнаружение сервисов), Hystrix (отказоустойчивость), Ribbon (балансировка нагрузки) и Zuul (API Gateway). Хотя эти инструменты имеют богатую функциональность, они требуют отдельной настройки и интеграции каждого компонента. Более того, многие из них уже не поддерживаются активно, что создаёт риски для долгосрочного использования.

Spring Cloud, популярный в Java, предлагает схожий с Dapr набор решений для типичных задач микросервисов. Однако, в отличие от Dapr, Spring Cloud привязан к экосистеме Java и Spring Framework. Для .NET-разработчиков использование Spring Cloud через Steeltoe даёт лишь ограниченный доступ к возможностям платформы и требует изучения Spring-специфичных концепций.

Service Mesh технологии, такие как Istio или Linkerd, решают проблемы сетевого взаимодействия, безопасности и наблюдаемости на уровне инфраструктуры. Они мощны, но часто комплексны в настройке и требуют глубоких знаний Kubernetes. В отличие от них, Dapr можно использовать как в Kubernetes, так и вне его, а его абстракции выходят далеко за рамки сетевого взаимодействия.

Serverless платформы, например, Azure Functions или AWS Lambda, упрощают разработку, автоматически масштабируя выполнение кода. Однако они часто привязаны к конкретному облачному провайдеру и имеют ограничения по времени выполнения функций, что делает их менее подходящими для длительных процессов.

В чём же преимущества Dapr перед этими альтернативами?

Во-первых, его языковая агностичность. Хотя наша статья ориентирована на .NET, Dapr одинаково хорошо работает с любым языком программирования. Это делает его идеальным выбором для полиглотных микросервисных архитектур.

Во-вторых, Dapr предлагает стандартизированные API для различных распределённых систем, не привязываясь к конкретным реализациям. Вы можете начать с простых компонентов, таких как Redis, а затем перейти к более мощным решениям, например, Kafka или Azure Cosmos DB, без изменения кода приложения.

В-третьих, Dapr работает там, где работает ваше приложение — будь то локальная машина разработчика, Kubernetes-кластер или любая другая платформа. Это значительно упрощает процесс разработки и переход между средами.

Что касается ограничений, Dapr всё ещё относительно молодой проект. Хотя он имеет статус graduated project в CNCF, некоторые его компоненты могут быть не так зрелы, как аналоги в более устоявшихся системах. Кроме того, добавление сайдкара увеличивает потребление ресурсов и добавляет небольшую латентность к взаимодействию между сервисами.

По производительности Dapr показывает хорошие результаты в большинстве сценариев. Тесты производительности HTTP API через Dapr демонстрируют увеличение задержки примерно на 3-5 мс по сравнению с прямыми вызовами, что незначительно для большинства бизнес-приложений. При этом Dapr поддерживает gRPC, что позволяет минимизировать накладные расходы при необходимости. Примечательно, что некоторые из наиболее критичных к производительности компаний, включая крупных ритейлеров и финансовые организации, успешно используют Dapr в продакшене. Это свидетельствует о его готовности к промышленному применению даже для требовательных сценариев.

Интеграция с .NET-проектами



Теперь поговорим об интеграции Dapr с существующими .NET-проектами. Если у вас уже есть работающее приложение, миграция на Dapr может происходить постепенно. Вы можете начать с добавления сайдкара Dapr к отдельным компонентам системы, не переписывая всё сразу.

Вот практический подход к интеграции Dapr в существующие системы:
1. Идентифицируйте компоненты, которые получат наибольшую выгоду от Dapr. Обычно это сервисы с тяжелой интеграционной логикой или те, которые чаще всего требуют изменений.
2. Добавьте необходимые пакеты NuGet и минимальные изменения кода для работы с DaprClient.
3. Создайте компоненты Dapr, которые будут имитировать текущее поведение системы. Например, если вы используете Azure Service Bus, создайте компонент pub/sub, указывающий на ту же очередь.
4. Постепенно переводите функциональность с прямого использования SDK на работу через Dapr.

Я наблюдал интересный кейс миграции монолита на Dapr в одной финансовой организации. Вместо того чтобы разбивать монолит сразу на микросервисы, команда сначала выделила ключевые модули внутри монолита и обернула их в Dapr-абстракции. Они продолжали работать как один процесс, но уже использовали строительные блоки Dapr. Затем, когда абстракции устоялись, модули постепенно выделялись в отдельные сервисы без необходимости переписывать интеграционный код.

Этот подход "странглер фасада" (strangler pattern) оказался особенно удачным, поскольку позволил размонолитизировать систему с минимальными рисками и сохранением постоянной работоспособности.

Что касается производительности в реальных проектах, результаты весьма обнадеживающие. Измерения показывают, что для типичного .NET-приложения с бизнес-логикой средней сложности добавление Dapr увеличивает задержку обработки запросов всего на 10-15 мс в худшем случае. Для большинства бизнес-приложений это незначительно.

Когда Dapr — не лучший выбор



Существует несколько сценариев, где другие решения могут быть предпочтительнее:

1. Супер-низкая латентность. Если ваше приложение требует минимальной задержки в микросекундах, дополнительные хопы через Dapr могут быть неприемлемы. В таких случаях оптимизированное решение с прямой интеграцией может работать лучше.
2. Очень простые приложения. Если у вас небольшой монолит без планов на расширение, добавление Dapr может быть излишним усложнением.
3. Команды с глубоким опытом в конкретных технологиях. Если ваша команда уже имеет отработанные практики работы с определенной стек технологий и не планирует менять его, преимущества абстракций Dapr могут быть не так заметны.
4. Экстремальные требования к ресурсам. В средах с жесткими ограничениями памяти дополнительные процессы сайдкара могут быть проблематичны.

Важно отметить, что Dapr активно развивается, и новые версии приносят улучшения производительности и безопасности. Например, недавно была добавлена поддержка HTTP/2 и улучшен механизм кеширования мутаций состояния, что значительно повысило производительность в сценариях с интенсивной записью.

Интересны случаи использования Dapr за пределами типичных микросервисных сценариев. Некоторые команды применяют его в IoT-решениях, где Dapr выступает единообразным интерфейсом для различных протоколов IoT. Другие используют Dapr для упрощения гибридных облачных архитектур, где часть сервисов работает на периферии, а часть — в центральном облаке.

В заключение, Dapr предлагает убедительную ценность для .NET-разработчиков, стремящихся к построению современных распределенных систем. Его главные преимущества — простота, переносимость и последовательность API — делают его отличным выбором для многих сценариев. Хотя Dapr не является универсальным решением для всех проблем, он заполняет важный пробел в экосистеме инструментов для построения микросервисных архитектур.

Введение в технологию WPF и введение, XAML
Объясните, пожалуйста, Имеется ввиду 1)Введение в технологию WPF и введение в технологию XAML? 2)Введение в технологию WPF, раздел XAML ...

Удобные решения на C# для разработчиков БД
Эта статья посвящена маленькой, но очень частой проблеме разработчиков приложений БД. Вспомните, как часто у Вас по какой-то неведомой причине...

Собираю материал для статьи о привычках крутых разработчиков
Поделитесь плиз опытом с начинающим IT-редактором. В инетовских статьях пишут в целом об одном и том же: - Я все время учусь новому - Пишу...

.NET Framework для разработчика и .NET Framework для простого пользователя это одно и тоже?
Если я обычный пользователь компьютера и не разрабатываю приложения .NET Framework, но запускаю их и пользуюсь ими на своём ПК и наоборот если я...

Документация для разработчиков под Microsoft Project Web Access
Есть какая-нибудь документация для разработчиков под Microsoft Project Web Access??? На MSDN только общие слова :( Проблема с установкой двух...

Есть ли какие подводные камни для разработчиков ПО при использовании executequeueditems
Есть ли какие подводные камни для разработчиков ПО при использовании executequeueditems на рабочей машине и для всех версий Framework? Или же...

ADO.NET EDM в dll для WinForms и ASP.NET проектво
Здравствуйте. Есть следующая задача: подцепить к проектам WinForms(Или WPF, ещё не определился) и ASP.NET БД на MS SQL Server. Приходилось несколько...

Что написать для практики (ASP.Net, ADO.Net)
Изучаю сейчас ASP.Net. Хотелось узнать, что такого можно написать, чтобы закреплять знания. А то примеры в книгах и статьях очень уж...

Можно ли использовать библиотеки написанные на .net Core для .net FW
Можно ли подключить библиотеку написанную на .net Core к WinForm приложению написанному на .net FW? Почитал описание .net Core 3. Похоже скоро...

Библиотека NETSquirrel для .NET и .NET Core - решение задач
Тема для решения задач с применением NETSquirrel. Просьба вопросы и замечания писать здесь.

Подойдет ли .NET Core 1.0 (RC2) для разработки cоциальной сети на ASP.NET?
Добрый день. У меня есть идея одна по написаю одной социальной сети. Как вы думаете подойдет ли NETCore 1.0 (RC2) для разработки. У...

Что выбрать .NET Core или .NET Framework для desktop-программирования?
Собственно вопрос в названии. Что выбрать .NET CORE .NET FRAMEWORK для desktop- программирования? Добавлено через 46 секунд С технологией WPF ...

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Обнаружение объектов в реальном времени на Python с YOLO и OpenCV
AI_Generated 29.04.2025
Компьютерное зрение — одна из самых динамично развивающихся областей искусственного интеллекта. В нашем мире, где визуальная информация стала доминирующим способом коммуникации, способность машин. . .
Эффективные парсеры и токенизаторы строк на C#
UnmanagedCoder 29.04.2025
Обработка текстовых данных — частая задача в программировании, с которой сталкивается почти каждый разработчик. Парсеры и токенизаторы составляют основу множества современных приложений: от. . .
C++ в XXI веке - Эволюция языка и взгляд Бьярне Страуструпа
bytestream 29.04.2025
C++ существует уже более 45 лет с момента его первоначальной концепции. Как и было задумано, он эволюционировал, отвечая на новые вызовы, но многие разработчики продолжают использовать C++ так, будто. . .
Слабые указатели в Go: управление памятью и предотвращение утечек ресурсов
golander 29.04.2025
Управление памятью — один из краеугольных камней разработки высоконагруженных приложений. Го (Go) занимает уникальную нишу в этом вопросе, предоставляя разработчикам автоматическое управление памятью. . .
Разработка кастомных расширений для компилятора C++
NullReferenced 29.04.2025
Создание кастомных расширений для компиляторов C++ — инструмент оптимизации кода, внедрения новых языковых функций и автоматизации задач. Многие разработчики недооценивают гибкость современных. . .
Гайд по обработке исключений в C#
stackOverflow 29.04.2025
Разработка надёжного программного обеспечения невозможна без грамотной обработки исключительных ситуаций. Любая программа, независимо от её размера и сложности, может столкнуться с непредвиденными. . .
Создаем RESTful API с Laravel
Jason-Webb 28.04.2025
REST (Representational State Transfer) — это архитектурный стиль, который определяет набор принципов для создания веб-сервисов. Этот подход к построению API стал стандартом де-факто в современной. . .
Дженерики в C# - продвинутые техники
stackOverflow 28.04.2025
История дженериков началась с простой идеи — создать механизм для разработки типобезопасного кода без потери производительности. До их появления программисты использовали неуклюжие преобразования. . .
Тестирование в Python: PyTest, Mock и лучшие практики TDD
py-thonny 28.04.2025
Тестирование кода играет весомую роль в жизненном цикле разработки программного обеспечения. Для разработчиков Python существует богатый выбор инструментов, позволяющих создавать надёжные и. . .
Работа с PDF в Java с iText
Javaican 28.04.2025
Среди всех форматов PDF (Portable Document Format) заслуженно занимает особое место. Этот формат, созданный компанией Adobe, превратился в универсальный стандарт для обмена документами, не зависящий. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru