Разработка распределенных систем никогда не была настолько востребованной и одновременно такой сложной. Если вы .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 в локальной среде разработки:
Эта команда развернет необходимые компоненты 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 ...
|