1 - 可插拔组件概述

可插拔组件的解剖结构和支持的组件类型概述

可插拔组件是指不包含在运行时中的组件,与 dapr init 附带内置组件不同。您可以配置 Dapr 使用可插拔组件,这些组件利用构建块 API,但注册方式与内置 Dapr 组件不同。

可插拔组件与内置组件

Dapr 提供了两种注册和创建组件的方式:

虽然两种注册选项都利用了 Dapr 的构建块 API,但每种方式的实现过程不同。

组件详情内置组件可插拔组件
语言只能用 Go 编写可以用任何 gRPC 支持的语言编写
运行位置作为 Dapr 运行时可执行文件的一部分作为独立的进程或容器中的 Pod 运行。与 Dapr 本身分开运行。
向 Dapr 注册集成到 Dapr 代码库中通过 Unix 域套接字(使用 gRPC)向 Dapr 注册
分发方式随 Dapr 版本发布。组件新功能的添加与 Dapr 版本保持一致独立于 Dapr 本身分发。可以根据需要随时添加新功能,并遵循自己的发布周期。
组件激活方式Dapr 启动时自动运行组件用户手动启动组件

为什么创建可插拔组件?

可插拔组件在以下场景中非常有用:

  • 您需要一个私有组件。
  • 您希望组件与 Dapr 发布流程分开。
  • 您不太熟悉 Go,或者用 Go 实现组件不是理想选择。

功能特性

实现可插拔组件

要实现可插拔组件,您需要在组件中实现 gRPC 服务。实现 gRPC 服务需要三个步骤:

  1. 找到 proto 定义文件
  2. 创建服务脚手架
  3. 定义服务

了解更多关于如何开发和实现可插拔组件的信息。

利用多个构建块实现组件

除了从同一组件实现多个 gRPC 服务(例如 StateStoreQueriableStateStoreTransactionalStateStore 等)外,可插拔组件还可以暴露其他组件接口的实现。这意味着单个可插拔组件可以同时充当状态存储、发布订阅和输入或输出绑定。换句话说,您可以将多个组件接口实现到一个可插拔组件中,并将其暴露为 gRPC 服务。

虽然在同一可插拔组件上暴露多个组件接口可以降低部署多个组件的运维负担,但它会使组件的实现和调试变得更加困难。如有疑问,请遵循"关注点分离"原则,仅在必要时将多个组件接口合并到同一个可插拔组件中。

将可插拔组件投入运维

内置组件和可插拔组件有一个共同点:两者都需要组件规范 。内置组件无需任何额外步骤即可使用:Dapr 自动准备好使用它们。

相比之下,可插拔组件在与 Dapr 通信之前需要额外步骤。您需要首先运行组件,并促进 Dapr-组件通信以启动注册过程。

后续步骤

2 - 如何操作:实现可插拔组件

了解如何编写和实现可插拔组件

在本指南中,您将了解为何以及如何实现可插拔组件。要了解如何配置和注册可插拔组件,请参阅如何操作:注册可插拔组件

实现可插拔组件

为了实现可插拔组件,您需要在组件中实现一个 gRPC 服务。实现 gRPC 服务需要三个步骤:

查找 proto 定义文件

为每个支持的服务接口(状态存储、发布订阅、绑定、密钥存储)都提供了 Proto 定义。

目前,支持以下组件 API:

  • 状态存储
  • 发布订阅
  • 绑定
  • 密钥存储
组件类型gRPC 定义内置参考实现文档
状态存储statestate.protoRedis概念, 如何操作, api 规范
发布订阅pubsubpubsub.protoRedis概念, 如何操作, api 规范
绑定bindingsbindings.protoKafka概念, 输入如何操作, 输出如何操作, api 规范
密钥存储secretstoressecretstore.protoHashicorp/Vault概念, howto-secrets, api 规范

以下是可插拔组件状态存储([state.proto])的 gRPC 服务定义片段:

// StateStore 服务为状态存储组件提供 gRPC 接口。
service StateStore {
  // 使用给定的元数据初始化状态存储组件。
  rpc Init(InitRequest) returns (InitResponse) {}
  // 返回已实现的状态存储功能列表。
  rpc Features(FeaturesRequest) returns (FeaturesResponse) {}
  // Ping 状态存储。用于存活性检查。
  rpc Ping(PingRequest) returns (PingResponse) {}
  
  // 从状态存储中删除指定的键。
  rpc Delete(DeleteRequest) returns (DeleteResponse) {}
  // 从给定键获取数据。
  rpc Get(GetRequest) returns (GetResponse) {}
  // 设置指定键的值。
  rpc Set(SetRequest) returns (SetResponse) {}


  // 一次删除多个键。
  rpc BulkDelete(BulkDeleteRequest) returns (BulkDeleteResponse) {}
  // 一次检索多个键。
  rpc BulkGet(BulkGetRequest) returns (BulkGetResponse) {}
  // 一次设置多个键的值。
  rpc BulkSet(BulkSetRequest) returns (BulkSetResponse) {}
}

StateStore 服务的接口总共公开了 9 个方法:

  • 2 个方法用于初始化和组件能力声明(Init 和 Features)
  • 1 个方法用于健康或存活性检查(Ping)
  • 3 个方法用于 CRUD 操作(Get、Set、Delete)
  • 3 个方法用于批量 CRUD 操作(BulkGet、BulkSet、BulkDelete)

创建服务脚手架

使用 协议缓冲区和 gRPC 工具 为服务创建必要的脚手架。通过 gRPC 概念文档了解有关这些工具的更多信息。

这些工具生成针对任何支持 gRPC 的语言的代码。此代码作为服务器的基座,它提供:

  • 处理客户端调用的功能
  • 基础设施,用于:
    • 解码传入请求
    • 执行服务方法
    • 编码服务响应

生成的代码是不完整的。它缺少:

  • 目标服务定义的方法的具体实现(可插拔组件的核心)。
  • 如何处理 Unix Socket Domain 集成的代码,这是 Dapr 特有的。
  • 处理与下游服务集成的代码。

在下一步中了解有关填补这些空白的信息。

定义服务

为所需服务提供具体实现。每个组件都有用于其核心功能的 gRPC 服务定义,这与核心组件接口相同。例如:

  • 状态存储

    可插拔状态存储必须提供 StateStore 服务接口的实现。

    除了此核心功能外,某些组件可能还会在其他可选服务下公开功能。例如,您可以通过为 QueriableStateStore 服务和 TransactionalStateStore 服务定义实现来添加额外功能。

  • 发布订阅

    可插拔发布订阅组件只有一个在 pubsub.proto 中定义的核心服务接口。它们没有可选服务接口。

  • 绑定

    可插拔输入和输出绑定在 bindings.proto 上有一个单一的核心服务定义。它们没有可选服务接口。

  • 密钥存储

    可插拔密钥存储在 secretstore.proto 上有一个单一的核心服务定义。它们没有可选服务接口。

使用 gRPC 和协议缓冲区工具生成上述状态存储示例的服务脚手架代码后,您可以为 service StateStore 下定义的 9 个方法定义具体实现,以及用于初始化和与依赖项通信的代码。

此具体实现和辅助代码是可插拔组件的核心。它们定义了组件在处理来自 Dapr 的 gRPC 请求时的行为。

返回语义错误

返回语义错误也是可插拔组件协议的一部分。组件必须返回对用户应用程序具有语义含义的特定 gRPC 代码,这些错误用于从并发要求到仅提供信息的各种情况。

错误gRPC 错误代码源组件描述
ETag 不匹配codes.FailedPrecondition状态存储用于满足并发要求的错误映射
ETag 无效codes.InvalidArgument状态存储
批量删除行不匹配codes.Internal状态存储

状态管理概述中了解有关并发要求的更多信息。

以下示例演示如何在您自己的可插拔组件中返回错误,您可以根据需要更改消息。

重要提示: 为了使用 .NET 进行错误映射,请先安装 Google.Api.CommonProtos NuGet 包

Etag 不匹配

var badRequest = new BadRequest();
var des = "提供的 ETag 字段与存储中的不匹配";
badRequest.FieldViolations.Add(    
   new Google.Rpc.BadRequest.Types.FieldViolation
       {        
         Field = "etag",
         Description = des
       });

var baseStatusCode = Grpc.Core.StatusCode.FailedPrecondition;
var status = new Google.Rpc.Status{    
   Code = (int)baseStatusCode
};

status.Details.Add(Google.Protobuf.WellKnownTypes.Any.Pack(badRequest));

var metadata = new Metadata();
metadata.Add("grpc-status-details-bin", status.ToByteArray());
throw new RpcException(new Grpc.Core.Status(baseStatusCode, "fake-err-msg"), metadata);

Etag 无效

var badRequest = new BadRequest();
var des = "ETag 字段必须仅包含字母数字字符";
badRequest.FieldViolations.Add(
   new Google.Rpc.BadRequest.Types.FieldViolation
   {
      Field = "etag",
      Description = des
   });

var baseStatusCode = Grpc.Core.StatusCode.InvalidArgument;
var status = new Google.Rpc.Status
{
   Code = (int)baseStatusCode
};

status.Details.Add(Google.Protobuf.WellKnownTypes.Any.Pack(badRequest));

var metadata = new Metadata();
metadata.Add("grpc-status-details-bin", status.ToByteArray());
throw new RpcException(new Grpc.Core.Status(baseStatusCode, "fake-err-msg"), metadata);

批量删除行不匹配

var errorInfo = new Google.Rpc.ErrorInfo();

errorInfo.Metadata.Add("expected", "100");
errorInfo.Metadata.Add("affected", "99");

var baseStatusCode = Grpc.Core.StatusCode.Internal;
var status = new Google.Rpc.Status{
    Code = (int)baseStatusCode
};

status.Details.Add(Google.Protobuf.WellKnownTypes.Any.Pack(errorInfo));

var metadata = new Metadata();
metadata.Add("grpc-status-details-bin", status.ToByteArray());
throw new RpcException(new Grpc.Core.Status(baseStatusCode, "fake-err-msg"), metadata);

就像 Dapr Java SDK 一样,Java 可插拔组件 SDK 使用 Project Reactor,它为 Java 提供异步 API。

可以通过以下方式直接返回错误:

  1. 在方法返回的 MonoFlux 中调用 .error() 方法
  2. 提供适当的异常作为参数。

您也可以引发异常,只要它被捕获并反馈到生成的 MonoFlux 中。

ETag 不匹配

final Status status = Status.newBuilder()
    .setCode(io.grpc.Status.Code.FAILED_PRECONDITION.value())
    .setMessage("fake-err-msg-for-etag-mismatch")
    .addDetails(Any.pack(BadRequest.FieldViolation.newBuilder()
        .setField("etag")
        .setDescription("提供的 ETag 字段与存储中的不匹配")
        .build()))
    .build();
return Mono.error(StatusProto.toStatusException(status));

ETag 无效

final Status status = Status.newBuilder()
    .setCode(io.grpc.Status.Code.INVALID_ARGUMENT.value())
    .setMessage("fake-err-msg-for-invalid-etag")
    .addDetails(Any.pack(BadRequest.FieldViolation.newBuilder()
        .setField("etag")
        .setDescription("ETag 字段必须仅包含字母数字字符")
        .build()))
    .build();
return Mono.error(StatusProto.toStatusException(status));

批量删除行不匹配

final Status status = Status.newBuilder()
    .setCode(io.grpc.Status.Code.INTERNAL.value())
    .setMessage("fake-err-msg-for-bulk-delete-row-mismatch")
    .addDetails(Any.pack(ErrorInfo.newBuilder()
        .putAllMetadata(Map.ofEntries(
            Map.entry("affected", "99"),
            Map.entry("expected", "100")
        ))
        .build()))
    .build();
return Mono.error(StatusProto.toStatusException(status));

ETag 不匹配

st := status.New(codes.FailedPrecondition, "fake-err-msg")
desc := "提供的 ETag 字段与存储中的不匹配"
v := &errdetails.BadRequest_FieldViolation{
	Field:       etagField,
	Description: desc,
}
br := &errdetails.BadRequest{}
br.FieldViolations = append(br.FieldViolations, v)
st, err := st.WithDetails(br)

ETag 无效

st := status.New(codes.InvalidArgument, "fake-err-msg")
desc := "ETag 字段必须仅包含字母数字字符"
v := &errdetails.BadRequest_FieldViolation{
	Field:       etagField,
	Description: desc,
}
br := &errdetails.BadRequest{}
br.FieldViolations = append(br.FieldViolations, v)
st, err := st.WithDetails(br)

批量删除行不匹配

st := status.New(codes.Internal, "fake-err-msg")
br := &errdetails.ErrorInfo{}
br.Metadata = map[string]string{
	affected: "99",
	expected: "100",
}
st, err := st.WithDetails(br)

后续步骤

3 - 可插拔组件 SDK

用你喜欢的语言开发可插拔组件

Dapr SDK 是创建可插拔组件的最简单方式。选择你喜欢的语言,在几分钟内开始创建组件。

可插拔组件 SDK

语言状态
Go开发中
.NET开发中

3.1 - Dapr 可插拔组件 .NET SDK 入门

如何开始使用 Dapr 可插拔组件 .NET SDK

Dapr 提供 NuGet 包以帮助开发 .NET 可插拔组件。

前置条件

创建项目

创建可插拔组件始于一个空的 ASP.NET 项目。

dotnet new web --name <project name>

添加 NuGet 包

添加 Dapr .NET 可插拔组件 NuGet 包。

dotnet add package Dapr.PluggableComponents.AspNetCore

创建应用和服务

创建 Dapr 可插拔组件应用类似于创建 ASP.NET 应用。在 Program.cs 中,将 WebApplication 相关代码替换为 Dapr 等效的 DaprPluggableComponentsApplication

using Dapr.PluggableComponents;

var app = DaprPluggableComponentsApplication.Create();

app.RegisterService(
    "<socket name>",
    serviceBuilder =>
    {
        // Register one or more components with this service.
    });

app.Run();

这将创建一个包含单个服务的应用。每个服务:

  • 对应单个 Unix 域套接字
  • 可以承载一种或多种组件类型

实现和注册组件

本地测试组件

可以通过在命令行启动应用并配置 Dapr 边车来使用它,从而测试可插拔组件。

要启动组件,在应用目录中:

dotnet run

要配置 Dapr 使用该组件,在资源路径目录中:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: <component name>
spec:
  type: state.<socket name>
  version: v1
  metadata:
  - name: key1
    value: value1
  - name: key2
    value: value2

当组件实例化时,任何 metadata 属性都将通过其 IPluggableComponent.InitAsync() 方法传递给组件。

要启动 Dapr(以及可选的,使用该服务的服务):

dapr run --app-id <app id> --resources-path <resources path> ...

此时,Dapr 边车将启动并通过 Unix 域套接字连接到组件。然后您可以通过以下任一方式与组件交互:

  • 通过使用该组件的服务(如果已启动),或
  • 直接使用 Dapr HTTP 或 gRPC API

创建容器

有多种方法可以为您的组件创建容器以进行最终部署。

使用 .NET SDK

.NET 7 及更高版本的 SDK 使您能够在使用 Dockerfile 的情况下为应用创建基于 .NET 的容器,即使是针对早期版本的 .NET SDK 的应用。这可能是目前为组件生成容器的最简单方法。

Microsoft.NET.Build.Containers NuGet 包添加到组件项目。

dotnet add package Microsoft.NET.Build.Containers

将应用发布为容器:

dotnet publish --os linux --arch x64 /t:PublishContainer -c Release

有关更多配置选项,例如控制容器名称、标签和基础镜像,请参阅 .NET 发布为容器指南

使用 Dockerfile

虽然有工具可以为 .NET 应用生成 Dockerfile,但 .NET SDK 本身不会。典型的 Dockerfile 可能如下所示:

FROM mcr.microsoft.com/dotnet/aspnet:<runtime> AS base
WORKDIR /app

# Creates a non-root user with an explicit UID and adds permission to access the /app folder
# For more info, please refer to https://aka.ms/vscode-docker-dotnet-configure-containers
RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser

FROM mcr.microsoft.com/dotnet/sdk:<runtime> AS build
WORKDIR /src
COPY ["<application>.csproj", "<application folder>/"]
RUN dotnet restore "<application folder>/<application>.csproj"
COPY . .
WORKDIR "/src/<application folder>"
RUN dotnet build "<application>.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "<application>.csproj" -c Release -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "<application>.dll"]

构建镜像:

docker build -f Dockerfile -t <image name>:<tag> .

演示

观看此视频,了解 使用 .NET 构建可插拔组件的演示

后续步骤

3.1.1 - 实现 .NET 输入/输出绑定组件

如何使用 Dapr 可插拔组件 .NET SDK 创建输入/输出绑定

创建绑定组件只需要几个基本步骤。

添加绑定命名空间

添加绑定相关命名空间的 using 语句。

using Dapr.PluggableComponents.Components;
using Dapr.PluggableComponents.Components.Bindings;

输入绑定:实现 IInputBinding

创建一个实现 IInputBinding 接口的类。

internal sealed class MyBinding : IInputBinding
{
    public Task InitAsync(MetadataRequest request, CancellationToken cancellationToken = default)
    {
        // 使用配置的元数据初始化组件时调用...
    }

    public async Task ReadAsync(MessageDeliveryHandler<InputBindingReadRequest, InputBindingReadResponse> deliveryHandler, CancellationToken cancellationToken = default)
    {
        // 直到被取消之前,检查底层存储中的消息并将其传递给 Dapr 运行时...
    }
}

ReadAsync() 方法的调用是"长期运行"的,因为该方法预期在取消之前不会返回(例如,通过 cancellationToken)。当从组件的底层存储读取消息时,这些消息通过 deliveryHandler 回调传递给 Dapr 运行时。传递操作允许组件在应用程序(由 Dapr 运行时服务)确认处理消息时接收通知。

    public async Task ReadAsync(MessageDeliveryHandler<InputBindingReadRequest, InputBindingReadResponse> deliveryHandler, CancellationToken cancellationToken = default)
    {
        TimeSpan pollInterval = // 轮询间隔(例如,来自初始化元数据)...

        // 轮询底层存储直到被取消...
        while (!cancellationToken.IsCancellationRequested)
        {
            var messages = // 从底层存储轮询消息...

            foreach (var message in messages)
            {
                // 将消息传递给 Dapr 运行时...
                await deliveryHandler(
                    new InputBindingReadResponse
                    {
                        // 设置消息内容...
                    },
                    // 当应用程序确认消息时调用的回调...
                    async request =>
                    {
                        // 处理响应数据或错误消息...
                    })
            }

            // 等待下一次轮询(或取消)...
            await Task.Delay(pollInterval, cancellationToken);
        }
    }

输出绑定:实现 IOutputBinding

创建一个实现 IOutputBinding 接口的类。

internal sealed class MyBinding : IOutputBinding
{
    public Task InitAsync(MetadataRequest request, CancellationToken cancellationToken = default)
    {
        // 使用配置的元数据初始化组件时调用...
    }

    public Task<OutputBindingInvokeResponse> InvokeAsync(OutputBindingInvokeRequest request, CancellationToken cancellationToken = default)
    {
        // 调用以执行特定操作...
    }

    public Task<string[]> ListOperationsAsync(CancellationToken cancellationToken = default)
    {
        // 调用以列出可执行的操作。
    }
}

输入和输出绑定组件

组件可以同时是输入和输出绑定,只需实现这两个接口即可。

internal sealed class MyBinding : IInputBinding, IOutputBinding
{
    // IInputBinding 实现...

    // IOutputBinding 实现...
}

注册绑定组件

在主程序文件(例如 Program.cs)中,在应用程序服务中注册绑定组件。

using Dapr.PluggableComponents;

var app = DaprPluggableComponentsApplication.Create();

app.RegisterService(
    "<socket name>",
    serviceBuilder =>
    {
        serviceBuilder.RegisterBinding<MyBinding>();
    });

app.Run();

后续步骤

3.1.2 - 实现 .NET 发布订阅组件

如何使用 Dapr 可插拔组件 .NET SDK 创建发布订阅

创建发布订阅组件只需几个基本步骤。

添加发布订阅命名空间

为发布订阅相关的命名空间添加 using 语句。

using Dapr.PluggableComponents.Components;
using Dapr.PluggableComponents.Components.PubSub;

实现 IPubSub

创建一个实现 IPubSub 接口的类。

internal sealed class MyPubSub : IPubSub
{
    public Task InitAsync(MetadataRequest request, CancellationToken cancellationToken = default)
    {
        // 调用以使用配置的元数据初始化组件...
    }

    public Task PublishAsync(PubSubPublishRequest request, CancellationToken cancellationToken = default)
    {
        // 将消息发送到"topic"...
    }

    public Task PullMessagesAsync(PubSubPullMessagesTopic topic, MessageDeliveryHandler<string?, PubSubPullMessagesResponse> deliveryHandler, CancellationToken cancellationToken = default)
    {
        // 直到取消之前,检查 topic 中的消息并将其传递给 Dapr runtime...
    }
}

PullMessagesAsync() 方法的调用是"长期存在"的,也就是说该方法在取消之前(例如通过 cancellationToken)不会返回。应从中拉取消息的"topic"通过 topic 参数传递,而向 Dapr runtime 的传递则通过 deliveryHandler 回调执行。传递允许组件在应用程序(由 Dapr runtime 提供服务)确认已处理消息时接收通知。

    public async Task PullMessagesAsync(PubSubPullMessagesTopic topic, MessageDeliveryHandler<string?, PubSubPullMessagesResponse> deliveryHandler, CancellationToken cancellationToken = default)
    {
        TimeSpan pollInterval = // 轮询间隔(例如来自初始化元数据)...

        // 轮询 topic 直到取消...
        while (!cancellationToken.IsCancellationRequested)
        {
            var messages = // 从 topic 轮询消息...

            foreach (var message in messages)
            {
                // 将消息传递给 Dapr runtime...
                await deliveryHandler(
                    new PubSubPullMessagesResponse(topicName)
                    {
                        // 设置消息内容...
                    },
                    // 当应用程序确认消息时调用的回调...
                    async errorMessage =>
                    {
                        // 空消息表示应用程序成功处理了消息...
                        if (String.IsNullOrEmpty(errorMessage))
                        {
                            // 从 topic 中删除消息...
                        }
                    })
            }

            // 等待下一次轮询(或取消)...
            await Task.Delay(pollInterval, cancellationToken);
        }
    }

注册发布订阅组件

在主程序文件(例如 Program.cs)中,向应用程序服务注册发布订阅组件。

using Dapr.PluggableComponents;

var app = DaprPluggableComponentsApplication.Create();

app.RegisterService(
    "<socket name>",
    serviceBuilder =>
    {
        serviceBuilder.RegisterPubSub<MyPubSub>();
    });

app.Run();

后续步骤

3.1.3 - 实现 .NET 状态存储组件

如何使用 Dapr 可插拔组件 .NET SDK 创建状态存储

创建状态存储组件只需要几个基本步骤。

添加状态存储命名空间

添加状态存储相关命名空间的 using 语句。

using Dapr.PluggableComponents.Components;
using Dapr.PluggableComponents.Components.StateStore;

实现 IStateStore

创建一个实现 IStateStore 接口的类。

internal sealed class MyStateStore : IStateStore
{
    public Task DeleteAsync(StateStoreDeleteRequest request, CancellationToken cancellationToken = default)
    {
        // 从状态存储中删除请求的键...
    }

    public Task<StateStoreGetResponse?> GetAsync(StateStoreGetRequest request, CancellationToken cancellationToken = default)
    {
        // 从状态存储中获取请求的键值,否则返回 null...
    }

    public Task InitAsync(MetadataRequest request, CancellationToken cancellationToken = default)
    {
        // 调用以使用配置的元数据初始化组件...
    }

    public Task SetAsync(StateStoreSetRequest request, CancellationToken cancellationToken = default)
    {
        // 在状态存储中设置请求的键为指定值...
    }
}

注册状态存储组件

在主程序文件(例如 Program.cs)中,向应用程序服务注册状态存储。

using Dapr.PluggableComponents;

var app = DaprPluggableComponentsApplication.Create();

app.RegisterService(
    "<socket name>",
    serviceBuilder =>
    {
        serviceBuilder.RegisterStateStore<MyStateStore>();
    });

app.Run();

批量状态存储

旨在支持批量操作的状态存储应实现可选的 IBulkStateStore 接口。其方法镜像了基础 IStateStore 接口的方法,但包含多个请求值。

internal sealed class MyStateStore : IStateStore, IBulkStateStore
{
    // ...

    public Task BulkDeleteAsync(StateStoreDeleteRequest[] requests, CancellationToken cancellationToken = default)
    {
        // 从状态存储中删除所有请求的值...
    }

    public Task<StateStoreBulkStateItem[]> BulkGetAsync(StateStoreGetRequest[] requests, CancellationToken cancellationToken = default)
    {
        // 从状态存储中返回所有请求的值...
    }

    public Task BulkSetAsync(StateStoreSetRequest[] requests, CancellationToken cancellationToken = default)
    {
        // 在状态存储中设置所有请求的键的值...
    }
}

事务状态存储

旨在支持事务的状态存储应实现可选的 ITransactionalStateStore 接口。其 TransactAsync() 方法接收一个请求,其中包含要在事务中执行的一系列删除和/或设置操作。状态存储应遍历该序列并调用每个操作的 Visit() 方法,传入代表对每种操作类型要执行的操作的回调。

internal sealed class MyStateStore : IStateStore, ITransactionalStateStore
{
    // ...

    public async Task TransactAsync(StateStoreTransactRequest request, CancellationToken cancellationToken = default)
    {
        // 开始事务...

        try
        {
            foreach (var operation in request.Operations)
            {
                await operation.Visit(
                    async deleteRequest =>
                    {
                        // 处理删除请求...

                    },
                    async setRequest =>
                    {
                        // 处理设置请求...
                    });
            }
        }
        catch
        {
            // 回滚事务...

            throw;
        }

        // 提交事务...
    }
}

可查询状态存储

旨在支持查询的状态存储应实现可选的 IQueryableStateStore 接口。其 QueryAsync() 方法接收有关查询的详细信息,例如筛选器、结果限制和分页,以及结果的排序顺序。状态存储应使用这些详细信息生成一组值作为其响应的一部分返回。

internal sealed class MyStateStore : IStateStore, IQueryableStateStore
{
    // ...

    public Task<StateStoreQueryResponse> QueryAsync(StateStoreQueryRequest request, CancellationToken cancellationToken = default)
    {
        // 生成并返回结果...
    }
}

ETag 和其他语义错误处理

Dapr 运行时对某些状态存储操作导致的某些错误条件有额外的处理。状态存储可以通过从其操作逻辑中抛出特定异常来指示此类情况:

异常适用操作描述
ETagInvalidExceptionDelete、Set、Bulk Delete、Bulk Set当 ETag 无效时
ETagMismatchExceptionDelete、Set、Bulk Delete、Bulk Set当 ETag 与预期值不匹配时
BulkDeleteRowMismatchExceptionBulk Delete当受影响的行数与预期行数不匹配时

后续步骤

3.1.4 - Dapr 可插拔组件 .NET SDK 的高级用法

如何使用 Dapr 可插拔组件 .NET SDK 的高级技术

尽管大多数人通常不需要,但这些指南展示了配置 .NET 可插拔组件的高级方法。

3.1.4.1 - .NET Dapr 可插拔组件中的多个服务

如何从 .NET 可插拔组件公开多个服务

可插拔组件可以托管多种类型的多个组件。您可能需要这样做:

  • 为了最小化集群中运行的边车数量
  • 为了分组可能共享库和实现的相关组件,例如:
    • 一个既作为通用状态存储公开的数据库,和
    • 允许更特定操作的输出绑定。

每个 Unix 域套接字可以管理对每种类型的一个组件的调用。要托管相同类型的多个组件,您可以将这些类型分散到多个套接字上。SDK 将每个套接字绑定到一个"服务",每个服务由一种或多种组件类型组成。

注册多个服务

每次调用 RegisterService() 都会将一个套接字绑定到一组已注册的组件,其中每个服务可以注册每种类型的组件中的一个。

var app = DaprPluggableComponentsApplication.Create();

app.RegisterService(
    "service-a",
    serviceBuilder =>
    {
        serviceBuilder.RegisterStateStore<MyDatabaseStateStore>();
        serviceBuilder.RegisterBinding<MyDatabaseOutputBinding>();
    });

app.RegisterService(
    "service-b",
    serviceBuilder =>
    {
        serviceBuilder.RegisterStateStore<AnotherStateStore>();
    });

app.Run();

class MyDatabaseStateStore : IStateStore
{
    // ...
}

class MyDatabaseOutputBinding : IOutputBinding
{
    // ...
}

class AnotherStateStore : IStateStore
{
    // ...
}

配置多个组件

配置 Dapr 以使用托管组件与任何单个组件相同 - 组件 YAML 引用关联的套接字。

#
# 此组件使用与套接字 `state-store-a` 关联的状态存储
#
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: state-store-a
spec:
  type: state.service-a
  version: v1
  metadata: []
#
# 此组件使用与套接字 `state-store-b` 关联的状态存储
#
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: state-store-b
spec:
  type: state.service-b
  version: v1
  metadata: []

后续步骤

3.1.4.2 - .NET Dapr 可插拔组件的应用环境

如何配置 .NET 可插拔组件的环境

.NET Dapr 可插拔组件应用可以像 ASP.NET 应用一样配置依赖注入、日志和配置值。DaprPluggableComponentsApplication 暴露了一组与 WebApplicationBuilder 相似的配置属性。

依赖注入

注册到服务的组件可以参与依赖注入。在创建组件时,组件构造函数中的参数将被注入,前提是这些类型已在应用中注册。你可以通过 DaprPluggableComponentsApplication 暴露的 IServiceCollection 注册它们。

var app = DaprPluggableComponentsApplication.Create();

// 将 MyService 注册为 IService 的单例实现。
app.Services.AddSingleton<IService, MyService>();

app.RegisterService(
    "<service name>",
    serviceBuilder =>
    {
        serviceBuilder.RegisterStateStore<MyStateStore>();
    });

app.Run();

interface IService
{
    // ...
}

class MyService : IService
{
    // ...
}

class MyStateStore : IStateStore
{
    // 在创建状态存储时注入 IService。
    public MyStateStore(IService service)
    {
        // ...
    }

    // ...
}

日志

.NET Dapr 可插拔组件可以使用标准 .NET 日志机制DaprPluggableComponentsApplication 暴露了一个 ILoggingBuilder,可以通过它进行配置。

var app = DaprPluggableComponentsApplication.Create();

// 清除默认日志记录器并设置新的日志记录器。
app.Logging.ClearProviders();
app.Logging.AddConsole();

app.RegisterService(
    "<service name>",
    serviceBuilder =>
    {
        serviceBuilder.RegisterStateStore<MyStateStore>();
    });

app.Run();

class MyStateStore : IStateStore
{
    // 在创建状态存储时注入日志记录器。
    public MyStateStore(ILogger<MyStateStore> logger)
    {
        // ...
    }

    // ...
}

配置值

由于 .NET 可插拔组件基于 ASP.NET 构建,它们可以使用其标准配置机制,并默认使用同一组预先注册的提供程序DaprPluggableComponentsApplication 暴露了一个 IConfigurationManager,可以通过它进行配置。

var app = DaprPluggableComponentsApplication.Create();

// 清除默认配置提供程序并添加新的提供程序。
((IConfigurationBuilder)app.Configuration).Sources.Clear();
app.Configuration.AddEnvironmentVariables();

// 在启动时获取配置值。
const value = app.Configuration["<name>"];

app.RegisterService(
    "<service name>",
    serviceBuilder =>
    {
        serviceBuilder.RegisterStateStore<MyStateStore>();
    });

app.Run();

class MyStateStore : IStateStore
{
    // 在创建状态存储时注入配置。
    public MyStateStore(IConfiguration configuration)
    {
        // ...
    }

    // ...
}

后续步骤

3.1.4.3 - .NET Dapr 可插拔组件的生命周期

如何控制 .NET 可插拔组件的生命周期

有两种方式注册组件:

  • 组件作为单例运行,其生命周期由 SDK 管理
  • 组件的生命周期由可插拔组件决定,可根据需要为多实例或单例

单例组件

_按类型_注册的组件是单例:一个实例将为与该 socket 关联的该类型的所有已配置组件提供服务。当该类型仅存在单个组件且在 Dapr 应用程序之间共享时,此方法最佳。

var app = DaprPluggableComponentsApplication.Create();

app.RegisterService(
    "service-a",
    serviceBuilder =>
    {
        serviceBuilder.RegisterStateStore<SingletonStateStore>();
    });

app.Run();

class SingletonStateStore : IStateStore
{
    // ...
}

多实例组件

可以通过传递"工厂方法"来注册组件。对于与该 socket 关联的该类型的每个已配置组件,都会调用此方法。该方法返回要与该组件关联的实例(无论是否共享)。当同一类型的多个组件可能使用不同的元数据集进行配置,或需要将组件操作彼此隔离时,此方法最佳。

工厂方法将接收上下文,例如已配置的 Dapr 组件的 ID,可用于区分组件实例。

var app = DaprPluggableComponentsApplication.Create();

app.RegisterService(
    "service-a",
    serviceBuilder =>
    {
        serviceBuilder.RegisterStateStore(
            context =>
            {
                return new MultiStateStore(context.InstanceId);
            });
    });

app.Run();

class MultiStateStore : IStateStore
{
    private readonly string instanceId;

    public MultiStateStore(string instanceId)
    {
        this.instanceId = instanceId;
    }

    // ...
}

后续步骤

3.2 - Dapr 可插拔组件 Go SDK 入门

如何开始使用 Dapr 可插拔组件 Go SDK

Dapr 提供了用于帮助开发 Go 可插拔组件的软件包。

前置条件

应用程序创建

创建可插拔组件首先需要创建一个空的 Go 应用程序。

mkdir example
cd example
go mod init example

导入 Dapr 软件包

导入 Dapr 可插拔组件 SDK 软件包。

go get github.com/dapr-sandbox/components-go-sdk@v0.1.0

创建 main 包

main.go 中,导入 Dapr 可插拔组件软件包并运行应用程序。

package main

import (
	dapr "github.com/dapr-sandbox/components-go-sdk"
)

func main() {
	dapr.MustRun()
}

这将创建一个不包含任何组件的应用程序。你需要实现并注册一个或多个组件。

实现并注册组件

本地测试组件

创建 Dapr 组件套接字目录

Dapr 通过公共目录中的 Unix 域套接字文件与可插拔组件通信。默认情况下,Dapr 和可插拔组件都使用 /tmp/dapr-components-sockets 目录。如果该目录尚不存在,你应该创建它。

mkdir /tmp/dapr-components-sockets

启动可插拔组件

可以通过在命令行启动应用程序来测试可插拔组件。

要启动组件,在应用程序目录中:

go run main.go

配置 Dapr 以使用可插拔组件

要配置 Dapr 使用该组件,请在 resources 目录中创建一个组件 YAML 文件。例如,对于状态存储组件:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: <component name>
spec:
  type: state.<socket name>
  version: v1
  metadata:
  - name: key1
    value: value1
  - name: key2
    value: value2

当组件实例化时,任何 metadata 属性都将通过组件的 Store.Init(metadata state.Metadata) 方法传递给组件。

启动 Dapr

要启动 Dapr(以及可选的,使用该服务的服务):

dapr run --app-id <app id> --resources-path <resources path> ...

此时,Dapr 边车将启动并通过 Unix 域套接字连接到组件。然后你可以通过以下方式与组件交互:

  • 通过使用该组件的服务(如果已启动),或
  • 直接使用 Dapr HTTP 或 gRPC API

创建容器

可插拔组件作为容器部署,作为应用程序的边车运行(就像 Dapr 本身一样)。用于为 Go 应用程序创建 Docker 镜像的典型 Dockerfile 可能如下所示:

FROM golang:1.20-alpine AS builder

WORKDIR /usr/src/app

# 下载依赖
COPY go.mod go.sum ./
RUN go mod download && go mod verify

# 构建应用程序
COPY . .
RUN go build -v -o /usr/src/bin/app .

FROM alpine:latest

# 设置非 root 用户和权限
RUN addgroup -S app && adduser -S app -G app
RUN mkdir /tmp/dapr-components-sockets && chown app /tmp/dapr-components-sockets

# 将应用程序复制到运行时镜像
COPY --from=builder --chown=app /usr/src/bin/app /app

USER app

CMD ["/app"]

构建镜像:

docker build -f Dockerfile -t <image name>:<tag> .

后续步骤

3.2.1 - 实现 Go 输入/输出绑定组件

如何使用 Dapr 可插拔组件 Go SDK 创建输入/输出绑定

创建绑定组件只需几个基本步骤。

导入绑定包

创建文件 components/inputbinding.go 并添加与绑定相关包的 import 语句。

package components

import (
	"context"
	"github.com/dapr/components-contrib/bindings"
)

输入绑定:实现 InputBinding 接口

创建一个实现 InputBinding 接口的类型。

type MyInputBindingComponent struct {
}

func (component *MyInputBindingComponent) Init(meta bindings.Metadata) error {
	// 调用以使用配置的元数据初始化组件...
}

func (component *MyInputBindingComponent) Read(ctx context.Context, handler bindings.Handler) error {
	// 直到取消为止,检查底层存储中的消息并将其传递给 Dapr 运行时...
}

预期 Read() 方法调用会建立一个用于检索消息的长效机制,但立即返回 nil(或在无法设置该机制时返回错误)。该机制应在取消时结束(例如,通过 ctx.Done() or ctx.Err() != nil)。当从组件的底层存储读取消息时,它们通过 handler 回调传递给 Dapr 运行时,该回调在应用程序(由 Dapr 运行时提供服务)确认消息处理完成之前不会返回。

func (b *MyInputBindingComponent) Read(ctx context.Context, handler bindings.Handler) error {
	go func() {
		for {
			err := ctx.Err()

			if err != nil {
				return
			}
	
			messages := // 轮询消息...

            for _, message := range messages {
                handler(ctx, &bindings.ReadResponse{
                    // 设置消息内容...
                })
            }

			select {
				case <-ctx.Done():
				case <-time.After(5 * time.Second):
			} 
		}
	}()

	return nil
}

输出绑定:实现 OutputBinding 接口

创建一个实现 OutputBinding 接口的类型。

type MyOutputBindingComponent struct {
}

func (component *MyOutputBindingComponent) Init(meta bindings.Metadata) error {
	// 调用以使用配置的元数据初始化组件...
}

func (component *MyOutputBindingComponent) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
	// 调用以调用特定操作...
}

func (component *MyOutputBindingComponent) Operations() []bindings.OperationKind {
	// 调用以列出可被调用的操作。
}

输入和输出绑定组件

组件可以同时既是输入绑定又是输出绑定。只需实现两个接口并将组件注册为两种绑定类型。

注册绑定组件

在主应用程序文件(例如 main.go)中,向应用程序注册绑定组件。

package main

import (
	"example/components"
	dapr "github.com/dapr-sandbox/components-go-sdk"
	"github.com/dapr-sandbox/components-go-sdk/bindings/v1"
)

func main() {
	// 注册输入绑定...
	dapr.Register("my-inputbinding", dapr.WithInputBinding(func() bindings.InputBinding {
		return &components.MyInputBindingComponent{}
	}))

	// 注册输出绑定...
	dapr.Register("my-outputbinding", dapr.WithOutputBinding(func() bindings.OutputBinding {
		return &components.MyOutputBindingComponent{}
	}))

	dapr.MustRun()
}

后续步骤

3.2.2 - 实现 Go 发布订阅组件

如何使用 Dapr 可插拔组件 Go SDK 创建发布订阅组件

创建发布订阅组件只需要几个基本步骤。

导入发布订阅包

创建文件 components/pubsub.go 并添加发布订阅相关包的 import 语句。

package components

import (
	"context"
	"github.com/dapr/components-contrib/pubsub"
)

实现 PubSub 接口

创建一个实现 PubSub 接口的类型。

type MyPubSubComponent struct {
}

func (component *MyPubSubComponent) Init(metadata pubsub.Metadata) error {
	// 调用此方法以使用配置的元数据初始化组件...
}

func (component *MyPubSubComponent) Close() error {
    // 不用于可插拔组件...
	return nil
}

func (component *MyPubSubComponent) Features() []pubsub.Feature {
	// 返回组件支持的功能列表...
}

func (component *MyPubSubComponent) Publish(req *pubsub.PublishRequest) error {
	// 将消息发送到 "topic"...
}

func (component *MyPubSubComponent) Subscribe(ctx context.Context, req pubsub.SubscribeRequest, handler pubsub.Handler) error {
	// 在取消之前,持续检查 topic 是否有消息并将其传递给 Dapr 运行时...
}

Subscribe() 方法的调用预期会建立一个用于检索消息的长效机制,但立即返回 nil(或错误,如果无法建立该机制)。该机制应在取消时结束(例如,通过 ctx.Done()ctx.Err() != nil)。应从中拉取消息的 “topic” 通过 req 参数传递,而传递给 Dapr 运行时则通过 handler 回调执行。回调在应用程序(由 Dapr 运行时提供服务)确认消息处理后才会返回。

func (component *MyPubSubComponent) Subscribe(ctx context.Context, req pubsub.SubscribeRequest, handler pubsub.Handler) error {
	go func() {
		for {
			err := ctx.Err()

			if err != nil {
				return
			}
	
			messages := // 轮询消息...

            for _, message := range messages {
                handler(ctx, &pubsub.NewMessage{
                    // 设置消息内容...
                })
            }

			select {
				case <-ctx.Done():
				case <-time.After(5 * time.Second):
			} 
		}
	}()

	return nil
}

注册发布订阅组件

在主应用程序文件(例如 main.go)中,向应用程序注册发布订阅组件。

package main

import (
	"example/components"
	dapr "github.com/dapr-sandbox/components-go-sdk"
	"github.com/dapr-sandbox/components-go-sdk/pubsub/v1"
)

func main() {
	dapr.Register("<socket name>", dapr.WithPubSub(func() pubsub.PubSub {
		return &components.MyPubSubComponent{}
	}))

	dapr.MustRun()
}

后续步骤

3.2.3 - 实现 Go 状态存储组件

如何使用 Dapr 可插拔组件 Go SDK 创建状态存储

创建状态存储组件只需要几个基本步骤。

导入状态存储包

创建文件 components/statestore.go 并添加状态存储相关包的 import 语句。

package components

import (
	"context"
	"github.com/dapr/components-contrib/state"
)

实现 Store 接口

创建一个实现 Store 接口的类型。

type MyStateStore struct {
}

func (store *MyStateStore) Init(metadata state.Metadata) error {
	// 使用配置的元数据初始化组件时调用...
}

func (store *MyStateStore) GetComponentMetadata() map[string]string {
    // 可插拔组件不使用此方法...
	return map[string]string{}
}

func (store *MyStateStore) Features() []state.Feature {
	// 返回状态存储支持的功能列表...
}

func (store *MyStateStore) Delete(ctx context.Context, req *state.DeleteRequest) error {
	// 从状态存储中删除请求的键...
}

func (store *MyStateStore) Get(ctx context.Context, req *state.GetRequest) (*state.GetResponse, error) {
	// 从状态存储中获取请求的键值,否则返回空响应...
}

func (store *MyStateStore) Set(ctx context.Context, req *state.SetRequest) error {
	// 在状态存储中将请求的键设置为指定值...
}

func (store *MyStateStore) BulkGet(ctx context.Context, req []state.GetRequest) (bool, []state.BulkGetResponse, error) {
	// 从状态存储中获取请求的键值...
}

func (store *MyStateStore) BulkDelete(ctx context.Context, req []state.DeleteRequest) error {
	// 从状态存储中删除请求的键...
}

func (store *MyStateStore) BulkSet(ctx context.Context, req []state.SetRequest) error {
	// 在状态存储中将请求的键设置为指定的值...
}

注册状态存储组件

在主应用程序文件(例如 main.go)中,将状态存储注册到应用程序服务。

package main

import (
	"example/components"
	dapr "github.com/dapr-sandbox/components-go-sdk"
	"github.com/dapr-sandbox/components-go-sdk/state/v1"
)

func main() {
	dapr.Register("<socket name>", dapr.WithStateStore(func() state.Store {
		return &components.MyStateStoreComponent{}
	}))

	dapr.MustRun()
}

批量状态存储

虽然状态存储需要支持批量操作,但其实现会顺序委托给单个操作方法。

事务性状态存储

支持事务的状态存储应该实现可选的 TransactionalStore 接口。其 Multi() 方法接收一个包含要在事务中执行的 delete 和/或 set 操作序列的请求。状态存储应遍历该序列并应用每个操作。

func (store *MyStateStoreComponent) Multi(ctx context.Context, request *state.TransactionalStateRequest) error {
    // 开始事务...

    for _, operation := range request.Operations {
		switch operation.Operation {
		case state.Delete:
			deleteRequest := operation.Request.(state.DeleteRequest)
			// 处理删除请求...
		case state.Upsert:
			setRequest := operation.Request.(state.SetRequest)
			// 处理设置请求...
		}
	}

    // 结束(或回滚)事务...

	return nil
}

可查询状态存储

支持查询的状态存储应该实现可选的 Querier 接口。其 Query() 方法接收有关查询的详细信息,例如过滤器、结果限制、分页和结果的排序顺序。状态存储使用这些详细信息生成一组值作为其响应的一部分返回。

func (store *MyStateStoreComponent) Query(ctx context.Context, req *state.QueryRequest) (*state.QueryResponse, error) {
	// 生成并返回结果...
}

ETag 和其他语义错误处理

Dapr 运行时对某些状态存储操作导致的某些错误条件有额外的处理。状态存储可以通过从其操作逻辑返回特定错误来指示此类条件:

错误适用操作描述
NewETagError(state.ETagInvalid, ...)Delete、Set、Bulk Delete、Bulk Set当 ETag 无效时
NewETagError(state.ETagMismatch, ...)Delete、Set、Bulk Delete、Bulk Set当 ETag 与预期值不匹配时
NewBulkDeleteRowMismatchError(...)Bulk Delete当受影响的行数与预期行数不匹配时

后续步骤

3.2.4 - Dapr 可插拔组件 Go SDK 的高级用法

如何使用 Dapr 可插拔组件 Go SDK 的高级技术

虽然大多数人通常不需要,但这些指南展示了配置 Go 可插拔组件的高级方法。

组件生命周期

可插拔组件通过传递一个"工厂方法"来注册,该方法会为与该 socket 关联的该类型的每个已配置 Dapr 组件调用。该方法返回与该 Dapr 组件关联的实例(无论是否共享)。这允许多个相同类型的 Dapr 组件使用不同的元数据集进行配置,当组件操作需要相互隔离时等。

注册多个服务

每次调用 Register() 会将一个 socket 绑定到一个注册的可插拔组件。每个 socket 可以注册每种组件类型中的一个(输入/输出绑定、发布订阅和状态存储)。

func main() {
	dapr.Register("service-a", dapr.WithStateStore(func() state.Store {
		return &components.MyDatabaseStoreComponent{}
	}))

	dapr.Register("service-a", dapr.WithOutputBinding(func() bindings.OutputBinding {
		return &components.MyDatabaseOutputBindingComponent{}
	}))

	dapr.Register("service-b", dapr.WithStateStore(func() state.Store {
		return &components.MyDatabaseStoreComponent{}
	}))

	dapr.MustRun()
}

在上面的示例中,一个状态存储和输出绑定注册到 socket service-a,而另一个状态存储注册到 socket service-b

配置多个组件

配置 Dapr 使用托管组件与配置任何单个组件相同 — 组件 YAML 引用关联的 socket。例如,要为上面注册的两个组件(到 socket service-aservice-b)配置 Dapr 状态存储,您需要创建两个配置文件,每个文件引用其各自的 socket。

#
# 此组件使用与 socket `service-a` 关联的状态存储
#
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: state-store-a
spec:
  type: state.service-a
  version: v1
  metadata: []
#
# 此组件使用与 socket `service-b` 关联的状态存储
#
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: state-store-b
spec:
  type: state.service-b
  version: v1
  metadata: []

后续步骤