eShopOnDapr
eShopOnDapr
通过集成 Dapr 构建基块来改进早期 eShopOnContainers 应用程序。 图 12-3 显示了新的解决方案体系结构:
图 12-3. eShopOnDapr 参考应用程序体系结构
虽然 eShopOnDapr 侧重于 Dapr,但体系结构也进行了简化。
-
Blazor WebAssembly 上运行的单页应用程序将用户请求发送到 API 网关。
-
API 网关从前端客户端抽象出后端核心微服务。 它是使用 Envoy(一个高性能的开放源代码服务代理)实现的。 Envoy 将传入请求路由到后端微服务。 大多数请求都是简单的 CRUD 操作(例如,从目录中获取品牌列表),通过直接调用后端微服务进行处理。
-
其他请求在逻辑上更加复杂,需要多个微服务调用协同工作。 对于这些情况,eShopOnDapr 实现了聚合器微服务,用于在完成操作所需的那些微服务之间编排工作流。
-
核心后端微服务实现了电子商务商店所需的功能。 每个微服务都是独立存在的。 按照广泛接受的域分解模式,每个微服务都隔离一个特定的业务功能:
- 购物篮服务管理客户的购物篮体验。
- 目录服务管理可供销售的产品项。
- 标识服务管理身份验证和标识。
- 订单处理服务处理下达订单和管理订单的所有方面。
- 付款服务处理客户的付款。
-
每个微服务都遵循最佳做法,维护其自己的持久性存储。 应用程序不共享单个数据存储。
-
最后,事件总线包装 Dapr 发布/订阅组件。 它实现了跨微服务异步发布/订阅消息传送。 开发人员可以插入任何 Dapr 支持的消息代理组件。
Dapr 构建基块的应用
在 eShopOnDapr 中,Dapr 构建基块取代了大量复杂且容易出错的管道代码。
图 12-4 显示了应用程序中的 Dapr 集成。
上图展示了每个 eShopOnDapr 服务使用的 Dapr 构建基块(以绿色数字框表示)。
- API 网关和 Web 购物聚合器服务使用服务调用构建基块来调用后端服务上的方法。
- 后端服务使用发布和订阅构建基块进行异步通信。
- 购物篮服务使用状态管理构建基块来存储客户的购物篮的状态。
- 原始 eShopOnContainers 演示了订单处理服务中的 DDD 概念和模式。 eShopOnDapr 使用执行组件构建基块作为替代实现。 通过执行组件基于轮次的访问模型,可以轻松实现支持取消的有状态订单处理过程。
- 订单处理服务使用绑定构建基块发送订单确认电子邮件。
- 机密管理由机密构建基块完成。
以下各节详细介绍了如何在 eShopOnDapr 中应用 Dapr 构建基块。
状态管理
在 eShopOnDapr 中,购物篮服务使用状态管理构建基块来保存客户购物篮的内容。
在更新的 eShopOnDapr 参考应用程序中,一个新的 DaprBasketRepository
类取代了 RedisBasketRepository
类:
public class DaprBasketRepository : IBasketRepository { private const string StoreName = "eshop-statestore"; private readonly DaprClient _daprClient; public DaprBasketRepository(DaprClient daprClient) { _daprClient = daprClient; } public Task<CustomerBasket> GetBasketAsync(string customerId) => _daprClient.GetStateAsync<CustomerBasket>(StoreName, customerId); // ... }
更新的代码使用 Dapr .NET SDK 通过状态管理构建基块读取和写入数据。 为客户加载购物篮的新步骤大大简化:
更新后的实现仍使用 Redis 作为基础数据存储。 但请注意 Dapr 是如何从应用程序抽象出 StackExchange.Redis
引用和复杂性的。 应用程序不再需要直接依赖于 Redis。 Dapr 配置文件就是所需的全部内容:
apiVersion: dapr.io/v1alpha1 kind: Component metadata: name: eshop-statestore namespace: eshop spec: type: state.redis version: v1 metadata: - name: redisHost value: redis:6379 - name: redisPassword secretKeyRef: name: redisPassword auth: secretStore: eshop-secretstore
服务调用
图 12-6. 使用 Dapr 更新的 eShop 体系结构。
请注意上图中的更新步骤:
-
前端仍使用 HTTP/REST 调用 API 网关。
-
API 网关将 HTTP 请求转发到其 Dapr 挎斗。
-
API 网关挎斗将请求发送到聚合器或后端服务的挎斗。
-
聚合器服务使用 Dapr .NET SDK 通过其挎斗体系结构调用后端服务。
Dapr 使用 gRPC 实现挎斗之间的调用。 因此,即使使用 HTTP/REST 语义调用远程服务,传输的一部分也是使用 gRPC 实现的。
eShopOnDapr 参考应用程序受益于 Dapr 服务调用构建基块。 优点还包括服务发现、自动 mTLS 和内置的可观察性。
使用 Envoy 和 Dapr 转发 HTTP 请求
原始的和更新的 eShop 应用程序都利用 Envoy 代理作为 API 网关。 Envoy 是一种开放源代码代理和通信总线,在新式分布式应用程序中非常受欢迎。
在原始 eShopOnContainers 实现中,Envoy API 网关将传入的 HTTP 请求直接转发到聚合器或后端服务。 在新的 eShopOnDapr 中,Envoy 代理将请求转发到 Dapr 挎斗。
Envoy 是使用 YAML 定义文件进行配置的,可以控制代理的行为。 为了使 Envoy 能够将 HTTP 请求转发到 Dapr 挎斗容器,需要将 dapr
群集添加到配置中。 群集配置包含一个主机,该主机指向 Dapr 挎斗正在侦听的 HTTP 端口:
clusters: - name: dapr connect_timeout: 0.25s type: strict_dns hosts: - socket_address: address: 127.0.0.1 port_value: 3500
使用 .NET SDK 进行聚合服务调用
来自 eShop 前端的大多数调用都是简单的 CRUD 调用。 API 网关将它们转发到单个服务进行处理。 但是,在某些情况下,需要多个后端服务协同工作以完成请求。 对于更复杂的调用,Web 购物聚合器服务会协调跨服务工作流。 图 12-7 显示了将商品添加到购物篮的处理顺序:
图 12-7. 需要多个服务的后端调用。
聚合器服务包含一个 BasketController
,它提供一个用于更新购物篮的终结点:
[Route("api/v1/[controller]")] [Authorize] [ApiController] public class BasketController : ControllerBase { private readonly ICatalogService _catalog; private readonly IBasketService _basket; [HttpPost] [HttpPut] [ProducesResponseType((int)HttpStatusCode.BadRequest)] [ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)] public async Task<ActionResult<BasketData>> UpdateAllBasketAsync( [FromBody] UpdateBasketRequest data, [FromHeader] string authorization) { BasketData basket; if (data.Items is null || !data.Items.Any()) { basket = new(); } else { // Get the item details from the catalog API. var catalogItems = await _catalog.GetCatalogItemsAsync( data.Items.Select(x => x.ProductId)); if (catalogItems == null) { return BadRequest( "Catalog items were not available for the specified items in the basket."); } // Check item availability and prices; store results in basket object. basket = CreateValidatedBasket(data.Items, catalogItems); } // Save the updated shopping basket. await _basket.UpdateAsync(basket, authorization.Substring("Bearer ".Length)); return basket; } // ... }
的实现使用 Dapr 服务调用,并支持 HttpClient:
public class CatalogService : ICatalogService { private readonly HttpClient _httpClient; public CatalogService(HttpClient httpClient) { _httpClient = httpClient; } public Task<IEnumerable<CatalogItem>> GetCatalogItemsAsync(IEnumerable<int> ids) { var requestUri = $"api/v1/catalog/items/by_ids?ids={string.Join(",", ids)}"; return _httpClient.GetFromJsonAsync<IEnumerable<CatalogItem>>(requestUri); } // ... }
请注意,进行服务调用时,不需要任何 Dapr 特定的代码。 所有通信都是使用标准 HttpClient 对象完成的。
Dapr HttpClient 是在程序启动时为 CatalogService
类配置的:
builder.Services.AddSingleton<ICatalogService, CatalogService>( _ => new CatalogService(DaprClient.CreateInvokeHttpClient("catalog-api")));
发布和订阅
eShopOnContainers 和 eShopOnDapr 都使用发布/订阅模式在微服务之间传达集成事件。 集成事件包括:
- 用户为购物篮中的商品结账。
- 订单付款成功。
- 购买宽限期到期。
备注
需要将集成事件视为跨多个服务发生的事件。