Dapr 服务调用构建基块

提示

此内容是适用于 .NET 开发人员的 eBook、Dapr 的摘录,可在 .NET Docs 上使用,也可以作为可脱机阅读的免费可下载 PDF。

Dapr for .NET Developers eBook cover thumbnail.

在分布式系统中,一项服务通常需要与其他服务进行通信才能完成业务运营。 Dapr 服务调用构建基块可帮助简化服务之间的通信。

它可解决的问题

在分布式应用程序中的服务之间进行调用可能看起来很容易,但其中涉及许多挑战。 例如:

  • 其他服务所在的位置。
  • 在给定服务地址的情况下,如何安全地调用服务。
  • 在发生短暂的暂时性错误时,如何处理重试。

最后,由于分布式应用程序包含许多不同的服务,因此捕获跨服务调用关系图的见解对于诊断生产问题至关重要。

服务调用构建基块通过使用 Dapr 挎斗作为服务的反向代理来解决这些难题。

工作原理

我们从一个示例开始。 假设有两个服务:“服务 A”和“服务 B”。 服务 A 需要调用服务 B 上的 catalog/items API。尽管服务 A 可以依赖于服务 B 并直接对其进行调用,但是服务 A 会调用 Dapr 挎斗上的服务调用 API。 图 6-1 显示了该操作。

How the Dapr service invocation works

图 6-1. Dapr 服务调用的工作原理。

请注意上图中的步骤:

  1. 服务 A 通过调用服务 A 挎斗上的服务调用 API 来调用服务 B 中的 catalog/items 终结点。

    注意

    挎斗使用可插入的名称解析组件来解析服务 B 的地址。在自承载模式下,Dapr 使用 mdn 来查找它。 在 Kubernetes 模式下运行时,由 Kubernetes DNS 服务决定地址。

  2. 服务 A 挎斗将请求转发到服务 B 挎斗。

  3. 服务 B 挎斗对服务 B API 发出实际 catalog/items 请求。

  4. 服务 B 执行请求,并将响应返回给其挎斗。

  5. 服务 B 挎斗将响应转发回服务 A 挎斗。

  6. 服务 A 挎斗将响应转发回服务 A。

由于调用通过挎斗,Dapr 可以注入一些有用的横切行为:

  • 失败时自动重试调用。
  • 通过相互 (mTLS) 身份验证(包括自动证书滚动更新),确保服务之间的调用安全。
  • 使用访问控制策略控制客户端可以执行的操作。
  • 捕获服务间所有调用的跟踪和指标,以提供见解和诊断。

任何应用程序都可以通过使用内置在 Dapr 中的本机“调用”API 来调用 Dapr 挎斗。 可使用 HTTP 或 gRPC 调用 API。 使用以下 URL 调用 HTTP API:

http://localhost:<dapr-port>/v1.0/invoke/<application-id>/method/<method-name>
  • <dapr-port> Dapr 正在侦听的 HTTP 端口。
  • <application-id> 要调用的服务的应用程序 ID。
  • <method-name> 要在远程服务上调用的方法的名称。

在以下例子中,对 Service Bcatalog/items GET 终结点发出的 curl 调用:

curl http://localhost:3500/v1.0/invoke/serviceb/method/catalog/items

注意

Dapr API 使任何支持 HTTP 或 gRPC 的应用程序堆栈都能够使用 Dapr 构建基块。 因此,服务调用构建基块可充当协议之间的桥梁。 服务可以使用 HTTP 和/或 gRPC 相互通信。

在下一部分中,你将了解如何使用 .NET SDK 来简化服务调用的调用。

使用 Dapr .NET SDK

Dapr .NET SDK 为 .NET 开发人员提供了直观的、特定于语言的方法来与 Dapr 交互。 SDK 为开发人员提供了三种方法来进行远程服务调用的调用:

  1. 使用 HttpClient 调用 HTTP 服务
  2. 使用 DaprClient 调用 HTTP 服务
  3. 使用 DaprClient 调用 gRPC 服务

使用 HttpClient 调用 HTTP 服务

调用 HTTP 终结点的首选方法是使用 Dapr 与的 HttpClient 丰富集成。 下面的示例通过调用 orderservice 应用程序的 submit 方法来提交订单:

var httpClient = DaprClient.CreateInvokeHttpClient();
await httpClient.PostAsJsonAsync("http://orderservice/submit", order);

在此示例中,DaprClient.CreateInvokeHttpClient 返回用于执行 Dapr 服务调用的 HttpClient 实例。 返回的 HttpClient 使用特殊的 Dapr 消息处理程序,该处理程序会重写传出请求的 URI。 主机名称被解释为要调用的服务的应用程序 ID。 实际调用的重写请求为:

http://127.0.0.1:3500/v1/invoke/orderservice/method/submit

此示例使用 Dapr HTTP 终结点的默认值,即 http://127.0.0.1:<dapr-http-port>/dapr-http-port 的值取自 DAPR_HTTP_PORT 环境变量。 如果未设置,则使用默认端口号 3500

或者,你可以在对 DaprClient.CreateInvokeHttpClient 的调用中配置自定义终结点:

var httpClient = DaprClient.CreateInvokeHttpClient(daprEndpoint: "localhost:4000");

还可以通过指定应用程序 ID 来直接设置基址。 这样做可以在调用时启用相对 URI:

var httpClient = DaprClient.CreateInvokeHttpClient("orderservice");
await httpClient.PostAsJsonAsync("/submit");

HttpClient 对象是长期存在的。 在应用程序的生存期内,可以重用单个 HttpClient 实例。 下一个方案演示了 OrderServiceClient 类如何重用 Dapr HttpClient 实例:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IOrderServiceClient, OrderServiceClient>(
    _ => new OrderServiceClient(DaprClient.CreateInvokeHttpClient("orderservice")));

在上面的代码片段中,OrderServiceClient 使用 ASP.NET Core 依赖关系注入系统注册为单一实例。 实现工厂通过调用 DaprClient.CreateInvokeHttpClient 创建一个新的 HttpClient 实例。 然后,它使用新创建的 HttpClient 来实例化 OrderServiceClient 对象。 通过将 OrderServiceClient 注册为单一实例,它将在应用程序的生存期内重复使用。

OrderServiceClient 本身没有特定于 Dapr 的代码。 即使在后台使用 Dapr 服务调用,你也可以像对待任何其他 HttpClient 一样处理 Dapr HttpClient:

public class OrderServiceClient : IOrderServiceClient
{
    private readonly HttpClient _httpClient;

    public OrderServiceClient(HttpClient httpClient)
    {
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
    }

    public async Task SubmitOrder(Order order)
    {
        var response = await _httpClient.PostAsJsonAsync("submit", order);
        response.EnsureSuccessStatusCode();
    }
}

将 HttpClient 类用于 Dapr 服务调用有很多好处:

  • HttpClient 是一个众所周知的类,许多开发人员已经在其代码中使用了它。 通过将 HttpClient 用于 Dapr 服务调用,开发人员可重复使用其现有技能。
  • HttpClient 支持高级方案,如自定义标头,以及对请求和响应消息的完全控制。
  • 在 .NET 5 中,HttpClient 支持使用 System.Text.Json 的自动序列化和反序列化。
  • HttpClient 集成了许多现有框架和库,如 RefitRestSharpPolly

使用 DaprClient 调用 HTTP 服务

尽管 HttpClient 是使用 HTTP 语义调用服务的首选方法,但也可以使用 DaprClient.InvokeMethodAsync 方法系列。 下面的示例通过调用 orderservice 应用程序的 submit 方法来提交订单:

var daprClient = new DaprClientBuilder().Build();
try
{
    var confirmation =
        await daprClient.InvokeMethodAsync<Order, OrderConfirmation>(
            "orderservice", "submit", order);
}
catch (InvocationException ex)
{
    // Handle error
}

第三个参数是 order 对象,在内部序列化(使用 System.Text.JsonSerializer)并作为请求有效负载发送。 .NET SDK 负责调用挎斗。 它还反序列化对 OrderConfirmation 对象的响应。 由于未指定 HTTP 方法,因此请求作为 HTTP POST 执行。

下一个示例演示如何通过指定 HttpMethod 来发出 HTTP GET 请求:

var catalogItems = await daprClient.InvokeMethodAsync<IEnumerable<CatalogItem>>(HttpMethod.Get, "catalogservice", "items");

在某些情况下,你可能需要对请求消息进行更多的控制。 例如,当你需要指定请求标头,或你想要将自定义序列化程序用于有效负载时。 DaprClient.CreateInvokeMethodRequest 创建 HttpRequestMessage。 下面的示例演示如何将 HTTP 授权标头添加到请求消息:

var request = daprClient.CreateInvokeMethodRequest("orderservice", "submit", order);
request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);

HttpRequestMessage 现在具有以下属性集:

  • Url = http://127.0.0.1:3500/v1.0/invoke/orderservice/method/submit
  • HttpMethod = POST
  • Content = 包含 JSON 序列化 orderJsonContent 对象
  • Headers.Authorization = "bearer <token>"

按所需方式设置请求后,请使用 DaprClient.InvokeMethodAsync 发送它:

var orderConfirmation = await daprClient.InvokeMethodAsync<OrderConfirmation>(request);

如果请求成功,DaprClient.InvokeMethodAsync 反序列化对 OrderConfirmation 对象的响应。 或者,可以使用 DaprClient.InvokeMethodWithResponseAsync 获取对基础 HttpResponseMessage 的完全访问权限:

var response = await daprClient.InvokeMethodWithResponseAsync(request);
response.EnsureSuccessStatusCode();

var orderConfirmation = response.Content.ReadFromJsonAsync<OrderConfirmation>();

注意

要使用 HTTP 进行服务调用的调用,可以考虑使用上一部分介绍的 Dapr HttpClient 集成。 使用 HttpClient 可带来其他好处,例如与现有框架和库集成。

使用 DaprClient 调用 gRPC 服务

DaprClient 提供了一系列用于调用 gRPC 终结点的 InvokeMethodGrpcAsync 方法。 与 HTTP 方法的主要区别是使用了 Protobuf 序列化程序而不是 JSON。 下面的示例通过 gRPC 调用 orderservicesubmitOrder 方法。

var daprClient = new DaprClientBuilder().Build();
try
{
    var confirmation = await daprClient.InvokeMethodGrpcAsync<Order, OrderConfirmation>("orderservice", "submitOrder", order);
}
catch (InvocationException ex)
{
    // Handle error
}

在以上示例中,DaprClient 使用 Protobuf 序列化给定的 order 对象,并使用结果作为 gRPC 请求正文。 同样,响应正文被 Protobuf 反序列化并返回给调用方。 Protobuf 通常可提供比 HTTP 服务调用中使用的 JSON 有效负载更好的性能。

名称解析组件

在撰写本文时,Dapr 提供了对以下名称解析组件的支持:

  • mDNS(运行自承载时的默认值)
  • Kubernetes 名称解析(在 Kubernetes 中运行时的默认值)
  • HashiCorp Consul

配置

若要使用非默认的名称解析组件,请向应用程序的 Dapr 配置文件添加 nameResolution 规范。 下面是启用 HashiCorp Consul 名称解析的 Dapr 配置文件示例:

apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: dapr-config
spec:
  nameResolution:
    component: "consul"
    configuration:
      selfRegister: true

示例应用程序:Dapr 流量控制

在 Dapr 流量控制示例应用中,FineCollection 服务使用 Dapr 服务调用构建基块从 VehicleRegistration 服务检索车辆和所有者信息。 图 6-2 显示了 Dapr 流量控制示例应用程序的概念体系结构。 Dapr 服务调用构建基块在图中标记为数字 1 的流中使用:

Conceptual architecture of the Dapr Traffic Control sample application.

图 6-2. Dapr 流量控制示例应用程序的概念体系结构。

信息由 FineCollection 服务中的 ASP.NET CollectionController 类检索。 CollectFine 方法需要传入 SpeedingViolation 参数。 它调用 Dapr 服务调用构建基块来调用 VehicleRegistration 服务。 代码片段如下所示。

[Topic("pubsub", "speedingviolations")]
[Route("collectfine")]
[HttpPost]
public async Task<ActionResult> CollectFine(SpeedingViolation speedingViolation, [FromServices] DaprClient daprClient)
{
   // ...

   // get owner info (Dapr service invocation)
   var vehicleInfo = _vehicleRegistrationService.GetVehicleInfo(speedingViolation.VehicleId).Result;

   // ...
}

该代码使用 VehicleRegistrationService 类型的代理来调用 VehicleRegistration 服务。 ASP.NET Core 使用构造函数注入来注入一个服务代理的实例:

public CollectionController(
    ILogger<CollectionController> logger,
    IFineCalculator fineCalculator,
    VehicleRegistrationService vehicleRegistrationService,
    DaprClient daprClient)
{
    // ...
}

VehicleRegistrationService 类包含单个方法:GetVehicleInfo。 它使用 ASP.NET Core HttpClient 来调用 VehicleRegistration 服务:

public class VehicleRegistrationService
{
    private HttpClient _httpClient;
    public VehicleRegistrationService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<VehicleInfo> GetVehicleInfo(string licenseNumber)
    {
        return await _httpClient.GetFromJsonAsync<VehicleInfo>(
            $"vehicleinfo/{licenseNumber}");
    }
}

代码不直接依赖于任何 Dapr 类。 而是利用 Dapr ASP.NET Core 集成,如本模块的使用 HttpClient 调用 HTTP 服务部分所述。 下面是 Startup 类的 ConfigureService 方法中注册 VehicleRegistrationService 代理的代码:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<VehicleRegistrationService>(_ =>
    new VehicleRegistrationService(DaprClient.CreateInvokeHttpClient(
        "vehicleregistrationservice", $"http://localhost:{daprHttpPort}"
    )));

DaprClient.CreateInvokeHttpClient 创建了一个 HttpClient 实例,该实例在后台使用服务调用构建基块来调用 VehicleRegistration 服务。 它同时需要目标服务的 Dapr app-id 及其 Dapr 挎斗的 URL。 在开始时,daprHttpPort 参数包含用于与 Dapr 挎斗进行 HTTP 通信的端口号。

在流量控制示例应用程序中使用 Dapr 服务调用具有多个优势:

  1. 分离目标服务的位置。
  2. 通过自动重试功能添加复原能力。
  3. 能够重用现有的基于 HttpClient 的代理(由 ASP.NET Core 集成提供)。

摘要

在本章中,你了解了服务调用构建基块。 了解了如何通过直接对 Dapr 挎斗进行 HTTP 调用和通过使用 Dapr .NET SDK 来调用远程方法。

Dapr .NET SDK 提供了多种调用远程方法的方法。 对于希望重用现有技能的开发人员来说,HttpClient 支持非常有用,并且与许多现有的框架和库兼容。 DaprClient 支持使用 HTTP 或 gRPC 语义直接使用 Dapr 服务调用 API。

参考