This is the multi-page printable view of this section. Click here to print.
使用 Dapr 开发应用程序
- 1: 构建块
- 1.1: 服务调用
- 1.1.1: 服务调用概述
- 1.1.2: 如何:使用 HTTP 调用服务
- 1.1.3: 如何:使用 gRPC 调用服务
- 1.1.4: 操作指南:使用 HTTP 调用非 Dapr 端点
- 1.1.5: 操作指南:跨命名空间的服务调用
- 1.2: 发布与订阅消息
- 1.2.1: 发布订阅概述
- 1.2.2: 操作指南:发布消息并订阅主题
- 1.2.3: 使用 CloudEvents 发布和订阅消息
- 1.2.4: 不使用 CloudEvents 发布和订阅消息
- 1.2.5: 操作指南:将消息路由到不同的事件处理程序
- 1.2.6: 声明式、流式和编程式订阅类型
- 1.2.7: 死信主题
- 1.2.8: 如何操作:设置发布订阅命名空间消费者组
- 1.2.9: 如何:通过 StatefulSet 水平扩展订阅者
- 1.2.10: 限制发布订阅主题访问
- 1.2.11: 消息生存时间 (TTL)
- 1.2.12: 批量发布和订阅消息
- 1.3: 工作流
- 1.3.1: 工作流概述
- 1.3.2: 功能与概念
- 1.3.3: 工作流版本控制
- 1.3.4: 工作流模式
- 1.3.5: 工作流架构
- 1.3.6: 方法指南:编写工作流
- 1.3.7: 操作指南:管理工作流
- 1.3.8: 多应用工作流
- 1.3.9: 历史保留策略
- 1.3.10: 工作流执行并发
- 1.4: 状态管理
- 1.4.1: 状态管理概述
- 1.4.2: 操作指南:保存和获取状态
- 1.4.3: 操作方法:查询状态
- 1.4.4: 操作指南:构建有状态服务
- 1.4.5: How-To:启用事务性 Outbox 模式
- 1.4.6: 操作指南:在应用程序之间共享状态
- 1.4.7: 操作指南:加密应用状态
- 1.4.8: 使用后端状态存储
- 1.4.8.1: Azure Cosmos DB
- 1.4.8.2: Redis
- 1.4.8.3: SQL Server
- 1.4.9: 状态生存时间 (TTL)
- 1.5: Bindings
- 1.5.1: Bindings 概述
- 1.5.2: 操作指南:使用输入绑定触发应用程序
- 1.5.3: 操作指南:使用输出绑定与外部资源交互
- 1.6: Actors
- 1.6.1: Actor 概述
- 1.6.2: Actor 运行时功能
- 1.6.3: Actor 运行时配置参数
- 1.6.4: 命名空间 Actor
- 1.6.5: Actor 定时器和提醒
- 1.6.6: 如何:通过脚本与虚拟 actor 交互
- 1.6.7: 如何:在 Dapr 中启用和使用 Actor 重入
- 1.7: 机密管理
- 1.7.1: 密钥管理概览
- 1.7.2: 操作指南:获取密钥
- 1.7.3: 如何使用:密钥作用域
- 1.8: Configuration
- 1.8.1: Configuration overview
- 1.8.2: 操作指南:从存储中管理配置
- 1.9: 分布式锁
- 1.10: 密码学
- 1.10.1: Cryptography 概述
- 1.10.2: 操作指南:使用 Cryptography API
- 1.11: Jobs
- 1.11.1: Jobs 概述
- 1.11.2: 特性与概念
- 1.11.3: 操作指南:调度和处理触发的作业
- 1.12: Conversation
- 1.12.1: 对话概览
- 1.12.2: How-To: 使用对话 API 与 LLM 对话
- 2: Dapr 软件开发工具包(SDK)
- 2.1: Dapr .NET SDK
- 2.1.1: Dapr 客户端 .NET SDK 入门
- 2.1.1.1: DaprClient 使用指南
- 2.1.2: Dapr Workflow .NET SDK
- 2.1.2.1: DaprWorkflowClient 生命周期管理与注册
- 2.1.2.2: .NET SDK 中的工作流序列化
- 2.1.2.3: .NET SDK 中的多应用程序工作流
- 2.1.2.4: 使用 DaprWorkflowClient 进行工作流管理操作
- 2.1.2.5: .NET SDK 中的工作流版本控制
- 2.1.2.6: .NET 工作流示例
- 2.1.3: Dapr Actors .NET SDK
- 2.1.3.1: IActorProxyFactory 接口
- 2.1.3.2: Author & run actors
- 2.1.3.3: .NET SDK 中的 Actor 序列化
- 2.1.3.4: 如何:在 .NET SDK 中运行和使用虚拟 Actor
- 2.1.4: Dapr AI .NET SDK
- 2.1.4.1: Dapr AI 客户端
- 2.1.4.2: 操作指南:在 .NET SDK 中创建和使用 Dapr AI 会话
- 2.1.4.3: 如何:在 Dapr 的 .NET Conversation SDK 中使用 Microsoft 的 AI 扩展
- 2.1.5: Dapr Jobs .NET SDK
- 2.1.5.1: 操作指南:在 .NET SDK 中编写和管理 Dapr Jobs
- 2.1.5.2: DaprJobsClient 使用
- 2.1.6: Dapr Cryptography .NET SDK
- 2.1.6.1: Dapr Cryptography Client
- 2.1.6.2: 如何操作:在 .NET SDK 中创建和使用 Dapr Cryptography
- 2.1.7: Dapr Messaging .NET SDK
- 2.1.7.1: 如何操作:使用 .NET SDK 创建和管理 Dapr 流式订阅
- 2.1.7.2: DaprPublishSubscribeClient 用法
- 2.1.8: Dapr 分布式锁 .NET SDK
- 2.1.8.1: 操作指南:在 .NET SDK 中创建和使用 Dapr 分布式锁
- 2.1.9: Dapr .NET SDK 最佳实践
- 2.1.9.1: Dapr .NET SDK 中的错误模型
- 2.1.9.2: Experimental Attributes
- 2.1.9.3: Integration testing with Dapr.Testcontainers
- 2.1.9.4: Dapr 源代码分析器和生成器
- 2.1.10: 使用 Dapr .NET SDK 开发应用程序
- 2.1.10.1: 使用 Dapr CLI 进行 Dapr .NET SDK 开发
- 2.1.10.2: 使用 Docker Compose 进行 Dapr .NET SDK 开发
- 2.1.10.3: 使用 .NET Aspire 进行 Dapr .NET SDK 开发
- 2.1.11: 如何使用 Dapr .NET SDK 进行故障排除和调试
- 2.1.11.1: 使用 .NET SDK 对发布订阅进行故障排查
- 2.2: Dapr Go SDK
- 2.2.1: Dapr 客户端 Go SDK 入门
- 2.2.2: Dapr Service(回调)SDK for Go 入门
- 2.2.2.1: Dapr HTTP Service SDK for Go 入门
- 2.2.2.2: Dapr 服务(回调)SDK for Go 入门
- 2.3: Dapr Java SDK
- 2.3.1: AI
- 2.3.2: Dapr 客户端 Java SDK 入门
- 2.3.2.1: 属性
- 2.3.3: 工作流
- 2.3.3.1: 操作指南:在 Java SDK 中编写和管理 Dapr 工作流
- 2.3.4: 作业
- 2.3.4.1: 如何:使用 Java SDK 编写和管理 Dapr Jobs
- 2.3.5: Dapr 与 Spring Boot 入门
- 2.3.5.1: 操作指南:使用 Spring Boot 编写和管理 Dapr 工作流
- 2.4: JavaScript SDK
- 2.4.1: JavaScript 客户端 SDK
- 2.4.2: JavaScript Server SDK
- 2.4.3: JavaScript SDK for Actors
- 2.4.4: JavaScript SDK 中的日志记录
- 2.4.5: JavaScript 示例
- 2.4.6: 如何:在 JavaScript SDK 中编写和管理 Dapr 工作流
- 2.5: Dapr PHP SDK
- 2.5.1: 虚拟 Actor
- 2.5.1.1: 生产环境参考:Actor
- 2.5.2: 使用 PHP 进行发布订阅
- 2.5.3: 应用
- 2.5.3.1: 单元测试
- 2.5.4: 使用 PHP 进行状态管理
- 2.5.5: 自定义序列化
- 2.6: Dapr Python SDK
- 2.6.1: Dapr 客户端 Python SDK 入门
- 2.6.2: Dapr Actor Python SDK 入门
- 2.6.3: Dapr Python SDK 扩展
- 2.6.3.1: Dapr Python gRPC 服务扩展入门
- 2.6.3.2: Dapr Python SDK 与 FastAPI 集成
- 2.6.3.3: Dapr Python SDK 与 Flask 集成
- 2.6.3.4: Dapr Python SDK 与 Dapr 工作流扩展集成
- 2.6.3.4.1: Dapr Workflow Python SDK 入门
- 2.6.4:
- 2.7: Dapr Rust SDK
- 2.7.1: 开始使用 Dapr 客户端 Rust SDK
- 3: 错误码
- 3.1: 错误概述
- 3.2: 错误码参考指南
- 3.3: 处理 HTTP 错误代码
- 3.4: 处理 gRPC 错误码
- 4: 本地开发
- 4.1: IDE 支持
- 4.1.1: Visual Studio Code 与 Dapr 集成
- 4.1.1.1: Dapr Visual Studio Code 扩展概述
- 4.1.1.2: 如何操作:使用 Visual Studio Code 调试 Dapr 应用程序
- 4.1.1.3: 使用 Dev Containers 开发 Dapr 应用程序
- 4.1.2: IntelliJ
- 4.2: 多应用运行
- 4.2.1: Multi-App Run 概述
- 4.2.2: 操作指南:使用多应用运行模板文件
- 4.3: 操作指南:在 Dapr 应用中使用 gRPC 接口
- 4.4: Dapr SDK 中的序列化
- 5: 调试 Dapr 应用程序和 Dapr 控制平面
- 5.1: 在 Kubernetes 模式下调试 Dapr
- 5.1.1: 在 Kubernetes 上调试 Dapr 控制平面
- 5.1.2: 在 Kubernetes 上调试 daprd
- 5.2: 调试在 Docker Compose 中运行的 Dapr 应用
- 6: 集成
- 6.1: 与 AWS 的集成
- 6.1.1: AWS 认证
- 6.2: 与 Azure 的集成
- 6.2.1: 对 Azure 进行身份验证
- 6.2.1.1: 向 Azure 进行身份验证
- 6.2.1.2: 如何使用工作负载身份联合
- 6.2.1.3: 操作指南:生成新的 Microsoft Entra ID 应用程序和服务主体
- 6.2.1.4: 如何:使用托管标识
- 6.2.2: Azure API Management 的 Dapr 集成策略
- 6.2.3: Dapr extension for Azure Functions runtime
- 6.2.4: 适用于 Azure Kubernetes Service (AKS) 的 Dapr 扩展
- 6.3: Diagrid 集成
- 6.4: 如何:使用 KEDA 自动扩缩 Dapr 应用
- 6.5: 如何:在 GitHub Actions 工作流中使用 Dapr CLI
- 6.6: 操作指南:使用 Dapr Kubernetes Operator
- 6.7: 如何:与 Kratix 集成
- 6.8: 操作指南:与 Argo CD 集成
- 7: 组件
- 7.1: 可插拔组件
- 7.1.1: 可插拔组件概述
- 7.1.2: 如何操作:实现可插拔组件
- 7.1.3: 可插拔组件 SDK
- 7.1.3.1: Dapr 可插拔组件 .NET SDK 入门
- 7.1.3.1.1: 实现 .NET 输入/输出绑定组件
- 7.1.3.1.2: 实现 .NET 发布订阅组件
- 7.1.3.1.3: 实现 .NET 状态存储组件
- 7.1.3.1.4: Dapr 可插拔组件 .NET SDK 的高级用法
- 7.1.3.1.4.1: .NET Dapr 可插拔组件中的多个服务
- 7.1.3.1.4.2: .NET Dapr 可插拔组件的应用环境
- 7.1.3.1.4.3: .NET Dapr 可插拔组件的生命周期
- 7.1.3.2: Dapr 可插拔组件 Go SDK 入门
- 7.1.3.2.1: 实现 Go 输入/输出绑定组件
- 7.1.3.2.2: 实现 Go 发布订阅组件
- 7.1.3.2.3: 实现 Go 状态存储组件
- 7.1.3.2.4: Dapr 可插拔组件 Go SDK 的高级用法
- 7.2: 如何编写中间件组件
1.1 - 服务调用
1.1.1 - 服务调用概述
通过服务调用,您的应用程序可以使用标准的 gRPC 或 HTTP 协议可靠且安全地与其他应用程序通信。
在许多基于微服务的应用程序中,多个服务需要相互通信的能力。这种服务间通信要求应用程序开发者处理以下问题:
- 服务发现。 如何发现不同的服务?
- 标准化服务间 API 调用。 如何在不同服务之间调用方法?
- 安全的服务间通信。 如何通过加密安全地调用其他服务,并对方法实施访问控制?
- 缓解请求超时或故障。 如何处理重试和瞬态错误?
- 实现可观测性和追踪。 如何使用追踪来查看带有指标的调用图,以诊断生产环境中的问题?
服务调用 API
Dapr 通过提供服务调用 API 来应对这些挑战,该 API 的行为类似于具有内置服务发现功能的反向代理,同时利用了内置的分布式追踪、指标、错误处理、加密等功能。
Dapr 使用边车架构。要使用 Dapr 调用应用程序:
- 您可以在 Dapr 实例上使用
invokeAPI。 - 每个应用程序与其自己的 Dapr 实例通信。
- Dapr 实例之间相互发现和通信。
以下概述视频和演示 演示了 Dapr 服务调用的工作原理。
下图是 Dapr 服务调用在两个 Dapr 化应用程序之间工作方式的概述。

- 服务 A 发送 HTTP 或 gRPC 调用到服务 B。调用首先到达本地 Dapr 边车。
- Dapr 使用名称解析组件发现服务 B 的位置,该组件运行在给定的托管平台上。
- Dapr 将消息转发到服务 B 的 Dapr 边车
- 注意:Dapr 边车之间的所有调用都通过 gRPC 进行以提高性能。只有服务与 Dapr 边车之间的调用可以是 HTTP 或 gRPC。
- 服务 B 的 Dapr 边车将请求转发到服务 B 上的指定端点(或方法)。然后服务 B 执行其业务逻辑代码。
- 服务 B 向服务 A 发送响应。响应首先到达服务 B 的边车。
- Dapr 将响应转发到服务 A 的 Dapr 边车。
- 服务 A 接收响应。
您还可以使用服务调用 API 调用非 Dapr HTTP 端点。例如,您可能只在部分整体应用程序中使用 Dapr,可能无法访问用于将现有应用程序迁移到使用 Dapr 的代码,或者只是需要调用外部 HTTP 服务。阅读“如何:使用 HTTP 调用非 Dapr 端点” 获取更多信息。
功能特性
服务调用提供了多项功能,使您可以轻松地在应用程序之间调用方法或调用外部 HTTP 端点。
HTTP 和 gRPC 服务调用
- HTTP:如果您已经在应用程序中使用 HTTP 协议,使用 Dapr HTTP 头可能是最简单的入门方式。您无需更改现有的端点 URL;只需添加
dapr-app-id头即可。更多信息请参阅使用 HTTP 调用服务。 - gRPC:Dapr 允许用户保留自己的 proto 服务并原生使用 gRPC。这意味着您可以使用服务调用来调用现有的 gRPC 应用程序,而无需包含任何 Dapr SDK 或自定义 gRPC 服务。更多信息请参阅 Dapr 和 gRPC 操作指南。
服务到服务安全
通过 Dapr Sentry 服务,Dapr 应用程序之间的所有调用都可以使用托管平台上的相互(mTLS)身份验证来保证安全,包括自动证书轮换。
更多信息请阅读服务到服务安全文章。
包括重试在内的弹性
在发生调用失败和瞬态错误的情况下,服务调用提供了一种弹性功能,可以执行带退避时间的自动重试。了解更多,请参阅弹性文章。
可观测性追踪和指标
默认情况下,应用程序之间的所有调用都会被追踪,并收集指标以提供洞察和诊断。这在生产场景中尤为重要,提供服务间调用的调用图和指标。更多信息请阅读可观测性。
访问控制
通过访问策略,应用程序可以控制:
- 允许哪些应用程序调用它们。
- 哪些应用程序被授权执行操作。
例如,您可以限制包含人员信息的敏感应用程序被未经授权的应用程序访问。结合服务到服务安全通信,您可以提供软多租户部署。
更多信息请阅读服务调用的访问控制白名单文章。
命名空间作用域
您可以将应用程序作用域限定到命名空间,用于部署和安全目的,并可以调用部署在不同命名空间中的服务。更多信息请阅读跨命名空间的服务调用文章。
使用 mDNS 的轮询负载均衡
Dapr 使用 mDNS 协议提供服务调用的轮询负载均衡,例如在单机上或多个联网的物理机器上。
下图展示了这如何工作的示例。如果您有 1 个应用程序 ID 为 FrontEnd 的应用程序实例和 3 个应用程序 ID 为 Cart 的应用程序实例,当您从 FrontEnd 应用调用 Cart 应用时,Dapr 会在这 3 个实例之间轮询。这些实例可以在同一台机器上,也可以在不同的机器上。

注意:应用程序 ID 在每个_应用程序_中是唯一的,而不是应用程序实例。无论该应用程序存在多少个实例(由于扩展),它们都将共享相同的应用程序 ID。
可交换的服务发现
Dapr 可以在各种托管平台上运行。为了使服务调用支持可交换的服务发现,Dapr 使用名称解析组件。例如,Kubernetes 名称解析组件使用 Kubernetes DNS 服务来解析集群中运行的其他应用程序的位置。
自托管机器可以使用 mDNS 名称解析组件。作为替代方案,您可以使用 SQLite 名称解析组件在单节点环境和本地开发场景中运行 Dapr。作为集群一部分的 Dapr 边车将其信息存储在本地机器上的 SQLite 数据库中。
Consul 名称解析组件特别适用于多机器部署,可用于任何托管环境,包括 Kubernetes、多个虚拟机或自托管。
HTTP 服务调用的流式处理
您可以在 HTTP 服务调用中将数据作为流处理。当使用 Dapr 通过 HTTP 调用另一个服务并处理大型请求或响应体时,这可以提供性能和内存利用率方面的改进。
下图演示了数据流的六个步骤。

- 请求:“App A” 到 “Dapr sidecar A”
- 请求:“Dapr sidecar A” 到 “Dapr sidecar B”
- 请求:“Dapr sidecar B” 到 “App B”
- 响应:“App B” 到 “Dapr sidecar B”
- 响应:“Dapr sidecar B” 到 “Dapr sidecar A”
- 响应:“Dapr sidecar A” 到 “App A”
示例架构
按照上述调用顺序,假设您有 Hello World 教程 中描述的应用程序,其中一个 Python 应用程序调用一个 Node.js 应用程序。在这种情况下,Python 应用程序是"服务 A",Node.js 应用程序是"服务 B"。
下图再次展示了在本地机器上显示 API 调用的序列 1-7:

- Node.js 应用程序的 Dapr 应用程序 ID 为
nodeapp。Python 应用程序通过 POSThttp://localhost:3500/v1.0/invoke/nodeapp/method/neworder来调用 Node.js 应用程序的neworder方法,该请求首先到达 Python 应用程序的本地 Dapr 边车。 - Dapr 使用名称解析组件(在本例中为自托管时的 mDNS)发现 Node.js 应用程序的位置,该组件在您的本地机器上运行。
- Dapr 使用刚刚获得的位置将请求转发到 Node.js 应用程序的边车。
- Node.js 应用程序的边车将请求转发到 Node.js 应用程序。Node.js 应用程序执行业务逻辑,记录传入消息,然后将订单 ID 持久化到 Redis(图中未显示)。
- Node.js 应用程序通过 Node.js 边车向 Python 应用程序发送响应。
- Dapr 将响应转发到 Python Dapr 边车。
- Python 应用程序接收响应。
试用服务调用
快速入门和教程
Dapr 文档包含多个快速入门,展示了在不同示例架构中利用服务调用构建块的方式。要直接了解服务调用 API 及其功能,我们建议从以下快速入门开始:
| 快速入门/教程 | 描述 |
|---|---|
| 服务调用快速入门 | 此快速入门让您直接与服务调用构建块交互。 |
| Hello world 教程 | 本教程展示如何同时使用服务调用和状态管理构建块,全部在本地机器上运行。 |
| Hello world Kubernetes 教程 | 本教程介绍如何在 Kubernetes 中使用 Dapr,涵盖服务调用和状态管理构建块。 |
直接在您的应用程序中开始使用服务调用
想跳过快速入门吗?没问题。您可以直接在应用程序中试用服务调用构建块,与其他服务安全通信。安装 Dapr 后,您可以通过以下方式开始使用服务调用 API。
使用以下方式调用服务:
- HTTP 和 gRPC 服务调用(推荐的设置方法)
- HTTP - 只需添加
dapr-app-id头即可开始使用。点击此处了解更多:使用 HTTP 调用服务。 - gRPC - 对于基于 gRPC 的应用程序,服务调用 API 也可用。运行 gRPC 服务器,然后使用 Dapr CLI 调用服务。在配置 Dapr 使用 gRPC和使用 gRPC 调用服务中了解更多。
- HTTP - 只需添加
- 直接调用 API - 除了代理之外,还有一个选项是直接调用服务调用 API 来调用 GET 端点。只需将您的地址 URL 更新为
localhost:<dapr-http-port>,您就可以直接调用 API。您还可以在上面 HTTP 代理部分链接的"使用 HTTP 调用服务"文档中了解更多。 - SDK - 如果您使用的是 Dapr SDK,您可以通过 SDK 直接使用服务调用。选择您需要的 SDK 并使用 Dapr 客户端来调用服务。在 Dapr SDK中了解更多。
要进行快速测试,请尝试使用 Dapr CLI 进行服务调用:
- Dapr CLI 命令 - 设置 Dapr CLI 后,使用
dapr invoke --method <method-name>命令以及方法标志和感兴趣的方法。在 Dapr CLI中了解更多。
后续步骤
- 阅读服务调用 API 规范。此服务调用参考指南描述了如何调用其他服务上的方法。
- 了解服务调用性能数据。
- 查看可观测性。在这里您可以深入了解 Dapr 的监控工具,如追踪、指标和日志。
- 阅读我们的mTLS 加密、令牌身份验证和端点授权安全实践。
1.1.2 - 如何:使用 HTTP 调用服务
本文演示了如何部署服务,每个服务都有一个唯一的 application ID,其他服务可以使用 HTTP 上的服务调用来发现它们并调用其端点。

注意
如果你还没有尝试过,请先体验服务调用快速入门来快速了解如何使用服务调用 API。为你的服务选择一个 ID
Dapr 允许你为你的应用分配一个全局唯一的 ID。这个 ID 封装了你的应用的状态,无论它可能有多少个实例。
dapr run --app-id checkout --app-protocol http --dapr-http-port 3500 -- python3 checkout/app.py
dapr run --app-id order-processor --app-port 8001 --app-protocol http --dapr-http-port 3501 -- python3 order-processor/app.py
如果你的应用使用 TLS,你可以通过设置 --app-protocol https 来告诉 Dapr 通过 TLS 连接调用你的应用:
dapr run --app-id checkout --app-protocol https --dapr-http-port 3500 -- python3 checkout/app.py
dapr run --app-id order-processor --app-port 8001 --app-protocol https --dapr-http-port 3501 -- python3 order-processor/app.py
dapr run --app-id checkout --app-protocol http --dapr-http-port 3500 -- npm start
dapr run --app-id order-processor --app-port 5001 --app-protocol http --dapr-http-port 3501 -- npm start
如果你的应用使用 TLS,你可以通过设置 --app-protocol https 来告诉 Dapr 通过 TLS 连接调用你的应用:
dapr run --app-id checkout --dapr-http-port 3500 --app-protocol https -- npm start
dapr run --app-id order-processor --app-port 5001 --dapr-http-port 3501 --app-protocol https -- npm start
dapr run --app-id checkout --app-protocol http --dapr-http-port 3500 -- dotnet run
dapr run --app-id order-processor --app-port 7001 --app-protocol http --dapr-http-port 3501 -- dotnet run
如果你的应用使用 TLS,你可以通过设置 --app-protocol https 来告诉 Dapr 通过 TLS 连接调用你的应用:
dapr run --app-id checkout --dapr-http-port 3500 --app-protocol https -- dotnet run
dapr run --app-id order-processor --app-port 7001 --dapr-http-port 3501 --app-protocol https -- dotnet run
dapr run --app-id checkout --app-protocol http --dapr-http-port 3500 -- java -jar target/CheckoutService-0.0.1-SNAPSHOT.jar
dapr run --app-id order-processor --app-port 9001 --app-protocol http --dapr-http-port 3501 -- java -jar target/OrderProcessingService-0.0.1-SNAPSHOT.jar
如果你的应用使用 TLS,你可以通过设置 --app-protocol https 来告诉 Dapr 通过 TLS 连接调用你的应用:
dapr run --app-id checkout --dapr-http-port 3500 --app-protocol https -- java -jar target/CheckoutService-0.0.1-SNAPSHOT.jar
dapr run --app-id order-processor --app-port 9001 --dapr-http-port 3501 --app-protocol https -- java -jar target/OrderProcessingService-0.0.1-SNAPSHOT.jar
dapr run --app-id checkout --dapr-http-port 3500 -- go run .
dapr run --app-id order-processor --app-port 6006 --app-protocol http --dapr-http-port 3501 -- go run .
如果你的应用使用 TLS,你可以通过设置 --app-protocol https 来告诉 Dapr 通过 TLS 连接调用你的应用:
dapr run --app-id checkout --dapr-http-port 3500 --app-protocol https -- go run .
dapr run --app-id order-processor --app-port 6006 --dapr-http-port 3501 --app-protocol https -- go run .
部署到 Kubernetes 时设置 app-id
在 Kubernetes 中,在你的 pod 上设置 dapr.io/app-id 注解:
apiVersion: apps/v1
kind: Deployment
metadata:
name: <language>-app
namespace: default
labels:
app: <language>-app
spec:
replicas: 1
selector:
matchLabels:
app: <language>-app
template:
metadata:
labels:
app: <language>-app
annotations:
dapr.io/enabled: "true"
dapr.io/app-id: "order-processor"
dapr.io/app-port: "6001"
...
如果你的应用使用 TLS 连接,你可以使用 app-protocol: "https" 注解(完整列表在这里)来告诉 Dapr 通过 TLS 调用你的应用。请注意,Dapr 不会验证应用提供的 TLS 证书。
调用服务
要使用 Dapr 调用应用程序,你可以在任何 Dapr 实例上使用 invoke API。边车编程模型鼓励每个应用程序与自己的 Dapr 实例交互。Dapr 边车之间相互发现和通信。
以下是利用 Dapr SDK 进行服务调用的代码示例。
#dependencies
import random
from time import sleep
import logging
import requests
#code
logging.basicConfig(level = logging.INFO)
while True:
sleep(random.randrange(50, 5000) / 1000)
orderId = random.randint(1, 1000)
#Invoke a service
result = requests.post(
url='%s/orders' % (base_url),
data=json.dumps(order),
headers=headers
)
logging.basicConfig(level = logging.INFO)
logging.info('Order requested: ' + str(orderId))
logging.info('Result: ' + str(result))
//dependencies
import axios from "axios";
//code
const daprHost = "127.0.0.1";
var main = function() {
for(var i=0;i<10;i++) {
sleep(5000);
var orderId = Math.floor(Math.random() * (1000 - 1) + 1);
start(orderId).catch((e) => {
console.error(e);
process.exit(1);
});
}
}
//Invoke a service
const result = await axios.post('order-processor' , "orders/" + orderId , axiosConfig);
console.log("Order requested: " + orderId);
console.log("Result: " + result.config.data);
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
main();
//dependencies
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using System.Threading;
//code
namespace EventService
{
class Program
{
static async Task Main(string[] args)
{
while(true) {
await Task.Delay(5000)
var random = new Random();
var orderId = random.Next(1,1000);
//Using Dapr SDK to invoke a method
var order = new Order(orderId.ToString());
var httpClient = DaprClient.CreateInvokeHttpClient();
var response = await httpClient.PostAsJsonAsync("http://order-processor/orders", order);
var result = await response.Content.ReadAsStringAsync();
Console.WriteLine("Order requested: " + orderId);
Console.WriteLine("Result: " + result);
}
}
}
}
//dependencies
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
//code
@SpringBootApplication
public class CheckoutServiceApplication {
private static final HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(10))
.build();
public static void main(String[] args) throws InterruptedException, IOException {
while (true) {
TimeUnit.MILLISECONDS.sleep(5000);
Random random = new Random();
int orderId = random.nextInt(1000 - 1) + 1;
// Create a Map to represent the request body
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("orderId", orderId);
// Add other fields to the requestBody Map as needed
HttpRequest request = HttpRequest.newBuilder()
.POST(HttpRequest.BodyPublishers.ofString(new JSONObject(requestBody).toString()))
.uri(URI.create(dapr_url))
.header("Content-Type", "application/json")
.header("dapr-app-id", "order-processor")
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Order passed: " + orderId);
TimeUnit.MILLISECONDS.sleep(1000);
log.info("Order requested: " + orderId);
log.info("Result: " + response.body());
}
}
}
package main
import (
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os"
"time"
)
func main() {
daprHttpPort := os.Getenv("DAPR_HTTP_PORT")
if daprHttpPort == "" {
daprHttpPort = "3500"
}
client := &http.Client{
Timeout: 15 * time.Second,
}
for i := 0; i < 10; i++ {
time.Sleep(5000)
orderId := rand.Intn(1000-1) + 1
url := fmt.Sprintf("http://localhost:%s/checkout/%v", daprHttpPort, orderId)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
panic(err)
}
// Adding target app id as part of the header
req.Header.Add("dapr-app-id", "order-processor")
// Invoking a service
resp, err := client.Do(req)
if err != nil {
log.Fatal(err.Error())
}
b, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Println(string(b))
}
}
其他 URL 格式
要调用 ‘GET’ 端点:
curl http://localhost:3602/v1.0/invoke/checkout/method/checkout/100
为了尽可能避免更改 URL 路径,Dapr 提供了以下方式调用服务调用 API:
- 将 URL 中的地址改为
localhost:<dapr-http-port>。 - 添加一个
dapr-app-id头来指定目标服务的 ID,或者通过 HTTP Basic Auth 传递 ID:http://dapr-app-id:<service-id>@localhost:3602/path。
例如,以下命令:
curl http://localhost:3602/v1.0/invoke/checkout/method/checkout/100
等价于:
curl -H 'dapr-app-id: checkout' 'http://localhost:3602/checkout/100' -X POST
或者:
curl 'http://dapr-app-id:checkout@localhost:3602/checkout/100' -X POST
使用 CLI:
dapr invoke --app-id checkout --method checkout/100
在 URL 中包含查询字符串
你也可以在 URL 末尾附加查询字符串或片段,Dapr 会原样传递。这意味着如果你需要在服务调用中传递一些不属于负载或路径的额外参数,可以通过在 URL 末尾附加 ? 来实现,后跟用 = 分隔的键值对,用 & 分隔。例如:
curl 'http://dapr-app-id:checkout@localhost:3602/checkout/100?basket=1234&key=abc' -X POST
命名空间
在支持命名空间的平台上运行时,你需要在 app ID 中包含目标应用的命名空间。例如,遵循 <app>.<namespace> 格式,使用 checkout.production。
使用此示例,带命名空间调用服务如下:
curl http://localhost:3602/v1.0/invoke/checkout.production/method/checkout/100 -X POST
有关命名空间的更多信息,请参见跨命名空间 API 规范。
查看追踪和日志
上面的示例向你展示了如何直接调用在本地或 Kubernetes 中运行的不同服务。Dapr:
- 输出指标、追踪和日志信息,
- 允许你可视化服务之间的调用图并记录错误,以及
- 可选地,记录负载正文。
有关追踪和日志的更多信息,请参见可观测性文章。
相关链接
1.1.3 - 如何:使用 gRPC 调用服务
本文介绍如何使用 Dapr 通过 gRPC 连接服务。
通过使用 Dapr 的 gRPC 代理能力,你可以使用现有的基于 proto 的 gRPC 服务,并让流量通过 Dapr 边车。这样做可以为开发者带来以下 Dapr 服务调用 优势:
- 双向认证
- 追踪
- 指标
- 访问列表
- 网络级别的弹性
- 基于 API token 的认证
Dapr 允许代理各种类型的 gRPC 调用,包括一元调用和基于流的调用。
步骤 1:运行 gRPC 服务器
以下示例取自 “hello world” grpc-go 示例。虽然此示例使用 Go,但相同的概念适用于 gRPC 支持的所有编程语言。
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "google.golang.org/grpc/examples/helloworld/helloworld"
)
const (
port = ":50051"
)
// server 用于实现 helloworld.GreeterServer。
type server struct {
pb.UnimplementedGreeterServer
}
// SayHello 实现 helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
此 Go 应用实现了 Greeter proto 服务,并公开了一个 SayHello 方法。
使用 Dapr CLI 运行 gRPC 服务器
dapr run --app-id server --app-port 50051 -- go run main.go
使用 Dapr CLI,我们通过 --app-id 标志为应用分配一个唯一 ID server。
步骤 2:调用服务
以下示例展示如何从 gRPC 客户端使用 Dapr 发现 Greeter 服务。
请注意,客户端不是直接在端口 50051 上调用目标服务,而是通过端口 50007 调用其本地 Dapr 边车,从而获得服务调用的所有能力,包括服务发现、追踪、mTLS 和重试。
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
pb "google.golang.org/grpc/examples/helloworld/helloworld"
"google.golang.org/grpc/metadata"
)
const (
address = "localhost:50007"
)
func main() {
// 建立与服务器的连接。
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
defer cancel()
ctx = metadata.AppendToOutgoingContext(ctx, "dapr-app-id", "server")
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "Darth Tyrannus"})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetMessage())
}
以下代码行告诉 Dapr 发现并调用名为 server 的应用:
ctx = metadata.AppendToOutgoingContext(ctx, "dapr-app-id", "server")
所有支持 gRPC 的语言都允许添加元数据。以下是几个示例:
Metadata headers = new Metadata();
Metadata.Key<String> jwtKey = Metadata.Key.of("dapr-app-id", "server");
GreeterService.ServiceBlockingStub stub = GreeterService.newBlockingStub(channel);
stub = MetadataUtils.attachHeaders(stub, header);
stub.SayHello(new HelloRequest() { Name = "Darth Malak" });
var metadata = new Metadata
{
{ "dapr-app-id", "server" }
};
var call = client.SayHello(new HelloRequest { Name = "Darth Nihilus" }, metadata);
metadata = (('dapr-app-id', 'server'),)
response = stub.SayHello(request={ name: 'Darth Revan' }, metadata=metadata)
const metadata = new grpc.Metadata();
metadata.add('dapr-app-id', 'server');
client.sayHello({ name: "Darth Malgus" }, metadata)
metadata = { 'dapr-app-id' : 'server' }
response = service.sayHello({ 'name': 'Darth Bane' }, metadata)
grpc::ClientContext context;
context.AddMetadata("dapr-app-id", "server");
使用 Dapr CLI 运行客户端
dapr run --app-id client --dapr-grpc-port 50007 -- go run main.go
查看遥测数据
如果你在本地运行 Dapr 并安装了 Zipkin,请在浏览器中打开 http://localhost:9411 查看客户端和服务器之间的追踪信息。
部署到 Kubernetes
在你的部署上设置以下 Dapr 注解:
apiVersion: apps/v1
kind: Deployment
metadata:
name: grpc-app
namespace: default
labels:
app: grpc-app
spec:
replicas: 1
selector:
matchLabels:
app: grpc-app
template:
metadata:
labels:
app: grpc-app
annotations:
dapr.io/enabled: "true"
dapr.io/app-id: "server"
dapr.io/app-protocol: "grpc"
dapr.io/app-port: "50051"
...
dapr.io/app-protocol: "grpc" 注解告诉 Dapr 使用 gRPC 调用应用。
如果你的应用使用 TLS 连接,你可以通过 app-protocol: "grpcs" 注解告诉 Dapr 通过 TLS 调用你的应用(完整列表见这里)。请注意,Dapr 不验证应用提供的 TLS 证书。
命名空间
在支持命名空间的平台上运行时,你需要在应用 ID 中包含目标应用的命名空间:myApp.production
例如,调用不同命名空间上的 gRPC 服务器:
ctx = metadata.AppendToOutgoingContext(ctx, "dapr-app-id", "server.production")
有关命名空间的更多信息,请参阅跨命名空间 API 规范。
步骤 3:查看追踪和日志
上面的示例向你展示如何直接调用本地或 Kubernetes 上运行的不同服务。Dapr 输出指标、追踪和日志信息,使你能够可视化服务之间的调用图、记录错误,并可选择记录有效负载正文。
有关追踪和日志的更多信息,请参阅可观测性文章。
代理流式 RPC
使用 Dapr 通过 gRPC 代理流式 RPC 调用时,必须设置一个额外的元数据选项 dapr-stream,其值为 true。
例如:
ctx = metadata.AppendToOutgoingContext(ctx, "dapr-app-id", "server")
ctx = metadata.AppendToOutgoingContext(ctx, "dapr-stream", "true")
Metadata headers = new Metadata();
Metadata.Key<String> jwtKey = Metadata.Key.of("dapr-app-id", "server");
Metadata.Key<String> jwtKey = Metadata.Key.of("dapr-stream", "true");
var metadata = new Metadata
{
{ "dapr-app-id", "server" },
{ "dapr-stream", "true" }
};
metadata = (('dapr-app-id', 'server'), ('dapr-stream', 'true'),)
const metadata = new grpc.Metadata();
metadata.add('dapr-app-id', 'server');
metadata.add('dapr-stream', 'true');
metadata = { 'dapr-app-id' : 'server' }
metadata = { 'dapr-stream' : 'true' }
grpc::ClientContext context;
context.AddMetadata("dapr-app-id", "server");
context.AddMetadata("dapr-stream", "true");
流式 gRPC 与弹性
目前,通过 gRPC 进行服务调用时不支持弹性策略。
代理流式 gRPC 时,由于其长期存在的特性,弹性策略仅应用于"初始握手"。因此:
- 如果流在初始握手后被中断,Dapr 不会自动重新建立它。你的应用将收到流已结束的通知,并需要重新创建它。
- 重试策略仅影响初始连接"握手"。如果你的弹性策略包含重试,Dapr 将检测建立与目标应用的初始连接时的失败,并将重试直到成功(或直到策略中定义的重试次数用尽)。
- 同样,弹性策略中定义的超时仅适用于初始"握手"。连接建立后,超时不再影响流。
相关链接
社区呼叫演示
观看此视频,了解如何使用 Dapr 的 gRPC 代理能力:
1.1.4 - 操作指南:使用 HTTP 调用非 Dapr 端点
本文演示如何使用 HTTP 通过 Dapr 调用非 Dapr 端点。
使用 Dapr 的服务调用 API,您可以与使用或不使用 Dapr 的端点进行通信。使用 Dapr 调用不使用 Dapr 的端点不仅能提供一致的 API,还能带来以下 Dapr 服务调用 优势:
- 能够应用弹性策略
- 通过追踪与指标实现可观测性调用
- 通过作用域实现安全访问控制
- 能够利用中间件管道组件
- 服务发现
- 通过使用标头进行身份验证
调用外部服务或非 Dapr 端点的 HTTP 服务调用
有时您需要调用非 Dapr HTTP 端点。例如:
- 您可能选择仅在整个应用程序的部分使用 Dapr,包括遗留开发
- 您可能无法访问代码来将现有应用程序迁移到使用 Dapr
- 您需要调用外部 HTTP 服务。
通过定义 HTTPEndpoint 资源,您可以声明式地定义与非 Dapr 端点交互的方式。然后使用服务调用 URL 来调用非 Dapr 端点。或者,您也可以直接将非 Dapr 的完全限定域名(FQDN)端点 URL 放入服务调用 URL 中。
HTTPEndpoint、FQDN URL 与 appId 之间的优先级顺序
使用服务调用时,Dapr 运行时遵循以下优先级顺序:
- 这是命名的
HTTPEndpoint资源吗? - 这是带有
http://或https://前缀的 FQDN URL 吗? - 这是
appID吗?
服务调用与非 Dapr HTTP 端点
下图概述了 Dapr 的服务调用在调用非 Dapr 端点时的工作原理。

- 服务 A 向服务 B(一个非 Dapr 端点)发起 HTTP 调用。该调用发送到本地 Dapr 边车。
- Dapr 使用
HTTPEndpoint或 FQDN URL 发现服务 B 的位置,然后将消息转发给服务 B。 - 服务 B 向服务 A 的 Dapr 边车发送响应。
- 服务 A 接收响应。
为非 Dapr 端点使用 HTTPEndpoint 资源或 FQDN URL
在与 Dapr 应用程序或非 Dapr 应用程序通信时,有两种方法可以调用非 Dapr 端点。Dapr 应用程序可以通过提供以下内容之一来调用非 Dapr 端点:
命名的
HTTPEndpoint资源,包括定义HTTPEndpoint资源类型。有关示例,请参阅 HTTPEndpoint 参考 指南。localhost:3500/v1.0/invoke/<HTTPEndpoint-name>/method/<my-method>例如,对于名为 “palpatine” 的
HTTPEndpoint资源和名为 “Order66” 的方法,调用方式如下:curl http://localhost:3500/v1.0/invoke/palpatine/method/order66指向非 Dapr 端点的 FQDN URL。
localhost:3500/v1.0/invoke/<URL>/method/<my-method>例如,对于名为
https://darthsidious.starwars的 FQDN 资源,调用方式如下:curl http://localhost:3500/v1.0/invoke/https://darthsidious.starwars/method/order66
调用启用 Dapr 的应用程序时使用 appId
调用使用 appID 的 Dapr 应用程序时,始终使用 AppID。有关更多信息,请阅读 操作指南:使用 HTTP 调用服务 指南。例如:
localhost:3500/v1.0/invoke/<appID>/method/<my-method>
curl http://localhost:3602/v1.0/invoke/orderprocessor/method/checkout
TLS 身份验证
使用 HTTPEndpoint 资源 允许您根据远程端点的身份验证要求,使用根证书、客户端证书和私钥的任意组合。
使用根证书的示例
apiVersion: dapr.io/v1alpha1
kind: HTTPEndpoint
metadata:
name: "external-http-endpoint-tls"
spec:
baseUrl: https://service-invocation-external:443
headers:
- name: "Accept-Language"
value: "en-US"
clientTLS:
rootCA:
secretKeyRef:
name: dapr-tls-client
key: ca.crt
使用客户端证书和私钥的示例
apiVersion: dapr.io/v1alpha1
kind: HTTPEndpoint
metadata:
name: "external-http-endpoint-tls"
spec:
baseUrl: https://service-invocation-external:443
headers:
- name: "Accept-Language"
value: "en-US"
clientTLS:
certificate:
secretKeyRef:
name: dapr-tls-client
key: tls.crt
privateKey:
secretKeyRef:
name: dapr-tls-key
key: tls.key
服务器发送事件
SSE 支持与流服务器和 MCP 服务器进行实时通信。
HTTP 端点支持 服务器发送事件(SSE)。
要使用 SSE,请在 HTTPEndpoint 资源或服务调用请求中将 Accept 标头设置为 text/event-stream。
apiVersion: dapr.io/v1alpha1
kind: HTTPEndpoint
metadata:
name: "mcp-server"
spec:
baseUrl: https://my-mcp-server:443
headers:
- name: "Accept"
value: "test/event-stream"
相关链接
社区电话演示
观看此 视频,了解如何使用服务调用调用非 Dapr 端点。
1.1.5 - 操作指南:跨命名空间的服务调用
在本文中,你将学习如何调用部署在不同命名空间中的服务。默认情况下,服务调用支持通过简单引用应用 ID(nodeapp)来调用同一命名空间内的服务:
localhost:3500/v1.0/invoke/nodeapp/method/neworder
服务调用也支持跨命名空间调用。在所有支持的托管平台上,Dapr 应用 ID 符合包含目标命名空间的有效 FQDN 格式。你可以同时指定:
- 应用 ID(
nodeapp),以及 - 应用运行的命名空间(
production)。
示例 1
调用 production 命名空间中 nodeapp 上的 neworder 方法:
localhost:3500/v1.0/invoke/nodeapp.production/method/neworder
使用服务调用调用命名空间中的应用程序时,需要用命名空间进行限定。这在 Kubernetes 集群中的跨命名空间调用中非常有用。
示例 2
调用 production 命名空间中 myapp 上的 ping 方法:
https://localhost:3500/v1.0/invoke/myapp.production/method/ping
示例 3
使用 curl 命令从外部 DNS 地址(此处为 api.demo.dapr.team)调用与示例 2 相同的 ping 方法,并提供 Dapr API 令牌进行身份验证:
MacOS/Linux:
curl -i -d '{ "message": "hello" }' \
-H "Content-type: application/json" \
-H "dapr-api-token: ${API_TOKEN}" \
https://api.demo.dapr.team/v1.0/invoke/myapp.production/method/ping
1.2 - 发布与订阅消息
更多关于 Dapr Pub/sub
了解如何使用 Dapr Pub/sub:
- 尝试 Pub/sub 快速入门。
- 通过任一支持的 Dapr SDK 探索 pub/sub。
- 查看 Pub/sub API 参考文档。
- 浏览支持的 pub/sub 组件规范。
1.2.1 - 发布订阅概述
发布订阅(pub/sub)使微服务能够使用消息进行通信,实现事件驱动架构。
- 生产者(发布者)将消息写入输入通道并发送到主题,但不知道哪个应用程序会接收它们。
- 消费者(订阅者)订阅主题并从输出通道接收消息,但不知道这些消息是由哪个服务产生的。
中间消息代理将每条消息从发布者的输入通道复制到所有对该消息感兴趣的订阅者的输出通道。当你需要将微服务相互解耦时,这种模式特别有用。

发布订阅 API
Dapr 中的发布订阅 API:
- 提供与平台无关的 API 来发送和接收消息。
- 提供至少一次消息传递保证。
- 集成各种消息代理和排队系统。
服务使用的特定消息代理是可插拔的,在运行时配置为 Dapr 发布订阅组件。这消除了服务对消息代理的依赖,使服务更具可移植性和灵活性。
使用 Dapr 中的发布订阅时:
- 你的服务向 Dapr 发布订阅构建块 API 发起网络调用。
- 发布订阅构建块调用封装了特定消息代理的 Dapr 发布订阅组件。
- 为了接收主题上的消息,Dapr 代表你的服务订阅发布订阅组件上的主题,并在消息到达时将消息传递到你的服务端点。
以下概述视频和演示 演示了 Dapr 发布订阅的工作原理。
在下图中,“shipping"服务和 “email” 服务都订阅了 “cart” 服务发布的主题。每个服务加载指向同一发布订阅消息代理组件的发布订阅组件配置文件;例如:Redis Streams、NATS Streaming、Azure Service Bus 或 GCP pub/sub。

在下图中,Dapr API 将来自发布 “cart” 服务的 “order” 主题发布到订阅服务 “shipping” 和 “email” 的 “order” 端点。

功能
发布订阅 API 构建块为你的应用程序带来多项功能。
使用 CloudEvents 发送消息
为了实现消息路由并在服务之间提供每个消息的额外上下文,Dapr 使用 CloudEvents 1.0 规范 作为其消息格式。任何使用 Dapr 发送到主题的应用程序消息都会自动包装在 Cloud Events 信封中,使用 Content-Type 头值 作为 datacontenttype 属性。
更多信息,请阅读 使用 CloudEvents 进行消息传递 或 发送不带 CloudEvents 的原始消息。
与未使用 Dapr 和 CloudEvents 的应用程序通信
如果你的一个应用程序使用 Dapr 而另一个不使用,你可以为发布者或订阅者禁用 CloudEvent 包装。这允许在无法同时采用 Dapr 的应用程序中部分采用 Dapr 发布订阅。
更多信息,请阅读 如何在不使用 CloudEvents 的情况下使用发布订阅。
设置消息内容类型
发布消息时,指定正在发送的数据的内容类型非常重要。除非指定,否则 Dapr 将假定为 text/plain。
- HTTP 客户端:可以在
Content-Type头中设置内容类型 - gRPC 客户端和 SDK:有专用的内容类型参数
消息传递
原则上,Dapr 认为订阅者处理消息并返回非错误响应后,消息即成功传递。为了更精细的控制,Dapr 的发布订阅 API 还提供明确的状态(在响应负载中定义),订阅者使用这些状态向 Dapr 指示特定的处理指令(例如,RETRY 或 DROP)。
使用主题订阅接收消息
Dapr 应用程序可以通过支持相同功能的三种订阅类型订阅已发布的主题:声明式、流式和编程式。
| 订阅类型 | 描述 |
|---|---|
| 声明式 | 订阅在外部文件中定义。声明式方法从代码中移除 Dapr 依赖,允许现有应用程序订阅主题,而无需更改代码。 |
| 流式 | 订阅在用户代码中定义。流式订阅是动态的,意味着它们允许在运行时添加或移除订阅。它们不需要应用程序中的订阅端点(这是编程式和声明式订阅都需要的),使得在代码中配置变得简单。流式订阅也不需要应用程序配置边车来接收消息。使用流式订阅,由于消息被发送到消息处理程序代码,因此没有路由或批量订阅的概念。 |
| 编程式 | 订阅在用户代码中定义。编程式方法实现静态订阅,需要代码中的端点。 |
更多信息,请阅读 关于订阅类型的订阅。
重新加载主题订阅
要重新加载以编程方式或声明方式定义的主题订阅,需要重启 Dapr 边车。
通过启用 HotReload 特性门控 可以使 Dapr 边车动态重新加载已更改的声明式主题订阅,而无需重启。
主题订阅的热重载目前是预览功能。
重新加载订阅时,正在传输的消息不受影响。
消息路由
Dapr 提供基于内容的路由模式。发布订阅路由 是此模式的实现,允许开发者使用表达式根据 CloudEvents 的内容将消息路由到应用程序中的不同 URI/路径和事件处理程序。如果没有路由匹配,则使用可选的默认路由。随着你的应用程序扩展以支持多个事件版本或特殊情况,这非常有用。
此功能适用于声明式和编程式订阅方法。
有关消息路由的更多信息,请阅读 Dapr 发布订阅 API 参考。
使用死信主题处理失败消息
有时,消息由于多种可能的问题无法处理,例如生产者或消费者应用程序内部的错误条件或导致应用程序代码出现问题的意外状态更改。Dapr 允许开发者设置死信主题来处理无法传递到应用程序的消息。此功能在所有发布订阅组件上可用,可防止消费者应用程序无休止地重试失败的消息。更多信息,请阅读 关于死信主题.
启用发件箱模式
Dapr 使开发者能够使用发件箱模式在事务性状态存储和任何消息代理之间实现单一事务。更多信息,请阅读 如何启用事务性发件箱消息传递。
命名空间消费者组
Dapr 通过消费者组命名空间 在大规模场景下解决多租户问题。只需在组件元数据中包含 "{namespace}" 值,即可让具有相同 app-id 的多个命名空间的应用程序发布和订阅到同一消息代理。
至少一次保证
Dapr 保证消息传递的至少一次语义。当应用程序使用发布订阅 API 向主题发布消息时,Dapr 确保消息至少一次传递到每个订阅者。
即使消息传递失败或应用程序崩溃,Dapr 也会重试重新传递消息直到成功传递。
所有 Dapr 发布订阅组件都支持至少一次保证。
订阅启动可靠性
Dapr 自动重试失败的订阅启动,以提高部署场景下的可靠性。这确保了你的发布订阅应用程序即使在面临临时连接或权限问题时也能保持弹性。
当 Dapr 遇到启动订阅的错误时,它会在日志中显示错误消息并继续尝试启动订阅。
消费者组和竞争消费者模式
Dapr 处理消费者组和竞争消费者模式的负担。在竞争消费者模式中,使用单个消费者组的多个应用程序实例竞争消息。当副本使用相同的 app-id 而没有明确的消费者组覆盖时,Dapr 强制执行竞争消费者模式。
当同一应用程序的多个实例(具有相同的 app-id)订阅主题时,Dapr 将每条消息传递到该应用程序的只有一个实例。此概念如下图所示。

同样,如果两个不同的应用程序(具有不同的 app-id)订阅同一主题,Dapr 将每条消息传递到每个应用程序的只有一个实例。
并非所有 Dapr 发布订阅组件都支持竞争消费者模式。目前,以下(非详尽)发布订阅组件支持此功能:
限制主题以增强安全性
默认情况下,与发布订阅组件实例关联的所有主题消息都可用于使用该组件配置的每个应用程序。你可以使用 Dapr 主题限制来限制哪些应用程序可以发布或订阅主题。更多信息,请阅读:发布订阅主题限制。
消息生存时间(TTL)
Dapr 可以按消息设置超时消息,这意味着如果消息未从发布订阅组件中读取,则消息将被丢弃。此超时消息可防止未读消息堆积。如果消息在队列中停留的时间超过配置的 TTL,它将被标记为死信。更多信息,请阅读 发布订阅消息 TTL。
发布和订阅批量消息
Dapr 支持在单个请求中发送和接收多条消息。编写需要发送或接收大量消息的应用程序时,使用批量操作可以减少请求总数来实现高吞吐量。更多信息,请阅读 发布订阅批量消息。
使用 StatefulSets 扩展订阅者
在 Kubernetes 上运行时,订阅者结合 StatefulSets 和 {podName} 标记可以拥有每个实例的粘性 consumerID。请参阅 如何使用 StatefulSets 水平扩展订阅者。
尝试发布订阅
快速入门和教程
想测试 Dapr 发布订阅 API?请完成以下快速入门和教程来了解发布订阅的实际应用:
| 快速入门/教程 | 描述 |
|---|---|
| 发布订阅快速入门 | 使用发布订阅 API 发送和接收消息。 |
| 发布订阅教程 | 演示如何使用 Dapr 启用发布订阅应用程序。使用 Redis 作为发布订阅组件。 |
直接在应用程序中使用发布订阅
想跳过快速入门?没问题。你可以直接在应用程序中试用发布订阅构建块来发布消息和订阅主题。安装 Dapr 后,你可以从 发布订阅操作指南 开始使用发布订阅 API。
下一步
- 了解 使用 CloudEvents 进行消息传递 以及何时可能需要 发送不带 CloudEvents 的消息。
- 遵循 操作指南:使用多个命名空间配置发布订阅组件。
- 查看 发布订阅组件 列表。
- 阅读 API 参考。
1.2.2 - 操作指南:发布消息并订阅主题
既然您已经了解了 Dapr 发布订阅构建块提供的能力,接下来了解如何在您的服务中使用它。下面的代码示例简单描述了一个包含两个服务的订单处理应用程序,每个服务都配置了 Dapr 边车:
- 一个结账服务,使用 Dapr 订阅消息队列中的主题。
- 一个订单处理服务,使用 Dapr 向 RabbitMQ 发布消息。

Dapr 会自动使用 Content-Type 头部的值作为 datacontenttype 属性,将用户负载封装在一个符合 CloudEvents v1.0 标准的信封中。了解更多关于 CloudEvents 消息的信息。
下面的示例演示了您的应用程序如何发布和订阅名为 orders 的主题。
注意
如果您还没有尝试过,请先体验发布订阅快速入门,快速了解如何使用发布订阅。设置发布/订阅组件
第一步是设置发布/订阅组件:
当您运行 dapr init 时,Dapr 会创建一个默认的 Redis pubsub.yaml 并在您的本地机器上运行一个 Redis 容器,位于:
- 在 Windows 上,位于
%UserProfile%\.dapr\components\pubsub.yaml - 在 Linux/MacOS 上,位于
~/.dapr/components/pubsub.yaml
通过 pubsub.yaml 组件,您可以轻松更换底层组件而无需更改应用程序代码。在本示例中,使用的是 RabbitMQ。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: order-pub-sub
spec:
type: pubsub.rabbitmq
version: v1
metadata:
- name: host
value: "amqp://localhost:5672"
- name: durable
value: "false"
- name: deletedWhenUnused
value: "false"
- name: autoAck
value: "false"
- name: reconnectWait
value: "0"
- name: concurrency
value: parallel
scopes:
- orderprocessing
- checkout
您可以通过创建一个包含该文件的组件目录(在本例中为 myComponents)并在 dapr run CLI 命令中使用 --resources-path 标志,用另一个发布订阅组件覆盖此文件。
要将其部署到 Kubernetes 集群中,请填写下面 YAML 中发布/订阅组件的 metadata 连接详情,将其保存为 pubsub.yaml,然后运行 kubectl apply -f pubsub.yaml。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: order-pub-sub
spec:
type: pubsub.rabbitmq
version: v1
metadata:
- name: connectionString
value: "amqp://localhost:5672"
- name: protocol
value: amqp
- name: hostname
value: localhost
- name: username
value: username
- name: password
value: password
- name: durable
value: "false"
- name: deletedWhenUnused
value: "false"
- name: autoAck
value: "false"
- name: reconnectWait
value: "0"
- name: concurrency
value: parallel
scopes:
- orderprocessing
- checkout
dapr run --app-id myapp --resources-path ./myComponents -- dotnet run
dapr run --app-id myapp --resources-path ./myComponents -- mvn spring-boot:run
dapr run --app-id myapp --resources-path ./myComponents -- python3 app.py
dapr run --app-id myapp --resources-path ./myComponents -- go run app.go
dapr run --app-id myapp --resources-path ./myComponents -- npm start
订阅主题
Dapr 提供了三种订阅主题的方法:
- 声明式,订阅在外部文件中定义。
- 流式,订阅在用户代码中定义。
- 编程式,订阅在用户代码中定义。
在声明式、流式和编程式订阅文档中了解更多信息。本示例演示声明式订阅。
创建一个名为 subscription.yaml 的文件并粘贴以下内容:
apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
name: order-pub-sub
spec:
topic: orders
routes:
default: /checkout
pubsubname: order-pub-sub
scopes:
- orderprocessing
- checkout
上面的示例展示了对 orders 主题的事件订阅,使用的发布订阅组件是 order-pub-sub。
route字段告诉 Dapr 将所有主题消息发送到应用程序中的/checkout端点。scopes字段为 ID 为orderprocessing和checkout的应用程序启用此订阅。
将 subscription.yaml 放在与 pubsub.yaml 组件相同的目录中。当 Dapr 启动时,它会与组件一起加载订阅。
注意
此功能目前处于预览状态。 可以使 Dapr “热重载” 声明式订阅,从而自动应用更新而无需重启。 这是通过HotReload 功能门控启用的。
为了防止重新处理或丢失未处理的消息,在热重载事件期间,Dapr 与您的应用程序之间正在传输的消息不受影响。以下是利用 Dapr SDK 订阅您在 subscription.yaml 中定义的主题的代码示例。
using System.Collections.Generic;
using System.Threading.Tasks;
using System;
using Microsoft.AspNetCore.Mvc;
using Dapr;
using Dapr.Client;
namespace CheckoutService.Controllers;
[ApiController]
public sealed class CheckoutServiceController : ControllerBase
{
//从 "order-pub-sub" 组件订阅名为 "orders" 的主题
[Topic("order-pub-sub", "orders")]
[HttpPost("checkout")]
public void GetCheckout([FromBody] int orderId)
{
Console.WriteLine("Subscriber received : " + orderId);
}
}
导航到包含上述代码的目录,然后运行以下命令来启动 Dapr 边车和订阅者应用程序:
dapr run --app-id checkout --app-port 6002 --dapr-http-port 3602 --dapr-grpc-port 60002 --app-protocol https dotnet run
//dependencies
import io.dapr.Topic;
import io.dapr.client.domain.CloudEvent;
import org.springframework.web.bind.annotation.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
//code
@RestController
public class CheckoutServiceController {
private static final Logger log = LoggerFactory.getLogger(CheckoutServiceController.class);
//订阅一个主题
@Topic(name = "orders", pubsubName = "order-pub-sub")
@PostMapping(path = "/checkout")
public Mono<Void> getCheckout(@RequestBody(required = false) CloudEvent<String> cloudEvent) {
return Mono.fromRunnable(() -> {
try {
log.info("Subscriber received: " + cloudEvent.getData());
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
}
导航到包含上述代码的目录,然后运行以下命令来启动 Dapr 边车和订阅者应用程序:
dapr run --app-id checkout --app-port 6002 --dapr-http-port 3602 --dapr-grpc-port 60002 mvn spring-boot:run
#dependencies
from cloudevents.sdk.event import v1
from dapr.ext.grpc import App
import logging
import json
#code
app = App()
logging.basicConfig(level = logging.INFO)
#订阅一个主题
@app.subscribe(pubsub_name='order-pub-sub', topic='orders')
def mytopic(event: v1.Event) -> None:
data = json.loads(event.Data())
logging.info('Subscriber received: ' + str(data))
app.run(6002)
导航到包含上述代码的目录,然后运行以下命令来启动 Dapr 边车和订阅者应用程序:
dapr run --app-id checkout --app-port 6002 --dapr-http-port 3602 --app-protocol grpc -- python3 CheckoutService.py
//dependencies
import (
"log"
"net/http"
"context"
"github.com/dapr/go-sdk/service/common"
daprd "github.com/dapr/go-sdk/service/http"
)
//code
var sub = &common.Subscription{
PubsubName: "order-pub-sub",
Topic: "orders",
Route: "/checkout",
}
func main() {
s := daprd.NewService(":6002")
//订阅一个主题
if err := s.AddTopicEventHandler(sub, eventHandler); err != nil {
log.Fatalf("error adding topic subscription: %v", err)
}
if err := s.Start(); err != nil && err != http.ErrServerClosed {
log.Fatalf("error listenning: %v", err)
}
}
func eventHandler(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
log.Printf("Subscriber received: %s", e.Data)
return false, nil
}
导航到包含上述代码的目录,然后运行以下命令来启动 Dapr 边车和订阅者应用程序:
dapr run --app-id checkout --app-port 6002 --dapr-http-port 3602 --dapr-grpc-port 60002 go run CheckoutService.go
//dependencies
import { DaprServer, CommunicationProtocolEnum } from '@dapr/dapr';
//code
const daprHost = "127.0.0.1";
const serverHost = "127.0.0.1";
const serverPort = "6002";
start().catch((e) => {
console.error(e);
process.exit(1);
});
async function start(orderId) {
const server = new DaprServer({
serverHost,
serverPort,
communicationProtocol: CommunicationProtocolEnum.HTTP,
clientOptions: {
daprHost,
daprPort: process.env.DAPR_HTTP_PORT,
},
});
//订阅一个主题
await server.pubsub.subscribe("order-pub-sub", "orders", async (orderId) => {
console.log(`Subscriber received: ${JSON.stringify(orderId)}`)
});
await server.start();
}
导航到包含上述代码的目录,然后运行以下命令来启动 Dapr 边车和订阅者应用程序:
dapr run --app-id checkout --app-port 6002 --dapr-http-port 3602 --dapr-grpc-port 60002 npm start
发布消息
启动一个 app-id 为 orderprocessing 的 Dapr 实例:
dapr run --app-id orderprocessing --dapr-http-port 3601
然后向 orders 主题发布一条消息:
dapr publish --publish-app-id orderprocessing --pubsub order-pub-sub --topic orders --data '{"orderId": "100"}'
curl -X POST http://localhost:3601/v1.0/publish/order-pub-sub/orders -H "Content-Type: application/json" -d '{"orderId": "100"}'
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '{"orderId": "100"}' -Uri 'http://localhost:3601/v1.0/publish/order-pub-sub/orders'
以下是利用 Dapr SDK 发布主题的代码示例。
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Dapr.Client;
using System.Threading;
const string PUBSUB_NAME = "order-pub-sub";
const string TOPIC_NAME = "orders";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();
var random = new Random();
var client = app.Services.GetRequiredService<DaprClient>();
while(true) {
await Task.Delay(TimeSpan.FromSeconds(5));
var orderId = random.Next(1,1000);
var source = new CancellationTokenSource();
var cancellationToken = source.Token;
//使用 Dapr SDK 发布主题
await client.PublishEventAsync(PUBSUB_NAME, TOPIC_NAME, orderId, cancellationToken);
Console.WriteLine("Published data: " + orderId);
}
导航到包含上述代码的目录,然后运行以下命令来启动 Dapr 边车和发布者应用程序:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 --app-protocol https dotnet run
//dependencies
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.Metadata;
import static java.util.Collections.singletonMap;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Random;
import java.util.concurrent.TimeUnit;
//code
@SpringBootApplication
public class OrderProcessingServiceApplication {
private static final Logger log = LoggerFactory.getLogger(OrderProcessingServiceApplication.class);
public static void main(String[] args) throws InterruptedException{
String MESSAGE_TTL_IN_SECONDS = "1000";
String TOPIC_NAME = "orders";
String PUBSUB_NAME = "order-pub-sub";
while(true) {
TimeUnit.MILLISECONDS.sleep(5000);
Random random = new Random();
int orderId = random.nextInt(1000-1) + 1;
DaprClient client = new DaprClientBuilder().build();
//使用 Dapr SDK 发布主题
client.publishEvent(
PUBSUB_NAME,
TOPIC_NAME,
orderId,
singletonMap(Metadata.TTL_IN_SECONDS, MESSAGE_TTL_IN_SECONDS)).block();
log.info("Published data:" + orderId);
}
}
}
导航到包含上述代码的目录,然后运行以下命令来启动 Dapr 边车和发布者应用程序:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 mvn spring-boot:run
#dependencies
import random
from time import sleep
import requests
import logging
import json
from dapr.clients import DaprClient
#code
logging.basicConfig(level = logging.INFO)
while True:
sleep(random.randrange(50, 5000) / 1000)
orderId = random.randint(1, 1000)
PUBSUB_NAME = 'order-pub-sub'
TOPIC_NAME = 'orders'
with DaprClient() as client:
#使用 Dapr SDK 发布主题
result = client.publish_event(
pubsub_name=PUBSUB_NAME,
topic_name=TOPIC_NAME,
data=json.dumps(orderId),
data_content_type='application/json',
)
logging.info('Published data: ' + str(orderId))
导航到包含上述代码的目录,然后运行以下命令来启动 Dapr 边车和发布者应用程序:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --app-protocol grpc python3 OrderProcessingService.py
//dependencies
import (
"context"
"log"
"math/rand"
"time"
"strconv"
dapr "github.com/dapr/go-sdk/client"
)
//code
var (
PUBSUB_NAME = "order-pub-sub"
TOPIC_NAME = "orders"
)
func main() {
for i := 0; i < 10; i++ {
time.Sleep(5000)
orderId := rand.Intn(1000-1) + 1
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
defer client.Close()
ctx := context.Background()
//使用 Dapr SDK 发布主题
if err := client.PublishEvent(ctx, PUBSUB_NAME, TOPIC_NAME, []byte(strconv.Itoa(orderId)));
err != nil {
panic(err)
}
log.Println("Published data: " + strconv.Itoa(orderId))
}
}
导航到包含上述代码的目录,然后运行以下命令来启动 Dapr 边车和发布者应用程序:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 go run OrderProcessingService.go
//dependencies
import { DaprServer, DaprClient, CommunicationProtocolEnum } from '@dapr/dapr';
const daprHost = "127.0.0.1";
var main = function() {
for(var i=0;i<10;i++) {
sleep(5000);
var orderId = Math.floor(Math.random() * (1000 - 1) + 1);
start(orderId).catch((e) => {
console.error(e);
process.exit(1);
});
}
}
async function start(orderId) {
const PUBSUB_NAME = "order-pub-sub"
const TOPIC_NAME = "orders"
const client = new DaprClient({
daprHost,
daprPort: process.env.DAPR_HTTP_PORT,
communicationProtocol: CommunicationProtocolEnum.HTTP
});
console.log("Published data:" + orderId)
//使用 Dapr SDK 发布主题
await client.pubsub.publish(PUBSUB_NAME, TOPIC_NAME, orderId);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
main();
导航到包含上述代码的目录,然后运行以下命令来启动 Dapr 边车和发布者应用程序:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 npm start
消息确认和重试
为了告诉 Dapr 消息已成功处理,需要返回 200 OK 响应。如果 Dapr 收到除 200 以外的任何返回状态码,或者您的应用程序崩溃,Dapr 将按照至少一次语义尝试重新传递消息。
演示视频
观看此演示视频以了解更多关于使用 Dapr 进行发布订阅消息传递的信息。
后续步骤
- 尝试发布订阅教程。
- 了解使用 CloudEvents 进行消息传递以及何时可能想要不使用 CloudEvents 发送消息。
- 查看发布订阅组件列表。
- 阅读API 参考。
1.2.3 - 使用 CloudEvents 发布和订阅消息
为启用消息路由并为每条消息提供额外的上下文,Dapr 使用 CloudEvents 1.0 规范 作为其消息格式。应用程序使用 Dapr 发送到主题的任何消息都会自动包装在 CloudEvents 信封中,并使用 Content-Type 头部的值 作为 datacontenttype 属性。
Dapr 使用 CloudEvents 为事件负载提供额外的上下文,从而启用以下功能:
- 追踪
- 用于正确反序列化事件数据的内容类型
- 验证发送方应用程序
你可以通过发布订阅选择三种方法之一来发布 CloudEvent:
- 发送一个发布订阅事件,然后由 Dapr 将其包装在 CloudEvents 信封中。
- 通过覆盖标准 CloudEvent 属性来替换 Dapr 提供的特定 CloudEvents 属性。
- 在发布订阅事件中自行编写 CloudEvent 信封。
Dapr 生成的 CloudEvents 示例
向 Dapr 发送发布操作会自动将其包装在包含以下字段的 CloudEvent 信封中:
idsourcespecversiontypetraceparenttraceidtracestatetopicpubsubnametimedatacontenttype(可选)
以下示例演示了 Dapr 为发布到 orders 主题的操作生成的 CloudEvent,其中包括:
- W3C
traceid,对每条消息唯一 data和 CloudEvent 的字段,其中数据内容被序列化为 JSON
{
"topic": "orders",
"pubsubname": "order_pub_sub",
"traceid": "00-113ad9c4e42b27583ae98ba698d54255-e3743e35ff56f219-01",
"tracestate": "",
"data": {
"orderId": 1
},
"id": "5929aaac-a5e2-4ca1-859c-edfe73f11565",
"specversion": "1.0",
"datacontenttype": "application/json; charset=utf-8",
"source": "checkout",
"type": "com.dapr.event.sent",
"time": "2020-09-23T06:23:21Z",
"traceparent": "00-113ad9c4e42b27583ae98ba698d54255-e3743e35ff56f219-01"
}
作为 v1.0 CloudEvent 的另一个示例,以下显示数据作为 XML 内容在序列化为 JSON 的 CloudEvent 消息中:
{
"topic": "orders",
"pubsubname": "order_pub_sub",
"traceid": "00-113ad9c4e42b27583ae98ba698d54255-e3743e35ff56f219-01",
"tracestate": "",
"data" : "<note><to></to><from>user2</from><message>Order</message></note>",
"id" : "id-1234-5678-9101",
"specversion" : "1.0",
"datacontenttype" : "text/xml",
"subject" : "Test XML Message",
"source" : "https://example.com/message",
"type" : "xml.message",
"time" : "2020-09-23T06:23:21Z"
}
替换 Dapr 生成的 CloudEvents 值
Dapr 自动生成多个 CloudEvent 属性。你可以通过提供以下可选元数据键/值来替换这些生成的 CloudEvent 属性:
cloudevent.id:覆盖idcloudevent.source:覆盖sourcecloudevent.type:覆盖typecloudevent.traceid:覆盖traceidcloudevent.tracestate:覆盖tracestatecloudevent.traceparent:覆盖traceparent
使用这些元数据属性替换 CloudEvents 属性的能力适用于所有发布订阅组件。
示例
例如,要在代码中替换上述 CloudEvent 示例中的 source 和 id 值:
with DaprClient() as client:
order = {'orderId': i}
# Publish an event/message using Dapr PubSub
result = client.publish_event(
pubsub_name='order_pub_sub',
topic_name='orders',
publish_metadata={'cloudevent.id': 'd99b228f-6c73-4e78-8c4d-3f80a043d317', 'cloudevent.source': 'payment'}
)
# or
cloud_event = {
'specversion': '1.0',
'type': 'com.example.event',
'source': 'payment',
'id': 'd99b228f-6c73-4e78-8c4d-3f80a043d317',
'data': {'orderId': i},
'datacontenttype': 'application/json',
...
}
# Set the data content type to 'application/cloudevents+json'
result = client.publish_event(
pubsub_name='order_pub_sub',
topic_name='orders',
data=json.dumps(cloud_event),
data_content_type='application/cloudevents+json',
)
var order = new Order(i);
using var client = new DaprClientBuilder().Build();
// Override cloudevent metadata
var metadata = new Dictionary<string,string>() {
{ "cloudevent.source", "payment" },
{ "cloudevent.id", "d99b228f-6c73-4e78-8c4d-3f80a043d317" }
}
// Publish an event/message using Dapr PubSub
await client.PublishEventAsync("order_pub_sub", "orders", order, metadata);
Console.WriteLine("Published data: " + order);
await Task.Delay(TimeSpan.FromSeconds(1));
JSON 负载随后会反映新的 source 和 id 值:
{
"topic": "orders",
"pubsubname": "order_pub_sub",
"traceid": "00-113ad9c4e42b27583ae98ba698d54255-e3743e35ff56f219-01",
"tracestate": "",
"data": {
"orderId": 1
},
"id": "d99b228f-6c73-4e78-8c4d-3f80a043d317",
"specversion": "1.0",
"datacontenttype": "application/json; charset=utf-8",
"source": "payment",
"type": "com.dapr.event.sent",
"time": "2020-09-23T06:23:21Z",
"traceparent": "00-113ad9c4e42b27583ae98ba698d54255-e3743e35ff56f219-01"
}
重要
虽然你可以替换traceid/traceparent 和 tracestate,但这样做可能会干扰事件追踪,并在追踪工具中报告不一致的结果。建议使用 Open Telemetry 进行分布式追踪。了解更多关于分布式追踪的信息。发布你自己的 CloudEvent
如果你想使用自己的 CloudEvent,请确保将 datacontenttype 指定为 application/cloudevents+json。
如果应用程序编写的 CloudEvent 不包含 CloudEvent 规范中的最小必填字段,则消息会被拒绝。如果以下字段缺失,Dapr 会将它们添加到 CloudEvent 中:
timetraceidtraceparenttracestatetopicpubsubnamesourcetypespecversion
你可以在自定义 CloudEvent 中添加不属于官方 CloudEvent 规范的额外字段。Dapr 会原样传递这些字段。
示例
向 orders 主题发布一个 CloudEvent:
dapr publish --publish-app-id orderprocessing --pubsub order-pub-sub --topic orders --data '{\"orderId\": \"100\"}'
向 orders 主题发布一个 CloudEvent:
curl -X POST http://localhost:3601/v1.0/publish/order-pub-sub/orders -H "Content-Type: application/cloudevents+json" -d '{"specversion" : "1.0", "type" : "com.dapr.cloudevent.sent", "source" : "testcloudeventspubsub", "subject" : "Cloud Events Test", "id" : "someCloudEventId", "time" : "2021-08-02T09:00:00Z", "datacontenttype" : "application/cloudevents+json", "data" : {"orderId": "100"}}'
向 orders 主题发布一个 CloudEvent:
Invoke-RestMethod -Method Post -ContentType 'application/cloudevents+json' -Body '{"specversion" : "1.0", "type" : "com.dapr.cloudevent.sent", "source" : "testcloudeventspubsub", "subject" : "Cloud Events Test", "id" : "someCloudEventId", "time" : "2021-08-02T09:00:00Z", "datacontenttype" : "application/cloudevents+json", "data" : {"orderId": "100"}}' -Uri 'http://localhost:3601/v1.0/publish/order-pub-sub/orders'
发布二进制 CloudEvents
在二进制模式下,传输负载仅包含事件主体,而 CloudEvent 属性通过以 ce_ 前缀开头的传输元数据提供(HTTP 头、Kafka 头、NATS 头等)。这在你已经生成二进制模式事件或想要发送任意二进制数据而不将其包装在额外的 JSON 信封中时非常有用。
要将二进制 CloudEvent 发布到 Dapr(通过 HTTP/gRPC 发布 API 或直接发布到 Dapr 读取的代理):
将传输的原生 content-type 元数据(例如 HTTP
Content-Type头或 Kafkacontent-type消息头)设置为表示二进制数据的 MIME 类型,即application/octet-stream。将必填的 CloudEvent 属性(
ce_specversion、ce_type、ce_source、ce_id)添加为传输元数据。可选属性如ce_subject、ce_time或ce_traceparent也会被识别。在消息主体中发送负载字节。
向 orders 主题发布二进制 CloudEvent:
curl -X POST http://localhost:3500/v1.0/publish/order-pub-sub/orders \
-H "Content-Type: application/octet-stream" \
-H "ce_specversion: 1.0" \
-H "ce_type: com.example.order.created" \
-H "ce_source: urn:example:/checkout" \
-H "ce_id: 2a8bbf52-1222-4c2c-85f0-8a8875c7bc10" \
-H "ce_subject: orders/100" \
--data-binary $'\x01\x02\x03\x04'
事件去重
使用 Dapr 创建的 cloud events 时,信封包含一个 id 字段,应用程序可以将其用于消息去重。Dapr 不会自动处理去重。Dapr 支持使用原生支持消息去重的消息代理。
下一步
- 了解为什么你可能不想使用 CloudEvents
- 尝试发布订阅快速入门
- 发布订阅组件列表
- 阅读 API 参考
1.2.4 - 不使用 CloudEvents 发布和订阅消息
在向应用程序添加 Dapr 时,某些服务可能仍需要通过未封装在 CloudEvents 中的发布/订阅消息进行通信,原因可能是兼容性要求,或某些应用程序未使用 Dapr。这些被称为"原始"发布/订阅消息。Dapr 使应用程序能够发布和订阅原始事件,这些事件未包装在 CloudEvent 中,以实现兼容性并发送不可 JSON 序列化的数据。
发布原始消息
Dapr 应用程序能够向发布/订阅主题发布不包含 CloudEvent 封装的原始事件,以实现与非 Dapr 应用程序的兼容。

警告
不使用 CloudEvents 将禁用对追踪、基于 messageId 的事件去重、内容类型元数据以及使用 CloudEvent schema 构建的任何其他功能的支持。要禁用 CloudEvent 包装,请在发布请求中设置 rawPayload 元数据为 true。这允许订阅者接收这些消息而无需解析 CloudEvent schema。
curl -X "POST" http://localhost:3500/v1.0/publish/pubsub/TOPIC_A?metadata.rawPayload=true -H "Content-Type: application/json" -d '{"order-number": "345"}'
using Dapr.Client;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers().AddDapr();
var app = builder.Build();
app.MapPost("/publish", async (DaprClient daprClient) =>
{
var message = new Message(
Guid.NewGuid().ToString(),
$"Hello at {DateTime.UtcNow}",
DateTime.UtcNow
);
await daprClient.PublishEventAsync(
"pubsub", // pubsub name
"messages", // topic name
message, // message data
new Dictionary<string, string>
{
{ "rawPayload", "true" },
{ "content-type", "application/json" }
}
);
return Results.Ok(message);
});
app.Run();
from dapr.clients import DaprClient
with DaprClient() as d:
req_data = {
'order-number': '345'
}
# Create a typed message with content type and body
resp = d.publish_event(
pubsub_name='pubsub',
topic_name='TOPIC_A',
data=json.dumps(req_data),
publish_metadata={'rawPayload': 'true'}
)
# Print the request
print(req_data, flush=True)
<?php
require_once __DIR__.'/vendor/autoload.php';
$app = \Dapr\App::create();
$app->run(function(\DI\FactoryInterface $factory) {
$publisher = $factory->make(\Dapr\PubSub\Publish::class, ['pubsub' => 'pubsub']);
$publisher->topic('TOPIC_A')->publish('data', ['rawPayload' => 'true']);
});
@RestController
@PathMapping("/publish")
public class PublishController {
@Inject
DaprClient client;
@PostMapping
public void sendRawMessage() {
Map<String, String> metadata = new HashMap<>();
metatada.put("content-type", "application/json");
metadata.put("rawPayload", "true");
Message message = new Message(UUID.random().toString(), "Hello from Dapr");
client.publishEvent(
"pubsub", // pubsub name
"messages", // topic name
message, // message data
metadata) // metadata
.block(); // wait for completion
}
}
订阅原始消息
Dapr 应用程序可以订阅来自发布/订阅主题的原始消息,即使这些消息不是作为 CloudEvents 发布的。然而,订阅的 Dapr 进程仍会在将这些原始消息传递给订阅应用程序之前将其包装在 CloudEvent 中。

以编程方式订阅原始事件
以编程方式订阅时,添加 rawPayload 的额外元数据条目以允许订阅者接收未由 CloudEvent 包装的消息。对于 .NET,此元数据条目称为 isRawPayload。
使用原始负载时,消息始终使用 base64 编码,内容类型为 application/octet-stream。
using System.Text.Json;
using System.Text.Json.Serialization;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/dapr/subscribe", () =>
{
var subscriptions = new[]
{
new
{
pubsubname = "pubsub",
topic = "messages",
route = "/messages",
metadata = new Dictionary<string, string>
{
{ "rawPayload", "true" },
{ "content-type", "application/json" }
}
}
};
return Results.Ok(subscriptions);
});
app.MapPost("/messages", async (HttpContext context) =>
{
using var reader = new StreamReader(context.Request.Body);
var json = await reader.ReadToEndAsync();
Console.WriteLine($"Raw message received: {json}");
return Results.Ok();
});
app.Run();
import flask
from flask import request, jsonify
from flask_cors import CORS
import json
import sys
app = flask.Flask(__name__)
CORS(app)
@app.route('/dapr/subscribe', methods=['GET'])
def subscribe():
subscriptions = [{'pubsubname': 'pubsub',
'topic': 'deathStarStatus',
'route': 'dsstatus',
'metadata': {
'rawPayload': 'true',
} }]
return jsonify(subscriptions)
@app.route('/dsstatus', methods=['POST'])
def ds_subscriber():
print(request.json, flush=True)
return json.dumps({'success':True}), 200, {'ContentType':'application/json'}
app.run()
<?php
require_once __DIR__.'/vendor/autoload.php';
$app = \Dapr\App::create(configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions(['dapr.subscriptions' => [
new \Dapr\PubSub\Subscription(pubsubname: 'pubsub', topic: 'deathStarStatus', route: '/dsstatus', metadata: [ 'rawPayload' => 'true'] ),
]]));
$app->post('/dsstatus', function(
#[\Dapr\Attributes\FromBody]
\Dapr\PubSub\CloudEvent $cloudEvent,
\Psr\Log\LoggerInterface $logger
) {
$logger->alert('Received event: {event}', ['event' => $cloudEvent]);
return ['status' => 'SUCCESS'];
}
);
$app->start();
@RequestMapping("/consumer")
@RestController
public class MessageConsumerController {
@PostMapping
@ResponseStatus(HttpStatus.OK)
@Topic(pubsubName = "pubsub", name = "messages", metadata = "{\"rawPayload\":\"true\", \"content-type\": \"application/json\"}")
public void consume(@RequestBody Message message) {
System.out.println("Message received: " + message);
}
@PostMapping
@ResponseStatus(HttpStatus.OK)
@Topic(pubsubName = "pubsub", name = "another-topic", metadata = """
{"rawPayload": "true", "content-type": "application/json"}
""") // Using Java 15 text block
public void consumeAnother(@RequestBody Message message) {
System.out.println("Message received: " + message);
}
}
以声明方式订阅原始事件
同样,您可以通过在订阅规范中添加 rawPayload 元数据条目来以声明方式订阅原始事件。
apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
name: myevent-subscription
spec:
topic: deathStarStatus
routes:
default: /dsstatus
pubsubname: pubsub
metadata:
isRawPayload: "true"
scopes:
- app1
- app2
后续步骤
- 了解更多关于发布和订阅消息
- 发布/订阅组件列表
- 阅读API 参考文档
- 阅读关于如何不使用 CloudEvents 消费 Kafka 消息的 .NET 示例
1.2.5 - 操作指南:将消息路由到不同的事件处理程序
发布订阅路由是基于内容的路由的一种实现,这是一种使用 DSL 而非命令式应用程序代码的消息传递模式。通过发布订阅路由,你可以使用表达式根据内容将 CloudEvents 路由到应用程序中的不同 URI/路径和事件处理程序。如果没有路由匹配,则使用可选的默认路由。当应用程序扩展以支持多个事件版本或特殊情况时,这非常有用。
虽然路由可以通过代码实现,但将路由规则保持在应用程序外部可以提高可移植性。
此功能适用于声明式和编程式订阅方法,但不适用于流式订阅。
声明式订阅
对于声明式订阅,使用 dapr.io/v2alpha1 作为 apiVersion。以下是使用路由的 subscriptions.yaml 示例:
apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
name: myevent-subscription
spec:
pubsubname: pubsub
topic: inventory
routes:
rules:
- match: event.type == "widget"
path: /widgets
- match: event.type == "gadget"
path: /gadgets
default: /products
scopes:
- app1
- app2
编程式订阅
在编程式方法中,返回 routes 结构而不是 route。JSON 结构与声明式 YAML 匹配:
import flask
from flask import request, jsonify
from flask_cors import CORS
import json
import sys
app = flask.Flask(__name__)
CORS(app)
@app.route('/dapr/subscribe', methods=['GET'])
def subscribe():
subscriptions = [
{
'pubsubname': 'pubsub',
'topic': 'inventory',
'routes': {
'rules': [
{
'match': 'event.type == "widget"',
'path': '/widgets'
},
{
'match': 'event.type == "gadget"',
'path': '/gadgets'
},
],
'default': '/products'
}
}]
return jsonify(subscriptions)
@app.route('/products', methods=['POST'])
def ds_subscriber():
print(request.json, flush=True)
return json.dumps({'success':True}), 200, {'ContentType':'application/json'}
app.run()
const express = require('express')
const bodyParser = require('body-parser')
const app = express()
app.use(bodyParser.json({ type: 'application/*+json' }));
const port = 3000
app.get('/dapr/subscribe', (req, res) => {
res.json([
{
pubsubname: "pubsub",
topic: "inventory",
routes: {
rules: [
{
match: 'event.type == "widget"',
path: '/widgets'
},
{
match: 'event.type == "gadget"',
path: '/gadgets'
},
],
default: '/products'
}
}
]);
})
app.post('/products', (req, res) => {
console.log(req.body);
res.sendStatus(200);
});
app.listen(port, () => console.log(`consumer app listening on port ${port}!`))
[Topic("pubsub", "inventory", "event.type ==\"widget\"", 1)]
[HttpPost("widgets")]
public async Task<ActionResult<Stock>> HandleWidget(Widget widget, [FromServices] DaprClient daprClient)
{
// Logic
return stock;
}
[Topic("pubsub", "inventory", "event.type ==\"gadget\"", 2)]
[HttpPost("gadgets")]
public async Task<ActionResult<Stock>> HandleGadget(Gadget gadget, [FromServices] DaprClient daprClient)
{
// Logic
return stock;
}
[Topic("pubsub", "inventory")]
[HttpPost("products")]
public async Task<ActionResult<Stock>> HandleProduct(Product product, [FromServices] DaprClient daprClient)
{
// Logic
return stock;
}
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
)
const appPort = 3000
type subscription struct {
PubsubName string `json:"pubsubname"`
Topic string `json:"topic"`
Metadata map[string]string `json:"metadata,omitempty"`
Routes routes `json:"routes"`
}
type routes struct {
Rules []rule `json:"rules,omitempty"`
Default string `json:"default,omitempty"`
}
type rule struct {
Match string `json:"match"`
Path string `json:"path"`
}
// This handles /dapr/subscribe
func configureSubscribeHandler(w http.ResponseWriter, _ *http.Request) {
t := []subscription{
{
PubsubName: "pubsub",
Topic: "inventory",
Routes: routes{
Rules: []rule{
{
Match: `event.type == "widget"`,
Path: "/widgets",
},
{
Match: `event.type == "gadget"`,
Path: "/gadgets",
},
},
Default: "/products",
},
},
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(t)
}
func main() {
router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/dapr/subscribe", configureSubscribeHandler).Methods("GET")
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", appPort), router))
}
<?php
require_once __DIR__.'/vendor/autoload.php';
$app = \Dapr\App::create(configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions(['dapr.subscriptions' => [
new \Dapr\PubSub\Subscription(pubsubname: 'pubsub', topic: 'inventory', routes: (
rules: => [
('match': 'event.type == "widget"', path: '/widgets'),
('match': 'event.type == "gadget"', path: '/gadgets'),
]
default: '/products')),
]]));
$app->post('/products', function(
#[\Dapr\Attributes\FromBody]
\Dapr\PubSub\CloudEvent $cloudEvent,
\Psr\Log\LoggerInterface $logger
) {
$logger->alert('Received event: {event}', ['event' => $cloudEvent]);
return ['status' => 'SUCCESS'];
}
);
$app->start();
通用表达式语言 (CEL)
在这些示例中,根据 event.type 的不同,应用程序将在以下路径被调用:
/widgets/gadgets/products
表达式以通用表达式语言 (CEL)编写,其中 event 表示云事件。可以引用 CloudEvents 核心规范中的任何属性。
表达式示例
匹配"重要"消息:
has(event.data.important) && event.data.important == true
匹配大于 $10,000 的存款:
event.type == "deposit" && int(event.data.amount) > 10000
匹配消息的多个版本:
event.type == "mymessage.v1"
event.type == "mymessage.v2"
CloudEvent 属性
作为参考,以下属性来自 CloudEvents 规范。
事件数据
data
根据术语 data 的定义,CloudEvents _可能_包含有关发生的领域特定信息。如果存在,此信息将封装在 data 中。
- 描述: 事件负载。此规范不对信息类型施加限制。它被编码为由
datacontenttype属性指定的媒体格式(例如 application/json),并在存在相应属性时遵循dataschema格式。 - 约束:
- 可选 (OPTIONAL)
限制
目前,只有当 data 是嵌套的 JSON 值而不是字符串中的 JSON 转义时,你才能访问 data 内部的属性。必需属性
以下属性在所有 CloudEvents 中是必需的:
id
- 类型:
String - 描述: 标识事件。生产者_必须_确保
source+id对于每个不同的事件都是唯一的。如果重复事件被重新发送(例如由于网络错误),它可能具有相同的id。消费者可以假定具有相同source和id的事件是重复的。 - 约束:
- 必需 (REQUIRED)
- 必须是非空字符串
- 必须在生产者范围内唯一
- 示例:
- 生产者维护的事件计数器
- UUID
source
类型:
URI-reference描述: 标识事件发生的上下文。通常这包括以下信息:
- 事件源的类型
- 发布事件的组织
- 产生事件的进程
URI 中编码的数据的确切语法和语义由事件生产者定义。
生产者_必须_确保
source+id对于每个不同的事件都是唯一的。应用程序可以:
- 为每个不同的生产者分配唯一的
source,从而更容易生成唯一 ID 并防止其他生产者具有相同的source。 - 使用 UUID、URN、DNS 权限或应用程序特定的方案来创建唯一的
source标识符。
一个源可能包含多个生产者。在这种情况下,生产者_必须_协作以确保
source+id对于每个不同的事件都是唯一的。约束:
- 必需 (REQUIRED)
- 必须是非空 URI 引用
- 建议使用绝对 URI
示例:
- 具有 DNS 权限的互联网范围内唯一 URI:
- https://github.com/cloudevents
- mailto:cncf-wg-serverless@lists.cncf.io
- 具有 UUID 的通用唯一 URN:
- urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66
- 应用程序特定的标识符:
- /cloudevents/spec/pull/123
- /sensors/tn-1234567/alerts
- 1-555-123-4567
- 具有 DNS 权限的互联网范围内唯一 URI:
specversion
类型:
String描述: 事件使用的 CloudEvents 规范版本。这能够解释上下文。合规的事件生产者在引用此版本的规范时_必须_使用值
1.0。目前,此属性仅包含"主"和"次"版本号。这允许对规范进行补丁更改而不更改序列化中此属性的值。
注意:对于"候选版本"版本,可能会使用后缀进行测试目的。
约束:
- 必需 (REQUIRED)
- 必须是非空字符串
type
- 类型:
String - 描述: 包含一个描述与源发事件相关的事件类型的值。通常,此属性用于路由、可观测性、策略执行等。格式由生产者定义,可能包括
type的版本等信息。有关更多信息,请参阅 Primer 中的 CloudEvents 版本控制。 - 约束:
- 必需 (REQUIRED)
- 必须是非空字符串
- 应以前向 DNS 名称作为前缀。前缀域指示组织,该组织定义此事件类型的语义。
- 示例:
- com.github.pull_request.opened
- com.example.object.deleted.v2
可选属性
以下属性在 CloudEvents 中是可选的。有关 OPTIONAL 定义的更多信息,请参阅符号约定部分。
datacontenttype
类型:
String,根据 RFC 2046描述:
data值的内容类型。此属性使data能够承载任何类型的内容,其中格式和编码可能与所选事件格式的不同。例如,使用 JSON 信封格式渲染的事件可能在
data中承载 XML 负载。通过将此属性设置为"application/xml"来通知消费者。对于不同
datacontenttype值如何渲染data内容的规则在事件格式规范中定义。例如,JSON 事件格式在第 3.1 节中定义了关系。对于某些二进制模式协议绑定,此字段直接映射到相应协议的 content-type 元数据属性。你可以在相应协议中找到二进制模式和 content-type 元数据映射的规范规则。
在某些事件格式中,你可以省略
datacontenttype属性。例如,如果 JSON 格式事件没有datacontenttype属性,则暗示data是符合"application/json"媒体类型的 JSON 值。换句话说:没有datacontenttype的 JSON 格式事件与具有datacontenttype="application/json"的事件完全等效。将没有
datacontenttype属性的事件消息转换为不同格式或协议绑定时,应将目标datacontenttype显式设置为源的隐式datacontenttype。约束:
- 可选 (OPTIONAL)
- 如果存在,必须遵守 RFC 2046 中指定的格式
有关媒体类型示例,请参阅 IANA 媒体类型
dataschema
- 类型:
URI - 描述: 标识
data遵循的模式。对模式的不兼容更改应通过不同的 URI 反映。有关更多信息,请参阅 Primer 中的 CloudEvents 版本控制。 - 约束:
- 可选 (OPTIONAL)
- 如果存在,必须是非空 URI
subject
类型:
String描述: 这在事件生产者(由
source标识)的上下文中描述事件主题。在发布-订阅场景中,订阅者通常会订阅由source发出的事件。如果source上下文具有内部子结构,则单独的source标识符可能不足以作为任何特定事件的限定符。在上下文元数据中标识事件主题(而不是仅在
data负载中)对于通用订阅过滤场景很有帮助,在这些场景中,中间件无法解释data内容。在上述示例中,订阅者可能只对名称以 ‘.jpg’ 或 ‘.jpeg’ 结尾的 blob 感兴趣。使用subject属性,你可以为该事件子集构建简单而高效的字符串后缀过滤器。约束:
- 可选 (OPTIONAL)
- 如果存在,必须是非空字符串
示例:
订阅者可能注册对新 blob 在 blob 存储容器中创建时的兴趣。在这种情况下:- 事件
source标识订阅范围(存储容器) - 事件
type标识"blob 已创建"事件 - 事件
id唯一标识事件实例,以区分同名的 blob 的单独创建事件。
新创建的 blob 的名称承载在
subject中:source: https://example.com/storage/tenant/containersubject: mynewfile.jpg
- 事件
time
- 类型:
Timestamp - 描述: 事件发生时的时间戳。如果无法确定事件发生的时间,则 CloudEvents 生产者可以将此属性设置为其他时间(例如当前时间)。但是,同一
source的所有生产者在这方面_必须_保持一致。换句话说,它们要么都使用事件的实际时间,要么都使用相同的算法来确定使用的值。 - 约束:
- 可选 (OPTIONAL)
- 如果存在,必须遵守 RFC 3339 中指定的格式
限制
目前,不支持与时间的比较(例如"现在"之前或之后)。社区电话演示
观看此视频,了解如何使用发布订阅进行消息路由:
后续步骤
1.2.6 - 声明式、流式和编程式订阅类型
发布订阅 API 订阅类型
Dapr 应用程序可以通过三种订阅类型订阅已发布的主题,这些类型支持相同的功能:声明式、流式和编程式。
| 订阅类型 | 描述 |
|---|---|
| 声明式 | 订阅在外部文件中定义。声明式方法从代码中移除了 Dapr 依赖,允许现有应用程序订阅主题,而无需更改代码。 |
| 流式 | 订阅在应用程序代码中定义。流式订阅是动态的,这意味着它们允许在运行时添加或删除订阅。它们不需要在应用程序中设置订阅端点(编程式和声明式订阅都需要),使其易于在代码中配置。流式订阅也不需要将应用程序配置为通过边车来接收消息。 |
| 编程式 | 订阅在应用程序代码中定义。编程式方法实现静态订阅并要求代码中有一个端点。 |
下面的示例演示了 checkout 应用程序和 orderprocessing 应用程序之间通过 orders 主题进行的发布订阅消息传递。这些示例演示了同一个 Dapr 发布订阅组件首先以声明方式使用,然后以编程方式使用。
声明式订阅
注意
此功能目前处于预览状态。 Dapr 可以"热重载"声明式订阅,从而自动获取更新而无需重启。 这是通过HotReload 功能门控启用的。
为了防止重新处理或丢失未处理的消息,Dapr 与应用程序之间正在传输的消息在热重载事件期间不受影响。您可以使用外部组件文件以声明方式订阅主题。此示例使用名为 subscription.yaml 的 YAML 组件文件:
apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
name: order
spec:
topic: orders
routes:
default: /orders
pubsubname: pubsub
scopes:
- orderprocessing
这里名为 order 的订阅:
- 使用名为
pubsub的发布订阅组件订阅名为orders的主题。 - 设置
route字段以将所有主题消息发送到应用程序中的/orders端点。 - 设置
scopes字段以将此订阅的范围限制为仅由 ID 为orderprocessing的应用程序访问。
运行 Dapr 时,设置 YAML 组件文件路径以将 Dapr 指向该组件。
dapr run --app-id myapp --resources-path ./myComponents -- dotnet run
dapr run --app-id myapp --resources-path ./myComponents -- mvn spring-boot:run
dapr run --app-id myapp --resources-path ./myComponents -- python3 app.py
dapr run --app-id myapp --resources-path ./myComponents -- npm start
dapr run --app-id myapp --resources-path ./myComponents -- go run app.go
在 Kubernetes 中,将组件应用到集群:
kubectl apply -f subscription.yaml
在应用程序代码中,订阅 Dapr 发布订阅组件中指定的主题。
//Subscribe to a topic
[HttpPost("orders")]
public void getCheckout([FromBody] int orderId)
{
Console.WriteLine("Subscriber received : " + orderId);
}
import io.dapr.client.domain.CloudEvent;
//Subscribe to a topic
@PostMapping(path = "/orders")
public Mono<Void> getCheckout(@RequestBody(required = false) CloudEvent<String> cloudEvent) {
return Mono.fromRunnable(() -> {
try {
log.info("Subscriber received: " + cloudEvent.getData());
}
});
}
from cloudevents.sdk.event import v1
#Subscribe to a topic
@app.route('/orders', methods=['POST'])
def checkout(event: v1.Event) -> None:
data = json.loads(event.Data())
logging.info('Subscriber received: ' + str(data))
const express = require('express')
const bodyParser = require('body-parser')
const app = express()
app.use(bodyParser.json({ type: 'application/*+json' }));
// listen to the declarative route
app.post('/orders', (req, res) => {
console.log(req.body);
res.sendStatus(200);
});
//Subscribe to a topic
var sub = &common.Subscription{
PubsubName: "pubsub",
Topic: "orders",
Route: "/orders",
}
func eventHandler(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
log.Printf("Subscriber received: %s", e.Data)
return false, nil
}
/orders 端点与订阅中定义的 route 匹配,这是 Dapr 将所有主题消息发送到的地方。
流式订阅
流式订阅是在应用程序代码中定义的订阅,可以在运行时动态停止和启动。 消息由应用程序从 Dapr 拉取。这意味着不需要端点来订阅主题,并且可以在边车上根本没有配置任何应用程序的情况下进行订阅。 可以同时订阅任意数量的发布订阅和主题。 当消息发送到给定的消息处理程序代码时,没有路由或批量订阅的概念。
下面的示例展示了流式订阅主题的不同方式。
您可以使用 DaprPublishSubscribeClient 上的 SubscribeAsync 方法来配置用于从流中拉取消息的消息处理程序。
using System.Text;
using Dapr.Messaging.PublishSubscribe;
using Dapr.Messaging.PublishSubscribe.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprPubSubClient();
var app = builder.Build();
var messagingClient = app.Services.GetRequiredService<DaprPublishSubscribeClient>();
//Create a dynamic streaming subscription and subscribe with a timeout of 30 seconds and 10 seconds for message handling
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var subscription = await messagingClient.SubscribeAsync("pubsub", "myTopic",
new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(10), TopicResponseAction.Retry)),
HandleMessageAsync, cancellationTokenSource.Token);
await Task.Delay(TimeSpan.FromMinutes(1));
//When you're done with the subscription, simply dispose of it
await subscription.DisposeAsync();
return;
//Process each message returned from the subscription
Task<TopicResponseAction> HandleMessageAsync(TopicMessage message, CancellationToken cancellationToken = default)
{
try
{
//Do something with the message
Console.WriteLine(Encoding.UTF8.GetString(message.Data.Span));
return Task.FromResult(TopicResponseAction.Success);
}
catch
{
return Task.FromResult(TopicResponseAction.Retry);
}
}
您可以使用 subscribe 方法,该方法返回一个 Subscription 对象,并允许您通过调用 next_message 方法从流中拉取消息。这在等待消息时运行并可能阻塞主线程。
import time
from dapr.clients import DaprClient
from dapr.clients.grpc.subscription import StreamInactiveError
counter = 0
def process_message(message):
global counter
counter += 1
# Process the message here
print(f'Processing message: {message.data()} from {message.topic()}...')
return 'success'
def main():
with DaprClient() as client:
global counter
subscription = client.subscribe(
pubsub_name='pubsub', topic='orders', dead_letter_topic='orders_dead'
)
try:
while counter < 5:
try:
message = subscription.next_message()
except StreamInactiveError as e:
print('Stream is inactive. Retrying...')
time.sleep(1)
continue
if message is None:
print('No message received within timeout period.')
continue
# Process the message
response_status = process_message(message)
if response_status == 'success':
subscription.respond_success(message)
elif response_status == 'retry':
subscription.respond_retry(message)
elif response_status == 'drop':
subscription.respond_drop(message)
finally:
print("Closing subscription...")
subscription.close()
if __name__ == '__main__':
main()
您也可以使用 subscribe_with_handler 方法,该方法接受一个回调函数,该函数对从流接收到的每条消息执行。这在单独的线程中运行,因此不会阻塞主线程。
import time
from dapr.clients import DaprClient
from dapr.clients.grpc._response import TopicEventResponse
counter = 0
def process_message(message):
# Process the message here
global counter
counter += 1
print(f'Processing message: {message.data()} from {message.topic()}...')
return TopicEventResponse('success')
def main():
with (DaprClient() as client):
# This will start a new thread that will listen for messages
# and process them in the `process_message` function
close_fn = client.subscribe_with_handler(
pubsub_name='pubsub', topic='orders', handler_fn=process_message,
dead_letter_topic='orders_dead'
)
while counter < 5:
time.sleep(1)
print("Closing subscription...")
close_fn()
if __name__ == '__main__':
main()
package main
import (
"context"
"log"
"github.com/dapr/go-sdk/client"
)
func main() {
cl, err := client.NewClient()
if err != nil {
log.Fatal(err)
}
sub, err := cl.Subscribe(context.Background(), client.SubscriptionOptions{
PubsubName: "pubsub",
Topic: "orders",
})
if err != nil {
panic(err)
}
// Close must always be called.
defer sub.Close()
for {
msg, err := sub.Receive()
if err != nil {
panic(err)
}
// Process the event
// We _MUST_ always signal the result of processing the message, else the
// message will not be considered as processed and will be redelivered or
// dead lettered.
// msg.Retry()
// msg.Drop()
if err := msg.Success(); err != nil {
panic(err)
}
}
}
或
package main
import (
"context"
"log"
"github.com/dapr/go-sdk/client"
"github.com/dapr/go-sdk/service/common"
)
func main() {
cl, err := client.NewClient()
if err != nil {
log.Fatal(err)
}
stop, err := cl.SubscribeWithHandler(context.Background(),
client.SubscriptionOptions{
PubsubName: "pubsub",
Topic: "orders",
},
eventHandler,
)
if err != nil {
panic(err)
}
// Stop must always be called.
defer stop()
<-make(chan struct{})
}
func eventHandler(e *common.TopicEvent) common.SubscriptionResponseStatus {
// Process message here
// common.SubscriptionResponseStatusRetry
// common.SubscriptionResponseStatusDrop
common.SubscriptionResponseStatusDrop, status);
}
return common.SubscriptionResponseStatusSuccess
}
演示
观看此视频了解流式订阅的概述:
编程式订阅
与声明式方法的 route YAML 结构不同,动态编程式方法在代码中返回 routes JSON 结构。
注意: 编程式订阅仅在应用程序启动期间读取一次。您不能动态添加新的编程式订阅,只能在编译时添加新的订阅。
禁用编程式订阅
如果您的应用程序不使用编程式订阅,可以禁用对/dapr/subscribe 的自动 HTTP 调用以减少日志噪音。在 dapr run 中使用 --disable-init-endpoints subscribe 标志,或在 Kubernetes 中使用 dapr.io/disable-init-endpoints: "subscribe" 注解。了解有关禁用初始化端点的更多信息。在下面的示例中,您在应用程序代码中定义上面声明式 YAML 订阅中找到的值。
[Topic("pubsub", "orders")]
[HttpPost("/orders")]
public async Task<ActionResult<Order>>Checkout(Order order, [FromServices] DaprClient daprClient)
{
// Logic
return order;
}
或
// Dapr subscription in [Topic] routes orders topic to this route
app.MapPost("/orders", [Topic("pubsub", "orders")] (Order order) => {
Console.WriteLine("Subscriber received : " + order);
return Results.Ok(order);
});
上面定义的处理程序还需要映射到 dapr/subscribe 端点。这是在定义端点时的应用程序启动代码中完成的。
app.UseEndpoints(endpoints =>
{
endpoints.MapSubscribeHandler();
});
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Topic(name = "orders", pubsubName = "pubsub")
@PostMapping(path = "/orders")
public Mono<Void> handleMessage(@RequestBody(required = false) CloudEvent<String> cloudEvent) {
return Mono.fromRunnable(() -> {
try {
System.out.println("Subscriber received: " + cloudEvent.getData());
System.out.println("Subscriber received: " + OBJECT_MAPPER.writeValueAsString(cloudEvent));
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
@app.route('/dapr/subscribe', methods=['GET'])
def subscribe():
subscriptions = [
{
'pubsubname': 'pubsub',
'topic': 'orders',
'routes': {
'rules': [
{
'match': 'event.type == "order"',
'path': '/orders'
},
],
'default': '/orders'
}
}]
return jsonify(subscriptions)
@app.route('/orders', methods=['POST'])
def ds_subscriber():
print(request.json, flush=True)
return json.dumps({'success':True}), 200, {'ContentType':'application/json'}
app.run()
const express = require('express')
const bodyParser = require('body-parser')
const app = express()
app.use(bodyParser.json({ type: 'application/*+json' }));
const port = 3000
app.get('/dapr/subscribe', (req, res) => {
res.json([
{
pubsubname: "pubsub",
topic: "orders",
routes: {
rules: [
{
match: 'event.type == "order"',
path: '/orders'
},
],
default: '/products'
}
}
]);
})
app.post('/orders', (req, res) => {
console.log(req.body);
res.sendStatus(200);
});
app.listen(port, () => console.log(`consumer app listening on port ${port}!`))
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
)
const appPort = 3000
type subscription struct {
PubsubName string `json:"pubsubname"`
Topic string `json:"topic"`
Metadata map[string]string `json:"metadata,omitempty"`
Routes routes `json:"routes"`
}
type routes struct {
Rules []rule `json:"rules,omitempty"`
Default string `json:"default,omitempty"`
}
type rule struct {
Match string `json:"match"`
Path string `json:"path"`
}
// This handles /dapr/subscribe
func configureSubscribeHandler(w http.ResponseWriter, _ *http.Request) {
t := []subscription{
{
PubsubName: "pubsub",
Topic: "orders",
Routes: routes{
Rules: []rule{
{
Match: `event.type == "order"`,
Path: "/orders",
},
},
Default: "/orders",
},
},
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(t)
}
func main() {
router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/dapr/subscribe", configureSubscribeHandler).Methods("GET")
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", appPort), router))
}
下一步
- 尝试发布订阅快速入门
- 按照操作指南:使用多个命名空间配置发布订阅组件
- 了解有关声明式和编程式订阅方法的更多信息。
- 了解主题作用域
- 了解消息 TTL
- 了解有关使用和不使用 CloudEvent 的发布订阅
- 发布订阅组件列表
- 阅读发布订阅 API 参考
1.2.7 - 死信主题
简介
应用程序有时可能因各种原因无法处理消息。例如,在检索处理消息所需的数据时可能存在瞬时问题,或者应用程序业务逻辑失败并返回错误。死信主题用于转发无法投递到订阅应用程序的消息。这减轻了应用程序的压力,使其无需处理这些失败的消息,允许开发者编写代码从死信主题读取消息,然后修复消息并重新发送,或完全放弃该消息。
死信主题通常与重试弹性策略以及死信订阅一起使用,死信订阅负责处理从死信主题转发来的消息所需的逻辑。
当设置了死信主题时,任何未能投递到已配置主题的应用程序的消息都会被放到死信主题上,以便转发给处理这些消息的订阅。这可能是同一个应用程序,也可能是完全不同的应用程序。
Dapr 为其所有发布/订阅组件启用死信主题,即使底层系统本身不支持此功能。例如,AWS SNS 组件 具有死信队列,RabbitMQ 具有死信主题。你需要确保正确配置此类组件。
下图是死信主题如何工作的示例。首先,从 orders 主题上的发布者发送一条消息。Dapr 代表订阅应用程序接收该消息,但是 orders 主题消息未能投递到应用程序上的 /checkout 端点,即使经过重试也是如此。由于投递失败,该消息被转发到 poisonMessages 主题,该主题将其传递到 /failedMessages 端点进行处理,在本例中是在同一应用程序上。failedMessages 处理代码可以丢弃消息或重新发送新消息。

使用声明式订阅配置死信主题
以下 YAML 显示如何为从 orders 主题消费的消息配置名为 poisonMessages 的死信主题的订阅。此订阅的作用域限定为具有 checkout ID 的应用程序。
apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
name: order
spec:
topic: orders
routes:
default: /checkout
pubsubname: pubsub
deadLetterTopic: poisonMessages
scopes:
- checkout
使用流式订阅配置死信主题
var deadLetterTopic = "poisonMessages"
sub, err := cl.Subscribe(context.Background(), client.SubscriptionOptions{
PubsubName: "pubsub",
Topic: "orders",
DeadLetterTopic: &deadLetterTopic,
})
使用编程式订阅配置死信主题
从 /subscribe 端点返回的 JSON 显示如何为从 orders 主题消费的消息配置名为 poisonMessages 的死信主题。
app.get('/dapr/subscribe', (_req, res) => {
res.json([
{
pubsubname: "pubsub",
topic: "orders",
route: "/checkout",
deadLetterTopic: "poisonMessages"
}
]);
});
重试和死信主题
默认情况下,当设置了死信主题时,任何失败的消息都会立即进入死信主题。因此,建议在订阅中使用死信主题时始终设置重试策略。要在将消息发送到死信主题之前启用重试,请将重试弹性策略 应用于发布/订阅组件。
此示例显示如何为 pubsub 发布/订阅组件设置名为 pubsubRetry 的恒定重试策略,最大投递尝试次数为 10 次,每 5 秒应用一次。
apiVersion: dapr.io/v1alpha1
kind: Resiliency
metadata:
name: myresiliency
spec:
policies:
retries:
pubsubRetry:
policy: constant
duration: 5s
maxRetries: 10
targets:
components:
pubsub:
inbound:
retry: pubsubRetry
配置用于处理死信主题的订阅
请记住,现在要配置一个订阅来处理死信主题。例如,你可以创建另一个声明式订阅,以在同一或不同的应用程序上接收这些消息。下面的示例显示 checkout 应用程序订阅 poisonMessages 主题,并通过另一个订阅将这些消息发送到 /failedmessages 端点进行处理。
apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
name: deadlettertopics
spec:
topic: poisonMessages
routes:
rules:
- match:
path: /failedMessages
pubsubname: pubsub
scopes:
- checkout
演示
后续步骤
- 有关弹性策略的更多信息,请阅读弹性概述。
- 有关主题订阅的更多信息,请阅读声明式、流式和编程式订阅方法。
1.2.8 - 如何操作:设置发布订阅命名空间消费者组
你已经设置了 Dapr 的发布订阅 API 构建块,你的应用程序正在使用中心化的消息代理顺畅地发布和订阅主题。如果你想为应用程序执行简单的 A/B 测试、蓝绿部署,甚至金丝雀部署,该怎么办呢?即使使用 Dapr,这也可能变得很困难。
Dapr 通过其发布订阅命名空间消费者组构造解决了大规模的多租户问题。
没有命名空间消费者组
假设你有一个 Kubernetes 集群,两个应用程序(App1 和 App2)部署在同一个命名空间(namespace-a)中。App2 发布到名为 order 的主题,而 App1 订阅名为 order 的主题。这将创建两个消费者组,以你的应用程序名称命名(App1 和 App2)。

为了在使用中心化消息代理的同时执行简单的测试和部署,你创建了另一个命名空间,其中包含两个具有相同 app-id 的应用程序 App1 和 App2。
Dapr 使用单个应用程序的 app-id 创建消费者组,因此消费者组名称将保持为 App1 和 App2。

为了避免这种情况,你需要根据运行的命名空间,在代码中添加一些内容来更改 app-id。这种变通方法很繁琐,是一个显著的痛点。
使用命名空间消费者组
Dapr 不仅允许你通过 UUID 和 Pod 名称的 consumerID 更改消费者组的行为,还提供了一个存在于发布/订阅组件元数据中的命名空间构造。例如,使用 Redis 作为消息代理:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
- name: consumerID
value: "{namespace}"
通过将 consumerID 配置为 {namespace} 值,你将能够从不同的命名空间使用相同的 app-id 和相同的主题。

在上图中,你有两个命名空间,每个命名空间都有具有相同 app-id 的应用程序,发布和订阅到同一个中心化消息代理 orders。然而这一次,Dapr 创建了以其运行的命名空间为前缀的消费者组名称。
无需更改代码/app-id,命名空间消费者组允许你:
- 添加更多命名空间
- 保持相同的主题
- 在命名空间之间保持相同的
app-id - 保持整个部署管道完整
只需在组件元数据中包含 "{namespace}" 消费者组构造即可。你不需要在元数据中编码命名空间。Dapr 理解它运行的命名空间,并为你完成命名空间值,就像运行时注入的动态元数据值一样。
注意
如果你之后将命名空间消费者组添加到元数据中,Dapr 会为你更新所有内容。这意味着你可以将命名空间元数据值添加到现有的发布/订阅部署中。演示
后续步骤
- 了解有关使用多个命名空间配置发布/订阅组件的更多信息 发布/订阅命名空间。
1.2.9 - 如何:通过 StatefulSet 水平扩展订阅者
与 Pod 为临时性的 Deployments 不同,StatefulSets 允许在 Kubernetes 上部署有状态应用程序,同时为每个 Pod 保持一个稳定的标识。
以下是使用 Dapr 的 StatefulSet 示例:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: python-subscriber
spec:
selector:
matchLabels:
app: python-subscriber # has to match .spec.template.metadata.labels
serviceName: "python-subscriber"
replicas: 3
template:
metadata:
labels:
app: python-subscriber # has to match .spec.selector.matchLabels
annotations:
dapr.io/enabled: "true"
dapr.io/app-id: "python-subscriber"
dapr.io/app-port: "5001"
spec:
containers:
- name: python-subscriber
image: ghcr.io/dapr/samples/pubsub-python-subscriber:latest
ports:
- containerPort: 5001
imagePullPolicy: Always
通过 Dapr 订阅发布订阅主题时,应用程序可以定义 consumerID,该 ID 决定了订阅者在队列或主题中的位置。借助 StatefulSets 的 Pod 稳定标识特性,每个 Pod 可以拥有唯一的 consumerID,从而实现订阅者应用程序的每个水平扩展。Dapr 会跟踪每个 Pod 的名称,该名称可在声明组件时使用 {podName} 标记。
在扩展给定主题的订阅者数量时,每个 Dapr 组件都有独特的设置来决定其行为。通常,多个消费者有两种选择:
- 广播:发布到主题的每条消息都会被所有订阅者消费。
- 共享:一条消息由任意订阅者消费(但不是全部)。
Kafka 通过 consumerID 隔离每个订阅者,并在主题中维护各自的位置。当实例重启时,它会重用相同的 consumerID 并从其最后已知位置继续,不会跳过消息。下面的组件演示了 Kafka 组件如何被多个 Pod 使用:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.kafka
version: v1
metadata:
- name: brokers
value: my-cluster-kafka-bootstrap.kafka.svc.cluster.local:9092
- name: consumerID
value: "{podName}"
- name: authRequired
value: "false"
MQTT3 协议具有共享主题功能,允许多个订阅者"竞争"主题中的消息,即一条消息仅由其中一个处理。例如:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: mqtt-pubsub
spec:
type: pubsub.mqtt3
version: v1
metadata:
- name: consumerID
value: "{podName}"
- name: cleanSession
value: "true"
- name: url
value: "tcp://admin:public@localhost:1883"
- name: qos
value: 1
- name: retain
value: "false"
下一步
- 尝试 发布订阅教程。
- 了解 使用 CloudEvents 进行消息传递 以及何时可能 发送不含 CloudEvents 的消息。
- 查看 发布订阅组件列表。
- 阅读 API 参考文档。
1.2.10 - 限制发布订阅主题访问
简介
命名空间或组件范围 可用于将组件访问限制到特定应用程序。添加到组件的这些应用程序范围仅允许具有特定 ID 的应用程序使用该组件。
除了此常规组件范围外,发布订阅组件还可以限制以下内容:
- 可使用哪些主题(发布或订阅)
- 允许哪些应用程序向特定主题发布
- 允许哪些应用程序订阅特定主题
这称为发布订阅主题范围。
发布订阅范围是为每个发布订阅组件定义的。您可能有一个名为 pubsub 的发布订阅组件,它具有一组范围,而另一个 pubsub2 具有不同的范围。
要使用此主题范围,可以为发布订阅组件设置三个元数据属性:
spec.metadata.publishingScopes- 以分号分隔的应用程序列表和以逗号分隔的主题列表,允许该应用程序向该主题列表发布
- 如果在
publishingScopes中未指定任何内容(默认行为),则所有应用程序都可以向所有主题发布 - 要拒绝应用程序向任何主题发布的能力,请将主题列表留空(
app1=;app2=topic2) - 例如,
app1=topic1;app2=topic2,topic3;app3=将允许 app1 向 topic1 发布,而不允许向其他主题发布;app2 仅向 topic2 和 topic3 发布;app3 不向任何主题发布
spec.metadata.subscriptionScopes- 以分号分隔的应用程序列表和以逗号分隔的主题列表,允许该应用程序订阅该主题列表
- 如果在
subscriptionScopes中未指定任何内容(默认行为),则所有应用程序都可以订阅所有主题 - 例如,
app1=topic1;app2=topic2,topic3将允许 app1 仅订阅 topic1,app2 订阅 topic2 和 topic3
spec.metadata.allowedTopics- 所有应用程序的允许主题的逗号分隔列表。
- 如果未设置
allowedTopics(默认行为),则所有主题均有效。如果存在subscriptionScopes和publishingScopes,它们仍然生效。 publishingScopes或subscriptionScopes可与allowedTopics结合使用以添加精细限制
spec.metadata.protectedTopics- 所有应用程序的受保护主题的逗号分隔列表。
- 如果某个主题被标记为受保护,则应用程序必须通过
publishingScopes或subscriptionScopes显式授予发布或订阅权限才能发布/订阅该主题。
这些元数据属性可用于所有发布订阅组件。以下示例使用 Redis 作为发布订阅组件。
示例 1:限制主题访问
如果您有包含敏感信息的主题,并且只允许您的应用程序的一个子集发布或订阅这些主题,那么限制哪些应用程序可以发布/订阅主题会很有用。
它还可以用于所有主题,以始终拥有哪些应用程序作为发布者/订阅者使用哪些主题的"真实来源"。
以下是三个应用程序和三个主题的示例:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: "localhost:6379"
- name: redisPassword
value: ""
- name: publishingScopes
value: "app1=topic1;app2=topic2,topic3;app3="
- name: subscriptionScopes
value: "app2=;app3=topic1"
下表显示了允许哪些应用程序向主题发布:
| topic1 | topic2 | topic3 | |
|---|---|---|---|
| app1 | ✅ | ||
| app2 | ✅ | ✅ | |
| app3 |
下表显示了允许哪些应用程序订阅主题:
| topic1 | topic2 | topic3 | |
|---|---|---|---|
| app1 | ✅ | ✅ | ✅ |
| app2 | |||
| app3 | ✅ |
注意:如果未列出应用程序(例如 subscriptionScopes 中的 app1),则允许它订阅所有主题。由于未使用
allowedTopics且 app1 没有任何订阅范围,因此它也可以使用上面未列出的其他主题。
示例 2:限制允许的主题
如果 Dapr 应用程序向主题发送消息,则会创建该主题。在某些情况下,应该控制此主题创建。例如:
- Dapr 应用程序中生成主题名称的 bug 可能导致创建无限数量的主题
- 精简主题名称和总数,并防止主题无限增长
在这些情况下,可以使用 allowedTopics。
以下是三个允许主题的示例:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: "localhost:6379"
- name: redisPassword
value: ""
- name: allowedTopics
value: "topic1,topic2,topic3"
所有应用程序都可以使用这些主题,但只能使用这些主题,不允许使用其他主题。
示例 3:结合 allowedTopics 和范围
有时您希望结合这两种范围,从而只有一组固定的允许主题并指定对特定应用程序的范围。
以下是三个应用程序和两个主题的示例:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: "localhost:6379"
- name: redisPassword
value: ""
- name: allowedTopics
value: "A,B"
- name: publishingScopes
value: "app1=A"
- name: subscriptionScopes
value: "app1=;app2=A"
注意:第三个应用程序未列出,因为如果应用程序未在范围内指定,则允许它使用所有主题。
下表显示了允许哪个应用程序向主题发布:
| A | B | C | |
|---|---|---|---|
| app1 | ✅ | ||
| app2 | ✅ | ✅ | |
| app3 | ✅ | ✅ |
下表显示了允许哪个应用程序订阅主题:
| A | B | C | |
|---|---|---|---|
| app1 | |||
| app2 | ✅ | ||
| app3 | ✅ | ✅ |
示例 4:将主题标记为受保护
如果您的主题涉及敏感数据,则必须在 publishingScopes 和 subscriptionScopes 中显式列出每个新应用程序,以确保它无法从该主题读取或向其写入。或者,您可以将主题指定为"受保护"(使用 protectedTopics)并仅授予真正需要它的特定应用程序的访问权限。
以下是三个应用程序和三个主题的示例,其中两个是受保护的:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: "localhost:6379"
- name: redisPassword
value: ""
- name: protectedTopics
value: "A,B"
- name: publishingScopes
value: "app1=A,B;app2=B"
- name: subscriptionScopes
value: "app1=A,B;app2=B"
在上面的示例中,主题 A 和 B 被标记为受保护。因此,即使 app3 未在 publishingScopes 或 subscriptionScopes 下列出,它也无法与这些主题交互。
下表显示了允许哪个应用程序向主题发布:
| A | B | C | |
|---|---|---|---|
| app1 | ✅ | ✅ | |
| app2 | ✅ | ||
| app3 | ✅ |
下表显示了允许哪个应用程序订阅主题:
| A | B | C | |
|---|---|---|---|
| app1 | ✅ | ✅ | |
| app2 | ✅ | ||
| app3 | ✅ |
演示
后续步骤
- 了解如何使用多个命名空间配置发布订阅组件
- 了解消息存活时间
- 发布订阅组件 列表
- 阅读 API 参考
1.2.11 - 消息生存时间 (TTL)
简介
Dapr 支持每条消息设置生存时间(TTL)。这意味着应用程序可以为每条消息设置生存时间,订阅者在消息过期后将不会收到这些消息。
所有 Dapr 发布订阅组件都兼容消息 TTL,因为 Dapr 在运行时内部处理 TTL 逻辑。只需在发布消息时设置 ttlInSeconds 元数据即可。
对于某些组件(如 Kafka),可以通过 retention.ms 在主题级别配置生存时间,详见 文档。通过 Dapr 的消息 TTL,使用 Kafka 的应用程序现在可以除了按主题设置外,还可以为每条消息设置生存时间。
原生消息 TTL 支持
当发布订阅组件原生支持消息生存时间时,Dapr 直接转发 TTL 配置,不添加任何额外逻辑,保持可预测的行为。这在组件以不同方式处理过期消息时很有帮助。例如,使用 Azure Service Bus 时,过期消息会被存储在死信队列中,而不会被简单删除。
注意
你也可以在创建消息代理时设置消息 TTL。请查看你正在使用的组件的具体特性,以确定这是否适合你的场景。支持的组件
Azure Service Bus
Azure Service Bus 支持实体级别生存时间。这意味着消息具有默认生存时间,但也可以在发布时设置为更短的时间范围。Dapr 会传播消息的 TTL 元数据,并让 Azure Service Bus 直接处理过期。
非 Dapr 订阅者
如果消息被不使用 Dapr 的订阅者消费,过期消息不会被自动丢弃,因为过期处理是由 Dapr 运行时在 Dapr 边车接收消息时执行的。但是,订阅者可以通过添加逻辑来处理云事件中的 expiration 属性来以编程方式丢弃过期消息,该属性遵循 RFC3339 格式。
当非 Dapr 订阅者使用原生处理消息 TTL 的组件(如 Azure Service Bus)时,它们不会收到过期的消息。这种情况下,不需要额外逻辑。
示例
消息 TTL 可以作为发布请求的一部分在元数据中设置:
[%] (tab "Python SDK" %%) ```python from dapr.clients import DaprClient with DaprClient() as d: req_data = { 'order-number': '345' } # Create a typed message with content type and body resp = d.publish_event( pubsub_name='pubsub', topic='TOPIC_A', data=json.dumps(req_data), publish_metadata={'ttlInSeconds': '120'} ) # Print the request print(req_data, flush=True) ```
curl -X "POST" http://localhost:3500/v1.0/publish/pubsub/TOPIC_A?metadata.ttlInSeconds=120 -H "Content-Type: application/json" -d '{"order-number": "345"}'
<?php
require_once __DIR__.'/vendor/autoload.php';
$app = \Dapr\App::create();
$app->run(function(\DI\FactoryInterface $factory) {
$publisher = $factory->make(\Dapr\PubSub\Publish::class, ['pubsub' => 'pubsub']);
$publisher->topic('TOPIC_A')->publish('data', ['ttlInSeconds' => '120']);
});
[%] (/tab “PHP SDK” %%) [%] (/tabpane >)
有关发布订阅 API 的参考,请参阅本指南。
下一步
- 了解主题作用域
- 了解如何配置多命名空间的发布订阅组件
- 发布订阅组件列表
- 阅读 API 参考
1.2.12 - 批量发布和订阅消息
借助批量发布和订阅 API,您可以在单个请求中发布和订阅多条消息。在编写需要发送或接收大量消息的应用程序时,使用批量操作可以通过减少 Dapr 边车、应用程序与底层发布/订阅代理之间的总体请求数量,从而实现高吞吐量。
批量发布消息
批量发布消息的限制
批量发布 API 允许您在单个请求中向主题发布多条消息。它是非事务性的,也就是说,在单个批量请求中,部分消息可能成功,部分消息可能失败。如果有任何消息发布失败,批量发布操作会返回失败消息列表。
批量发布操作也不保证消息的任何顺序。
示例
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.BulkPublishResponse;
import io.dapr.client.domain.BulkPublishResponseFailedEntry;
import java.util.ArrayList;
import java.util.List;
class BulkPublisher {
private static final String PUBSUB_NAME = "my-pubsub-name";
private static final String TOPIC_NAME = "topic-a";
public void publishMessages() {
try (DaprClient client = (new DaprClientBuilder()).build()) {
// 创建要发布的消息列表
List<String> messages = new ArrayList<>();
for (int i = 0; i < 10; i++) {
String message = String.format("This is message #%d", i);
messages.add(message);
}
// 使用批量发布 API 发布消息列表
BulkPublishResponse<String> res = client.publishEvents(PUBSUB_NAME, TOPIC_NAME, "text/plain", messages).block();
}
}
}
import { DaprClient } from "@dapr/dapr";
const pubSubName = "my-pubsub-name";
const topic = "topic-a";
async function start() {
const client = new DaprClient();
// 向主题发布多条消息。
await client.pubsub.publishBulk(pubSubName, topic, ["message 1", "message 2", "message 3"]);
// 使用显式的批量发布消息向主题发布多条消息。
const bulkPublishMessages = [
{
entryID: "entry-1",
contentType: "application/json",
event: { hello: "foo message 1" },
},
{
entryID: "entry-2",
contentType: "application/cloudevents+json",
event: {
specversion: "1.0",
source: "/some/source",
type: "example",
id: "1234",
data: "foo message 2",
datacontenttype: "text/plain"
},
},
{
entryID: "entry-3",
contentType: "text/plain",
event: "foo message 3",
},
];
await client.pubsub.publishBulk(pubSubName, topic, bulkPublishMessages);
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
using System;
using System.Collections.Generic;
using Dapr.Client;
const string PubsubName = "my-pubsub-name";
const string TopicName = "topic-a";
IReadOnlyList<object> BulkPublishData = new List<object>() {
new { Id = "17", Amount = 10m },
new { Id = "18", Amount = 20m },
new { Id = "19", Amount = 30m }
};
using var client = new DaprClientBuilder().Build();
var res = await client.BulkPublishEventAsync(PubsubName, TopicName, BulkPublishData);
if (res == null) {
throw new Exception("null response from dapr");
}
if (res.FailedEntries.Count > 0)
{
Console.WriteLine("Some events failed to be published!");
foreach (var failedEntry in res.FailedEntries)
{
Console.WriteLine("EntryId: " + failedEntry.Entry.EntryId + " Error message: " +
failedEntry.ErrorMessage);
}
}
else
{
Console.WriteLine("Published all events!");
}
import requests
import json
base_url = "http://localhost:3500/v1.0/publish/bulk/{}/{}"
pubsub_name = "my-pubsub-name"
topic_name = "topic-a"
payload = [
{
"entryId": "ae6bf7c6-4af2-11ed-b878-0242ac120002",
"event": "first text message",
"contentType": "text/plain"
},
{
"entryId": "b1f40bd6-4af2-11ed-b878-0242ac120002",
"event": {
"message": "second JSON message"
},
"contentType": "application/json"
}
]
response = requests.post(base_url.format(pubsub_name, topic_name), json=payload)
print(response.status_code)
package main
import (
"fmt"
"strings"
"net/http"
"io/ioutil"
)
const (
pubsubName = "my-pubsub-name"
topicName = "topic-a"
baseUrl = "http://localhost:3500/v1.0/publish/bulk/%s/%s"
)
func main() {
url := fmt.Sprintf(baseUrl, pubsubName, topicName)
method := "POST"
payload := strings.NewReader(`[
{
"entryId": "ae6bf7c6-4af2-11ed-b878-0242ac120002",
"event": "first text message",
"contentType": "text/plain"
},
{
"entryId": "b1f40bd6-4af2-11ed-b878-0242ac120002",
"event": {
"message": "second JSON message"
},
"contentType": "application/json"
}
]`)
client := &http.Client {}
req, _ := http.NewRequest(method, url, payload)
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
// ...
}
curl -X POST http://localhost:3500/v1.0/publish/bulk/my-pubsub-name/topic-a \
-H 'Content-Type: application/json' \
-d '[
{
"entryId": "ae6bf7c6-4af2-11ed-b878-0242ac120002",
"event": "first text message",
"contentType": "text/plain"
},
{
"entryId": "b1f40bd6-4af2-11ed-b878-0242ac120002",
"event": {
"message": "second JSON message"
},
"contentType": "application/json"
},
]'
Invoke-RestMethod -Method Post -ContentType 'application/json' -Uri 'http://localhost:3500/v1.0/publish/bulk/my-pubsub-name/topic-a' `
-Body '[
{
"entryId": "ae6bf7c6-4af2-11ed-b878-0242ac120002",
"event": "first text message",
"contentType": "text/plain"
},
{
"entryId": "b1f40bd6-4af2-11ed-b878-0242ac120002",
"event": {
"message": "second JSON message"
},
"contentType": "application/json"
},
]'
批量订阅消息
批量订阅 API 允许您在单个请求中从主题订阅多条消息。 正如我们从如何操作:发布和订阅主题中了解到的,有三种方式订阅主题:
- 声明式 - 订阅在外部文件中定义。
- 编程式 - 订阅在代码中定义。
- 流式 - 不支持批量订阅,因为消息会发送到处理程序代码。
要批量订阅主题,我们只需要使用 bulkSubscribe 规范属性,如下所示:
apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
name: order-pub-sub
spec:
topic: orders
routes:
default: /checkout
pubsubname: order-pub-sub
bulkSubscribe:
enabled: true
maxMessagesCount: 100
maxAwaitDurationMs: 40
scopes:
- orderprocessing
- checkout
在上面的示例中,bulkSubscribe 是_可选的_。如果您使用 bulkSubscribe,则:
enabled是必需的,用于在此主题上启用或禁用批量订阅- 您可以选择配置在批量消息中传递的最大消息数量(
maxMessagesCount)。 对于不支持批量订阅的组件,maxMessagesCount的默认值为 100,即应用程序与 Dapr 之间的默认批量事件。请参阅组件如何处理批量消息的发布和订阅。 如果组件支持批量订阅,则可以在该组件的文档中找到此参数的默认值。 - 您可以选择在批量消息发送到应用程序之前提供等待的最大持续时间(
maxAwaitDurationMs)。 对于不支持批量订阅的组件,maxAwaitDurationMs的默认值为 1000,即应用程序与 Dapr 之间的默认批量事件。请参阅组件如何处理批量消息的发布和订阅。 如果组件支持批量订阅,则可以在该组件的文档中找到此参数的默认值。
应用程序会收到与批量消息中每个条目(单独的消息)关联的 EntryId。应用程序必须使用此 EntryId 来传达该特定条目的状态。如果应用程序未能通知 EntryId 状态,则视为 RETRY。
需要发送一个 JSON 编码的负载正文,其中包含每个条目的处理状态:
{
"statuses":
[
{
"entryId": "<entryId1>",
"status": "<status>"
},
{
"entryId": "<entryId2>",
"status": "<status>"
}
]
}
可能的状态值:
| 状态 | 描述 |
|---|---|
SUCCESS | 消息处理成功 |
RETRY | 消息将由 Dapr 重试 |
DROP | 记录警告并丢弃消息 |
有关响应的更多见解,请参阅批量订阅的预期 HTTP 响应。
示例
以下代码示例演示如何使用批量订阅。
import io.dapr.Topic;
import io.dapr.client.domain.BulkSubscribeAppResponse;
import io.dapr.client.domain.BulkSubscribeAppResponseEntry;
import io.dapr.client.domain.BulkSubscribeAppResponseStatus;
import io.dapr.client.domain.BulkSubscribeMessage;
import io.dapr.client.domain.BulkSubscribeMessageEntry;
import io.dapr.client.domain.CloudEvent;
import io.dapr.springboot.annotations.BulkSubscribe;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import reactor.core.publisher.Mono;
class BulkSubscriber {
@BulkSubscribe()
// @BulkSubscribe(maxMessagesCount = 100, maxAwaitDurationMs = 40)
@Topic(name = "topicbulk", pubsubName = "orderPubSub")
@PostMapping(path = "/topicbulk")
public Mono<BulkSubscribeAppResponse> handleBulkMessage(
@RequestBody(required = false) BulkSubscribeMessage<CloudEvent<String>> bulkMessage) {
return Mono.fromCallable(() -> {
List<BulkSubscribeAppResponseEntry> entries = new ArrayList<BulkSubscribeAppResponseEntry>();
for (BulkSubscribeMessageEntry<?> entry : bulkMessage.getEntries()) {
try {
CloudEvent<?> cloudEvent = (CloudEvent<?>) entry.getEvent();
System.out.printf("Bulk Subscriber got: %s\n", cloudEvent.getData());
entries.add(new BulkSubscribeAppResponseEntry(entry.getEntryId(), BulkSubscribeAppResponseStatus.SUCCESS));
} catch (Exception e) {
e.printStackTrace();
entries.add(new BulkSubscribeAppResponseEntry(entry.getEntryId(), BulkSubscribeAppResponseStatus.RETRY));
}
}
return new BulkSubscribeAppResponse(entries);
});
}
}
import { DaprServer } from "@dapr/dapr";
const pubSubName = "orderPubSub";
const topic = "topicbulk";
const daprHost = process.env.DAPR_HOST || "127.0.0.1";
const daprPort = process.env.DAPR_HTTP_PORT || "3502";
const serverHost = process.env.SERVER_HOST || "127.0.0.1";
const serverPort = process.env.APP_PORT || "5001";
async function start() {
const server = new DaprServer({
serverHost,
serverPort,
clientOptions: {
daprHost,
daprPort,
},
});
// 使用默认配置向主题发布多条消息。
await client.pubsub.bulkSubscribeWithDefaultConfig(pubSubName, topic, (data) => console.log("Subscriber received: " + JSON.stringify(data)));
// 使用特定的 maxMessagesCount 和 maxAwaitDurationMs 向主题发布多条消息。
await client.pubsub.bulkSubscribeWithConfig(pubSubName, topic, (data) => console.log("Subscriber received: " + JSON.stringify(data)), 100, 40);
}
using Microsoft.AspNetCore.Mvc;
using Dapr.AspNetCore;
using Dapr;
namespace DemoApp.Controllers;
[ApiController]
[Route("[controller]")]
public class BulkMessageController : ControllerBase
{
private readonly ILogger<BulkMessageController> logger;
public BulkMessageController(ILogger<BulkMessageController> logger)
{
this.logger = logger;
}
[BulkSubscribe("messages", 10, 10)]
[Topic("pubsub", "messages")]
public ActionResult<BulkSubscribeAppResponse> HandleBulkMessages([FromBody] BulkSubscribeMessage<BulkMessageModel<BulkMessageModel>> bulkMessages)
{
List<BulkSubscribeAppResponseEntry> responseEntries = new List<BulkSubscribeAppResponseEntry>();
logger.LogInformation($"Received {bulkMessages.Entries.Count()} messages");
foreach (var message in bulkMessages.Entries)
{
try
{
logger.LogInformation($"Received a message with data '{message.Event.Data.MessageData}'");
responseEntries.Add(new BulkSubscribeAppResponseEntry(message.EntryId, BulkSubscribeAppResponseStatus.SUCCESS));
}
catch (Exception e)
{
logger.LogError(e.Message);
responseEntries.Add(new BulkSubscribeAppResponseEntry(message.EntryId, BulkSubscribeAppResponseStatus.RETRY));
}
}
return new BulkSubscribeAppResponse(responseEntries);
}
public class BulkMessageModel
{
public string MessageData { get; set; }
}
}
目前,您只能在 Python 中使用 HTTP 客户端进行批量订阅。
import json
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/dapr/subscribe', methods=['GET'])
def subscribe():
# 定义批量订阅配置
subscriptions = [{
"pubsubname": "pubsub",
"topic": "TOPIC_A",
"route": "/checkout",
"bulkSubscribe": {
"enabled": True,
"maxMessagesCount": 3,
"maxAwaitDurationMs": 40
}
}]
print('Dapr pub/sub is subscribed to: ' + json.dumps(subscriptions))
return jsonify(subscriptions)
# 定义处理传入消息的端点
@app.route('/checkout', methods=['POST'])
def checkout():
messages = request.json
print(messages)
for message in messages:
print(f"Received message: {message}")
return json.dumps({'success': True}), 200, {'ContentType': 'application/json'}
if __name__ == '__main__':
app.run(port=5000)
组件如何处理批量消息的发布和订阅
对于事件发布/订阅,涉及两种类型的网络传输。
- 从/到应用程序到/从Dapr。
- 从/到Dapr到/从Pubsub 代理。
这些是可以进行优化的机会。当优化时,会发出批量请求,从而减少总体调用次数,因此提高吞吐量并提供更好的延迟。
在启用批量发布和/或批量订阅时,应用程序与 Dapr 边车之间的通信(上面的第 1 点)对所有组件都进行了优化。
从 Dapr 边车到发布/订阅代理的优化取决于许多因素,例如:
- 代理必须本身支持批量发布/订阅
- Dapr 组件必须更新以支持代理提供的批量 API 的使用
目前,以下组件已更新以支持此级别的优化:
| 组件 | 批量发布 | 批量订阅 |
|---|---|---|
| Kafka | 是 | 是 |
| Azure Servicebus | 是 | 是 |
| Azure Eventhubs | 是 | 是 |
演示
观看以下关于批量发布/订阅的演示和演示文稿。
KubeCon Europe 2023 演讲
Dapr 社区会议 #77 演讲
相关链接
- 支持的发布/订阅组件列表
- 阅读API 参考
1.3 - 工作流
1.3.1 - 工作流概述
Dapr workflow 使开发者能够以可靠的方式编写业务逻辑和集成。 由于 Dapr workflows 是有状态的,因此支持长时间运行和容错的应用程序,非常适合编排微服务。 Dapr workflow 可与其他 Dapr 构建块无缝协作,例如服务调用、发布订阅、状态管理和绑定。
持久化、有弹性的 Dapr Workflow 能力:
- 提供内置的工作流运行时来驱动 Dapr Workflow 执行。
- 提供用于以任何语言编写工作流的 SDK。
- 提供 HTTP 和 gRPC API 用于管理工作流(启动、查询、暂停/恢复、触发事件、终止、清除)。

Dapr Workflow 可以执行的一些示例场景包括:
- 涉及库存管理、支付系统和物流服务之间编排的订单处理。
- 跨多个部门和参与者协调任务的 HR 入职工作流。
- 在全国连锁餐厅中编排数字菜单更新的推出。
- 涉及基于 API 的分类和存储的图像处理工作流。
功能
工作流和活动
使用 Dapr Workflow,你可以编写活动,然后在工作流中编排这些活动。 工作流活动具有以下特点:
- 工作流中的基本工作单元
- 用于调用其他(Dapr)服务、与状态存储和发布订阅代理交互。
- 用于调用外部第三方服务。
子工作流
除了活动之外,你还可以编写工作流来调度其他工作流作为子工作流。 子工作流有其自己的实例 ID、历史记录和状态,独立于启动它的父工作流,但终止父工作流会终止其创建的所有子工作流除外。 子工作流还支持自动重试策略。
多应用程序工作流
多应用程序工作流使你能够编排跨多个应用程序的复杂业务流程。 这允许工作流在不同应用程序中调用活动或启动子工作流,在保持 Dapr 工作流引擎的安全、可靠性和持久性保证的同时分配工作流执行。
定时器和提醒
与 Dapr actors 一样,你可以为任意时间范围安排类似提醒的持久延迟。
用于管理工作流的 Workflow HTTP 调用
当你使用工作流代码创建应用程序并使用 Dapr 运行它时,你可以调用驻留在该应用程序中的特定工作流。 每个独立的工作流可以:
- 通过 POST 请求启动或终止
- 通过 POST 请求触发传递命名事件
- 通过 POST 请求暂停然后恢复
- 通过 POST 请求从状态存储中清除
- 通过 GET 请求查询工作流状态
工作流模式
Dapr Workflow 简化了微服务架构中复杂的有状态协调需求。 以下部分描述了可以从 Dapr Workflow 中受益的几种应用程序模式。
详细了解不同类型的工作流模式
工作流 SDK
Dapr Workflow authoring SDK 是特定语言的 SDK,包含用于实现工作流逻辑的类型和函数。 工作流逻辑存在于你的应用程序中,并通过 gRPC 流由 Dapr 边车中运行的 Dapr Workflow 引擎编排。
支持的 SDK
你可以使用以下 SDK 编写工作流。
| 语言堆栈 | 包 |
|---|---|
| Python | dapr-ext-workflow |
| JavaScript | DaprWorkflowClient |
| .NET | Dapr.Workflow |
| Java | io.dapr.workflows |
| Go | workflow |
试用工作流
快速入门和教程
想测试工作流吗?请完成以下快速入门和教程来查看工作流的实际应用:
| 快速入门/教程 | 描述 |
|---|---|
| Workflow 快速入门 | 运行一个包含四个工作流活动的工作流应用程序,了解 Dapr Workflow 的实际应用 |
| Workflow Python SDK 示例 | 了解如何使用 Python dapr-ext-workflow 包创建 Dapr Workflow 并调用它。 |
| Workflow JavaScript SDK 示例 | 了解如何使用 JavaScript SDK 创建 Dapr Workflow 并调用它。 |
| Workflow .NET SDK 示例 | 了解如何使用 ASP.NET Core Web API 创建 Dapr Workflow 并调用它。 |
| Workflow Java SDK 示例 | 了解如何使用 Java io.dapr.workflows 包创建 Dapr Workflow 并调用它。 |
| Workflow Go SDK 示例 | 了解如何使用 Go workflow 包创建 Dapr Workflow 并调用它。 |
直接在你的应用中开始使用工作流
想跳过快速入门吗?没问题。你可以直接在你的应用程序中试用工作流构建块。 安装 Dapr后,你可以开始使用工作流,从如何编写工作流开始。
管理工作流
Dapr 通过 HTTP API 和 CLI 提供全面的工作流管理能力。
工作流生命周期操作
启动工作流
dapr workflow run MyWorkflow --app-id myapp --input '{"key": "value"}'
监控工作流
# 列出给定应用程序的活跃工作流
dapr workflow list --app-id myapp --filter-status RUNNING
# 查看执行历史
dapr workflow history <instance-id> --app-id myapp
控制工作流
# 暂停、恢复或终止
dapr workflow suspend <instance-id> --app-id myapp
dapr workflow resume <instance-id> --app-id myapp
dapr workflow terminate <instance-id> --app-id myapp
维护操作
# 清除已完成的工作流
dapr workflow purge --app-id myapp --all-older-than 720h
详细说明请参阅如何:管理工作流。
限制
- **状态存储:**你只能使用支持工作流的状态存储,如所述。
- Azure Cosmos DB 有有效负载和工作流复杂性限制。
- AWS DynamoDB 有工作流复杂性限制。
观看演示
下一步
工作流功能和概念 >>相关链接
1.3.2 - 功能与概念
现在您已经对工作流构建块有了整体了解,让我们深入探讨 Dapr 工作流引擎和 SDK 提供的功能和概念。 Dapr 工作流公开了几个核心功能和概念,这些在所有支持的语言中都是通用的。
注意
有关工作流状态管理方式的更多信息,请参阅工作流架构指南。工作流
Dapr 工作流是您编写的函数,用于定义要按特定顺序执行的一系列任务。 Dapr 工作流引擎负责任务的调度和执行,包括管理失败和重试。 如果托管工作流的应用程序扩展到多台机器上,工作流引擎会在多台机器之间对工作流及其任务的执行进行负载均衡。
工作流可以调度多种不同类型的任务,包括:
工作流实例管理
查询工作流状态
您可以使用 CLI 查询工作流实例:
# 查找所有运行中的工作流
dapr workflow list --app-id myapp --filter-status RUNNING
# 按名称查找工作流
dapr workflow list --app-id myapp --filter-name OrderProcessing
# 查找最近的工作流(过去 2 小时)
dapr workflow list --app-id myapp --filter-max-age 2h
# 获取详细的 JSON 输出
dapr workflow list --app-id myapp --output json
工作流历史
查看完整的执行历史:
dapr workflow history wf-12345 --app-id myapp --output json
这将显示所有事件、活动和状态转换。
外部事件
通过 CLI 触发事件
dapr workflow raise-event wf-12345/ApprovalReceived \
--app-id myapp \
--input '{"approved": true, "comments": "Approved by manager"}'
工作流暂停与恢复
使用 CLI
# 暂停以进行手动干预
dapr workflow suspend wf-12345 \
--app-id myapp \
--reason "Awaiting customer response"
# 准备就绪后恢复
dapr workflow resume wf-12345 \
--app-id myapp \
--reason "Customer responded"
工作流标识
每个您定义的工作流都有一个类型名称,而工作流的每次单独执行都需要一个唯一的_实例 ID_。工作流实例 ID 可以由您的应用程序代码生成,这在工作流对应于文档或作业等业务实体时非常有用;也可以是自动生成的 UUID。工作流的实例 ID 可用于调试,也可用于使用 Workflow API 管理工作流。
任何给定时刻只能存在一个具有给定 ID 的工作流实例。但是,如果某个工作流实例完成或失败,其 ID 可以被新的工作流实例重用。但请注意,新的工作流实例将有效地替换配置的状态存储中的旧实例。
工作流重放
Dapr 工作流使用一种称为事件溯源的技术来维护其执行状态。工作流引擎不存储工作流的当前状态快照,而是管理一个仅追加的历史事件日志,记录描述工作流所执行的各种步骤。当使用工作流 SDK 时,这些历史事件会在工作流"等待"调度任务的结果时自动存储。
当工作流"等待"调度任务时,它会从内存中卸载,直到任务完成。一旦任务完成,工作流引擎会调度工作流函数再次运行。第二次工作流函数执行称为_重放_。
当工作流函数被重放时,它会从头开始运行。但是,当它遇到已经完成的任务时,工作流引擎不会再次调度该任务,而是:
- 将已完成任务的存储结果返回给工作流。
- 继续执行直到下一个"等待"点。
这种"重放"行为会持续进行,直到工作流函数完成或以错误失败。
使用这种重放技术,工作流能够从任何"等待"点恢复执行,就好像它从未从内存中卸载一样。甚至可以恢复前次运行中的局部变量值,而工作流引擎无需知道它们存储了什么数据。这种恢复状态的能力使 Dapr 工作流具有_持久性_和_容错性_。
注意
此处描述的工作流重放行为要求工作流函数代码具有_确定性_。确定性工作流函数在提供完全相同的输入时会执行完全相同的操作。了解更多关于确定性工作流代码的限制。无限循环与永生工作流
如工作流重放章节所述,工作流维护其所有操作的只写事件溯源历史日志。为了避免资源失控使用,工作流必须限制其调度的操作数量。例如,确保您的工作流不会:
- 在其实现中使用无限循环
- 调度数千个任务。
如果工作流可能需要调度大量任务,您可以使用以下两种技术来编写工作流:
使用 continue-as-new API: 每个工作流 SDK 都暴露了一个 continue-as-new API,工作流可以调用它来使用新的输入和历史重新启动自身。continue-as-new API 特别适合实现"永生工作流",如监控代理,否则将使用类似
while (true)的构造来实现。使用 continue-as-new 是保持工作流历史记录规模较小的好方法。continue-as-new API 会截断现有历史记录,用新的历史记录替换它。
使用子工作流: 每个工作流 SDK 都暴露了用于创建子工作流的 API。子工作流的行为与任何其他工作流一样,只是它由父工作流调度。子工作流具有:
- 自己的历史记录
- 跨多台机器分发工作流函数执行的好处。
如果工作流需要调度数千个或更多任务,建议将这些任务分布在子工作流中,以避免单个工作流的历史记录规模过大。
更新工作流代码
由于工作流是长期运行且持久的,更新工作流代码必须非常小心。如工作流确定性限制章节所述,工作流代码必须具有确定性。如果系统中存在任何未完成的工作流实例,则对工作流代码的更新必须保持这种确定性。否则,对工作流代码的更新可能导致这些工作流下次执行时出现运行时故障。
工作流活动
工作流活动是工作流中的基本工作单元,也是业务流程中被编排的任务。例如,您可能创建一个工作流来处理订单。任务可能涉及检查库存、向客户收费和创建发货。每个任务都是一个单独的活动。这些活动可以串行执行、并行执行或两者结合执行。
与工作流不同,活动对您可以在其中执行的工作类型没有限制。活动经常用于发出网络调用或运行 CPU 密集型操作。活动也可以将数据返回给工作流。
Dapr 工作流引擎保证每个被调用的活动作为工作流执行的一部分至少执行一次。由于活动只保证至少一次执行,建议尽可能将活动逻辑实现为幂等的。
子工作流
除了活动,工作流还可以将其他工作流调度为_子工作流_。子工作流有自己的实例 ID、历史记录和状态,独立于启动它的父工作流。
子工作流有许多好处:
- 您可以将大型工作流拆分为一系列较小的子工作流,使代码更易于维护。
- 您可以跨多个计算节点并发分发工作流逻辑,如果您的工作流逻辑需要协调大量任务,这将非常有用。
- 您可以通过保持父工作流的历史记录较小来减少内存使用和 CPU 开销。
子工作流的返回值就是其输出。如果子工作流因异常失败,则该异常会像活动任务因异常失败一样被暴露到父工作流。子工作流也支持自动重试策略。
终止父工作流会终止由该工作流实例创建的所有子工作流。详见终止工作流 API。
持久化定时器
Dapr 工作流允许您为任意时间范围安排类似提醒的持久化延迟,包括分钟、天甚至数年。这些_持久化定时器_可以由工作流调度以实现简单延迟或为其他异步任务设置临时超时。更具体地说,持久化定时器可以设置为在特定日期触发或在指定持续时间后触发。持久化定时器的最大持续时间没有限制,它们在内部由内部 actor 提醒器支持。例如,跟踪某项服务 30 天免费订阅的工作流可以使用在创建工作流 30 天后触发的持久化定时器来实现。工作流在等待持久化定时器触发时可以安全地从内存中卸载。
注意
工作流创作 SDK 中的一些 API 可能会在内部调度持久化定时器以实现内部超时行为。重试策略
工作流支持针对活动和子工作流的持久化重试策略。工作流重试策略与 Dapr 弹性策略 在以下方面有所不同:
- 工作流重试策略由工作流作者在代码中配置,而 Dapr 弹性策略由应用程序操作员在 YAML 中配置。
- 工作流重试策略是持久的,在应用程序重启后保持其状态,而 Dapr 弹性策略不是持久的,必须在应用程序重启后重新应用。
- 工作流重试策略由活动和子工作流中未处理的错误/异常触发,而 Dapr 弹性策略由操作超时和连接故障触发。
重试在内部使用持久化定时器实现。这意味着工作流在等待重试触发时可以安全地从内存中卸载,从而节省系统资源。这也意味着重试之间的延迟可以任意长,包括分钟、小时甚至数天。
注意
重试策略执行的操作会保存到工作流的历史记录中。必须注意不要在工作流已经执行后更改重试策略的行为。否则,工作流在重放时可能会出现意外行为。请参阅有关更新工作流代码的说明以获取更多信息。可以同时使用工作流重试策略和 Dapr 弹性策略。例如,如果工作流活动使用 Dapr 客户端调用服务,则 Dapr 客户端使用配置好的弹性策略。详见快速入门:服务间弹性以获取更多信息和示例。但是,如果活动本身因任何原因失败,包括耗尽弹性策略的重试次数,则工作流的弹性策略会介入。
注意
同时使用工作流重试策略和弹性策略可能导致意外行为。例如,如果工作流活动耗尽其配置的重试策略,工作流引擎仍会根据工作流重试策略重试该活动。这可能导致活动被重试的次数超出预期。由于工作流重试策略是在代码中配置的,确切的开发者体验可能因工作流 SDK 版本而异。一般来说,工作流重试策略可以使用以下参数进行配置:
| 参数 | 描述 |
|---|---|
| 最大尝试次数 | 执行活动或子工作流的最大次数。如果设置为 0,则不会进行任何尝试。 |
| 首次重试间隔 | 等待第一次重试的时间量。 |
| 退避系数 | 用于确定退避增加速率的系数。例如,系数为 2 会使每次后续重试的等待时间翻倍。 |
| 最大重试间隔 | 每次后续重试之前等待的最大时间量。如果设置为 0,则不会发生重试。 |
| 重试超时 | 重试的全局超时时间,无论配置的最大尝试次数如何。此超时到期后,将不再尝试执行活动。 |
外部事件
有时工作流需要等待由外部系统触发的事件。例如,审批工作流可能要求人工在工作流处理订单时明确批准订单请求(如果总成本超过某个阈值)。另一个例子是琐事游戏编排工作流,在等待所有参与者提交他们对琐事问题的答案时暂停。这些中途执行的输入称为_外部事件_。
外部事件具有_名称_和_有效载荷_,并被传递到单个工作流实例。工作流可以创建"等待外部事件"任务来订阅外部事件,并_等待_这些任务以阻塞执行直到收到事件。然后工作流可以读取这些事件的有效载荷,并决定下一步采取什么行动。外部事件可以串行或并行处理。外部事件可以由其他工作流或工作流代码触发。
工作流也可以等待多个相同名称的外部事件信号,在这种情况下,它们会以先进先出(FIFO)的方式被分派到相应的工作流任务。如果工作流收到外部事件信号但尚未创建"等待外部事件"任务,该事件将被保存到工作流的历史记录中,并在工作流请求该事件后立即被消费。
了解更多关于与外部系统交互的信息。
清除
工作流状态可以从状态存储中清除,清除其所有历史记录并移除与特定工作流实例相关的所有元数据。清除功能用于已运行到 COMPLETED、FAILED 或 TERMINATED 状态的工作流。
在 workflow API 参考指南 中了解更多。
版本控制
工作流代码是长期运行的,必须在更新期间保持确定性。有关修补和命名工作流版本控制的详细信息,请参阅工作流版本控制。
限制
工作流确定性与代码约束
为了利用工作流重放技术,您的工作流代码需要具有确定性。为了使您的工作流代码具有确定性,您可能需要解决一些限制。
工作流函数必须调用确定性 API
生成随机数、随机 UUID 或获取当前日期的 API 是_非确定性的_。要解决此限制,您可以:
- 在活动函数中使用这些 API,或
- (推荐)使用 SDK 提供的内置等效 API。例如,每个创作 SDK 都提供了一种以确定性方式获取当前时间的 API。
例如,不要这样做:
// 不要这样做!
DateTime currentTime = DateTime.UtcNow;
Guid newIdentifier = Guid.NewGuid();
string randomString = GetRandomString();
// 不要这样做!
Instant currentTime = Instant.now();
UUID newIdentifier = UUID.randomUUID();
String randomString = getRandomString();
// 不要这样做!
const currentTime = new Date();
const newIdentifier = uuidv4();
const randomString = getRandomString();
// 不要这样做!
const currentTime = time.Now()
这样做:
// 这样做!!
DateTime currentTime = context.CurrentUtcDateTime;
Guid newIdentifier = context.NewGuid();
string randomString = await context.CallActivityAsync<string>(nameof("GetRandomString")); //使用 "nameof" 以防止指定应用程序中不存在的活动名称
// 这样做!!
Instant currentTime = context.getCurrentInstant();
Guid newIdentifier = context.newGuid();
String randomString = context.callActivity(GetRandomString.class.getName(), String.class).await();
// 这样做!!
const currentTime = context.getCurrentUtcDateTime();
const randomString = yield context.callActivity(getRandomString);
const currentTime = ctx.CurrentUTCDateTime()
工作流函数必须仅_间接_与外部状态交互。
外部数据包括任何不存储在工作流状态中的数据。工作流不得与全局变量、环境变量、文件系统交互,或进行网络调用。
相反,工作流应该使用工作流输入、活动任务和外部事件处理来_间接_地与外部状态交互。
例如,不要这样做:
// 不要这样做!
string configuration = Environment.GetEnvironmentVariable("MY_CONFIGURATION")!;
string data = await new HttpClient().GetStringAsync("https://example.com/api/data");
// 不要这样做!
String configuration = System.getenv("MY_CONFIGURATION");
HttpRequest request = HttpRequest.newBuilder().uri(new URI("https://postman-echo.com/post")).GET().build();
HttpResponse<String> response = HttpClient.newBuilder().build().send(request, HttpResponse.BodyHandlers.ofString());
// 不要这样做!
// 访问环境变量 (Node.js)
const configuration = process.env.MY_CONFIGURATION;
fetch('https://postman-echo.com/get')
.then(response => response.text())
.then(data => {
console.log(data);
})
.catch(error => {
console.error('Error:', error);
});
// 不要这样做!
resp, err := http.Get("http://example.com/api/data")
这样做:
// 这样做!!
string configuration = workflowInput.Configuration; // 假设的工作流输入参数
string data = await context.CallActivityAsync<string>(nameof("MakeHttpCall"), "https://example.com/api/data");
// 这样做!!
String configuration = ctx.getInput(InputType.class).getConfiguration(); // 假设的工作流输入参数
String data = ctx.callActivity(MakeHttpCall.class, "https://example.com/api/data", String.class).await();
// 这样做!!
const configuration = workflowInput.getConfiguration(); // 假设的工作流输入参数
const data = yield ctx.callActivity(makeHttpCall, "https://example.com/api/data");
// 这样做!!
err := ctx.CallActivity(MakeHttpCallActivity, workflow.ActivityInput("https://example.com/api/data")).Await(&output)
工作流函数必须仅在工作流调度线程上执行。
每种语言 SDK 的实现都要求所有工作流函数操作在与调度函数的同一线程(goroutine 等)上操作。工作流函数必须永远不要:
- 调度后台线程,或
- 使用调度回调函数在另一个线程上运行的 API。
违反此规则可能导致未定义的行为。任何后台处理都应委托给活动任务,活动任务可以串行或并发调度。
例如,不要这样做:
// 不要这样做!
Task t = Task.Run(() => context.CallActivityAsync("DoSomething"));
await context.CreateTimer(5000).ConfigureAwait(false);
// 不要这样做!
new Thread(() -> {
ctx.callActivity(DoSomethingActivity.class.getName()).await();
}).start();
ctx.createTimer(Duration.ofSeconds(5)).await();
不要将 JavaScript 工作流声明为 async。Node.js 运行时不能保证异步函数是确定性的。
// 不要这样做!
go func() {
err := ctx.CallActivity(DoSomething).Await(nil)
}()
err := ctx.CreateTimer(time.Second).Await(nil)
这样做:
// 这样做!!
Task t = context.CallActivityAsync(nameof("DoSomething"));
await context.CreateTimer(5000).ConfigureAwait(true);
// 这样做!!
ctx.callActivity(DoSomethingActivity.class.getName()).await();
ctx.createTimer(Duration.ofSeconds(5)).await();
由于 Node.js 运行时不能保证异步函数是确定性的,请始终将 JavaScript 工作流声明为同步生成器函数。
// 这样做!
task := ctx.CallActivity(DoSomething)
task.Await(nil)
更新工作流代码
确保您对工作流代码的更新保持其确定性。以下是一些可能破坏工作流确定性的代码更新示例:
- 更改工作流函数签名:更改工作流或活动的名称、输入或输出被视为破坏性更改,必须避免。
- 更改工作流任务的数量或顺序:更改工作流任务的数量或顺序会导致工作流的历史记录与工作流代码不再匹配,并可能导致运行时错误或其他意外行为。
要解决这些约束,请使用版本控制指南中描述的工作流版本控制概念来修补和引入新的命名工作流版本,以确定性地将更改合并到您的工作流中。
下一步
工作流模式 >>相关链接
1.3.3 - 工作流版本控制
版本控制
在许多场景中,需要在工作流活跃运行时引入对工作流代码的更改。在这些更改具有非确定性的情况下,需要采用版本控制策略,以便现有工作流可以继续使用原始代码执行,而新工作流则使用更新后的版本。
工作流可以使用两种互补的方法进行版本控制,这两种方法旨在结合使用。它们是:
注意
在任一版本控制策略中,工作流都可能达到 stalled(停滞)状态。这通常发生在滚动部署期间,此时新旧版本的工作流代码同时运行。
当工作流在新副本上启动,但后续尝试在旧副本上重放时,版本化的工作流将变为停滞状态。
工作流可能会在整个滚动部署期间保持停滞状态,直到只有新副本可用为止。一旦停滞的工作流被调度到新副本上,它将继续执行。
如果工作流从未继续,则表示导致停滞的条件仍然存在,需要解决。请参阅下面每种版本控制方法中导致停滞的具体原因。
版本控制解决了什么问题?
为了最好地理解版本控制如何工作以及如何有效地使用它,首先了解它解决了什么以及为什么需要版本控制会很有帮助。工作流必须是确定性的,这意味着每次运行时,它们都会产生与上次完全相同的输出。
这是一个关键概念,因为 Dapr 工作流实现使用事件溯源方法来持久化工作流状态。当在工作流中执行活动和子工作流时,Dapr 会维护一个历史记录,记录这些边界执行的输入和输出。当每个活动完成时,我们会持久化更新后的事件历史,然后从顶部重新调用工作流(这称为"重放")。这一次,当它遇到这些活动或子工作流之一时,它会替换已实现的输出并跳过重新执行,然后重复。
因此,至关重要的是,在工作流运行期间不得随意引入代码,因为当引擎重放工作流时,您的更改可能会导致工作流不再与其历史记录匹配并失败。
修补
修补是我们的两种版本控制方法中的第一种,它允许您在保持重放之间确定性的同时引入对工作流的更改。它的工作原理是允许您通过 if 语句插入与命名标识符关联的分支逻辑。工作流历史记录会在重放工作流时跟踪这些命名标识符的观察位置,从而确保在重放之间遵循一致的逻辑路径。
这使您能够仅对需要调整的特定位置对工作流代码进行有针对性的更改。
要添加补丁,请选择一个稳定的唯一名称来标识该补丁。这可以是描述性的内容,如 use-sms,它描述了补丁更改的内容,也可以是本质不同的内容,如当前时间戳或日期。您使用的具体值并不重要,只要不使用已部署到生产环境中的标识符即可。您将通过添加一个 if 语句来插入补丁,该语句评估工作流是否已应用此补丁——如果是,则使用补丁的代码;如果不是,则采用原始代码路径。
补丁标识符的唯一性要求仅扩展到此工作流;不会评估工作流之间的补丁,因此可以在多个工作流中使用重复的标识符而不会产生冲突。
以下示例演示了这一点,通过检查此工作流实例是否已应用 use-sms 补丁。如果是,则执行 SendSMS 活动;否则,它回退到原始路径并使用 SendEmail 活动。
public sealed class MyWorkflow : Workflow<string, object?>
{
public overrride async Task<object?> RunAsync(WorkflowContext context, string input)
{
// ...
if (context.IsPatched("use-sms"))
{
// 修补后的代码
await context.CallActivityAsync(nameof(SendSMS), input);
}
else {
// 原始代码
await context.CallActivityAsync(nameof(SendEmail), input);
}
return null;
}
}
import "github.com/dapr/durabletask-go/workflow"
func Workflow(ctx *workflow.WorkflowContext) error {
// ...
if ctx.IsPatched("use-sms") {
if err := ctx.CallActivity("SendSMS", ctx.GetInput()).Await(); err != nil {
return err
}
} else {
if err := ctx.CallActivity("SendEmail", ctx.GetInput()).Await(); err != nil {
return err
}
}
// ...
}
from dapr.ext.workflow import DaprWorkflowContext, WorkflowRuntime
wfr = WorkflowRuntime()
@wfr.workflow
def my_workflow(ctx: DaprWorkflowContext, wf_input: str):
# ...
if ctx.is_patched("use-sms"):
yield ctx.call_activity(send_sms)
else:
yield ctx.call_activity(send_email)
# ...
使用这种方法,新的工作流实例将采用修补后的代码路径,但在引入补丁时正在运行的现有工作流将继续使用原始代码路径。补丁检查在首次评估时会记录在工作流实例历史记录中,这意味着可以在工作流的不同位置安全地使用相同的标识符。
同样,至关重要的是,一旦修补后的工作流在生产环境中运行,您就不应再重新使用该标识符。部署后,应假设引擎将以确定性方式处理工作流,但使用以前使用的标识符添加新补丁会破坏该契约:如果您的新代码插入位置早于正在运行的工作流实例评估到的位置,当它重放时,它将采用您的补丁分支,但您的更改可能不再与现有工作流历史记录匹配,并导致工作流无限期停滞(至少直到您删除新补丁或更新它以使用不同的唯一标识符)。
应用于工作流的补丁列表存储在工作流的历史记录中,因此重要的是要注意,您使用的补丁越多,工作流状态历史记录就越大。这将越来越对工作流性能产生负面影响,因为每次重放都需要检索它,并且随着状态的增长,这会增加检索和解析开销。
验证停滞
除了上述重新使用标识符外,使用补丁时还有其他几个原因导致工作流停滞:
- 删除(或重命名)补丁标识符
- 更改补丁的顺序
您可以使用 Dapr CLI 中的工作流命令 来检查停滞的工作流。例如,以下是当工作流停滞时 dapr workflow list 命令的显示结果:
> dapr workflow list
NAME ID STATUS AGE
workflow <ID> STALLED 9m39s
以下是当工作流停滞时 dapr workflow history 命令的显示结果:
> dapr workflow history <ID> -k -o wide
PLAY TYPE NAME TIMESTAMP ELAPSED STATUS DETAILS ROUTER EXECUTION ID ATTRS
0 ExecutionStarted workflow <TIMESTAMP> Age:15.8h RUNNING workflowStart workflows <EXEC_ID> input=2026-01-22T14:44:02.728101
1 OrchestratorStarted <TIMESTAMP> 25.3ms RUNNING replay versionName=workflow_v1
1 ExecutionStalled <TIMESTAMP> 8.9m STALLED reason=VERSION_NAME_MISMATCH;description=Version not available: workflow_v1
修补最佳实践
以下代表一些额外的建议,应考虑这些建议以消除因使用补丁而导致工作流停滞或失败的可能性:
- 应用补丁时,原始代码必须保持可用且不变,以便正在运行的工作流能够以相同的方式评估它。
- 补丁应以增量方式应用,这意味着一旦添加了补丁并部署了应用程序,就不应在工作流中移动或删除它们。它们必须存在以保持正在运行的工作流的确定性。
- 如上所述,您不得重新使用以前部署中使用的补丁标识符,因为这将破坏确定性保证。但是,您可以在同一部署中的多个位置使用相同的标识符,而无需在不同工作流之间担心冲突。
- 如果您可以在应用补丁时避免使用
else分支,将使应用未来的补丁更容易。虽然替换现有代码时通常无法避免这种情况,但如果您只是向工作流添加新逻辑,这当然是可以管理的。 - 如果必须嵌套补丁,则必须在现有补丁内进行。例如,您的新补丁不能在
else分支中包装现有的补丁。否则,正在运行的工作流将无法访问原始代码,从而破坏确定性保证。
命名工作流版本控制
命名工作流版本控制代表我们的第二种方法,有助于以确定性安全的方式对工作流进行版本控制。在此方法中,您通过复制现有工作流并为其分配一个新的"版本化"名称来引入新的工作流版本。因为您可以从头开始重构工作流逻辑以删除任何补丁并引入您想要的任何其他更改,所以这种方法提供了与以前工作流版本的干净断开。
虽然实现此目的的具体细节取决于您使用的语言 SDK,但通常,您将复制最新的工作流,为其分配一个反映较新版本的唯一名称,并向您的 SDK 注册它。将为每个工作流构建一个注册表,以便在运行时,Dapr 将使用工作流名称进行调用,SDK 将路由到旧工作流(如果正在运行)或较新版本。
请注意,在所有 SDK 中,运行时不会在版本之间顺序迁移工作流。相反,当 SDK 收到运行新工作流的请求时,它选择运行最新版本,而不仅仅是"下一个"版本,因此无需处理版本之间的任何补偿逻辑。
语言 SDK 可能会公开一种在使用相同工作流名称时注册版本的方法,但这因 SDK 而异,因此请参阅特定 SDK 的文档以获取更多信息。例如,某些可能使用一种机制,您可以在其中显式提供 is_latest 标志来指示哪个版本是最新版本。
当 SDK 收到运行未注册版本的工作流的请求时,工作流将停滞。这可能在应用程序滚动部署期间自然发生,但也可能在某个版本被删除而某些工作流实例仍在使用它时发生。建议是,除非您已独立确认没有未完成的(正在运行或休眠的)工作流实例正在针对该版本运行,否则不应删除较旧的命名工作流版本。
.NET 使用源生成器自动识别和注册您的工作流版本,因此无需手动注册工作流(活动需要注册)。默认情况下,.NET 使用内置的可配置数字版本控制策略,其中版本作为名称的后缀提供。有关如何更改版本控制策略或配置以及如何在应用程序中设置版本控制,请参阅 .NET SDK 文档。
由于 .NET 应用程序不允许存在多个具有相同名称的类型,因此这不是此 SDK 中的选项。
要创建新的命名工作流版本,请复制现有工作流并修改类的名称以使用相同的前缀,但更改后缀中的版本标识符以反映较新的版本。重新构建应用程序,SDK 将自动处理其余部分。
给定以下工作流定义:
import "github.com/dapr/durabletask-go/workflow"
func WorkflowV1(ctx *workflow.WorkflowContext) error {
// ...
return nil
}
func WorkflowV2(ctx *workflow.WorkflowContext) error {
// ...
return nil
}
这是您注册这两个工作流的方式:
import "github.com/dapr/durabletask-go/workflow"
registry := workflow.NewRegistry()
// 这是以前的工作流版本,因此 `isLatest` 为 false
registry.AddVersionedWorkflow("Workflow", false, WorkflowV1)
// 这是最新的工作流版本,因此 `isLatest` 为 true
registry.AddVersionedWorkflow("Workflow", true, WorkflowV2)
这是相同的示例,但显式设置版本名称:
import "github.com/dapr/durabletask-go/workflow"
registry := workflow.NewRegistry()
// 这是以前的工作流版本,因此 `isLatest` 为 false
registry.AddVersionedWorkflow("Workflow", "WorkflowV1", false, WorkflowV1)
// 这是最新的工作流版本,因此 `isLatest` 为 true
registry.AddVersionedWorkflow("Workflow", "WorkflowV2", true, WorkflowV2)
注意:AddVersionedOrchestrator 和 AddVersionedOrchestratorN 的布尔参数指示工作流是否是最新版本。
这是使用不同版本的工作流:
from dapr.ext.workflow import DaprWorkflowContext, WorkflowRuntime
wfr = WorkflowRuntime()
@wfr.versioned_workflow(name="workflow")
def workflow_v1(ctx: DaprWorkflowContext, wf_input: str):
# ...
@wfr.versioned_workflow(name="workflow", is_latest=True)
def workflow_v2(ctx: DaprWorkflowContext, wf_input: str):
# ...
这是相同的示例,但显式设置版本名称:
from dapr.ext.workflow import DaprWorkflowContext, WorkflowRuntime
wfr = WorkflowRuntime()
@wfr.versioned_workflow(name="workflow", version_name="workflow_v1")
def workflow_v1(ctx: DaprWorkflowContext, wf_input: str):
# ...
@wfr.versioned_workflow(name="workflow", version_name="workflow_v2", is_latest=True)
def workflow_v2(ctx: DaprWorkflowContext, wf_input: str):
# ...
版本控制流程指导
由于命名工作流与补丁完全兼容,因此该方法是对工作流的更改最初通过向现有工作流逻辑应用补丁来进行的。最终,在应用了几个补丁后,您将遇到以下问题之一:
- 由于应用的补丁数量,您担心工作流状态历史记录的开销;
- 很难遵循工作流逻辑的黄金路径;
- 您需要进行另一个工作流调整,但无法弄清楚如何应用不破坏确定性保证的补丁。
此时,建议您引入工作流的命名版本。复制现有工作流,更改其名称以反映您在 SDK 中使用的任何版本控制策略,并重构以删除所有补丁,仅保留预期的"最新"逻辑。应用您的新更改(此处无需补丁——这是一个新的工作流)并部署它。
在这里,根据需要再次应用补丁以解决未来的更改,并在必要时引入另一个命名工作流版本并继续。无限重复此过程。
建议您保留旧的工作流版本,直到您完全确信没有任何正在运行的(包括使用长期运行计时器延迟的)使用任何旧工作流版本的内容。
工作流版本控制不解决更改工作流本身的输入和输出类型的问题。作为一般指导,建议要么返回序列化值(如字符串),使输出的使用者能够随着时间的推移反序列化不同的输出,要么采用包含可选属性的复杂类型以包含新的预期输出值。
1.3.4 - 工作流模式
Dapr 工作流简化了微服务架构中复杂的有状态协调需求。以下各节介绍了几种可以从 Dapr 工作流中受益的应用程序模式。
任务链
在任务链模式中,工作流中的多个步骤按顺序运行,一个步骤的输出可以作为下一步的输入传递。任务链工作流通常涉及创建需要对某些数据执行的操作序列,例如过滤、转换和规约。

在某些情况下,工作流的步骤可能需要跨多个微服务进行编排。为了提高可靠性和可扩展性,您可能还会使用队列来触发各个步骤。
虽然该模式很简单,但在实现中隐藏了许多复杂性。例如:
- 如果某个微服务在较长时间内不可用,会发生什么?
- 失败的步骤是否可以自动重试?
- 如果不能,如何方便之前已完成的步骤的回滚(如果适用)?
- 抛开实现细节,是否有方法可视化工作流,以便其他工程师可以理解它的作用和工作原理?
Dapr 工作流通过允许您使用您选择的编程语言将任务链模式简洁地实现为一个简单的函数来解决这些复杂性,如以下示例所示。
import dapr.ext.workflow as wf
def task_chain_workflow(ctx: wf.DaprWorkflowContext, wf_input: int):
try:
result1 = yield ctx.call_activity(step1, input=wf_input)
result2 = yield ctx.call_activity(step2, input=result1)
result3 = yield ctx.call_activity(step3, input=result2)
except Exception as e:
yield ctx.call_activity(error_handler, input=str(e))
raise
return [result1, result2, result3]
def step1(ctx, activity_input):
print(f'Step 1: Received input: {activity_input}.')
# Do some work
return activity_input + 1
def step2(ctx, activity_input):
print(f'Step 2: Received input: {activity_input}.')
# Do some work
return activity_input * 2
def step3(ctx, activity_input):
print(f'Step 3: Received input: {activity_input}.')
# Do some work
return activity_input ^ 2
def error_handler(ctx, error):
print(f'Executing error handler: {error}.')
# Apply some compensating work
Note 工作流重试策略将在 Python SDK 的未来版本中提供。
import { DaprWorkflowClient, WorkflowActivityContext, WorkflowContext, WorkflowRuntime, TWorkflow } from "@dapr/dapr";
async function start() {
// Update the gRPC client and worker to use a local address and port
const daprHost = "localhost";
const daprPort = "50001";
const workflowClient = new DaprWorkflowClient({
daprHost,
daprPort,
});
const workflowRuntime = new WorkflowRuntime({
daprHost,
daprPort,
});
const hello = async (_: WorkflowActivityContext, name: string) => {
return `Hello ${name}!`;
};
const sequence: TWorkflow = async function* (ctx: WorkflowContext): any {
const cities: string[] = [];
const result1 = yield ctx.callActivity(hello, "Tokyo");
cities.push(result1);
const result2 = yield ctx.callActivity(hello, "Seattle");
cities.push(result2);
const result3 = yield ctx.callActivity(hello, "London");
cities.push(result3);
return cities;
};
workflowRuntime.registerWorkflow(sequence).registerActivity(hello);
// Wrap the worker startup in a try-catch block to handle any errors during startup
try {
await workflowRuntime.start();
console.log("Workflow runtime started successfully");
} catch (error) {
console.error("Error starting workflow runtime:", error);
}
// Schedule a new orchestration
try {
const id = await workflowClient.scheduleNewWorkflow(sequence);
console.log(`Orchestration scheduled with ID: ${id}`);
// Wait for orchestration completion
const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30);
console.log(`Orchestration completed! Result: ${state?.serializedOutput}`);
} catch (error) {
console.error("Error scheduling or waiting for orchestration:", error);
}
await workflowRuntime.stop();
await workflowClient.stop();
// stop the dapr side car
process.exit(0);
}
start().catch((e) => {
console.error(e);
process.exit(1);
# Apply custom compensation logic
});
// Expotential backoff retry policy that survives long outages
var retryOptions = new WorkflowTaskOptions
{
RetryPolicy = new WorkflowRetryPolicy(
firstRetryInterval: TimeSpan.FromMinutes(1),
backoffCoefficient: 2.0,
maxRetryInterval: TimeSpan.FromHours(1),
maxNumberOfAttempts: 10),
};
try
{
var result1 = await context.CallActivityAsync<string>("Step1", wfInput, retryOptions);
var result2 = await context.CallActivityAsync<byte[]>("Step2", result1, retryOptions);
var result3 = await context.CallActivityAsync<long[]>("Step3", result2, retryOptions);
return string.Join(", ", result4);
}
catch (TaskFailedException) // Task failures are surfaced as TaskFailedException
{
// Retries expired - apply custom compensation logic
await context.CallActivityAsync<long[]>("MyCompensation", options: retryOptions);
throw;
}
Note 在上面的示例中,
"Step1"、"Step2"、"Step3"和"MyCompensation"表示工作流活动,这些是实际实现工作流步骤的代码中的函数。为简洁起见,这些活动的实现未包含在此示例中。
public class ChainWorkflow extends Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
StringBuilder sb = new StringBuilder();
String wfInput = ctx.getInput(String.class);
String result1 = ctx.callActivity("Step1", wfInput, String.class).await();
String result2 = ctx.callActivity("Step2", result1, String.class).await();
String result3 = ctx.callActivity("Step3", result2, String.class).await();
String result = sb.append(result1).append(',').append(result2).append(',').append(result3).toString();
ctx.complete(result);
};
}
}
class Step1 implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
Logger logger = LoggerFactory.getLogger(Step1.class);
logger.info("Starting Activity: " + ctx.getName());
// Do some work
return null;
}
}
class Step2 implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
Logger logger = LoggerFactory.getLogger(Step2.class);
logger.info("Starting Activity: " + ctx.getName());
// Do some work
return null;
}
}
class Step3 implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
Logger logger = LoggerFactory.getLogger(Step3.class);
logger.info("Starting Activity: " + ctx.getName());
// Do some work
return null;
}
}
func TaskChainWorkflow(ctx *workflow.WorkflowContext) (any, error) {
var input int
if err := ctx.GetInput(&input); err != nil {
return "", err
}
var result1 int
if err := ctx.CallActivity(Step1, workflow.ActivityInput(input)).Await(&result1); err != nil {
return nil, err
}
var result2 int
if err := ctx.CallActivity(Step2, workflow.ActivityInput(input)).Await(&result2); err != nil {
return nil, err
}
var result3 int
if err := ctx.CallActivity(Step3, workflow.ActivityInput(input)).Await(&result3); err != nil {
return nil, err
}
return []int{result1, result2, result3}, nil
}
func Step1(ctx workflow.ActivityContext) (any, error) {
var input int
if err := ctx.GetInput(&input); err != nil {
return "", err
}
fmt.Printf("Step 1: Received input: %s", input)
return input + 1, nil
}
func Step2(ctx workflow.ActivityContext) (any, error) {
var input int
if err := ctx.GetInput(&input); err != nil {
return "", err
}
fmt.Printf("Step 2: Received input: %s", input)
return input * 2, nil
}
func Step3(ctx workflow.ActivityContext) (any, error) {
var input int
if err := ctx.GetInput(&input); err != nil {
return "", err
}
fmt.Printf("Step 3: Received input: %s", input)
return int(math.Pow(float64(input), 2)), nil
}
如您所见,工作流被表示为您选择的编程语言中的一系列简单语句。这使得组织中的任何工程师都可以快速理解端到端流程,而无需一定理解端到端系统架构。
在幕后,Dapr 工作流运行时:
- 负责执行工作流并确保其运行完成。
- 自动保存进度。
- 如果工作流进程因任何原因失败,会自动从最后完成的步骤恢复工作流。
- 允许用目标编程语言自然地表达错误处理,使您可以轻松实现补偿逻辑。
- 提供内置的重试配置原语,以简化为工作流中的各个步骤配置复杂重试策略的过程。
扇出/扇入
在扇出/扇入设计模式中,您跨多个工作器同时执行多个任务,等待它们完成,然后对结果执行某种聚合。

除了前面模式中提到的挑战之外,手动实现扇出/扇入模式时还有几个重要问题需要考虑:
- 您如何控制并行度?
- 您如何知道何时触发后续聚合步骤?
- 如果并行步骤的数量是动态的怎么办?
Dapr 工作流提供了一种将扇出/扇入模式表示为简单函数的方法,如以下示例所示:
# Start the workflow
dapr workflow run DataProcessingWorkflow \
--app-id processor \
--input '{"items": ["item1", "item2", "item3"]}'
# Monitor parallel execution
dapr workflow history <instance-id> --app-id processor --output json
import time
from typing import List
import dapr.ext.workflow as wf
def batch_processing_workflow(ctx: wf.DaprWorkflowContext, wf_input: int):
# get a batch of N work items to process in parallel
work_batch = yield ctx.call_activity(get_work_batch, input=wf_input)
# schedule N parallel tasks to process the work items and wait for all to complete
parallel_tasks = [ctx.call_activity(process_work_item, input=work_item) for work_item in work_batch]
outputs = yield wf.when_all(parallel_tasks)
# aggregate the results and send them to another activity
total = sum(outputs)
yield ctx.call_activity(process_results, input=total)
def get_work_batch(ctx, batch_size: int) -> List[int]:
return [i + 1 for i in range(batch_size)]
def process_work_item(ctx, work_item: int) -> int:
print(f'Processing work item: {work_item}.')
time.sleep(5)
result = work_item * 2
print(f'Work item {work_item} processed. Result: {result}.')
return result
def process_results(ctx, final_result: int):
print(f'Final result: {final_result}.')
import {
Task,
DaprWorkflowClient,
WorkflowActivityContext,
WorkflowContext,
WorkflowRuntime,
TWorkflow,
} from "@dapr/dapr";
// Wrap the entire code in an immediately-invoked async function
async function start() {
// Update the gRPC client and worker to use a local address and port
const daprHost = "localhost";
const daprPort = "50001";
const workflowClient = new DaprWorkflowClient({
daprHost,
daprPort,
});
const workflowRuntime = new WorkflowRuntime({
daprHost,
daprPort,
});
function getRandomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
async function getWorkItemsActivity(_: WorkflowActivityContext): Promise<string[]> {
const count: number = getRandomInt(2, 10);
console.log(`generating ${count} work items...`);
const workItems: string[] = Array.from({ length: count }, (_, i) => `work item ${i}`);
return workItems;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function processWorkItemActivity(context: WorkflowActivityContext, item: string): Promise<number> {
console.log(`processing work item: ${item}`);
// Simulate some work that takes a variable amount of time
const sleepTime = Math.random() * 5000;
await sleep(sleepTime);
// Return a result for the given work item, which is also a random number in this case
// For more information about random numbers in workflow please check
// https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-code-constraints?tabpane=csharp#random-numbers
return Math.floor(Math.random() * 11);
}
const workflow: TWorkflow = async function* (ctx: WorkflowContext): any {
const tasks: Task<any>[] = [];
const workItems = yield ctx.callActivity(getWorkItemsActivity);
for (const workItem of workItems) {
tasks.push(ctx.callActivity(processWorkItemActivity, workItem));
}
const results: number[] = yield ctx.whenAll(tasks);
const sum: number = results.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
return sum;
};
workflowRuntime.registerWorkflow(workflow);
workflowRuntime.registerActivity(getWorkItemsActivity);
workflowRuntime.registerActivity(processWorkItemActivity);
// Wrap the worker startup in a try-catch block to handle any errors during startup
try {
await workflowRuntime.start();
console.log("Worker started successfully");
} catch (error) {
console.error("Error starting worker:", error);
}
// Schedule a new orchestration
try {
const id = await workflowClient.scheduleNewWorkflow(workflow);
console.log(`Orchestration scheduled with ID: ${id}`);
// Wait for orchestration completion
const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30);
console.log(`Orchestration completed! Result: ${state?.serializedOutput}`);
} catch (error) {
console.error("Error scheduling or waiting for orchestration:", error);
}
// stop worker and client
await workflowRuntime.stop();
await workflowClient.stop();
// stop the dapr side car
process.exit(0);
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
// Get a list of N work items to process in parallel.
object[] workBatch = await context.CallActivityAsync<object[]>("GetWorkBatch", null);
// Schedule the parallel tasks, but don't wait for them to complete yet.
var parallelTasks = new List<Task<int>>(workBatch.Length);
for (int i = 0; i < workBatch.Length; i++)
{
Task<int> task = context.CallActivityAsync<int>("ProcessWorkItem", workBatch[i]);
parallelTasks.Add(task);
}
// Everything is scheduled. Wait here until all parallel tasks have completed.
await Task.WhenAll(parallelTasks);
// Aggregate all N outputs and publish the result.
int sum = parallelTasks.Sum(t => t.Result);
await context.CallActivityAsync("PostResults", sum);
public class FaninoutWorkflow extends Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
// Get a list of N work items to process in parallel.
Object[] workBatch = ctx.callActivity("GetWorkBatch", Object[].class).await();
// Schedule the parallel tasks, but don't wait for them to complete yet.
List<Task<Integer>> tasks = Arrays.stream(workBatch)
.map(workItem -> ctx.callActivity("ProcessWorkItem", workItem, int.class))
.collect(Collectors.toList());
// Everything is scheduled. Wait here until all parallel tasks have completed.
List<Integer> results = ctx.allOf(tasks).await();
// Aggregate all N outputs and publish the result.
int sum = results.stream().mapToInt(Integer::intValue).sum();
ctx.complete(sum);
};
}
}
func BatchProcessingWorkflow(ctx *workflow.WorkflowContext) (any, error) {
var input int
if err := ctx.GetInput(&input); err != nil {
return 0, err
}
var workBatch []int
if err := ctx.CallActivity(GetWorkBatch, workflow.ActivityInput(input)).Await(&workBatch); err != nil {
return 0, err
}
parallelTasks := workflow.NewTaskSlice(len(workBatch))
for i, workItem := range workBatch {
parallelTasks[i] = ctx.CallActivity(ProcessWorkItem, workflow.ActivityInput(workItem))
}
var outputs int
for _, task := range parallelTasks {
var output int
err := task.Await(&output)
if err == nil {
outputs += output
} else {
return 0, err
}
}
if err := ctx.CallActivity(ProcessResults, workflow.ActivityInput(outputs)).Await(nil); err != nil {
return 0, err
}
return 0, nil
}
func GetWorkBatch(ctx workflow.ActivityContext) (any, error) {
var batchSize int
if err := ctx.GetInput(&batchSize); err != nil {
return 0, err
}
batch := make([]int, batchSize)
for i := 0; i < batchSize; i++ {
batch[i] = i
}
return batch, nil
}
func ProcessWorkItem(ctx workflow.ActivityContext) (any, error) {
var workItem int
if err := ctx.GetInput(&workItem); err != nil {
return 0, err
}
fmt.Printf("Processing work item: %d\n", workItem)
time.Sleep(time.Second * 5)
result := workItem * 2
fmt.Printf("Work item %d processed. Result: %d\n", workItem, result)
return result, nil
}
func ProcessResults(ctx workflow.ActivityContext) (any, error) {
var finalResult int
if err := ctx.GetInput(&finalResult); err != nil {
return 0, err
}
fmt.Printf("Final result: %d\n", finalResult)
return finalResult, nil
}
此示例的关键要点是:
- 扇出/扇入模式可以使用普通编程构造表示为简单的函数
- 并行任务的数量可以是静态的或动态的
- 工作流本身能够聚合并行执行的结果
此外,工作流的执行是持久的。如果一个工作流启动 100 个并行任务执行,并且在进程崩溃之前只完成了 40 个,工作流会自动重新启动,并且只安排剩余的 60 个任务。
您可以进一步使用简单的特定语言构造来限制并发度。下面的示例代码说明了如何将扇出度限制为仅 5 个并发活动执行:
//Revisiting the earlier example...
// Get a list of N work items to process in parallel.
object[] workBatch = await context.CallActivityAsync<object[]>("GetWorkBatch", null);
const int MaxParallelism = 5;
var results = new List<int>();
var inFlightTasks = new HashSet<Task<int>>();
foreach(var workItem in workBatch)
{
if (inFlightTasks.Count >= MaxParallelism)
{
var finishedTask = await Task.WhenAny(inFlightTasks);
results.Add(finishedTask.Result);
inFlightTasks.Remove(finishedTask);
}
inFlightTasks.Add(context.CallActivityAsync<int>("ProcessWorkItem", workItem));
}
results.AddRange(await Task.WhenAll(inFlightTasks));
var sum = results.Sum(t => t);
await context.CallActivityAsync("PostResults", sum);
您可以通过使用 WorkflowContext 上的以下扩展方法来并行处理工作流活动,同时对并发性设置上限:
//Revisiting the earlier example...
// Get a list of work items to process
var workBatch = await context.CallActivityAsync<object[]>("GetWorkBatch", null);
// Process deterministically in parallel with an upper cap of 5 activities at a time
var results = await context.ProcessInParallelAsync(workBatch, workItem => context.CallActivityAsync<int>("ProcessWorkItem", workItem), maxConcurrency: 5);
var sum = results.Sum(t => t);
await context.CallActivityAsync("PostResults", sum);
以这种方式限制并发度对于限制对共享资源的争用非常有用。例如,如果活动需要调用具有自己并发限制的外部资源(如数据库或外部 API),确保不超过指定数量的活动同时调用该资源会很有用。
异步 HTTP API
异步 HTTP API 通常使用异步请求-回复模式来实现。传统实现此模式涉及以下内容:
- 客户端向 HTTP API 端点发送请求(启动 API)
- 启动 API 将消息写入后端队列,触发长时间运行操作的开始
- 在安排后端操作后,启动 API 立即向客户端返回 HTTP 202 响应,其中包含可用于轮询状态的标识符
- 状态 API 查询包含长时间运行操作状态的数据库
- 客户端重复轮询 状态 API,直到某个超时过期或收到"完成"响应
端到端流程如下图所示。

实现异步请求-回复模式的挑战在于它涉及使用多个 API 和状态存储。它还涉及正确实现协议,以便客户端知道如何自动轮询状态并了解操作何时完成。
Dapr 工作流 HTTP API 开箱即用地支持异步请求-回复模式,无需您编写任何代码或进行任何状态管理。
以下 curl 命令说明了工作流 API 如何支持此模式。
curl -X POST http://localhost:3500/v1.0/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 -d '{"Name":"Paperclips","Quantity":1,"TotalCost":9.95}'
前面的命令将产生以下响应 JSON:
{"instanceID":"12345678"}
然后,HTTP 客户端可以使用工作流实例 ID 构造状态查询 URL,并重复轮询它,直到在有效负载中看到"COMPLETE"、“FAILURE"或"TERMINATED"状态。
curl http://localhost:3500/v1.0/workflows/dapr/12345678
以下是正在进行中的工作流状态可能看起来像的示例。
{
"instanceID": "12345678",
"workflowName": "OrderProcessingWorkflow",
"createdAt": "2023-05-03T23:22:11.143069826Z",
"lastUpdatedAt": "2023-05-03T23:22:22.460025267Z",
"runtimeStatus": "RUNNING",
"properties": {
"dapr.workflow.custom_status": "",
"dapr.workflow.input": "{\"Name\":\"Paperclips\",\"Quantity\":1,\"TotalCost\":9.95}"
}
}
从上一个示例中可以看出,工作流的运行时状态是 RUNNING,这让客户端知道应该继续轮询。
如果工作流已完成,状态可能如下所示。
{
"instanceID": "12345678",
"workflowName": "OrderProcessingWorkflow",
"createdAt": "2023-05-03T23:30:11.381146313Z",
"lastUpdatedAt": "2023-05-03T23:30:52.923870615Z",
"runtimeStatus": "COMPLETED",
"properties": {
"dapr.workflow.custom_status": "",
"dapr.workflow.input": "{\"Name\":\"Paperclips\",\"Quantity\":1,\"TotalCost\":9.95}",
"dapr.workflow.output": "{\"Processed\":true}"
}
}
从上一个示例中可以看出,工作流的运行时状态现在是 COMPLETED,这意味着客户端可以停止轮询更新。
监视器
监视器模式是一个循环过程,通常:
- 检查系统状态
- 根据该状态采取某些操作 - 例如,发送通知
- 休眠一段时间
- 重复
下图大致说明了此模式。

根据业务需求,可能只有一个监视器,也可能有多个监视器,每个业务实体一个(例如,股票)。此外,根据情况,休眠的时间可能需要更改。这些要求使得使用基于 cron 的调度系统变得不切实际。
Dapr 工作流通过允许您实现_永久工作流_来原生支持此模式。无需编写无限 while 循环(这是一种反模式),Dapr 工作流公开了一个 continue-as-new API,工作流作者可以使用它从头开始使用新输入重新启动工作流函数。
from dataclasses import dataclass
from datetime import timedelta
import random
import dapr.ext.workflow as wf
@dataclass
class JobStatus:
job_id: str
is_healthy: bool
def status_monitor_workflow(ctx: wf.DaprWorkflowContext, job: JobStatus):
# poll a status endpoint associated with this job
status = yield ctx.call_activity(check_status, input=job)
if not ctx.is_replaying:
print(f"Job '{job.job_id}' is {status}.")
if status == "healthy":
job.is_healthy = True
next_sleep_interval = 60 # check less frequently when healthy
else:
if job.is_healthy:
job.is_healthy = False
ctx.call_activity(send_alert, input=f"Job '{job.job_id}' is unhealthy!")
next_sleep_interval = 5 # check more frequently when unhealthy
yield ctx.create_timer(fire_at=ctx.current_utc_datetime + timedelta(minutes=next_sleep_interval))
# restart from the beginning with a new JobStatus input
ctx.continue_as_new(job)
def check_status(ctx, _) -> str:
return random.choice(["healthy", "unhealthy"])
def send_alert(ctx, message: str):
print(f'*** Alert: {message}')
const statusMonitorWorkflow: TWorkflow = async function* (ctx: WorkflowContext): any {
let duration;
const status = yield ctx.callActivity(checkStatusActivity);
if (status === "healthy") {
// Check less frequently when in a healthy state
// set duration to 1 hour
duration = 60 * 60;
} else {
yield ctx.callActivity(alertActivity, "job unhealthy");
// Check more frequently when in an unhealthy state
// set duration to 5 minutes
duration = 5 * 60;
}
// Put the workflow to sleep until the determined time
ctx.createTimer(duration);
// Restart from the beginning with the updated state
ctx.continueAsNew();
};
public override async Task<object> RunAsync(WorkflowContext context, MyEntityState myEntityState)
{
TimeSpan nextSleepInterval;
var status = await context.CallActivityAsync<string>("GetStatus");
if (status == "healthy")
{
myEntityState.IsHealthy = true;
// Check less frequently when in a healthy state
nextSleepInterval = TimeSpan.FromMinutes(60);
}
else
{
if (myEntityState.IsHealthy)
{
myEntityState.IsHealthy = false;
await context.CallActivityAsync("SendAlert", myEntityState);
}
// Check more frequently when in an unhealthy state
nextSleepInterval = TimeSpan.FromMinutes(5);
}
// Put the workflow to sleep until the determined time
await context.CreateTimer(nextSleepInterval);
// Restart from the beginning with the updated state
context.ContinueAsNew(myEntityState);
return null;
}
此示例假设您有一个预定义的
MyEntityState类,其中包含布尔IsHealthy属性。
public class MonitorWorkflow extends Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
Duration nextSleepInterval;
var status = ctx.callActivity(DemoWorkflowStatusActivity.class.getName(), DemoStatusActivityOutput.class).await();
var isHealthy = status.getIsHealthy();
if (isHealthy) {
// Check less frequently when in a healthy state
nextSleepInterval = Duration.ofMinutes(60);
} else {
ctx.callActivity(DemoWorkflowAlertActivity.class.getName()).await();
// Check more frequently when in an unhealthy state
nextSleepInterval = Duration.ofMinutes(5);
}
// Put the workflow to sleep until the determined time
try {
ctx.createTimer(nextSleepInterval);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// Restart from the beginning with the updated state
ctx.continueAsNew();
}
}
}
type JobStatus struct {
JobID string `json:"job_id"`
IsHealthy bool `json:"is_healthy"`
}
func StatusMonitorWorkflow(ctx *workflow.WorkflowContext) (any, error) {
var sleepInterval time.Duration
var job JobStatus
if err := ctx.GetInput(&job); err != nil {
return "", err
}
var status string
if err := ctx.CallActivity(CheckStatus, workflow.ActivityInput(job)).Await(&status); err != nil {
return "", err
}
if status == "healthy" {
job.IsHealthy = true
sleepInterval = time.Minutes * 60
} else {
if job.IsHealthy {
job.IsHealthy = false
err := ctx.CallActivity(SendAlert, workflow.ActivityInput(fmt.Sprintf("Job '%s' is unhealthy!", job.JobID))).Await(nil)
if err != nil {
return "", err
}
}
sleepInterval = time.Minutes * 5
}
if err := ctx.CreateTimer(sleepInterval).Await(nil); err != nil {
return "", err
}
ctx.ContinueAsNew(job, false)
return "", nil
}
func CheckStatus(ctx workflow.ActivityContext) (any, error) {
statuses := []string{"healthy", "unhealthy"}
return statuses[rand.Intn(1)], nil
}
func SendAlert(ctx workflow.ActivityContext) (any, error) {
var message string
if err := ctx.GetInput(&message); err != nil {
return "", err
}
fmt.Printf("*** Alert: %s", message)
return "", nil
}
实现监视器模式的工作流可以永远循环,也可以通过不调用 continue-as-new 来优雅地终止自己。
Note
此模式也可以使用 actor 和提醒来实现。区别在于此工作流表示为单个函数,输入和状态存储在局部变量中。如果必要,工作流还可以以更强的可靠性保证执行一系列操作。外部系统交互
在某些情况下,工作流可能需要暂停并等待外部系统执行某些操作。例如,工作流可能需要暂停并等待收到付款。在这种情况下,支付系统可能会在收到付款时向发布/订阅主题发布事件,并且该主题上的侦听器可以使用引发事件工作流 API向工作流引发事件。
另一个非常常见的场景是工作流需要暂停并等待人员,例如在批准采购订单时。Dapr 工作流通过外部事件功能支持此事件模式。
以下是涉及人员的采购订单的工作流示例:
- 收到采购订单时触发工作流。
- 工作流中的规则确定需要人员执行某些操作。例如,采购订单成本超过某个自动批准阈值。
- 工作流发送请求人员操作的通知。例如,它向指定的批准者发送包含批准链接的电子邮件。
- 工作流暂停并等待人员通过单击链接来批准或拒绝订单。
- 如果在指定时间内未收到批准,工作流将恢复并执行某些补偿逻辑,例如取消订单。
下图说明了此流程。

以下示例代码显示了如何使用 Dapr 工作流实现此模式。
from dataclasses import dataclass
from datetime import timedelta
import dapr.ext.workflow as wf
@dataclass
class Order:
cost: float
product: str
quantity: int
def __str__(self):
return f'{self.product} ({self.quantity})'
@dataclass
class Approval:
approver: str
@staticmethod
def from_dict(dict):
return Approval(**dict)
def purchase_order_workflow(ctx: wf.DaprWorkflowContext, order: Order):
# Orders under $1000 are auto-approved
if order.cost < 1000:
return "Auto-approved"
# Orders of $1000 or more require manager approval
yield ctx.call_activity(send_approval_request, input=order)
# Approvals must be received within 24 hours or they will be canceled.
approval_event = ctx.wait_for_external_event("approval_received")
timeout_event = ctx.create_timer(timedelta(hours=24))
winner = yield wf.when_any([approval_event, timeout_event])
if winner == timeout_event:
return "Cancelled"
# The order was approved
yield ctx.call_activity(place_order, input=order)
approval_details = Approval.from_dict(approval_event.get_result())
return f"Approved by '{approval_details.approver}'"
def send_approval_request(_, order: Order) -> None:
print(f'*** Sending approval request for order: {order}')
def place_order(_, order: Order) -> None:
print(f'*** Placing order: {order}')
import {
Task,
DaprWorkflowClient,
WorkflowActivityContext,
WorkflowContext,
WorkflowRuntime,
TWorkflow,
} from "@dapr/dapr";
import * as readlineSync from "readline-sync";
// Wrap the entire code in an immediately-invoked async function
async function start() {
class Order {
cost: number;
product: string;
quantity: number;
constructor(cost: number, product: string, quantity: number) {
this.cost = cost;
this.product = product;
this.quantity = quantity;
}
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Update the gRPC client and worker to use a local address and port
const daprHost = "localhost";
const daprPort = "50001";
const workflowClient = new DaprWorkflowClient({
daprHost,
daprPort,
});
const workflowRuntime = new WorkflowRuntime({
daprHost,
daprPort,
});
// Activity function that sends an approval request to the manager
const sendApprovalRequest = async (_: WorkflowActivityContext, order: Order) => {
// Simulate some work that takes an amount of time
await sleep(3000);
console.log(`Sending approval request for order: ${order.product}`);
};
// Activity function that places an order
const placeOrder = async (_: WorkflowActivityContext, order: Order) => {
console.log(`Placing order: ${order.product}`);
};
// Orchestrator function that represents a purchase order workflow
const purchaseOrderWorkflow: TWorkflow = async function* (ctx: WorkflowContext, order: Order): any {
// Orders under $1000 are auto-approved
if (order.cost < 1000) {
return "Auto-approved";
}
// Orders of $1000 or more require manager approval
yield ctx.callActivity(sendApprovalRequest, order);
// Approvals must be received within 24 hours or they will be cancled.
const tasks: Task<any>[] = [];
const approvalEvent = ctx.waitForExternalEvent("approval_received");
const timeoutEvent = ctx.createTimer(24 * 60 * 60);
tasks.push(approvalEvent);
tasks.push(timeoutEvent);
const winner = ctx.whenAny(tasks);
if (winner == timeoutEvent) {
return "Cancelled";
}
yield ctx.callActivity(placeOrder, order);
const approvalDetails = approvalEvent.getResult();
return `Approved by ${approvalDetails.approver}`;
};
workflowRuntime
.registerWorkflow(purchaseOrderWorkflow)
.registerActivity(sendApprovalRequest)
.registerActivity(placeOrder);
// Wrap the worker startup in a try-catch block to handle any errors during startup
try {
await workflowRuntime.start();
console.log("Worker started successfully");
} catch (error) {
console.error("Error starting worker:", error);
}
// Schedule a new orchestration
try {
const cost = readlineSync.questionInt("Cost of your order:");
const approver = readlineSync.question("Approver of your order:");
const timeout = readlineSync.questionInt("Timeout for your order in seconds:");
const order = new Order(cost, "MyProduct", 1);
const id = await workflowClient.scheduleNewWorkflow(purchaseOrderWorkflow, order);
console.log(`Orchestration scheduled with ID: ${id}`);
// prompt for approval asynchronously
promptForApproval(approver, workflowClient, id);
// Wait for orchestration completion
const state = await workflowClient.waitForWorkflowCompletion(id, undefined, timeout + 2);
console.log(`Orchestration completed! Result: ${state?.serializedOutput}`);
} catch (error) {
console.error("Error scheduling or waiting for orchestration:", error);
}
// stop worker and client
await workflowRuntime.stop();
await workflowClient.stop();
// stop the dapr side car
process.exit(0);
}
async function promptForApproval(approver: string, workflowClient: DaprWorkflowClient, id: string) {
if (readlineSync.keyInYN("Press [Y] to approve the order... Y/yes, N/no")) {
const approvalEvent = { approver: approver };
await workflowClient.raiseEvent(id, "approval_received", approvalEvent);
} else {
return "Order rejected";
}
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
public override async Task<OrderResult> RunAsync(WorkflowContext context, OrderPayload order)
{
// ...(other steps)...
// Require orders over a certain threshold to be approved
if (order.TotalCost > OrderApprovalThreshold)
{
try
{
// Request human approval for this order
await context.CallActivityAsync(nameof(RequestApprovalActivity), order);
// Pause and wait for a human to approve the order
ApprovalResult approvalResult = await context.WaitForExternalEventAsync<ApprovalResult>(
eventName: "ManagerApproval",
timeout: TimeSpan.FromDays(3));
if (approvalResult == ApprovalResult.Rejected)
{
// The order was rejected, end the workflow here
return new OrderResult(Processed: false);
}
}
catch (TaskCanceledException)
{
// An approval timeout results in automatic order cancellation
return new OrderResult(Processed: false);
}
}
// ...(other steps)...
// End the workflow with a success result
return new OrderResult(Processed: true);
}
Note 在上面的示例中,
RequestApprovalActivity是要调用的工作流活动的名称,而ApprovalResult是由工作流应用定义的枚举。为简洁起见,这些定义未包含在示例代码中。
public class ExternalSystemInteractionWorkflow extends Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
// ...other steps...
Integer orderCost = ctx.getInput(int.class);
// Require orders over a certain threshold to be approved
if (orderCost > ORDER_APPROVAL_THRESHOLD) {
try {
// Request human approval for this order
ctx.callActivity("RequestApprovalActivity", orderCost, Void.class).await();
// Pause and wait for a human to approve the order
boolean approved = ctx.waitForExternalEvent("ManagerApproval", Duration.ofDays(3), boolean.class).await();
if (!approved) {
// The order was rejected, end the workflow here
ctx.complete("Process reject");
}
} catch (TaskCanceledException e) {
// An approval timeout results in automatic order cancellation
ctx.complete("Process cancel");
}
}
// ...other steps...
// End the workflow with a success result
ctx.complete("Process approved");
};
}
}
type Order struct {
Cost float64 `json:"cost"`
Product string `json:"product"`
Quantity int `json:"quantity"`
}
type Approval struct {
Approver string `json:"approver"`
}
func PurchaseOrderWorkflow(ctx *workflow.WorkflowContext) (any, error) {
var order Order
if err := ctx.GetInput(&order); err != nil {
return "", err
}
// Orders under $1000 are auto-approved
if order.Cost < 1000 {
return "Auto-approved", nil
}
// Orders of $1000 or more require manager approval
if err := ctx.CallActivity(SendApprovalRequest, workflow.ActivityInput(order)).Await(nil); err != nil {
return "", err
}
// Approvals must be received within 24 hours or they will be cancelled
var approval Approval
if err := ctx.WaitForExternalEvent("approval_received", time.Hour*24).Await(&approval); err != nil {
// Assuming that a timeout has taken place - in any case; an error.
return "error/cancelled", err
}
// The order was approved
if err := ctx.CallActivity(PlaceOrder, workflow.ActivityInput(order)).Await(nil); err != nil {
return "", err
}
return fmt.Sprintf("Approved by %s", approval.Approver), nil
}
func SendApprovalRequest(ctx workflow.ActivityContext) (any, error) {
var order Order
if err := ctx.GetInput(&order); err != nil {
return "", err
}
fmt.Printf("*** Sending approval request for order: %v\n", order)
return "", nil
}
func PlaceOrder(ctx workflow.ActivityContext) (any, error) {
var order Order
if err := ctx.GetInput(&order); err != nil {
return "", err
}
fmt.Printf("*** Placing order: %v", order)
return "", nil
}
向等待的工作流实例传递事件以恢复工作流执行的代码在工作流外部。可以使用引发事件工作流管理 API 将工作流事件传递给等待的工作流实例,如以下示例所示:
from dapr.clients import DaprClient
from dataclasses import asdict
with DaprClient() as d:
d.raise_workflow_event(
instance_id=instance_id,
workflow_component="dapr",
event_name="approval_received",
event_data=asdict(Approval("Jane Doe")))
import { DaprClient } from "@dapr/dapr";
public async raiseEvent(workflowInstanceId: string, eventName: string, eventPayload?: any) {
this._innerClient.raiseOrchestrationEvent(workflowInstanceId, eventName, eventPayload);
}
// Raise the workflow event to the waiting workflow
await daprClient.RaiseWorkflowEventAsync(
instanceId: orderId,
workflowComponent: "dapr",
eventName: "ManagerApproval",
eventData: ApprovalResult.Approved);
System.out.println("**SendExternalMessage: RestartEvent**");
client.raiseEvent(restartingInstanceId, "RestartEvent", "RestartEventPayload");
func raiseEvent() {
daprClient, err := client.NewClient()
if err != nil {
log.Fatalf("failed to initialize the client")
}
err = daprClient.RaiseEventWorkflow(context.Background(), &client.RaiseEventWorkflowRequest{
InstanceID: "instance_id",
WorkflowComponent: "dapr",
EventName: "approval_received",
EventData: Approval{
Approver: "Jane Doe",
},
})
if err != nil {
log.Fatalf("failed to raise event on workflow")
}
log.Println("raised an event on specified workflow")
}
外部事件不必直接由人员触发。它们也可以由其他系统触发。例如,工作流可能需要暂停并等待收到付款。在这种情况下,支付系统可能会在收到付款时向发布/订阅主题发布事件,并且该主题上的侦听器可以使用引发事件工作流 API 向工作流引发事件。
补偿
补偿模式(也称为 Saga 模式)提供了一种在工作流中途失败时回滚或撤销已执行操作的机制。此模式对于跨越多个微服务且传统数据库事务不可行的长时间运行的工作流特别重要。
在分布式微服务架构中,您通常需要跨多个服务协调操作。当这些操作无法包含在单个事务中时,补偿模式通过为工作流中的每个步骤定义补偿操作来提供一种保持一致性的方法。
补偿模式解决了几个关键挑战:
- 分布式事务管理:当工作流跨越多个微服务时,每个微服务都有自己的数据存储,传统的 ACID 事务是不可能的。补偿模式通过确保操作要么全部成功完成,要么通过补偿全部撤销来提供事务一致性。
- 部分故障恢复:如果工作流在某些步骤成功完成后失败,补偿模式允许您优雅地撤销这些已完成的步骤。
- 业务流程完整性:确保业务流程可以在故障时正确回滚,从而保持业务操作的完整性。
- 长时间运行的流程:对于可能运行数小时、数天或更长时间的工作流,传统的锁定机制是不切实际的。补偿提供了一种在这些场景中处理故障的方法。
补偿模式的常见用例包括:
- 电子商务订单处理:预留库存、处理付款和发货订单。如果发货失败,您需要释放库存并退还付款。
- 金融交易:在转账中,如果贷记目标账户失败,您需要回滚对源账户的借记。
- 资源预配:在跨多个提供程序预配云资源时,如果某个步骤失败,您需要清理所有以前预配的资源。
- 多步骤业务流程:任何涉及多个不可逆步骤的业务流程,这些步骤可能需要在后续失败时撤销。
Dapr 工作流提供对补偿模式的支持,允许您为每个步骤注册补偿活动,并在需要时以相反的顺序执行它们。
以下是电子商务流程的工作流示例:
- 收到订单时触发工作流。
- 在库存中为订单进行预留。
- 处理付款。
- 发货订单。
- 如果上述任何操作导致错误,则使用另一个操作进行补偿:
- 取消发货。
- 退还付款。
- 释放库存预留。
下图说明了此流程。

public class PaymentProcessingWorkflow implements Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
ctx.getLogger().info("Starting Workflow: " + ctx.getName());
var orderId = ctx.getInput(String.class);
List<String> compensations = new ArrayList<>();
try {
// Step 1: Reserve inventory
String reservationId = ctx.callActivity(ReserveInventoryActivity.class.getName(), orderId, String.class).await();
ctx.getLogger().info("Inventory reserved: {}", reservationId);
compensations.add("ReleaseInventory");
// Step 2: Process payment
String paymentId = ctx.callActivity(ProcessPaymentActivity.class.getName(), orderId, String.class).await();
ctx.getLogger().info("Payment processed: {}", paymentId);
compensations.add("RefundPayment");
// Step 3: Ship order
String shipmentId = ctx.callActivity(ShipOrderActivity.class.getName(), orderId, String.class).await();
ctx.getLogger().info("Order shipped: {}", shipmentId);
compensations.add("CancelShipment");
} catch (TaskFailedException e) {
ctx.getLogger().error("Activity failed: {}", e.getMessage());
// Execute compensations in reverse order
Collections.reverse(compensations);
for (String compensation : compensations) {
try {
switch (compensation) {
case "CancelShipment":
String shipmentCancelResult = ctx.callActivity(
CancelShipmentActivity.class.getName(),
orderId,
String.class).await();
ctx.getLogger().info("Shipment cancellation completed: {}", shipmentCancelResult);
break;
case "RefundPayment":
String refundResult = ctx.callActivity(
RefundPaymentActivity.class.getName(),
orderId,
String.class).await();
ctx.getLogger().info("Payment refund completed: {}", refundResult);
break;
case "ReleaseInventory":
String releaseResult = ctx.callActivity(
ReleaseInventoryActivity.class.getName(),
orderId,
String.class).await();
ctx.getLogger().info("Inventory release completed: {}", releaseResult);
break;
}
} catch (TaskFailedException ex) {
ctx.getLogger().error("Compensation activity failed: {}", ex.getMessage());
}
}
ctx.complete("Order processing failed, compensation applied");
}
// Step 4: Send confirmation
ctx.callActivity(SendConfirmationActivity.class.getName(), orderId, Void.class).await();
ctx.getLogger().info("Confirmation sent for order: {}", orderId);
ctx.complete("Order processed successfully: " + orderId);
};
}
}
// Example activities
class ReserveInventoryActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String orderId = ctx.getInput(String.class);
// Logic to reserve inventory
String reservationId = "reservation_" + orderId;
System.out.println("Reserved inventory for order: " + orderId);
return reservationId;
}
}
class ReleaseInventoryActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String reservationId = ctx.getInput(String.class);
// Logic to release inventory reservation
System.out.println("Released inventory reservation: " + reservationId);
return "Released: " + reservationId;
}
}
class ProcessPaymentActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String orderId = ctx.getInput(String.class);
// Logic to process payment
String paymentId = "payment_" + orderId;
System.out.println("Processed payment for order: " + orderId);
return paymentId;
}
}
class RefundPaymentActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String paymentId = ctx.getInput(String.class);
// Logic to refund payment
System.out.println("Refunded payment: " + paymentId);
return "Refunded: " + paymentId;
}
}
class ShipOrderActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String orderId = ctx.getInput(String.class);
// Logic to ship order
String shipmentId = "shipment_" + orderId;
System.out.println("Shipped order: " + orderId);
return shipmentId;
}
}
class CancelShipmentActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String shipmentId = ctx.getInput(String.class);
// Logic to cancel shipment
System.out.println("Canceled shipment: " + shipmentId);
return "Canceled: " + shipmentId;
}
}
class SendConfirmationActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String orderId = ctx.getInput(String.class);
// Logic to send confirmation
System.out.println("Sent confirmation for order: " + orderId);
return null;
}
}
使用 Dapr 工作流补偿模式的主要好处包括:
- 补偿控制:您可以完全控制何时以及如何执行补偿活动。
- 灵活配置:您可以实现自定义逻辑来确定要运行哪些补偿。
- 错误处理:根据您的特定业务需求处理补偿失败。
- 简单实现:无需额外的框架依赖 - 只需标准的工作流活动和异常处理。
补偿模式确保您的分布式工作流可以保持一致性并从故障中优雅地恢复,使其成为构建可靠微服务架构的重要工具。
后续步骤
工作流架构 >>相关链接
1.3.5 - 工作流架构
Dapr 工作流允许开发者使用多种编程语言的普通代码来定义工作流。工作流引擎运行在 Dapr 边车内部,协调作为应用程序一部分部署的工作流代码。Dapr 工作流构建在 Dapr Actor 之上,为工作流执行提供持久性和可扩展性。
本文介绍:
- Dapr 工作流引擎的架构
- 工作流引擎如何与应用程序代码交互
- 工作流引擎如何融入整体 Dapr 架构
- 不同的工作流后端如何与工作流引擎配合使用
有关如何在应用程序中编写 Dapr 工作流的更多信息,请参阅如何:编写工作流。
Dapr 工作流引擎内部由 Dapr 的 actor 运行时驱动。下图展示了 Kubernetes 模式下的 Dapr 工作流架构:

要使用 Dapr 工作流构建块,您需要使用 Dapr Workflow SDK 在应用程序中编写工作流代码,SDK 内部使用 gRPC 流连接到边车。这会注册工作流以及任何工作流活动,或工作流可以调度的任务。
引擎直接嵌入到边车中,并使用 durabletask-go 框架库实现。该框架允许您交换不同的存储提供程序,包括为 Dapr 创建的存储提供程序,它在幕后利用内部 actor。由于 Dapr 工作流使用 actor,您可以将工作流状态存储在状态存储中。
边车交互
当工作流应用程序启动时,它使用工作流编写 SDK 向 Dapr 边车发送 gRPC 请求,并获取工作流工作项的流,遵循服务器流式 RPC 模式。这些工作项可以是任何内容,从"启动新的 X 工作流"(其中 X 是工作流的类型)到"代表工作流 X 调度具有输入 Z 的活动 Y"。
工作流应用程序执行适当的工作流代码,然后向边车发送 gRPC 请求,其中包含执行结果。

所有交互都通过单个 gRPC 通道进行,并由应用程序发起,这意味着应用程序不需要打开任何入站端口。 这些交互的细节由特定语言的 Dapr 工作流编写 SDK 内部处理。
工作流与应用程序 actor 交互的区别
如果您熟悉 Dapr actor,您可能会注意到,与应用程序定义的 actor 相比,工作流的边车交互方式有一些差异。
| Actor | 工作流 |
|---|---|
| 应用程序创建的 actor 可以使用 HTTP 或 gRPC 与边车交互。 | 工作流仅使用 gRPC。由于工作流 gRPC 协议的复杂性,实现工作流时_必须_使用 SDK。 |
| Actor 操作从边车推送到应用程序代码。这要求应用程序监听特定的_应用端口_。 | 对于工作流,操作由应用程序使用流式协议从边车_拉取_。应用程序不需要监听任何端口即可运行工作流。 |
| Actor 向边车显式注册自己。 | 工作流不向边车注册自己。嵌入式引擎不跟踪工作流类型。此职责委托给工作流应用程序及其 SDK。 |
工作流分布式跟踪
工作流引擎使用的 durabletask-go 核心使用 Open Telemetry SDK 编写分布式跟踪。
这些跟踪由 Dapr 边车自动捕获,并导出到配置的 Open Telemetry 提供程序,例如 Zipkin。
引擎管理的每个工作流实例表示为一个或多个 span。 有一个表示完整工作流执行的父 span,以及各种任务的子 span,包括活动任务执行和持久化计时器的 span。
工作流活动代码当前_不能_访问跟踪上下文。
工作流 actor
当工作流客户端连接到边车时,会注册两种类型的 actor 以支持工作流引擎:
dapr.internal.{namespace}.{appID}.workflowdapr.internal.{namespace}.{appID}.activity
{namespace} 值是 Dapr 命名空间,如果未配置命名空间,则默认为 default。
{appID} 值是应用程序的 ID。
例如,如果您有一个名为"wfapp"的工作流应用程序,则工作流 actor 的类型为 dapr.internal.default.wfapp.workflow,活动 actor 的类型为 dapr.internal.default.wfapp.activity。
下图演示了工作流 actor 在 Kubernetes 场景中如何运行:

与用户定义的 actor 一样,工作流 actor 通过 actor 放置服务提供的哈希查找表分布在集群中。 它们还维护自己的状态并使用提醒。 但是,与应用程序代码中的 actor 不同,这些工作流 actor 嵌入到 Dapr 边车中。 应用程序代码完全不知道这些 actor 的存在。
注意
工作流 actor 类型仅在应用程序使用 Dapr Workflow SDK 注册工作流后才注册。 如果应用程序从未注册工作流,则永远不会注册内部工作流 actor。
工作流 actor
工作流使用两种不同类型的 actor:工作流 actor 和活动 actor。 工作流 actor 负责管理应用程序中运行的所有工作流的状态和位置。 为每个被调度的工作流实例激活一个新的工作流 actor 实例。 工作流 actor 的 ID 是工作流的 ID。 此工作流 actor 在工作流进行时存储工作流的状态,并通过 actor 查找表确定工作流代码在哪个节点上执行。
由于工作流基于 actor,所有工作流和活动工作在实现工作流的应用程序的所有副本之间随机分布。 工作流启动的位置与每个工作项执行的位置之间没有局部性或关系。
每个工作流 actor 使用配置的 actor 状态存储中的以下键保存其状态:
| 键 | 描述 |
|---|---|
inbox-NNNNNN | 工作流的收件箱实际上是驱动工作流执行的_消息_的 FIFO 队列。示例消息包括工作流创建消息、活动任务完成消息等。每条消息作为单独的键存储在状态存储中,名称为 inbox-NNNNNN,其中 NNNNNN 是一个 6 位数字,表示消息的顺序。一旦工作流消耗了相应的消息,这些状态键就会被删除。 |
history-NNNNNN | 工作流的历史是表示工作流执行历史的事件的有序列表。历史中的每个键保存单个历史事件的数据。像仅追加日志一样,工作流历史事件只添加,从不删除(除非工作流执行"继续作为新"操作,这会清除所有历史记录并使用新输入重新启动工作流)。 |
customStatus | 包含用户定义的工作流状态值。每个工作流 actor 实例恰好有一个 customStatus 键。 |
metadata | 包含有关工作流的元信息,作为 JSON blob,包括收件箱长度、历史记录长度以及表示工作流生成的 64 位整数(用于实例 ID 被重用的情况)。长度信息用于确定在加载或保存工作流状态更新时需要读取或写入哪些键。 |
警告
工作流 actor 状态在工作流完成后仍保留在状态存储中。
创建大量工作流可能导致无限制的存储使用。 要解决此问题,可以使用工作流的 ID 清除工作流或直接删除工作流数据库存储中的条目。
下图说明了工作流 actor 的典型生命周期。

总结:
- 工作流 actor 在收到新消息时被激活。
- 新消息随后触发相关的工作流代码(在您的应用程序中)运行,并将执行结果返回给工作流 actor。
- 收到结果后,actor 根据需要调度任何任务。
- 调度后,actor 更新其在状态存储中的状态。
- 最后,actor 进入空闲状态,直到收到另一条消息。在此空闲期间,边车可能决定从内存中卸载工作流 actor。
活动 actor
活动 actor 负责管理所有工作流活动调用的状态和位置。
为工作流调度的每个活动任务激活一个新的活动 actor 实例。
活动 actor 的 ID 是工作流的 ID 加上序列号(序列号从 0 开始)以及"生成"(在使用 continue as new 重新运行的实例期间递增)的组合。
例如,如果工作流的 ID 为 876bf371,并且是工作流调度的第三个活动,其 ID 将为 876bf371::2::1,其中 2 是序列号,1 是生成。
如果活动在 continue as new 后再次被调度,ID 将为 876bf371::2::2。
活动 actor 不存储任何状态,而是将所有结果数据发送回父工作流 actor。
下图说明了活动 actor 的典型生命周期。

活动 actor 是短寿命的:
- 当工作流 actor 调度活动任务时,活动 actor 被激活。
- 活动 actor 立即调用工作流应用程序以调用相关的活动代码。
- 活动代码运行完成并返回其结果后,活动 actor 向父工作流 actor 发送带有执行结果的消息。
- 活动 actor 然后停用自己。
- 发送结果后,工作流被触发继续进行下一步。
提醒使用和执行保证
Dapr 工作流通过使用 actor 提醒来确保工作流容错性,以从瞬态系统故障中恢复。 在调用应用程序工作流代码之前,工作流或活动 actor 将创建一个新的提醒。 这些提醒是"一次性"的,意味着它们将在成功触发后过期。 如果应用程序代码无中断地执行,提醒将被触发并过期。 但是,如果托管相关工作流或活动的节点或边车崩溃,提醒将重新激活相应的 actor,并且将永远重试执行。

状态存储使用
Dapr 工作流内部使用 actor 来驱动工作流的执行。
与任何 actor 一样,这些工作流 actor 将其状态存储在配置的 actor 状态存储中。
这是通过在 Dapr 配置中指定状态存储组件,然后在配置的 actors 部分的 actorStateStore 属性中引用该状态存储来完成的。
阅读状态 API 参考和 actor API 参考以了解更多关于 actor 状态存储的信息。
如工作流 actor部分所述,工作流通过附加到历史日志来增量保存其状态。 工作流的历史日志分布在多个状态存储键中,以便每个"检查点"仅需要附加最新的条目。
每个检查点的大小由工作流在进入空闲状态之前调度的并发操作数确定。 顺序工作流因此会对状态存储进行较小的批量更新,而扇出/扇入工作流将需要更大的批量。 批量大小也受工作流调用活动或子工作流时输入和输出大小的影响。

不同的状态存储实现可能会隐式地限制您可以编写的工作流类型。 例如,Azure Cosmos DB 状态存储将项目大小限制为 2 MB 的 UTF-8 编码 JSON(来源)。 活动或子工作流的输入或输出负载作为单个记录存储在状态存储中,因此 2 MB 的项目限制意味着工作流和活动的输入和输出不能超过 2 MB 的 JSON 序列化数据。
同样,如果状态存储对批量事务的大小施加限制,这可能会限制工作流可以调度的并行操作数量。
可以从状态存储中清除工作流状态,包括其所有历史记录。 每个 Dapr SDK 都公开了用于清除特定工作流实例的所有元数据的 API。
状态存储记录数
每次工作流运行在状态存储中保存为历史记录的记录数由其复杂性或"形状"决定。换句话说,即活动、计时器、子工作流等的数量。 下表显示了不同工作流任务保存的记录数的一般指南。 根据重试或并发性,此数字可能更大或更小。
| 任务类型 | 保存的记录数 |
|---|---|
| 启动工作流 | 5 条记录 |
| 调用活动 | 3 条记录 |
| 计时器 | 3 条记录 |
| 触发事件 | 3 条记录 |
| 启动子工作流 | 8 条记录 |
查询工作流历史
dapr workflow --app-id myapp list
dapr workflow --app-id myapp history <instance-id>
支持的状态存储
工作流引擎支持以下状态存储:
- PostgreSQL
- MySQL
- SQL Server
- SQLite
- Oracle Database
- CockroachDB
- MongoDB
- Redis
工作流可扩展性
由于 Dapr 工作流内部使用 actor 实现,Dapr 工作流具有与 actor 相同的可扩展性特征。 放置服务:
- 不区分工作流 actor 和您在应用程序中定义的 actor
- 将使用与 actor 相同的算法对工作流进行负载平衡
工作流的预期可扩展性由以下因素决定:
- 用于托管工作流应用程序的计算机数量
- 运行工作流的计算机上可用的 CPU 和内存资源
- 为 actor 配置的状态存储的可扩展性
- actor 放置服务和提醒子系统的可扩展性
目标应用程序中工作流代码的实现细节也在单个工作流实例的可扩展性中发挥作用。 每个工作流实例一次在单个节点上执行,但工作流可以调度在其他节点上运行的活动和子工作流。
工作流还可以调度这些活动和子工作流并行运行,允许单个工作流可能在整个集群的所有可用节点上分布计算任务。
您可以使用 Dapr 配置配置工作流和活动的最大并发性,如下一节所述。
重要
默认情况下,工作流和活动并发性没有施加全局限制。 因此,如果失控工作流尝试并行调度太多任务,可能会消耗集群中的所有资源。 在编写并行调度大批量工作的 Dapr 工作流时,请小心。重要
Dapr 工作流引擎要求工作流应用程序的所有实例注册完全相同的一组工作流和活动。 换句话说,不可能独立扩展某些工作流或活动。 应用程序中的所有工作流和活动必须一起扩展。工作流不控制负载如何在集群中分布的细节。 例如,如果工作流调度 10 个活动任务并行运行,所有 10 个任务可能运行在多达 10 个不同的计算节点上,也可能运行在单个计算节点上。 实际扩展行为由 actor 放置服务决定,该服务管理表示工作流每个任务的 actor 的分布。

工作流延迟
为了提供持久性和弹性保证,Dapr 工作流频繁写入状态存储并依赖提醒来驱动执行。 因此,Dapr 工作流可能不适合延迟敏感的工作负载。 高延迟的预期来源包括:
- 持久化工作流状态时状态存储的延迟。
- 使用大型历史记录重新水合工作流时状态存储的延迟。
- 集群中太多活动提醒引起的延迟。
- 集群中高 CPU 使用率引起的延迟。
有关工作流 actor 的设计如何影响执行延迟的更多详细信息,请参阅提醒使用和执行保证部分。
提高调度吞吐量
默认情况下,当客户端调度工作流时,工作流引擎会等待工作流完全启动后再向客户端返回响应。 在返回之前等待工作流启动会降低工作流的调度吞吐量。 当调度具有开始时间的工作流时,工作流引擎不会等待工作流启动就向客户端返回响应。 要提高调度吞吐量,请考虑在调度工作流时添加"现在"的开始时间。 以下显示了在 Go SDK 中调度开始时间为"现在"的工作流的示例:
client.ScheduleNewWorkflow(ctx, "MyCoolWorkflow", workflow.WithStartTime(time.Now()))
使用 Dapr Shared 与工作流时的工作流集群部署
注意
以下功能仅在启用工作流集群部署预览功能时可用。当使用 Dapr Shared时,可能会有多个 daprd 边车在单个负载均衡器或服务后面运行。
因此,接收工作的辅助实例可能不是接收工作结果的实例。
Dapr 创建第三种 actor 类型来处理此场景:dapr.internal.{namespace}.{appID}.executor,用于将辅助结果路由回正确的工作流 actor,以确保正确操作。
后续步骤
编写工作流 >>相关链接
1.3.6 - 方法指南:编写工作流
本文提供有关如何编写由 Dapr 工作流引擎执行的工作流的高级概述。
Note
如果你还没有尝试过,建议先体验一下 工作流快速入门 ,快速了解如何使用工作流。以代码形式编写工作流
Dapr 工作流逻辑使用通用编程语言实现,使你能够:
- 使用你偏好的编程语言(无需学习新的 DSL 或 YAML 模式)。
- 访问语言的标准库。
- 构建你自己的库和抽象。
- 使用调试器并检查局部变量。
- 像应用程序的其他部分一样编写工作流的单元测试。
Dapr 边车不会加载任何工作流定义。相反,边车只是驱动工作流的执行,所有工作流活动都作为应用程序的一部分。
编写工作流活动
工作流活动 是工作流中的基本工作单元,是在业务流程中被编排的任务。
定义你希望工作流执行的工作流活动。活动是一个函数定义,可以接受输入和输出。以下示例创建了一个名为 hello_act 的计数器(活动),用于通知用户当前的计数器值。hello_act 是一个从名为 WorkflowActivityContext 的类派生的函数。
@wfr.activity(name='hello_act')
def hello_act(ctx: WorkflowActivityContext, wf_input):
global counter
counter += wf_input
print(f'New counter value is: {counter}!', flush=True)
定义你希望工作流执行的工作流活动。活动被包装在 WorkflowActivityContext 类中,该类实现了工作流活动。
export default class WorkflowActivityContext {
private readonly _innerContext: ActivityContext;
constructor(innerContext: ActivityContext) {
if (!innerContext) {
throw new Error("ActivityContext cannot be undefined");
}
this._innerContext = innerContext;
}
public getWorkflowInstanceId(): string {
return this._innerContext.orchestrationId;
}
public getWorkflowActivityId(): number {
return this._innerContext.taskId;
}
}
定义你希望工作流执行的工作流活动。活动是一个类定义,可以接受输入和输出。活动还参与依赖注入,例如绑定到 Dapr 客户端。
以下示例中调用的活动包括:
NotifyActivity:接收新订单通知。ReserveInventoryActivity:检查是否有足够的库存来满足新订单。ProcessPaymentActivity:处理订单付款。包括NotifyActivity用于发送成功订单通知。
NotifyActivity
public class NotifyActivity : WorkflowActivity<Notification, object>
{
//...
public NotifyActivity(ILoggerFactory loggerFactory)
{
this.logger = loggerFactory.CreateLogger<NotifyActivity>();
}
//...
}
查看完整的 NotifyActivity.cs 工作流活动示例。
ReserveInventoryActivity
public class ReserveInventoryActivity : WorkflowActivity<InventoryRequest, InventoryResult>
{
//...
public ReserveInventoryActivity(ILoggerFactory loggerFactory, DaprClient client)
{
this.logger = loggerFactory.CreateLogger<ReserveInventoryActivity>();
this.client = client;
}
//...
}
查看完整的 ReserveInventoryActivity.cs 工作流活动示例。
ProcessPaymentActivity
public class ProcessPaymentActivity : WorkflowActivity<PaymentRequest, object>
{
//...
public ProcessPaymentActivity(ILoggerFactory loggerFactory)
{
this.logger = loggerFactory.CreateLogger<ProcessPaymentActivity>();
}
//...
}
定义你希望工作流执行的工作流活动。活动被包装在公共 DemoWorkflowActivity 类中,该类实现了工作流活动。
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class DemoWorkflowActivity implements WorkflowActivity {
@Override
public DemoActivityOutput run(WorkflowActivityContext ctx) {
Logger logger = LoggerFactory.getLogger(DemoWorkflowActivity.class);
logger.info("Starting Activity: " + ctx.getName());
var message = ctx.getInput(DemoActivityInput.class).getMessage();
var newMessage = message + " World!, from Activity";
logger.info("Message Received from input: " + message);
logger.info("Sending message to output: " + newMessage);
logger.info("Sleeping for 5 seconds to simulate long running operation...");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
logger.info("Activity finished");
var output = new DemoActivityOutput(message, newMessage);
logger.info("Activity returned: " + output);
return output;
}
}
定义工作流活动
定义你希望工作流执行的每个工作流活动。活动输入可以使用 ctx.GetInput 从上下文中解组。活动应定义为接受 ctx workflow.ActivityContext 参数并返回接口和错误的函数。
func BusinessActivity(ctx workflow.ActivityContext) (any, error) {
var input int
if err := ctx.GetInput(&input); err != nil {
return "", err
}
// Do something here
return "result", nil
}
定义工作流
使用参数 ctx *workflow.WorkflowContext 定义工作流函数,并返回 any 和 error。从工作流内部调用你定义的活动。
func BusinessWorkflow(ctx *workflow.WorkflowContext) (any, error) {
var input int
if err := ctx.GetInput(&input); err != nil {
return nil, err
}
var output string
if err := ctx.CallActivity(BusinessActivity, workflow.ActivityInput(input)).Await(&output); err != nil {
return nil, err
}
if err := ctx.WaitForExternalEvent("businessEvent", time.Minute*60).Await(&output); err != nil {
return nil, err
}
if err := ctx.CreateTimer(time.Second).Await(nil); err != nil {
return nil, nil
}
return output, nil
}
注册工作流和活动
在你的应用程序可以执行工作流之前,你必须将工作流编排器和其活动注册到工作流注册表中。这确保 Dapr 知道在执行工作流时调用哪些函数。
func main() {
// Create a workflow registry
r := workflow.NewRegistry()
// Register the workflow orchestrator
if err := r.AddWorkflow(BusinessWorkflow); err != nil {
log.Fatal(err)
}
fmt.Println("BusinessWorkflow registered")
// Register the workflow activities
if err := r.AddActivity(BusinessActivity); err != nil {
log.Fatal(err)
}
fmt.Println("BusinessActivity registered")
// Create workflow client and start worker
wclient, err := client.NewWorkflowClient()
if err != nil {
log.Fatal(err)
}
fmt.Println("Worker initialized")
ctx, cancel := context.WithCancel(context.Background())
if err = wclient.StartWorker(ctx, r); err != nil {
log.Fatal(err)
}
fmt.Println("runner started")
// Your application logic continues here...
// Example: Start a workflow
instanceID, err := wclient.ScheduleWorkflow(ctx, "BusinessWorkflow", workflow.WithInput(1))
if err != nil {
log.Fatalf("failed to start workflow: %v", err)
}
fmt.Printf("workflow started with id: %v\n", instanceID)
// Stop workflow worker when done
cancel()
fmt.Println("workflow worker successfully shutdown")
}
关于注册的关键要点:
- 使用
workflow.NewRegistry()创建工作流注册表 - 使用
r.AddWorkflow()注册工作流函数 - 使用
r.AddActivity()注册活动函数 - 使用
client.NewWorkflowClient()创建工作流客户端 - 调用
wclient.StartWorker()开始处理工作流 - 使用
wclient.ScheduleWorkflow调度命名的工作流实例
编写工作流
接下来,在工作流中注册和调用活动。
hello_world_wf 函数是一个从名为 DaprWorkflowContext 的类派生的函数,具有输入和输出参数类型。它还包括一个 yield 语句,该语句完成工作流的核心逻辑并调用工作流活动。
@wfr.workflow(name='hello_world_wf')
def hello_world_wf(ctx: DaprWorkflowContext, wf_input):
print(f'{wf_input}')
yield ctx.call_activity(hello_act, input=1)
yield ctx.call_activity(hello_act, input=10)
yield ctx.call_activity(hello_retryable_act, retry_policy=retry_policy)
yield ctx.call_child_workflow(child_retryable_wf, retry_policy=retry_policy)
# Change in event handling: Use when_any to handle both event and timeout
event = ctx.wait_for_external_event(event_name)
timeout = ctx.create_timer(timedelta(seconds=30))
winner = yield when_any([event, timeout])
if winner == timeout:
print('Workflow timed out waiting for event')
return 'Timeout'
yield ctx.call_activity(hello_act, input=100)
yield ctx.call_activity(hello_act, input=1000)
return 'Completed'
接下来,向 WorkflowRuntime 类注册工作流并启动工作流运行时。
export default class WorkflowRuntime {
//..
// Register workflow implementation for handling orchestrations
public registerWorkflow(workflow: TWorkflow): WorkflowRuntime {
const name = getFunctionName(workflow);
const workflowWrapper = (ctx: OrchestrationContext, input: any): any => {
const workflowContext = new WorkflowContext(ctx);
return workflow(workflowContext, input);
};
this.worker.addNamedOrchestrator(name, workflowWrapper);
return this;
}
// Register workflow activities
public registerActivity(fn: TWorkflowActivity<TInput, TOutput>): WorkflowRuntime {
const name = getFunctionName(fn);
const activityWrapper = (ctx: ActivityContext, input: TInput): TOutput => {
const wfActivityContext = new WorkflowActivityContext(ctx);
return fn(wfActivityContext, input);
};
this.worker.addNamedActivity(name, activityWrapper);
return this;
}
// Start the workflow runtime processing items and block.
public async start() {
await this.worker.start();
}
}
OrderProcessingWorkflow 类是从名为 Workflow 的基类派生的,具有输入和输出参数类型。它还包括一个 RunAsync 方法,该方法完成工作流的核心逻辑并调用工作流活动。
class OrderProcessingWorkflow : Workflow<OrderPayload, OrderResult>
{
public override async Task<OrderResult> RunAsync(WorkflowContext context, OrderPayload order)
{
//...
await context.CallActivityAsync(
nameof(NotifyActivity),
new Notification($"Received order {orderId} for {order.Name} at {order.TotalCost:c}"));
//...
InventoryResult result = await context.CallActivityAsync<InventoryResult>(
nameof(ReserveInventoryActivity),
new InventoryRequest(RequestId: orderId, order.Name, order.Quantity));
//...
await context.CallActivityAsync(
nameof(ProcessPaymentActivity),
new PaymentRequest(RequestId: orderId, order.TotalCost, "USD"));
await context.CallActivityAsync(
nameof(NotifyActivity),
new Notification($"Order {orderId} processed successfully!"));
// End the workflow with a success result
return new OrderResult(Processed: true);
}
}
接下来,向 WorkflowRuntimeBuilder 注册工作流并启动工作流运行时。
public class DemoWorkflowWorker {
public static void main(String[] args) throws Exception {
// Register the Workflow with the builder.
WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder().registerWorkflow(DemoWorkflow.class);
builder.registerActivity(DemoWorkflowActivity.class);
// Build and then start the workflow runtime pulling and executing tasks
try (WorkflowRuntime runtime = builder.build()) {
System.out.println("Start workflow runtime");
runtime.start();
}
System.exit(0);
}
}
使用参数 ctx *workflow.WorkflowContext 定义工作流函数,并返回 any 和 error。从工作流内部调用你定义的活动。
func BusinessWorkflow(ctx *workflow.WorkflowContext) (any, error) {
var input int
if err := ctx.GetInput(&input); err != nil {
return nil, err
}
var output string
if err := ctx.CallActivity(BusinessActivity, workflow.ActivityInput(input)).Await(&output); err != nil {
return nil, err
}
if err := ctx.WaitForExternalEvent("businessEvent", time.Minute*60).Await(&output); err != nil {
return nil, err
}
if err := ctx.CreateTimer(time.Second).Await(nil); err != nil {
return nil, nil
}
return output, nil
}
编写应用程序
最后,使用工作流编写应用程序。
在以下示例中,对于使用 Python SDK 的基本 Python hello world 应用程序,你的项目代码应包括:
- 一个名为
DaprClient的 Python 包,用于接收 Python SDK 功能。 - 一个带有扩展的构建器:
- API 调用。在以下示例中,这些调用启动、暂停、恢复、清除和完成工作流。
from datetime import timedelta
from time import sleep
from dapr.ext.workflow import (
WorkflowRuntime,
DaprWorkflowContext,
WorkflowActivityContext,
RetryPolicy,
DaprWorkflowClient,
when_any,
)
from dapr.conf import Settings
from dapr.clients.exceptions import DaprInternalError
settings = Settings()
counter = 0
retry_count = 0
child_orchestrator_count = 0
child_orchestrator_string = ''
child_act_retry_count = 0
instance_id = 'exampleInstanceID'
child_instance_id = 'childInstanceID'
workflow_name = 'hello_world_wf'
child_workflow_name = 'child_wf'
input_data = 'Hi Counter!'
event_name = 'event1'
event_data = 'eventData'
non_existent_id_error = 'no such instance exists'
retry_policy = RetryPolicy(
first_retry_interval=timedelta(seconds=1),
max_number_of_attempts=3,
backoff_coefficient=2,
max_retry_interval=timedelta(seconds=10),
retry_timeout=timedelta(seconds=100),
)
wfr = WorkflowRuntime()
@wfr.workflow(name='hello_world_wf')
def hello_world_wf(ctx: DaprWorkflowContext, wf_input):
print(f'{wf_input}')
yield ctx.call_activity(hello_act, input=1)
yield ctx.call_activity(hello_act, input=10)
yield ctx.call_activity(hello_retryable_act, retry_policy=retry_policy)
yield ctx.call_child_workflow(child_retryable_wf, retry_policy=retry_policy)
# Change in event handling: Use when_any to handle both event and timeout
event = ctx.wait_for_external_event(event_name)
timeout = ctx.create_timer(timedelta(seconds=30))
winner = yield when_any([event, timeout])
if winner == timeout:
print('Workflow timed out waiting for event')
return 'Timeout'
yield ctx.call_activity(hello_act, input=100)
yield ctx.call_activity(hello_act, input=1000)
return 'Completed'
@wfr.activity(name='hello_act')
def hello_act(ctx: WorkflowActivityContext, wf_input):
global counter
counter += wf_input
print(f'New counter value is: {counter}!', flush=True)
@wfr.activity(name='hello_retryable_act')
def hello_retryable_act(ctx: WorkflowActivityContext):
global retry_count
if (retry_count % 2) == 0:
print(f'Retry count value is: {retry_count}!', flush=True)
retry_count += 1
raise ValueError('Retryable Error')
print(f'Retry count value is: {retry_count}! This print statement verifies retry', flush=True)
retry_count += 1
@wfr.workflow(name='child_retryable_wf')
def child_retryable_wf(ctx: DaprWorkflowContext):
global child_orchestrator_string, child_orchestrator_count
if not ctx.is_replaying:
child_orchestrator_count += 1
print(f'Appending {child_orchestrator_count} to child_orchestrator_string!', flush=True)
child_orchestrator_string += str(child_orchestrator_count)
yield ctx.call_activity(
act_for_child_wf, input=child_orchestrator_count, retry_policy=retry_policy
)
if child_orchestrator_count < 3:
raise ValueError('Retryable Error')
@wfr.activity(name='act_for_child_wf')
def act_for_child_wf(ctx: WorkflowActivityContext, inp):
global child_orchestrator_string, child_act_retry_count
inp_char = chr(96 + inp)
print(f'Appending {inp_char} to child_orchestrator_string!', flush=True)
child_orchestrator_string += inp_char
if child_act_retry_count % 2 == 0:
child_act_retry_count += 1
raise ValueError('Retryable Error')
child_act_retry_count += 1
def main():
wfr.start()
wf_client = DaprWorkflowClient()
print('==========Start Counter Increase as per Input:==========')
wf_client.schedule_new_workflow(
workflow=hello_world_wf, input=input_data, instance_id=instance_id
)
wf_client.wait_for_workflow_start(instance_id)
# Sleep to let the workflow run initial activities
sleep(12)
assert counter == 11
assert retry_count == 2
assert child_orchestrator_string == '1aa2bb3cc'
# Pause Test
wf_client.pause_workflow(instance_id=instance_id)
metadata = wf_client.get_workflow_state(instance_id=instance_id)
print(f'Get response from {workflow_name} after pause call: {metadata.runtime_status.name}')
# Resume Test
wf_client.resume_workflow(instance_id=instance_id)
metadata = wf_client.get_workflow_state(instance_id=instance_id)
print(f'Get response from {workflow_name} after resume call: {metadata.runtime_status.name}')
sleep(2) # Give the workflow time to reach the event wait state
wf_client.raise_workflow_event(instance_id=instance_id, event_name=event_name, data=event_data)
print('========= Waiting for Workflow completion', flush=True)
try:
state = wf_client.wait_for_workflow_completion(instance_id, timeout_in_seconds=30)
if state.runtime_status.name == 'COMPLETED':
print('Workflow completed! Result: {}'.format(state.serialized_output.strip('"')))
else:
print(f'Workflow failed! Status: {state.runtime_status.name}')
except TimeoutError:
print('*** Workflow timed out!')
wf_client.purge_workflow(instance_id=instance_id)
try:
wf_client.get_workflow_state(instance_id=instance_id)
except DaprInternalError as err:
if non_existent_id_error in err._message:
print('Instance Successfully Purged')
sleep(10000)
wfr.shutdown()
if __name__ == '__main__':
main()
以下示例 是使用 JavaScript SDK 的基本 JavaScript 应用程序。与此示例一样,你的项目代码应包括:
- 一个带有扩展的构建器:
- API 调用。以下示例是一个使用工作流 API 的简单项目:
mkdir my-wf && cd my-wf
npm init -y
npm i @dapr/dapr @microsoft/durabletask-js
npm i -D typescript ts-node @types/node
创建以下 tsconfig.json 文件:
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src"]
}
创建以下 src/app.ts 文件:
import {
WorkflowRuntime,
WorkflowActivityContext,
WorkflowContext,
DaprWorkflowClient,
TWorkflow
} from "@dapr/dapr";
const workflowClient = new DaprWorkflowClient();
const workflowRuntime = new WorkflowRuntime();
// simple activity
const hello = async (_: WorkflowActivityContext, name: string) => `Hello ${name}!`;
// simple workflow: call the activity 3 times
const sequence: TWorkflow = async function* (ctx: WorkflowContext): any {
const out: string[] = [];
out.push(yield ctx.callActivity(hello, "Tokyo"));
out.push(yield ctx.callActivity(hello, "Seattle"));
out.push(yield ctx.callActivity(hello, "London"));
out.push(yield ctx.waitForExternalEvent("continue"));
return out;
};
async function main() {
workflowRuntime.registerWorkflow(sequence).registerActivity(hello);
await workflowRuntime.start();
const id = await workflowClient.scheduleNewWorkflow(sequence);
console.log("Scheduled:", id);
workflowClient.raiseEvent(id, "continue", "Go go go!");
const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30);
console.log("Done:", state?.runtimeStatus, "output:", state?.serializedOutput);
await new Promise(f => setTimeout(f, 100000));
await workflowRuntime.stop();
await workflowClient.stop();
}
main().catch((e) => { console.error(e); });
在以下 Program.cs 示例中,对于使用 .NET SDK 的基本 ASP.NET 订单处理应用程序,你的项目代码应包括:
- 一个名为
Dapr.Workflow的 NuGet 包,用于接收 .NET SDK 功能 - 一个带有扩展方法
AddDaprWorkflow的构建器- 这允许你注册工作流和工作流活动(工作流可以调度的任务)
- HTTP API 调用
- 一个用于提交新订单
- 一个用于检查现有订单的状态
using Dapr.Workflow;
//...
// Dapr Workflows are registered as part of the service configuration
builder.Services.AddDaprWorkflow(options =>
{
// Note that it's also possible to register a lambda function as the workflow
// or activity implementation instead of a class.
options.RegisterWorkflow<OrderProcessingWorkflow>();
// These are the activities that get invoked by the workflow(s).
options.RegisterActivity<NotifyActivity>();
options.RegisterActivity<ReserveInventoryActivity>();
options.RegisterActivity<ProcessPaymentActivity>();
});
WebApplication app = builder.Build();
// POST starts new order workflow instance
app.MapPost("/orders", async (DaprWorkflowClient client, [FromBody] OrderPayload orderInfo) =>
{
if (orderInfo?.Name == null)
{
return Results.BadRequest(new
{
message = "Order data was missing from the request",
example = new OrderPayload("Paperclips", 99.95),
});
}
//...
});
// GET fetches state for order workflow to report status
app.MapGet("/orders/{orderId}", async (string orderId, DaprWorkflowClient client) =>
{
WorkflowState state = await client.GetWorkflowStateAsync(orderId, true);
if (!state.Exists)
{
return Results.NotFound($"No order with ID = '{orderId}' was found.");
}
var httpResponsePayload = new
{
details = state.ReadInputAs<OrderPayload>(),
status = state.RuntimeStatus.ToString(),
result = state.ReadOutputAs<OrderResult>(),
};
//...
}).WithName("GetOrderInfoEndpoint");
app.Run();
如以下示例所示,使用 Java SDK 和 Dapr Workflow 的 hello-world 应用程序应包括:
- 一个名为
io.dapr.workflows.client的 Java 包,用于接收 Java SDK 客户端功能。 - 导入
io.dapr.workflows.Workflow DemoWorkflow类,它扩展了Workflow- 使用输入和输出创建工作流。
- API 调用。在以下示例中,这些调用启动并调用工作流活动。
package io.dapr.examples.workflows;
import com.microsoft.durabletask.CompositeTaskFailedException;
import com.microsoft.durabletask.Task;
import com.microsoft.durabletask.TaskCanceledException;
import io.dapr.workflows.Workflow;
import io.dapr.workflows.WorkflowStub;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
/**
* Implementation of the DemoWorkflow for the server side.
*/
public class DemoWorkflow extends Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
ctx.getLogger().info("Starting Workflow: " + ctx.getName());
// ...
ctx.getLogger().info("Calling Activity...");
var input = new DemoActivityInput("Hello Activity!");
var output = ctx.callActivity(DemoWorkflowActivity.class.getName(), input, DemoActivityOutput.class).await();
// ...
};
}
}
如以下示例所示,使用 Go SDK 和 Dapr Workflow 的 hello-world 应用程序应包括:
- 一个名为
client的 Go 包,用于接收 Go SDK 客户端功能。 BusinessWorkflow方法- 使用输入和输出创建工作流。
- API 调用。在以下示例中,这些调用启动并调用工作流活动。
package main
import (
"context"
"errors"
"fmt"
"log"
"strconv"
"time"
"github.com/dapr/durabletask-go/workflow"
"github.com/dapr/go-sdk/client"
)
var stage = 0
var failActivityTries = 0
func main() {
r := workflow.NewRegistry()
if err := r.AddWorkflow(BusinessWorkflow); err != nil {
log.Fatal(err)
}
fmt.Println("BusinessWorkflow registered")
if err := r.AddActivity(BusinessActivity); err != nil {
log.Fatal(err)
}
fmt.Println("BusinessActivity registered")
if err := r.AddActivity(FailActivity); err != nil {
log.Fatal(err)
}
fmt.Println("FailActivity registered")
wclient, err := client.NewWorkflowClient()
if err != nil {
log.Fatal(err)
}
fmt.Println("Worker initialized")
ctx, cancel := context.WithCancel(context.Background())
if err = wclient.StartWorker(ctx, r); err != nil {
log.Fatal(err)
}
fmt.Println("runner started")
// Start workflow test
// Set the start time to the current time to not wait for the workflow to
// "start". This is useful for increasing the throughput of creating
// workflows.
// workflow.WithStartTime(time.Now())
instanceID, err := wclient.ScheduleWorkflow(ctx, "BusinessWorkflow", workflow.WithInstanceID("a7a4168d-3a1c-41da-8a4f-e7f6d9c718d9"), workflow.WithInput("1"))
if err != nil {
log.Fatalf("failed to start workflow: %v", err)
}
fmt.Printf("workflow started with id: %v\n", instanceID)
// Pause workflow test
err = wclient.SuspendWorkflow(ctx, instanceID, "")
if err != nil {
log.Fatalf("failed to pause workflow: %v", err)
}
respFetch, err := wclient.FetchWorkflowMetadata(ctx, instanceID, workflow.WithFetchPayloads(true))
if err != nil {
log.Fatalf("failed to fetch workflow: %v", err)
}
if respFetch.RuntimeStatus != workflow.StatusSuspended {
log.Fatalf("workflow not paused: %s: %v", respFetch.RuntimeStatus, respFetch)
}
fmt.Printf("workflow paused\n")
// Resume workflow test
err = wclient.ResumeWorkflow(ctx, instanceID, "")
if err != nil {
log.Fatalf("failed to resume workflow: %v", err)
}
respFetch, err = wclient.FetchWorkflowMetadata(ctx, instanceID, workflow.WithFetchPayloads(true))
if err != nil {
log.Fatalf("failed to get workflow: %v", err)
}
if respFetch.RuntimeStatus != workflow.StatusRunning {
log.Fatalf("workflow not running")
}
fmt.Println("workflow resumed")
fmt.Printf("stage: %d\n", stage)
// Raise Event
err = wclient.RaiseEvent(ctx, instanceID, "businessEvent", workflow.WithEventPayload("testData"))
if err != nil {
fmt.Printf("failed to raise event: %v", err)
}
fmt.Println("workflow event raised")
time.Sleep(time.Second) // allow workflow to advance
fmt.Printf("stage: %d\n", stage)
_, err = wclient.WaitForWorkflowCompletion(ctx, instanceID)
if err != nil {
log.Fatalf("failed to wait for workflow: %v", err)
}
fmt.Printf("fail activity executions: %d\n", failActivityTries)
respFetch, err = wclient.FetchWorkflowMetadata(ctx, instanceID, workflow.WithFetchPayloads(true))
if err != nil {
log.Fatalf("failed to get workflow: %v", err)
}
fmt.Printf("workflow status: %v\n", respFetch.String())
// Purge workflow test
err = wclient.PurgeWorkflowState(ctx, instanceID)
if err != nil {
log.Fatalf("failed to purge workflow: %v", err)
}
respFetch, err = wclient.FetchWorkflowMetadata(ctx, instanceID, workflow.WithFetchPayloads(true))
if err == nil || respFetch != nil {
log.Fatalf("failed to purge workflow: %v", err)
}
fmt.Println("workflow purged")
fmt.Printf("stage: %d\n", stage)
// Terminate workflow test
id, err := wclient.ScheduleWorkflow(ctx, "BusinessWorkflow", workflow.WithInstanceID("a7a4168d-3a1c-41da-8a4f-e7f6d9c718d9"), workflow.WithInput("1"))
if err != nil {
log.Fatalf("failed to start workflow: %v", err)
}
fmt.Printf("workflow started with id: %v\n", instanceID)
metadata, err := wclient.WaitForWorkflowStart(ctx, id)
if err != nil {
log.Fatalf("failed to get workflow: %v", err)
}
fmt.Printf("workflow status: %s\n", metadata.String())
err = wclient.TerminateWorkflow(ctx, id)
if err != nil {
log.Fatalf("failed to terminate workflow: %v", err)
}
fmt.Println("workflow terminated")
err = wclient.PurgeWorkflowState(ctx, id)
if err != nil {
log.Fatalf("failed to purge workflow: %v", err)
}
fmt.Println("workflow purged")
<-ctx.Done()
cancel()
fmt.Println("workflow worker successfully shutdown")
}
func BusinessWorkflow(ctx *workflow.WorkflowContext) (any, error) {
var input string
if err := ctx.GetInput(&input); err != nil {
return nil, err
}
var output string
if err := ctx.CallActivity(BusinessActivity, workflow.WithActivityInput(input)).Await(&output); err != nil {
return nil, err
}
err := ctx.WaitForExternalEvent("businessEvent", time.Minute*60).Await(&output)
if err != nil {
return nil, err
}
if err := ctx.CallActivity(BusinessActivity, workflow.WithActivityInput(input)).Await(&output); err != nil {
return nil, err
}
if err := ctx.CallActivity(FailActivity, workflow.WithActivityRetryPolicy(&workflow.RetryPolicy{
MaxAttempts: 3,
InitialRetryInterval: 100 * time.Millisecond,
BackoffCoefficient: 2,
MaxRetryInterval: 1 * time.Second,
})).Await(nil); err == nil {
return nil, fmt.Errorf("unexpected no error executing fail activity")
}
return output, nil
}
func BusinessActivity(ctx workflow.ActivityContext) (any, error) {
var input string
if err := ctx.GetInput(&input); err != nil {
return "", err
}
iinput, err := strconv.Atoi(input)
if err != nil {
return "", err
}
stage += iinput
return fmt.Sprintf("Stage: %d", stage), nil
}
func FailActivity(ctx workflow.ActivityContext) (any, error) {
failActivityTries += 1
return nil, errors.New("dummy activity error")
}
重要提示
由于基于重放的工作流的执行方式,你需要在活动内部编写执行 I/O 和与系统交互等逻辑的代码。同时,工作流方法仅用于编排这些活动。运行工作流并使用 Diagrid Dashboard 检查工作流执行
通过你的 IDE 或 Dapr CLI 启动工作流应用程序(如果你想启动多个应用程序,请使用 Dapr 多应用运行;如果只启动一个应用程序,请使用常规 Dapr run 命令),然后调度一个新的工作流实例。
使用本地 Diagrid Dashboard 可视化和检查你的工作流状态,并深入查看详细的工作流执行历史。仪表板作为容器运行,连接到 Dapr 工作流使用的状态存储(默认情况下是本地 Redis 实例)。

使用 Docker 启动 Diagrid Dashboard 容器:
docker run -p 8080:8080 ghcr.io/diagridio/diagrid-dashboard:latest
Note
如果你使用的状态存储不是默认的 Redis 实例,你需要提供一些额外的参数来运行容器,请参阅 Diagrid Dashboard 参考文档。在浏览器中打开仪表板,地址为 http://localhost:8080。
通过 Dapr CLI 测试工作流
编写工作流后,你可以使用 Dapr CLI 进行测试:
运行工作流应用程序
dapr run --app-id workflow-app python3 app.py
确保应用程序正在运行:
dapr list
运行工作流
dapr workflow run hello_world_wf --app-id workflow-app --input 'hello world' --instance-id test-run
检查工作流状态
dapr workflow list --app-id workflow-app -o wide
查看已完成的工作流
dapr workflow list --app-id workflow-app --filter-status COMPLETED -o wide
查看工作流历史
dapr workflow history --app-id workflow-app test-run
运行工作流应用程序
dapr run --app-id workflow-app npx ts-node src/app.ts
确保应用程序正在运行:
dapr list
运行工作流
dapr workflow run sequence --app-id workflow-app --input 'hello world' --instance-id test-run
检查工作流状态
dapr workflow list --app-id workflow-app -o wide
触发等待的外部事件
dapr workflow raise-event --app-id workflow-app test-run/businessEvent
查看已完成的工作流
dapr workflow list --app-id workflow-app --filter-status COMPLETED -o wide
查看工作流历史
dapr workflow history --app-id workflow-app test-run
运行工作流应用程序
dapr run --app-id workflow-app dotnet run
确保应用程序正在运行:
dapr list
运行工作流
dapr workflow run OrderProcessingWorkflow --app-id workflow-app --instance-id test-run --input '{"name": "Paperclips", "totalCost": 99.95}'
检查工作流状态
dapr workflow list --app-id workflow-app -o wide
触发等待的外部事件
dapr workflow raise-event --app-id workflow-app test-run/incoming-purchase-order --input '{"name": "Paperclips", "totalCost": 99.95}'
查看已完成的工作流
dapr workflow list --app-id workflow-app --filter-status COMPLETED -o wide
查看工作流历史
dapr workflow history --app-id workflow-app test-run
运行工作流应用程序
dapr run --app-id workflow-app -- java -jar target/WorkflowService-0.0.1-SNAPSHOT.jar
确保应用程序正在运行:
dapr list
运行工作流
dapr workflow run DemoWorkflow --app-id workflow-app --instance-id test-run --input "input data"
检查工作流状态
dapr workflow list --app-id workflow-app -o wide
触发等待的外部事件
dapr workflow raise-event --app-id workflow-app test-run/TestEvent --input 'TestEventPayload'
dapr workflow raise-event --app-id workflow-app test-run/event1 --input 'TestEvent 1 Payload'
dapr workflow raise-event --app-id workflow-app test-run/event2 --input 'TestEvent 2 Payload'
dapr workflow raise-event --app-id workflow-app test-run/event3 --input 'TestEvent 3 Payload'
查看已完成的工作流
dapr workflow list --app-id workflow-app --filter-status COMPLETED -o wide
查看工作流历史
dapr workflow history --app-id workflow-app test-run
运行工作流应用程序
dapr run --app-id workflow-app go run main.go
确保应用程序正在运行:
dapr list
运行工作流
dapr workflow run BusinessWorkflow --app-id workflow-app --input '1' --instance-id test-run
检查工作流状态
dapr workflow list --app-id workflow-app -o wide
触发等待的外部事件
dapr workflow raise-event --app-id workflow-app test-run/businessEvent
查看已完成的工作流
dapr workflow list --app-id workflow-app --filter-status COMPLETED -o wide
查看工作流历史
dapr workflow history test-run --app-id workflow-app
监控工作流执行
dapr workflow list --app-id workflow-app --filter-status RUNNING -o wide
dapr workflow list --app-id workflow-app --filter-status FAILED -o wide
dapr workflow list --app-id workflow-app --filter-status COMPLETED -o wide
测试外部事件
# 触发你的工作流正在等待的事件
dapr workflow raise-event <instance-id>/ApprovalReceived \
--app-id workflow-app \
--input '{"approved": true, "approver": "manager@company.com"}'
调试失败的工作流
# 列出失败的工作流
dapr workflow list --app-id workflow-app --filter-status FAILED --output wide
# 获取失败工作流的详细历史
dapr workflow history <failed-instance-id> --app-id workflow-app --output json
# 修复问题后重新运行工作流
dapr workflow rerun <failed-instance-id> --app-id workflow-app --input '<new-input-json-data>'
下一步
现在你已经编写了一个工作流,学习如何管理它。
管理工作流 >>相关链接
1.3.7 - 操作指南:管理工作流
现在您已经在应用程序中编写了工作流及其活动,可以使用 CLI 或 API 调用来启动、终止、重运行和获取工作流信息。
使用 Dapr CLI 管理工作流
Dapr CLI 提供了在自托管和 Kubernetes 环境中管理工作流实例的命令。
另请参阅工作流保留策略了解如何为已完成的工作流配置保留策略。
基本工作流操作
启动工作流
# 使用 `orderprocessing` 应用程序,启动一个新的工作流实例并传入输入数据
dapr workflow run OrderProcessingWorkflow \
--app-id orderprocessing \
--input '{"orderId": "12345", "amount": 100.50}'
# 使用特定实例 ID 启动新工作流
dapr workflow run OrderProcessingWorkflow \
--app-id orderprocessing \
--instance-id order-12345 \
--input '{"orderId": "12345"}'
# 计划在 2024 年 12 月 25 日上午 10:00:00(协调世界时 UTC)启动新工作流
dapr workflow run OrderProcessingWorkflow \
--app-id orderprocessing \
--start-time "2024-12-25T10:00:00Z"
列出工作流实例
# 列出应用的所有工作流
dapr workflow list
# 按状态筛选
dapr workflow list --filter-status RUNNING
# 按工作流名称和应用 ID 筛选
dapr workflow list --app-id orderprocessing --filter-name OrderProcessingWorkflow
# 按时间筛选(过去 24 小时内启动的工作流)
dapr workflow list --filter-max-age 24h
# 获取详细输出
dapr workflow list -o wide
查看工作流历史
# 获取执行历史
dapr workflow history order-12345
# 在特定应用 ID 上以 JSON 格式获取历史
dapr workflow history order-12345 --app-id orderprocessing --output json
控制工作流执行
# 暂停正在运行的工作流
dapr workflow suspend order-12345 \
--app-id orderprocessing \
--reason "Waiting for manual approval"
# 恢复已暂停的工作流
dapr workflow resume order-12345 \
--app-id orderprocessing \
--reason "Approved by manager"
# 终止工作流
dapr workflow terminate order-12345 \
--app-id orderprocessing \
--output '{"reason": "Cancelled by customer"}'
触发外部事件
# 为等待中的工作流触发事件
dapr workflow raise-event order-12345/PaymentReceived \
--app-id orderprocessing \
--input '{"paymentId": "pay-67890", "amount": 100.50}'
重运行工作流
# 从头重运行
dapr workflow rerun order-12345
# 从特定事件 ID 重运行(通过 history 命令发现)
dapr workflow rerun order-12345 --event-id 5
# 使用新的指定实例 ID 重运行
dapr workflow rerun order-12345 --new-instance-id order-12345-retry
清理已完成的工作流
请注意,从 CLI 清理工作流也会删除所有关联的 Scheduler 提醒。
重要
<p>应用程序中必须运行工作流客户端才能执行清理操作。</p>
需要工作流客户端连接才能保持工作流状态机的完整性,防止数据损坏。 以下错误表明工作流客户端未运行:
failed to purge orchestration state: rpc error: code = FailedPrecondition desc = failed to purge orchestration state: failed to lookup actor: api error: code = FailedPrecondition desc = did not find address for actor
可以使用 --force 标志在没有工作流应用程序运行的情况下清理工作流;但是,这只应在您确定没有工作流实例正在运行时使用,否则将会损坏工作流状态机。
# 清理特定实例
dapr workflow purge order-12345
# 清理 30 天前已完成的所有工作流
dapr workflow purge --all-older-than 720h
# 清理所有终结状态的工作流(谨慎使用!)
dapr workflow purge --app-id orderprocessing --all
# 在没有运行工作流客户端的情况下强制清理(极度谨慎使用!)
dapr workflow purge order-12345 --force
Kubernetes 操作
所有命令都支持 Kubernetes 部署的 -k 标志:
# 在 Kubernetes 中列出工作流
dapr workflow list \
--kubernetes \
--namespace production \
--app-id orderprocessing
# 在 Kubernetes 中暂停工作流
dapr workflow suspend order-12345 \
--kubernetes \
--namespace production \
--app-id orderprocessing \
--reason "Maintenance window"
列出工作流
在自托管模式下,只需运行:
dapr workflow list
在 Kubernetes 模式下,需要指定 --kubernetes/-k 标志以及命名空间和应用 ID:
dapr workflow list -k
工作流管理最佳实践
监控正在运行的工作流:使用筛选列表跟踪长期运行的实例
dapr workflow list --app-id orderprocessing --filter-status RUNNING --filter-max-age 24h使用实例 ID:分配有意义的实例 ID 以便更轻松地跟踪
dapr workflow run OrderWorkflow --app-id orderprocessing --instance-id "order-$(date +%s)"导出以供分析:导出工作流数据以供分析
dapr workflow list --app-id orderprocessing --output json > workflows.json
使用 Dapr CLI 管理工作流提醒
工作流提醒存储在 Scheduler 中,可以使用 dapr scheduler CLI 进行管理。
列出工作流提醒
dapr scheduler list --filter workflow
NAME BEGIN COUNT LAST TRIGGER
workflow/my-app/instance1/timer-0-ABC123 +50.0h 0
workflow/my-app/instance2/timer-0-XYZ789 +50.0h 0
获取提醒详情
dapr scheduler get workflow/my-app/instance1/timer-0-ABC123 -o yaml
删除工作流提醒
删除单个提醒:
dapr scheduler delete workflow/my-app/instance1/timer-0-ABC123
删除给定工作流应用的所有提醒:
dapr scheduler delete-all workflow/my-app
删除特定工作流实例的所有提醒:
dapr scheduler delete-all workflow/my-app/instance1
备份和恢复提醒
导出所有提醒:
dapr scheduler export -o workflow-reminders-backup.bin
从备份文件恢复:
dapr scheduler import -f workflow-reminders-backup.bin
在代码中管理工作流。在编写工作流指南的工作流示例中,工作流使用以下 API 注册到代码中:
- schedule_new_workflow:启动工作流实例
- get_workflow_state:获取工作流状态信息
- pause_workflow:暂停或挂起工作流实例,以便后续恢复
- resume_workflow:恢复已暂停的工作流实例
- raise_workflow_event:向工作流触发事件
- purge_workflow:移除与特定工作流实例相关的所有元数据
- wait_for_workflow_completion:等待特定工作流实例完成
from dapr.ext.workflow import WorkflowRuntime, DaprWorkflowContext, WorkflowActivityContext
from dapr.clients import DaprClient
# 合理的参数
instanceId = "exampleInstanceID"
workflowComponent = "dapr"
workflowName = "hello_world_wf"
eventName = "event1"
eventData = "eventData"
# 启动工作流
wf_client.schedule_new_workflow(
workflow=hello_world_wf, input=input_data, instance_id=instance_id
)
# 获取工作流信息
wf_client.get_workflow_state(instance_id=instance_id)
# 暂停工作流
wf_client.pause_workflow(instance_id=instance_id)
metadata = wf_client.get_workflow_state(instance_id=instance_id)
# 恢复工作流
wf_client.resume_workflow(instance_id=instance_id)
# 向工作流触发事件
wf_client.raise_workflow_event(instance_id=instance_id, event_name=event_name, data=event_data)
# 清理工作流
wf_client.purge_workflow(instance_id=instance_id)
# 等待工作流完成
wf_client.wait_for_workflow_completion(instance_id, timeout_in_seconds=30)
在代码中管理工作流。在编写工作流指南的工作流示例中,工作流使用以下 API 注册到代码中:
- client.workflow.start:启动工作流实例
- client.workflow.get:获取工作流状态信息
- client.workflow.pause:暂停或挂起工作流实例,以便后续恢复
- client.workflow.resume:恢复已暂停的工作流实例
- client.workflow.purge:移除与特定工作流实例相关的所有元数据
- client.workflow.terminate:终止或停止工作流的特定实例
import { DaprClient } from "@dapr/dapr";
async function printWorkflowStatus(client: DaprClient, instanceId: string) {
const workflow = await client.workflow.get(instanceId);
console.log(
`Workflow ${workflow.workflowName}, created at ${workflow.createdAt.toUTCString()}, has status ${
workflow.runtimeStatus
}`,
);
console.log(`Additional properties: ${JSON.stringify(workflow.properties)}`);
console.log("--------------------------------------------------\n\n");
}
async function start() {
const client = new DaprClient();
// 启动新的工作流实例
const instanceId = await client.workflow.start("OrderProcessingWorkflow", {
Name: "Paperclips",
TotalCost: 99.95,
Quantity: 4,
});
console.log(`Started workflow instance ${instanceId}`);
await printWorkflowStatus(client, instanceId);
// 暂停工作流实例
await client.workflow.pause(instanceId);
console.log(`Paused workflow instance ${instanceId}`);
await printWorkflowStatus(client, instanceId);
// 恢复工作流实例
await client.workflow.resume(instanceId);
console.log(`Resumed workflow instance ${instanceId}`);
await printWorkflowStatus(client, instanceId);
// 终止工作流实例
await client.workflow.terminate(instanceId);
console.log(`Terminated workflow instance ${instanceId}`);
await printWorkflowStatus(client, instanceId);
// 等待工作流完成,30 秒!
await new Promise((resolve) => setTimeout(resolve, 30000));
await printWorkflowStatus(client, instanceId);
// 清理工作流实例
await client.workflow.purge(instanceId);
console.log(`Purged workflow instance ${instanceId}`);
// 这会抛出错误,因为工作流实例已不存在
await printWorkflowStatus(client, instanceId);
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
在代码中管理工作流。在编写工作流指南的 OrderProcessingWorkflow 示例中,工作流已注册到代码中。现在您可以启动、终止和获取运行中工作流的信息:
string orderId = "exampleOrderId";
OrderPayload input = new OrderPayload("Paperclips", 99.95);
Dictionary<string, string> workflowOptions; // 这是一个可选参数
// 使用 orderId 作为工作流 ID 启动工作流。这返回一个字符串,包含特定工作流实例的实例 ID,无论我们是自行提供还是由系统生成
await daprWorkflowClient.ScheduleNewWorkflowAsync(nameof(OrderProcessingWorkflow), orderId, input, workflowOptions);
// 获取工作流信息。此响应包含工作流状态、启动时间等信息!
WorkflowState currentState = await daprWorkflowClient.GetWorkflowStateAsync(orderId, orderId);
// 终止工作流
await daprWorkflowClient.TerminateWorkflowAsync(orderId);
// 触发事件(传入的采购订单),工作流将等待该事件
await daprWorkflowClient.RaiseEventAsync(orderId, "incoming-purchase-order", input);
// 暂停
await daprWorkflowClient.SuspendWorkflowAsync(orderId);
// 恢复
await daprWorkflowClient.ResumeWorkflowAsync(orderId);
// 清理工作流,从关联实例中移除所有收件箱和历史信息
await daprWorkflowClient.PurgeInstanceAsync(orderId);
在代码中管理工作流。在 Java SDK 的工作流示例中,工作流使用以下 API 注册到代码中:
- scheduleNewWorkflow:启动新的工作流实例
- getInstanceState:获取工作流状态信息
- waitForInstanceStart:暂停或挂起工作流实例,以便后续恢复
- raiseEvent:为运行中的工作流实例触发事件/任务
- waitForInstanceCompletion:等待工作流完成任务
- purgeInstance:移除与特定工作流实例相关的所有元数据
- terminateWorkflow:终止工作流
- purgeInstance:移除与特定工作流相关的所有元数据
package io.dapr.examples.workflows;
import io.dapr.workflows.client.DaprWorkflowClient;
import io.dapr.workflows.client.WorkflowInstanceStatus;
// ...
public class DemoWorkflowClient {
// ...
public static void main(String[] args) throws InterruptedException {
DaprWorkflowClient client = new DaprWorkflowClient();
try (client) {
// 启动工作流
String instanceId = client.scheduleNewWorkflow(DemoWorkflow.class, "input data");
// 获取工作流的状态信息
WorkflowInstanceStatus workflowMetadata = client.getInstanceState(instanceId, true);
// 等待或暂停工作流实例启动
try {
WorkflowInstanceStatus waitForInstanceStartResult =
client.waitForInstanceStart(instanceId, Duration.ofSeconds(60), true);
}
// 为工作流触发事件;您可以并行触发多个事件
client.raiseEvent(instanceId, "TestEvent", "TestEventPayload");
client.raiseEvent(instanceId, "event1", "TestEvent 1 Payload");
client.raiseEvent(instanceId, "event2", "TestEvent 2 Payload");
client.raiseEvent(instanceId, "event3", "TestEvent 3 Payload");
// 等待工作流完成任务
try {
WorkflowInstanceStatus waitForInstanceCompletionResult =
client.waitForInstanceCompletion(instanceId, Duration.ofSeconds(60), true);
}
// 清理工作流实例,移除与其关联的所有元数据
boolean purgeResult = client.purgeInstance(instanceId);
// 终止工作流实例
client.terminateWorkflow(instanceToTerminateId, null);
System.exit(0);
}
}
在代码中管理工作流。在 Go SDK 的工作流示例中,工作流使用以下 API 注册到代码中:
- StartWorkflow:启动新的工作流实例
- GetWorkflow:获取工作流状态信息
- PauseWorkflow:暂停或挂起工作流实例,以便后续恢复
- RaiseEventWorkflow:为运行中的工作流实例触发事件/任务
- ResumeWorkflow:等待工作流完成任务
- PurgeWorkflow:移除与特定工作流实例相关的所有元数据
- TerminateWorkflow:终止工作流
// 启动工作流
type StartWorkflowRequest struct {
InstanceID string // 可选的实例标识符
WorkflowComponent string
WorkflowName string
Options map[string]string // 可选的元数据
Input any // 可选的输入
SendRawInput bool // 设置为 True 以禁用输入上的序列化
}
type StartWorkflowResponse struct {
InstanceID string
}
// 获取工作流状态
type GetWorkflowRequest struct {
InstanceID string
WorkflowComponent string
}
type GetWorkflowResponse struct {
InstanceID string
WorkflowName string
CreatedAt time.Time
LastUpdatedAt time.Time
RuntimeStatus string
Properties map[string]string
}
// 清理工作流
type PurgeWorkflowRequest struct {
InstanceID string
WorkflowComponent string
}
// 终止工作流
type TerminateWorkflowRequest struct {
InstanceID string
WorkflowComponent string
}
// 暂停工作流
type PauseWorkflowRequest struct {
InstanceID string
WorkflowComponent string
}
// 恢复工作流
type ResumeWorkflowRequest struct {
InstanceID string
WorkflowComponent string
}
// 为运行中的工作流触发事件
type RaiseEventWorkflowRequest struct {
InstanceID string
WorkflowComponent string
EventName string
EventData any
SendRawData bool // 设置为 True 以禁用数据上的序列化
}
使用 HTTP 调用管理工作流。下面的示例将编写工作流示例中的属性与随机实例 ID 结合使用。
启动工作流
使用 ID 12345678 启动工作流,运行:
curl -X POST "http://localhost:3500/v1.0/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678"
请注意,工作流实例 ID 只能包含字母数字字符、下划线和破折号。
终止工作流
使用 ID 12345678 终止工作流,运行:
curl -X POST "http://localhost:3500/v1.0/workflows/dapr/12345678/terminate"
触发事件
对于支持订阅外部事件的工作流组件(如 Dapr Workflow engine),您可以使用以下"触发事件" API 将命名事件传递到特定的工作流实例。
curl -X POST "http://localhost:3500/v1.0/workflows/<workflowComponentName>/<instanceID>/raiseEvent/<eventName>"
eventName可以是任意函数。
暂停或恢复工作流
为了计划停机时间、等待输入等,您可以暂停然后恢复工作流。使用 ID 12345678 暂停工作流直到触发恢复,运行:
curl -X POST "http://localhost:3500/v1.0/workflows/dapr/12345678/pause"
使用 ID 12345678 恢复工作流,运行:
curl -X POST "http://localhost:3500/v1.0/workflows/dapr/12345678/resume"
清理工作流
清理 API 可用于从底层状态存储中永久删除工作流元数据,包括所有存储的输入、输出和工作流历史记录。这通常有助于实现数据保留策略和释放资源。
只有处于 COMPLETED、FAILED 或 TERMINATED 状态的工作流实例才能被清理。如果工作流处于任何其他状态,调用清理将返回错误。
curl -X POST "http://localhost:3500/v1.0/workflows/dapr/12345678/purge"
获取工作流信息
使用 ID 12345678 获取工作流信息(输出和输入),运行:
curl -X GET "http://localhost:3500/v1.0/workflows/dapr/12345678"
下一步
现在您已了解如何管理工作流,学习如何在多个应用程序中执行工作流
多应用程序工作流>>相关链接
1.3.8 - 多应用工作流
单个工作流通常跨越多个应用程序、微服务或编程语言。 在这种情况下,活动或子工作流将在与托管父工作流不同的应用程序上执行。
这非常有用的一些场景包括:
- 机器学习(ML)训练活动必须在启用 GPU 的机器上执行,而工作流的其余部分在仅支持 CPU 的编排机器上运行。
- 活动需要访问仅对特定身份或区域可用的敏感数据或凭据。
- 工作流的不同部分需要在不同的信任区域或网络中执行。
- 由于数据驻留要求,工作流的不同部分需要在不同的地理区域中执行。
- 涉及的业务流程跨越多个团队或部门,每个团队或部门拥有自己的应用程序。
- 基于团队专业知识或现有代码库,工作流的实现跨越不同的编程语言。
- 不同的团队边界或微服务所有权。

下图显示了一个复杂工作流的示例场景,该工作流跨多个用不同语言编写的应用程序进行编排。每个应用程序的主要步骤和活动包括:
• App1: 主工作流服务 - 协调整个 ML 管道的顶级编排器
- 启动流程
- 调用 App2 上的数据处理活动
- 调用 App3 上的 ML 训练子工作流
- 调用 App4 上的模型部署
- 结束完整工作流
- 语言: Java
• App2: 数据处理管道 - 仅限 GPU 活动
- 数据摄取活动(GPU 加速)
- 特征工程活动(GPU 加速)
- 向主工作流返回完成信号
- 语言: Go
• App3: ML 训练子工作流 - 包含子工作流和活动
- 子工作流编排:
- 数据处理活动
- 模型训练活动(GPU 密集型)
- 模型验证活动
- 由 App2 的活动完成触发
- 向主工作流返回完成信号
- 语言: Java
• App4: 模型服务 - 强大的 GPU 应用程序,仅包含活动
- 模型加载活动(GPU 内存密集型)
- 推理设置活动(GPU 加速推理)
- 由 App3 的工作流完成触发
- 向主工作流返回完成信号
- 语言: Go
多应用工作流
工作流执行路由基于托管 Dapr 应用程序的 App ID。 默认情况下,完整的工作流执行托管在启动该工作流的 App ID 上。该工作流可以在该 App ID 的任何副本上执行,而不仅仅是调度该工作流的单个副本。
可以通过在工作流执行代码中指定目标 App ID 参数,在不同的 App ID 上执行活动和子工作流。 执行时,目标 App ID 执行活动或子工作流,并将结果返回给原始 App ID 的父工作流。
整个工作流执行可以分布在多个 App ID 上,没有限制,每个活动或子工作流都可以指定目标 App ID。 工作流的最终历史记录将由托管最顶层父工作流(或可将其视为根工作流)的 App ID 保存。
限制
与 Dapr 中的其他 API 构建块和资源一样,工作流限定在单个命名空间内。 这意味着参与多应用工作流的所有 App ID 必须位于同一命名空间中。 同样,所有 App ID 必须使用相同的工作流(或 actor)状态存储。 最后,目标 App ID 必须定义并注册该活动或子工作流,否则父工作流将无限重试。注意
多应用工作流需要 Dapr 运行时 v1.16.0 或更高版本。.NET SDK 支持从 v1.17.0 开始可用。重要限制
支持多应用工作流的 SDK - 多应用工作流通过 SDK 使用。 目前支持以下 SDK:
- Java(仅活动调用)
- Go(同时支持活动和子工作流调用)
- Python(同时支持活动和子工作流调用)
- .NET(同时支持活动和子工作流调用,需要 .NET SDK v1.17.0+)
- JavaScript SDK 支持计划在未来的版本中提供
错误处理
调用多应用活动或子工作流时:
- 如果目标应用程序不存在,将使用提供的重试策略重试调用。
- 如果目标应用程序存在但不包含指定的活动或工作流,调用将返回错误。
- 标准工作流重试策略适用于多应用调用。
拥有不同 App ID 的团队之间必须进行协调,以确保活动和子工作流在需要时已定义并可用,这一点至关重要。
持久活动结果
活动通常需要一定的时间来完成,或者在资源或美元成本上执行起来很昂贵。 因此,即使在异常路径中,也不希望对同一轮次执行这些活动超过一次。 在 1.17 之前的多应用场景中,活动会通过网络调用将响应发布给托管拥有工作流的其他应用程序。 在托管工作流应用程序关闭或无法访问的情况下,结果将丢失,活动将被重试,从而导致活动的重复执行。
在 1.17 中,启用 `WorkflowsRemoteActivityReminder feature gate 将使活动结果在托管工作流应用程序处于离线或无法访问时,通过提醒发送给拥有该工作流的应用程序,从而确保结果不会丢失并避免重复执行。 在所有应用程序上使用 Dapr 1.17 版本的所有用户都应启用此选项。 为了在 Dapr 版本之间保持向后兼容性,该选项默认处于 禁用 状态,但将在未来的版本中默认启用。
多应用活动示例

以下示例展示如何在目标应用程序 App2 上执行活动 ActivityA。
func BusinessWorkflow(ctx *workflow.WorkflowContext) (any, error) {
var output string
err := ctx.CallActivity("ActivityA",
workflow.WithActivityInput("my-input"),
workflow.WithActivityAppID("App2"), // 这里我们设置将执行此活动的目标 App ID。
).Await(&output)
if err != nil {
return nil, err
}
return output, nil
}
public class BusinessWorkflow implements Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
String output = ctx.callActivity(
ActivityA.class.getName(),
"my-input",
new WorkflowTaskOptions("App2"), // 这里我们设置将执行此活动的目标 App ID。
String.class
).await();
ctx.complete(output);
};
}
}
@wfr.workflow
def app1_workflow(ctx: wf.DaprWorkflowContext):
output = yield ctx.call_activity('ActivityA', input='my-input', app_id='App2')
return output
public sealed class BusinessWorkflow : Workflow<string, string>
{
public override async Task<string> RunAsync(WorkflowContext context, string input)
{
var options = new WorkflowTaskOptions { TargetAppId = "App2" };
var output = await context.CallActivityAsync<string>(nameof(ActivityA), input, options);
return output;
}
}
多应用子工作流示例

以下示例展示如何在目标应用程序 App2 上执行子工作流 Workflow2。
func BusinessWorkflow(ctx *workflow.WorkflowContext) (any, error) {
var output string
err := ctx.CallChildWorkflow("Workflow2",
workflow.WithChildWorkflowInput("my-input"),
workflow.WithChildWorkflowAppID("App2"), // 这里我们设置将执行此子工作流的目标 App ID。
).Await(&output)
if err != nil {
return nil, err
}
return output, nil
}
@wfr.workflow
def workflow1(ctx: wf.DaprWorkflowContext):
output = yield ctx.call_child_workflow(workflow='Workflow2', input='my-input', app_id='App2')
return output
public sealed class BusinessWorkflow : Workflow<string, string>
{
public override async Task<string> RunAsync(WorkflowContext context, string input)
{
var options = new ChildWorkflowTaskOptions { TargetAppId = "App2" };
var output = await context.CallChildWorkflowAsync<string>(nameof(Workflow2), input, options);
return output;
}
}
相关链接
1.3.9 - 历史保留策略
Dapr 工作流状态存储在[执行器状态存储]{{ %ref workflow-architecture.md#state-store-usage %}}中。 默认情况下,Dapr Workflows 会无限期保留工作流状态变更的完整历史记录。 这意味着历史工作流可以随时被查询和检查。 运行大量工作流或生成大量状态变更历史记录可能导致存储使用量增加,最终可能填满状态存储磁盘空间。
为帮助管理存储使用,Dapr Workflows 支持配置历史保留策略。
该保留策略定义了工作流状态变更历史在被删除前保留多长时间。
工作流只有在达到终态(Completed、Failed 或 Terminated)后才有资格被删除。
每个工作流终态都可以设置自定义的保留时长,也可以为未明确配置的终态设置默认保留时长。
时长定义为 Go duration string(例如,72h 表示 72 小时,30m 表示 30 分钟)。
为 Completed 工作流配置较短的保留时长可能很有用,同时为 Failed 和 Terminated 工作流保留较长时间以便进行调查。
以下示例配置设置了每个终态。
这里设置的 anyTerminal 属性不会生效,因为所有终态都已明确配置,但它包含在此处作为参考。
有关如何将配置应用到 Dapr 应用程序的更多信息,请参阅 Dapr 配置文档。
kind: Configuration
metadata:
name: appconfig
spec:
workflow:
stateRetentionPolicy:
anyTerminal: "360h"
completed: "1m"
failed: "720h"
terminated: "360h"
相关链接
1.3.10 - 工作流执行并发
您可以使用以下配置来设置在任何时候可以执行的最大并发工作流和活动数量。 这些限制是按每个边车实例实施的,这意味着如果您的工作流应用有 10 个副本,则有效限制将是配置值的 10 倍。
设置这些限制可以帮助防止您的 Dapr 边车和应用程序出现资源耗尽,或者在活动突发导致资源争用时帮助减少积压的工作流。 这些限制不会区分不同的工作流或活动定义,因此它们适用于在边车中运行的所有工作流和活动。
有关如何将配置应用到您的 Dapr 应用程序的更多信息,请参阅 Dapr 配置文档。
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: appconfig
spec:
workflow:
maxConcurrentWorkflowInvocations: 100 # 默认为无限
maxConcurrentActivityInvocations: 1000 # 默认为无限
相关链接
1.4 - 状态管理
1.4.1 - 状态管理概述
您的应用程序可以使用 Dapr 的状态管理 API 在受支持的状态存储中保存、读取和查询键值对。使用状态存储组件,您可以构建有状态的、长时间运行的应用程序,用于保存和检索其状态(例如购物车或游戏会话状态)。例如,在下图中:
- 使用 HTTP POST 来保存或查询键值对。
- 使用 HTTP GET 来读取特定键并获取其值。

以下概述视频和演示展示了 Dapr 状态管理的工作原理。
功能
通过状态管理 API 构建块,您的应用程序可以利用通常构建复杂且容易出错的特性,包括:
- 设置并发控制和数据一致性选项。
- 执行批量更新操作 CRUD,包括多个事务性操作。
- 查询和过滤键值数据。
以下是状态管理 API 提供的功能:
可插拔状态存储
Dapr 数据存储被建模为组件,可以在不更改服务代码的情况下进行替换。请参阅受支持的状态存储查看列表。
可配置的状态存储行为
使用 Dapr,您可以在状态操作请求中包含额外的元数据,用于描述您希望如何处理该请求。您可以附加:
- 并发要求
- 一致性要求
默认情况下,您的应用程序应假设数据存储是最终一致的,并使用**最后写入胜出(last-write-wins)**的并发模式。
并非所有存储都是平等的。为了确保应用程序的可移植性,您可以查询存储的元数据功能,并使代码适应不同的存储功能。
并发
Dapr 使用 ETags 支持乐观并发控制(OCC)。当请求状态值时,Dapr 始终将 ETag 属性附加到返回的状态。当用户代码:
- 更新状态时,预期会通过请求正文附加 ETag。
- 删除状态时,预期会通过
If-Match标头附加 ETag。
当提供的 ETag 与状态存储中的 ETag 匹配时,write 操作成功。
为什么 Dapr 选择乐观并发控制(OCC)
在许多应用程序中,数据更新冲突很少发生,因为客户端在业务上下文中自然分离以操作不同的数据。但是,如果您的应用程序选择使用 ETags,ETag 不匹配可能会导致请求被拒绝。建议您在代码中使用重试策略来补偿使用 ETags 时的冲突。
如果您的应用程序在写入请求中省略 ETags,Dapr 将在处理请求时跳过 ETag 检查。这将启用最后写入胜出模式,而使用 ETags 则是**首次写入胜出(first-write-wins)**模式。
关于 ETags 的说明
对于本身不支持 ETags 的存储,相应的 Dapr 状态存储实现应模拟 ETags,并在处理状态时遵循 Dapr 状态管理 API 规范。由于 Dapr 状态存储实现在技术上是底层数据存储的客户端,因此使用存储提供的并发控制机制进行模拟应该很简单。阅读 API 参考以了解如何设置并发选项。
一致性
Dapr 支持强一致性和最终一致性,默认行为是最终一致性。
- 强一致性:Dapr 等待所有副本(或指定的法定人数)确认后,才确认写入请求。
- 最终一致性:Dapr 在底层数据存储接受写入请求后立即返回,即使只是单个副本。
阅读 API 参考以了解如何设置一致性选项。
设置内容类型
状态存储组件可能根据内容类型以不同方式维护和操作数据。Dapr 支持在状态管理 API中传递内容类型作为请求元数据的一部分。
设置内容类型是_可选的_,组件决定是否使用它。Dapr 只提供将此信息传递给组件的手段。
- 使用 HTTP API:通过 URL 查询参数
metadata.contentType设置内容类型。例如,http://localhost:3500/v1.0/state/store?metadata.contentType=application/json。 - 使用 gRPC API:通过在请求元数据中添加键值对
"contentType" : <content type>来设置内容类型。
多操作
Dapr 支持两种类型的多读或多写操作:批量或事务。阅读 API 参考以了解如何使用批量和多选项。
批量读取操作
您可以将多个读取请求分组到批量(或批处理)操作中。在批量操作中,Dapr 将读取请求作为单独的请求提交给底层数据存储,并将它们作为单个结果返回。
事务操作
您可以将写入、更新和删除操作分组到一个请求中,然后作为原子事务处理。该请求将作为事务操作集成功或失败。
Actor 状态
事务状态存储可用于存储 actor 状态。要指定用于 actor 的状态存储,请在状态存储组件的元数据部分将属性 actorStateStore 的值指定为 true。Actor 状态以特定方案存储在事务状态存储中,从而允许一致的查询。所有 actor 只能使用单个状态存储组件作为状态存储。如果您的状态存储由分布式数据库支持,您必须确保它提供强一致性。阅读状态 API 参考和 actor API 参考以了解有关 actor 状态存储的更多信息。
Actor 状态的生存时间(TTL)
您在保存 actor 状态时,应始终设置 TTL 元数据字段(ttlInSeconds),或使用所选 SDK 中的等效 API 调用,以确保状态最终被删除。阅读 actor 概述以获取更多信息。
状态加密
Dapr 支持应用程序状态的自动客户端加密,并支持密钥轮换。这适用于所有 Dapr 状态存储。有关更多信息,请阅读如何:加密应用程序状态主题。
应用程序之间共享状态
不同的应用程序在共享状态方面的需求各不相同。在一种场景中,您可能希望将所有状态封装在给定的应用程序中,并让 Dapr 为您管理访问。在另一种场景中,您可能希望两个应用程序处理相同的状态以获取和保存相同的键。
Dapr 使状态能够:
- 隔离到应用程序。
- 在应用程序之间的状态存储中共享。
- 在不同状态存储之间的多个应用程序之间共享。
有关更多详细信息,请阅读如何:在应用程序之间共享状态
启用发件箱模式
Dapr 使开发者能够使用发件箱模式在事务状态存储和任何消息代理之间实现单个事务。有关更多信息,请阅读如何启用事务性发件箱消息传递
查询状态
有两种查询状态的方式:
- 使用 Dapr 运行时中提供的状态管理查询 API。
- 使用存储的原生 SDK 直接查询状态存储。
查询 API
使用_可选的_状态管理查询 API,您可以查询保存在状态存储中的键值数据,而不管底层数据库或存储技术如何。使用状态管理查询 API,您可以过滤、排序和分页键值数据。有关更多详细信息,请阅读如何:查询状态。
直接查询状态存储
Dapr 保存和检索状态值时不进行任何转换。您可以直接从底层状态存储查询和聚合状态。 例如,要在 Redis 中获取与应用程序 ID “myApp” 关联的所有状态键,请使用:
KEYS "myApp*"
关于直接查询的说明
由于您不是通过 Dapr 运行时调用,因此状态存储的直接查询不受 Dapr 并发控制约束。您看到的是已提交数据的快照,可用于跨多个 actor 的只读查询。写入应通过 Dapr 状态管理或 actor API 完成。查询 actor 状态
如果数据存储支持 SQL 查询,您可以使用 SQL 查询 actor 的状态。例如:
SELECT * FROM StateTable WHERE Id='<app-id>||<actor-type>||<actor-id>||<key>'
您还可以通过跨 actor 实例执行聚合查询来避免 actor 框架的常见基于回合的并发限制。例如,要计算所有温度计 actor 的平均温度,请使用:
SELECT AVG(value) FROM StateTable WHERE Id LIKE '<app-id>||<thermometer>||*||temperature'
状态生存时间(TTL)
Dapr 支持每个状态设置请求的生存时间(TTL)。这意味着应用程序可以为每个存储的状态设置生存时间,这些状态在过期后无法检索。
状态管理 API
状态管理 API 可在状态管理 API 参考中找到,该参考描述了如何通过提供键来检索、保存、删除和查询状态值。
尝试状态管理
快速入门和教程
想要测试 Dapr 状态管理 API?通过以下快速入门和教程了解状态管理的实际应用:
| 快速入门/教程 | 描述 |
|---|---|
| 状态管理快速入门 | 使用状态管理 API 创建有状态的应用程序。 |
| Hello World | 推荐 演示如何在本地运行 Dapr。突出显示服务调用和状态管理。 |
| Hello World Kubernetes | 推荐 演示如何在 Kubernetes 中运行 Dapr。突出显示服务调用和_状态管理_。 |
直接在应用程序中开始使用状态管理
想跳过快速入门?没问题。您可以直接在应用程序中试用状态管理构建块。安装 Dapr后,您可以从状态管理操作指南开始使用状态管理 API。
后续步骤
- 开始学习状态管理操作指南,从以下开始:
- 查看状态存储组件列表
- 阅读状态管理 API 参考
- 阅读actor API 参考
1.4.2 - 操作指南:保存和获取状态
状态管理是任何新应用程序、遗留应用程序、单体应用程序或微服务应用程序最常见的需求之一。处理和测试不同的数据库库以及处理重试和故障可能既困难又耗时。
在本指南中,您将学习使用键/值状态 API 的基础知识,使应用程序能够保存、获取和删除状态。
下面的代码示例大致描述了一个处理订单的应用程序,该应用程序使用带有 Dapr 边车的订单处理服务。订单处理服务使用 Dapr 将状态存储在 Redis 状态存储中。

设置状态存储
状态存储组件代表 Dapr 用于与数据库通信的资源。
为了本指南的目的,我们将使用 Redis 状态存储,但支持列表中的任何状态存储都可以使用。
当您在自托管模式下运行 dapr init 时,Dapr 会创建一个默认的 Redis statestore.yaml 并在您的本地机器上运行 Redis 状态存储,位于:
- 在 Windows 上,位于
%UserProfile%\.dapr\components\statestore.yaml - 在 Linux/MacOS 上,位于
~/.dapr/components/statestore.yaml
使用 statestore.yaml 组件,您可以轻松替换底层组件,而无需更改应用程序代码。
要将其部署到 Kubernetes 集群,请在下面的 YAML 中填写状态存储组件的 metadata 连接详细信息,保存为 statestore.yaml,然后运行 kubectl apply -f statestore.yaml。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
重要
设置一个app-id,因为状态键会以此值作为前缀。如果您不设置 app-id,系统会在运行时为您生成一个。下次运行该命令时,会生成一个新的 app-id,您将无法访问之前保存的状态。保存和检索单个状态
下面的示例展示了如何使用 Dapr 状态管理 API 保存和检索单个键/值对。
using System.Text;
using System.Threading.Tasks;
using Dapr.Client;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();
var random = new Random();
//从依赖注入注册中解析 DaprClient
using var client = app.Services.GetRequiredService<DaprClient>();
while(true)
{
await Task.Delay(TimeSpan.FromSeconds(5));
var orderId = random.Next(1,1000);
//使用 Dapr SDK 保存和获取状态
await client.SaveStateAsync(DAPR_STORE_NAME, "order_1", orderId.ToString());
await client.SaveStateAsync(DAPR_STORE_NAME, "order_2", orderId.ToString());
var result = await client.GetStateAsync<string>(DAPR_STORE_NAME, "order_1");
Console.WriteLine($"Result after get: {result}");
}
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 dotnet run
//依赖项
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.State;
import io.dapr.client.domain.TransactionalStateOperation;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import java.util.Random;
import java.util.concurrent.TimeUnit;
//代码
@SpringBootApplication
public class OrderProcessingServiceApplication {
private static final Logger log = LoggerFactory.getLogger(OrderProcessingServiceApplication.class);
private static final String STATE_STORE_NAME = "statestore";
public static void main(String[] args) throws InterruptedException{
while(true) {
TimeUnit.MILLISECONDS.sleep(5000);
Random random = new Random();
int orderId = random.nextInt(1000-1) + 1;
DaprClient client = new DaprClientBuilder().build();
//使用 Dapr SDK 保存和获取状态
client.saveState(STATE_STORE_NAME, "order_1", Integer.toString(orderId)).block();
client.saveState(STATE_STORE_NAME, "order_2", Integer.toString(orderId)).block();
Mono<State<String>> result = client.getState(STATE_STORE_NAME, "order_1", String.class);
log.info("Result after get" + result);
}
}
}
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 mvn spring-boot:run
#依赖项
import random
from time import sleep
import requests
import logging
from dapr.clients import DaprClient
from dapr.clients.grpc._state import StateItem
from dapr.clients.grpc._request import TransactionalStateOperation, TransactionOperationType
#代码
logging.basicConfig(level = logging.INFO)
DAPR_STORE_NAME = "statestore"
while True:
sleep(random.randrange(50, 5000) / 1000)
orderId = random.randint(1, 1000)
with DaprClient() as client:
#使用 Dapr SDK 保存和获取状态
client.save_state(DAPR_STORE_NAME, "order_1", str(orderId))
result = client.get_state(DAPR_STORE_NAME, "order_1")
logging.info('Result after get: ' + result.data.decode('utf-8'))
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 -- python3 OrderProcessingService.py
// 依赖项
import (
"context"
"log"
"math/rand"
"strconv"
"time"
dapr "github.com/dapr/go-sdk/client"
)
// 代码
func main() {
const STATE_STORE_NAME = "statestore"
rand.Seed(time.Now().UnixMicro())
for i := 0; i < 10; i++ {
orderId := rand.Intn(1000-1) + 1
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
defer client.Close()
ctx := context.Background()
err = client.SaveState(ctx, STATE_STORE_NAME, "order_1", []byte(strconv.Itoa(orderId)), nil)
if err != nil {
panic(err)
}
result, err := client.GetState(ctx, STATE_STORE_NAME, "order_1", nil)
if err != nil {
panic(err)
}
log.Println("Result after get:", string(result.Value))
time.Sleep(2 * time.Second)
}
}
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 go run OrderProcessingService.go
//依赖项
import { DaprClient, HttpMethod, CommunicationProtocolEnum } from '@dapr/dapr';
//代码
const daprHost = "127.0.0.1";
var main = function() {
for(var i=0;i<10;i++) {
sleep(5000);
var orderId = Math.floor(Math.random() * (1000 - 1) + 1);
start(orderId).catch((e) => {
console.error(e);
process.exit(1);
});
}
}
async function start(orderId) {
const client = new DaprClient({
daprHost,
daprPort: process.env.DAPR_HTTP_PORT,
communicationProtocol: CommunicationProtocolEnum.HTTP,
});
const STATE_STORE_NAME = "statestore";
//使用 Dapr SDK 保存和获取状态
await client.state.save(STATE_STORE_NAME, [
{
key: "order_1",
value: orderId.toString()
},
{
key: "order_2",
value: orderId.toString()
}
]);
var result = await client.state.get(STATE_STORE_NAME, "order_1");
console.log("Result after get: " + result);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
main();
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 npm start
启动 Dapr 边车:
dapr run --app-id orderprocessing --dapr-http-port 3601
在单独的终端中,将一个键/值对保存到您的状态存储中:
curl -X POST -H "Content-Type: application/json" -d '[{ "key": "order_1", "value": "250"}]' http://localhost:3601/v1.0/state/statestore
现在获取您刚刚保存的状态:
curl http://localhost:3601/v1.0/state/statestore/order_1
重启您的边车并尝试再次检索状态,以观察状态独立于应用程序持久存在。
启动 Dapr 边车:
dapr --app-id orderprocessing --dapr-http-port 3601 run
在单独的终端中,将一个键/值对保存到您的状态存储中:
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '[{"key": "order_1", "value": "250"}]' -Uri 'http://localhost:3601/v1.0/state/statestore'
现在获取您刚刚保存的状态:
Invoke-RestMethod -Uri 'http://localhost:3601/v1.0/state/statestore/order_1'
重启您的边车并尝试再次检索状态,以观察状态独立于应用程序持久存在。
删除状态
以下是利用 Dapr SDK 删除状态的代码示例。
using Dapr.Client;
using System.Threading.Tasks;
const string DAPR_STORE_NAME = "statestore";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();
//从依赖注入注册中解析 DaprClient
using var client = app.Services.GetRequiredService<DaprClient>();
//使用 DaprClient 删除状态
await client.DeleteStateAsync(DAPR_STORE_NAME, "order_1", cancellationToken: cancellationToken);
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 dotnet run
//依赖项
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import org.springframework.boot.autoconfigure.SpringBootApplication;
//代码
@SpringBootApplication
public class OrderProcessingServiceApplication {
public static void main(String[] args) throws InterruptedException{
String STATE_STORE_NAME = "statestore";
//使用 Dapr SDK 删除状态
DaprClient client = new DaprClientBuilder().build();
String storedEtag = client.getState(STATE_STORE_NAME, "order_1", String.class).block().getEtag();
client.deleteState(STATE_STORE_NAME, "order_1", storedEtag, null).block();
}
}
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 mvn spring-boot:run
#依赖项
from dapr.clients.grpc._request import TransactionalStateOperation, TransactionOperationType
#代码
logging.basicConfig(level = logging.INFO)
DAPR_STORE_NAME = "statestore"
#使用 Dapr SDK 删除状态
with DaprClient() as client:
client.delete_state(store_name=DAPR_STORE_NAME, key="order_1")
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 -- python3 OrderProcessingService.py
//依赖项
import (
"context"
dapr "github.com/dapr/go-sdk/client"
)
//代码
func main() {
STATE_STORE_NAME := "statestore"
//使用 Dapr SDK 删除状态
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
defer client.Close()
ctx := context.Background()
if err := client.DeleteState(ctx, STATE_STORE_NAME, "order_1"); err != nil {
panic(err)
}
}
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 go run OrderProcessingService.go
//依赖项
import { DaprClient, HttpMethod, CommunicationProtocolEnum } from '@dapr/dapr';
//代码
const daprHost = "127.0.0.1";
var main = function() {
const STATE_STORE_NAME = "statestore";
//使用 Dapr SDK 保存和获取状态
const client = new DaprClient({
daprHost,
daprPort: process.env.DAPR_HTTP_PORT,
communicationProtocol: CommunicationProtocolEnum.HTTP,
});
await client.state.delete(STATE_STORE_NAME, "order_1");
}
main();
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 npm start
使用与上面相同的 Dapr 实例运行,执行以下命令:
curl -X DELETE 'http://localhost:3601/v1.0/state/statestore/order_1'
尝试再次获取状态。请注意,没有返回任何值。
使用与上面相同的 Dapr 实例运行,执行以下命令:
Invoke-RestMethod -Method Delete -Uri 'http://localhost:3601/v1.0/state/statestore/order_1'
尝试再次获取状态。请注意,没有返回任何值。
保存和检索多个状态
以下是利用 Dapr SDK 保存和检索多个状态的代码示例。
using Dapr.Client;
using System.Threading.Tasks;
const string DAPR_STORE_NAME = "statestore";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();
//从依赖注入注册中解析 DaprClient
using var client = app.Services.GetRequiredService<DaprClient>();
IReadOnlyList<BulkStateItem> multipleStateResult = await client.GetBulkStateAsync(DAPR_STORE_NAME, new List<string> { "order_1", "order_2" }, parallelism: 1);
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 dotnet run
上面的示例返回一个 BulkStateItem,其中包含您保存到状态的值的序列化格式。如果您希望 SDK 在每个批量响应项中对值进行反序列化,可以改为使用以下代码:
using Dapr.Client;
using System.Threading.Tasks;
const string DAPR_STORE_NAME = "statestore";
var builder = WebApplication.CreateBuilder(args);
builder.Serivces.AddDaprClient();
var app = builder.Build();
//从依赖注入注册中解析 DaprClient
using var client = app.Services.GetRequiredService<DaprClient>();
IReadOnlyList<BulkStateItem<Widget>> mulitpleStateResult = await client.GetBulkStateAsync<Widget>(DAPR_STORE_NAME, new List<string> { "widget_1", "widget_2" }, parallelism: 1);
record Widget(string Size, string Color);
//依赖项
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.State;
import java.util.Arrays;
//代码
@SpringBootApplication
public class OrderProcessingServiceApplication {
private static final Logger log = LoggerFactory.getLogger(OrderProcessingServiceApplication.class);
public static void main(String[] args) throws InterruptedException{
String STATE_STORE_NAME = "statestore";
//使用 Dapr SDK 检索多个状态
DaprClient client = new DaprClientBuilder().build();
Mono<List<State<String>>> resultBulk = client.getBulkState(STATE_STORE_NAME,
Arrays.asList("order_1", "order_2"), String.class);
}
}
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 mvn spring-boot:run
#依赖项
from dapr.clients import DaprClient
from dapr.clients.grpc._state import StateItem
#代码
logging.basicConfig(level = logging.INFO)
DAPR_STORE_NAME = "statestore"
orderId = 100
#使用 Dapr SDK 保存和检索多个状态
with DaprClient() as client:
client.save_bulk_state(store_name=DAPR_STORE_NAME, states=[StateItem(key="order_2", value=str(orderId))])
result = client.get_bulk_state(store_name=DAPR_STORE_NAME, keys=["order_1", "order_2"], states_metadata={"metakey": "metavalue"}).items
logging.info('Result after get bulk: ' + str(result))
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 -- python3 OrderProcessingService.py
// 依赖项
import (
"context"
"log"
"math/rand"
"strconv"
"time"
dapr "github.com/dapr/go-sdk/client"
)
// 代码
func main() {
const STATE_STORE_NAME = "statestore"
rand.Seed(time.Now().UnixMicro())
for i := 0; i < 10; i++ {
orderId := rand.Intn(1000-1) + 1
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
defer client.Close()
ctx := context.Background()
err = client.SaveState(ctx, STATE_STORE_NAME, "order_1", []byte(strconv.Itoa(orderId)), nil)
if err != nil {
panic(err)
}
keys := []string{"key1", "key2", "key3"}
items, err := client.GetBulkState(ctx, STATE_STORE_NAME, keys, nil, 100)
if err != nil {
panic(err)
}
for _, item := range items {
log.Println("Item from GetBulkState:", string(item.Value))
}
}
}
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 go run OrderProcessingService.go
//依赖项
import { DaprClient, HttpMethod, CommunicationProtocolEnum } from '@dapr/dapr';
//代码
const daprHost = "127.0.0.1";
var main = function() {
const STATE_STORE_NAME = "statestore";
var orderId = 100;
//使用 Dapr SDK 保存和检索多个状态
const client = new DaprClient({
daprHost,
daprPort: process.env.DAPR_HTTP_PORT,
communicationProtocol: CommunicationProtocolEnum.HTTP,
});
await client.state.save(STATE_STORE_NAME, [
{
key: "order_1",
value: orderId.toString()
},
{
key: "order_2",
value: orderId.toString()
}
]);
result = await client.state.getBulk(STATE_STORE_NAME, ["order_1", "order_2"]);
}
main();
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 npm start
使用与上面相同的 Dapr 实例运行,将两个键/值对保存到您的状态存储中:
curl -X POST -H "Content-Type: application/json" -d '[{ "key": "order_1", "value": "250"}, { "key": "order_2", "value": "550"}]' http://localhost:3601/v1.0/state/statestore
现在获取您刚刚保存的状态:
curl -X POST -H "Content-Type: application/json" -d '{"keys":["order_1", "order_2"]}' http://localhost:3601/v1.0/state/statestore/bulk
使用与上面相同的 Dapr 实例运行,将两个键/值对保存到您的状态存储中:
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '[{ "key": "order_1", "value": "250"}, { "key": "order_2", "value": "550"}]' -Uri 'http://localhost:3601/v1.0/state/statestore'
现在获取您刚刚保存的状态:
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '{"keys":["order_1", "order_2"]}' -Uri 'http://localhost:3601/v1.0/state/statestore/bulk'
执行状态事务
注意
状态事务需要支持多项目事务的状态存储。有关完整列表,请参阅支持的状态存储页面。以下是利用 Dapr SDK 执行状态事务的代码示例。
using Dapr.Client;
using System.Threading.Tasks;
const string DAPR_STORE_NAME = "statestore";
var builder = WebApplication.CreateBuilder(args);
builder.Serivces.AddDaprClient();
var app = builder.Build();
//从依赖注入注册中解析 DaprClient
using var client = app.Services.GetRequiredService<DaprClient>();
var random = new Random();
while (true)
{
await Task.Delay(TimeSpan.FromSeconds(5));
var orderId = random.Next(1, 1000);
var requests = new List<StateTransactionRequest>
{
new StateTransactionRequest("order_3", JsonSerializer.SerializeToUtf8Bytes(orderId.ToString()), StateOperationType.Upsert),
new StateTransactionRequest("order_2", null, StateOperationType.Delete)
};
var cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;
//使用 DaprClient 执行状态事务
await client.ExecuteStateTransactionAsync(DAPR_STORE_NAME, requests, cancellationToken: cancellationToken);
Console.WriteLine($"Order requested: {orderId}");
Console.WriteLine($"Result: {result}");
}
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 dotnet run
//依赖项
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.State;
import io.dapr.client.domain.TransactionalStateOperation;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
//代码
@SpringBootApplication
public class OrderProcessingServiceApplication {
private static final Logger log = LoggerFactory.getLogger(OrderProcessingServiceApplication.class);
private static final String STATE_STORE_NAME = "statestore";
public static void main(String[] args) throws InterruptedException{
while(true) {
TimeUnit.MILLISECONDS.sleep(5000);
Random random = new Random();
int orderId = random.nextInt(1000-1) + 1;
DaprClient client = new DaprClientBuilder().build();
List<TransactionalStateOperation<?>> operationList = new ArrayList<>();
operationList.add(new TransactionalStateOperation<>(TransactionalStateOperation.OperationType.UPSERT,
new State<>("order_3", Integer.toString(orderId), "")));
operationList.add(new TransactionalStateOperation<>(TransactionalStateOperation.OperationType.DELETE,
new State<>("order_2")));
//使用 Dapr SDK 执行状态事务
client.executeStateTransaction(STATE_STORE_NAME, operationList).block();
log.info("Order requested: " + orderId);
}
}
}
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 mvn spring-boot:run
#依赖项
import random
from time import sleep
import requests
import logging
from dapr.clients import DaprClient
from dapr.clients.grpc._state import StateItem
from dapr.clients.grpc._request import TransactionalStateOperation, TransactionOperationType
#代码
logging.basicConfig(level = logging.INFO)
DAPR_STORE_NAME = "statestore"
while True:
sleep(random.randrange(50, 5000) / 1000)
orderId = random.randint(1, 1000)
with DaprClient() as client:
#使用 Dapr SDK 执行状态事务
client.execute_state_transaction(store_name=DAPR_STORE_NAME, operations=[
TransactionalStateOperation(
operation_type=TransactionOperationType.upsert,
key="order_3",
data=str(orderId)),
TransactionalStateOperation(key="order_3", data=str(orderId)),
TransactionalStateOperation(
operation_type=TransactionOperationType.delete,
key="order_2",
data=str(orderId)),
TransactionalStateOperation(key="order_2", data=str(orderId))
])
client.delete_state(store_name=DAPR_STORE_NAME, key="order_1")
logging.basicConfig(level = logging.INFO)
logging.info('Order requested: ' + str(orderId))
logging.info('Result: ' + str(result))
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 -- python3 OrderProcessingService.py
// 依赖项
package main
import (
"context"
"log"
"math/rand"
"strconv"
"time"
dapr "github.com/dapr/go-sdk/client"
)
// 代码
func main() {
const STATE_STORE_NAME = "statestore"
rand.Seed(time.Now().UnixMicro())
for i := 0; i < 10; i++ {
orderId := rand.Intn(1000-1) + 1
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
defer client.Close()
ctx := context.Background()
err = client.SaveState(ctx, STATE_STORE_NAME, "order_1", []byte(strconv.Itoa(orderId)), nil)
if err != nil {
panic(err)
}
result, err := client.GetState(ctx, STATE_STORE_NAME, "order_1", nil)
if err != nil {
panic(err)
}
ops := make([]*dapr.StateOperation, 0)
data1 := "data1"
data2 := "data2"
op1 := &dapr.StateOperation{
Type: dapr.StateOperationTypeUpsert,
Item: &dapr.SetStateItem{
Key: "key1",
Value: []byte(data1),
},
}
op2 := &dapr.StateOperation{
Type: dapr.StateOperationTypeDelete,
Item: &dapr.SetStateItem{
Key: "key2",
Value: []byte(data2),
},
}
ops = append(ops, op1, op2)
meta := map[string]string{}
err = client.ExecuteStateTransaction(ctx, STATE_STORE_NAME, meta, ops)
log.Println("Result after get:", string(result.Value))
time.Sleep(2 * time.Second)
}
}
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 go run OrderProcessingService.go
//依赖项
import { DaprClient, HttpMethod, CommunicationProtocolEnum } from '@dapr/dapr';
//代码
const daprHost = "127.0.0.1";
var main = function() {
for(var i=0;i<10;i++) {
sleep(5000);
var orderId = Math.floor(Math.random() * (1000 - 1) + 1);
start(orderId).catch((e) => {
console.error(e);
process.exit(1);
});
}
}
async function start(orderId) {
const client = new DaprClient({
daprHost,
daprPort: process.env.DAPR_HTTP_PORT,
communicationProtocol: CommunicationProtocolEnum.HTTP,
});
const STATE_STORE_NAME = "statestore";
//使用 Dapr SDK 保存和检索多个状态
await client.state.transaction(STATE_STORE_NAME, [
{
operation: "upsert",
request: {
key: "order_3",
value: orderId.toString()
}
},
{
operation: "delete",
request: {
key: "order_2"
}
}
]);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
main();
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 npm start
使用与上面相同的 Dapr 实例运行,执行两个状态事务:
curl -X POST -H "Content-Type: application/json" -d '{"operations": [{"operation":"upsert", "request": {"key": "order_1", "value": "250"}}, {"operation":"delete", "request": {"key": "order_2"}}]}' http://localhost:3601/v1.0/state/statestore/transaction
现在查看您的状态事务的结果:
curl -X POST -H "Content-Type: application/json" -d '{"keys":["order_1", "order_2"]}' http://localhost:3601/v1.0/state/statestore/bulk
使用与上面相同的 Dapr 实例运行,将两个键/值对保存到您的状态存储中:
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '{"operations": [{"operation":"upsert", "request": {"key": "order_1", "value": "250"}}, {"operation":"delete", "request": {"key": "order_2"}}]}' -Uri 'http://localhost:3601/v1.0/state/statestore/transaction'
现在查看您的状态事务的结果:
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '{"keys":["order_1", "order_2"]}' -Uri 'http://localhost:3601/v1.0/state/statestore/bulk'
后续步骤
1.4.3 - 操作方法:查询状态
alpha
状态查询 API 目前处于 alpha 阶段。通过状态查询 API,你可以检索、筛选和排序存储在状态存储组件中的键/值数据。查询 API 不是完整查询语言的替代品。
尽管状态存储是键/值存储,但 value 可能是一个具有自身层次结构、键和值的 JSON 文档。查询 API 允许你使用这些键/值来检索相应的文档。
查询状态
通过 HTTP POST/PUT 或 gRPC 提交查询请求。请求体是包含 3 个条目的 JSON 映射:
filtersortpage
filter
filter 以树的形式指定查询条件,其中每个节点代表一元或多操作数操作。
支持以下操作:
| 操作符 | 操作数 | 描述 |
|---|---|---|
EQ | key:value | key == value |
NEQ | key:value | key != value |
GT | key:value | key > value |
GTE | key:value | key >= value |
LT | key:value | key < value |
LTE | key:value | key <= value |
IN | key:[]value | key == value[0] OR key == value[1] OR … OR key == value[n] |
AND | []operation | operation[0] AND operation[1] AND … AND operation[n] |
OR | []operation | operation[0] OR operation[1] OR … OR operation[n] |
操作数中的 key 类似于 JSONPath 表示法。键中的每个点表示嵌套的 JSON 结构。例如,考虑以下结构:
{
"shape": {
"name": "rectangle",
"dimensions": {
"height": 24,
"width": 10
},
"color": {
"name": "red",
"code": "#FF0000"
}
}
}
要比较颜色代码的值,键将是 shape.color.code。
如果省略 filter 部分,查询将返回所有条目。
sort
sort 是一个有序的 key:order 对数组,其中:
key是状态存储中的键order是一个可选字符串,指示排序顺序:"ASC"表示升序"DESC"表示降序
如果省略,默认为升序。
page
page 包含 limit 和 token 参数。
limit设置页面大小。token是组件返回的迭代令牌,用于后续查询。
在后台,此查询请求被转换为原生查询语言并由状态存储组件执行。
示例数据和查询
让我们看一些从简单到复杂的实际示例。
作为数据集,考虑一个包含员工 ID、组织、州和城市的 员工记录集合。请注意,该数据集是一个键/值对数组,其中:
key是唯一 IDvalue是包含员工记录的 JSON 对象。
为了更好地说明功能,组织名称 和员工 ID 是一个嵌套的 JSON person 对象。
首先创建一个 MongoDB 实例作为你的状态存储。
docker run -d --rm -p 27017:27017 --name mongodb mongo:5
接下来,启动一个 Dapr 应用程序。请参阅 组件配置文件,该文件指示 Dapr 使用 MongoDB 作为其状态存储。
dapr run --app-id demo --dapr-http-port 3500 --resources-path query-api-examples/components/mongodb
使用员工数据集填充状态存储,以便稍后查询。
curl -X POST -H "Content-Type: application/json" -d @query-api-examples/dataset.json http://localhost:3500/v1.0/state/statestore
填充完成后,你可以检查状态存储中的数据。在下图中,MongoDB UI 的一部分显示了员工记录。

每个条目都有 _id 成员作为连接的对象键,以及包含 JSON 记录的 value 成员。
查询 API 允许你从此 JSON 结构中选择记录。
现在你可以运行示例查询了。
示例 1
首先,查找加利福尼亚州的所有员工,并按员工 ID 降序排序。
这是 查询:
{
"filter": {
"EQ": { "state": "CA" }
},
"sort": [
{
"key": "person.id",
"order": "DESC"
}
]
}
此查询的 SQL 等效形式为:
SELECT * FROM c WHERE
state = "CA"
ORDER BY
person.id DESC
使用以下命令执行查询:
curl -s -X POST -H "Content-Type: application/json" -d @query-api-examples/query1.json http://localhost:3500/v1.0-alpha1/state/statestore/query | jq .
Invoke-RestMethod -Method Post -ContentType 'application/json' -InFile query-api-examples/query1.json -Uri 'http://localhost:3500/v1.0-alpha1/state/statestore/query'
查询结果是按请求顺序排列的匹配键/值对数组:
{
"results": [
{
"key": "3",
"data": {
"person": {
"org": "Finance",
"id": 1071
},
"city": "Sacramento",
"state": "CA"
},
"etag": "44723d41-deb1-4c23-940e-3e6896c3b6f7"
},
{
"key": "7",
"data": {
"city": "San Francisco",
"state": "CA",
"person": {
"id": 1015,
"org": "Dev Ops"
}
},
"etag": "0e69e69f-3dbc-423a-9db8-26767fcd2220"
},
{
"key": "5",
"data": {
"state": "CA",
"person": {
"org": "Hardware",
"id": 1007
},
"city": "Los Angeles"
},
"etag": "f87478fa-e5c5-4be0-afa5-f9f9d75713d8"
},
{
"key": "9",
"data": {
"person": {
"org": "Finance",
"id": 1002
},
"city": "San Diego",
"state": "CA"
},
"etag": "f5cf05cd-fb43-4154-a2ec-445c66d5f2f8"
}
]
}
示例 2
现在,查找来自 “Dev Ops” 和 “Hardware” 组织的所有员工。
这是 查询:
{
"filter": {
"IN": { "person.org": [ "Dev Ops", "Hardware" ] }
}
}
此查询的 SQL 等效形式为:
SELECT * FROM c WHERE
person.org IN ("Dev Ops", "Hardware")
使用以下命令执行查询:
curl -s -X POST -H "Content-Type: application/json" -d @query-api-examples/query2.json http://localhost:3500/v1.0-alpha1/state/statestore/query | jq .
Invoke-RestMethod -Method Post -ContentType 'application/json' -InFile query-api-examples/query2.json -Uri 'http://localhost:3500/v1.0-alpha1/state/statestore/query'
与上一个示例类似,结果是匹配键/值对的数组。
示例 3
在此示例中,查找:
- “Dev Ops” 部门的所有员工。
- 居住在华盛顿州和加利福尼亚州的 “Finance” 部门的员工。
此外,首先按州按字母降序排序,然后按员工 ID 升序排序。让我们一次处理最多 3 条记录。
这是 查询:
{
"filter": {
"OR": [
{
"EQ": { "person.org": "Dev Ops" }
},
{
"AND": [
{
"EQ": { "person.org": "Finance" }
},
{
"IN": { "state": [ "CA", "WA" ] }
}
]
}
]
},
"sort": [
{
"key": "state",
"order": "DESC"
},
{
"key": "person.id"
}
],
"page": {
"limit": 3
}
}
此查询的 SQL 等效形式为:
SELECT * FROM c WHERE
person.org = "Dev Ops" OR
(person.org = "Finance" AND state IN ("CA", "WA"))
ORDER BY
state DESC,
person.id ASC
LIMIT 3
使用以下命令执行查询:
curl -s -X POST -H "Content-Type: application/json" -d @query-api-examples/query3.json http://localhost:3500/v1.0-alpha1/state/statestore/query | jq .
Invoke-RestMethod -Method Post -ContentType 'application/json' -InFile query-api-examples/query3.json -Uri 'http://localhost:3500/v1.0-alpha1/state/statestore/query'
成功执行后,状态存储会返回一个 JSON 对象,其中包含匹配记录列表和分页令牌:
{
"results": [
{
"key": "1",
"data": {
"person": {
"org": "Dev Ops",
"id": 1036
},
"city": "Seattle",
"state": "WA"
},
"etag": "6f54ad94-dfb9-46f0-a371-e42d550adb7d"
},
{
"key": "4",
"data": {
"person": {
"org": "Dev Ops",
"id": 1042
},
"city": "Spokane",
"state": "WA"
},
"etag": "7415707b-82ce-44d0-bf15-6dc6305af3b1"
},
{
"key": "10",
"data": {
"person": {
"org": "Dev Ops",
"id": 1054
},
"city": "New York",
"state": "NY"
},
"etag": "26bbba88-9461-48d1-8a35-db07c374e5aa"
}
],
"token": "3"
}
分页令牌在 后续查询 中"按原样"使用,以获取下一批记录:
{
"filter": {
"OR": [
{
"EQ": { "person.org": "Dev Ops" }
},
{
"AND": [
{
"EQ": { "person.org": "Finance" }
},
{
"IN": { "state": [ "CA", "WA" ] }
}
]
}
]
},
"sort": [
{
"key": "state",
"order": "DESC"
},
{
"key": "person.id"
}
],
"page": {
"limit": 3,
"token": "3"
}
}
curl -s -X POST -H "Content-Type: application/json" -d @query-api-examples/query3-token.json http://localhost:3500/v1.0-alpha1/state/statestore/query | jq .
Invoke-RestMethod -Method Post -ContentType 'application/json' -InFile query-api-examples/query3-token.json -Uri 'http://localhost:3500/v1.0-alpha1/state/statestore/query'
此查询的结果为:
{
"results": [
{
"key": "9",
"data": {
"person": {
"org": "Finance",
"id": 1002
},
"city": "San Diego",
"state": "CA"
},
"etag": "f5cf05cd-fb43-4154-a2ec-445c66d5f2f8"
},
{
"key": "7",
"data": {
"city": "San Francisco",
"state": "CA",
"person": {
"id": 1015,
"org": "Dev Ops"
}
},
"etag": "0e69e69f-3dbc-423a-9db8-26767fcd2220"
},
{
"key": "3",
"data": {
"person": {
"org": "Finance",
"id": 1071
},
"city": "Sacramento",
"state": "CA"
},
"etag": "44723d41-deb1-4c23-940e-3e6896c3b6f7"
}
],
"token": "6"
}
通过这种方式,你可以在查询中更新分页令牌并遍历结果,直到不再返回记录。
限制
状态查询 API 具有以下限制:
- 要查询存储在状态存储中的 actor 状态,你需要使用特定数据库的查询 API。请参阅 查询 actor 状态。
- 该 API 不适用于 Dapr 加密状态存储功能。由于加密是由 Dapr 运行时完成的并作为加密数据存储,因此这实际上阻止了服务器端查询。
你可以在 相关链接 部分找到其他信息。
相关链接
- 请参阅 查询 API 参考。
- 查看 实现查询支持的状态存储组件。
- 查看 状态存储查询 API 实现指南。
- 了解如何 查询 Redis 状态存储。
1.4.4 - 操作指南:构建有状态服务
在本文中,你将学习如何使用可选的并发和一致性模型创建可水平扩展的有状态服务。使用状态管理 API 可以让开发者无需处理复杂的状态协调、冲突解决和故障处理。
设置状态存储
状态存储组件代表 Dapr 用于与数据库通信的资源。在本指南中,我们将使用默认的 Redis 状态存储。
使用 Dapr CLI
在自托管模式下运行 dapr init 时,Dapr 会创建一个默认的 Redis statestore.yaml,并在本地机器上运行一个 Redis 状态存储,位于:
- 在 Windows 上,位于
%UserProfile%\.dapr\components\statestore.yaml - 在 Linux/MacOS 上,位于
~/.dapr/components/statestore.yaml
使用 statestore.yaml 组件,你可以轻松地交换底层组件,而无需更改应用程序代码。
请参阅支持的状态存储列表。
Kubernetes
强一致性和最终一致性
使用强一致性时,Dapr 确保底层状态存储:
- 在数据写入所有副本后返回响应。
- 在写入或删除状态之前从法定数量获取 ACK。
对于 get 请求,Dapr 确保存储返回副本之间一致的最新数据。默认值为最终一致性,除非在状态 API 的请求中另有指定。
以下示例说明了如何使用强一致性保存、获取和删除状态。该示例使用 Python 编写,但适用于任何编程语言。
保存状态
import requests
import json
store_name = "redis-store" # name of the state store as specified in state store component yaml file
dapr_state_url = "http://localhost:3500/v1.0/state/{}".format(store_name)
stateReq = '[{ "key": "k1", "value": "Some Data", "options": { "consistency": "strong" }}]'
response = requests.post(dapr_state_url, json=stateReq)
获取状态
import requests
import json
store_name = "redis-store" # name of the state store as specified in state store component yaml file
dapr_state_url = "http://localhost:3500/v1.0/state/{}".format(store_name)
response = requests.get(dapr_state_url + "/key1", headers={"consistency":"strong"})
print(response.headers['ETag'])
删除状态
import requests
import json
store_name = "redis-store" # name of the state store as specified in state store component yaml file
dapr_state_url = "http://localhost:3500/v1.0/state/{}".format(store_name)
response = requests.delete(dapr_state_url + "/key1", headers={"consistency":"strong"})
如果未指定 concurrency 选项,则默认为最后写入并发模式。
先写优先和后写优先
Dapr 允许开发者在处理数据存储时选择两种常见的并发模式:
- 先写优先(First-write-wins):适用于有多个应用程序实例同时写入同一键的情况。
- 后写优先(Last-write-wins):Dapr 的默认模式。
Dapr 使用版本号来确定特定键是否已更新。你可以:
- 读取键的数据时保留版本号。
- 在更新(如写入和删除)期间使用版本号。
如果自获取版本号以来版本信息已更改,则会抛出错误,要求你执行另一次读取以获取最新版本信息和状态。
Dapr 利用 ETag 来确定状态的版本号。ETag 从状态请求中的 ETag 头返回。使用 ETag,你的应用程序可以通过在 ETag 不匹配时出错来知道资源自上次检查以来已更新。
以下示例说明如何:
- 获取 ETag。
- 使用 ETag 保存状态。
- 删除状态。
以下示例使用 Python 编写,但适用于任何编程语言。
import requests
import json
store_name = "redis-store" # name of the state store as specified in state store component yaml file
dapr_state_url = "http://localhost:3500/v1.0/state/{}".format(store_name)
response = requests.get(dapr_state_url + "/key1", headers={"concurrency":"first-write"})
etag = response.headers['ETag']
newState = '[{ "key": "k1", "value": "New Data", "etag": {}, "options": { "concurrency": "first-write" }}]'.format(etag)
requests.post(dapr_state_url, json=newState)
response = requests.delete(dapr_state_url + "/key1", headers={"If-Match": "{}".format(etag)})
处理版本不匹配失败
在以下示例中,你将看到当版本已更改时如何重试保存状态操作:
import requests
import json
# This method saves the state and returns false if failed to save state
def save_state(data):
try:
store_name = "redis-store" # name of the state store as specified in state store component yaml file
dapr_state_url = "http://localhost:3500/v1.0/state/{}".format(store_name)
response = requests.post(dapr_state_url, json=data)
if response.status_code == 200:
return True
except:
return False
return False
# This method gets the state and returns the response, with the ETag in the header -->
def get_state(key):
response = requests.get("http://localhost:3500/v1.0/state/<state_store_name>/{}".format(key), headers={"concurrency":"first-write"})
return response
# Exit when save state is successful. success will be False if there's an ETag mismatch -->
success = False
while success != True:
response = get_state("key1")
etag = response.headers['ETag']
newState = '[{ "key": "key1", "value": "New Data", "etag": {}, "options": { "concurrency": "first-write" }}]'.format(etag)
success = save_state(newState)
1.4.5 - How-To:启用事务性 Outbox 模式
事务性 outbox 模式是一种众所周知的设计模式,用于发送与应用程序状态变更相关的通知。事务性 outbox 模式使用一个横跨数据库与消息代理的单一事务来投递通知。
开发者在尝试自行实现此模式时面临着许多困难的技术挑战,通常需要编写容易出错的核心协调管理器,最多只能支持一两组数据库与消息代理的组合。
例如,你可以使用 outbox 模式:
- 向账户数据库写入新的用户记录。
- 发送一条表示账户已成功创建的通知消息。
借助 Dapr 的 outbox 支持,当调用 Dapr 的事务 API创建或更新应用程序状态时,你可以通知订阅者。
下图是从高层次概述 outbox 功能的工作原理:
- 服务 A 使用事务将状态保存/更新到状态存储。
- 在同一事务下向消息代理写入一条消息。当消息成功投递到消息代理后,事务完成,从而确保状态与消息作为一个整体完成事务。
- 消息代理将消息主题投递给任何订阅者 — 本例中为服务 B。

Outbox 在底层的工作原理
Dapr outbox 在两个流程中处理请求:用户请求流程与后台消息流程。二者共同确保状态与事件保持一致。

交互顺序如下:
应用程序调用 Dapr 状态管理 API,以事务方式写入状态。
这是业务数据(例如订单或资料更新)提交进行持久化的入口点。Dapr 向一个内部 outbox 主题发布一条带有唯一事务 ID 的意向消息。
这条持久化记录确保在任何数据库提交发生之前,事件意向就已经存在。状态与一个事务标记以原子方式写入同一个状态存储。
业务数据与标记在同一事务中提交,防止部分写入。事务提交后,应用程序收到成功响应。
此时应用程序可以继续执行,已知状态已保存,事件意向得到保证。后台订阅者读取意向消息。
当 outbox 启用时,Dapr 会启动消费者来处理内部 outbox 主题。订阅者在状态存储中验证事务标记。
此项检查确认数据库提交已成功,然后才会进行外部发布。验证后的业务事件被发布到外部发布订阅主题。
事件被发送到已配置的代理(Kafka、RabbitMQ 等),其他服务可以消费该事件。标记从状态存储中清理(删除)。
事件成功投递后,这可防止数据库无限制地增长。消息被确认并从内部主题中移除
如果发布或清理失败,Dapr 会重试,确保可靠的至少一次投递。
要求
outbox 功能需要 Dapr 支持的事务型状态存储。
了解更多关于你可以使用的事务方法。任何 Dapr 支持的发布订阅代理均可与 outbox 功能一起使用。
注意
建议使用支持竞争消费者模式的消息代理(例如 Apache Kafka),以降低重复事件的可能性。内部 outbox 主题
当启用 outbox 时,Dapr 会使用以下命名约定创建一个内部主题:{namespace}{appID}{topic}outbox,其中:namespace:Dapr 应用程序命名空间(如果已配置)appID:Dapr 应用程序标识符topic:在outboxPublishTopic元数据中指定的值
通过这种方式,每个 outbox 主题在每个应用程序和外部主题范围内被唯一标识,防止多租户环境中的路由冲突。
注意
确保主题已提前创建,或者 Dapr 在启动时拥有足够的权限来创建该主题。
启用 outbox 模式
要启用 outbox 功能,请在状态存储组件上添加以下必填和可选字段:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: mysql-outbox
spec:
type: state.mysql
version: v1
metadata:
- name: connectionString
value: "<CONNECTION STRING>"
- name: outboxPublishPubsub # 必填
value: "mypubsub"
- name: outboxPublishTopic # 必填
value: "newOrder"
- name: outboxPubsub # 可选
value: "myOutboxPubsub"
- name: outboxDiscardWhenMissingState #可选。默认为 false
value: false
元数据字段
| 名称 | 必填 | 默认值 | 描述 |
|---|---|---|---|
| outboxPublishPubsub | 是 | N/A | 设置发布订阅组件的名称,用于在发布状态变更时投递通知 |
| outboxPublishTopic | 是 | N/A | 设置在用 outboxPublishPubsub 配置的发布订阅上接收状态变更的主题。消息正文将是针对 insert 或 update 操作的状态事务项 |
| outboxPubsub | 否 | outboxPublishPubsub | 设置 Dapr 用于协调状态与发布订阅事务的发布订阅组件。如果未设置,则使用用 outboxPublishPubsub 配置的发布订阅组件。如果你想将用于发送通知状态变更的发布订阅组件与用于协调事务的组件分开,此设置会很有用 |
| outboxDiscardWhenMissingState | 否 | false | 通过将 outboxDiscardWhenMissingState 设置为 true,如果 Dapr 无法在数据库中找到状态,它将丢弃该事务并且不再重试。如果状态存储数据在 Dapr 能够投递消息之前因任何原因被删除,并且你希望 Dapr 从发布订阅中删除这些项并停止重试以获取状态,则此设置会很有用 |
其他配置
在同一个状态存储上组合使用 outbox 与非 outbox 消息
如果你希望使用同一个状态存储来发送 outbox 与非 outbox 消息,只需定义两个连接到同一个状态存储的状态存储组件,其中一个启用 outbox 功能,另一个不启用。
不带 outbox 的 MySQL 状态存储
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: mysql
spec:
type: state.mysql
version: v1
metadata:
- name: connectionString
value: "<CONNECTION STRING>"
带 outbox 的 MySQL 状态存储
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: mysql-outbox
spec:
type: state.mysql
version: v1
metadata:
- name: connectionString
value: "<CONNECTION STRING>"
- name: outboxPublishPubsub # 必填
value: "mypubsub"
- name: outboxPublishTopic # 必填
value: "newOrder"
定制 outbox 模式消息
你可以通过设置另一个事务来覆盖发布到发布订阅代理的 outbox 模式消息,该事务不会被保存到数据库,并且被明确标记为投影(projection)。该事务会添加一个名为 outbox.projection 的元数据键,其值设置为 true。当添加到在事务中保存的状态数组时,写入状态时会忽略此负载,并将该数据用作发送到上游订阅者的负载。
若要正确使用,状态存储上的操作与消息投影之间的 key 值必须匹配。如果键不匹配,整个事务将失败。
如果你为同一个键启用了两个或更多 outbox.projection 状态项,则使用第一个定义的项,忽略其他项。
在以下状态事务的 Python SDK 示例中,值 "2" 被保存到数据库,而值 "3" 被发布到最终用户主题。
DAPR_STORE_NAME = "statestore"
async def main():
client = DaprClient()
client.execute_state_transaction(
store_name=DAPR_STORE_NAME,
operations=[
# 定义第一个状态操作以保存值 "2"
TransactionalStateOperation(
key='key1', data='2', metadata={'outbox.projection': 'false'}
),
# 定义第二个状态操作以发布带有元数据的值 "3"
TransactionalStateOperation(
key='key1', data='3', metadata={'outbox.projection': 'true'}
),
],
)
print("State transaction executed.")
通过将元数据项 "outbox.projection" 设置为 "true" 并确保 key 值匹配(key1):
- 第一个操作被写入状态存储,不会向消息代理写入消息。
- 第二个操作值被发布到已配置的发布订阅主题。
在以下状态事务的 JavaScript SDK 示例中,值 "2" 被保存到数据库,而值 "3" 被发布到最终用户主题。
const { DaprClient, StateOperationType } = require('@dapr/dapr');
const DAPR_STORE_NAME = "statestore";
async function main() {
const client = new DaprClient();
// 定义第一个状态操作以保存值 "2"
const op1 = {
operation: StateOperationType.UPSERT,
request: {
key: "key1",
value: "2"
}
};
// 定义第二个状态操作以发布带有元数据的值 "3"
const op2 = {
operation: StateOperationType.UPSERT,
request: {
key: "key1",
value: "3",
metadata: {
"outbox.projection": "true"
}
}
};
// 创建状态操作列表
const ops = [op1, op2];
// 执行状态事务
await client.state.transaction(DAPR_STORE_NAME, ops);
console.log("State transaction executed.");
}
main().catch(err => {
console.error(err);
});
通过将元数据项 "outbox.projection" 设置为 "true" 并确保 key 值匹配(key1):
- 第一个操作被写入状态存储,不会向消息代理写入消息。
- 第二个操作值被发布到已配置的发布订阅主题。
在以下状态事务的 .NET SDK 示例中,值 "2" 被保存到数据库,而值 "3" 被发布到最终用户主题。
public class Program
{
private const string DAPR_STORE_NAME = "statestore";
public static async Task Main(string[] args)
{
var client = new DaprClientBuilder().Build();
// 定义第一个状态操作以保存值 "2"
var op1 = new StateTransactionRequest(
key: "key1",
value: Encoding.UTF8.GetBytes("2"),
operationType: StateOperationType.Upsert
);
// 定义第二个状态操作以发布带有元数据的值 "3"
var metadata = new Dictionary<string, string>
{
{ "outbox.projection", "true" }
};
var op2 = new StateTransactionRequest(
key: "key1",
value: Encoding.UTF8.GetBytes("3"),
operationType: StateOperationType.Upsert,
metadata: metadata
);
// 创建状态操作列表
var ops = new List<StateTransactionRequest> { op1, op2 };
// 执行状态事务
await client.ExecuteStateTransactionAsync(DAPR_STORE_NAME, ops);
Console.WriteLine("State transaction executed.");
}
}
通过将元数据项 "outbox.projection" 设置为 "true" 并确保 key 值匹配(key1):
- 第一个操作被写入状态存储,不会向消息代理写入消息。
- 第二个操作值被发布到已配置的发布订阅主题。
在以下状态事务的 Java SDK 示例中,值 "2" 被保存到数据库,而值 "3" 被发布到最终用户主题。
public class Main {
private static final String DAPR_STORE_NAME = "statestore";
public static void main(String[] args) {
try (DaprClient client = new DaprClientBuilder().build()) {
// 定义第一个状态操作以保存值 "2"
State<String> state1 = new State<>(
"key1",
"2",
null, // etag
null // 并发与一致性选项
);
// 定义第二个状态操作以发布带有元数据的值 "3"
Map<String, String> metadata = new HashMap<>();
metadata.put("outbox.projection", "true");
State<String> state2 = new State<>(
"key1",
"3",
null, // etag
metadata,
null // 并发与一致性选项
);
TransactionalStateOperation<String> op1 = new TransactionalStateOperation<>(
TransactionalStateOperation.OperationType.UPSERT, state1
);
TransactionalStateOperation<String> op2 = new TransactionalStateOperation<>(
TransactionalStateOperation.OperationType.UPSERT, state2
);
// 创建事务状态操作列表
List<TransactionalStateOperation<?>> ops = new ArrayList<>();
ops.add(op1);
ops.add(op2);
// 配置事务请求,设置状态存储
ExecuteStateTransactionRequest transactionRequest = new ExecuteStateTransactionRequest(DAPR_STORE_NAME);
transactionRequest.setOperations(ops);
// 执行状态事务
client.executeStateTransaction(transactionRequest).block();
System.out.println("State transaction executed.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
通过将元数据项 "outbox.projection" 设置为 "true" 并确保 key 值匹配(key1):
- 第一个操作被写入状态存储,不会向消息代理写入消息。
- 第二个操作值被发布到已配置的发布订阅主题。
在以下状态事务的 Go SDK 示例中,值 "2" 被保存到数据库,而值 "3" 被发布到最终用户主题。
ops := make([]*dapr.StateOperation, 0)
op1 := &dapr.StateOperation{
Type: dapr.StateOperationTypeUpsert,
Item: &dapr.SetStateItem{
Key: "key1",
Value: []byte("2"),
},
}
op2 := &dapr.StateOperation{
Type: dapr.StateOperationTypeUpsert,
Item: &dapr.SetStateItem{
Key: "key1",
Value: []byte("3"),
// 覆盖保存到数据库的数据负载
Metadata: map[string]string{
"outbox.projection": "true",
},
},
}
ops = append(ops, op1, op2)
meta := map[string]string{}
err := testClient.ExecuteStateTransaction(ctx, store, meta, ops)
通过将元数据项 "outbox.projection" 设置为 "true" 并确保 key 值匹配(key1):
- 第一个操作被写入状态存储,不会向消息代理写入消息。
- 第二个操作值被发布到已配置的发布订阅主题。
你可以使用以下 HTTP 请求传递消息覆盖:
curl -X POST http://localhost:3500/v1.0/state/starwars/transaction \
-H "Content-Type: application/json" \
-d '{
"operations": [
{
"operation": "upsert",
"request": {
"key": "order1",
"value": {
"orderId": "7hf8374s",
"type": "book",
"name": "The name of the wind"
}
}
},
{
"operation": "upsert",
"request": {
"key": "order1",
"value": {
"orderId": "7hf8374s"
},
"metadata": {
"outbox.projection": "true"
},
"contentType": "application/json"
}
}
]
}'
通过将元数据项 "outbox.projection" 设置为 "true" 并确保 key 值匹配(key1):
- 第一个操作被写入状态存储,不会向消息代理写入消息。
- 第二个操作值被发布到已配置的发布订阅主题。
覆盖 Dapr 生成的 CloudEvent 字段
你可以使用自定义 CloudEvent 元数据来覆盖已发布的 outbox 事件上的 Dapr 生成的 CloudEvent 字段。
async def execute_state_transaction():
async with DaprClient() as client:
# 定义状态操作
ops = []
op1 = {
'operation': 'upsert',
'request': {
'key': 'key1',
'value': b'2', # 将字符串转换为字节数组
'metadata': {
'cloudevent.id': 'unique-business-process-id',
'cloudevent.source': 'CustomersApp',
'cloudevent.type': 'CustomerCreated',
'cloudevent.subject': '123',
'my-custom-ce-field': 'abc'
}
}
}
ops.append(op1)
# 执行状态事务
store_name = 'your-state-store-name'
try:
await client.execute_state_transaction(store_name, ops)
print('State transaction executed.')
except Exception as e:
print('Error executing state transaction:', e)
# 运行异步函数
if __name__ == "__main__":
asyncio.run(execute_state_transaction())
const { DaprClient } = require('dapr-client');
async function executeStateTransaction() {
// 初始化 Dapr 客户端
const daprClient = new DaprClient();
// 定义状态操作
const ops = [];
const op1 = {
operationType: 'upsert',
request: {
key: 'key1',
value: Buffer.from('2'),
metadata: {
'id': 'unique-business-process-id',
'source': 'CustomersApp',
'type': 'CustomerCreated',
'subject': '123',
'my-custom-ce-field': 'abc'
}
}
};
ops.push(op1);
// 执行状态事务
const storeName = 'your-state-store-name';
const metadata = {};
}
executeStateTransaction();
public class StateOperationExample
{
public async Task ExecuteStateTransactionAsync()
{
var daprClient = new DaprClientBuilder().Build();
// 将值 "2" 定义为字符串并将其序列化为字节数组
var value = "2";
var valueBytes = JsonSerializer.SerializeToUtf8Bytes(value);
// 定义第一个状态操作以保存带有元数据的值 "2"
// 覆盖 Cloudevent 元数据
var metadata = new Dictionary<string, string>
{
{ "cloudevent.id", "unique-business-process-id" },
{ "cloudevent.source", "CustomersApp" },
{ "cloudevent.type", "CustomerCreated" },
{ "cloudevent.subject", "123" },
{ "my-custom-ce-field", "abc" }
};
var op1 = new StateTransactionRequest(
key: "key1",
value: valueBytes,
operationType: StateOperationType.Upsert,
metadata: metadata
);
// 创建状态操作列表
var ops = new List<StateTransactionRequest> { op1 };
// 执行状态事务
var storeName = "your-state-store-name";
await daprClient.ExecuteStateTransactionAsync(storeName, ops);
Console.WriteLine("State transaction executed.");
}
public static async Task Main(string[] args)
{
var example = new StateOperationExample();
await example.ExecuteStateTransactionAsync();
}
}
public class StateOperationExample {
public static void main(String[] args) {
executeStateTransaction();
}
public static void executeStateTransaction() {
// 构建 Dapr 客户端
try (DaprClient daprClient = new DaprClientBuilder().build()) {
// 覆盖 CloudEvent 元数据
Map<String, String> metadata = new HashMap<>();
metadata.put("cloudevent.id", "unique-business-process-id");
metadata.put("cloudevent.source", "CustomersApp");
metadata.put("cloudevent.type", "CustomerCreated");
metadata.put("cloudevent.subject", "123");
metadata.put("my-custom-ce-field", "abc");
State<String> state = new State<>(
"key1", // 定义键 "key1"
"value1", // 定义值 "value1"
null, // etag
metadata,
null // 并发与一致性选项
);
// 定义状态操作
List<TransactionalStateOperation<?>> ops = new ArrayList<>();
TransactionalStateOperation<String> op1 = new TransactionalStateOperation<>(
TransactionalStateOperation.OperationType.UPSERT,
state
);
ops.add(op1);
// 执行状态事务
String storeName = "your-state-store-name";
daprClient.executeStateTransaction(storeName, ops).block();
System.out.println("State transaction executed.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
func main() {
// 创建 Dapr 客户端
client, err := dapr.NewClient()
if err != nil {
log.Fatalf("failed to create Dapr client: %v", err)
}
defer client.Close()
ctx := context.Background()
store := "your-state-store-name"
// 定义状态操作
ops := make([]*dapr.StateOperation, 0)
op1 := &dapr.StateOperation{
Type: dapr.StateOperationTypeUpsert,
Item: &dapr.SetStateItem{
Key: "key1",
Value: []byte("2"),
// 覆盖 Cloudevent 元数据
Metadata: map[string]string{
"cloudevent.id": "unique-business-process-id",
"cloudevent.source": "CustomersApp",
"cloudevent.type": "CustomerCreated",
"cloudevent.subject": "123",
"my-custom-ce-field": "abc",
},
},
}
ops = append(ops, op1)
// 事务的元数据(如果有)
meta := map[string]string{}
// 执行状态事务
err = client.ExecuteStateTransaction(ctx, store, meta, ops)
if err != nil {
log.Fatalf("failed to execute state transaction: %v", err)
}
log.Println("State transaction executed.")
}
curl -X POST http://localhost:3500/v1.0/state/starwars/transaction \
-H "Content-Type: application/json" \
-d '{
"operations": [
{
"operation": "upsert",
"request": {
"key": "key1",
"value": "2"
}
},
],
"metadata": {
"id": "unique-business-process-id",
"source": "CustomersApp",
"type": "CustomerCreated",
"subject": "123",
"my-custom-ce-field": "abc",
}
}'
注意
data CloudEvent 字段保留仅供 Dapr 使用,不可自定义。演示
后续步骤
1.4.6 - 操作指南:在应用程序之间共享状态
Dapr 提供了多种在应用程序之间共享状态的方式。
不同的架构在共享状态方面可能有不同的需求。在一种场景中,你可能希望:
- 将所有状态封装在给定应用程序内
- 让 Dapr 为你管理访问权限
在另一种场景中,你可能需要两个应用程序同时操作相同的状态,获取和保存相同的键。
为启用状态共享,Dapr 支持以下键前缀策略:
| 键前缀 | 描述 |
|---|---|
appid | 默认策略,允许你仅通过具有指定 appid 的应用程序管理状态。所有状态键都将以 appid 为前缀,并限定在该应用程序范围内。 |
name | 使用状态存储组件的名称作为前缀。多个应用程序可以共享给定状态存储的相同状态。 |
namespace | 如果设置,此设置会将 appid 键加上配置的命名空间作为前缀,从而得到一个限定在给定命名空间范围内的键。这允许具有相同 appid 但在不同命名空间中的应用重用相同的状态存储。如果未配置命名空间,该设置会回退到 appid 策略。有关 Dapr 中命名空间的更多信息,请参阅操作指南:将组件限定到一个或多个应用程序 |
none | 不使用前缀。多个应用程序在不同状态存储之间共享状态。 |
指定状态前缀策略
要指定前缀策略,请在状态组件上添加名为 keyPrefix 的元数据键:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
namespace: production
spec:
type: state.redis
version: v1
metadata:
- name: keyPrefix
value: <key-prefix-strategy>
示例
以下示例演示了使用每种支持的前缀策略进行状态检索的情况。
appid(默认)
在下面的示例中,应用程序 ID 为 myApp 的 Dapr 应用程序正在将状态保存到名为 redis 的状态存储中:
curl -X POST http://localhost:3500/v1.0/state/redis \
-H "Content-Type: application/json"
-d '[
{
"key": "darth",
"value": "nihilus"
}
]'
该键将保存为 myApp||darth。
namespace
在命名空间 production 中运行、应用程序 ID 为 myApp 的 Dapr 应用程序正在将状态保存到名为 redis 的状态存储中:
curl -X POST http://localhost:3500/v1.0/state/redis \
-H "Content-Type: application/json"
-d '[
{
"key": "darth",
"value": "nihilus"
}
]'
该键将保存为 production.myApp||darth。
name
在下面的示例中,应用程序 ID 为 myApp 的 Dapr 应用程序正在将状态保存到名为 redis 的状态存储中:
curl -X POST http://localhost:3500/v1.0/state/redis \
-H "Content-Type: application/json"
-d '[
{
"key": "darth",
"value": "nihilus"
}
]'
该键将保存为 redis||darth。
none
在下面的示例中,应用程序 ID 为 myApp 的 Dapr 应用程序正在将状态保存到名为 redis 的状态存储中:
curl -X POST http://localhost:3500/v1.0/state/redis \
-H "Content-Type: application/json"
-d '[
{
"key": "darth",
"value": "nihilus"
}
]'
该键将保存为 darth。
1.4.7 - 操作指南:加密应用状态
对应用状态进行静态加密,以在企业工作负载或受监管环境中提供更强的安全性。Dapr 基于 Galois/Counter Mode (GCM) 中的 AES 提供自动客户端加密,支持 128、192 和 256 位密钥。
除了自动加密外,Dapr 还支持主加密密钥和辅助加密密钥,使开发人员和运维团队更容易启用密钥轮换策略。所有 Dapr 状态存储都支持此功能。
加密密钥始终从机密中获取,不能在 metadata 部分以纯文本值形式提供。
启用自动加密
将以下 metadata 部分添加到任何 Dapr 支持的状态存储:
metadata:
- name: primaryEncryptionKey
secretKeyRef:
name: mysecret
key: mykey # key 是可选的。
例如,这是一个 Redis 加密状态存储的完整 YAML:
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: primaryEncryptionKey
secretKeyRef:
name: mysecret
key: mykey
现在您已经配置了一个 Dapr 状态存储,可以从名为 mysecret 的机密中获取加密密钥,该机密在名为 mykey 的键中包含实际的加密密钥。
实际的加密密钥必须是有效的十六进制编码的加密密钥。虽然支持 192 位和 256 位密钥,但建议您使用 128 位加密密钥。如果加密密钥无效,Dapr 会报错并退出。
例如,您可以使用以下命令生成随机的十六进制编码的 128 位(16 字节)密钥:
openssl rand 16 | hexdump -v -e '/1 "%02x"'
# 结果将类似于 "cb321007ad11a9d23f963bff600d58e0"
请注意,机密存储不必支持键。
密钥轮换
为了支持密钥轮换,Dapr 提供了一种指定辅助加密密钥的方法:
metadata:
- name: primaryEncryptionKey
secretKeyRef:
name: mysecret
key: mykey
- name: secondaryEncryptionKey
secretKeyRef:
name: mysecret2
key: mykey2
当 Dapr 启动时,它会获取包含 metadata 部分中列出的加密密钥的机密。Dapr 自动知道哪个状态项已使用哪个密钥加密,因为它将 secretKeyRef.name 字段附加到实际状态键的末尾。
要轮换密钥:
- 将
primaryEncryptionKey更改为指向包含您新密钥的机密。 - 将旧的主加密密钥移动到
secondaryEncryptionKey。
新数据将使用新密钥加密,而任何检索到的旧数据将使用辅助密钥解密。
对使用旧密钥加密的数据项的任何更新都将使用新密钥重新加密。
注意
当您轮换密钥时,除非您的应用程序再次写入,否则使用旧密钥加密的数据不会自动重新加密。如果您删除轮换的密钥(现在的辅助加密密钥),您将无法访问使用该密钥加密的数据。相关链接
1.4.8.1 - Azure Cosmos DB
Dapr 在保存和检索状态时不会转换状态值。Dapr 要求所有状态存储实现遵守特定的键格式方案(参见[状态管理规范](https://docs.dapr.io/zh-hans/reference/api/state_api/)。您可以直接与底层存储交互来操作状态数据,例如:
- 查询状态。
- 创建聚合视图。
- 制作备份。
注意
Azure Cosmos DB 是一个支持多种 API 的多模态数据库。默认的 Dapr Cosmos DB 状态存储实现使用 Azure Cosmos DB SQL API。连接到 Azure Cosmos DB
要连接到您的 Cosmos DB 实例,您可以:
- 使用 Azure 管理门户上的数据资源管理器。
- 使用各种 SDK 和工具。
注意
当您为 Dapr 配置 Azure Cosmos DB 时,请指定要使用的确切数据库和集合。以下 Cosmos DB SQL API 示例假设您已经连接到正确的数据库和一个名为 “states” 的集合。按 App ID 列出键
要获取与应用程序 “myapp” 关联的所有状态键,请使用以下查询:
SELECT * FROM states WHERE CONTAINS(states.id, 'myapp||')
上述查询返回 id 包含 “myapp-” 的所有文档,这是状态键的前缀。
获取特定状态数据
要获取应用程序 “myapp” 中键为 “balance” 的状态数据,请使用以下查询:
SELECT * FROM states WHERE states.id = 'myapp||balance'
读取返回文档中的 value 字段。要获取状态版本/ETag,请使用以下命令:
SELECT states._etag FROM states WHERE states.id = 'myapp||balance'
读取 Actor 状态
要获取应用程序 ID 为 “mypets” 中 Actor 类型为 “cat” 且实例 ID 为 “leroy” 的 Actor 关联的所有状态键,请使用以下命令:
SELECT * FROM states WHERE CONTAINS(states.id, 'mypets||cat||leroy||')
而要获取特定的 Actor 状态(如 “food”),请使用以下命令:
SELECT * FROM states WHERE states.id = 'mypets||cat||leroy||food'
警告
不应手动更新或删除存储中的状态。所有写入和删除操作应通过 Dapr 运行时完成。唯一的例外: 通常需要删除状态存储中的 Actor 记录,一旦您知道这些记录已不再使用,以防止未使用的 Actor 实例堆积而可能永远无法再次加载。1.4.8.2 - Redis
Dapr 在保存和检索状态时不会转换状态值。Dapr 要求所有状态存储实现都遵循一定的键格式方案(参见状态管理规范)。您可以直接与底层存储交互来操作状态数据,例如:
- 查询状态。
- 创建聚合视图。
- 进行备份。
Dapr 提供了一个 Redis state store component 作为可插拔组件。
注意
以下示例使用 Redis CLI 针对使用默认 Dapr 状态存储实现的 Redis 存储进行操作。连接到 Redis
您可以使用官方的 redis-cli 或任何其他兼容 Redis 的工具连接到 Redis 状态存储,直接查询 Dapr 状态。如果您是在容器中运行 Redis,使用 redis-cli 的最简单方法是通过容器:
docker run --rm -it --link <Redis 容器的名称> redis redis-cli -h <Redis 容器的名称>
按应用 ID 列出键
要获取与应用 “myapp” 关联的所有状态键,请使用命令:
KEYS myapp*
上述命令返回现有键的列表,例如:
1) "myapp||balance"
2) "myapp||amount"
获取特定状态数据
Dapr 将状态值保存为哈希值。每个哈希值包含一个 “data” 字段,其中包含:
- 状态数据。
- 一个 “version” 字段,其中包含一个持续递增的版本,用作 ETag。
例如,要获取应用 “myapp” 的键 “balance” 对应的状态数据,请使用命令:
HGET myapp||balance data
要获取状态版本/ETag,请使用命令:
HGET myapp||balance version
读取 Actor 状态
要获取与应用 ID “mypets” 中 actor 类型为 “cat” 且实例 ID 为 “leroy” 的 actor 关联的所有状态键,请使用命令:
KEYS mypets||cat||leroy*
要获取特定的 actor 状态(如 “food”),请使用命令:
HGET mypets||cat||leroy||food value
警告
您不应手动更新或删除存储中的状态。所有写入和删除操作都应通过 Dapr 运行时完成。**唯一例外:**通常需要删除状态存储中的 actor 记录,一旦您知道这些 actor 不再使用,以防止可能永远不会再加载的未使用 actor 实例积累。1.4.8.3 - SQL Server
Dapr 在保存和检索状态时不会转换状态值。Dapr 要求所有状态存储实现都遵守一定的键格式方案(请参阅状态管理规范)。你可以直接与底层存储交互来操作状态数据,例如:
- 查询状态。
- 创建聚合视图。
- 进行备份。
连接到 SQL Server
连接 SQL Server 实例最简单的方式是使用:
- Azure Data Studio(Windows、macOS、Linux)
- SQL Server Management Studio(Windows)
注意
为 Dapr 配置 Azure SQL 数据库时,需要指定要使用的确切表名。以下 Azure SQL 示例假设你已经连接到正确的数据库,且表中有一个名为 “states” 的表。按 App ID 列出键
要获取与应用程序 “myapp” 关联的所有状态键,请使用以下查询:
SELECT * FROM states WHERE [Key] LIKE 'myapp||%'
上述查询返回所有 ID 包含 “myapp||” 的行,这是状态键的前缀。
获取特定状态数据
要获取应用程序 “myapp” 中键为 “balance” 的状态数据,请使用以下查询:
SELECT * FROM states WHERE [Key] = 'myapp||balance'
读取返回行的 Data 字段。要获取状态版本/ETag,请使用以下命令:
SELECT [RowVersion] FROM states WHERE [Key] = 'myapp||balance'
获取过滤后的状态数据
要获取 JSON 数据中 “color” 值等于 “blue” 的所有状态数据,请使用以下查询:
SELECT * FROM states WHERE JSON_VALUE([Data], '$.color') = 'blue'
读取 Actor 状态
要获取属于应用程序 ID 为 “mypets”、Actor 类型为 “cat”、实例 ID 为 “leroy” 的所有 Actor 状态键,请使用以下命令:
SELECT * FROM states WHERE [Key] LIKE 'mypets||cat||leroy||%'
要获取特定的 Actor 状态(如 “food”),请使用以下命令:
SELECT * FROM states WHERE [Key] = 'mypets||cat||leroy||food'
警告
你不应该手动更新或删除存储中的状态。所有写入和删除操作都应通过 Dapr 运行时完成。**唯一例外:**通常需要删除状态存储中的 Actor 记录,一旦你确定这些记录不再使用,以防止未使用的 Actor 实例堆积,而这些实例可能永远不会再次被加载。1.4.9 - 状态生存时间 (TTL)
Dapr 支持为每个状态设置请求级的生存时间(TTL)。这意味着应用程序可以为每个存储的状态设置 TTL,过期后将无法检索这些状态。
对于支持的 状态存储,您只需在发布消息时设置 ttlInSeconds 元数据。其他 状态存储 将忽略此值。对于某些 状态存储,您可以按表/容器指定默认过期时间。
原生状态 TTL 支持
当 状态存储 组件原生支持状态 TTL 时,Dapr 直接转发 TTL 配置,不添加任何额外逻辑,保持可预测的行为。当组件以不同方式处理过期状态时,这非常有用。
未指定 TTL 时,将保留 状态存储 的默认行为。
显式持久化绕过全局定义的 TTL
持久化状态适用于所有允许为所有数据指定默认 TTL 的 状态存储,无论是:
- 通过 Dapr 组件设置全局 TTL 值,或
- 在 Dapr 外部创建 状态存储 并设置全局 TTL 值。
未指定特定 TTL 时,数据将在该全局 TTL 时间段后过期。这不由 Dapr 处理。
此外,所有 状态存储 还支持_显式_持久化数据的选项。这意味着您可以忽略默认数据库策略(可能在 Dapr 外部设置或通过 Dapr 组件设置)来无限期保留给定的数据库记录。您可以通过将 ttlInSeconds 设置为 -1 值来实现此目的。该值表示忽略任何设置的 TTL 值。
支持的组件
请参阅 状态存储 组件指南中的 TTL 列。
示例
您可以在 状态存储 set 请求的元数据中设置状态 TTL:
#dependencies
from dapr.clients import DaprClient
#code
DAPR_STORE_NAME = "statestore"
with DaprClient() as client:
client.save_state(DAPR_STORE_NAME, "order_1", str(orderId), state_metadata={
'ttlInSeconds': '120'
})
要启动 Dapr 边车 并运行上述示例应用程序,您需要运行类似于以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 -- python3 OrderProcessingService.py
// dependencies
using Dapr.Client;
// code
await client.SaveStateAsync(storeName, stateKeyName, state, metadata: new Dictionary<string, string>() {
{
"ttlInSeconds", "120"
}
});
要启动 Dapr 边车 并运行上述示例应用程序,您需要运行类似于以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 dotnet run
// dependencies
import (
dapr "github.com/dapr/go-sdk/client"
)
// code
md := map[string]string{"ttlInSeconds": "120"}
if err := client.SaveState(ctx, store, "key1", []byte("hello world"), md); err != nil {
panic(err)
}
要启动 Dapr 边车 并运行上述示例应用程序,您需要运行类似于以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 go run .
curl -X POST -H "Content-Type: application/json" -d '[{ "key": "order_1", "value": "250", "metadata": { "ttlInSeconds": "120" } }]' http://localhost:3601/v1.0/state/statestore
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '[{"key": "order_1", "value": "250", "metadata": {"ttlInSeconds": "120"}}]' -Uri 'http://localhost:3601/v1.0/state/statestore'
相关链接
- 查看状态 API 参考指南。
- 了解如何使用键值对持久化状态。
- 状态存储 组件列表。
- 阅读API 参考。
1.5 - Bindings
1.5.1 - Bindings 概述
使用 Dapr 的绑定 API,您可以通过来自外部系统的事件触发您的应用程序,并与外部系统进行交互。通过绑定 API,您可以:
- 避免连接和轮询消息系统(如队列和消息总线)的复杂性。
- 专注于业务逻辑,而不是与系统交互的实现细节。
- 使您的代码不包含 SDK 或库。
- 处理重试和故障恢复。
- 在运行时切换绑定。
- 构建可移植的应用程序,使用特定环境的绑定设置,无需更改代码。
例如,通过绑定,您的应用程序可以响应传入的 Twilio/SMS 消息,而无需:
- 添加或配置第三方 Twilio SDK
- 担心从 Twilio 轮询(或使用 WebSockets 等)

在上图中:
- 输入绑定触发应用程序上的方法。
- 在组件上执行输出绑定操作,例如
"create"。
绑定是独立于 Dapr 运行时开发的。您可以查看并贡献绑定。
输入绑定
使用输入绑定,您可以在外部资源发生事件时触发您的应用程序。可选的有效负载和元数据可能会随请求一起发送。
以下概述视频和演示演示了 Dapr 输入绑定的工作原理。
要接收来自输入绑定的事件:
- 定义描述绑定类型及其元数据(连接信息等)的组件 YAML。
- 使用以下方式监听传入事件:
- HTTP 端点
- gRPC proto 库以获取传入事件。
注意
在启动时,Dapr 会向应用程序发送所有已定义输入绑定的 OPTIONS 请求。如果应用程序想要订阅绑定,Dapr 期望状态代码为 2xx 或 405。阅读使用输入绑定创建事件驱动应用程序指南以开始使用输入绑定。
输出绑定
使用输出绑定,您可以调用外部资源。可选的有效负载和元数据可以随调用请求一起发送。
以下概述视频和演示演示了 Dapr 输出绑定的工作原理。
要调用输出绑定:
- 定义描述绑定类型及其元数据(连接信息等)的组件 YAML。
- 使用 HTTP 端点或 gRPC 方法调用绑定,可选择携带有效负载。
- 指定输出操作。输出操作取决于您使用的绑定组件,可以包括:
"create""update""delete""exec"
阅读使用输出绑定与外部资源交互指南以开始使用输出绑定。
绑定方向(可选)
您可以提供 direction 元数据字段来指示绑定组件支持的方向。这样做可以避免 Dapr 边车处于"等待应用程序就绪"状态,从而减少 Dapr 边车与应用程序之间的生命周期依赖关系:
"input""output""input, output"
注意
强烈建议所有输入绑定都应包含direction 属性。尝试绑定
快速入门和教程
想要测试 Dapr 绑定 API?通过以下快速入门和教程来了解绑定的实际操作:
| 快速入门/教程 | 描述 |
|---|---|
| 绑定快速入门 | 使用输入绑定响应事件,使用输出绑定调用操作,与外部系统协作。 |
| 绑定教程 | 演示如何使用 Dapr 为其他组件创建输入和输出绑定。使用 Kafka 绑定。 |
直接在您的应用程序中开始使用绑定
想要跳过快速入门?没问题。您可以直接在应用程序中尝试绑定构建块,以调用输出绑定和触发输入绑定。在安装 Dapr后,您可以从输入绑定操作指南开始使用绑定 API。
后续步骤
1.5.2 - 操作指南:使用输入绑定触发应用程序
使用输入绑定,当外部资源发生事件时,可以触发您的应用程序。外部资源可以是队列、消息管道、云服务、文件系统等。请求可以随附可选的 payload 和 metadata。
输入绑定非常适合事件驱动处理、数据管道,或通常用于响应事件并执行进一步处理。Dapr 输入绑定允许您:
- 接收事件而无需包含特定的 SDK 或库
- 更换绑定而无需更改代码
- 专注于业务逻辑而非事件资源实现

本指南以 Kafka 绑定为例。您可以从绑定组件列表中找到您首选的绑定规范。在本指南中:
- 示例使用
checkout(要调用的绑定名称)调用/binding端点。 - payload 放入必填的
data字段中,可以是任何 JSON 可序列化的值。 operation字段告知绑定需要采取什么操作。例如,Kafka 绑定支持create操作。- 您可以检查每个输出绑定支持哪些操作(特定于每个组件)。
注意
如果您还没有尝试过,请先尝试绑定快速入门,快速了解如何使用绑定 API。创建绑定
创建一个 binding.yaml 文件并保存到应用程序目录中的 components 子文件夹。
创建一个名为 checkout 的新绑定组件。在 metadata 部分,配置以下与 Kafka 相关的属性:
- 您将向其发布消息的 topic
- broker
创建绑定组件时,指定绑定支持的 direction。
在 dapr run 命令中使用 --resources-path 标志指向您的自定义资源目录。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: checkout
spec:
type: bindings.kafka
version: v1
metadata:
# Kafka broker 连接设置
- name: brokers
value: localhost:9092
# consumer 配置:topic 和 consumer group
- name: topics
value: sample
- name: consumerGroup
value: group1
# publisher 配置:topic
- name: publishTopic
value: sample
- name: authRequired
value: false
- name: direction
value: input
要部署到 Kubernetes 集群,运行 kubectl apply -f binding.yaml。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: checkout
spec:
type: bindings.kafka
version: v1
metadata:
# Kafka broker 连接设置
- name: brokers
value: localhost:9092
# consumer 配置:topic 和 consumer group
- name: topics
value: sample
- name: consumerGroup
value: group1
# publisher 配置:topic
- name: publishTopic
value: sample
- name: authRequired
value: false
- name: direction
value: input
监听传入事件(输入绑定)
配置您的应用程序以接收传入事件。如果您使用 HTTP,则需要:
- 监听一个
POST端点,端点名称为绑定名称,即binding.yaml文件中metadata.name指定的名称。 - 验证您的应用程序允许 Dapr 对此端点进行
OPTIONS请求。
以下是利用 Dapr SDK 演示输入绑定的代码示例。
以下示例演示如何使用 ASP.NET Core 控制器配置输入绑定。
using System.Collections.Generic;
using System.Threading.Tasks;
using System;
using Microsoft.AspNetCore.Mvc;
namespace CheckoutService.controller;
[ApiController]
public sealed class CheckoutServiceController : ControllerBase
{
[HttpPost("/checkout")]
public ActionResult<string> getCheckout([FromBody] int orderId)
{
Console.WriteLine($"Received Message: {orderId}");
return $"CID{orderId}";
}
}
以下示例演示如何使用 minimal API 方式配置相同的输入绑定:
app.MapPost("checkout", ([FromBody] int orderId) =>
{
Console.WriteLine($"Received Message: {orderId}");
return $"CID{orderId}"
});
以下示例演示如何使用 minimal API 方式配置相同的输入绑定:
app.MapPost("checkout", ([FromBody] int orderId) =>
{
Console.WriteLine($"Received Message: {orderId}");
return $"CID{orderId}"
});
//dependencies
import org.springframework.web.bind.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
//code
@RestController
@RequestMapping("/")
public class CheckoutServiceController {
private static final Logger log = LoggerFactory.getLogger(CheckoutServiceController.class);
@PostMapping(path = "/checkout")
public Mono<String> getCheckout(@RequestBody(required = false) byte[] body) {
return Mono.fromRunnable(() ->
log.info("Received Message: " + new String(body)));
}
}
#dependencies
import logging
from dapr.ext.grpc import App, BindingRequest
#code
app = App()
@app.binding('checkout')
def getCheckout(request: BindingRequest):
logging.basicConfig(level = logging.INFO)
logging.info('Received Message : ' + request.text())
app.run(6002)
//dependencies
import (
"encoding/json"
"log"
"net/http"
"github.com/gorilla/mux"
)
//code
func getCheckout(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var orderId int
err := json.NewDecoder(r.Body).Decode(&orderId)
log.Println("Received Message: ", orderId)
if err != nil {
log.Printf("error parsing checkout input binding payload: %s", err)
w.WriteHeader(http.StatusOK)
return
}
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/checkout", getCheckout).Methods("POST", "OPTIONS")
http.ListenAndServe(":6002", r)
}
//dependencies
import { DaprServer, CommunicationProtocolEnum } from '@dapr/dapr';
//code
const daprHost = "127.0.0.1";
const serverHost = "127.0.0.1";
const serverPort = "6002";
const daprPort = "3602";
start().catch((e) => {
console.error(e);
process.exit(1);
});
async function start() {
const server = new DaprServer({
serverHost,
serverPort,
communicationProtocol: CommunicationProtocolEnum.HTTP,
clientOptions: {
daprHost,
daprPort,
}
});
await server.binding.receive('checkout', async (orderId) => console.log(`Received Message: ${JSON.stringify(orderId)}`));
await server.start();
}
确认事件
从您的 HTTP 处理程序返回 200 OK 响应,告知 Dapr 您已成功处理事件。
拒绝事件
返回 200 OK 以外的任何响应,告知 Dapr 事件在您的应用程序中未正确处理,并计划重新传递该事件。例如,返回 500 Error。
指定自定义路由
默认情况下,传入事件将发送到与输入绑定名称对应的 HTTP 端点。您可以通过在 binding.yaml 中设置以下 metadata 属性来覆盖此设置:
name: mybinding
spec:
type: binding.rabbitmq
metadata:
- name: route
value: /onevent
事件传递保证
事件传递保证由绑定实现控制。根据绑定实现的不同,事件传递可以是恰好一次或至少一次。
参考
1.5.3 - 操作指南:使用输出绑定与外部资源交互
使用输出绑定,您可以调用外部资源。调用请求中可以发送可选的有效负载和元数据。

本指南以 Kafka 绑定为例。您可以从绑定组件列表中找到您需要的绑定规范。在本指南中:
- 示例通过调用
/binding端点,并传递checkout(要调用的绑定名称)来执行操作。 - 有效负载放入必需的
data字段中,可以是任何可序列化为 JSON 的值。 operation字段告诉绑定需要执行什么操作。例如,Kafka 绑定支持create操作。- 您可以查看每个输出绑定支持的操作(特定于各组件)。
注意
如果您还没有尝试过,可以先体验绑定快速入门,快速了解如何使用绑定 API。创建绑定
创建一个 binding.yaml 文件,并将其保存到应用程序目录中的 components 子文件夹中。
创建一个名为 checkout 的新绑定组件。在 metadata 部分中,配置以下与 Kafka 相关的属性:
- 您要向其发布消息的主题
- 代理
创建绑定组件时,指定绑定的受支持的 direction。
使用 dapr run 命令时,通过 --resources-path 标志指向您的自定义资源目录。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: checkout
spec:
type: bindings.kafka
version: v1
metadata:
# Kafka broker 连接设置
- name: brokers
value: localhost:9092
# 消费者配置:主题和消费者组
- name: topics
value: sample
- name: consumerGroup
value: group1
# 发布者配置:主题
- name: publishTopic
value: sample
- name: authRequired
value: false
- name: direction
value: output
要将以下 binding.yaml 文件部署到 Kubernetes 集群中,请运行 kubectl apply -f binding.yaml。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: checkout
spec:
type: bindings.kafka
version: v1
metadata:
# Kafka broker 连接设置
- name: brokers
value: localhost:9092
# 消费者配置:主题和消费者组
- name: topics
value: sample
- name: consumerGroup
value: group1
# 发布者配置:主题
- name: publishTopic
value: sample
- name: authRequired
value: false
- name: direction
value: output
发送事件(输出绑定)
下面的代码示例利用 Dapr SDK 来调用运行中的 Dapr 实例上的输出绑定端点。
以下是在 .NET 6+ 中使用顶级语句的控制台应用程序示例:
using System.Text;
using System.Threading.Tasks;
using Dapr.Client;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();
const string BINDING_NAME = "checkout";
const string BINDING_OPERATION = "create";
var random = new Random();
using var daprClient = app.Services.GetRequiredService<DaprClient>();
while (true)
{
await Task.Delay(TimeSpan.FromSeconds(5));
var orderId = random.Next(1, 1000);
await client.InvokeBindingAsync(BINDING_NAME, BINDING_OPERATION, orderId);
Console.WriteLine($"Sending message: {orderId}");
}
//dependencies
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.HttpExtension;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Random;
import java.util.concurrent.TimeUnit;
//code
@SpringBootApplication
public class OrderProcessingServiceApplication {
private static final Logger log = LoggerFactory.getLogger(OrderProcessingServiceApplication.class);
public static void main(String[] args) throws InterruptedException{
String BINDING_NAME = "checkout";
String BINDING_OPERATION = "create";
while(true) {
TimeUnit.MILLISECONDS.sleep(5000);
Random random = new Random();
int orderId = random.nextInt(1000-1) + 1;
DaprClient client = new DaprClientBuilder().build();
//使用 Dapr SDK 调用输出绑定
client.invokeBinding(BINDING_NAME, BINDING_OPERATION, orderId).block();
log.info("Sending message: " + orderId);
}
}
}
#dependencies
import random
from time import sleep
import requests
import logging
import json
from dapr.clients import DaprClient
#code
logging.basicConfig(level = logging.INFO)
BINDING_NAME = 'checkout'
BINDING_OPERATION = 'create'
while True:
sleep(random.randrange(50, 5000) / 1000)
orderId = random.randint(1, 1000)
with DaprClient() as client:
#使用 Dapr SDK 调用输出绑定
resp = client.invoke_binding(BINDING_NAME, BINDING_OPERATION, json.dumps(orderId))
logging.basicConfig(level = logging.INFO)
logging.info('Sending message: ' + str(orderId))
//dependencies
import (
"context"
"log"
"math/rand"
"time"
"strconv"
dapr "github.com/dapr/go-sdk/client"
)
//code
func main() {
BINDING_NAME := "checkout";
BINDING_OPERATION := "create";
for i := 0; i < 10; i++ {
time.Sleep(5000)
orderId := rand.Intn(1000-1) + 1
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
defer client.Close()
ctx := context.Background()
//使用 Dapr SDK 调用输出绑定
in := &dapr.InvokeBindingRequest{ Name: BINDING_NAME, Operation: BINDING_OPERATION , Data: []byte(strconv.Itoa(orderId))}
err = client.InvokeOutputBinding(ctx, in)
log.Println("Sending message: " + strconv.Itoa(orderId))
}
}
//dependencies
import { DaprClient, CommunicationProtocolEnum } from "@dapr/dapr";
//code
const daprHost = "127.0.0.1";
(async function () {
for (var i = 0; i < 10; i++) {
await sleep(2000);
const orderId = Math.floor(Math.random() * (1000 - 1) + 1);
try {
await sendOrder(orderId)
} catch (err) {
console.error(e);
process.exit(1);
}
}
})();
async function sendOrder(orderId) {
const BINDING_NAME = "checkout";
const BINDING_OPERATION = "create";
const client = new DaprClient({
daprHost,
daprPort: process.env.DAPR_HTTP_PORT,
communicationProtocol: CommunicationProtocolEnum.HTTP,
});
//使用 Dapr SDK 调用输出绑定
const result = await client.binding.send(BINDING_NAME, BINDING_OPERATION, orderId);
console.log("Sending message: " + orderId);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
您也可以使用 HTTP 来调用输出绑定端点:
curl -X POST -H 'Content-Type: application/json' http://localhost:3601/v1.0/bindings/checkout -d '{ "data": 100, "operation": "create" }'
观看此视频了解如何使用双向输出绑定。
参考
1.6 - Actors
1.6.1 - Actor 概述
Actor 模式将 actor 描述为最低级别的"计算单元"。换句话说,你需要将代码编写在一个自包含的单元(称为 actor)中,该单元接收消息并一次处理一条消息,无需任何并发或线程机制。
当代码处理消息时,它可以向其他 actor 发送一条或多条消息,或创建新的 actor。底层运行时负责管理每个 actor 的运行方式、时间和位置,并在 actor 之间路由消息。
大量 actor 可以同时执行,并且 actor 之间相互独立执行。
Dapr 中的 Actor
Dapr 包含一个专门实现虚拟 Actor 模式的运行时。通过 Dapr 的实现,你可以根据 actor 模型编写 Dapr actor,Dapr 利用底层平台提供的可扩展性和可靠性保证。
每个 actor 都定义为 actor 类型的实例,就像对象是类的实例一样。例如,可能有一个实现了计算器功能的 actor 类型,并且可能有多个该类型的 actor 分布在集群中的各个节点上。每个这样的 actor 都通过 actor ID 进行唯一标识。

下面的概述视频和演示展示了 Dapr 中 actor 的工作原理。
Dapr Actor 与 Dapr Workflow 的对比
Dapr actor 基于状态管理和服务调用 API 创建具有身份的有状态、长时间运行的对象。Dapr Workflow 与 Dapr Actor 相关,workflow 构建在 actor 之上,提供更高级别的抽象来编排一组 actor,实现常见的 workflow 模式并代表你管理 actor 的生命周期。
Dapr actor 旨在提供一种在分布式系统中封装状态和行为的方式。actor 可以由客户端应用程序按需激活。当 actor 被激活时,它会被分配一个唯一的身份,这允许它在多次调用之间维护其状态。这使得 actor 非常适合构建有状态、可扩展和容错的分布式应用程序。
另一方面,Dapr Workflow 提供了一种定义和编排涉及分布式系统中多个服务和组件的复杂工作流的方式。Workflow 允许你定义需要按特定顺序执行的步骤或任务序列,并可用于实现业务流程、事件驱动的工作流和其他类似场景。
如上所述,Dapr Workflow 构建在 Dapr Actor 之上,管理其激活和生命周期。
何时使用 Dapr actor
与任何其他技术决策一样,你应该根据要解决的问题来决定是否使用 actor。例如,如果你正在构建聊天应用程序,你可能会使用 Dapr actor 来实现聊天室和用户之间的单独聊天会话,因为每个聊天会话都需要维护自己的状态并且是可扩展和容错的。
一般来说,如果满足以下条件,请考虑使用 actor 模式来为你的问题或场景建模:
- 你的问题空间涉及大量(数千或更多)小型、独立和隔离的状态与逻辑单元。
- 你希望使用单线程对象,这些对象不需要来自外部组件的大量交互,包括跨一组 actor 查询状态。
- 你的 actor 实例不会通过发出 I/O 操作来以不可预测的延迟阻塞调用者。
何时使用 Dapr Workflow
当你需要定义和编排涉及多个服务和组件的复杂工作流时,你将使用 Dapr Workflow。例如,使用前面的聊天应用程序示例,你可能会使用 Dapr Workflow 来定义应用程序的总体工作流,例如如何注册新用户、如何发送和接收消息,以及应用程序如何处理错误和异常。
了解有关 Dapr Workflow 的更多信息以及如何在应用程序中使用工作流。
Actor 类型和 actor ID
Actor 唯一定义为 actor 类型的实例,类似于对象是类的实例。例如,你可能有一个实现了计算器功能的 actor 类型。该类型的许多 actor 可能分布在集群中的各个节点上。
每个 actor 都通过 actor ID 进行唯一标识。actor ID 可以是你选择的_任何_字符串值。如果你不提供 actor ID,Dapr 会为你生成一个随机字符串作为 ID。
功能
命名空间 Actor
Dapr 支持命名空间 actor。可以将 actor 类型部署到不同的命名空间中。你可以在同一命名空间中调用这些 actor 的实例。
Actor 生命周期
由于 Dapr actor 是虚拟的,因此不需要显式创建或销毁它们。Dapr actor 运行时:
- 一旦接收到针对该 actor ID 的初始请求,就会自动激活 actor。
- 对未使用的 actor 的内存对象进行垃圾回收。
- 维护 actor 存在的知识,以防以后重新激活。
actor 的状态超出对象的生命周期,因为状态存储在为 Dapr 运行时配置的状态提供程序中。
分布和故障转移
为了提供可扩展性和可靠性,actor 实例分布在集群中,Dapr 在整个集群中分布 actor 实例并自动将它们迁移到健康节点。
Actor 通信
你可以通过 HTTP 调用 actor 方法来调用它们,如下面的常规示例所示。

- 服务调用边车上的 actor API。
- 使用来自放置服务的缓存分区信息,边车确定哪个 actor 服务实例将托管 actor ID 3。调用被转发到相应的边车。
- Pod 2 中的边车实例调用服务实例以调用 actor 并执行 actor 方法。
并发性
Dapr actor 运行时为访问 actor 方法提供了一个简单的基于轮次的访问模型。基于轮次的访问大大简化了并发系统,因为不需要用于数据访问的同步机制。
状态
事务性状态存储可用于存储 actor 状态。无论你是否打算在 actor 中存储任何状态,都必须在状态存储组件的元数据部分为属性 actorStateStore 指定值 true。Actor 状态以特定方案存储在事务性状态存储中,从而允许进行一致的查询。只能使用单个状态存储组件作为所有 actor 的状态存储。阅读状态 API 参考和 actor API 参考以了解有关 actor 状态存储的更多信息。
Actor 定时器和提醒
Actor 可以通过注册定时器或提醒来为自己安排定期工作。
定时器和提醒的功能非常相似。主要区别在于,Dapr actor 运行时在停用后不保留有关定时器的任何信息,而使用 Dapr actor 状态提供程序持久化有关提醒的信息。
这种区别允许用户在轻量级但无状态的定时器与资源需求更高但有状态的提醒之间进行权衡。
下面的概述视频和演示展示了 actor 定时器和提醒的工作原理。
后续步骤
Actor 功能和概念 >>相关链接
1.6.2 - Actor 运行时功能
既然您已经从高层次了解了 Actor 构建块,让我们深入探讨 Dapr 中 Actor 包含的功能和概念。
Actor 生命周期
Dapr Actor 是虚拟的,这意味着它们的生命周期与其内存中的表示形式无关。因此,它们不需要被显式创建或销毁。Dapr Actor 运行时在首次收到针对该 Actor ID 的请求时自动激活 Actor。如果 Actor 在一段时间内未被使用,Dapr Actor 运行时会回收内存中的对象。如果稍后需要重新激活,它也会保留有关 Actor 存在的知识。
调用 Actor 方法、定时器和提醒会重置 Actor 空闲时间。例如,提醒触发会保持 Actor 处于活动状态。
- Actor 提醒无论 Actor 处于活动状态还是非活动状态都会触发。如果为非活动 Actor 触发,它会先激活该 Actor。
- Actor 定时器触发会重置空闲时间;但是,定时器仅在 Actor 处于活动状态时才会触发。
Dapr 运行时用于检查 Actor 是否可以被垃圾回收的空闲超时和扫描间隔是可配置的。当 Dapr 运行时调用 Actor 服务以获取支持的 Actor 类型时,可以传递此信息。
由于虚拟 Actor 模型,这种虚拟 Actor 生命周期抽象存在一些注意事项,实际上 Dapr Actor 的实现有时会偏离此模型。
首次向其 Actor ID 发送消息时,Actor 会自动激活(导致构造 Actor 对象)。一段时间后,Actor 对象被垃圾回收。将来,再次使用 Actor ID 会导致构造新的 Actor 对象。Actor 的状态比对象的生命周期更长,因为状态存储在为 Dapr 运行时配置的状态提供程序中。
分布和故障转移
为了提供可扩展性和可靠性,Actor 实例分布在整个集群中,Dapr 会根据需要自动将它们从故障节点迁移到健康节点。
Actor 分布在 Actor 服务的实例中,这些实例分布在集群中的节点上。每个服务实例包含给定 Actor 类型的一组 Actor。
Actor 放置服务
Dapr Actor 运行时通过 Actor Placement 服务为您管理分布方案和键范围设置。当创建服务的新实例时:
- 边车调用 Actor 服务以检索注册的 Actor 类型和配置设置。
- 相应的 Dapr 运行时注册它可以创建的 Actor 类型。
Placement服务计算给定 Actor 类型在所有实例中的分区。
每个 Actor 类型的此分区数据表会在环境中运行的每个 Dapr 实例中更新和存储,并且可以随着创建和销毁新的 Actor 服务实例而动态变化。

当客户端调用具有特定 ID 的 Actor(例如,actor id 123)时,客户端的 Dapr 实例会对 Actor 类型和 ID 进行哈希处理,并使用该信息调用可以服务于该特定 Actor ID 请求的相应 Dapr 实例。因此,对于任何给定的 Actor ID,总是调用相同的分区(或服务实例)。下图显示了这一点。

这简化了一些选择,但也带来了一些考虑:
- 默认情况下,Actor 被随机放置到 pod 中,从而实现均匀分布。
- 由于 Actor 是随机放置的,因此应预期 Actor 操作始终需要网络通信,包括方法调用数据的序列化和反序列化,从而产生延迟和开销。
注意
注意:Dapr Actor Placement 服务仅用于 Actor 放置,因此如果您的服务不使用 Dapr Actor,则不需要它。Placement 服务可以在所有托管环境中运行,包括自托管和 Kubernetes。Actor 通信
您可以通过调用 HTTP 端点与 Dapr 交互以调用 Actor 方法。
POST/GET/PUT/DELETE http://localhost:3500/v1.0/actors/<actorType>/<actorId>/<method/state/timers/reminders>
您可以在请求正文中为 Actor 方法提供任何数据,请求的响应将在响应正文中,即来自 Actor 调用的数据。
另一种也许更方便的与 Actor 交互的方式是通过 SDK。Dapr 目前支持 .NET、Java 和 Python 的 Actor SDK。
有关更多详细信息,请参阅 Dapr Actor 功能。
并发
Dapr Actor 运行时为访问 Actor 方法提供了一个简单的基于轮次的访问模型。这意味着在任何时候,一个 Actor 对象的代码中只能有一个线程处于活动状态。基于轮次的访问大大简化了并发系统,因为不需要数据访问的同步机制。这也意味着系统在设计时必须特别考虑每个 Actor 实例的单线程访问性质。
单个 Actor 实例一次不能处理多个请求。如果期望 Actor 实例处理并发请求,它可能会导致吞吐量瓶颈。
如果两个 Actor 之间存在循环请求,同时对外部请求之一发出外部请求,Actor 可能会相互死锁。Dapr Actor 运行时会在 Actor 调用时自动超时并向调用者抛出异常,以中断可能的死锁情况。

重入
要允许 Actor “重入"并调用自身的方法,请参阅 Actor 重入。
基于轮次的访问
一个轮次包括响应来自其他 Actor 或客户端的请求而对 Actor 方法进行的完整执行,或定时器/提醒回调的完整执行。尽管这些方法和回调是异步的,但 Dapr Actor 运行时不会交错它们。一个轮次必须完全完成后才允许进行新的轮次。换句话说,当前正在执行的 Actor 方法或定时器/提醒回调必须在允许新的方法或回调调用之前完全完成。如果执行已从方法或回调返回,并且方法或回调返回的任务已完成,则认为方法或回调已完成。值得强调的是,即使在不同方法、定时器和回调之间也会遵守基于轮次的并发。
Dapr Actor 运行时通过在轮次开始时获取每个 Actor 的锁并在轮次结束时释放锁来强制执行基于轮次的并发。因此,基于轮次的并发是针对每个 Actor 强制执行的,而不是跨 Actor 强制执行的。Actor 方法和定时器/提醒回调可以同时代表不同的 Actor 执行。
以下示例说明了上述概念。考虑一个实现两个异步方法(例如,Method1 和 Method2)、一个定时器和一个提醒的 Actor 类型。下图显示了代表属于此 Actor 类型的两个 Actor(ActorId1 和 ActorId2)执行这些方法和回调的时间线示例。

后续步骤
定时器和提醒 >>相关链接
1.6.3 - Actor 运行时配置参数
您可以使用以下配置参数修改默认的 Dapr Actor 运行时行为。
| 参数 | 描述 | 默认值 |
|---|---|---|
entities | 此主机支持的 Actor 类型。 | N/A |
actorIdleTimeout | 停用空闲 Actor 前的超时时间。每隔 actorScanInterval 间隔检查一次超时。 | 60 分钟 |
actorScanInterval | 扫描需要停用的空闲 Actor 的频率。空闲时间超过 actor_idle_timeout 的 Actor 将被停用。 | 30 秒 |
drainOngoingCallTimeout | 正在迁移重平衡 Actor 时的持续时间。这指定了当前活动 Actor 方法完成的超时时间。如果没有当前 Actor 方法调用,则忽略此参数。 | 60 秒 |
drainRebalancedActors | 如果为 true,Dapr 将等待 drainOngoingCallTimeout 持续时间,以允许当前 Actor 调用完成,然后再尝试停用 Actor。 | true |
reentrancy (ActorReentrancyConfig) | 配置 Actor 的重入行为。如果未提供,则禁用重入。 | 禁用,false |
entitiesConfig | 使用配置数组为每个 Actor 类型单独配置。在各个实体配置中指定的任何实体也必须在顶层 entities 字段中指定。 | N/A |
示例
// In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Register actor runtime with DI
services.AddActors(options =>
{
// Register actor types and configure actor settings
options.Actors.RegisterActor<MyActor>();
// Configure default settings
options.ActorIdleTimeout = TimeSpan.FromMinutes(60);
options.ActorScanInterval = TimeSpan.FromSeconds(30);
options.DrainOngoingCallTimeout = TimeSpan.FromSeconds(60);
options.DrainRebalancedActors = true;
options.ReentrancyConfig = new() { Enabled = false };
// Add a configuration for a specific actor type.
// This actor type must have a matching value in the base level 'entities' field. If it does not, the configuration will be ignored.
// If there is a matching entity, the values here will be used to overwrite any values specified in the root configuration.
// In this example, `ReentrantActor` has reentrancy enabled; however, 'MyActor' will not have reentrancy enabled.
options.Actors.RegisterActor<ReentrantActor>(typeOptions: new()
{
ReentrancyConfig = new()
{
Enabled = true,
}
});
});
// Register additional services for use with actors
services.AddSingleton<BankService>();
}
import { CommunicationProtocolEnum, DaprClient, DaprServer } from "@dapr/dapr";
// Configure the actor runtime with the DaprClientOptions.
const clientOptions = {
actor: {
actorIdleTimeout: "1h",
actorScanInterval: "30s",
drainOngoingCallTimeout: "1m",
drainRebalancedActors: true,
reentrancy: {
enabled: true,
maxStackDepth: 32,
},
},
};
// Use the options when creating DaprServer and DaprClient.
// Note, DaprServer creates a DaprClient internally, which needs to be configured with clientOptions.
const server = new DaprServer(serverHost, serverPort, daprHost, daprPort, clientOptions);
const client = new DaprClient(daprHost, daprPort, CommunicationProtocolEnum.HTTP, clientOptions);
from datetime import timedelta
from dapr.actor.runtime.config import ActorRuntimeConfig, ActorReentrancyConfig
ActorRuntime.set_actor_config(
ActorRuntimeConfig(
actor_idle_timeout=timedelta(hours=1),
actor_scan_interval=timedelta(seconds=30),
drain_ongoing_call_timeout=timedelta(minutes=1),
drain_rebalanced_actors=True,
reentrancy=ActorReentrancyConfig(enabled=False),
)
)
// import io.dapr.actors.runtime.ActorRuntime;
// import java.time.Duration;
ActorRuntime.getInstance().getConfig().setActorIdleTimeout(Duration.ofMinutes(60));
ActorRuntime.getInstance().getConfig().setActorScanInterval(Duration.ofSeconds(30));
ActorRuntime.getInstance().getConfig().setDrainOngoingCallTimeout(Duration.ofSeconds(60));
ActorRuntime.getInstance().getConfig().setDrainBalancedActors(true);
ActorRuntime.getInstance().getConfig().setActorReentrancyConfig(false, null);
const (
defaultActorType = "basicType"
reentrantActorType = "reentrantType"
)
type daprConfig struct {
Entities []string `json:"entities,omitempty"`
ActorIdleTimeout string `json:"actorIdleTimeout,omitempty"`
ActorScanInterval string `json:"actorScanInterval,omitempty"`
DrainOngoingCallTimeout string `json:"drainOngoingCallTimeout,omitempty"`
DrainRebalancedActors bool `json:"drainRebalancedActors,omitempty"`
Reentrancy config.ReentrancyConfig `json:"reentrancy,omitempty"`
EntitiesConfig []config.EntityConfig `json:"entitiesConfig,omitempty"`
}
var daprConfigResponse = daprConfig{
Entities: []string{defaultActorType, reentrantActorType},
ActorIdleTimeout: actorIdleTimeout,
ActorScanInterval: actorScanInterval,
DrainOngoingCallTimeout: drainOngoingCallTimeout,
DrainRebalancedActors: drainRebalancedActors,
Reentrancy: config.ReentrancyConfig{Enabled: false},
EntitiesConfig: []config.EntityConfig{
{
// Add a configuration for a specific actor type.
// This actor type must have a matching value in the base level 'entities' field. If it does not, the configuration will be ignored.
// If there is a matching entity, the values here will be used to overwrite any values specified in the root configuration.
// In this example, `reentrantActorType` has reentrancy enabled; however, 'defaultActorType' will not have reentrancy enabled.
Entities: []string{reentrantActorType},
Reentrancy: config.ReentrancyConfig{
Enabled: true,
MaxStackDepth: &maxStackDepth,
},
},
},
}
func configHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(daprConfigResponse)
}
相关链接
1.6.4 - 命名空间 Actor
Dapr 中的命名空间提供隔离能力,从而实现多租户。通过 Actor 命名空间,相同的 Actor 类型可以部署到不同的命名空间中。你可以调用同一命名空间中的这些 Actor 实例。
注意
每个命名空间 Actor 部署必须使用各自独立的状态存储,尤其是当相同的 Actor 类型跨命名空间使用时。换言之,命名空间信息不会作为 Actor 记录的一部分写入,因此每个命名空间都需要独立的状态存储。有关示例,请参见为命名空间配置 Actor 状态存储章节。创建和配置命名空间
你可以在自托管模式或 Kubernetes 上使用命名空间。
在自托管模式下,你可以通过设置 NAMESPACE 环境变量来指定 Dapr 实例的命名空间。
在 Kubernetes 上,你可以在部署 Actor 应用程序时创建和配置命名空间。例如,从以下 kubectl 命令开始:
kubectl create namespace namespace-actorA
kubectl config set-context --current --namespace=namespace-actorA
然后,将你的 Actor 应用程序部署到此命名空间中(本例中为 namespace-actorA)。
为命名空间配置 Actor 状态存储
每个命名空间 Actor 部署必须使用各自独立的状态存储。虽然你可以为每个 Actor 命名空间使用不同的物理数据库,但某些状态存储组件提供了通过表、前缀、集合等方式进行逻辑隔离的方法。这允许你使用相同的物理数据库来支持多个命名空间,只要你在 Dapr 组件定义中提供逻辑隔离即可。
以下是一些示例。
示例 1:通过 etcd 中的前缀
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.etcd
version: v2
metadata:
- name: endpoints
value: localhost:2379
- name: keyPrefixPath
value: namespace-actorA
- name: actorStateStore
value: "true"
示例 2:通过 SQLite 中的表名
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.sqlite
version: v1
metadata:
- name: connectionString
value: "data.db"
- name: tableName
value: "namespace-actorA"
- name: actorStateStore
value: "true"
示例 3:通过 Redis 中的逻辑数据库编号
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"
- name: redisDB
value: "1"
- name: redisPassword
secretKeyRef:
name: redis-secret
key: redis-password
- name: actorStateStore
value: "true"
- name: redisDB
value: "1"
auth:
secretStore: <SECRET_STORE_NAME>
请查看你的状态存储组件规范以了解其提供的功能。
注意
命名空间 Actor 使用多租户 Placement 服务。使用此控制平面服务时,每个应用程序部署都有自己的命名空间,属于命名空间"ActorA"应用程序的边车不会收到命名空间"ActorB"中应用程序的放置信息。后续步骤
1.6.5 - Actor 定时器和提醒
Actor 可以通过注册定时器或提醒来调度自身的周期性工作。
定时器和提醒的功能非常相似。主要区别在于,Dapr actor 运行时在停用后不保留关于定时器的任何信息,而使用 Dapr Scheduler 持久化关于提醒的信息。
这种区别允许用户在轻量级但无状态的定时器与资源消耗更大但有状态的提醒之间进行权衡。
定时器和提醒的调度配置总结如下:
data 是一个可选参数,包含在调用时传递给提醒回调方法的数据。
dueTime 是一个可选参数,设置首次调用回调的时间或时间间隔。如果省略 dueTime,回调将在定时器/提醒注册后立即被调用。
支持的格式:
- RFC3339 日期格式,例如
2020-10-02T15:00:00Z - time.Duration 格式,例如
2h30m - ISO 8601 duration 格式,例如
PT2H30M
period 是一个可选参数,设置两次连续回调调用之间的时间间隔。当以 ISO 8601-1 duration 格式指定时,你还可以配置重复次数以限制回调调用的总次数。
如果省略 period,回调将只被调用一次。
支持的格式:
- time.Duration 格式(使用持续时间值时支持亚秒级精度),例如
2h30m、500ms - ISO 8601 duration 格式,例如
PT2H30M、R5/PT1M30S
ttl 是一个可选参数,设置定时器/提醒到期和删除的时间或时间间隔。如果省略 ttl,则不应用任何限制。
支持的格式:
- RFC3339 日期格式,例如
2020-10-02T15:00:00Z - time.Duration 格式,例如
2h30m - ISO 8601 duration 格式。例如:
PT2H30M
仅适用于提醒。
overwrite 是一个可选布尔参数,指示是否覆盖同名的现有提醒。
如果 overwrite 设置为 true,任何同名的现有提醒将被新配置替换。
如果 overwrite 设置为 false 或省略,并且已存在同名的提醒,操作将失败并返回已存在的错误。
请注意,覆盖现有提醒将重置其状态,包括调用次数和下次触发时间,就像创建新提醒一样。
仅适用于提醒。
failurePolicy 是一个可选参数,定义提醒调用失败时的行为。
支持的失败策略包括:
drop:丢弃失败的调用,提醒继续按计划进行下一次调用,就好像失败没有发生一样。constant:提醒将以固定的间隔重试失败的调用指定次数。interval:每次重试尝试之间的时间间隔。如果未指定,间隔为 “0s”,意味着立即尝试重试。maxRetries:重试尝试的最大次数。如果未指定,调用将以指定间隔无限期重试,直到成功。
如果未指定失败策略,将应用默认的失败策略:重试 3 次,间隔为 1 秒。
actor 运行时会验证调度配置的正确性,并在输入无效时返回错误。
当你在 period 中指定重复次数以及 ttl 时,定时器/提醒将在任一条件满足时停止,以先发生者为准。
Actor 定时器
你可以为 actor 注册一个基于定时器执行的回调。
Dapr actor 运行时确保回调方法遵守基于轮次的并发保证。这意味着在此回调完成执行之前,不会有其他 actor 方法或定时器/提醒回调正在进行。
Dapr actor 运行时在回调完成时保存对 actor 状态所做的更改。如果在保存状态时发生错误,该 actor 对象将被停用,并将激活一个新实例。
当 actor 作为垃圾回收的一部分被停用时,所有定时器都会停止。之后不会再调用定时器回调。此外,Dapr actor 运行时不保留关于停用前正在运行的定时器的任何信息。由 actor 在将来重新激活时注册其所需的任何定时器。
你可以通过调用对 Dapr 的 HTTP/gRPC 请求来为 actor 创建定时器,如下所示,或通过 Dapr SDK。
POST/PUT http://localhost:3500/v1.0/actors/<actorType>/<actorId>/timers/<name>
示例
定时器参数在请求体中指定。
以下请求体配置了一个定时器,dueTime 为 9 秒,period 为 3 秒。这意味着它将在 9 秒后首次触发,然后每 3 秒触发一次。
{
"dueTime":"0h0m9s0ms",
"period":"0h0m3s0ms"
}
以下请求体配置了一个定时器,period 为 3 秒(ISO 8601 duration 格式)。它还将调用次数限制为 10 次。这意味着它将触发 10 次:首先在注册后立即触发,然后每 3 秒触发一次。
{
"period":"R10/PT3S",
}
以下请求体配置了一个定时器,period 为 3 秒(ISO 8601 duration 格式),ttl 为 20 秒。这意味着它在注册后立即触发,然后在 20 秒的持续时间内每 3 秒触发一次。
{
"period":"PT3S",
"ttl":"20s"
}
以下请求体配置了一个定时器,dueTime 为 10 秒,period 为 3 秒,ttl 为 10 秒。它还将调用次数限制为 4 次。这意味着它将在 10 秒后首次触发,然后在 10 秒的持续时间内每 3 秒触发一次,但总共不超过 4 次。
{
"dueTime":"10s",
"period":"R4/PT3S",
"ttl":"10s"
}
你可以通过调用以下命令来删除 actor 定时器
DELETE http://localhost:3500/v1.0/actors/<actorType>/<actorId>/timers/<name>
有关更多详细信息,请参阅 API 规范。
Actor 提醒
提醒是一种在指定时间在 actor 上触发持久回调的机制。它们的功能类似于定时器。但与定时器不同,提醒在所有情况下都会被触发,直到 actor 显式注销它们或调用次数用尽。具体来说,提醒会在 actor 停用和故障转移期间触发,因为 Dapr actor 运行时使用 Dapr Scheduler 服务 持久化关于 actor 提醒的信息。
你可以通过调用对 Dapr 的 HTTP/gRPC 请求为 actor 创建持久提醒,如下所示,或通过 Dapr SDK。
POST/PUT http://localhost:3500/v1.0/actors/<actorType>/<actorId>/reminders/<name>
与定时器不同,提醒支持 overwrite 选项,允许你替换同名的现有提醒,以及 failurePolicy 选项,定义提醒调用失败时的行为。
检索 actor 提醒
你可以通过调用以下命令来检索 actor 提醒
GET http://localhost:3500/v1.0/actors/<actorType>/<actorId>/reminders/<name>
删除 actor 提醒
你可以通过调用以下命令来删除 actor 提醒
DELETE http://localhost:3500/v1.0/actors/<actorType>/<actorId>/reminders/<name>
如果触发 actor 提醒且应用未向运行时返回 2** 代码(例如,由于连接问题),actor 提醒将根据其失败策略进行重试,默认情况下是三次,每次尝试之间的退避间隔为 1 秒。 根据任何可选应用的 actor 弹性策略,可能会尝试额外的重试。
有关更多详细信息,请参阅 API 规范。
错误处理
当 actor 的方法成功完成时,运行时将继续按照指定的定时器或提醒计划调用该方法。 为了允许 actor 从故障中恢复并在崩溃或重启后重试,你可以通过配置状态存储(例如 Redis 或 Azure Cosmos DB)来持久化 actor 的状态。
如果方法的调用失败,定时器不会被删除。定时器仅在以下情况下被删除:
- sidecar 终止
- 执行次数用尽
- 你显式删除它
使用 CLI 管理提醒
Actor 提醒持久化在 Scheduler 中。 你可以使用 dapr scheduler CLI 命令管理它们。
列出 actor 提醒
dapr scheduler list --filter actor
NAME BEGIN COUNT LAST TRIGGER
actor/MyActorType/actorid1/test1 -3.89s 1 2025-10-03T16:58:55Z
actor/MyActorType/actorid2/test2 -3.89s 1 2025-10-03T16:58:55Z
获取提醒详细信息
dapr scheduler get actor/MyActorType/actorid1/test1 -o yaml
删除提醒
删除单个提醒:
dapr scheduler delete actor/MyActorType/actorid1/test1
删除给定 actor 类型的所有提醒:
dapr scheduler delete-all actor/MyActorType
使用 CLI 管理提醒
Actor 提醒持久化在 Scheduler 中。 你可以使用 dapr scheduler CLI 命令管理它们。
列出 actor 提醒
dapr scheduler list --filter actor
NAME BEGIN COUNT LAST TRIGGER
actor/MyActorType/actorid1/test1 -3.89s 1 2025-10-03T16:58:55Z
actor/MyActorType/actorid2/test2 -3.89s 1 2025-10-03T16:58:55Z
获取提醒详细信息
dapr scheduler get actor/MyActorType/actorid1/test1 -o yaml
删除提醒
删除单个提醒:
dapr scheduler delete actor/MyActorType/actorid1/test1
删除给定 actor 类型的所有提醒:
dapr scheduler delete-all actor/MyActorType
删除特定 actor 实例的所有提醒:
dapr scheduler delete-all actor/MyActorType/actorid1
删除特定 actor 类型的所有提醒:
dapr scheduler delete-all actor/MyActorType
删除所有提醒
dapr scheduler delete-all actor
备份和恢复提醒
导出所有提醒:
dapr scheduler export -o reminders-backup.bin
从备份文件恢复:
dapr scheduler import -f reminders-backup.bin
总结
- 提醒存储在 Dapr Scheduler 中,而不是应用中。
- 通过 Actors API 创建提醒
- 使用
dapr schedulerCLI 管理现有提醒(列出、获取、删除、备份/恢复)。
后续步骤
配置 actor 运行时行为 >>相关链接
1.6.6 - 如何:通过脚本与虚拟 actor 交互
学习如何通过调用 HTTP/gRPC 端点来使用虚拟 actor。
调用 actor 方法
您可以通过调用 HTTP/gRPC 端点与 Dapr 交互来调用 actor 方法。
POST/GET/PUT/DELETE http://localhost:3500/v1.0/actors/<actorType>/<actorId>/method/<method>
在请求正文中提供 actor 方法的数据。请求的响应(来自 actor 方法调用的数据)位于响应正文中。
有关更多详细信息,请参阅 Actor API 规范。
注意
或者,您可以使用 Dapr SDK 来使用 actor。使用 actor 保存状态
您可以通过 HTTP/gRPC 端点与 Dapr 交互,利用 Dapr actor 状态管理功能可靠地保存状态。
要使用 actor,您的状态存储必须支持多项目事务。这意味着您的状态存储组件必须实现 TransactionalStore 接口。
查看支持事务/actor 的组件列表。所有 actor 只能使用单个状态存储组件作为状态存储。
后续步骤
Actor 重入 >>相关链接
1.6.7 - 如何:在 Dapr 中启用和使用 Actor 重入
虚拟 Actor 模式 的一个核心原则是 Actor 执行的单线程特性。在没有重入的情况下,Dapr 运行时会对所有 Actor 请求进行加锁。第二个请求无法在第一个请求完成之前开始。这意味着 Actor 无法调用自身,或者让另一个 Actor 调用它,即使它是同一调用链的一部分。
重入通过允许来自同一链或上下文的请求重新进入已锁定的 Actor 来解决这个问题。这在以下场景中非常有用:
- Actor 想要调用自身的方法
- Actor 在工作流中用于执行工作,然后回调到协调 Actor。
重入所允许的调用链示例如下:
Actor A -> Actor A
ActorA -> Actor B -> Actor A
通过重入,你可以执行更复杂的 Actor 调用,而不会牺牲虚拟 Actor 的单线程行为。

maxStackDepth 参数设置一个值,用于控制可以对同一 Actor 进行多少次重入调用。默认情况下,此值设置为 32,这在大多数情况下绰绰有余。
配置 Actor 运行时以启用重入
可重入的 Actor 必须提供适当的配置。这是通过 Actor 的端点 GET /dapr/config 完成的,类似于其他 Actor 配置元素。
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<BankService>();
services.AddActors(options =>
{
options.Actors.RegisterActor<DemoActor>();
options.ReentrancyConfig = new Dapr.Actors.ActorReentrancyConfig()
{
Enabled = true,
MaxStackDepth = 32,
};
});
}
}
import { CommunicationProtocolEnum, DaprClient, DaprServer } from "@dapr/dapr";
// 使用 DaprClientOptions 配置 actor 运行时。
const clientOptions = {
actor: {
reentrancy: {
enabled: true,
maxStackDepth: 32,
},
},
};
from fastapi import FastAPI
from dapr.ext.fastapi import DaprActor
from dapr.actor.runtime.config import ActorRuntimeConfig, ActorReentrancyConfig
from dapr.actor.runtime.runtime import ActorRuntime
from demo_actor import DemoActor
reentrancyConfig = ActorReentrancyConfig(enabled=True)
config = ActorRuntimeConfig(reentrancy=reentrancyConfig)
ActorRuntime.set_actor_config(config)
app = FastAPI(title=f'{DemoActor.__name__}Service')
actor = DaprActor(app)
@app.on_event("startup")
async def startup_event():
# 注册 DemoActor
await actor.register_actor(DemoActor)
@app.get("/MakeExampleReentrantCall")
def do_something_reentrant():
# 在此处调用另一个 actor,重入将自动处理
return
下面是用 Golang 编写的 Actor 片段,它通过 HTTP API 提供重入配置。Go SDK 尚未包含重入功能。
type daprConfig struct {
Entities []string `json:"entities,omitempty"`
ActorIdleTimeout string `json:"actorIdleTimeout,omitempty"`
ActorScanInterval string `json:"actorScanInterval,omitempty"`
DrainOngoingCallTimeout string `json:"drainOngoingCallTimeout,omitempty"`
DrainRebalancedActors bool `json:"drainRebalancedActors,omitempty"`
Reentrancy config.ReentrancyConfig `json:"reentrancy,omitempty"`
}
var daprConfigResponse = daprConfig{
[]string{defaultActorType},
actorIdleTimeout,
actorScanInterval,
drainOngoingCallTimeout,
drainRebalancedActors,
config.ReentrancyConfig{Enabled: true, MaxStackDepth: &maxStackDepth},
}
func configHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(daprConfigResponse)
}
处理重入请求
重入请求的关键是 Dapr-Reentrancy-Id 请求头。此请求头的值用于将请求与其调用链进行匹配,并允许它们绕过 Actor 的锁。
此请求头由 Dapr 运行时为任何指定了重入配置的 Actor 请求生成。一旦生成,它就会被用来锁定 Actor,并且必须传递给所有后续请求。下面是 Actor 处理重入请求的示例:
func reentrantCallHandler(w http.ResponseWriter, r *http.Request) {
/*
* 省略。
*/
req, _ := http.NewRequest("PUT", url, bytes.NewReader(nextBody))
reentrancyID := r.Header.Get("Dapr-Reentrancy-Id")
req.Header.Add("Dapr-Reentrancy-Id", reentrancyID)
client := http.Client{}
resp, err := client.Do(req)
/*
* 省略。
*/
}
演示
观看此视频,了解如何使用 Actor 重入。
后续步骤
Dapr SDK 中的 Actors相关链接
1.7 - 机密管理
1.7.1 - 密钥管理概览
应用程序通常使用专用的密钥存储来存储敏感信息。例如,你使用存储在密钥存储(如 AWS Secrets Manager、Azure Key Vault、Hashicorp Vault 等)中的连接字符串、密钥、令牌和其他应用级密钥来对数据库、服务和外部系统进行身份验证。
要访问这些密钥存储,应用程序需要导入密钥存储 SDK,这通常需要大量无关的样板代码。在多云场景中,这会带来更大的挑战,因为可能会使用不同供应商特定的密钥存储。
密钥管理 API
Dapr 专用的密钥构建块 API 使开发者更容易从密钥存储中消费应用密钥。要使用 Dapr 的密钥存储构建块,你需要:
- 为特定的密钥存储解决方案设置一个组件。
- 在应用代码中使用 Dapr 密钥 API 检索密钥。
- (可选)在 Dapr 组件文件中引用密钥。
以下概览视频和演示展示了 Dapr 密钥管理的工作原理。
功能特性
密钥管理 API 构建块为你的应用程序带来了多项功能。
无需更改应用代码即可配置密钥
你可以在应用代码中调用密钥 API 来检索和使用来自 Dapr 支持的密钥存储的密钥。观看此视频,了解如何在你的应用程序中使用密钥管理 API 的示例。
例如,下图显示了一个应用程序从名为"vault"的密钥存储中请求名为"mysecret"的密钥,该密钥存储来自已配置的云密钥存储。

应用程序还可以使用密钥 API 访问 Kubernetes 密钥存储中的密钥。默认情况下,Dapr 在 Kubernetes 模式下启用内置的 Kubernetes 密钥存储,通过以下方式部署:
- Helm 默认值,或
dapr init -k
如果你使用其他密钥存储,可以通过在 deployment.yaml 文件中添加注解 dapr.io/disable-builtin-k8s-secret-store: "true" 来禁用(不配置)Dapr Kubernetes 密钥存储。默认值为 false。
在下面的示例中,应用程序从 Kubernetes 密钥存储中检索相同的密钥"mysecret"。

在 Azure 中,你可以配置 Dapr 使用托管身份通过 Azure Key Vault 进行身份验证来检索密钥。在下面的示例中:
- 配置 Azure Kubernetes Service (AKS) 群集以使用托管身份。
- Dapr 使用 Pod 标识代表应用程序从 Azure Key Vault 检索密钥。

在上面的示例中,应用程序代码无需更改即可获取相同的密钥。Dapr 通过密钥管理构建块 API 使用密钥管理组件。
使用我们的快速入门或教程之一试用密钥 API。
在 Dapr 组件中引用密钥存储
在配置 Dapr 组件(如状态存储)时,通常需要在组件文件中包含凭证。或者,你可以将凭证放在 Dapr 支持的密钥存储中,并在 Dapr 组件中引用该密钥。这是首选方法,也是推荐的最佳实践,尤其是在生产环境中。
有关更多信息,请阅读在组件中引用密钥存储。
限制对密钥的访问
为了对密钥访问提供更精细的控制,Dapr 提供了定义作用域和限制访问权限的功能。了解有关使用密钥作用域的更多信息。
试用密钥管理
快速入门和教程
想要测试 Dapr 密钥管理 API?通过以下快速入门和教程了解 Dapr 密钥的实际应用:
| 快速入门/教程 | 描述 |
|---|---|
| 密钥管理快速入门 | 使用密钥管理 API 从已配置的密钥存储在应用代码中检索密钥。 |
| 密钥存储教程 | 演示如何使用 Dapr 密钥 API 访问密钥存储。 |
直接在你的应用中开始管理密钥
想跳过快速入门?没问题。你可以直接在你的应用程序中试用密钥管理构建块来检索和管理密钥。在安装 Dapr后,你可以从密钥操作指南开始使用密钥管理 API。
后续步骤
- 了解如何使用密钥作用域。
- 阅读密钥 API 参考文档。
1.7.2 - 操作指南:获取密钥
既然您已经了解了 Dapr 密钥构建块提供什么功能,了解它如何在您的服务中工作。本指南演示如何调用密钥 API 并从配置的密钥存储中检索应用代码中的密钥。

Note
如果您还没有尝试过,请试用密钥管理快速入门以快速了解如何使用密钥 API。设置密钥存储
在应用的代码中检索密钥之前,您必须配置一个密钥存储组件。本示例配置了一个使用本地 JSON 文件存储密钥的密钥存储。
Warning
在生产级应用中,不推荐使用本地密钥存储。请查找替代方案来安全地管理您的密钥。在项目目录中,创建一个名为 secrets.json 的文件,包含以下内容:
{
"secret": "Order Processing pass key"
}
创建一个名为 components 的新目录。导航到该目录并创建一个名为 local-secret-store.yaml 的组件文件,包含以下内容:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: localsecretstore
spec:
type: secretstores.local.file
version: v1
metadata:
- name: secretsFile
value: secrets.json #path to secrets file
- name: nestedSeparator
value: ":"
Warning
密钥存储 JSON 的路径是相对于您调用dapr run 的位置。更多信息:
- 了解如何配置不同类型的密钥存储。
- 查看支持的密钥存储以获取不同密钥存储解决方案的具体详情。
获取密钥
通过使用密钥 API 调用 Dapr 边车来获取密钥:
curl http://localhost:3601/v1.0/secrets/localsecretstore/secret
查看完整 API 参考。
从代码中调用密钥 API
现在您已经设置了本地密钥存储,调用 Dapr 从应用代码中获取密钥。以下是利用 Dapr SDK 获取密钥的代码示例。
using System;
using System.Threading.Tasks;
using Dapr.Client;
namespace EventService;
const string SECRET_STORE_NAME = "localsecretstore";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();
//Resolve a DaprClient from DI
var daprClient = app.Services.GetRequiredService<DaprClient>();
//Use the Dapr SDK to get a secret
var secret = await daprClient.GetSecretAsync(SECRET_STORE_NAME, "secret");
Console.WriteLine($"Result: {string.Join(", ", secret)}");
//dependencies
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
//code
@SpringBootApplication
public class OrderProcessingServiceApplication {
private static final Logger log = LoggerFactory.getLogger(OrderProcessingServiceApplication.class);
private static final ObjectMapper JSON_SERIALIZER = new ObjectMapper();
private static final String SECRET_STORE_NAME = "localsecretstore";
public static void main(String[] args) throws InterruptedException, JsonProcessingException {
DaprClient client = new DaprClientBuilder().build();
//Using Dapr SDK to get a secret
Map<String, String> secret = client.getSecret(SECRET_STORE_NAME, "secret").block();
log.info("Result: " + JSON_SERIALIZER.writeValueAsString(secret));
}
}
#dependencies
import random
from time import sleep
import requests
import logging
from dapr.clients import DaprClient
from dapr.clients.grpc._state import StateItem
from dapr.clients.grpc._request import TransactionalStateOperation, TransactionOperationType
#code
logging.basicConfig(level = logging.INFO)
DAPR_STORE_NAME = "localsecretstore"
key = 'secret'
with DaprClient() as client:
#Using Dapr SDK to get a secret
secret = client.get_secret(store_name=DAPR_STORE_NAME, key=key)
logging.info('Result: ')
logging.info(secret.secret)
#Using Dapr SDK to get bulk secrets
secret = client.get_bulk_secret(store_name=DAPR_STORE_NAME)
logging.info('Result for bulk secret: ')
logging.info(sorted(secret.secrets.items()))
//dependencies
import (
"context"
"log"
dapr "github.com/dapr/go-sdk/client"
)
//code
func main() {
client, err := dapr.NewClient()
SECRET_STORE_NAME := "localsecretstore"
if err != nil {
panic(err)
}
defer client.Close()
ctx := context.Background()
//Using Dapr SDK to get a secret
secret, err := client.GetSecret(ctx, SECRET_STORE_NAME, "secret", nil)
if secret != nil {
log.Println("Result : ")
log.Println(secret)
}
//Using Dapr SDK to get bulk secrets
secretBulk, err := client.GetBulkSecret(ctx, SECRET_STORE_NAME, nil)
if secret != nil {
log.Println("Result for bulk: ")
log.Println(secretBulk)
}
}
//dependencies
import { DaprClient, HttpMethod, CommunicationProtocolEnum } from '@dapr/dapr';
//code
const daprHost = "127.0.0.1";
async function main() {
const client = new DaprClient({
daprHost,
daprPort: process.env.DAPR_HTTP_PORT,
communicationProtocol: CommunicationProtocolEnum.HTTP,
});
const SECRET_STORE_NAME = "localsecretstore";
//Using Dapr SDK to get a secret
var secret = await client.secret.get(SECRET_STORE_NAME, "secret");
console.log("Result: " + secret);
//Using Dapr SDK to get bulk secrets
secret = await client.secret.getBulk(SECRET_STORE_NAME);
console.log("Result for bulk: " + secret);
}
main();
相关链接
- 查看 Dapr 密钥 API 功能。
- 了解如何使用密钥作用域。
- 阅读密钥 API 参考并查看支持的密钥。
- 了解如何设置不同的密钥存储组件以及如何在组件中引用密钥。
1.7.3 - 如何使用:密钥作用域
一旦你为应用配置了密钥存储,该存储中定义的任何密钥默认都可从 Dapr 应用访问。
你可以通过定义密钥作用域来限制 Dapr 应用对特定密钥的访问。只需向应用配置添加具有限制性权限的密钥作用域策略。
密钥作用域策略适用于任何密钥存储,包括:
- 本地密钥存储
- Kubernetes 密钥存储
- 公有云密钥存储
观看此视频了解如何将密钥作用域与应用结合使用的演示。
场景1:拒绝对某个密钥存储的所有密钥的访问
在此示例中,将拒绝在 Kubernetes 集群上运行的应用访问所有密钥,该集群已配置名为 mycustomsecretstore 的Kubernetes 密钥存储。除了用户定义的自定义存储外,该示例还配置 Kubernetes 默认存储(名为 kubernetes)以确保拒绝所有密钥的访问。了解更多关于 Kubernetes 默认密钥存储的信息。
定义以下 appconfig.yaml 配置并使用命令 kubectl apply -f appconfig.yaml 将其应用到 Kubernetes 集群。
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: appconfig
spec:
secrets:
scopes:
- storeName: kubernetes
defaultAccess: deny
- storeName: mycustomsecreststore
defaultAccess: deny
对于需要被拒绝访问 Kubernetes 密钥存储的应用,遵循这些说明,并向应用 Pod 添加以下注解:
dapr.io/config: appconfig
定义后,应用将无法再访问 Kubernetes 密钥存储中的任何密钥。
场景2:仅允许访问某个密钥存储中的特定密钥
此示例使用名为 vault 的密钥存储。这可以是为应用设置的 Hashicorp 密钥存储组件。要允许 Dapr 应用仅访问 vault 密钥存储中的 secret1 和 secret2,定义以下 appconfig.yaml:
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: appconfig
spec:
secrets:
scopes:
- storeName: vault
defaultAccess: deny
allowedSecrets: ["secret1", "secret2"]
对 vault 密钥存储的默认访问权限为 deny,而基于 allowedSecrets 列表,应用可以访问某些密钥。了解如何将配置应用到边车。
场景3:拒绝对某个密钥存储中某些敏感密钥的访问
定义以下 config.yaml:
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: appconfig
spec:
secrets:
scopes:
- storeName: vault
defaultAccess: allow # 此为默认值,可省略该行
deniedSecrets: ["secret1", "secret2"]
此示例配置明确拒绝访问名为 vault 的密钥存储中的 secret1 和 secret2,同时允许访问所有其他密钥。了解如何将配置应用到边车。
权限优先级
allowedSecrets 和 deniedSecrets 列表值优先于 defaultAccess 策略。
| 场景 | defaultAccess | allowedSecrets | deniedSecrets | 权限 |
|---|---|---|---|---|
| 1 - 仅默认访问 | deny/allow | 空 | 空 | deny/allow |
| 2 - 默认拒绝,包含允许列表 | deny | [“s1”] | 空 | 仅能访问 “s1” |
| 3 - 默认允许,包含拒绝列表 | allow | 空 | [“s1”] | 仅不能访问 “s1” |
| 4 - 默认允许,包含允许列表 | allow | [“s1”] | 空 | 仅能访问 “s1” |
| 5 - 默认拒绝,包含拒绝列表 | deny | 空 | [“s1”] | deny |
| 6 - 默认拒绝/允许,包含两个列表 | deny/allow | [“s1”] | [“s2”] | 仅能访问 “s1” |
相关链接
1.8 - Configuration
关于 Dapr Configuration 的更多信息
了解更多关于如何使用 Dapr Configuration 的信息:
- 尝试 Configuration 快速入门。
- 通过任意支持的 Dapr SDK 探索配置。
- 查看 Configuration API 参考文档。
- 浏览支持的 configuration component specs。
1.8.1 - Configuration overview
在编写应用程序时,消费应用程序配置是一项常见任务。配置存储通常用于管理这些配置数据。配置项通常是动态的,并且与消费它的应用程序的需求紧密耦合。
例如,应用程序配置可能包括:
- 密钥名称
- 不同的标识符
- 分区或消费者 ID
- 要连接的数据库名称等
通常,配置项以键/值对的形式存储在状态存储或数据库中。开发人员或运维人员可以在运行时更改配置存储中的应用程序配置。更改完成后,会通知服务加载新配置。
从应用程序 API 的角度来看,配置数据是只读的,配置存储的更新通过运维工具完成。使用 Dapr 的 Configuration API,您可以:
- 消费以只读键/值对形式返回的配置项
- 订阅配置项更改通知

Note
Configuration API 不应与 Dapr 边车和控制平面配置 混淆,后者用于在 Dapr 边车实例或已安装的 Dapr 控制平面上设置策略和配置。试用 configuration
快速入门
想测试 Dapr configuration API 吗?请参阅以下快速入门,了解配置 API 的实际应用:
| 快速入门 | 描述 |
|---|---|
| Configuration 快速入门 | 使用 configuration API 获取配置项或订阅配置更改。 |
在应用程序中直接开始使用 configuration API
想跳过快速入门吗?没问题。您可以在应用程序中直接试用 configuration 构建块来读取和管理配置数据。安装 Dapr 后,您可以开始使用 configuration API,从 configuration 操作指南 开始。
观看演示
后续步骤
按照这些指南操作:
1.8.2 - 操作指南:从存储中管理配置
此示例使用 Redis 配置存储组件来演示如何检索配置项。

注意
如果您还没有尝试过,可以先体验一下配置快速入门 ,快速了解如何使用配置 API。禁用配置初始化端点
如果您的应用程序不使用配置构建块,则可以在初始化期间禁用对/dapr/config 端点的自动 HTTP 调用,以减少日志噪音。在使用 dapr run 时使用 --disable-init-endpoints config 标志,或在 Kubernetes 中使用 dapr.io/disable-init-endpoints: "config" 注解。了解有关禁用初始化端点的更多信息。在存储中创建配置项
在支持的配置存储中创建一个配置项。这可以是一个简单的键值项,键可以由您自行选择。如前所述,此示例使用 Redis 配置存储组件。
使用 Docker 运行 Redis
docker run --name my-redis -p 6379:6379 -d redis:6
保存项
使用 Redis CLI 连接到 Redis 实例:
redis-cli -p 6379
保存一个配置项:
MSET orderId1 "101||1" orderId2 "102||1"
配置 Dapr 配置存储
将以下组件文件保存到您机器上的默认组件文件夹中。您可以使用此文件作为 Dapr 组件 YAML:
- 对于使用
kubectl的 Kubernetes 环境。 - 使用 Dapr CLI 运行时的场景。
注意
由于 Redis 配置组件的元数据与 Redisstatestore.yaml 组件相同,如果您已经有 Redis statestore.yaml,可以简单地复制/更改 Redis 状态存储组件类型。apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: configstore
spec:
type: configuration.redis
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: <REDIS_PASSWORD>
检索配置项
获取配置项
以下示例展示如何使用 Dapr Configuration API 获取已保存的配置项。
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Dapr.Client;
const string CONFIG_STORE_NAME = "configstore";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();
using var client = app.Services.GetRequiredServices<DaprClient>();
var configuration = await client.GetConfiguration(CONFIG_STORE_NAME, [ "orderId1", "orderId2" ]);
Console.WriteLine($"Got key=\n{configuration[0].Key} -> {configuration[0].Value}\n{configuration[1].Key} -> {configuration[1].Value}");
//dependencies
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.DaprClient;
import io.dapr.client.domain.ConfigurationItem;
import io.dapr.client.domain.GetConfigurationRequest;
import io.dapr.client.domain.SubscribeConfigurationRequest;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
//code
private static final String CONFIG_STORE_NAME = "configstore";
public static void main(String[] args) throws Exception {
try (DaprClient client = (new DaprClientBuilder()).build()) {
List<String> keys = new ArrayList<>();
keys.add("orderId1");
keys.add("orderId2");
GetConfigurationRequest req = new GetConfigurationRequest(CONFIG_STORE_NAME, keys);
try {
Mono<List<ConfigurationItem>> items = client.getConfiguration(req);
items.block().forEach(ConfigurationClient::print);
} catch (Exception ex) {
System.out.println(ex.getMessage());
}
}
}
#dependencies
from dapr.clients import DaprClient
#code
with DaprClient() as d:
CONFIG_STORE_NAME = 'configstore'
keys = ['orderId1', 'orderId2']
#Startup time for dapr
d.wait(20)
configuration = d.get_configuration(store_name=CONFIG_STORE_NAME, keys=[keys], config_metadata={})
print(f"Got key={configuration.items[0].key} value={configuration.items[0].value} version={configuration.items[0].version}")
package main
import (
"context"
"fmt"
dapr "github.com/dapr/go-sdk/client"
)
func main() {
ctx := context.Background()
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
items, err := client.GetConfigurationItems(ctx, "configstore", ["orderId1","orderId2"])
if err != nil {
panic(err)
}
for key, item := range items {
fmt.Printf("get config: key = %s value = %s version = %s",key,(*item).Value, (*item).Version)
}
}
import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";
// JS SDK does not support Configuration API over HTTP protocol yet
const protocol = CommunicationProtocolEnum.GRPC;
const host = process.env.DAPR_HOST ?? "localhost";
const port = process.env.DAPR_GRPC_PORT ?? 3500;
const DAPR_CONFIGURATION_STORE = "configstore";
const CONFIGURATION_ITEMS = ["orderId1", "orderId2"];
async function main() {
const client = new DaprClient(host, port, protocol);
// Get config items from the config store
try {
const config = await client.configuration.get(DAPR_CONFIGURATION_STORE, CONFIGURATION_ITEMS);
Object.keys(config.items).forEach((key) => {
console.log("Configuration for " + key + ":", JSON.stringify(config.items[key]));
});
} catch (error) {
console.log("Could not get config item, err:" + error);
process.exit(1);
}
}
main().catch((e) => console.error(e));
Launch a dapr sidecar:
dapr run --app-id orderprocessing --dapr-http-port 3601
在另一个终端中,获取之前保存的配置项:
curl http://localhost:3601/v1.0/configuration/configstore?key=orderId1
启动 Dapr 边车:
dapr run --app-id orderprocessing --dapr-http-port 3601
在另一个终端中,获取之前保存的配置项:
Invoke-RestMethod -Uri 'http://localhost:3601/v1.0/configuration/configstore?key=orderId1'
订阅配置项更新
以下是使用 SDK 订阅使用 configstore 存储组件的键 [orderId1, orderId2] 的代码示例。
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Dapr.Client;
using System.Text.Json;
const string DAPR_CONFIGURATION_STORE = "configstore";
var CONFIGURATION_ITEMS = new List<string> { "orderId1", "orderId2" };
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();
var client = app.Services.GetRequiredService<DaprClient>();
// Subscribe for configuration changes
var subscribe = await client.SubscribeConfiguration(DAPR_CONFIGURATION_STORE, CONFIGURATION_ITEMS);
// Print configuration changes
await foreach (var items in subscribe.Source)
{
// First invocation when app subscribes to config changes only returns subscription id
if (items.Keys.Count == 0)
{
Console.WriteLine("App subscribed to config changes with subscription id: " + subscribe.Id);
subscriptionId = subscribe.Id;
continue;
}
var cfg = JsonSerializer.Serialize(items);
Console.WriteLine("Configuration update " + cfg);
}
导航到包含上述代码的目录,然后运行以下命令启动 Dapr 边车和订阅者应用程序:
dapr run --app-id orderprocessing -- dotnet run
using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Dapr.Client;
using Dapr.Extensions.Configuration;
using System.Collections.Generic;
using System.Threading;
Console.WriteLine("Starting application.");
var builder = WebApplication.CreateBuilder(args);
// Unlike most other situations, we build a `DaprClient` here using its factory because we cannot rely on `IConfiguration`
// or other injected services to configure it because we haven't yet built the DI container.
var client = new DaprClientBuilder().Build();
// In a real-world application, you'd also add the following line to register the `DaprClient` with the DI container so
// it can be injected into other services. In this demonstration, it's not necessary as we're not injecting it anywhere.
// builder.Services.AddDaprClient();
// Get the initial value and continue to watch it for changes
builder.Configuration.AddDaprConfigurationStore("configstore", new List<string>() { "orderId1","orderId2" }, client, TimeSpan.FromSeconds(20));
builder.Configuration.AddStreamingDaprConfigurationStore("configstore", new List<string>() { "orderId1","orderId2" }, client, TimeSpan.FromSeconds(20));
await builder.Build().RunAsync();
Console.WriteLine("Closing application.");
导航到包含上述代码的目录,然后运行以下命令启动 Dapr 边车和订阅者应用程序:
dapr run --app-id orderprocessing -- dotnet run
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.DaprClient;
import io.dapr.client.domain.ConfigurationItem;
import io.dapr.client.domain.GetConfigurationRequest;
import io.dapr.client.domain.SubscribeConfigurationRequest;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
//code
private static final String CONFIG_STORE_NAME = "configstore";
private static String subscriptionId = null;
public static void main(String[] args) throws Exception {
try (DaprClient client = (new DaprClientBuilder()).build()) {
// Subscribe for config changes
List<String> keys = new ArrayList<>();
keys.add("orderId1");
keys.add("orderId2");
Flux<SubscribeConfigurationResponse> subscription = client.subscribeConfiguration(DAPR_CONFIGURATON_STORE,keys);
// Read config changes for 20 seconds
subscription.subscribe((response) -> {
// First ever response contains the subscription id
if (response.getItems() == null || response.getItems().isEmpty()) {
subscriptionId = response.getSubscriptionId();
System.out.println("App subscribed to config changes with subscription id: " + subscriptionId);
} else {
response.getItems().forEach((k, v) -> {
System.out.println("Configuration update for " + k + ": {'value':'" + v.getValue() + "'}");
});
}
});
Thread.sleep(20000);
}
}
导航到包含上述代码的目录,然后运行以下命令启动 Dapr 边车和订阅者应用程序:
dapr run --app-id orderprocessing -- -- mvn spring-boot:run
#dependencies
from dapr.clients import DaprClient
#code
def handler(id: str, resp: ConfigurationResponse):
for key in resp.items:
print(f"Subscribed item received key={key} value={resp.items[key].value} "
f"version={resp.items[key].version} "
f"metadata={resp.items[key].metadata}", flush=True)
def executeConfiguration():
with DaprClient() as d:
storeName = 'configurationstore'
keys = ['orderId1', 'orderId2']
id = d.subscribe_configuration(store_name=storeName, keys=keys,
handler=handler, config_metadata={})
print("Subscription ID is", id, flush=True)
sleep(20)
executeConfiguration()
导航到包含上述代码的目录,然后运行以下命令启动 Dapr 边车和订阅者应用程序:
dapr run --app-id orderprocessing -- python3 OrderProcessingService.py
package main
import (
"context"
"fmt"
"time"
dapr "github.com/dapr/go-sdk/client"
)
func main() {
ctx := context.Background()
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
subscribeID, err := client.SubscribeConfigurationItems(ctx, "configstore", []string{"orderId1", "orderId2"}, func(id string, items map[string]*dapr.ConfigurationItem) {
for k, v := range items {
fmt.Printf("get updated config key = %s, value = %s version = %s \n", k, v.Value, v.Version)
}
})
if err != nil {
panic(err)
}
time.Sleep(20*time.Second)
}
导航到包含上述代码的目录,然后运行以下命令启动 Dapr 边车和订阅者应用程序:
dapr run --app-id orderprocessing -- go run main.go
import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";
// JS SDK does not support Configuration API over HTTP protocol yet
const protocol = CommunicationProtocolEnum.GRPC;
const host = process.env.DAPR_HOST ?? "localhost";
const port = process.env.DAPR_GRPC_PORT ?? 3500;
const DAPR_CONFIGURATION_STORE = "configstore";
const CONFIGURATION_ITEMS = ["orderId1", "orderId2"];
async function main() {
const client = new DaprClient(host, port, protocol);
// Subscribe to config updates
try {
const stream = await client.configuration.subscribeWithKeys(
DAPR_CONFIGURATION_STORE,
CONFIGURATION_ITEMS,
(config) => {
console.log("Configuration update", JSON.stringify(config.items));
}
);
// Unsubscribe to config updates and exit app after 20 seconds
setTimeout(() => {
stream.stop();
console.log("App unsubscribed to config changes");
process.exit(0);
}, 20000);
} catch (error) {
console.log("Error subscribing to config updates, err:" + error);
process.exit(1);
}
}
main().catch((e) => console.error(e));
导航到包含上述代码的目录,然后运行以下命令启动 Dapr 边车和订阅者应用程序:
dapr run --app-id orderprocessing --app-protocol grpc --dapr-grpc-port 3500 -- node index.js
取消订阅配置项更新
订阅监视配置项后,您将收到所有订阅键的更新。要停止接收更新,您需要显式调用取消订阅 API。
以下是展示如何使用取消订阅 API 取消订阅配置更新的代码示例。
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Dapr.Client;
var builder = WebApplication.CreateBuilder();
builder.Services.AddDaprClient();
var app = builder.Build();
const string DAPR_CONFIGURATION_STORE = "configstore";
const string SubscriptionId = "abc123"; //Replace with the subscription identifier to unsubscribe from
var client = app.Services.GetRequiredService<DaprClient>();
await client.UnsubscribeConfiguration(DAPR_CONFIGURATION_STORE, SubscriptionId);
Console.WriteLine("App unsubscribed from config changes");
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.DaprClient;
import io.dapr.client.domain.ConfigurationItem;
import io.dapr.client.domain.GetConfigurationRequest;
import io.dapr.client.domain.SubscribeConfigurationRequest;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
//code
private static final String CONFIG_STORE_NAME = "configstore";
private static String subscriptionId = null;
public static void main(String[] args) throws Exception {
try (DaprClient client = (new DaprClientBuilder()).build()) {
// Unsubscribe from config changes
UnsubscribeConfigurationResponse unsubscribe = client
.unsubscribeConfiguration(subscriptionId, DAPR_CONFIGURATON_STORE).block();
if (unsubscribe.getIsUnsubscribed()) {
System.out.println("App unsubscribed to config changes");
} else {
System.out.println("Error unsubscribing to config updates, err:" + unsubscribe.getMessage());
}
} catch (Exception e) {
System.out.println("Error unsubscribing to config updates," + e.getMessage());
System.exit(1);
}
}
import asyncio
import time
import logging
from dapr.clients import DaprClient
subscriptionID = ""
with DaprClient() as d:
isSuccess = d.unsubscribe_configuration(store_name='configstore', id=subscriptionID)
print(f"Unsubscribed successfully? {isSuccess}", flush=True)
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"time"
dapr "github.com/dapr/go-sdk/client"
)
var DAPR_CONFIGURATION_STORE = "configstore"
var subscriptionID = ""
func main() {
client, err := dapr.NewClient()
if err != nil {
log.Panic(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := client.UnsubscribeConfigurationItems(ctx, DAPR_CONFIGURATION_STORE , subscriptionID); err != nil {
panic(err)
}
}
import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";
// JS SDK does not support Configuration API over HTTP protocol yet
const protocol = CommunicationProtocolEnum.GRPC;
const host = process.env.DAPR_HOST ?? "localhost";
const port = process.env.DAPR_GRPC_PORT ?? 3500;
const DAPR_CONFIGURATION_STORE = "configstore";
const CONFIGURATION_ITEMS = ["orderId1", "orderId2"];
async function main() {
const client = new DaprClient(host, port, protocol);
try {
const stream = await client.configuration.subscribeWithKeys(
DAPR_CONFIGURATION_STORE,
CONFIGURATION_ITEMS,
(config) => {
console.log("Configuration update", JSON.stringify(config.items));
}
);
setTimeout(() => {
// Unsubscribe to config updates
stream.stop();
console.log("App unsubscribed to config changes");
process.exit(0);
}, 20000);
} catch (error) {
console.log("Error subscribing to config updates, err:" + error);
process.exit(1);
}
}
main().catch((e) => console.error(e));
curl 'http://localhost:<DAPR_HTTP_PORT>/v1.0/configuration/configstore/<subscription-id>/unsubscribe'
Invoke-RestMethod -Uri 'http://localhost:<DAPR_HTTP_PORT>/v1.0/configuration/configstore/<subscription-id>/unsubscribe'
下一步
1.9 - 分布式锁
1.9.1 - 分布式锁概述
简介
锁用于提供对资源的互斥访问。例如,您可以使用锁来:
- 提供对数据库行、表或整个数据库的独占访问
- 以顺序方式锁定从队列读取消息
任何发生更新的共享资源都可以成为锁的目标。锁通常用于改变状态的操作,而不是读取操作。
每个锁都有一个名称。应用程序决定命名锁访问的资源。通常,同一应用程序的多个实例使用此命名锁来独占访问资源并执行更新。
例如,在竞争消费者模式中,应用程序的多个实例访问一个队列。您可以决定在应用程序运行其业务逻辑时锁定队列。
在下图中,同一应用程序 App1 的两个实例使用 Redis 锁组件 对共享资源进行加锁。
- 第一个应用实例获取命名锁并获得独占访问权限。
- 第二个应用实例无法获取锁,因此不允许访问资源,直到锁被释放,要么:
- 通过 unlock API 由应用程序显式释放,或
- 由于租约超时,在一段时间后自动释放。

*此 API 目前处于 Alpha 状态。
功能
对资源的互斥访问
在任何给定时刻,只有一个应用程序实例可以持有命名锁。锁的作用域限定为 Dapr app-id。
使用租约避免死锁
Dapr 分布式锁使用基于租约的锁定机制。如果应用程序获取锁后遇到异常而无法释放锁,则锁会在一段时间后通过租约自动释放。这可以防止应用程序失败时的资源死锁。
演示
后续步骤
遵循以下指南:
1.9.2 - 操作指南:使用锁
现在你已经了解了 Dapr 分布式锁 API 构建块所提供的功能,接下来学习它如何在你的服务中工作。在本指南中,一个示例应用程序使用 Redis 锁组件获取锁,以演示如何锁定资源。有关支持的锁存储列表,请参阅此参考页面。
在下图中,同一应用程序的两个实例尝试获取锁,其中一个实例成功,另一个被拒绝。

下图显示同一应用程序的两个实例,其中一个实例释放锁后,另一个实例能够获取该锁。

下图显示两个不同应用程序的实例,在同一资源上获取不同的锁。

配置锁组件
将以下组件文件保存到你机器上的默认组件文件夹中。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: lockstore
spec:
type: lock.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: <PASSWORD>
获取锁
curl -X POST http://localhost:3500/v1.0-alpha1/lock/lockstore
-H 'Content-Type: application/json'
-d '{"resourceId":"my_file_name", "lockOwner":"random_id_abc123", "expiryInSeconds": 60}'
using System;
using Dapr.Client;
namespace LockService
{
class Program
{
[Obsolete("Distributed Lock API is in Alpha, this can be removed once it is stable.")]
static async Task Main(string[] args)
{
string DAPR_LOCK_NAME = "lockstore";
string fileName = "my_file_name";
var client = new DaprClientBuilder().Build();
await using (var fileLock = await client.Lock(DAPR_LOCK_NAME, fileName, "random_id_abc123", 60))
{
if (fileLock.Success)
{
Console.WriteLine("Success");
}
else
{
Console.WriteLine($"Failed to lock {fileName}.");
}
}
}
}
}
package main
import (
"fmt"
dapr "github.com/dapr/go-sdk/client"
)
func main() {
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
defer client.Close()
resp, err := client.TryLockAlpha1(ctx, "lockstore", &dapr.LockRequest{
LockOwner: "random_id_abc123",
ResourceID: "my_file_name",
ExpiryInSeconds: 60,
})
fmt.Println(resp.Success)
}
解锁现有锁
curl -X POST http://localhost:3500/v1.0-alpha1/unlock/lockstore
-H 'Content-Type: application/json'
-d '{"resourceId":"my_file_name", "lockOwner":"random_id_abc123"}'
using System;
using Dapr.Client;
namespace LockService
{
class Program
{
static async Task Main(string[] args)
{
string DAPR_LOCK_NAME = "lockstore";
var client = new DaprClientBuilder().Build();
var response = await client.Unlock(DAPR_LOCK_NAME, "my_file_name", "random_id_abc123"));
Console.WriteLine(response.status);
}
}
}
package main
import (
"fmt"
dapr "github.com/dapr/go-sdk/client"
)
func main() {
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
defer client.Close()
resp, err := client.UnlockAlpha1(ctx, "lockstore", &UnlockRequest{
LockOwner: "random_id_abc123",
ResourceID: "my_file_name",
})
fmt.Println(resp.Status)
}
下一步
阅读分布式锁 API 概述了解更多内容。
1.10 - 密码学
1.10.1 - Cryptography 概述
通过 cryptography 构建块,您可以以安全且一致的方式使用加密功能。Dapr 公开的 API 允许您在密钥保管库或 Dapr 边车中执行加密和解密等操作,而不会将加密密钥暴露给您的应用程序。
为什么需要 Cryptography?
应用程序广泛使用加密技术,如果实施正确,即使数据泄露,也能使解决方案更加安全。在某些情况下,您可能需要使用加密来满足行业法规(例如金融领域)或法律要求(包括 GDPR 等隐私法规)。
然而,正确使用加密技术可能很困难。您需要:
- 选择正确的算法和选项
- 学习正确管理和保护密钥的方法
- 在希望限制对加密密钥材料的访问时处理操作复杂性
安全的一个重要要求是限制对加密密钥的访问,这通常被称为"原始密钥材料"。Dapr 可以与密钥保管库(如 Azure Key Vault,未来的版本将支持更多组件)集成,这些保管库在安全飞地中存储密钥并在保管库内执行加密操作,而不会将密钥暴露给您的应用程序或 Dapr。
或者,您可以配置 Dapr 为您管理加密密钥,在边车内执行操作,同样不会将原始密钥材料暴露给您的应用程序。
Dapr 中的 Cryptography
使用 Dapr,您可以执行加密操作而不会将加密密钥暴露给您的应用程序。

通过使用 cryptography 构建块,您可以:
- 以更安全的方式更轻松地执行加密操作。Dapr 提供了防止使用不安全算法或使用不安全选项的防护措施。
- 将密钥保存在应用程序之外。应用程序永远不会看到"原始密钥材料",但可以请求保管库使用密钥执行操作。当使用 Dapr 的加密引擎时,操作在 Dapr 边车内安全执行。
- 实现更好的关注点分离。通过使用外部保管库或加密组件,只有授权团队可以访问私钥材料。
- 更轻松地管理和轮换密钥。密钥在保管库中管理,不在应用程序中,轮换时无需开发人员参与(甚至无需重启应用程序)。
- 支持更好的审计日志,以监控何时使用保管库中的密钥执行了操作。
Note
虽然 HTTP 和 gRPC 在 alpha 版本中都受支持,但使用 gRPC API 与支持的 Dapr SDK 是使用 cryptography 的推荐方法。功能特性
Cryptography 组件
Dapr cryptography 构建块包含两类组件:
允许与管理服务或保管库(“密钥保管库”)交互的组件。
类似于 Dapr 如何在各种密钥存储或状态存储之上提供"抽象层",这些组件允许与各种密钥保管库(如 Azure Key Vault,未来 Dapr 版本将支持更多)交互。使用这些组件时,对私钥的加密操作在保管库内执行,Dapr 永远不会看到您的私钥。基于 Dapr 自身加密引擎的组件。
当密钥保管库不可用时,您可以利用基于 Dapr 自身加密引擎的组件。这些名称中包含.dapr.的组件在 Dapr 边车内执行加密操作,密钥存储在文件、Kubernetes Secret 或其他来源中。虽然 Dapr 知道私钥,但您的应用程序仍然无法访问它们。
两类组件(无论是利用密钥保管库还是使用 Dapr 中的加密引擎)都提供相同的抽象层。这允许您的解决方案根据需要在各种保管库和/或加密组件之间切换。例如,您可以在开发期间使用本地存储的密钥,在生产环境中使用云保管库。
Cryptography API
Cryptography API 允许使用 Dapr Crypto Scheme v1 加密和解密数据。这是一种固执己见的加密方案,旨在使用现代、安全的加密标准,并将数据(包括大文件)作为流高效处理。
体验 Cryptography
快速入门和教程
想测试 Dapr Cryptography API?请参阅以下快速入门和教程,了解 cryptography 的实际应用:
| 快速入门/教程 | 描述 |
|---|---|
| Cryptography 快速入门 | 使用 RSA 和 AES 密钥通过 cryptography API 加密和解密消息和大文件。 |
直接在您的应用程序中开始使用 Cryptography
想跳过快速入门?没问题。您可以直接在应用程序中试用 cryptography 构建块来加密和解密您的应用程序。安装 Dapr 后,您可以开始使用 cryptography API,从 cryptography 操作指南 开始。
演示
观看 Dapr Community Call #83 中 Cryptography API 的演示视频:
下一步
使用 Cryptography API >>相关链接
1.10.2 - 操作指南:使用 Cryptography API
现在您已经阅读了 Cryptography 作为 Dapr 构建块 的相关内容,让我们通过 SDK 来演练如何使用 cryptography API。
注意
Dapr cryptography 目前处于 Alpha 阶段。加密
在您的项目中使用 Dapr SDK(通过 gRPC API),您可以加密数据流,例如文件或字符串:
# 当传递数据(缓冲区或字符串)时,`encrypt` 返回包含加密消息的 Buffer
def encrypt_decrypt_string(dapr: DaprClient):
message = 'The secret is "passw0rd"'
# 加密消息
resp = dapr.encrypt(
data=message.encode(),
options=EncryptOptions(
# Cryptography 组件的名称(必需)
component_name=CRYPTO_COMPONENT_NAME,
# 存储在 cryptography 组件中的密钥(必需)
key_name=RSA_KEY_NAME,
# 用于包装密钥的算法,必须由上述命名的密钥支持。
# 选项包括:"RSA"、"AES"
key_wrap_algorithm='RSA',
),
)
# 该方法返回一个可读流,我们在内存中完整读取它
encrypt_bytes = resp.read()
print(f'Encrypted the message, got {len(encrypt_bytes)} bytes')
在您的项目中使用 Dapr SDK(通过 gRPC API),您可以加密缓冲区或字符串中的数据:
// 当传递数据(缓冲区或字符串)时,`encrypt` 返回包含加密消息的 Buffer
const ciphertext = await client.crypto.encrypt(plaintext, {
// Dapr 组件的名称(必需)
componentName: "mycryptocomponent",
// 存储在组件中的密钥名称(必需)
keyName: "mykey",
// 用于包装密钥的算法,必须由上述命名的密钥支持。
// 选项包括:"RSA"、"AES"
keyWrapAlgorithm: "RSA",
});
这些 API 也可以与流一起使用,以便在数据来自流时更高效地加密数据。下面的示例使用流加密文件,并将其写入另一个文件:
// `encrypt` 可以用作 Duplex 流
await pipeline(
fs.createReadStream("plaintext.txt"),
await client.crypto.encrypt({
// Dapr 组件的名称(必需)
componentName: "mycryptocomponent",
// 存储在组件中的密钥名称(必需)
keyName: "mykey",
// 用于包装密钥的算法,必须由上述命名的密钥支持。
// 选项包括:"RSA"、"AES"
keyWrapAlgorithm: "RSA",
}),
fs.createWriteStream("ciphertext.out"),
);
在您的项目中使用 Dapr SDK(通过 gRPC API),您可以加密字符串或字节数组中的数据:
using var client = new DaprClientBuilder().Build();
const string componentName = "azurekeyvault"; // 更改此项以匹配您的 cryptography 组件
const string keyName = "myKey"; // 更改此项以匹配您的加密存储中密钥的名称
const string plainText = "This is the value we're going to encrypt today";
// 将字符串编码为 UTF-8 字节数组并加密
var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
var encryptedBytesResult = await client.EncryptAsync(componentName, plaintextBytes, keyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa));
在您的项目中使用 Dapr SDK,您可以加密数据流,例如文件。
out, err := sdkClient.Encrypt(context.Background(), rf, dapr.EncryptOptions{
// Dapr 组件的名称(必需)
ComponentName: "mycryptocomponent",
// 存储在组件中的密钥名称(必需)
KeyName: "mykey",
// 用于包装密钥的算法,必须由上述命名的密钥支持。
// 选项包括:"RSA"、"AES"
Algorithm: "RSA",
})
下面的示例将 Encrypt API 放在上下文中,代码读取文件、加密文件,然后将结果存储在另一个文件中。
// 输入文件,明文
rf, err := os.Open("input")
if err != nil {
panic(err)
}
defer rf.Close()
// 输出文件,已加密
wf, err := os.Create("output.enc")
if err != nil {
panic(err)
}
defer wf.Close()
// 使用 Dapr 加密数据
out, err := sdkClient.Encrypt(context.Background(), rf, dapr.EncryptOptions{
// 这是 3 个必需参数
ComponentName: "mycryptocomponent",
KeyName: "mykey",
Algorithm: "RSA",
})
if err != nil {
panic(err)
}
// 读取流并将其复制到输出文件
n, err := io.Copy(wf, out)
if err != nil {
panic(err)
}
fmt.Println("Written", n, "bytes")
下面的示例使用 Encrypt API 来加密字符串。
// 输入字符串
rf := strings.NewReader("Amor, ch'a nullo amato amar perdona, mi prese del costui piacer sì forte, che, come vedi, ancor non m'abbandona")
// 使用 Dapr 加密数据
enc, err := sdkClient.Encrypt(context.Background(), rf, dapr.EncryptOptions{
ComponentName: "mycryptocomponent",
KeyName: "mykey",
Algorithm: "RSA",
})
if err != nil {
panic(err)
}
// 将加密数据读入字节切片
enc, err := io.ReadAll(enc)
if err != nil {
panic(err)
}
解密
要解密数据流,请使用 decrypt。
def encrypt_decrypt_string(dapr: DaprClient):
message = 'The secret is "passw0rd"'
# ...
# 解密加密的数据
resp = dapr.decrypt(
data=encrypt_bytes,
options=DecryptOptions(
# Cryptography 组件的名称(必需)
component_name=CRYPTO_COMPONENT_NAME,
# 存储在 cryptography 组件中的密钥(必需)
key_name=RSA_KEY_NAME,
),
)
# 该方法返回一个可读流,我们在内存中完整读取它
decrypt_bytes = resp.read()
print(f'Decrypted the message, got {len(decrypt_bytes)} bytes')
print(decrypt_bytes.decode())
assert message == decrypt_bytes.decode()
使用 Dapr SDK,您可以解密缓冲区中的数据或使用流。
// 当将数据作为缓冲区传递时,`decrypt` 返回包含解密消息的 Buffer
const plaintext = await client.crypto.decrypt(ciphertext, {
// 唯一必需的选项是组件名称
componentName: "mycryptocomponent",
});
// `decrypt` 也可以用作 Duplex 流
await pipeline(
fs.createReadStream("ciphertext.out"),
await client.crypto.decrypt({
// 唯一必需的选项是组件名称
componentName: "mycryptocomponent",
}),
fs.createWriteStream("plaintext.out"),
);
要解密字符串,请在您的项目中使用 ‘DecryptAsync’ gRPC API。
在下面的示例中,我们将获取一个字节数组(例如来自上面的示例)并将其解密为 UTF-8 编码的字符串。
public async Task<string> DecryptBytesAsync(byte[] encryptedBytes)
{
using var client = new DaprClientBuilder().Build();
const string componentName = "azurekeyvault"; // 更改此项以匹配您的 cryptography 组件
const string keyName = "myKey"; // 更改此项以匹配您的加密存储中密钥的名称
var decryptedBytes = await client.DecryptAsync(componentName, encryptedBytes, keyName);
var decryptedString = Encoding.UTF8.GetString(decryptedBytes.ToArray());
return decryptedString;
}
要解密文件,请在您的项目中使用 Decrypt gRPC API。
在下面的示例中,out 是一个可以写入文件或在内存中读取的流,如上面的示例所示。
out, err := sdkClient.Decrypt(context.Background(), rf, dapr.EncryptOptions{
// 唯一必需的选项是组件名称
ComponentName: "mycryptocomponent",
})
后续步骤
1.11 - Jobs
1.11.1 - Jobs 概述
许多应用程序需要作业调度,或者需要在将来执行某个操作。Jobs API 是一个编排器,用于调度这些未来的作业,可以在特定时间或特定间隔执行。
Jobs API 不仅帮助你调度作业,而且在内部,Dapr 使用 Scheduler 服务来调度 actor reminders。
Dapr 中的 Jobs 由以下部分组成:

工作原理
Jobs API 是一个作业调度器,而不是运行作业的执行器。该设计保证至少一次作业执行,优先考虑持久化和水平扩展而非精确性。这意味着:
- 保证: 作业永远不会在计划时间之前被调用。
- 不保证: 在到期时间之后调用作业的时间上限。
所有计划作业的作业详细信息和用户关联数据都存储在 Scheduler 服务中的嵌入式 Etcd 数据库中。
你可以使用 jobs 来:
场景
作业调度在以下场景中可能很有用:
自动化数据库备份: 确保数据库每天备份以防止数据丢失。安排备份脚本每天凌晨 2 点运行,这将创建数据库备份并将其存储在安全位置。
定期数据处理和 ETL(提取、转换、加载): 处理和转换来自各种来源的原始数据并将其加载到数据仓库中。安排 ETL 作业在特定时间运行(例如:每小时、每天)以获取新数据、处理它,并用最新信息更新数据仓库。
电子邮件通知和报告: 通过电子邮件接收每日销售报告和每周绩效摘要。安排一个作业生成所需的报告,并通过电子邮件发送,每天报告在每天早上 6 点发送,每周摘要在每周一早上 8 点发送。
维护任务和系统更新: 执行定期维护任务,如清除临时文件、更新软件和检查系统运行状况。安排各种维护脚本在非高峰时段运行,例如周末或深夜,以最大程度减少对用户的干扰。
金融交易的批处理: 处理大量需要批处理并在每个工作日结束时进行结算的交易。安排批处理作业在每个工作日下午 5 点运行,汇总当天的交易并执行必要的结算和对账。
Dapr 的 jobs API 确保这些场景中代表的任务始终如一且可靠地执行,无需手动干预,从而提高效率并降低错误风险。
功能
Jobs API 的主要功能允许你创建、检索和删除计划作业。默认情况下,当你创建一个名称已存在的作业时,操作会失败,除非你显式地将 overwrite 标志设置为 true。这确保现有作业不会被意外修改或覆盖。
跨多个副本调度作业
当你创建一个作业时,它不会替换同名的现有作业,除非你显式设置 overwrite 标志。这意味着每次创建作业时,它都会重置计数,并且只在该作业的嵌入式 etcd 中保留 1 条记录。因此,你不必担心创建和触发多个作业——只有最新的作业被记录和执行,即使你的所有应用程序在启动时都调度相同的作业。
Scheduler 服务使作业的调度能够跨多个副本扩展,同时保证作业仅由 1 个 Scheduler 服务实例触发。
试用 jobs API
你可以在你的应用程序中试用 jobs API。在安装 Dapr后,你就可以开始使用 jobs API,从操作指南:调度作业指南开始。
后续步骤
1.11.2 - 特性与概念
现在你已经大致了解了 jobs 构建块 ,让我们深入了解 Dapr Jobs 和各种 SDK 包含的特性和概念。Dapr Jobs:
- 提供了一个强大且可扩展的 API,用于调度未来要触发的操作。
- 暴露了在所有支持的语言中都通用的多项功能。
- 使用持续时间值时支持亚秒级精度(例如
500ms)。实际触发精度可能因运行时而异;基于 Cron 的调度仅支持秒级精度。
作业标识
所有作业都使用区分大小写的作业名称进行注册。这些名称在整个与 Dapr 运行时交互的服务中应该是唯一的。名称用作创建和修改作业时的标识符,也用于指示触发的调用与哪个作业相关联。
任意时刻一个名称只能关联一个作业。默认情况下,尝试创建与现有作业同名的作业会报错。然而,如果 overwrite 标志设置为 true,则新作业会覆盖同名作业。
调度作业
可以通过以下方式调度作业:
- 使用 Cron 表达式、持续时间值或周期表达式的间隔
- 特定的日期和时间
对于所有基于时间的调度,如果通过 RFC3339 规范提供了带时区的时间戳,则使用该时区。如果未提供,则使用运行 Dapr 的服务器所在的时区。换句话说,除非在调度作业时另有指定,否则不要假设时间以 UTC 时区运行。
使用 Cron 表达式调度
使用 Cron 表达式调度作业按特定间隔执行时,表达式使用 6 个字段,字段值范围见下表:
| seconds | minutes | hours | day of month | month | day of week |
|---|---|---|---|---|---|
| 0-59 | 0-59 | 0-23 | 1-31 | 1-12/jan-dec | 0-6/sun-sat |
示例 1
"0 30 * * * *" 每小时的半点触发。
示例 2
"0 15 3 * * *" 每天 03:15 触发。
使用持续时间值调度
可以使用 Go 持续时间字符串 调度作业,其中字符串由一个(可能有符号的)十进制数字序列组成,每个数字可带有可选的小数部分和单位后缀。有效的时间单位为 "ns"、"us"、"ms"、"s"、"m" 或 "h"。
示例 1
"2h45m" 每 2 小时 45 分钟触发一次。
示例 2
"37m25s" 每 37 分钟 25 秒触发一次。
使用周期表达式调度
支持以下周期表达式。"@every" 表达式也接受 Go 持续时间字符串。
| 表达式 | 描述 | 等效 Cron 表达式 |
|---|---|---|
| @every | 每隔指定时间运行(例如 “@every 1h30m”) | 不适用 |
| @yearly(或 @annually) | 每年一次,1 月 1 日午夜 | 0 0 0 1 1 * |
| @monthly | 每月一次,月初午夜 | 0 0 0 1 * * |
| @weekly | 每周一次,周日午夜 | 0 0 0 * * 0 |
| @daily 或 @midnight | 每天午夜运行一次 | 0 0 0 * * * |
| @hourly | 每小时整点运行一次 | 0 0 * * * * |
使用特定日期/时间调度
也可以通过使用 RFC3339 规范 提供日期,将作业调度到特定时间点执行。
示例 1
"2025-12-09T16:09:53+00:00" 表示作业应在 2025 年 12 月 9 日 UTC 时间下午 4:09:53 执行。
定时触发器
当预定的 Dapr 作业被触发时,运行时根据服务启动时向 Dapr 注册的方式,通过 HTTP 或 gRPC 向调度该作业的服务发送消息。
gRPC
当作业到达预定的触发时间时,触发的作业通过以下回调函数发送回应用程序:
注意: 以下示例使用 Go 语言,但适用于任何支持 gRPC 的编程语言。
import rtv1 "github.com/dapr/dapr/pkg/proto/runtime/v1"
...
func (s *JobService) OnJobEventAlpha1(ctx context.Context, in *rtv1.JobEventRequest) (*rtv1.JobEventResponse, error) {
// Handle the triggered job
}
此函数在 gRPC 服务器的上下文中处理触发的作业。设置服务器时,请确保注册回调服务器,该服务器在作业被触发时调用此函数:
...
js := &JobService{}
rtv1.RegisterAppCallbackAlphaServer(server, js)
在此设置下,你可以完全控制如何接收和处理触发的作业,因为它们通过此 gRPC 方法直接路由。
HTTP
如果应用程序启动时未向 Dapr 注册 gRPC 服务器,则 Dapr 会通过向端点 /job/<job-name> 发送 POST 请求来触发作业。请求体包含以下作业信息:
Schedule:作业触发的时间RepeatCount:可选值,指示作业应重复的次数DueTime:可选的时间点,表示作业应执行的单次时间(如果非重复),或者调度生效的最早开始时间Ttl:可选值,指示作业何时过期Payload:包含作业调度时原始存储数据的字节集合Overwrite:标志,允许请求的作业覆盖已存在的同名作业FailurePolicy:作业的可选失败策略
DueTime 和 Ttl 字段将反映 RFC3339 时间戳值,反映作业最初调度时提供的时区。如果未提供时区,这些值表示运行 Dapr 的服务器使用的时区。
管理作业
虽然作业通过 API 调用创建,但你可以通过 dapr scheduler CLI 命令管理(列出、检查、删除、备份和恢复)作业。
列出作业
dapr scheduler list --filter app
NAME BEGIN COUNT LAST TRIGGER
app/my-app/my-job -3.89s 1 2025-10-03T16:58:55Z
app/my-app/another-job -3.89s 1 2025-10-03T16:58:55Z
dapr scheduler list -o wide
NAMESPACE NAME BEGIN EXPIRATION SCHEDULE DUE TIME TTL REPEATS COUNT LAST TRIGGER
default app/my-app/my-job 2025-10-03T16:58:55Z @every 5s 2025-10-03T17:58:55+01:00 100 1 2025-10-03T16:58:55Z
dapr scheduler get app/my-app/my-job -o yaml
删除作业
删除指定作业:
dapr scheduler delete app/my-app/my-job
删除应用的所有作业:
dapr scheduler delete-all app/my-app
备份和恢复作业
导出所有作业:
dapr scheduler export -o jobs-backup.bin
之后导入:
dapr scheduler import -f jobs-backup.bin
摘要
- 使用 Jobs API 从应用程序创建或更新作业。
- 使用 dapr scheduler CLI 查看、检查、备份或删除作业。
- 作业存储在 Dapr Scheduler 中,确保在重启和部署时的可靠性。
1.11.3 - 操作指南:调度和处理触发的作业
既然你已经了解了作业构建块提供了什么,让我们来看一个如何使用该 API 的示例。下面的代码示例描述了一个为数据库备份应用程序调度作业并在触发时间处理它们的应用程序,触发时间也称为作业因其到达 dueTime 而被发送回应用程序的时间。
启动 Scheduler 服务
当你在自托管模式或 Kubernetes 上运行 dapr init时,Dapr Scheduler 服务会自动启动。
设置 Jobs API
在你的代码中,设置和调度应用程序内的作业。
下面的 .NET SDK 代码示例调度名为 prod-db-backup 的作业。作业数据包含有关你将定期备份的数据库的信息。在本示例的整个过程中,你将:
- 定义本示例其余部分中使用的类型
- 在应用程序启动期间注册一个端点,用于处理服务上所有作业触发调用
- 向 Dapr 注册作业
在下面的示例中,你将创建记录,这些记录将与作业一起序列化和注册,以便在将来作业被触发时信息可用:
- 备份任务的名称(
db-backup) - 备份任务的
Metadata,包括:- 数据库名称(
DBName) - 数据库位置(
BackupLocation)
- 数据库名称(
创建一个 ASP.NET Core 项目并从 NuGet 添加最新版本的 Dapr.Jobs。
注意: 虽然你的项目不必严格使用
Microsoft.NET.Sdk.WebSDK 来创建作业,但在编写本文档时,只有调度作业的服务才会接收其触发调用。由于这些调用期望有一个可以处理作业触发的端点,并且需要Microsoft.NET.Sdk.WebSDK,因此建议你为此目的使用 ASP.NET Core 项目。
首先定义类型以持久化我们的备份作业数据,并对属性应用我们自己的 JSON 属性名称属性,使其与其他语言示例保持一致。
//Define the types that we'll represent the job data with
internal sealed record BackupJobData([property: JsonPropertyName("task")] string Task, [property: JsonPropertyName("metadata")] BackupMetadata Metadata);
internal sealed record BackupMetadata([property: JsonPropertyName("DBName")]string DatabaseName, [property: JsonPropertyName("BackupLocation")] string BackupLocation);
接下来,作为应用程序设置的一部分,设置一个处理程序,该处理程序将在任何时候应用程序上触发作业时被调用。该处理程序负责根据提供的作业名称确定应如何处理作业。
这是通过在 /job/<job-name> 处向 ASP.NET Core 注册一个处理程序来实现的,其中 <job-name> 是参数化的,并传递给此处理程序委托,这满足了 Dapr 期望有一个端点来处理触发的命名作业的要求。
使用以下内容填充你的 Program.cs 文件:
using System.Text;
using System.Text.Json;
using Dapr.Jobs;
using Dapr.Jobs.Extensions;
using Dapr.Jobs.Models;
using Dapr.Jobs.Models.Responses;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprJobsClient();
var app = builder.Build();
//Registers an endpoint to receive and process triggered jobs
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5));
app.MapDaprScheduledJobHandler((string jobName, ReadOnlyMemory<byte> jobPayload, ILogger logger, CancellationToken cancellationToken) => {
logger?.LogInformation("Received trigger invocation for job '{jobName}'", jobName);
switch (jobName)
{
case "prod-db-backup":
// Deserialize the job payload metadata
var jobData = JsonSerializer.Deserialize<BackupJobData>(jobPayload);
// Process the backup operation - we assume this is implemented elsewhere in your code
await BackupDatabaseAsync(jobData, cancellationToken);
break;
}
}, cancellationTokenSource.Token);
await app.RunAsync();
最后,作业本身需要向 Dapr 注册,以便它可以在以后的时间点被触发。你可以通过将 DaprJobsClient 注入到类中并作为应用程序的入站操作的一部分来执行此操作,但为了本示例的目的,它将放在你上面开始使用的 Program.cs 文件的底部。因为你将使用通过依赖注入注册的 DaprJobsClient,所以首先创建一个作用域以便你可以访问它。
//Create a scope so we can access the registered DaprJobsClient
await using scope = app.Services.CreateAsyncScope();
var daprJobsClient = scope.ServiceProvider.GetRequiredService<DaprJobsClient>();
//Create the payload we wish to present alongside our future job triggers
var jobData = new BackupJobData("db-backup", new BackupMetadata("my-prod-db", "/backup-dir"));
//Serialize our payload to UTF-8 bytes
var serializedJobData = JsonSerializer.SerializeToUtf8Bytes(jobData);
//Schedule our backup job to run every minute, but only repeat 10 times
await daprJobsClient.ScheduleJobAsync("prod-db-backup", DaprJobSchedule.FromDuration(TimeSpan.FromMinutes(1)),
serializedJobData, repeats: 10);
下面的 Go SDK 代码示例调度名为 prod-db-backup 的作业。作业数据位于备份数据库("my-prod-db")中,并使用 ScheduleJobAlpha1 进行调度。这提供了 jobData,包括:
- 备份
Task名称 - 备份任务的
Metadata,包括:- 数据库名称(
DBName) - 数据库位置(
BackupLocation)
- 数据库名称(
package main
import (
//...
daprc "github.com/dapr/go-sdk/client"
"github.com/dapr/go-sdk/examples/dist-scheduler/api"
"github.com/dapr/go-sdk/service/common"
daprs "github.com/dapr/go-sdk/service/grpc"
)
func main() {
// Initialize the server
server, err := daprs.NewService(":50070")
// ...
if err = server.AddJobEventHandler("prod-db-backup", prodDBBackupHandler); err != nil {
log.Fatalf("failed to register job event handler: %v", err)
}
log.Println("starting server")
go func() {
if err = server.Start(); err != nil {
log.Fatalf("failed to start server: %v", err)
}
}()
// ...
// Set up backup location
jobData, err := json.Marshal(&api.DBBackup{
Task: "db-backup",
Metadata: api.Metadata{
DBName: "my-prod-db",
BackupLocation: "/backup-dir",
},
},
)
// ...
}
作业通过设置的 Schedule 和所需的 Repeats 数量进行调度。这些设置确定作业应被触发并发送回应用程序的最大次数。
在此示例中,在触发时间(根据 Schedule 为 @every 1s),此作业被触发并发送回应用程序,直到达到最大 Repeats(10)。
// ...
// Set up the job
job := daprc.Job{
Name: "prod-db-backup",
Schedule: "@every 1s",
Repeats: 10,
Data: &anypb.Any{
Value: jobData,
},
}
当作业被触发时,Dapr 会自动将作业路由到你在服务器初始化期间设置的事件处理程序。例如,在 Go 中,你会像这样注册事件处理程序:
...
if err = server.AddJobEventHandler("prod-db-backup", prodDBBackupHandler); err != nil {
log.Fatalf("failed to register job event handler: %v", err)
}
Dapr 处理底层路由。当作业被触发时,会使用触发的作业数据调用你的 prodDBBackupHandler 函数。以下是处理触发的作业的示例:
// ...
// At job trigger time this function is called
func prodDBBackupHandler(ctx context.Context, job *common.JobEvent) error {
var jobData common.Job
if err := json.Unmarshal(job.Data, &jobData); err != nil {
// ...
}
var jobPayload api.DBBackup
if err := json.Unmarshal(job.Data, &jobPayload); err != nil {
// ...
}
fmt.Printf("job %d received:\n type: %v \n typeurl: %v\n value: %v\n extracted payload: %v\n", jobCount, job.JobType, jobData.TypeURL, jobData.Value, jobPayload)
jobCount++
return nil
}
运行 Dapr 边车
一旦你在应用程序中设置了 Jobs API,在终端窗口中使用以下命令运行 Dapr 边车。
dapr run --app-id=distributed-scheduler \
--metrics-port=9091 \
--dapr-grpc-port 50001 \
--app-port 50070 \
--app-protocol grpc \
--log-level debug \
go run ./main.go
后续步骤
1.12 - Conversation
1.12.1 - 对话概览
Alpha
对话 API 目前处于 alpha 阶段。Dapr 的对话 API 降低了大规模安全可靠地与大型语言模型 (LLM) 交互的复杂性。无论您是缺乏必要原生 SDK 的开发者,还是只想专注于 LLM 交互的提示工程的多语言团队,对话 API 都提供了一个一致的 API 入口点来与底层 LLM 提供商通信。

除了启用关键的性能和安全功能(如缓存和 PII 清理),对话 API 还提供:
- 工具调用能力:允许 LLM 与外部函数和 API 交互,实现更复杂的 AI 应用
- OpenAI 兼容接口:与现有 AI 工作流和工具无缝集成
您还可以将对话 API 与 Dapr 功能结合使用,例如:
- 弹性策略,包括处理重复错误的断路器、保护慢响应的超时机制,以及临时网络故障的重试
- 使用 OpenTelemetry 和 Zipkin 的可观测性,包括指标和分布式追踪
- 验证进出 LLM 请求的中间件
功能
以下功能是所有支持的对话组件 开箱即用的。
缓存
对话 API 支持两种缓存:
- 提示缓存:某些 LLM 提供商在本地缓存提示前缀,以加快重复提示的速度并降低成本。您可以通过 API 使用
promptCacheRetention参数在每个请求中启用此功能(例如,24h用于 OpenAI)。有关请求级选项,请参阅对话 API 参考。支持情况取决于提供商。 - 响应缓存:对话组件可以在边车中缓存完整的 LLM 响应。当您设置组件元数据字段
responseCacheTTL(例如10m)时,Dapr 会根据请求(提示和选项)缓存响应。重复的相同请求将从缓存提供,无需调用 LLM,从而降低延迟和成本。此缓存在内存中且每个边车独立。请在您的对话组件 spec 中配置。
响应格式化
您可以通过在请求中传递 responseFormat(JSON Schema)来请求模型的结构化输出。支持 Deepseek、Google AI、Hugging Face、OpenAI 和 Anthropic。请参阅对话 API 参考。
使用量指标
响应可以包含对话的令牌使用量(promptTokens、completionTokens、totalTokens)。请参阅 API 参考中的响应内容。
个人身份信息 (PII) 混淆
PII 混淆功能可识别并清除对话响应中任何形式的敏感用户信息。只需在输入和输出数据上启用 PII 混淆即可保护您的隐私,清除可能被用来识别个人的敏感细节。
PII 清理器会混淆以下用户信息:
- 电话号码
- 电子邮件地址
- IP 地址
- 街道地址
- 信用卡
- 社会安全号码
- ISBN
- 媒体访问控制 (MAC) 地址
- 安全哈希算法 1 (SHA-1) 十六进制
- SHA-256 十六进制
- MD5 十六进制
工具调用支持
对话 API 支持高级工具调用能力,允许 LLM 与外部函数和 API 交互。这使您能够构建复杂的 AI 应用,这些应用可以:
- 根据用户请求执行自定义函数
- 与外部服务和数据库集成
- 提供动态的上下文感知响应
- 创建多步骤工作流和自动化
工具调用遵循 OpenAI 的函数调用格式,便于与现有 AI 开发工作流和工具集成。
演示
观看在 Diagrid 的 Dapr v1.15 庆祝活动 上进行的演示,了解如何使用 .NET SDK 了解对话 API 的工作原理。
尝试对话 API
快速入门和教程
想让 Dapr 对话 API 接受测试吗?通过以下快速入门和教程了解实际效果:
| 快速入门/教程 | 描述 |
|---|---|
| 对话快速入门 | 了解如何使用对话 API 与大型语言模型 (LLM) 交互。 |
直接在您的应用中使用对话 API
想跳过快速入门吗?没问题。您可以直接在应用程序中试用对话构建块。在安装 Dapr 后,您可以开始使用对话 API,从操作指南开始。
后续步骤
1.12.2 - How-To: 使用对话 API 与 LLM 对话
Alpha
对话 API 目前处于 alpha 状态。让我们开始使用对话 API。在本指南中,您将学习如何:
- 设置可与对话 API 配合使用的可用 Dapr 组件(echo)。
- 将对话客户端添加到您的应用程序。
- 使用
dapr run运行连接。
设置对话组件
创建一个名为 conversation.yaml 的新配置文件,并保存到应用程序目录下的 components 或 config 子文件夹中。
选择您的首选对话组件规范用于您的 conversation.yaml 文件。
对于此场景,我们使用简单的 echo 组件。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: echo
spec:
type: conversation.echo
version: v1
使用 OpenAI 组件
要与真实的 LLM 交互,请使用其他支持的对话组件,包括 OpenAI、Hugging Face、Anthropic、DeepSeek 等。
例如,要将 echo 模拟组件替换为 OpenAI 组件,请使用以下内容替换 conversation.yaml 文件。您需要将 API 密钥复制到组件文件中。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: openai
spec:
type: conversation.openai
metadata:
- name: key
value: <REPLACE_WITH_YOUR_KEY>
- name: model
value: gpt-4-turbo
连接对话客户端
以下示例使用 Dapr SDK 客户端与 LLM 进行交互。
using Dapr.AI.Conversation;
using Dapr.AI.Conversation.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprConversationClient();
var app = builder.Build();
var conversationClient = app.Services.GetRequiredService<DaprConversationClient>();
var response = await conversationClient.ConverseAsync("conversation",
new List<DaprConversationInput>
{
new DaprConversationInput(
"Please write a witty haiku about the Dapr distributed programming framework at dapr.io",
DaprConversationRole.Generic)
});
Console.WriteLine("conversation output: ");
foreach (var resp in response.Outputs)
{
Console.WriteLine($"\t{resp.Result}");
}
//dependencies
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.DaprPreviewClient;
import io.dapr.client.domain.ConversationInput;
import io.dapr.client.domain.ConversationRequest;
import io.dapr.client.domain.ConversationResponse;
import reactor.core.publisher.Mono;
import java.util.List;
public class Conversation {
public static void main(String[] args) {
String prompt = "Please write a witty haiku about the Dapr distributed programming framework at dapr.io";
try (DaprPreviewClient client = new DaprClientBuilder().buildPreviewClient()) {
System.out.println("Input: " + prompt);
ConversationInput daprConversationInput = new ConversationInput(prompt);
// Component name is the name provided in the metadata block of the conversation.yaml file.
Mono<ConversationResponse> responseMono = client.converse(new ConversationRequest("echo",
List.of(daprConversationInput))
.setContextId("contextId")
.setScrubPii(true).setTemperature(1.1d));
ConversationResponse response = responseMono.block();
System.out.printf("conversation output: %s", response.getConversationOutputs().get(0).getResult());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
#dependencies
from dapr.clients import DaprClient
from dapr.clients.grpc._request import ConversationInput
#code
with DaprClient() as d:
inputs = [
ConversationInput(content="Please write a witty haiku about the Dapr distributed programming framework at dapr.io", role='user', scrub_pii=True),
]
metadata = {
'model': 'modelname',
'key': 'authKey',
'responseCacheTTL': '10m',
}
response = d.converse_alpha1(
name='echo', inputs=inputs, temperature=0.7, context_id='chat-123', metadata=metadata
)
for output in response.outputs:
print(f'conversation output: {output.result}')
package main
import (
"context"
"fmt"
dapr "github.com/dapr/go-sdk/client"
"log"
)
func main() {
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
input := dapr.ConversationInput{
Content: "Please write a witty haiku about the Dapr distributed programming framework at dapr.io",
// Role: "", // Optional
// ScrubPII: false, // Optional
}
fmt.Printf("conversation input: %s\n", input.Content)
var conversationComponent = "echo"
request := dapr.NewConversationRequest(conversationComponent, []dapr.ConversationInput{input})
resp, err := client.ConverseAlpha1(context.Background(), request)
if err != nil {
log.Fatalf("err: %v", err)
}
fmt.Printf("conversation output: %s\n", resp.Outputs[0].Result)
}
use dapr::client::{ConversationInputBuilder, ConversationRequestBuilder};
use std::thread;
use std::time::Duration;
type DaprClient = dapr::Client<dapr::client::TonicClient>;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Sleep to allow for the server to become available
thread::sleep(Duration::from_secs(5));
// Set the Dapr address
let address = "https://127.0.0.1".to_string();
let mut client = DaprClient::connect(address).await?;
let input = ConversationInputBuilder::new("Please write a witty haiku about the Dapr distributed programming framework at dapr.io").build();
let conversation_component = "echo";
let request =
ConversationRequestBuilder::new(conversation_component, vec![input.clone()]).build();
println!("conversation input: {:?}", input.content);
let response = client.converse_alpha1(request).await?;
println!("conversation output: {:?}", response.outputs[0].result);
Ok(())
}
运行对话连接
使用 dapr run 命令启动连接。例如,对于此场景,我们在一个 app ID 为 conversation 的应用程序上运行 dapr run,并指向 ./config 目录中的对话 YAML 文件。
dapr run --app-id conversation --dapr-grpc-port 50001 --log-level debug --resources-path ./config -- dotnet run
dapr run --app-id conversation --dapr-grpc-port 50001 --log-level debug --resources-path ./config -- mvn spring-boot:run
dapr run --app-id conversation --dapr-grpc-port 50001 --log-level debug --resources-path ./config -- python3 app.py
dapr run --app-id conversation --dapr-grpc-port 50001 --log-level debug --resources-path ./config -- go run ./main.go
dapr run --app-id=conversation --resources-path ./config --dapr-grpc-port 3500 -- cargo run --example conversation
预期输出
- '== APP == conversation output: Please write a witty haiku about the Dapr distributed programming framework at dapr.io'
高级功能
对话 API 支持以下功能:
提示缓存: 允许开发者在 Dapr 中缓存提示,从而获得更快的响应时间,并降低 LLM 提供商缓存插入提示的出口成本。
PII 清理: 允许对进入和离开 LLM 的数据进行混淆处理。
工具调用: 允许 LLM 与外部函数和 API 进行交互。
要了解如何启用这些功能,请参阅对话 API 参考指南。
Dapr SDK 仓库中的对话 API 示例
使用支持 SDK 仓库中提供的完整示例来体验对话 API。
下一步
2 - Dapr 软件开发工具包(SDK)
Dapr SDK 是将 Dapr 集成到应用程序的最简单方式。选择您喜欢的语言,几分钟内即可上手使用 Dapr。
SDK 包
选择下方的您偏好的语言,以了解更多关于客户端、服务端、Actor 和工作流包的信息。
- 客户端(Client):Dapr 客户端允许您调用 Dapr 构建块 API 并执行各构建块的操作
- 服务扩展(Server extensions):Dapr 服务扩展允许您创建可被其他服务调用并可订阅主题的服务
- Actor:Dapr Actor SDK 允许您构建包含方法、状态、定时器和持久化提醒的虚拟 Actor
- 工作流(Workflow):Dapr 工作流让您能够以可靠的方式轻松编写长时间运行的业务逻辑和集成
SDK 支持的语言
| 语言 | 状态 | 客户端 | 服务扩展 | Actor | 工作流 |
|---|---|---|---|---|---|
| .NET | Stable | ✔ | ASP.NET Core | ✔ | ✔ |
| Python | Stable | ✔ | gRPC FastAPI Flask | ✔ | ✔ |
| Java | Stable | ✔ | Spring Boot Quarkus | ✔ | ✔ |
| Go | Stable | ✔ | ✔ | ✔ | ✔ |
| PHP | Stable | ✔ | ✔ | ✔ | |
| JavaScript | Stable | ✔ | ✔ | ✔ | |
| C++ | In development | ✔ | |||
| Rust | In development | ✔ | ✔ |
框架
| 框架 | 语言 | 状态 | 描述 |
|---|---|---|---|
| Dapr Agents | Python | In development | 一个用于构建由大语言模型(LLM)驱动的自主代理的框架,该框架利用 Dapr 的分布式系统能力实现可靠执行,并内置安全性、可观测性和状态管理。 |
延伸阅读
2.1 - Dapr .NET SDK
Dapr 提供了多种包来帮助开发 .NET 应用程序。使用它们,您可以创建用于 Dapr 的 .NET 客户端、服务器和虚拟 actor。
前置条件
注意
Dapr .NET SDK 支持 .NET 8、.NET 9 和 .NET 10。.NET 8 和 .NET 9 将持续支持至其生命周期结束(2026 年 11 月)之后的第一个 Dapr 版本,之后我们预计将转而支持 .NET 10 和 .NET 11。安装
要开始使用 Client .NET SDK,请安装 Dapr .NET SDK 包:
dotnet add package Dapr.Client
试用
测试 Dapr .NET SDK。通过 .NET 快速入门和教程了解 Dapr 的实际应用:
| SDK 示例 | 描述 |
|---|---|
| 快速入门 | 使用 .NET SDK 在几分钟内体验 Dapr 的 API 构建块。 |
| SDK 示例 | 克隆 SDK 仓库以尝试一些示例并开始使用。 |
| 发布订阅教程 | 了解 Dapr .NET SDK 如何与其他 Dapr SDK 协作以实现发布订阅应用程序。 |
可用包
| 包名称 | 文档链接 | 描述 |
|---|---|---|
| Dapr.Client | 文档 | 创建与 Dapr 边车和其他 Dapr 应用程序交互的 .NET 客户端。 |
| Dapr.AI | 文档 | 在 .NET 中创建和管理 AI 操作。 |
| Dapr.AI.A2a | 使用 A2A 框架实现 agent-to-agent 操作的 Dapr SDK。 | |
| Dapr.AI.Microsoft.Extensions | 文档 | 通过 Dapr Conversation 构建块,以对话方式和使用工具轻松与 LLM 交互。 |
| Dapr.AspNetCore | 文档 | 使用 Dapr SDK 在 .NET 中编写服务器和服务。包括提供与 ASP.NET Core 更深度集成的支持和实用程序。 |
| Dapr.Actors | 文档 | 创建具有状态、提醒/定时器和方法的虚拟 actor。 |
| Dapr.Actors.AspNetCore | 文档 | 创建具有状态、提醒/定时器和方法的虚拟 actor,与 ASP.NET Core 深度集成。 |
| Dapr.Actors.Analyzers | 文档 | 一组 Roslyn 源代码生成器和分析器,用于在 .NET 中使用 Dapr Actors 时实现更好的实践并防止常见错误。 |
| Dapr.Cryptography | 文档 | 使用 Dapr 密码学构建块加密和解密任意大小的流状态。 |
| Dapr.Jobs | 文档 | 创建和管理作业的调度和编排。 |
| Dapr.Jobs.Analyzers | 文档 | 一组 Roslyn 源代码生成器和分析器,用于在 .NET 中使用 Dapr Jobs 时实现更好的实践并防止常见错误。 |
| Dapr.DistributedLocks | 文档 | 创建和管理分布式锁以管理独占资源访问。 |
| Dapr.Extensions.Configuration | 用于 Microsoft.Extensions.Configuration 的 Dapr 密钥存储配置提供程序实现。 | |
| Dapr.PluggableComponents | 用于使用 .NET 实现 Dapr 可插拔组件。 | |
| Dapr.PluggableComponents.AspNetCore | 使用 .NET 实现 Dapr 可插拔组件,提供丰富的 ASP.NET Core 支持。 | |
| Dapr.PluggableComponents.Protos | 注意: 开发人员无需在其应用程序中直接安装此包。 | |
| Dapr.Messaging | 文档 | 使用 Dapr Messaging SDK 构建分布式应用程序,该 SDK 利用流式发布订阅订阅等消息组件。 |
| Dapr.Testcontainers | 文档 | 使用基于 Testcontainers 的测试工具运行 Dapr 集成测试。 |
| Dapr.Workflow | 文档 | 创建和管理与其他 Dapr API 协作的工作流。 |
| Dapr.Workflow.Versioning | 文档 | 添加工作流版本控制策略以演进长时间运行的工作流。 |
| Dapr.Workflow.Analyzers | 文档 | 一组 Roslyn 源代码生成器和分析器,用于在 .NET 中使用 Dapr Workflows 时实现更好的实践并防止常见错误 |
更多信息
了解有关本地开发选项、最佳实践的更多信息,或浏览 NuGet 包以添加到您现有的 .NET 应用程序中。
2.1.1 - Dapr 客户端 .NET SDK 入门
Dapr 客户端包允许你从 .NET 应用程序与其他 Dapr 应用程序进行交互。
注意
如果尚未尝试,请尝试快速入门之一,快速了解如何使用 API 构建块结合 Dapr .NET SDK。构建块
.NET SDK 允许你与所有 Dapr 构建块 进行交互。
注意
我们只会在第一个示例(服务调用)中包含DaprClient 的依赖注入注册。在几乎所有其他示例中,我们假设你已经在后续示例的应用程序中注册了 DaprClient,并且已将 DaprClient 的实例作为名为 client 的实例注入到代码中。调用服务
HTTP
你可以使用 DaprClient 或 System.Net.Http.HttpClient 来调用服务。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();
using var scope = app.Services.CreateScope();
var client = scope.ServiceProvider.GetRequiredService<DaprClient>();
// 调用名为 "deposit" 的 POST 方法,该方法接受类型为 "Transaction" 的输入
var data = new { id = "17", amount = 99m };
var account = await client.InvokeMethodAsync<Account>("routing", "deposit", data, cancellationToken);
Console.WriteLine("Returned: id:{0} | Balance:{1}", account.Id, account.Balance);
using Microsoft.Extensins.Hosting; using Microsoft.Extensions.DependencyInjection;
var builder = Host.CreateApplicationBuilder(args); builder.Services.AddDaprClient(); var app = builder.Build();
using var scope = app.Services.CreateScope();
var client = scope.ServiceProvider.GetRequiredService
// 调用名为 “deposit” 的 POST 方法,该方法接受类型为 “Transaction” 的输入
var data = new { id = “17”, amount = 99m };
var account = await client.InvokeMethodAsync
var client = DaprClient.CreateInvokeHttpClient(appId: "routing");
// 在 HTTP 客户端上设置超时:
client.Timeout = TimeSpan.FromSeconds(2);
var deposit = new Transaction { Id = "17", Amount = 99m };
var response = await client.PostAsJsonAsync("/deposit", deposit, cancellationToken);
var account = await response.Content.ReadFromJsonAsync<Account>(cancellationToken: cancellationToken);
Console.WriteLine("Returned: id:{0} | Balance:{1}", account.Id, account.Balance);
gRPC
你可以使用 DaprClient 通过 gRPC 调用服务。
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
var invoker = DaprClient.CreateInvocationInvoker(appId: myAppId, daprEndpoint: serviceEndpoint);
var client = new MyService.MyServiceClient(invoker);
var options = new CallOptions(cancellationToken: cts.Token, deadline: DateTime.UtcNow.AddSeconds(1));
await client.MyMethodAsync(new Empty(), options);
Assert.Equal(StatusCode.DeadlineExceeded, ex.StatusCode);
- 有关服务调用的完整指南,请访问操作指南:调用服务。
保存和获取应用程序状态
var state = new Widget() { Size = "small", Color = "yellow", };
await client.SaveStateAsync(storeName, stateKeyName, state, cancellationToken: cancellationToken);
Console.WriteLine("Saved State!");
state = await client.GetStateAsync<Widget>(storeName, stateKeyName, cancellationToken: cancellationToken);
Console.WriteLine($"Got State: {state.Size} {state.Color}");
await client.DeleteStateAsync(storeName, stateKeyName, cancellationToken: cancellationToken);
Console.WriteLine("Deleted State!");
查询状态(Alpha)
var query = "{" +
"\"filter\": {" +
"\"EQ\": { \"value.Id\": \"1\" }" +
"}," +
"\"sort\": [" +
"{" +
"\"key\": \"value.Balance\"," +
"\"order\": \"DESC\"" +
"}" +
"]" +
"}";
var queryResponse = await client.QueryStateAsync<Account>("querystore", query, cancellationToken: cancellationToken);
Console.WriteLine($"Got {queryResponse.Results.Count}");
foreach (var account in queryResponse.Results)
{
Console.WriteLine($"Account: {account.Data.Id} has {account.Data.Balance}");
}
- 有关状态操作的完整列表,请访问操作指南:获取和保存状态。
发布消息
var eventData = new { Id = "17", Amount = 10m, };
await client.PublishEventAsync(pubsubName, "deposit", eventData, cancellationToken);
Console.WriteLine("Published deposit event!");
- 有关状态操作的完整列表,请访问操作指南:发布和订阅。
- 访问 .NET SDK 示例 获取代码示例和试用发布订阅的说明
与输出绑定交互
调用 InvokeBindingAsync 时,你可以选择自己处理序列化和编码,或者让 SDK 为你将其序列化为 JSON 然后编码为字节。
重要
绑定所期望的数据形状各不相同,请特别注意并确保你发送的数据得到相应处理。如果你正在编写输出和输入,请确保它们都遵循相同的序列化约定。手动序列化
对于大多数场景,建议使用 InvokeBindingAsync 的此重载,因为它为你提供了清晰度和对数据处理方式的控制。
在此示例中,数据作为字符串的 UTF-8 字节表示发送。
using var client = new DaprClientBuilder().Build();
var request = new BindingRequest("send-email", "create")
{
// 注意:这是 Twilio SendGrid 绑定的示例负载
Data = Encoding.UTF8.GetBytes("<h1>Testing Dapr Bindings</h1>This is a test.<br>Bye!"),
Metadata =
{
{ "emailTo", "customer@example.com" },
{ "subject", "An email from Dapr SendGrid binding" },
},
}
await client.InvokeBindingAsync(request);
自动序列化和编码
在此示例中,数据作为序列化为 JSON 的值的 UTF-8 编码字节表示发送。
using var client = new DaprClientBuilder().Build();
var email = new
{
// 注意:这是 Twilio SendGrid 绑定的示例负载
data = "<h1>Testing Dapr Bindings</h1>This is a test.<br>Bye!",
metadata = new
{
emailTo = "customer@example.com",
subject = "An email from Dapr SendGrid binding",
},
};
await client.InvokeBindingAsync("send-email", "create", email);
- 有关输出绑定的完整指南,请访问操作指南:使用绑定。
检索机密
在检索机密之前,重要的是确保出站通道已注册并准备就绪,否则 SDK 将无法与 Dapr 边车进行双向通信。SDK 提供了一个用于此目的的辅助方法,称为 CheckOutboundHealthAsync。这不是指从 SDK 到运行时的出站,而是指从 Dapr 运行时回到使用 SDK 的客户端应用程序的出站。
此方法只是打开到 https://docs.dapr.io/zh-hans/reference/api/health_api/#wait-for-specific-health-check-against-outbound-path Dapr Health API 中的端点的连接,并评估返回的 HTTP 状态码以确定运行时报告的端点的健康状况。
重要的是要注意,WaitForSidecarAsync 方法和此方法执行几乎相同的操作;WaitForSidecarAsync
无限期轮询 CheckOutboundHealthAsync 端点,直到它返回健康状态值。它们仅用于机密或配置检索等情况。在其他场景中使用它们会导致意外行为(例如,端点永远无法准备就绪,因为没有注册使用"出站"通道的组件)。
此行为将在未来版本中更改,应谨慎依赖。
// 从 DI 获取 DaprClient 实例
var client = scope.GetRequiredService<DaprClient>();
// 等待出站通道建立 - 仅用于此场景,而非一般用途
await client.WaitForOutboundHealthAsync();
// 检索基于键值对的机密 - 返回 Dictionary<string, string>
var secrets = await client.GetSecretAsync("mysecretstore", "key-value-pair-secret");
Console.WriteLine($"Got secret keys: {string.Join(", ", secrets.Keys)}");
// 从 DI 获取 DaprClient 实例
var client = scope.GetRequiredService<DaprClient>();
// 等待出站通道建立 - 仅用于此场景,而非一般用途
await client.WaitForOutboundHealthAsync();
// 检索基于键值对的机密 - 返回 Dictionary<string, string>
var secrets = await client.GetSecretAsync("mysecretstore", "key-value-pair-secret");
Console.WriteLine($"Got secret keys: {string.Join(", ", secrets.Keys)}");
// 检索单值机密 - 返回 Dictionary<string, string>
// 包含一个以机密名称为键的单个值
var data = await client.GetSecretAsync("mysecretstore", "single-value-secret");
var value = data["single-value-secret"]
Console.WriteLine("Got a secret value, I'm not going to be print it, it's a secret!");
- 有关机密的完整指南,请访问操作指南:检索机密。
获取配置键
// 检索特定键集。
var specificItems = await client.GetConfiguration("configstore", new List<string>() { "key1", "key2" });
Console.WriteLine($"Here are my values:\n{specificItems[0].Key} -> {specificItems[0].Value}\n{specificItems[1].Key} -> {specificItems[1].Value}");
// 通过提供空列表检索所有配置项。
var specificItems = await client.GetConfiguration("configstore", new List<string>());
Console.WriteLine($"I got {configItems.Count} entires!");
foreach (var item in configItems)
{
Console.WriteLine($"{item.Key} -> {item.Value}")
}
订阅配置键
// 订阅配置 API 返回 IAsyncEnumerable<IEnumerable<ConfigurationItem>> 的包装器。
// 通过在 foreach 循环中访问其 Source 来迭代它。当流被中断
// 或取消令牌被取消时,循环将结束。
var subscribeConfigurationResponse = await daprClient.SubscribeConfiguration(store, keys, metadata, cts.Token);
await foreach (var items in subscribeConfigurationResponse.Source.WithCancellation(cts.Token))
{
foreach (var item in items)
{
Console.WriteLine($"{item.Key} -> {item.Value}")
}
}
分布式锁(Alpha)
获取锁
using System;
using Dapr.Client;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace LockService
{
class Program
{
[Obsolete("Distributed Lock API is in Alpha, this can be removed once it's stable.")]
static async Task Main(string[] args)
{
const string daprLockName = "lockstore";
const string fileName = "my_file_name";
var builder = Host.CreateDefaultBuilder();
builder.ConfigureServices(services =>
{
services.AddDaprClient();
});
var app = builder.Build();
using var scope = app.Services.CreateScope();
var client = scope.ServiceProvider.GetRequiredService<DaprClient>();
// 使用此方法锁定也会自动解锁它,因为这是一个可释放对象
await using (var fileLock = await client.Lock(DAPR_LOCK_NAME, fileName, "random_id_abc123", 60))
{
if (fileLock.Success)
{
Console.WriteLine("Success");
}
else
{
Console.WriteLine($"Failed to lock {fileName}.");
}
}
}
}
}
解锁现有锁
using System;
using Dapr.Client;
namespace LockService
{
class Program
{
static async Task Main(string[] args)
{
var daprLockName = "lockstore";
var builder = Host.CreateDefaultBuilder();
builder.ConfigureServices(services =>
{
services.AddDaprClient();
});
var app = builder.Build();
using var scope = app.Services.CreateScope();
var client = scope.ServiceProvider.GetRequiredService<DaprClient>();
var response = await client.Unlock(DAPR_LOCK_NAME, "my_file_name", "random_id_abc123"));
Console.WriteLine(response.status);
}
}
}
边车 API
边车健康检查
虽然 .NET SDK 提供了一种轮询边车健康状态的方法,但通常不建议开发人员使用此功能,除非他们明确使用 Dapr 来检索机密或配置值。
有两种方法可用:
CheckOutboundHealthAsync查询 Dapr Health API https://docs.dapr.io/zh-hans/reference/api/health_api/#wait-for-specific-health-check-against-outbound-path 中的出站就绪端点,以获取成功的 HTTP 状态码并基于此值报告就绪状态。WaitForSidecarAsync持续轮询CheckOutboundHealthAsync直到它返回成功的状态码。
“出站"方向是指从 Dapr 运行时到你的应用程序的出站通信。如果你的应用程序不使用 Actors、机密管理、配置检索或工作流,运行时将不会尝试创建出站连接。这意味着如果你的应用程序依赖于 WaitForSidecarAsync 而不使用任何这些 Dapr 组件,它将在启动期间无限期锁定,因为永远不会建立端点。
未来的版本将完全删除这些方法,并将其作为内部 SDK 操作执行,因此一般不应依赖任何一种方法。请在 Discord #dotnet-sdk 频道中联系以获取更多说明,了解你的场景是否可能需要使用此功能,但在大多数情况下,不应需要这些方法。
关闭边车
var client = new DaprClientBuilder().Build();
await client.ShutdownSidecarAsync();
相关链接
2.1.1.1 - DaprClient 使用指南
生命周期管理
DaprClient 持有对网络资源的访问,这些资源以用于与 Dapr 边车通信的 TCP 套接字形式存在。DaprClient 实现了 IDisposable 以支持资源的主动清理。
依赖注入
AddDaprClient() 方法会将 Dapr 客户端注册到 ASP.NET Core 依赖注入容器中。此方法接受一个可选的 options 委托来配置 DaprClient,以及一个 ServiceLifetime 参数,允许你为注册的资源指定不同的生命周期,而不是使用默认的 Singleton 值。
以下示例假设所有默认值都是可接受的,足以注册 DaprClient。
services.AddDaprClient();
可选的配置委托用于通过在提供的 DaprClientBuilder 上指定选项来配置 DaprClient,如以下示例所示:
services.AddDaprClient(daprBuilder => {
daprBuilder.UseJsonSerializerOptions(new JsonSerializerOptions {
WriteIndented = true,
MaxDepth = 8
});
daprBuilder.UseTimeout(TimeSpan.FromSeconds(30));
});
另一个可选的配置委托重载提供了对 DaprClientBuilder 和 IServiceProvider 的访问,允许进行更高级的配置,这些配置可能需要从依赖注入容器中注入服务。
services.AddSingleton<SampleService>();
services.AddDaprClient((serviceProvider, daprBuilder) => {
var sampleService = serviceProvider.GetRequiredService<SampleService>();
var timeoutValue = sampleService.TimeoutOptions;
daprBuilder.UseTimeout(timeoutValue);
});
手动实例化
除了使用依赖注入,还可以使用静态客户端构建器来构建 DaprClient。
为获得最佳性能,应创建一个单一的长生命周期 DaprClient 实例,并在整个应用程序中提供对该共享实例的访问。DaprClient 实例是线程安全的,旨在共享使用。
避免为每个操作创建 DaprClient 并在操作完成时将其释放。
配置 DaprClient
可以通过在调用 .Build() 创建客户端之前调用 DaprClientBuilder 类上的方法来配置 DaprClient。每个 DaprClient 对象的设置是独立的,在调用 .Build() 后无法更改。
var daprClient = new DaprClientBuilder()
.UseJsonSerializerSettings( ... ) // 配置 JSON 序列化器
.Build();
默认情况下,DaprClientBuilder 将按以下顺序优先考虑以下位置来获取配置值:
- 提供给
DaprClientBuilder上某个方法的值(例如UseTimeout(TimeSpan.FromSeconds(30))) - 从可选注入的
IConfiguration中提取的值,其名称与相关环境变量的预期名称匹配 - 从相关环境变量中提取的值
- 默认值
在 DaprClientBuilder 上配置
DaprClientBuilder 包含以下方法来设置配置选项:
UseHttpEndpoint(string):Dapr 边车的 HTTP 端点UseGrpcEndpoint(string):设置 Dapr 边车的 gRPC 端点UseGrpcChannelOptions(GrpcChannelOptions):设置用于连接到 Dapr 边车的 gRPC 通道选项UseHttpClientFactory(IHttpClientFactory):配置DaprClient在构建HttpClient实例时使用已注册的IHttpClientFactoryUseJsonSerializationOptions(JsonSerializerOptions):用于配置 JSON 序列化UseDaprApiToken(string):将提供的令牌添加到每个请求以向 Dapr 边车进行身份验证UseTimeout(TimeSpan):指定HttpClient与 Dapr 边车通信时使用的超时值
从 IConfiguration 配置
除了直接从环境变量获取配置值,或者因为值是从依赖注入的服务中获取的,另一种选择是使这些值在 IConfiguration 上可用。
例如,你可能要在多租户环境中注册应用程序,并且需要为所使用的环境变量添加前缀。以下示例展示了当这些环境变量的键以 test_ 为前缀时,如何将这些值从环境变量获取到你的 IConfiguration 中:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddEnvironmentVariables("test_"); // 检索所有以 "test_" 开头的环境变量,并在从 IConfiguration 获取时移除前缀
builder.Services.AddDaprClient();
从环境变量配置
SDK 将读取以下环境变量来配置默认值:
DAPR_HTTP_ENDPOINT:用于查找 Dapr 边车的 HTTP 端点,示例:https://dapr-api.mycompany.comDAPR_GRPC_ENDPOINT:用于查找 Dapr 边车的 gRPC 端点,示例:https://dapr-grpc-api.mycompany.comDAPR_HTTP_PORT:如果未设置DAPR_HTTP_ENDPOINT,则用于查找 Dapr 边车的 HTTP 本地端点,假定主机为 ‘127.0.0.1’DAPR_GRPC_PORT:如果未设置DAPR_GRPC_ENDPOINT,则用于查找 Dapr 边车的 gRPC 本地端点,假定主机为 ‘127.0.0.1’DAPR_API_TOKEN:用于设置 API 令牌
注意
如果同时指定了DAPR_HTTP_ENDPOINT 和 DAPR_HTTP_PORT,则将忽略 DAPR_HTTP_PORT 中的端口值,优先使用 DAPR_HTTP_ENDPOINT 上隐式或显式定义的端口。对于 DAPR_GRPC_ENDPOINT 和 DAPR_GRPC_PORT 同理。作为一般规则,应同时指定 HTTP 和 gRPC 端口,或同时指定 HTTP 和 gRPC 端点。实际上,使用 HTTP 还是 gRPC 取决于你使用的具体 Dapr 服务以及 .NET SDK 是否支持 HTTP 或 gRPC 协议。随着时间的推移,绝大多数 .NET SDK 将专门支持 gRPC,但这永远不会适用于所有服务,因此为你的应用程序提供面向未来的保障并始终指定这两个值是更好的做法。
配置 gRPC 通道选项
Dapr 使用 CancellationToken 进行取消操作依赖于 gRPC 通道选项的配置,这是默认启用的。如果你需要自己配置这些选项,请确保启用 ThrowOperationCanceledOnCancellation 设置。
var daprClient = new DaprClientBuilder()
.UseGrpcChannelOptions(new GrpcChannelOptions { ... ThrowOperationCanceledOnCancellation = true })
.Build();
使用取消令牌与 DaprClient
DaprClient 上执行异步操作的 API 接受一个可选的 CancellationToken 参数。这遵循 .NET 可取消操作的标准惯例。请注意,当发生取消时,无法保证远程端点停止处理请求,只能保证客户端已停止等待完成。
当操作被取消时,它将抛出 OperationCancelledException。
理解 DaprClient JSON 序列化
DaprClient 的许多方法使用 System.Text.Json 序列化器执行 JSON 序列化。接受应用程序数据类型作为参数的方法将对其进行 JSON 序列化,除非文档另有明确说明。
如果你有高级需求,值得阅读 System.Text.Json 文档。Dapr .NET SDK 不提供独特的序列化行为或自定义 - 它依赖底层序列化器在应用程序的 .NET 类型之间转换数据。
DaprClient 配置为使用从 JsonSerializerDefaults.Web 配置的序列化器选项对象。这意味着 DaprClient 将对属性名称使用 camelCase,允许读取带引号的数字("10.99"),并且将以不区分大小写的方式绑定属性。这些是与 ASP.NET Core 和 System.Text.Json.Http API 使用的相同设置,旨在遵循可互操作的 Web 约定。
从 .NET 5.0 开始,System.Text.Json 对 F# 语言的所有内置功能支持不佳。如果你使用 F#,你可能希望使用其中一个为 F# 功能添加支持的转换器包,例如 FSharp.SystemTextJson。
JSON 序列化的简单指导
如果你使用映射到 JSON 类型系统的功能集,使用 JSON 序列化和 DaprClient 的体验将会很顺畅。这些是可以简化代码的一般性指导原则。
- 避免继承和多态
- 不要尝试序列化具有循环引用的数据
- 不要在构造函数或属性访问器中放置复杂或昂贵的逻辑
- 使用干净映射到 JSON 类型的 .NET 类型(数值类型、字符串、
DateTime) - 为顶级消息、事件或状态值创建自己的类,以便将来可以添加属性
- 使用具有
get/set属性的类型,或使用支持不可变类型的支持的模式与 JSON 一起使用
多态性和序列化
DaprClient 使用的 System.Text.Json 序列化器在执行序列化时使用值的声明类型。
本节将在示例中使用 DaprClient.SaveStateAsync<TValue>(...),但该建议适用于 SDK 公开的任何 Dapr 构建块。
public class Widget
{
public string Color { get; set; }
}
...
// 将 Widget 值作为 JSON 存储在状态存储中
Widget widget = new Widget() { Color = "Green", };
await client.SaveStateAsync("mystatestore", "mykey", widget);
在上面的示例中,类型参数 TValue 的类型参数是从 widget 变量的类型推断出来的。这很重要,因为 System.Text.Json 序列化器将基于值的声明类型执行序列化。结果是存储 JSON 值 { "color": "Green" }。
考虑当你尝试使用 Widget 的派生类型时会发生什么:
public class Widget
{
public string Color { get; set; }
}
public class SuperWidget : Widget
{
public bool HasSelfCleaningFeature { get; set; }
}
...
// 将 SuperWidget 值作为 JSON 存储在状态存储中
Widget widget = new SuperWidget() { Color = "Green", HasSelfCleaningFeature = true, };
await client.SaveStateAsync("mystatestore", "mykey", widget);
在此示例中,我们使用的是 SuperWidget,但变量的声明类型是 Widget。由于 JSON 序列化器的行为由声明类型决定,它只看到一个简单的 Widget,并将保存值 { "color": "Green" } 而不是 { "color": "Green", "hasSelfCleaningFeature": true }。
如果你希望 SuperWidget 的属性被序列化,那么最好的选择是用 object 覆盖类型参数。这将导致序列化器包含所有数据,因为它对该类型一无所知。
Widget widget = new SuperWidget() { Color = "Green", HasSelfCleaningFeature = true, };
await client.SaveStateAsync<object>("mystatestore", "mykey", widget);
错误处理
当遇到失败时,DaprClient 的方法将抛出 DaprException 或其子类。
try
{
var widget = new Widget() { Color = "Green", };
await client.SaveStateAsync("mystatestore", "mykey", widget);
}
catch (DaprException ex)
{
// 处理异常、记录日志、重试等
}
最常见的失败情况将与以下内容相关:
- Dapr 组件配置不正确
- 瞬态故障(例如网络问题)
- 无效数据(例如反序列化 JSON 失败)
在任何这些情况下,你都可以通过 .InnerException 属性检查更多异常详细信息。
2.1.2 - Dapr Workflow .NET SDK
2.1.2.1 - DaprWorkflowClient 生命周期管理与注册
DaprWorkflowClient 生命周期管理与依赖注入生命周期管理
DaprWorkflowClient 掌控着用于与 Dapr 边车通信的 TCP 套接字形式持有的网络资源,以及其他用于工作流管理和操作的类型。DaprWorkflowClient 实现了 IAsyncDisposable 以支持资源的主动清理。
依赖注入
AddDaprWorkflow() 方法会将 Dapr 工作流服务注册到 ASP.NET Core 的依赖注入容器中。这个方法需要一个选项委托,用于定义你希望在应用中注册和使用的每个工作流和活动。
注意
此方法将尝试注册DaprClient 实例,但仅在该实例尚未被其他生命周期注册时才会成功。例如,若之前已通过 AddDaprClient() 注册为单例生命周期,则无论为工作流客户端选择何种生命周期,都将始终使用该单例。DaprClient 实例将用于与 Dapr 边车通信;如果尚未注册,则 AddDaprWorkflow() 注册时提供的生命周期将同时用于注册 DaprWorkflowClient 及其自身依赖项。修改 gRPC 消息大小限制
在注册时,你还可以为工作流客户端配置 gRPC 消息大小限制。当工作流负载超出默认 gRPC 限制时,这会很有用。
services
.AddDaprWorkflowClient()
.WithGrpcMessageSizeLimits(
maxReceiveMessageSize: 16 * 1024 * 1024,
maxSendMessageSize: 16 * 1024 * 1024);
单例注册
默认情况下,AddDaprWorkflow 方法以单例生命周期注册 DaprWorkflowClient 及其相关服务。这意味着这些服务只会被实例化一次。
以下示例展示了如何在典型的 Program.cs 文件中注册 DaprWorkflowClient:
builder.Services.AddDaprWorkflow(options => {
options.RegisterWorkflow<YourWorkflow>();
options.RegisterActivity<YourActivity>();
});
var app = builder.Build();
await app.RunAsync();
作用域注册
虽然默认行为在你的场景中通常是可以接受的,但你可能希望覆盖指定的生命周期。这可以通过在 AddDaprWorkflow 中传递 ServiceLifetime 参数来实现。例如,你可能希望在 ASP.NET Core 处理管道中注入另一个需要 DaprClient 所用上下文的作用域服务,如果该服务被注册为单例,这些上下文将无法获取。
以下示例展示了如何操作:
builder.Services.AddDaprWorkflow(options => {
options.RegisterWorkflow<YourWorkflow>();
options.RegisterActivity<YourActivity>();
}, ServiceLifecycle.Scoped);
var app = builder.Build();
await app.RunAsync();
瞬态注册
最后,Dapr 服务也可以注册为瞬态生命周期,这意味着每次注入时都会重新初始化。以下示例展示了如何操作:
builder.Services.AddDaprWorkflow(options => {
options.RegisterWorkflow<YourWorkflow>();
options.RegisterActivity<YourActivity>();
}, ServiceLifecycle.Transient);
var app = builder.Build();
await app.RunAsync();
创建 DaprWorkflowClient 实例
在 ASP.Net Core 应用中,你可以通过方法注入或构造函数注入将 DaprWorkflowClient 注入到方法或控制器中。本示例展示了在最小 API 场景下的方法注入:
app.MapPost("/start", async (
[FromServices] DaprWorkflowClient daprWorkflowClient,
Order order
) => {
var instanceId = await daprWorkflowClient.ScheduleNewWorkflowAsync(
nameof(OrderProcessingWorkflow),
input: order);
return Results.Accepted(instanceId);
});
要在控制台应用中创建 DaprWorkflowClient 实例,请从 ServiceProvider 中获取它:
using var scope = host.Services.CreateAsyncScope();
var daprWorkflowClient = scope.ServiceProvider.GetRequiredService<DaprWorkflowClient>();
现在,你可以使用此客户端执行工作流管理操作,例如启动、暂停、恢复和终止工作流实例。有关这些操作的更多信息,请参阅使用 DaprWorkflowClient 进行工作流管理操作。
将服务注入到工作流活动中
工作流活动支持开发者对现代 C# 应用程序所期望的相同依赖注入方式。假设在启动时进行了正确的注册,任何此类类型都可以注入到工作流活动的构造函数中,并在工作流执行期间供其使用。这使得通过注入 ILogger 轻松添加日志记录,或通过注入 DaprClient 或 DaprJobsClient 访问其他 Dapr 构建块变得简单。
internal sealed class SquareNumberActivity : WorkflowActivity<int, int>
{
private readonly ILogger _logger;
public MyActivity(ILogger logger)
{
this._logger = logger;
}
public override Task<int> RunAsync(WorkflowActivityContext context, int input)
{
this._logger.LogInformation("Squaring the value {number}", input);
var result = input * input;
this._logger.LogInformation("Got a result of {squareResult}", result);
return Task.FromResult(result);
}
}
活动任务执行标识符
从 Dapr .NET SDK v1.17.0 开始,WorkflowActivityContext 暴露了一个任务执行标识符,该标识符具有以下特性:
- 对每个活动任务唯一
- 在重试期间保持稳定
这使得它可用于幂等键、任务级状态跟踪和日志关联。
internal sealed class IdempotentActivity : WorkflowActivity<int, int>
{
public override Task<int> RunAsync(WorkflowActivityContext context, int input)
{
var executionId = context.TaskExecutionId;
// 将 executionId 用作幂等键或任务状态键。
return Task.FromResult(input * input);
}
}
在工作流中使用 ILogger
由于工作流必须是确定性的,因此无法向其注入任意服务。例如,如果能够将标准 ILogger 注入到工作流中,并且由于错误需要重放工作流,那么从事件源日志进行的后续重放将导致日志记录额外的操作,这些操作实际上并没有发生第二次或第三次,因为它们的结果是从日志中获取的。这可能会引入大量的混淆。相反,提供了一个重放安全的记录器供在工作流内使用。它只会在工作流首次运行时记录事件,而在重放工作流时不会记录任何内容。
此记录器可以从工作流实例上可用的 WorkflowContext 中存在的方法获取,并且可以像使用 ILogger 实例一样精确使用。
在 .NET SDK 仓库中可以看到演示此功能的完整示例,但下面提供了该示例的简要摘录。
public class OrderProcessingWorkflow : Workflow<OrderPayload, OrderResult>
{
public override async Task<OrderResult> RunAsync(WorkflowContext context, OrderPayload order)
{
string orderId = context.InstanceId;
var logger = context.CreateReplaySafeLogger<OrderProcessingWorkflow>(); // 使用此方法访问记录器实例
logger.LogInformation("Received order {orderId} for {quantity} {name} at ${totalCost}", orderId, order.Quantity, order.Name, order.TotalCost);
//...
}
}
后续步骤
2.1.2.2 - .NET SDK 中的工作流序列化
概述
从 Dapr .NET SDK v1.17.0 开始,Dapr.Workflow 支持可插拔序列化。SDK 默认继续使用 System.Text.Json,但现在您可以:
- 覆盖默认的
System.Text.Json设置。 - 注册自定义序列化器(例如,MessagePack 或 BSON)。
序列化配置完全在客户端进行,不需要特定版本的 Dapr 运行时。
注意
此功能需要 Dapr .NET SDK v1.17.0 或更高版本。兼容性和重大变更
警告
更改序列化可能是现有工作流的重大变更。序列化实现之间没有支持的迁移路径。
所有 Dapr SDK 默认使用标准 JSON 约定。如果您在 .NET 工作流和活动中更改序列化设置或切换到自定义序列化器,跨 SDK 工作流可能会失败,因为其他 SDK 可能不支持您的自定义序列化格式。
默认 JSON 序列化
默认情况下,.NET SDK 使用 System.Text.Json 并采用 JsonSerializerDefaults.Web(请参阅
JsonSerializerDefaults.Web 参考)。
这意味着:
- 属性名称不区分大小写。
- 属性名称使用 “camelCase” 格式。
- 读取时允许带引号的数字(数字属性的 JSON 字符串)。
此默认约定旨在与其他 Dapr 语言 SDK 兼容,以支持多应用工作流。
覆盖 System.Text.Json 默认值
要覆盖默认 JSON 设置,请使用工作流构建器注册工作流客户端,以便您可以提供自定义的 JsonSerializerOptions:
builder.Services
.AddDaprWorkflowBuilder(options =>
{
options.RegisterWorkflow<MyWorkflow>();
options.RegisterActivity<MyActivity>();
})
.WithJsonSerializer(new JsonSerializerOptions { PropertyNamingPolicy = null });
从 DI 解析的所有 DaprWorkflowClient 实例都将使用提供的 JsonSerializerOptions 来处理工作流和活动负载。
自定义序列化提供程序
自定义序列化器必须实现 IWorkflowSerializer 接口。以下示例展示了基于 MessagePack 的实现,该实现将数据编码为 Base64 字符串进行传输:
public sealed class MessagePackWorkflowSerializer : IWorkflowSerializer
{
private readonly MessagePackSerializerOptions _options;
public MessagePackWorkflowSerializer(MessagePackSerializerOptions options)
{
_options = options;
}
/// <inheritdoc/>
public string Serialize(object? value, Type? inputType = null)
{
if (value == null)
{
return string.Empty;
}
var targetType = inputType ?? value.GetType();
var bytes = MessagePackSerializer.Serialize(targetType, value, _options);
return Convert.ToBase64String(bytes);
}
/// <inheritdoc/>
public T? Deserialize<T>(string? data)
{
return (T?)Deserialize(data, typeof(T));
}
/// <inheritdoc/>
public object? Deserialize(string? data, Type returnType)
{
if (returnType == null)
{
throw new ArgumentNullException(nameof(returnType));
}
if (string.IsNullOrEmpty(data))
{
return default;
}
try
{
var bytes = Convert.FromBase64String(data);
return MessagePackSerializer.Deserialize(returnType, bytes, _options);
}
catch (FormatException ex)
{
throw new InvalidOperationException(
"Failed to decode Base64 data. The input may not be valid MessagePack-serialized data.",
ex);
}
catch (MessagePackSerializationException ex)
{
throw new InvalidOperationException(
$"Failed to deserialize data to type {returnType.FullName}.",
ex);
}
}
}
注册自定义序列化器
使用工作流构建器注册序列化器:
builder.Services
.AddDaprWorkflowBuilder(options =>
{
options.RegisterWorkflow<MyWorkflow>();
options.RegisterActivity<MyActivity>();
})
.WithSerializer(new MessagePackWorkflowSerializer(MessagePackSerializerOptions.Standard));
如果需要 DI 提供的配置,请使用接收 IServiceProvider 的重载:
builder.Services
.AddDaprWorkflowBuilder(options =>
{
options.RegisterWorkflow<MyWorkflow>();
options.RegisterActivity<MyActivity>();
})
.WithSerializer(serviceProvider =>
{
var options = serviceProvider
.GetRequiredService<IOptions<MessagePackSerializerOptions>>()
.Value;
return new MessagePackWorkflowSerializer(options);
});
2.1.2.3 - .NET SDK 中的多应用程序工作流
概述
Dapr 工作流可以调用托管在不同 Dapr 应用程序中的活动或子工作流。在 .NET 中,多应用程序工作流的支持从以下版本开始:
- Dapr 运行时 v1.16.0+
- Dapr .NET SDK v1.17.0+
概念性指导和约束在 多应用程序工作流 中介绍。
要求
多应用程序工作流调用需要:
- 目标应用 ID 必须存在,并且必须注册你调用的活动或工作流。
- 所有参与的应用 ID 必须位于同一命名空间中。
- 所有参与的应用 ID 必须使用相同的工作流(actor)状态存储。
在另一个应用程序中调用活动
在调用活动时,在 WorkflowTaskOptions 上设置 TargetAppId 以在另一个应用程序中执行它:
public sealed class BusinessWorkflow : Workflow<string, string>
{
public override async Task<string> RunAsync(WorkflowContext context, string input)
{
var options = new WorkflowTaskOptions { TargetAppId = "App2" };
var output = await context.CallActivityAsync<string>(nameof(ActivityA), input, options);
return output;
}
}
父工作流继续在本地进行编排并接收活动结果。
在另一个应用程序中调用子工作流
在调用子工作流时,在 ChildWorkflowTaskOptions 上设置 TargetAppId 以在另一个应用程序中执行它:
public sealed class BusinessWorkflow : Workflow<string, string>
{
public override async Task<string> RunAsync(WorkflowContext context, string input)
{
var options = new ChildWorkflowTaskOptions { TargetAppId = "App2" };
var output = await context.CallChildWorkflowAsync<string>(nameof(Workflow2), input, options);
return output;
}
}
注意
当在另一个应用程序中调用工作流时,你需要使用该应用程序期望的工作流名称。 如果目标应用程序是使用命名工作流版本控制的 .NET 应用程序,你可以通过其规范(未版本化)工作流名称调用它,目标应用程序会将其路由到最新版本。命名工作流版本控制需要 Dapr 运行时 v1.17.0 或更高版本(多应用程序工作流仅需要 v1.16.0+)。后续步骤
2.1.2.4 - 使用 DaprWorkflowClient 进行工作流管理操作
DaprWorkflowClient 管理工作流使用 DaprWorkflowClient 进行工作流管理操作
DaprWorkflowClient 类提供了管理工作流实例的方法。以下是你可以使用 DaprWorkflowClient 执行的操作。
调度新的工作流实例
要启动新的工作流实例,请使用 ScheduleNewWorkflowAsync 方法。此方法需要工作流类型名称和工作流所需的输入参数。工作流的 instanceId 是一个可选参数;如果未提供,DaprWorkflowClient 会生成一个新的 GUID。最后一个可选参数是 DateTimeOffset 类型的 startTime,可用于定义工作流实例应该何时开始。该方法返回已调度工作流的 instanceId,用于其他工作流管理操作。
var instanceId = $"order-workflow-{Guid.NewGuid().ToString()[..8]}";
var input = new Order("Paperclips", 1000, 9.95);
await daprWorkflowClient.ScheduleNewWorkflowAsync(
nameof(OrderProcessingWorkflow),
instanceId,
input);
获取工作流实例的状态
要获取工作流实例的当前状态,请使用 GetWorkflowStateAsync 方法。此方法需要工作流的实例 ID,并返回一个包含工作流当前状态详细信息的 WorkflowStatus 对象。
var workflowStatus = await daprWorkflowClient.GetWorkflowStateAsync(instanceId);
向正在运行的工作流实例发送事件
要向正在等待外部事件的运行中工作流实例发送事件,请使用 RaiseEventAsync 方法。此方法需要工作流的实例 ID、事件名称,以及可选的事件负载。
await daprWorkflowClient.RaiseEventAsync(instanceId, "Approval", true);
暂停正在运行的工作流实例
可以使用 SuspendWorkflowAsync 方法暂停正在运行的工作流实例。此方法需要工作流的实例 ID。你可以选择性地提供暂停工作流的原因。
await daprWorkflowClient.SuspendWorkflowAsync(instanceId);
恢复已暂停的工作流实例
可以使用 ResumeWorkflowAsync 方法恢复已暂停的工作流实例。此方法需要工作流的实例 ID。你可以选择性地提供恢复工作流的原因。
await daprWorkflowClient.ResumeWorkflowAsync(instanceId);
终止工作流实例
要终止工作流实例,请使用 TerminateWorkflowAsync 方法。此方法需要工作流的实例 ID。你可以选择性地提供一个 string 类型的 output 参数。终止工作流实例也会终止所有子工作流实例,但对正在执行的活动没有影响。
await daprWorkflowClient.TerminateWorkflowAsync(instanceId);
清除工作流实例
要从 Dapr Workflow 状态存储中删除工作流实例历史记录,请使用 PurgeWorkflowAsync 方法。此方法需要工作流的实例 ID。只有已完成、失败或已终止的工作流实例才能被清除。
await daprWorkflowClient.PurgeWorkflowAsync(instanceId);
后续步骤
2.1.2.5 - .NET SDK 中的工作流版本控制
概述
Dapr 工作流版本控制允许你演进工作流,而不会中断进行中实例的确定性执行。 .NET SDK 支持两种方法:
- 基于补丁的版本控制:引入由
context.IsPatched("patch-name")守护的条件分支。 - 基于名称的版本控制:创建一个新的工作流类型名称,并让版本控制策略选择最新版本。
对于小型就地更改,使用基于补丁的版本控制。对于需要全新的工作流类型的大型重构,使用基于名称的版本控制。
注意
工作流版本控制需要 Dapr .NET SDK v1.17.0 或更高版本以及 Dapr 运行时 v1.17.0 或更高版本。何时使用每种方法
基于补丁的版本控制 适用于以下情况:
- 你需要在现有工作流中进行小型、增量式的更改。
- 你希望现有实例在部署后保持确定性行为。
- 你希望暂时避免引入新的工作流类型。
基于名称的版本控制 适用于以下情况:
- 你想要一个没有累积补丁的全新工作流类型。
- 你准备好删除旧的补丁块并更自由地重构。
- 你希望基于命名约定自动选择版本。
基于补丁的版本控制
基于补丁的版本控制依赖于工作流内部的确定性开关。使用 WorkflowContext.IsPatched 来守护新行为:
public override async Task RunAsync(WorkflowContext context, OrderPayload input)
{
await context.CallActivityAsync(nameof(ReserveInventoryActivity), input);
if (context.IsPatched("v2"))
{
await context.CallActivityAsync(nameof(ChargePaymentActivityV2), input);
}
else
{
await context.CallActivityAsync(nameof(ChargePaymentActivity), input);
}
}
补丁规则
- 补丁名称可以在同一工作流中多次出现,并且可以嵌套。
- 补丁名称在部署中必须唯一。例如,如果你部署了一个带有补丁名称
"v1"的工作流, - 你不得在后续编辑中重复使用
"v1"。请使用新的标识符(如"v2")以避免非确定性行为。 IsPatched在WorkflowContext上可用;无需额外设置。
提示
保持补丁名称简单且单调(例如,"v2"、"v3"),以便清楚了解每次部署引入的更改。基于名称的版本控制
基于名称的版本控制允许你通过更改工作流类型名称来创建新版本的工作流。推荐的模式是将现有工作流复制到新文件中,重命名类,根据需要进行重构,然后在必要时再次开始打补丁。
例如,如果你有 OrderWorkflow,则创建 OrderWorkflowV2 并重构它。较旧的版本可以保留给进行中的实例,而新实例使用最新版本。
默认命名行为
默认情况下,基于名称的版本控制使用内置的 NumericVersionStrategy 和数字后缀。以下都是有效示例:
MyWorkflow(被视为版本0)MyWorkflow2MyWorkflowV2
默认策略假设较高的数字值是较新的版本(例如,MyWorkflowV10 比 MyWorkflowV2 更新)。.NET SDK 还包含其他内置策略(Date、SemVer 和 Numeric)以及对自定义策略的支持。
内置策略和选项
.NET SDK 附带了几个内置的基于名称的策略。每个策略都支持允许你调整如何解析后缀以及当没有后缀时该如何处理的选项。
- DateVersionStrategy:从尾随后缀派生基于日期的版本(例如,
MyWorkflow20220611)。 选项包括:- 日期格式:使用标准 C# 日期格式规则;默认为
yyyyMMdd。 - 默认版本:当未提供后缀时使用;默认为
0。 - 前缀:在日期后缀之前匹配的可选前缀,带有可选区分大小写。
- 日期格式:使用标准 C# 日期格式规则;默认为
- SemVerVersionStrategy:从尾随后缀派生 SemVer 版本(例如,
MyWorkflow1.2.3)。 选项包括:- 前缀:在 SemVer 后缀之前匹配的可选前缀,带有可选区分大小写。
- 预发布/生成支持:可以解析预发布注释和生成元数据。
- 默认版本:当未提供后缀时的可选默认值(如果配置为允许缺少后缀)。
- NumericVersionStrategy:从尾随后缀派生数字版本(例如,
MyWorkflow42或MyWorkflowV42)。 选项包括:- 前缀:在数字后缀之前匹配的可选前缀,带有可选区分大小写。
- 零填充宽度:可选宽度,允许使用带前导零的固宽数字。
- 默认版本:当未提供后缀时使用。
配置基于名称的版本控制
1. 安装版本控制包
将 Dapr.Workflow.Versioning 包添加到你的项目中。
2. 注册工作流版本控制
在启动期间将版本控制添加到 DI:
builder.Services.AddDaprWorkflowVersioning();
3. 选择策略(可选)
你可以在调用 AddDaprWorkflowVersioning 后在 DI 中注册策略来选择:
builder.Services.UseDefaultWorkflowStrategy<NumericVersionStrategy>("workflow-versioning-options");
可选字符串键用于定位策略选项。
4. 配置策略选项(可选)
使用相同的键注册策略选项:
builder.Services.ConfigureStrategyOptions<NumericVersionStrategyOptions>("workflow-versioning-options", o =>
{
o.SuffixPrefix = "V";
});
注意
选项键字符串必须完全匹配,否则选项将不会应用。5. 注册活动
活动照常注册。使用基于名称的版本控制时不需要注册工作流,因为源生成器会在构建时自动发现它们。
builder.Services.AddDaprWorkflow(w =>
{
w.RegisterActivity<SendEmailActivity>();
});
配置完成后,命名工作流版本控制将在运行时自动应用。
跨程序集工作流发现
默认情况下,工作流版本控制源生成器仅扫描执行的程序集。如果你将工作流保留在单独的引用程序集中,则除非你选择加入引用扫描,否则不会发现这些实现。
默认情况下禁用引用扫描,因为它可能会增加构建时间(生成器必须检查所有引用的程序集以查找 Workflow<,> 实现)。要启用它,请将以下内容添加到执行应用程序的 .csproj 文件中:
<ItemGroup>
<CompilerVisibleProperty Include="DaprWorkflowVersioningScanReferences" />
</ItemGroup>
启用后,源生成器会将引用程序集中发现的任何工作流实现添加到用于版本跟踪的内部注册表中。
覆盖名称和版本
如果你需要覆盖从工作流类型检测到的规范名称或版本,请将 [WorkflowVersion] 属性应用于实现工作流的类,并显式指定值。
最佳实践
- 按规范名称调度:调度新的工作流实例时,使用规范工作流名称(未版本化的名称)。SDK 会自动发现版本化类型并将规范名称映射到最新版本。避免使用特定的版本化名称进行调度。
- 保留旧版本:无限期保留较旧的工作流类型。只有在确定没有进行中的实例引用它们时才能删除它们。过早删除旧版本可能会导致长时间运行的工作流停滞。
- 单向版本控制:始终向前推进版本。避免重命名或重复使用较旧的版本标识符。
当前限制
- 单个项目不能为不同的工作流混合使用多个版本控制策略。
- 没有从一个版本控制策略到另一个策略的自动迁移。
- 工作流版本控制不能跨应用程序边界工作。如果你使用多应用工作流,必须根据目标应用的任何版本控制策略使用该应用期望的类型名称。调用此 .NET 应用程序的其他应用程序如果设置为使用基于名称的工作流版本控制,则可以简单地使用规范名称。
示例项目
端到端示例可在 Dapr .NET SDK 仓库的 examples/Workflow/WorkflowVersioning 中找到。
后续步骤
2.1.2.6 - .NET 工作流示例
Dapr Quickstarts 仓库中的工作流教程
GitHub 上的 Dapr Quickstarts 仓库包含许多工作流教程,展示各种工作流模式以及如何使用工作流管理操作。你可以在 quickstarts/tutorials/workflow/csharp 文件夹中找到这些教程。
.NET SDK 仓库中的工作流示例
GitHub 上的 Dapr .NET SDK 仓库包含多个示例,演示如何将 Dapr 工作流与 .NET 结合使用。你可以在 examples/Workflow 文件夹中找到这些示例。
后续步骤
2.1.3 - Dapr Actors .NET SDK
通过 Dapr Actor 包,您可以从 .NET 应用程序与 Dapr 虚拟 Actor 进行交互。
要开始使用,请参阅 Dapr Actors 操作指南。
2.1.3.1 - IActorProxyFactory 接口
在 Actor 类或 ASP.NET Core 项目中,建议使用 IActorProxyFactory 接口来创建 actor 客户端。
AddActors(...) 方法会将 actor 服务注册到 ASP.NET Core 依赖注入中。
- 在 actor 实例外部:
IActorProxyFactory实例作为单例服务通过依赖注入可用。 - 在 actor 实例内部:
IActorProxyFactory实例作为属性(this.ProxyFactory)可用。
以下是在 actor 内部创建代理的示例:
public Task<MyData> GetDataAsync()
{
var proxy = this.ProxyFactory.CreateActorProxy<IOtherActor>(ActorId.CreateRandom(), "OtherActor");
await proxy.DoSomethingGreat();
return this.StateManager.GetStateAsync<MyData>("my_data");
}
在本指南中,你将学习如何使用 IActorProxyFactory。
提示
对于非依赖注入的应用程序,你可以使用ActorProxy 上的静态方法。由于 ActorProxy 方法容易出错,在配置自定义设置时尽量避免使用它们。标识 actor
IActorProxyFactory 上的所有 API 都需要 actor 类型 和 actor id 才能与 actor 通信。对于强类型客户端,你还需要它的接口之一。
- Actor 类型 在整个应用程序中唯一标识 actor 实现。
- Actor id 唯一标识该类型的一个实例。
如果你没有 actor id 并且想要与一个新实例通信,可以使用 ActorId.CreateRandom() 创建一个随机 id。由于随机 id 是一个加密强度高的标识符,当你与它交互时,运行时会创建一个新的 actor 实例。
你可以使用 ActorReference 类型与其他 actor 交换 actor 类型和 actor id,作为消息的一部分。
两种 actor 客户端风格
actor 客户端支持两种不同的调用风格:
| Actor 客户端风格 | 描述 |
|---|---|
| 强类型 | 强类型客户端基于 .NET 接口,提供强类型的典型好处。它们不适用于非 .NET actor。 |
| 弱类型 | 弱类型客户端使用 ActorProxy 类。建议仅在需要互操作性或其他高级原因时使用这些客户端。 |
使用强类型客户端
以下示例使用 CreateActorProxy<> 方法创建强类型客户端。CreateActorProxy<> 需要一个 actor 接口类型,并将返回该接口的一个实例。
// 为 IOtherActor 创建一个代理,类型为 OtherActor,具有随机 id
var proxy = this.ProxyFactory.CreateActorProxy<IOtherActor>(ActorId.CreateRandom(), "OtherActor");
// 调用接口定义的方法来调用 actor
//
// proxy 是 IOtherActor 的实现,所以我们可以直接调用其方法
await proxy.DoSomethingGreat();
使用弱类型客户端
以下示例使用 Create 方法创建弱类型客户端。Create 返回 ActorProxy 的一个实例。
// 为类型 OtherActor 创建一个代理,具有随机 id
var proxy = this.ProxyFactory.Create(ActorId.CreateRandom(), "OtherActor");
// 按名称调用方法来调用 actor
//
// proxy 是 ActorProxy 的一个实例
await proxy.InvokeMethodAsync("DoSomethingGreat");
由于 ActorProxy 是弱类型代理,你需要将 actor 方法名称作为字符串传入。
你还可以使用 ActorProxy 调用具有请求和响应消息的方法。请求和响应消息将使用 System.Text.Json 序列化器进行序列化。
// 为类型 OtherActor 创建一个代理,具有随机 id
var proxy = this.ProxyFactory.Create(ActorId.CreateRandom(), "OtherActor");
// 调用代理上的方法来调用 actor
//
// proxy 是 ActorProxy 的一个实例
var request = new MyRequest() { Message = "Hi, it's me.", };
var response = await proxy.InvokeMethodAsync<MyRequest, MyResponse>("DoSomethingGreat", request);
使用弱类型代理时,你_必须_主动定义正确的 actor 方法名称和消息类型。使用强类型代理时,这些名称和类型作为接口定义的一部分为你定义。
Actor 方法调用异常详细信息
actor 方法调用异常详细信息会向调用方和被调用方公开,提供追踪问题的入口点。异常详细信息包括:
- 方法名称
- 行号
- 异常类型
- UUID
你使用 UUID 在调用方和被调用方之间匹配异常。以下是异常详细信息的示例:
Dapr.Actors.ActorMethodInvocationException: Remote Actor Method Exception, DETAILS: Exception: NotImplementedException, Method Name: ExceptionExample, Line Number: 14, Exception uuid: d291a006-84d5-42c4-b39e-d6300e9ac38b
后续步骤
2.1.3.2 - Author & run actors
创建 actor
ActorHost
ActorHost:
- 是所有 actor 必需的构造函数参数
- 由运行时提供
- 必须传递给基类构造函数
- 包含允许该 actor 实例与运行时通信的所有状态
internal class MyActor : Actor, IMyActor, IRemindable
{
public MyActor(ActorHost host) // Accept ActorHost in the constructor
: base(host) // Pass ActorHost to the base class constructor
{
}
}
由于 ActorHost 包含 actor 独有的状态,你无需将实例传递到代码的其他部分。建议仅在测试中创建自己的 ActorHost 实例。
依赖注入
Actor 支持将额外参数依赖注入到构造函数中。你定义的任何其他参数都将从依赖注入容器中获取其值。
internal class MyActor : Actor, IMyActor, IRemindable
{
public MyActor(ActorHost host, BankService bank) // Accept BankService in the constructor
: base(host)
{
...
}
}
actor 类型应具有单个 public 构造函数。actor 基础设施使用 ActivatorUtilities 模式来构造 actor 实例。
你可以在 Startup.cs 中注册类型以使其可用于依赖注入。阅读更多关于注册类型的不同方式。
// In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
...
// Register additional types with dependency injection.
services.AddSingleton<BankService>();
}
每个 actor 实例都有自己的依赖注入作用域,并在执行操作后的段时间内保留在内存中。在此期间,与 actor 关联的依赖注入作用域也被视为活动状态。当 actor 被停用时,该作用域将被释放。
如果 actor 在构造函数中注入了 IServiceProvider,则 actor 将接收对其作用域关联的 IServiceProvider 的引用。IServiceProvider 可用于在未来动态解析服务。
internal class MyActor : Actor, IMyActor, IRemindable
{
public MyActor(ActorHost host, IServiceProvider services) // Accept IServiceProvider in the constructor
: base(host)
{
...
}
}
使用此模式时,避免创建许多实现 IDisposable 的瞬态服务实例。由于与 actor 关联的作用域可能在较长时间内被视为有效,因此可能会在内存中累积许多服务。有关更多信息,请参阅依赖注入指南。
IDisposable 和 actor
Actor 可以实现 IDisposable 或 IAsyncDisposable。建议依赖依赖注入进行资源管理,而不是在应用程序代码中实现 dispose 功能。在确实必要的罕见情况下才提供 dispose 支持。
日志记录
在 actor 类中,你可以通过基 Actor 类上的属性访问 ILogger 实例。此实例连接到 ASP.NET Core 日志系统,应用于 actor 内的所有日志记录。阅读更多关于日志记录。你可以配置多种不同的日志格式和输出接收器。
使用带有_命名占位符_的_结构化日志记录_,如下所示:
public Task<MyData> GetDataAsync()
{
this.Logger.LogInformation("Getting state at {CurrentTime}", DateTime.UtcNow);
return this.StateManager.GetStateAsync<MyData>("my_data");
}
记录日志时,避免使用格式字符串,如:$"Getting state at {DateTime.UtcNow}"
日志记录应使用命名占位符语法,它提供更好的性能和与日志系统的集成。
使用显式 actor 类型名称
默认情况下,客户端看到的 actor 类型_派生自 actor 实现类的_名称。默认名称将是类名(不带命名空间)。
如果需要,可以通过将 ActorAttribute 属性附加到 actor 实现类来指定显式类型名称。
[Actor(TypeName = "MyCustomActorTypeName")]
internal class MyActor : Actor, IMyActor
{
// ...
}
在上面的示例中,名称将是 MyCustomActorTypeName。
无需更改向运行时注册 actor 类型的代码,通过属性提供值就是所需的全部。
在服务器上托管 actor
注册 actor
Actor 注册是 Startup.cs 中 ConfigureServices 的一部分。你可以通过 ConfigureServices 方法注册具有依赖注入的服务。注册 actor 类型集是 actor 服务注册的一部分。
在 ConfigureServices 内部,你可以:
- 注册 actor 运行时(
AddActors) - 注册 actor 类型(
options.Actors.RegisterActor<>) - 配置 actor 运行时设置
options - 注册额外的服务类型以注入到 actor 的依赖注入中(
services)
// In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Register actor runtime with DI
services.AddActors(options =>
{
// Register actor types and configure actor settings
options.Actors.RegisterActor<MyActor>();
// Configure default settings
options.ActorIdleTimeout = TimeSpan.FromMinutes(10);
options.ActorScanInterval = TimeSpan.FromSeconds(35);
options.DrainOngoingCallTimeout = TimeSpan.FromSeconds(35);
options.DrainRebalancedActors = true;
});
// Register additional services for use with actors
services.AddSingleton<BankService>();
}
配置 JSON 选项
actor 运行时使用 System.Text.Json 进行:
- 将数据序列化到状态存储
- 处理来自弱类型客户端的请求
默认情况下,actor 运行时使用基于 JsonSerializerDefaults.Web 的设置。
你可以作为 ConfigureServices 的一部分配置 JsonSerializerOptions:
// In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddActors(options =>
{
...
// Customize JSON options
options.JsonSerializerOptions = ...
});
}
Actor 和路由
ASP.NET Core 对 actor 的托管支持使用终结点路由系统。.NET SDK 不支持使用早期 ASP.NET Core 版本中的旧路由系统托管 actor。
由于 actor 使用终结点路由,actor HTTP 处理程序是中间件管道的一部分。以下是设置带有 actor 的中间件管道的 Configure 方法的最小示例。
// in Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
// Register actors handlers that interface with the Dapr runtime.
endpoints.MapActorsHandlers();
});
}
UseRouting 和 UseEndpoints 调用是配置路由所必需的。通过在终结点中间件内添加 MapActorsHandlers 将 actor 配置为管道的一部分。
这是一个最小示例,Actor 功能与以下功能并存是有效的:
- Controllers
- Razor Pages
- Blazor
- gRPC Services
- Dapr pub/sub handler
- 其他终结点,如运行状况检查
有问题的中间件
某些中间件可能会干扰 Dapr 请求到 actor 处理程序的路由。特别是,UseHttpsRedirection 对 Dapr 的默认配置有问题。Dapr 默认情况下通过未加密的 HTTP 发送请求,UseHttpsRedirection 中间件将阻止这些请求。此中间件目前不能与 Dapr 一起使用。
// in Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// INVALID - this will block non-HTTPS requests
app.UseHttpsRedirection();
// INVALID - this will block non-HTTPS requests
app.UseRouting();
app.UseEndpoints(endpoints =>
{
// Register actors handlers that interface with the Dapr runtime.
endpoints.MapActorsHandlers();
});
}
后续步骤
2.1.3.3 - .NET SDK 中的 Actor 序列化
Actor 序列化
Dapr actor 包使您能够在 .NET 应用程序中使用弱类型或强类型客户端来使用 Dapr virtual actors。每种方式使用不同的序列化方法。本文档将回顾这些差异,并传达一些关键的基本规则,以便在任何一种情况下都能理解。
请注意,由于这些不同的序列化方法,不支持可互换地使用弱类型或强类型 actor 客户端。使用一个 Actor 客户端持久化的数据将无法使用另一个 Actor 客户端访问,因此重要的是选择一个并在整个应用程序中一致地使用它。
弱类型 Dapr Actor 客户端
在本节中,您将学习如何配置 C# 类型,以便在使用弱类型 actor 客户端时在运行时正确序列化和反序列化。这些客户端使用基于字符串的方法名称,以及使用 System.Text.Json 序列化程序序列化的请求和响应负载。请注意,此序列化框架并非特定于 Dapr,而是由 .NET 团队在 .NET GitHub 仓库 中单独维护。
当使用弱类型 Dapr Actor 客户端从各种 actor 调用方法时,不需要独立序列化或反序列化方法负载,因为 SDK 将代表您透明地执行此操作。
客户端将针对您构建的 .NET 版本可用的最新版本的 System.Text.Json,并且序列化遵循相关 .NET 文档中提供的所有固有功能。
序列化程序将配置为使用 JsonSerializerOptions.Web 默认选项,除非使用自定义选项配置覆盖,这意味着应用以下内容:
- 属性名称的反序列化以不区分大小写的方式执行
- 属性名称的序列化使用 camel casing 执行,除非属性被
[JsonPropertyName]属性覆盖 - 反序列化将从数字和/或字符串值中读取数值
基本序列化
在以下示例中,我们展示了一个名为 Doodad 的简单类,但它也可以是一个记录。
public class Doodad
{
public Guid Id { get; set; }
public string Name { get; set; }
public int Count { get; set; }
}
默认情况下,这将使用类型中使用的成员名称以及其实例化的任何值进行序列化:
{"id": "a06ced64-4f42-48ad-84dd-46ae6a7e333d", "name": "DoodadName", "count": 5}
覆盖序列化属性名称
可以通过将 [JsonPropertyName] 属性应用于所需的属性来覆盖默认属性名称。
通常,对于您持久化到 actor 状态的类型,这不是必需的,因为您不打算独立于 Dapr 相关功能读取或写入它们,但以下内容仅是为了清楚地说明这是可能的。
覆盖类上的属性名称
以下示例演示了使用 JsonPropertyName 更改序列化后第一个属性的名称。请注意,Count 属性上最后一次使用 JsonPropertyName 与预期序列化的名称相匹配。这主要是为了证明应用此属性不会产生任何负面影响——事实上,如果您稍后决定更改默认序列化选项但仍需要在更改之前一致地访问以前序列化的属性,这可能是更可取的,因为 JsonPropertyName 将覆盖这些选项。
public class Doodad
{
[JsonPropertyName("identifier")]
public Guid Id { get; set; }
public string Name { get; set; }
[JsonPropertyName("count")]
public int Count { get; set; }
}
这将序列化为以下内容:
{"identifier": "a06ced64-4f42-48ad-84dd-46ae6a7e333d", "name": "DoodadName", "count": 5}
覆盖记录上的属性名称
让我们尝试对 C# 12 或更高版本的记录执行相同的操作:
public record Thingy(string Name, [JsonPropertyName("count")] int Count);
由于在主构造函数中传递的参数(在 C# 12 中引入)可以应用于记录中的属性或字段,因此在某些不明确的情况下,使用 [JsonPropertyName] 属性可能需要指定您打算将属性应用于属性而不是字段。如果有必要,您可以在主构造函数中这样指示:
public record Thingy(string Name, [property: JsonPropertyName("count")] int Count);
如果在不需要的情况下将 [property: ] 应用于 [JsonPropertyName] 属性,则不会对序列化或反序列化产生负面影响,因为操作将正常进行,就好像它是一个属性(通常情况下,如果没有这样标记)。
枚举类型
枚举(包括平面枚举)可以序列化为 JSON,但持久化的值可能会让您感到惊讶。同样,开发人员不应该独立于 Dapr 处理序列化数据,但以下信息可能至少有助于诊断为什么看似微小的版本迁移无法按预期工作。
采用以下 enum 类型提供一年中的各个季节:
public enum Season
{
Spring,
Summer,
Fall,
Winter
}
我们将继续使用一个单独的演示类型来引用我们的 Season,并同时演示它如何与记录一起使用:
public record Engagement(string Name, Season TimeOfYear);
给定以下初始化实例:
var myEngagement = new Engagement("Ski Trip", Season.Winter);
这将序列化为以下 JSON:
{"name": "Ski Trip", "season": 3}
我们的 Season.Winter 值表示为 3 可能是出乎意料的,但这是因为序列化程序将自动使用枚举值的数字表示形式,第一个值从零开始,并递增每个可用附加值的数值。同样,如果正在进行迁移并且开发人员翻转了枚举的顺序,这将对您的解决方案产生重大更改,因为在反序列化时序列化的数值将指向不同的值。
相反,System.Text.Json 提供了一个 JsonConverter,它将选择使用基于字符串的值而不是数值。需要将 [JsonConverter] 属性应用于枚举类型本身以启用此功能,但随后将在任何引用枚举的下游序列化或反序列化操作中实现。
[JsonConverter(typeof(JsonStringEnumConverter<Season>))]
public enum Season
{
Spring,
Summer,
Fall,
Winter
}
使用上面 myEngagement 实例中的相同值,这将生成以下 JSON:
{"name": "Ski Trip", "season": "Winter"}
因此,可以移动枚举成员,而无需担心在反序列化期间引入错误。
自定义枚举值
System.Text.Json 序列化平台开箱即用,不支持使用 [EnumMember] 来允许您更改序列化或反序列化期间使用的枚举值,但在某些情况下,这可能很有用。同样,假设您负责重构解决方案,以便为各种枚举应用更好的名称。您使用了上面详述的 JsonStringEnumConverter<TType>,因此您将枚举的名称保存为值而不是数值,但如果您更改枚举名称,这将引入重大更改,因为该名称将不再与状态中的名称匹配。
请注意,如果您选择使用此方法,应该用 [EnumMember] 属性装饰所有枚举成员,以便为每个枚举值一致地应用值,而不是杂乱无章。没有任何东西会在构建或运行时验证这一点,但它被视为最佳实践操作。
在这种情况下,如何指定持久化的精确值,同时更改枚举成员的名称?使用自定义 JsonConverter 和扩展方法,该方法可以在提供的情况下从附加的 [EnumMember] 属性中提取值。将以下内容添加到您的解决方案中:
public sealed class EnumMemberJsonConverter<T> : JsonConverter<T> where T : struct, Enum
{
/// <summary>读取并将 JSON 转换为类型 <typeparamref name="T" />。</summary>
/// <param name="reader">读取器。</param>
/// <param name="typeToConvert">要转换的类型。</param>
/// <param name="options">指定要使用的序列化选项的对象。</param>
/// <returns>转换后的值。</returns>
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// 从 JSON 读取器获取字符串值
var value = reader.GetString();
// 遍历所有枚举值
foreach (var enumValue in Enum.GetValues<T>())
{
// 从 EnumMember 属性获取值(如果有)
var enumMemberValue = GetValueFromEnumMember(enumValue);
// 如果值匹配,则返回枚举值
if (value == enumMemberValue)
{
return enumValue;
}
}
// 如果未找到匹配项,则引发异常
throw new JsonException($"Invalid value for {typeToConvert.Name}: {value}");
}
/// <summary>将指定值写入 JSON。</summary>
/// <param name="writer">要写入的写入器。</param>
/// <param name="value">要转换为 JSON 的值。</param>
/// <param name="options">指定要使用的序列化选项的对象。</param>
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
// 从 EnumMember 属性获取值(如果有)
var enumMemberValue = GetValueFromEnumMember(value);
// 将值写入 JSON 写入器
writer.WriteStringValue(enumMemberValue);
}
private static string GetValueFromEnumMember(T value)
{
MemberInfo[] member = typeof(T).GetMember(value.ToString(), BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.Public);
if (member.Length == 0)
return value.ToString();
object[] customAttributes = member.GetCustomAttributes(typeof(EnumMemberAttribute), false);
if (customAttributes.Length != 0)
{
EnumMemberAttribute enumMemberAttribute = (EnumMemberAttribute)customAttributes;
if (enumMemberAttribute != null && enumMemberAttribute.Value != null)
return enumMemberAttribute.Value;
}
return value.ToString();
}
}
现在让我们添加一个示例枚举器。我们将设置一个值,该值使用每个枚举成员的小写版本来演示这一点。不要忘记用 JsonConverter 属性装饰枚举,并引用我们的自定义转换器,而不是上一节中使用的数字到字符串转换器。
[JsonConverter(typeof(EnumMemberJsonConverter<Season>))]
public enum Season
{
[EnumMember(Value="spring")]
Spring,
[EnumMember(Value="summer")]
Summer,
[EnumMember(Value="fall")]
Fall,
[EnumMember(Value="winter")]
Winter
}
让我们使用之前的示例记录。我们还将添加一个 [JsonPropertyName] 属性只是为了增强演示:
public record Engagement([property: JsonPropertyName("event")] string Name, Season TimeOfYear);
最后,让我们初始化一个新的实例:
var myEngagement = new Engagement("Conference", Season.Fall);
这一次,序列化将考虑来自附加的 [EnumMember] 属性的值,为我们提供了一种重构应用程序的机制,而不需要对状态中现有枚举值进行复杂的版本控制方案。
{"event": "Conference", "season": "fall"}
多态序列化
在 Dapr Actor 客户端中使用多态类型时,必须正确处理序列化和反序列化,以确保实例化适当的派生类型。多态序列化允许您序列化基类型的对象,同时保留特定的派生类型信息。
要启用多态反序列化,必须在基类型上使用 [JsonPolymorphic] 属性。此外,至关重要的是包含 [AllowOutOfOrderMetadataProperties] 属性,以确保元数据属性(如 $type)可以被 System.Text.Json 正确处理,即使它们不是 JSON 对象中的第一个属性。
示例
[JsonPolymorphic]
[AllowOutOfOrderMetadataProperties]
public abstract class SampleValueBase
{
public string CommonProperty { get; set; }
}
public class DerivedSampleValue : SampleValueBase
{
public string SpecificProperty { get; set; }
}
在此示例中,SampleValueBase 类标记了 [JsonPolymorphic] 和 [AllowOutOfOrderMetadataProperties] 属性。此设置确保 $type 元数据属性可以在反序列化期间正确识别和处理,无论它在 JSON 对象中的位置如何。
通过遵循此方法,您可以在 Dapr Actor 客户端中有效地管理多态序列化和反序列化,确保实例化和使用正确的派生类型。
强类型 Dapr Actor 客户端
在本节中,您将学习如何配置类和记录,以便在使用强类型 actor 客户端时在运行时正确序列化和反序列化。这些客户端使用 .NET 接口实现,与使用其他语言编写的 Dapr Actor 不兼容。
此 actor 客户端使用名为 Data Contract Serializer 的序列化引擎序列化数据,该引擎将 C# 类型转换为 XML 文档并从中转换。此序列化框架并非特定于 Dapr,而是由 .NET 团队在 .NET GitHub 仓库 中单独维护。
发送或接收基元类型(如字符串或整数)时,此序列化会透明地发生,您无需进行任何必要的准备。但是,在处理您自己创建的复杂类型时,需要考虑一些重要规则,以便此过程顺利进行。
可序列化类型
使用 Data Contract Serializer 时,有几个重要的注意事项需要牢记:
- 默认情况下,所有类型、读/写属性(构造后)和标记为公开可见的字段都会被序列化
- 所有类型必须公开公共无参构造函数或使用 DataContractAttribute 属性进行装饰
- 仅支持使用 DataContractAttribute 属性的 Init-only 设置器
- 只读字段、没有 Get 和 Set 方法的属性以及具有私有 Get 和 Set 方法的内部或私有属性在序列化期间将被忽略
- 支持对使用本身未标记 DataContractAttribute 属性的其他复杂类型的类型进行序列化,通过使用 KnownTypesAttribute 属性
- 如果类型标记了 DataContractAttribute 属性,则您希望序列化和反序列化的所有成员也必须使用 DataMemberAttribute 属性进行装饰,否则将设置为其默认值
反序列化如何工作?
反序列化使用的方法取决于类型是否使用 DataContractAttribute 属性进行装饰。如果不存在此属性,则使用无参构造函数创建类型的实例。然后,每个属性和字段使用其各自的设置器映射到类型中,并将实例返回给调用者。
如果类型标记了 [DataContract],序列化程序将改为使用反射读取类型的元数据,并根据是否使用 DataMemberAttribute 属性标记来确定应包含哪些属性或字段,因为它是基于选择性加入的方式执行的。然后,它在内存中分配一个未初始化的对象(避免使用任何构造函数,无论是否有参数),然后直接在每个映射的属性或字段上设置值,即使是私有的或使用 init-only 设置器。在此过程中,将根据情况调用序列化回调,然后将对象返回给调用者。
强烈建议使用序列化属性,因为它们提供了更大的灵活性来覆盖名称和命名空间,并且通常使用更多的现代 C# 功能。虽然默认序列化程序可以用于基元类型,但不建议将其用于您自己的任何类型,无论是类、结构体还是记录。如果您使用 DataContractAttribute 属性装饰类型,还建议显式使用 DataMemberAttribute 属性装饰要序列化或反序列化的每个成员。
.NET 类
类在 Data Contract Serializer 中得到完全支持,前提是还遵循本文档和 Data Contract Serializer 文档中详述的其他规则。
这里要记住的最重要的事情是,您必须拥有公共无参构造函数,或者必须使用适当的属性对其进行装饰。让我们查看一些示例来真正阐明什么可行,什么不可行。
在以下示例中,我们展示了一个名为 Doodad 的简单类。这里我们没有提供显式构造函数,因此编译器将提供默认的无参构造函数。因为我们使用的是 支持的基元类型(Guid、string 和 int32),并且我们的所有成员都具有公共 getter 和 setter,所以不需要任何属性,并且我们可以毫无问题地在 Dapr actor 方法中发送和接收此类时使用此类。
public class Doodad
{
public Guid Id { get; set; }
public string Name { get; set; }
public int Count { get; set; }
}
默认情况下,这将使用类型中使用的成员名称以及其实例化的任何值进行序列化:
<Doodad>
<Id>a06ced64-4f42-48ad-84dd-46ae6a7e333d</Id>
<Name>DoodadName</Name>
<Count>5</Count>
</Doodad>
所以让我们调整它——让我们添加自己的构造函数,并且只在成员上使用 init-only 设置器。这将无法序列化和反序列化,不是因为使用了 init-only 设置器,而是因为没有无参构造函数。
// 无法正确序列化!
public class Doodad
{
public Doodad(string name, int count)
{
Id = Guid.NewGuid();
Name = name;
Count = count;
}
public Guid Id { get; set; }
public string Name { get; init; }
public int Count { get; init; }
}
如果我们向类型添加公共无参构造函数,我们就可以开始了,这将无需进一步注释即可工作。
public class Doodad
{
public Doodad()
{
}
public Doodad(string name, int count)
{
Id = Guid.NewGuid();
Name = name;
Count = count;
}
public Guid Id { get; set; }
public string Name { get; set; }
public int Count { get; set; }
}
但是如果我们不想添加这个构造函数呢?也许您不希望您的开发人员意外地使用非预期的构造函数创建此 Doodad 的实例。这就是更灵活的属性有用的地方。如果使用 DataContractAttribute 属性装饰类型,则可以删除无参构造函数,它将再次工作。
[DataContract]
public class Doodad
{
public Doodad(string name, int count)
{
Id = Guid.NewGuid();
Name = name;
Count = count;
}
public Guid Id { get; set; }
public string Name { get; set; }
public int Count { get; set; }
}
在上面的示例中,我们不需要使用 DataMemberAttribute 属性,因为同样,我们使用的是序列化程序支持的 内置基元类型。但是,如果我们使用属性,我们会获得更多的灵活性。通过 DataContractAttribute 属性,我们可以使用 Namespace 参数指定我们自己的 XML 命名空间,并通过 Name 参数,在序列化到 XML 文档时更改类型的名称。
建议的做法是将 DataContractAttribute 属性附加到类型,并将 DataMemberAttribute 属性附加到要序列化的所有成员——如果它们不是必需的并且您没有更改默认值,它们将被忽略,但它们为您提供了一种选择序列化本来不会包含的成员的机制,例如标记为私有的成员或本身就是复杂类型或集合的成员。
请注意,如果您选择序列化私有成员,它们的值将被序列化为纯文本——根据您在序列化后处理数据的方式,它们很有可能被查看、拦截并可能被篡改,因此重要的是在您的用例中考虑是否要标记这些成员。
在以下示例中,我们将查看使用属性来更改某些成员的序列化名称,以及引入 IgnoreDataMemberAttribute 属性。顾名思义,这告诉序列化程序跳过此属性,即使它本来符合序列化条件。此外,因为我使用 DataContractAttribute 属性装饰类型,这意味着我可以在属性上使用 init-only 设置器。
[DataContract(Name="Doodad")]
public class Doodad
{
public Doodad(string name = "MyDoodad", int count = 5)
{
Id = Guid.NewGuid();
Name = name;
Count = count;
}
[DataMember(Name = "id")]
public Guid Id { get; init; }
[IgnoreDataMember]
public string Name { get; init; }
[DataMember]
public int Count { get; init; }
}
序列化时,因为我们更改了序列化成员的名称,我们可以期望使用默认值的新 Doodad 实例序列化为:
<Doodad>
<id>a06ced64-4f42-48ad-84dd-46ae6a7e333d</id>
<Count>5</Count>
</Doodad>
C# 12 中的类 - 主构造函数
C# 12 为我们带来了类的主构造函数。使用主构造函数意味着编译器将被阻止创建默认的隐式无参构造函数。虽然类上的主构造函数不会生成任何公共属性,但这意味着如果向此主构造函数传递任何参数或在类中有非基元类型,则要么需要指定自己的无参构造函数,要么使用序列化属性。
这是一个示例,我们使用主构造函数将 ILogger 注入到字段并添加我们自己的无参构造函数,而无需任何属性。
public class Doodad(ILogger<Doodad> _logger)
{
public Doodad() {} // 我们的无参构造函数
public Doodad(string name, int count)
{
Id = Guid.NewGuid();
Name = name;
Count = count;
}
public Guid Id { get; set; }
public string Name { get; set; }
public int Count { get; set; }
}
并使用我们的序列化属性(同样,因为我们使用序列化属性,所以选择使用 init-only 设置器):
[DataContract]
public class Doodad(ILogger<Doodad> _logger)
{
public Doodad(string name, int count)
{
Id = Guid.NewGuid();
Name = name;
Count = count;
}
[DataMember]
public Guid Id { get; init; }
[DataMember]
public string Name { get; init; }
[DataMember]
public int Count { get; init; }
}
.NET 结构体
结构体由 Data Contract 序列化程序支持,前提是它们使用 DataContractAttribute 属性标记,并且您希望序列化的成员使用 DataMemberAttribute 属性标记。此外,为了支持反序列化,结构体还需要具有无参构造函数。即使在 C# 10 中定义了自己的无参构造函数,这也可以工作。
[DataContract]
public struct Doodad
{
[DataMember]
public int Count { get; set; }
}
.NET 记录
记录是在 C# 9 中引入的,在序列化方面遵循与类完全相同的规则。我们建议您应该使用 DataContractAttribute 属性装饰所有记录,并使用 DataMemberAttribute 属性装饰要序列化的成员,以免在使用此或其他较新的 C# 功能时遇到任何反序列化问题。因为记录类默认对属性使用 init-only 设置器并鼓励使用主构造函数,所以将这些属性应用于类型可确保序列化程序能够正确地容纳您的类型。
通常,记录使用新的主构造函数概念呈现为简单的单行语句:
public record Doodad(Guid Id, string Name, int Count);
一旦您在 Dapr actor 方法调用中使用它,这将抛出一个错误,鼓励使用序列化属性,因为没有可用的无参构造函数,也没有使用上述属性进行装饰。
这里我们添加一个显式的无参构造函数,它不会抛出错误,但在反序列化期间不会设置任何值,因为它们是使用 init-only 设置器创建的。因为它没有对任何成员使用 DataContractAttribute 属性或 DataMemberAttribute 属性,所以序列化程序将无法在反序列化期间正确映射目标成员。
public record Doodad(Guid Id, string Name, int Count)
{
public Doodad() {}
}
这种方法不需要额外的构造函数,而是依赖于序列化属性。因为我们使用 DataContractAttribute 属性标记类型,并使用自己的 DataMemberAttribute 属性装饰每个成员,所以序列化引擎将能够毫无问题地从 XML 文档映射到我们的类型。
[DataContract]
public record Doodad(
[property: DataMember] Guid Id,
[property: DataMember] string Name,
[property: DataMember] int Count)
支持的基元类型
.NET 中内置了多种类型,这些类型被视为基元类型,无需开发人员付出额外努力即可进行序列化:
还有其他不是真正基元类型但具有类似内置支持的类型:
同样,如果您想通过 actor 方法传递这些类型,无需额外考虑,因为它们将毫无问题地序列化和反序列化。此外,本身标记了 (SerializeableAttribute)[https://learn.microsoft.com/dotnet/api/system.serializableattribute] 属性的类型也将被序列化。
枚举类型
枚举(包括标志枚举)如果适当标记则是可序列化的。您希望序列化的枚举成员必须使用 EnumMemberAttribute 属性标记才能进行序列化。在此属性的可选 Value 参数中传递自定义值将允许您指定成员在序列化文档中使用的值,而不是让序列化程序从成员名称派生它。
枚举类型不要求使用 DataContractAttribute 属性装饰类型——只需要您希望序列化的成员使用 EnumMemberAttribute 属性标记即可。
public enum Colors
{
[EnumMember]
Red,
[EnumMember(Value="g")]
Green,
Blue, // 即使被类型使用,此值也不会被序列化,因为它没有使用 EnumMember 属性装饰
}
集合类型
关于数据约定序列化程序,所有实现 IEnumerable 接口的集合类型(包括数组和泛型集合)都被视为集合。实现 IDictionary 或泛型 IDictionary<TKey, TValue> 的类型被视为字典集合;所有其他类型都被视为列表集合。
与其他复杂类型一样,集合类型必须具有可用的无参构造函数。此外,它们还必须具有名为 Add 的方法,以便可以正确序列化和反序列化。这些集合类型使用的类型本身必须标记 DataContractAttribute 属性或按照本文档中的描述可序列化。
数据约定版本控制
由于数据约定序列化程序仅在 Dapr 中用于通过代理方法序列化和反序列化 .NET SDK 中的值与 Dapr actor 实例之间,因此几乎不需要考虑数据约定的版本控制,因为数据不会在使用相同序列化程序的应用程序版本之间持久化。对于那些有兴趣了解有关数据约定版本控制的更多信息的人,请访问此处。
已知类型
通过使用 DataContractAttribute 属性标记每个类型,可以轻松嵌套您自己的复杂类型。这会通知序列化程序应如何执行反序列化。
但是,如果您正在处理多态类型,并且您的成员之一是具有派生类或其他实现的基类或接口呢?在这里,您将使用 KnownTypeAttribute 属性向序列化程序提供有关如何继续的提示。
当您将 KnownTypeAttribute 属性应用于类型时,您是在通知数据约定序列化程序它可能遇到哪些子类型,允许它正确处理这些类型的序列化和反序列化,即使运行时的实际类型与声明的类型不同。
[DataContract]
[KnownType(typeof(DerivedClass))]
public class BaseClass
{
// 基类的成员
}
[DataContract]
public class DerivedClass : BaseClass
{
// 派生类的其他成员
}
在此示例中,BaseClass 标记了 [KnownType(typeof(DerivedClass))],这告诉数据约定序列化程序 DerivedClass 是它可能需要序列化或反序列化的 BaseClass 的可能实现。如果没有此属性,序列化程序在遇到实际上是 DerivedClass 类型的 BaseClass 实例时将不知道 DerivedClass,这可能会导致序列化异常,因为序列化程序将不知道如何处理派生类型。通过将所有可能的派生类型指定为已知类型,可以确保序列化程序可以正确处理该类型及其成员。
有关使用 [KnownType] 的更多信息和示例,请参阅官方文档。
2.1.3.4 - 如何:在 .NET SDK 中运行和使用虚拟 Actor
Dapr Actor 包允许你从 .NET 应用程序与 Dapr 虚拟 Actor 交互。在本指南中,你将学习如何:
- 创建一个 Actor(
MyActor)。 - 在客户端应用程序上调用其方法。
MyActor --- MyActor.Interfaces
|
+- MyActorService
|
+- MyActorClient
接口项目 (\MyActor\MyActor.Interfaces)
该项目包含 actor 的接口定义。Actor 接口可以在任何名称的项目中定义。接口定义了由以下各方共享的 actor 契约:
- actor 实现
- 调用 actor 的客户端
由于客户端项目可能依赖它,最好在独立于 actor 实现的程序集中定义它。
Actor 服务项目 (\MyActor\MyActorService)
该项目实现承载 actor 的 ASP.Net Core Web 服务。它包含 actor 的实现 MyActor.cs。Actor 实现是一个类,它:
- 派生自基类型 Actor
- 实现
MyActor.Interfaces项目中定义的接口。
Actor 类还必须实现一个构造函数,该构造函数接受一个 ActorService 实例和一个 ActorId,并将它们传递给基 Actor 类。
Actor 客户端项目 (\MyActor\MyActorClient)
该项目包含 actor 客户端的实现,该客户端调用 MyActor 的在 Actor Interfaces 中定义的方法。
前置条件
步骤 0:准备
由于我们将创建 3 个项目,请选择一个空目录作为起点,并在你选择的终端中打开它。
步骤 1:创建 actor 接口
Actor 接口定义了由 actor 实现和调用 actor 的客户端共享的 actor 契约。
Actor 接口定义需满足以下要求:
- Actor 接口必须继承
Dapr.Actors.IActor接口 - Actor 方法的返回类型必须是
Task或Task<object> - Actor 方法最多只能有一个参数
创建接口项目并添加依赖项
# Create Actor Interfaces
dotnet new classlib -o MyActor.Interfaces
cd MyActor.Interfaces
# Add Dapr.Actors nuget package. Please use the latest package version from nuget.org
dotnet add package Dapr.Actors
cd ..
实现 IMyActor 接口
定义 IMyActor 接口和 MyData 数据对象。将以下代码粘贴到 MyActor.Interfaces 项目的 MyActor.cs 中。
using Dapr.Actors;
using Dapr.Actors.Runtime;
using System.Threading.Tasks;
namespace MyActor.Interfaces
{
public interface IMyActor : IActor
{
Task<string> SetDataAsync(MyData data);
Task<MyData> GetDataAsync();
Task RegisterReminder();
Task UnregisterReminder();
Task<IActorReminder> GetReminder();
Task RegisterTimer();
Task UnregisterTimer();
}
public class MyData
{
public string PropertyA { get; set; }
public string PropertyB { get; set; }
public override string ToString()
{
var propAValue = this.PropertyA == null ? "null" : this.PropertyA;
var propBValue = this.PropertyB == null ? "null" : this.PropertyB;
return $"PropertyA: {propAValue}, PropertyB: {propBValue}";
}
}
}
步骤 2:创建 actor 服务
Dapr 使用 ASP.NET Web 服务来承载 Actor 服务。本节将实现 IMyActor actor 接口并将 Actor 注册到 Dapr Runtime。
创建 actor 服务项目并添加依赖项
# Create ASP.Net Web service to host Dapr actor
dotnet new web -o MyActorService
cd MyActorService
# Add Dapr.Actors.AspNetCore nuget package. Please use the latest package version from nuget.org
dotnet add package Dapr.Actors.AspNetCore
# Add Actor Interface reference
dotnet add reference ../MyActor.Interfaces/MyActor.Interfaces.csproj
cd ..
添加 actor 实现
实现 IMyActor 接口并派生自 Dapr.Actors.Actor 类。以下示例还展示了如何使用 Actor Reminders。要使 Actors 使用 Reminders,它必须派生自 IRemindable。如果你不打算使用 Reminder 功能,可以跳过实现 IRemindable 和下面代码中显示的 reminder 特定方法。
将以下代码粘贴到 MyActorService 项目的 MyActor.cs 中:
using Dapr.Actors;
using Dapr.Actors.Runtime;
using MyActor.Interfaces;
using System;
using System.Threading.Tasks;
namespace MyActorService
{
internal class MyActor : Actor, IMyActor, IRemindable
{
// The constructor must accept ActorHost as a parameter, and can also accept additional
// parameters that will be retrieved from the dependency injection container
//
/// <summary>
/// Initializes a new instance of MyActor
/// </summary>
/// <param name="host">The Dapr.Actors.Runtime.ActorHost that will host this actor instance.</param>
public MyActor(ActorHost host)
: base(host)
{
}
/// <summary>
/// This method is called whenever an actor is activated.
/// An actor is activated the first time any of its methods are invoked.
/// </summary>
protected override Task OnActivateAsync()
{
// Provides opportunity to perform some optional setup.
Console.WriteLine($"Activating actor id: {this.Id}");
return Task.CompletedTask;
}
/// <summary>
/// This method is called whenever an actor is deactivated after a period of inactivity.
/// </summary>
protected override Task OnDeactivateAsync()
{
// Provides Opporunity to perform optional cleanup.
Console.WriteLine($"Deactivating actor id: {this.Id}");
return Task.CompletedTask;
}
/// <summary>
/// Set MyData into actor's private state store
/// </summary>
/// <param name="data">the user-defined MyData which will be stored into state store as "my_data" state</param>
public async Task<string> SetDataAsync(MyData data)
{
// Data is saved to configured state store implicitly after each method execution by Actor's runtime.
// Data can also be saved explicitly by calling this.StateManager.SaveStateAsync();
// State to be saved must be DataContract serializable.
await this.StateManager.SetStateAsync<MyData>(
"my_data", // state name
data); // data saved for the named state "my_data"
return "Success";
}
/// <summary>
/// Get MyData from actor's private state store
/// </summary>
/// <return>the user-defined MyData which is stored into state store as "my_data" state</return>
public Task<MyData> GetDataAsync()
{
// Gets state from the state store.
return this.StateManager.GetStateAsync<MyData>("my_data");
}
/// <summary>
/// Register MyReminder reminder with the actor
/// </summary>
public async Task RegisterReminder()
{
await this.RegisterReminderAsync(
"MyReminder", // The name of the reminder
null, // User state passed to IRemindable.ReceiveReminderAsync()
TimeSpan.FromSeconds(5), // Time to delay before invoking the reminder for the first time
TimeSpan.FromSeconds(5)); // Time interval between reminder invocations after the first invocation
}
/// <summary>
/// Get MyReminder reminder details with the actor
/// </summary>
public async Task<IActorReminder> GetReminder()
{
await this.GetReminderAsync("MyReminder");
}
/// <summary>
/// Unregister MyReminder reminder with the actor
/// </summary>
public Task UnregisterReminder()
{
Console.WriteLine("Unregistering MyReminder...");
return this.UnregisterReminderAsync("MyReminder");
}
// <summary>
// Implement IRemindeable.ReceiveReminderAsync() which is call back invoked when an actor reminder is triggered.
// </summary>
public Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period)
{
Console.WriteLine("ReceiveReminderAsync is called!");
return Task.CompletedTask;
}
/// <summary>
/// Register MyTimer timer with the actor
/// </summary>
public Task RegisterTimer()
{
return this.RegisterTimerAsync(
"MyTimer", // The name of the timer
nameof(this.OnTimerCallBack), // Timer callback
null, // User state passed to OnTimerCallback()
TimeSpan.FromSeconds(5), // Time to delay before the async callback is first invoked
TimeSpan.FromSeconds(5)); // Time interval between invocations of the async callback
}
/// <summary>
/// Unregister MyTimer timer with the actor
/// </summary>
public Task UnregisterTimer()
{
Console.WriteLine("Unregistering MyTimer...");
return this.UnregisterTimerAsync("MyTimer");
}
/// <summary>
/// Timer callback once timer is expired
/// </summary>
private Task OnTimerCallBack(byte[] data)
{
Console.WriteLine("OnTimerCallBack is called!");
return Task.CompletedTask;
}
}
}
在 ASP.NET Core 启动时注册 actor 运行时
Actor 运行时通过 ASP.NET Core Startup.cs 进行配置。
运行时使用 ASP.NET Core 依赖注入系统来注册 actor 类型和基本服务。此集成通过 ConfigureServices(...) 中的 AddActors(...) 方法调用提供。使用传递给 AddActors(...) 的委托来注册 actor 类型并配置 actor 运行时设置。你可以在 ConfigureServices(...) 内部注册其他类型以进行依赖注入。这些类型将可用于注入到 Actor 类型的构造函数中。
Actors 通过与 Dapr 运行时的 HTTP 调用来实现。此功能是应用程序 HTTP 处理管道的一部分,并在 Configure(...) 内部的 UseEndpoints(...) 中注册。
将以下代码粘贴到 MyActorService 项目的 Startup.cs 中:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace MyActorService
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddActors(options =>
{
// Register actor types and configure actor settings
options.Actors.RegisterActor<MyActor>();
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
// Register actors handlers that interface with the Dapr runtime.
app.MapActorsHandlers();
}
}
}
步骤 3:添加客户端
创建一个简单的控制台应用程序来调用 actor 服务。Dapr SDK 提供 Actor Proxy 客户端来调用 Actor Interface 中定义的 actor 方法。
创建 actor 客户端项目并添加依赖项
# Create Actor's Client
dotnet new console -o MyActorClient
cd MyActorClient
# Add Dapr.Actors nuget package. Please use the latest package version from nuget.org
dotnet add package Dapr.Actors
# Add Actor Interface reference
dotnet add reference ../MyActor.Interfaces/MyActor.Interfaces.csproj
cd ..
使用强类型客户端调用 actor 方法
你可以使用 ActorProxy.Create<IMyActor>(..) 创建强类型客户端并调用 actor 上的方法。
将以下代码粘贴到 MyActorClient 项目的 Program.cs 中:
using System;
using System.Threading.Tasks;
using Dapr.Actors;
using Dapr.Actors.Client;
using MyActor.Interfaces;
namespace MyActorClient
{
class Program
{
static async Task MainAsync(string[] args)
{
Console.WriteLine("Startup up...");
// Registered Actor Type in Actor Service
var actorType = "MyActor";
// An ActorId uniquely identifies an actor instance
// If the actor matching this id does not exist, it will be created
var actorId = new ActorId("1");
// Create the local proxy by using the same interface that the service implements.
//
// You need to provide the type and id so the actor can be located.
var proxy = ActorProxy.Create<IMyActor>(actorId, actorType);
// Now you can use the actor interface to call the actor's methods.
Console.WriteLine($"Calling SetDataAsync on {actorType}:{actorId}...");
var response = await proxy.SetDataAsync(new MyData()
{
PropertyA = "ValueA",
PropertyB = "ValueB",
});
Console.WriteLine($"Got response: {response}");
Console.WriteLine($"Calling GetDataAsync on {actorType}:{actorId}...");
var savedData = await proxy.GetDataAsync();
Console.WriteLine($"Got response: {savedData}");
}
}
}
运行代码
你现在可以测试你创建的项目。
运行 MyActorService
由于
MyActorService承载着 actors,它需要使用 Dapr CLI 运行。cd MyActorService dapr run --app-id myapp --app-port 5000 --dapr-http-port 3500 -- dotnet run你将在此终端中看到来自
daprd和MyActorService的命令行输出。你应该会看到类似以下内容,这表明应用程序已成功启动。... ℹ️ Updating metadata for app command: dotnet run ✅ You're up and running! Both Dapr and your app logs will appear here. == APP == info: Microsoft.Hosting.Lifetime[0] == APP == Now listening on: https://localhost:5001 == APP == info: Microsoft.Hosting.Lifetime[0] == APP == Now listening on: http://localhost:5000 == APP == info: Microsoft.Hosting.Lifetime[0] == APP == Application started. Press Ctrl+C to shut down. == APP == info: Microsoft.Hosting.Lifetime[0] == APP == Hosting environment: Development == APP == info: Microsoft.Hosting.Lifetime[0] == APP == Content root path: /Users/ryan/actortest/MyActorService运行 MyActorClient
MyActorClient充当客户端,可以使用dotnet run正常运行。打开一个新终端并导航到
MyActorClient目录。然后使用以下命令运行项目:dotnet run你应该会看到类似以下的命令行输出:
Startup up... Calling SetDataAsync on MyActor:1... Got response: Success Calling GetDataAsync on MyActor:1... Got response: PropertyA: ValueA, PropertyB: ValueB
💡 此示例依赖于一些假设。ASP.NET Core Web 项目的默认监听端口为 5000,该端口作为
--app-port 5000传递给dapr run。Dapr sidecar 的默认 HTTP 端口为 3500。我们告诉MyActorService的 sidecar 使用 3500,以便MyActorClient可以依赖默认值。
现在你已成功创建了 actor 服务和客户端。请参阅相关链接部分以了解更多信息。
相关链接
2.1.4 - Dapr AI .NET SDK
使用 Dapr AI 包,您可以从 .NET 应用程序与 Dapr AI 工作负载进行交互。
目前,Dapr 提供对话 API 来与大语言模型进行交互。要开始使用此工作负载,请阅读 Dapr 对话 AI 操作指南。
2.1.4.1 - Dapr AI 客户端
Dapr AI 客户端包允许您与 Dapr 边车提供的 AI 功能进行交互。
注意
Dapr Conversation 构建块需要 Dapr 运行时 v1.16.0 或更高版本。响应格式、提示缓存保留以及令牌使用统计需要 Dapr 运行时 v1.17.0 或更高版本。生命周期管理
DaprConversationClient 是 Dapr 客户端的专用版本,专门用于与 Dapr Conversation API 交互。它可以与 DaprClient 和其他 Dapr 客户端一起注册,不会产生任何问题。
它维护用于与 Dapr 边车通信的 TCP 套接字形式的网络资源访问。
为获得最佳性能,请创建一个 DaprConversationClient 的单一长期实例,并在整个应用程序中提供对该共享实例的访问。DaprConversationClient 实例是线程安全的,旨在共享使用。
利用依赖注入功能可以更好地实现这一点。注册方法支持注册为单例、作用域实例或瞬态(意味着每次注入时都会重新创建),但同时也支持注册以使用 IConfiguration 的值或其他注入的服务,这在每个类中从头创建客户端时是不切实际的。
避免为每次操作创建一个 DaprConversationClient。
通过 DaprConversationClientBuilder 配置 DaprConversationClient
可以通过在调用 .Build() 创建客户端本身之前调用 DaprConversationClientBuilder 类上的方法来配置 DaprConversationClient。每个 DaprConversationClient 的设置是独立的,在调用 .Build() 后无法更改。
var daprConversationClient = new DaprConversationClientBuilder()
.UseDaprApiToken("abc123") // 指定用于向其他 Dapr 边车进行身份验证的 API 令牌
.Build();
DaprConversationClientBuilder 包含以下设置:
- Dapr 边车的 HTTP 端点
- Dapr 边车的 gRPC 端点
- 用于配置 JSON 序列化的
JsonSerializerOptions对象 - 用于配置 gRPC 的
GrpcChannelOptions对象 - 用于向边车验证请求的 API 令牌
- 用于创建 SDK 使用的
HttpClient实例的工厂方法 - 向边车发出请求时
HttpClient实例使用的超时时间
SDK 将读取以下环境变量以配置默认值:
DAPR_HTTP_ENDPOINT:用于查找 Dapr 边车的 HTTP 端点,例如:https://dapr-api.mycompany.comDAPR_GRPC_ENDPOINT:用于查找 Dapr 边车的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.comDAPR_HTTP_PORT:如果未设置DAPR_HTTP_ENDPOINT,则用于查找 Dapr 边车的 HTTP 本地端点DAPR_GRPC_PORT:如果未设置DAPR_GRPC_ENDPOINT,则用于查找 Dapr 边车的 gRPC 本地端点DAPR_API_TOKEN:用于设置 API 令牌
配置 gRPC 通道选项
Dapr 使用 CancellationToken 进行取消操作依赖于 gRPC 通道选项的配置。如果您需要自己配置这些选项,请确保启用 ThrowOperationCanceledOnCancellation 设置。
var daprConversationClient = new DaprConversationClientBuilder()
.UseGrpcChannelOptions(new GrpcChannelOptions { ... ThrowOperationCanceledOnCancellation = true })
.Build();
使用 DaprConversationClient 进行取消操作
DaprConversationClient 上的 API 执行异步操作并接受可选的 CancellationToken 参数。这遵循了 .NET 用于可取消操作的标准实践。请注意,当发生取消时,无法保证远程端点停止处理请求,只能保证客户端已停止等待完成。
当操作被取消时,它将抛出 OperationCancelledException。
通过依赖注入配置 DaprConversationClient
使用内置的扩展方法在依赖注入容器中注册 DaprConversationClient 可以带来以下好处:一次性注册长期服务、集中复杂配置,并通过在可能的情况下重用类似的长期资源(例如 HttpClient 实例)来提高性能。
有三种重载可用,为开发人员在其场景中配置客户端提供了最大的灵活性。如果尚未注册 IHttpClientFactory,每种方法都会代表您注册它,并配置 DaprConversationClientBuilder 在创建 HttpClient 实例时使用它,以尽可能重用同一实例并避免套接字耗尽和其他问题。
在第一种方法中,开发人员不进行任何配置,DaprConversationClient 使用默认设置进行配置。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprConversationClient(); // 注册 `DaprConversationClient` 以按需注入
var app = builder.Build();
发送对话请求
使用 ConversationInput 和 ConversationOptions 向您的对话组件发送提示:
var inputs = new[]
{
new ConversationInput(new IConversationMessage[]
{
new SystemMessage("You are a helpful assistant."),
new UserMessage("Summarize the following text...")
})
};
var options = new ConversationOptions("my-conversation-component")
{
Temperature = 0.2
};
var response = await daprConversationClient.ConverseAsync(inputs, options);
响应格式(JSON 架构)
您可以使用 ConversationOptions.ResponseFormat 提供 JSON 架构,以将响应强制转换为特定格式。此功能需要 Dapr 运行时 v1.17.0 或更高版本。
using Google.Protobuf.WellKnownTypes;
var responseFormat = new Struct
{
Fields =
{
["type"] = new Value { StringValue = "object" },
["properties"] = new Value
{
StructValue = new Struct
{
Fields =
{
["answer"] = new Value
{
StructValue = new Struct
{
Fields =
{
["type"] = new Value { StringValue = "string" }
}
}
}
}
}
},
["required"] = new Value
{
ListValue = new ListValue { Values = { new Value { StringValue = "answer" } } }
}
}
};
var options = new ConversationOptions("my-conversation-component")
{
ResponseFormat = responseFormat
};
提示缓存保留
如果您的对话组件支持提示缓存,您可以使用 ConversationOptions.PromptCacheRetention 请求缓存保留窗口。此功能需要 Dapr 运行时 v1.17.0 或更高版本。
var options = new ConversationOptions("my-conversation-component")
{
PromptCacheRetention = TimeSpan.FromMinutes(30)
};
令牌使用统计
当组件和运行时支持时,响应包含可选的令牌使用统计。使用数据可在 ConversationResponseResult.Usage 上获得,包括总数和详细细分:
var response = await daprConversationClient.ConverseAsync(inputs, options);
var result = response.Outputs[0];
if (result.Usage is not null)
{
var totalTokens = result.Usage.TotalTokens;
var promptCachedTokens = result.Usage.PromptTokensDetails?.CachedTokens;
var reasoningTokens = result.Usage.CompletionTokensDetails?.ReasoningTokens;
}
有时开发人员需要使用上面详述的各种配置选项来配置创建的客户端。这是通过传入 DaprConversationClientBuiler 的重载来完成的,该重载公开了配置必要选项的方法。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprConversationClient((_, daprConversationClientBuilder) => {
// 设置 API 令牌
daprConversationClientBuilder.UseDaprApiToken("abc123");
// 指定非标准的 HTTP 端点
daprConversationClientBuilder.UseHttpEndpoint("http://dapr.my-company.com");
});
var app = builder.Build();
最后,开发人员可能需要从另一个服务检索信息以填充这些配置值。该值可能从 DaprClient 实例、供应商特定的 SDK 或某个本地服务提供,但只要它也在 DI 中注册,就可以通过最后一个重载将其注入到此配置操作中:
var builder = WebApplication.CreateBuilder(args);
// 注册一个从某处检索机密的虚构服务
builder.Services.AddSingleton<SecretService>();
builder.Services.AddDaprConversationClient((serviceProvider, daprConversationClientBuilder) => {
// 从服务提供程序检索 `SecretService` 的实例
var secretService = serviceProvider.GetRequiredService<SecretService>();
var daprApiToken = secretService.GetSecret("DaprApiToken").Value;
// 配置 `DaprConversationClientBuilder`
daprConversationClientBuilder.UseDaprApiToken(daprApiToken);
});
var app = builder.Build();
2.1.4.2 - 操作指南:在 .NET SDK 中创建和使用 Dapr AI 会话
前置条件
安装
要开始使用 Dapr AI .NET SDK 客户端,请从 NuGet 安装 Dapr.AI 包:
dotnet add package Dapr.AI
DaprConversationClient 维护对用于与 Dapr 边车通信的 TCP 套接字形式的网络资源的访问。
依赖注入
AddDaprAiConversation() 方法会将 Dapr 客户端注册到 ASP.NET Core 依赖注入容器中,这是使用此包的推荐方式。此方法接受一个可选的选项委托来配置 DaprConversationClient,以及一个 ServiceLifetime 参数,允许您为注册的服务指定不同的生命周期,而不是默认的 Singleton 值。
以下示例假定所有默认值都可以接受,足以注册 DaprConversationClient:
services.AddDaprAiConversation();
可选的配置委托用于通过在 DaprConversationClientBuilder 上指定选项来配置 DaprConversationClient,如下例所示:
services.AddSingleton<DefaultOptionsProvider>();
services.AddDaprAiConversation((serviceProvider, clientBuilder) => {
//注入一个服务以从中获取值
var optionsProvider = serviceProvider.GetRequiredService<DefaultOptionsProvider>();
var standardTimeout = optionsProvider.GetStandardTimeout();
//在客户端构建器上配置值
clientBuilder.UseTimeout(standardTimeout);
});
手动实例化
除了使用依赖注入,也可以使用静态客户端构建器来构建 DaprConversationClient。
为了获得最佳性能,请创建一个单一的长期存活的 DaprConversationClient 实例,并在整个应用程序中提供对该共享实例的访问。DaprConversationClient 实例是线程安全的,旨在共享使用。
避免为每次操作创建一个 DaprConversationClient。
可以通过在调用 .Build() 创建客户端之前,在 DaprConversationClientBuilder 类上调用方法来配置 DaprConversationClient。每个 DaprConversationClient 的设置是独立的,在调用 .Build() 后无法更改。
var daprConversationClient = new DaprConversationClientBuilder()
.UseJsonSerializerSettings( ... ) //配置 JSON 序列化器
.Build();
有关通过构建器配置 Dapr 客户端时可用选项的更多信息,请参阅 .NET 文档。
试用
测试 Dapr AI .NET SDK。浏览示例以查看 Dapr 的实际效果:
| SDK 示例 | 描述 |
|---|---|
| SDK 示例 | 克隆 SDK 存储库以试用一些示例并开始使用。 |
构建块
.NET SDK 的这一部分允许您与会话 API 交互,以从大型语言模型发送和接收消息。
2.1.4.3 - 如何:在 Dapr 的 .NET Conversation SDK 中使用 Microsoft 的 AI 扩展
前置条件
安装
要开始使用此 SDK,请从 NuGet 安装 Dapr.AI 和 Dapr.AI.Microsoft.Extensions 包:
dotnet add package Dapr.AI
dotnet add package Dapr.AI.Microsoft.Extensions
DaprChatClient 是 IChatClient 接口的基于 Dapr 的实现,该接口由
Microsoft.Extensions.AI.Abstractions 包提供,使用 Dapr 的[对话构建块]({{ ref conversation-overview.md }})。它允许
开发者针对 Microsoft 提供的抽象类型进行开发,同时提供与 Dapr 对话构建块的最大一致性。由于这两种方法都采用 OpenAI 的 API 方式,
预计它们在未来会日益趋同。
Dapr 对话构建块
请注意,Dapr 的对话构建块仍处于 alpha 状态,这意味着 API 的形状 可能会在未来版本中发生变化。此 SDK 包的目的是提供一个与 Microsoft 的 AI 扩展对齐且同时映射到并符合 Dapr API 的 API, 但类型和属性的名称可能会从一个版本更改为下一个版本,因此在使用此 SDK 时请注意这种可能性。关于 Microsoft.Extensions.AI
Dapr.AI.Microsoft.Extensions 包实现了 Microsoft.Extensions.AI 抽象,为
.NET 应用程序中的 AI 服务提供统一的 API。Microsoft.Extensions.AI 旨在为
不同的 AI 提供商和场景提供一致的编程模型。有关 Microsoft.Extensions.AI 的详细信息,请参阅
官方文档。
有限支持
请注意,Microsoft 的 AI 扩展提供的属性和方法比 Dapr 的对话构建块当前 支持的要多得多。此包将仅映射那些具有 Dapr 支持的属性,而忽略其他属性,因此仅仅 它在 Microsoft.Extensions.AI 包中可用并不意味着 Dapr 支持它。请依赖此文档 和包中公开的 XML 文档来了解什么支持、什么不支持。服务注册
可以使用多个扩展方法将 DaprChatClient 注册到依赖注入容器中。首先,
确保注册来自 NuGet 的 Dapr.AI 包中的 DaprConversationClient:
services.AddDaprConversationClient();
然后使用您的对话组件名称注册 DaprChatClient:
services.AddDaprChatClient("my-conversation-component");
配置选项
您可以通过 DaprChatClientOptions 配置 DaprChatClient,尽管当前实现仅
为组件名称本身提供配置。这预计在未来的版本中会发生变化。
services.AddDaprChatClient("my-conversation-component", options =>
{
// 在此处配置其他选项
});
您还可以配置服务生命周期(默认为 ServiceLifetime.Scoped):
services.AddDaprChatClient("my-conversation-component", ServiceLifetime.Singleton);
使用
注册后,您可以在您的服务中注入和使用 IChatClient:
public class ChatService(IChatClient chatClient)
{
public async Task<IReadOnlyList<string>> GetResponseAsync(string message)
{
var response = await chatClient.GetResponseAsync([
new ChatMessage(ChatRole.User,
"Please write me a poem in iambic pentameter about the joys of using Dapr to develop distributed applications with .NET")
]);
return response.Messages.Select(msg => msg.Text).ToList();
}
}
流式对话
DaprChatClient 尚不支持流式响应,使用相应的 GetStreamingResponseAsync
方法将抛出 NotImplemenetedException。一旦 Dapr 运行时
支持此功能,这预计在未来的版本中会发生变化。
工具集成
客户端通过 Microsoft.Extensions.AI 工具集成支持函数调用。注册到对话的
工具将自动可供大语言模型使用。
string GetCurrentWeather() => Random.Shared.NextDouble() > 0.5 ? "It's sunny today!" : "It's raining today!";
var toolChatOptions = new ChatOptions { Tools = [AIFunctionFactory.Create(GetCurrentWeather, "weather")] };
var toolResponse = await chatClient.GetResponseAsync("What's the weather like today?", toolChatOptions);
foreach (var toolResp in toolResponse.Messages)
{
Console.WriteLine(toolResp);
}
错误处理
DaprChatClient 与 Dapr 的错误处理集成,并在发生问题时抛出适当的异常。
配置和元数据
底层的 Dapr 对话组件可以通过 Dapr 对话构建块配置来配置元数据和参数。
DaprChatClient 在调用对话组件时将遵循这些设置。
最佳实践
服务生命周期:为
DaprChatClient注册使用ServiceLifetime.Scoped或ServiceLifetime.Singleton,以避免不必要地创建多个实例。错误处理:始终将调用包装在适当的 try-catch 块中,以处理 Dapr 特定和常规异常。
资源管理:
DaprChatClient通过其基类正确实现了IDisposable,因此在使用依赖注入时会自动管理资源。配置:正确配置您的 Dapr 对话组件以确保最佳性能和可靠性。
相关链接
- [Dapr 对话构建块]({{ ref conversation-overview.md }})
- Microsoft.Extensions.AI 文档
- Dapr .NET Conversation SDK
2.1.5 - Dapr Jobs .NET SDK
使用 Dapr Job 包,您可以在 .NET 应用程序中与 Dapr Job API 交互,按照预定义的计划触发未来操作运行,并支持可选的有效负载。
2.1.5.1 - 操作指南:在 .NET SDK 中编写和管理 Dapr Jobs
让我们创建一个在 Dapr Jobs 触发时会被调用的端点,然后在同一应用中调度该任务。我们将使用此处提供的简单示例进行以下演示,并以此作为说明,介绍如何使用间隔时间或 Cron 表达式来调度一次性或重复任务。在本指南中,你将:
- 部署一个 .NET Web API 应用程序 (JobsSample)
- 使用 Dapr .NET Jobs SDK 来调度任务调用并设置要触发的端点
在 .NET 示例项目中:
- 主要的
Program.cs文件包含了本演示的全部内容。
前置条件
设置环境
克隆 .NET SDK 仓库。
git clone https://github.com/dapr/dotnet-sdk.git
从 .NET SDK 根目录导航到 Dapr Jobs 示例。
cd examples/Jobs
在本地运行应用程序
要运行 Dapr 应用程序,你需要启动 .NET 程序和 Dapr 边车。导航到 JobsSample 目录。
cd JobsSample
我们将运行一个同时启动 Dapr 边车和 .NET 程序的命令。
dapr run --app-id jobsapp --dapr-grpc-port 4001 --dapr-http-port 3500 -- dotnet run
Dapr 在
http://localhost:3500监听 HTTP 请求,在http://localhost:4001监听内部 Jobs gRPC 请求。
使用依赖注入注册 Dapr Jobs 客户端
Dapr Jobs SDK 提供了一个扩展方法来简化 Dapr Jobs 客户端的注册。在 Program.cs 中完成依赖注入注册之前,添加以下行:
var builder = WebApplication.CreateBuilder(args);
//Add anywhere between these two lines
builder.Services.AddDaprJobsClient();
var app = builder.Build();
请注意,在 Jobs API 的当前实现中,调度任务的应用也将是接收触发通知的应用。换句话说,你无法调度一个在另一个应用中运行的触发器。因此,虽然你不需要显式地在应用中注册 Dapr Jobs 客户端来调度触发器调用端点,但如果没有同一应用以某种方式调度任务(无论是通过此 Dapr Jobs .NET SDK 还是通过对边车的 HTTP 调用),你的端点永远不会被调用。
你可能希望为 Dapr Jobs 客户端提供一些配置选项,这些选项应在每次对边车的调用时都存在,例如 Dapr API 令牌,或者你想使用非标准的 HTTP 或 gRPC 端点。这可以通过使用注册方法的重载来实现,该重载允许配置 DaprJobsClientBuilder 实例:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprJobsClient((_, daprJobsClientBuilder) =>
{
daprJobsClientBuilder.UseDaprApiToken("abc123");
daprJobsClientBuilder.UseHttpEndpoint("http://localhost:8512"); //非标准的边车 HTTP 端点
});
var app = builder.Build();
不过,你希望注入的任何值可能需要从其他来源获取,这些来源本身已注册为依赖。还有一个重载可以使用,它可以将 IServiceProvider 注入到配置操作方法中。在以下示例中,我们注册了一个虚构的单例,该单例可以从某个地方获取密钥,并将其传递给 AddDaprJobClient 的配置方法,这样我们就可以从其他地方检索 Dapr API 令牌以在此处进行注册:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<SecretRetriever>();
builder.Services.AddDaprJobsClient((serviceProvider, daprJobsClientBuilder) =>
{
var secretRetriever = serviceProvider.GetRequiredService<SecretRetriever>();
var daprApiToken = secretRetriever.GetSecret("DaprApiToken").Value;
daprJobsClientBuilder.UseDaprApiToken(daprApiToken);
daprJobsClientBuilder.UseHttpEndpoint("http://localhost:8512");
});
var app = builder.Build();
使用 IConfiguration 配置 Dapr Jobs 客户端
也可以使用已注册的 IConfiguration 中的值来配置 Dapr Jobs 客户端,而无需像上一节演示的那样使用 DaprJobsClientBuilder 显式指定每个值覆盖。相反,通过填充通过依赖注入提供的 IConfiguration,AddDaprJobsClient() 注册将自动使用这些值而不是各自的默认值。
首先,在配置中填充值。这可以通过几种不同的方式来完成,如下所示。
通过 ConfigurationBuilder 进行配置
可以在不使用配置源的情况下配置应用程序设置,而是通过使用 ConfigurationBuilder 实例在内存中填充值:
var builder = WebApplication.CreateBuilder();
//Create the configuration
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> {
{ "DAPR_HTTP_ENDPOINT", "http://localhost:54321" },
{ "DAPR_API_TOKEN", "abc123" }
})
.Build();
builder.Configuration.AddConfiguration(configuration);
builder.Services.AddDaprJobsClient(); //这将自动从 IConfiguration 填充 HTTP 端点和 API 令牌值
通过环境变量进行配置
可以从应用程序可用的环境变量中访问应用程序设置。
以下环境变量将用于填充注册 Dapr Jobs 客户端时使用的 HTTP 端点和 API 令牌。
| 键 | 值 |
|---|---|
| DAPR_HTTP_ENDPOINT | http://localhost:54321 |
| DAPR_API_TOKEN | abc123 |
var builder = WebApplication.CreateBuilder();
builder.Configuration.AddEnvironmentVariables();
builder.Services.AddDaprJobsClient();
Dapr Jobs 客户端将被配置为使用 HTTP 端点 http://localhost:54321,并在所有出站请求中填充 API 令牌头 abc123。
通过带前缀的环境变量进行配置
然而,在多个应用程序在同一台机器上运行而不使用容器的共享主机场景或开发环境中,为环境变量添加前缀并不少见。以下示例假设 HTTP 端点和 API 令牌都将从前缀为 “myapp_” 的环境变量中提取。在此场景中使用的两个环境变量如下:
| 键 | 值 |
|---|---|
| myapp_DAPR_HTTP_ENDPOINT | http://localhost:54321 |
| myapp_DAPR_API_TOKEN | abc123 |
这些环境变量将在以下示例中加载到已注册的配置中,并在不带前缀的情况下可用。
var builder = WebApplication.CreateBuilder();
builder.Configuration.AddEnvironmentVariables(prefix: "myapp_");
builder.Services.AddDaprJobsClient();
Dapr Jobs 客户端将被配置为使用 HTTP 端点 http://localhost:54321,并在所有出站请求中填充 API 令牌头 abc123。
在不依赖依赖注入的情况下使用 Dapr Jobs 客户端
虽然使用依赖注入简化了 .NET 中复杂类型的使用,并使处理复杂配置变得更加容易,但你并不需要以这种方式注册 DaprJobsClient。相反,你也可以选择从 DaprJobsClientBuilder 实例创建它的实例,如下所示:
public class MySampleClass
{
public void DoSomething()
{
var daprJobsClientBuilder = new DaprJobsClientBuilder();
var daprJobsClient = daprJobsClientBuilder.Build();
//使用 `daprJobsClient` 做一些事情
}
}
设置一个在任务触发时被调用的端点
如果你对 ASP.NET Core 中的最小 API 有点熟悉,那么设置任务端点就很简单,因为两者的语法是相同的。
完成依赖注入注册后,像处理通过 ASP.NET Core 中的最小 API 功能映射 HTTP 请求那样配置应用程序。作为扩展方法实现,传入它应该响应的任务名称和一个委托。你可以根据需要将服务注入到委托的参数中,并且可以从最初提供给任务注册的 ReadOnlyMemory<byte> 访问任务负载。
这里可以使用两个委托。如果你需要将其他服务注入到处理程序中,其中一个提供 IServiceProvider:
//我们从上面的示例中得到这个
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprJobsClient();
var app = builder.Build();
//添加我们的端点注册
app.MapDaprScheduledJob("myJob", (IServiceProvider serviceProvider, string jobName, ReadOnlyMemory<byte> jobPayload) => {
var logger = serviceProvider.GetService<ILogger>();
logger?.LogInformation("Received trigger invocation for '{jobName}'", "myJob");
//做一些事情...
});
app.Run();
如果没有必要,委托的另一个重载不需要 IServiceProvider:
//我们从上面的示例中得到这个
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprJobsClient();
var app = builder.Build();
//添加我们的端点注册
app.MapDaprScheduledJob("myJob", (string jobName, ReadOnlyMemory<byte> jobPayload) => {
//做一些事情...
});
app.Run();
在处理映射调用时支持取消令牌
你可能希望确保在任务调用时处理超时,这样它们就不会无限期挂起并使用系统资源。在设置任务映射时,有一个可选的 TimeSpan 参数可以作为最后一个参数提供,以指定请求的超时时间。每次触发任务映射调用时,都会使用此超时参数创建一个新的 CancellationTokenSource,并从中创建一个 CancellationToken 来限制请求的处理时间。如果未提供超时,则默认为 CancellationToken.None,并且不会自动对映射应用超时。
//我们从上面的示例中得到这个
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprJobsClient();
var app = builder.Build();
//添加我们的端点注册
app.MapDaprScheduledJob("myJob", (string jobName, ReadOnlyMemory<byte> jobPayload) => {
//做一些事情...
}, TimeSpan.FromSeconds(15)); //为处理调用请求分配最大 15 秒的超时时间
app.Run();
注册任务
最后,我们必须注册要调度的任务。请注意,从这里开始,所有 SDK 方法都支持取消令牌,如果未另外设置,则使用默认令牌。
有三种不同的方式来设置任务,具体取决于你想如何配置调度。以下显示了调度任务时可用的不同参数:
| 参数名称 | 类型 | 描述 | 必填 |
|---|---|---|---|
| jobName | string | 正在调度的任务的名称。 | 是 |
| schedule | DaprJobSchedule | 定义任务何时触发的调度。 | 是 |
| payload | ReadOnlyMemory | 在触发时提供给调用端点的任务数据。 | 否 |
| startingFrom | DateTime | 任务调度应开始的时间点。 | 否 |
| repeats | int | 任务应触发的最大次数。 | 否 |
| ttl | 任务何时应过期且不再触发。 | 否 | |
| overwrite | bool | 一个标志,指示提交时是否应覆盖现有任务,如果为 false,则需要先删除同名的现有任务。 | 否 |
| cancellationToken | CancellationToken | 用于提前取消操作,例如由于操作超时。 | 否 |
DaprJobSchedule
所有任务都是通过 SDK 使用 DaprJobSchedule 调度的,它创建一个传递给运行时的表达式来调度任务。DaprJobSchedule 上公开了几个静态方法,用于简化每种可用任务调度的注册,如下所示。这将指定任务调度本身与任何其他选项(如重复操作或提供取消令牌)分离开来。
一次性任务
一次性任务就是这样;它将在单个时间点运行,不会重复。
这种方法要求你选择一个任务名称并指定它应该被触发的时间。
DaprJobSchedule.FromDateTime(DateTimeOffset scheduledTime)
一次性任务可以从 Dapr Jobs 客户端调度,如下例所示:
public class MyOperation(DaprJobsClient daprJobsClient)
{
public async Task ScheduleOneTimeJobAsync(CancellationToken cancellationToken)
{
var today = DateTimeOffset.UtcNow;
var threeDaysFromNow = today.AddDays(3);
var schedule = DaprJobSchedule.FromDateTime(threeDaysFromNow);
await daprJobsClient.ScheduleJobAsync("job", schedule, cancellationToken: cancellationToken);
}
}
基于间隔的任务
基于间隔的任务是在配置为固定时间的循环上运行的任务,就像今天 Actors 构建块中的 提醒 的工作方式一样。
DaprJobSchedule.FromDuration(TimeSpan interval)
基于间隔的任务可以从 Dapr Jobs 客户端调度,如下例所示:
public class MyOperation(DaprJobsClient daprJobsClient)
{
public async Task ScheduleIntervalJobAsync(CancellationToken cancellationToken)
{
var hourlyInterval = TimeSpan.FromHours(1);
//每小时触发一次任务,但最多 5 次
var schedule = DaprJobSchedule.FromDuration(hourlyInterval);
await daprJobsClient.ScheduleJobAsync("job", schedule, repeats: 5, cancellationToken: cancellationToken);
}
}
基于 Cron 的任务
基于 Cron 的任务是使用 Cron 表达式调度的。这提供了更多基于日历的控制,因为可以在表达式中使用基于日历的值。
DaprJobSchedule.FromCronExpression(string cronExpression)
在 Dapr SDK 中,支持两种不同的方法来调度基于 Cron 的任务。
提供你自己的 Cron 表达式
你可以通过 DaprJobSchedule.FromExpression() 通过字符串提供你自己的 Cron 表达式:
public class MyOperation(DaprJobsClient daprJobsClient)
{
public async Task ScheduleCronJobAsync(CancellationToken cancellationToken)
{
//在每月第五天的每隔一小时的顶部
const string cronSchedule = "0 */2 5 * *";
var schedule = DaprJobSchedule.FromExpression(cronSchedule);
//直到下个月才开始
var now = DateTime.UtcNow;
var oneMonthFromNow = now.AddMonths(1);
var firstOfNextMonth = new DateTime(oneMonthFromNow.Year, oneMonthFromNow.Month, 1, 0, 0, 0);
await daprJobsClient.ScheduleJobAsync("myJobName", )
await daprJobsClient.ScheduleCronJobAsync("myJobName", schedule, dueTime: firstOfNextMonth, cancellationToken: cancellationToken);
}
}
使用 CronExpressionBuilder
或者,你可以使用我们的流畅构建器来生成有效的 Cron 表达式:
public class MyOperation(DaprJobsClient daprJobsClient)
{
public async Task ScheduleCronJobAsync(CancellationToken cancellationToken)
{
//在每月第五天的每隔一小时的顶部
var cronExpression = new CronExpressionBuilder()
.Every(EveryCronPeriod.Hour, 2)
.On(OnCronPeriod.DayOfMonth, 5)
.ToString();
var schedule = DaprJobSchedule.FromExpression(cronExpression);
//直到下个月才开始
var now = DateTime.UtcNow;
var oneMonthFromNow = now.AddMonths(1);
var firstOfNextMonth = new DateTime(oneMonthFromNow.Year, oneMonthFromNow.Month, 1, 0, 0, 0);
await daprJobsClient.ScheduleJobAsync("myJobName", )
await daprJobsClient.ScheduleCronJobAsync("myJobName", schedule, dueTime: firstOfNextMonth, cancellationToken: cancellationToken);
}
}
获取已调度任务的详细信息
如果你知道已调度任务的名称,你可以检索其元数据而无需等待它被触发。返回的 JobDetails 暴露了一些有用的属性,用于从 Dapr Jobs API 使用信息:
- 如果
Schedule属性包含 Cron 表达式,IsCronExpression属性将为 true,表达式也可以在CronExpression属性中获得。 - 如果
Schedule属性包含持续时间值,IsIntervalExpression属性将为 true,该值将转换为可从Interval属性访问的TimeSpan值。
可以通过使用以下内容来完成:
public class MyOperation(DaprJobsClient daprJobsClient)
{
public async Task<JobDetails> GetJobDetailsAsync(string jobName, CancellationToken cancellationToken)
{
var jobDetails = await daprJobsClient.GetJobAsync(jobName, canecllationToken);
return jobDetails;
}
}
删除已调度的任务
要删除已调度的任务,你需要知道它的名称。从那里,就像在 Dapr Jobs 客户端上调用 DeleteJobAsync 方法一样简单:
public class MyOperation(DaprJobsClient daprJobsClient)
{
public async Task DeleteJobAsync(string jobName, CancellationToken cancellationToken)
{
await daprJobsClient.DeleteJobAsync(jobName, cancellationToken);
}
}
2.1.5.2 - DaprJobsClient 使用
生命周期管理
DaprJobsClient 是 Dapr 客户端的一个专用版本,专门用于与 Dapr Jobs API 交互。它可以与 DaprClient 及其他 Dapr 客户端一起注册,不会有任何问题。
它维护用于与 Dapr 边车通信的 TCP 套接字形式的网络资源访问,并实现 IDisposable 以支持资源的即时清理。
为了获得最佳性能,请创建一个单一的长生命周期 DaprJobsClient 实例,并在整个应用程序中提供对该共享实例的访问。DaprJobsClient 实例是线程安全的,旨在被共享。
这可以通过利用依赖注入功能来辅助实现。注册方法支持注册为单例、作用域实例或瞬态(意味着每次注入时都会重新创建),但还支持注册以利用来自 IConfiguration 或其他注入服务的值,这在每个类中从头开始创建客户端时是不切实际的。
避免为每次操作创建一个 DaprJobsClient 并在操作完成时将其释放。
通过 DaprJobsClientBuilder 配置 DaprJobsClient
可以通过在调用 .Build() 创建客户端本身之前调用 DaprJobsClientBuilder 类上的方法来配置 DaprJobsClient。每个 DaprJobsClient 的设置是独立的,在调用 .Build() 后无法更改。
var daprJobsClient = new DaprJobsClientBuilder()
.UseDaprApiToken("abc123") // 指定用于向其他 Dapr 边车进行身份验证的 API 令牌
.Build();
DaprJobsClientBuilder 包含以下设置:
- Dapr 边车的 HTTP 端点
- Dapr 边车的 gRPC 端点
- 用于配置 JSON 序列化的
JsonSerializerOptions对象 - 用于配置 gRPC 的
GrpcChannelOptions对象 - 用于对边车请求进行身份验证的 API 令牌
- 用于创建 SDK 使用的
HttpClient实例的工厂方法 - SDK 在向边车发出请求时
HttpClient实例使用的超时时间
SDK 将读取以下环境变量来配置默认值:
DAPR_HTTP_ENDPOINT:用于查找 Dapr 边车的 HTTP 端点,例如:https://dapr-api.mycompany.comDAPR_GRPC_ENDPOINT:用于查找 Dapr 边车的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.comDAPR_HTTP_PORT:如果未设置DAPR_HTTP_ENDPOINT,则用于查找 Dapr 边车的 HTTP 本地端点DAPR_GRPC_PORT:如果未设置DAPR_GRPC_ENDPOINT,则用于查找 Dapr 边车的 gRPC 本地端点DAPR_API_TOKEN:用于设置 API 令牌
配置 gRPC 通道选项
Dapr 使用 CancellationToken 进行取消依赖于 gRPC 通道选项的配置。如果您需要自己配置这些选项,请确保启用 ThrowOperationCanceledOnCancellation 设置。
var daprJobsClient = new DaprJobsClientBuilder()
.UseGrpcChannelOptions(new GrpcChannelOptions { ... ThrowOperationCanceledOnCancellation = true })
.Build();
在 DaprJobsClient 中使用取消操作
DaprJobsClient 上的 API 执行异步操作并接受一个可选的 CancellationToken 参数。这遵循 .NET 可取消操作的标准实践。请注意,当取消发生时,无法保证远程端点停止处理请求,只能保证客户端已停止等待完成。
当操作被取消时,它将抛出 OperationCancelledException。
通过依赖注入配置 DaprJobsClient
使用内置的扩展方法在依赖注入容器中注册 DaprJobsClient 可以带来以下好处:只需一次注册长生命周期服务、集中化复杂配置,并通过在可能的情况下重新使用类似的长生命周期资源(例如 HttpClient 实例)来提高性能。
提供了三种重载,以便开发人员为其场景配置客户端时具有最大的灵活性。如果尚未注册 IHttpClientFactory,每种方法都会代表您注册它,并配置 DaprJobsClientBuilder 在创建 HttpClient 实例时使用它,以便尽可能重复使用相同的实例,并避免套接字耗尽和其他问题。
在第一种方法中,开发人员不进行任何配置,DaprJobsClient 使用默认设置进行配置。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprJobsClient(); // 注册 `DaprJobsClient` 以便按需注入
var app = builder.Build();
有时开发人员需要使用上面详述的各种配置选项来配置创建的客户端。这是通过传入 DaprJobsClientBuilder 的重载来完成的,该重载公开了用于配置必要选项的方法。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprJobsClient((_, daprJobsClientBuilder) => {
// 设置 API 令牌
daprJobsClientBuilder.UseDaprApiToken("abc123");
// 指定非标准的 HTTP 端点
daprJobsClientBuilder.UseHttpEndpoint("http://dapr.my-company.com");
});
var app = builder.Build();
最后,开发人员可能需要从另一个服务检索信息以填充这些配置值。该值可能来自 DaprClient 实例、特定于供应商的 SDK 或某些本地服务,但只要它也在 DI 中注册,就可以通过最后一个重载注入到此配置操作中:
var builder = WebApplication.CreateBuilder(args);
// 注册一个从某处检索机密的虚构服务
builder.Services.AddSingleton<SecretService>();
builder.Services.AddDaprJobsClient((serviceProvider, daprJobsClientBuilder) => {
// 从服务提供程序检索 `SecretService` 的实例
var secretService = serviceProvider.GetRequiredService<SecretService>();
var daprApiToken = secretService.GetSecret("DaprApiToken").Value;
// 配置 `DaprJobsClientBuilder`
daprJobsClientBuilder.UseDaprApiToken(daprApiToken);
});
var app = builder.Build();
理解 DaprJobsClient 上的负载序列化
虽然 DaprClient 上有许多方法可以使用 System.Text.Json 序列化程序自动序列化和反序列化数据,但此 SDK 采用不同的理念。相反,相关方法接受一个可选的 ReadOnlyMemory<byte> 负载,这意味着序列化是留给开发人员的练习,通常不由 SDK 处理。
也就是说,对于每种调度方法,都有一些辅助扩展方法可用。如果您知道要使用可 JSON 序列化的类型,可以对每种调度类型使用 Schedule*WithPayloadAsync 方法,该方法接受一个 object 作为负载,并接受一个可选的 JsonSerializerOptions 在序列化值时使用。为了方便起见,这将把值转换为 UTF-8 编码的字节。这是调度 Cron 表达式时的示例:
public sealed record Doodad (string Name, int Value);
//...
var doodad = new Doodad("Thing", 100);
await daprJobsClient.ScheduleCronJobWithPayloadAsync("myJob", "5 * * * *", doodad);
同样,如果您有一个纯字符串值,可以使用同一方法的重载来序列化字符串类型的负载,将跳过 JSON 序列化步骤,并且只会编码为 UTF-8 编码的字节数组。这是调度一次性作业时的示例:
var now = DateTime.UtcNow;
var oneWeekFromNow = now.AddDays(7);
await daprJobsClient.ScheduleOneTimeJobWithPayloadAsync("myOtherJob", oneWeekFromNow, "This is a test!");
处理作业调用的委托期望至少存在两个参数:
- 一个填充了
jobName的string,提供被调用作业的名称 - 一个填充了作业注册期间最初提供的字节的
ReadOnlyMemory<byte>
由于负载存储为 ReadOnlyMemory<byte>,开发人员可以自由地序列化和反序列化,但同样包含两个辅助扩展,可以将其反序列化为 JSON 兼容类型或字符串。两种方法都假定开发人员对最初调度的作业进行了编码(可能使用了辅助序列化方法),因为这些方法不会强制字节表示它们不是的内容。
要将字节反序列化为字符串,可以使用以下辅助方法:
var payloadAsString = Encoding.UTF8.GetString(jobPayload.Span); // 如果成功,则返回包含值的字符串
错误处理
如果在 SDK 和运行在 Dapr 边车上的 Jobs API 服务之间遇到问题,DaprJobsClient 上的方法将抛出 DaprJobsServiceException。如果由于通过此 SDK 向 Jobs API 服务发出的格式错误的请求而导致失败,将抛出 DaprMalformedJobException。在非法参数值的情况下,将抛出相应的标准异常(例如 ArgumentOutOfRangeException 或 ArgumentNullException)以及违规参数的名称。对于其他任何情况,将抛出 DaprException。
最常见的失败情况与以下内容相关:
- 与 Jobs API 交互时参数格式不正确
- 瞬态故障,例如网络问题
- 无效数据,例如无法将值反序列化回最初未序列化的类型
在任何这些情况下,您都可以通过 .InnerException 属性检查更多异常详细信息。
2.1.6 - Dapr Cryptography .NET SDK
使用 Dapr Cryptography 包,您可以执行高性能的加密和解密操作。
要开始使用此功能,请阅读 [Dapr Cryptography(https://docs.dapr.io/zh-hans/developing-applications/sdks/dotnet/dotnet-cryptography/dotnet-cryptography-howto/) 操作指南。
2.1.6.1 - Dapr Cryptography Client
Dapr Cryptography 包允许您执行由 Dapr 边车提供的加密和解密操作。
生命周期管理
DaprEncryptionClient 是 Dapr 客户端的专用版本,专门用于与 Dapr Cryptography API 交互。它可以与 DaprClient 及其他 Dapr 客户端一起注册而不会出现问题。
它维护与网络资源的连接,这些资源以用于与 Dapr 边车通信的 TCP 套接字形式存在。
为了获得最佳性能,请创建一个 DaprEncryptionClient 的长期实例,并在整个应用程序中提供对该共享实例的访问。DaprEncryptionClient 实例是线程安全的,旨在共享使用。
利用依赖注入功能可以辅助实现这一点。注册方法支持将其注册为单例、作用域实例或瞬态(意味着每次注入时都会重新创建),此外还支持注册时使用来自 IConfiguration 或其他注入服务的值,这在每个类中从头创建客户端时是不切实际的。
避免为每次操作都创建一个 DaprEncryptionClient。
通过 DaprEncryptionClientBuilder 配置 DaprEncryptionClient
可以通过在调用 .Build() 创建客户端本身之前调用 DaprEncryptionClientBuilder 类上的方法来配置 DaprCryptographyClient。每个 DaprEncryptionClientBuilder 的设置是独立的,在调用 .Build() 后无法更改。
var daprEncryptionClient = new DaprEncryptionClientBuilder()
.UseDaprApiToken("abc123") // 指定用于向 Dapr 边车进行身份验证的 API 令牌
.Build();
DaprEncryptionClientBuilder 包含以下设置:
- Dapr 边车的 HTTP 端点
- Dapr 边车的 gRPC 端点
- 用于配置 JSON 序列化的
JsonSerializerOptions对象 - 用于配置 gRPC 的
GrpcChannelOptions对象 - 用于向边车验证请求的 API 令牌
- 用于创建 SDK 使用的
HttpClient实例的工厂方法 - 在向边车发出请求时
HttpClient实例使用的超时时间
SDK 将读取以下环境变量以配置默认值:
DAPR_HTTP_ENDPOINT:用于查找 Dapr 边车的 HTTP 端点,例如:https://dapr-api.mycompany.comDAPR_GRPC_ENDPOINT:用于查找 Dapr 边车的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.comDAPR_HTTP_PORT:如果未设置DAPR_HTTP_ENDPOINT,则用于查找 Dapr 边车的 HTTP 本地端点DAPR_GRPC_PORT:如果未设置DAPR_GRPC_ENDPOINT,则用于查找 Dapr 边车的 gRPC 本地端点DAPR_API_TOKEN:用于设置 API 令牌
配置 gRPC 通道选项
Dapr 使用 CancellationToken 进行取消操作依赖于 gRPC 通道选项的配置。如果您需要自己配置这些选项,请确保启用 ThrowOperationCanceledOnCancellation 设置。
var daprEncryptionClient = new DaprEncryptionClientBuilder()
.UseGrpcChannelOptions(new GrpcChannelOptions { .. ThrowOperationCanceledOnCancellation = true })
.Build();
使用 DaprEncryptionClient 进行取消操作
DaprEncryptionClient 上的 API 执行异步操作并接受可选的 CancellationToken 参数。这遵循了 .NET 中可取消操作的标准实践。请注意,当发生取消时,无法保证远程端点停止处理请求,只能保证客户端已停止等待完成。
当操作被取消时,它将抛出 OperationCancelledException。
通过依赖注入配置 DaprEncryptionClient
使用内置扩展方法在依赖注入容器中注册 DaprEncryptionClient 可以带来以下好处:只需注册一次长期服务、集中复杂配置,并通过在可能的情况下重用类似的长期资源(例如 HttpClient 实例)来提高性能。
提供了三种重载,以便开发人员为其场景配置客户端时获得最大的灵活性。如果尚未注册 IHttpClientFactory,每个重载都将代表您注册它,并配置 DaprEncryptionClientBuilder 在创建 HttpClient 实例时使用它,以便尽可能重用同一实例并避免套接字耗尽和其他问题。
在第一种方法中,开发人员不进行任何配置,DaprEncryptionClient 使用默认设置进行配置。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprEncryptionClent(); // 注册 `DaprEncryptionClient` 以便在需要时注入
var app = builder.Build();
有时开发人员需要使用上面详述的各种配置选项来配置创建的客户端。这是通过传入 DaprEncryptionClientBuiler 的重载来完成的,并公开了配置必要选项的方法。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprEncryptionClient((_, daprEncrpyptionClientBuilder) => {
// 设置 API 令牌
daprEncryptionClientBuilder.UseDaprApiToken("abc123");
// 指定非标准 HTTP 端点
daprEncryptionClientBuilder.UseHttpEndpoint("http://dapr.my-company.com");
});
var app = builder.Build();
最后,开发人员可能需要从另一个服务检索信息以填充这些配置值。该值可以从 DaprClient 实例、供应商特定的 SDK 或某些本地服务提供,但只要它也在 DI 中注册,就可以通过最后一个重载注入到此配置操作中:
var builder = WebApplication.CreateBuilder(args);
// 注册从某处检索机密的虚构服务
builder.Services.AddSingleton<SecretService>();
builder.Services.AddDaprEncryptionClient((serviceProvider, daprEncryptionClientBuilder) => {
// 从服务提供程序检索 `SecretService` 的实例
var secretService = serviceProvider.GetRequiredService<SecretService>();
var daprApiToken = secretService.GetSecret("DaprApiToken").Value;
// 配置 `DaprEncryptionClientBuilder`
daprEncryptionClientBuilder.UseDaprApiToken(daprApiToken);
});
var app = builder.Build();
2.1.6.2 - 如何操作:在 .NET SDK 中创建和使用 Dapr Cryptography
前置条件
安装
要开始使用 Dapr Cryptography 客户端,请从 NuGet 安装 Dapr.Cryptography 包:
dotnet add package Dapr.Cryptography
DaprEncryptionClient 保持对网络资源的访问,这些资源以用于与 Dapr 边车通信的 TCP 套接字形式存在。
依赖注入
AddDaprEncryptionClient() 方法会将 Dapr 客户端注册到依赖注入容器中,这是使用此包的推荐方式。该方法接受一个可选的 options 委托用于配置 DaprEncryptionClient,以及一个 ServiceLifetime 参数,允许您为注册的服务指定不同的生命周期,而不是使用默认的 Singleton 值。
以下示例假设所有默认值都是可接受的,足以注册 DaprEncryptionClient:
services.AddDaprEncryptionClient();
可选的配置委托用于通过在 DaprEncryptionClientBuilder 上指定选项来配置 DaprEncryptionClient,如下例所示:
services.AddSingleton<DefaultOptionsProvider>();
services.AddDaprEncryptionClient((serviceProvider, clientBuilder) => {
// 注入一个服务以从中获取值
var optionsProvider = serviceProvider.GetRequiredService<DefaultOptionsProvider>();
var standardTimeout = optionsProvider.GetStandardTimeout();
// 在客户端构建器上配置值
clientBuilder.UseTimeout(standardTimeout);
});
手动实例化
除了使用依赖注入外,也可以使用静态客户端构建器构建 DaprEncryptionClient。
为了获得最佳性能,请创建单个长期存在的 DaprEncryptionClient 实例,并在整个应用程序中提供对该共享实例的访问。DaprEncryptionClient 实例是线程安全的,旨在共享。
避免为每次操作创建 DaprEncryptionClient。
可以通过在调用 .Build() 创建客户端之前调用 DaprEncryptionClientBuilder 类上的方法来配置 DaprEncryptionClient。每个 DaprEncryptionClient 的设置是独立的,无法在调用 .Build() 后更改。
var daprEncryptionClient = new DaprEncryptionClientBuilder()
.UseJsonSerializerSettings( ... ) // 配置 JSON 序列化器
.Build();
有关通过构建器配置 Dapr 客户端时可用选项的更多信息,请参阅 .NET 文档。
试用一下
测试 Dapr AI .NET SDK。浏览示例以查看 Dapr 的实际应用:
| SDK 示例 | 描述 |
|---|---|
| SDK 示例 | 克隆 SDK 存储库以尝试一些示例并开始使用。 |
2.1.7 - Dapr Messaging .NET SDK
使用 Dapr Messaging 包,你可以从 .NET 应用程序与 Dapr messaging API 交互。在 v1.15 版本中,此包仅包含与流式发布订阅功能对应的功能。
未来的 Dapr .NET SDK 版本会将现有的 messaging 功能从 Dapr.Client 迁移到此 Dapr.Messaging 包。这将提前在发行说明、文档和过时属性中进行说明。
要开始使用,请阅读 Dapr Messaging 操作指南,并参考最佳实践文档获取更多指导。
2.1.7.1 - 如何操作:使用 .NET SDK 创建和管理 Dapr 流式订阅
让我们使用流式处理能力创建一个对发布/订阅主题或队列的订阅。在接下来的演示中,我们将使用此处提供的简单示例,作为说明如何配置消息处理程序的指南,这些配置在运行时进行,无需预先配置端点。在本指南中,您将:
- 部署 .NET Web API 应用程序 (StreamingSubscriptionExample)
- 使用 Dapr .NET Messaging SDK 动态订阅发布/订阅主题。
前置条件
- Dapr CLI
- 已初始化的 Dapr 环境
- 已安装 .NET 8、.NET 9 或 .NET 10
- 已将 Dapr.Messaging NuGet 包安装到您的项目中
设置环境
克隆 .NET SDK 仓库。
git clone https://github.com/dapr/dotnet-sdk.git
从 .NET SDK 根目录导航到 Dapr 流式 PubSub 示例。
cd examples/Client/PublishSubscribe
在本地运行应用程序
要运行 Dapr 应用程序,您需要启动 .NET 程序和 Dapr 边车。导航到 StreamingSubscriptionExample 目录。
cd StreamingSubscriptionExample
我们将运行一个同时启动 Dapr 边车和 .NET 程序的命令。
dapr run --app-id pubsubapp --dapr-grpc-port 4001 --dapr-http-port 3500 -- dotnet run
Dapr 在
http://localhost:3500监听 HTTP 请求,在http://localhost:4001监听内部 Jobs gRPC 请求。
使用依赖注入注册 Dapr PubSub 客户端
Dapr Messaging SDK 提供了一个扩展方法来简化 Dapr PubSub 客户端的注册。在完成 Program.cs 中的依赖注入注册之前,添加以下行:
var builder = WebApplication.CreateBuilder(args);
//Add anywhere between these two
builder.Services.AddDaprPubSubClient(); //That's it
var app = builder.Build();
您可能需要为 Dapr PubSub 客户端提供一些配置选项,这些选项应在每次对边车的调用时都存在,例如 Dapr API 令牌,或者您想使用非标准的 HTTP 或 gRPC 端点。这可以通过使用注册方法的重载来实现,该方法允许配置 DaprPublishSubscribeClientBuilder 实例:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprPubSubClient((_, daprPubSubClientBuilder) => {
daprPubSubClientBuilder.UseDaprApiToken("abc123");
daprPubSubClientBuilder.UseHttpEndpoint("http://localhost:8512"); //Non-standard sidecar HTTP endpoint
});
var app = builder.Build();
不过,您可能希望从其他来源检索要注入的值,而这些值本身已注册为依赖项。您还可以使用另一个重载将 IServiceProvider 注入到配置操作方法中。在以下示例中,我们注册一个虚拟的单例,该单例可以从某处检索机密,并将其传递给 AddDaprJobClient 的配置方法,以便我们可以从其他地方检索我们的 Dapr API 令牌以在此处进行注册:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<SecretRetriever>();
builder.Services.AddDaprPubSubClient((serviceProvider, daprPubSubClientBuilder) => {
var secretRetriever = serviceProvider.GetRequiredService<SecretRetriever>();
var daprApiToken = secretRetriever.GetSecret("DaprApiToken").Value;
daprPubSubClientBuilder.UseDaprApiToken(daprApiToken);
daprPubSubClientBuilder.UseHttpEndpoint("http://localhost:8512");
});
var app = builder.Build();
使用 IConfiguration 配置 Dapr PubSub 客户端
也可以使用已注册的 IConfiguration 中的值来配置 Dapr PubSub 客户端,而无需按照上一节演示的那样使用 DaprPublishSubscribeClientBuilder 显式指定每个值覆盖。相反,通过填充通过依赖注入提供的 IConfiguration,AddDaprPubSubClient() 注册将自动使用这些值而不是其各自的默认值。
首先填充配置中的值。可以通过以下几种不同方式来完成此操作,如下所示。
通过 ConfigurationBuilder 配置
可以在不使用配置源的情况下配置应用程序设置,而是使用 ConfigurationBuilder 实例在内存中填充值:
var builder = WebApplication.CreateBuilder();
//Create the configuration
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> {
{ "DAPR_HTTP_ENDPOINT", "http://localhost:54321" },
{ "DAPR_API_TOKEN", "abc123" }
})
.Build();
builder.Configuration.AddConfiguration(configuration);
builder.Services.AddDaprPubSubClient(); //This will automatically populate the HTTP endpoint and API token values from the IConfiguration
通过环境变量配置
可以从应用程序可用的环境变量访问应用程序设置。
以下环境变量将用于填充注册 Dapr PubSub 客户端所使用的 HTTP 端点和 API 令牌。
| 键 | 值 |
|---|---|
| DAPR_HTTP_ENDPOINT | http://localhost:54321 |
| DAPR_API_TOKEN | abc123 |
var builder = WebApplication.CreateBuilder();
builder.Configuration.AddEnvironmentVariables();
builder.Services.AddDaprPubSubClient();
Dapr PubSub 客户端将被配置为使用 HTTP 端点 http://localhost:54321,并在所有出站请求中填充 API 令牌头 abc123。
通过带前缀的环境变量配置
但是,在不使用容器的共享主机场景中(在同一台机器上运行多个应用程序)或在开发环境中,为环境变量添加前缀并不罕见。以下示例假定 HTTP 端点和 API 令牌都将从带有值 “myapp_” 前缀的环境变量中提取。在此场景中使用的两个环境变量如下:
| 键 | 值 |
|---|---|
| myapp_DAPR_HTTP_ENDPOINT | http://localhost:54321 |
| myapp_DAPR_API_TOKEN | abc123 |
这些环境变量将在以下示例中加载到已注册的配置中,并在不带前缀的情况下可用。
var builder = WebApplication.CreateBuilder();
builder.Configuration.AddEnvironmentVariables(prefix: "myapp_");
builder.Services.AddDaprPubSubClient();
Dapr PubSub 客户端将被配置为使用 HTTP 端点 http://localhost:54321,并在所有出站请求中填充 API 令牌头 abc123。
不依赖依赖注入使用 Dapr PubSub 客户端
虽然使用依赖注入简化了 .NET 中复杂类型的使用,并使处理复杂配置变得更加容易,但您不需要以这种方式注册 DaprPublishSubscribeClient。相反,您也可以选择从 DaprPublishSubscribeClientBuilder 实例创建它的实例,如下所示:
public class MySampleClass
{
public void DoSomething()
{
var daprPubSubClientBuilder = new DaprPublishSubscribeClientBuilder();
var daprPubSubClient = daprPubSubClientBuilder.Build();
//Do something with the `daprPubSubClient`
}
}
设置消息处理程序
Dapr 中的流式订阅实现通过将消息保留在 Dapr 运行时中,直到您的应用程序准备好接受它们为止,从而为您提供更大的控制权来处理来自事件的背压。.NET SDK 支持高性能队列,用于在处理挂起时在应用程序中维护这些消息的本地缓存。这些消息将保留在队列中,直到每个消息的处理超时或对每个消息执行响应操作(通常在处理成功或失败之后)。在 Dapr 运行时收到此响应操作之前,消息将由 Dapr 持久化,并在服务故障的情况下可用。
可用的各种响应操作如下:
| 响应操作 | 描述 |
|---|---|
| Retry | 该事件应在将来再次传递。 |
| Drop | 该事件应被删除(或转发到死信队列,如果已配置)并且不再尝试。 |
| Success | 该事件应被删除,因为它已成功处理。 |
处理程序一次将只接收一条消息,如果向订阅提供了取消令牌,则该令牌将在处理程序调用期间提供。
处理程序必须配置为返回 Task<TopicResponseAction>,以指示这些操作之一,即使来自 try/catch 块。如果您的处理程序未捕获异常,订阅将使用订阅注册期间在选项中配置的响应操作。
以下演示了示例中提供的示例消息处理程序:
Task<TopicResponseAction> HandleMessageAsync(TopicMessage message, CancellationToken cancellationToken = default)
{
try
{
//Do something with the message
Console.WriteLine(Encoding.UTF8.GetString(message.Data.Span));
return Task.FromResult(TopicResponseAction.Success);
}
catch
{
return Task.FromResult(TopicResponseAction.Retry);
}
}
配置和订阅 PubSub 主题
流式订阅的配置需要向 Dapr 注册的 PubSub 组件的名称、正在订阅的主题或队列的名称、提供订阅配置的 DaprSubscriptionOptions、消息处理程序和可选的取消令牌。DaprSubscriptionOptions 唯一必需的参数是默认的 MessageHandlingPolicy,它由每个事件的超时和在该超时时采取的 TopicResponseAction 组成。
其他选项如下:
| 属性名称 | 描述 |
|---|---|
| 元数据 | 附加订阅元数据 |
| 死信主题 | 用于将丢弃的消息发送到的死信主题的可选名称。 |
| 最大排队消息数 | 默认情况下,不会对内部队列强制执行最大边界,但设置此 |
| 属性将强加一个上限。 | |
| 最大清理超时 | 当订阅被释放或令牌发出取消请求时,这指定 |
| 可用于处理内部队列中剩余消息的最长时间。 |
然后,订阅将配置为以下示例:
var messagingClient = app.Services.GetRequiredService<DaprPublishSubscribeClient>();
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60)); //Override the default of 30 seconds
var options = new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(10), TopicResponseAction.Retry));
var subscription = await messagingClient.SubscribeAsync("pubsub", "mytopic", options, HandleMessageAsync, cancellationTokenSource.Token);
终止和清理订阅
当您完成订阅并希望停止接收新事件时,只需在订阅实例上等待对 DisposeAsync() 的调用。这将导致客户端取消注册其他事件,并在释放任何内部资源之前继续处理背压队列中仍然剩余的所有事件(如果有)。此清理将受到订阅注册时在 DaprSubscriptionOptions 中提供的超时间隔的限制,默认情况下,这设置为 30 秒。
2.1.7.2 - DaprPublishSubscribeClient 用法
生命周期管理
DaprPublishSubscribeClient 是 Dapr 客户端的一个版本,专门用于与 Dapr 消息传递 API 交互。
它可以与 DaprClient 和其他 Dapr 客户端一起注册而不会产生问题。
它维护对网络资源的访问,这些资源以用于与 Dapr 边车通信的 TCP 套接字形式存在,并实现
IAsyncDisposable 以支持资源的积极清理。
为获得最佳性能,应创建一个 DaprPublishSubscribeClient 的单一长期实例,并在整个应用程序中提供对该共享
实例的访问。DaprPublishSubscribeClient 实例是线程安全的,旨在共享使用。
可以利用依赖注入功能来辅助实现这一点。注册方法支持注册为
单例、作用域实例或瞬态(意味着每次注入时都会重新创建),但同时也支持
利用 IConfiguration 或其他注入服务的值进行注册,这在每次
从头创建客户端时是不切实际的。
避免为每个操作创建一个 DaprPublishSubscribeClient 并在操作完成时将其释放。
DaprPublishSubscribeClient 仅应在您不再希望在订阅上接收事件时才被释放,因为释放它将取消新事件的持续接收。
通过 DaprPublishSubscribeClientBuilder 配置 DaprPublishSubscribeClient
可以通过在调用 .Build() 创建客户端本身之前调用 DaprPublishSubscribeClientBuilder 类上的方法来配置 DaprPublishSubscribeClient。每个 DaprPublishSubscribeClient 的设置是独立的,
无法在调用 .Build() 后更改。
var daprPubsubClient = new DaprPublishSubscribeClientBuilder()
.UseDaprApiToken("abc123") // 指定用于向其他 Dapr 边车进行身份验证的 API 令牌
.Build();
DaprPublishSubscribeClientBuilder 包含以下设置:
- Dapr 边车的 HTTP 端点
- Dapr 边车的 gRPC 端点
- 用于配置 JSON 序列化的
JsonSerializerOptions对象 - 用于配置 gRPC 的
GrpcChannelOptions对象 - 用于向边车验证请求的 API 令牌
- 用于创建 SDK 使用的
HttpClient实例的工厂方法 - 在向边车发出请求时
HttpClient实例使用的超时时间
SDK 将读取以下环境变量来配置默认值:
DAPR_HTTP_ENDPOINT:用于查找 Dapr 边车的 HTTP 端点,示例:https://dapr-api.mycompany.comDAPR_GRPC_ENDPOINT:用于查找 Dapr 边车的 gRPC 端点,示例:https://dapr-grpc-api.mycompany.comDAPR_HTTP_PORT:如果未设置DAPR_HTTP_ENDPOINT,则使用此项来查找 Dapr 边车的 HTTP 本地端点DAPR_GRPC_PORT:如果未设置DAPR_GRPC_ENDPOINT,则使用此项来查找 Dapr 边车的 gRPC 本地端点DAPR_API_TOKEN:用于设置 API 令牌
配置 gRPC 通道选项
Dapr 使用 CancellationToken 进行取消依赖于 gRPC 通道选项的配置。如果您
需要自己配置这些选项,请确保启用 ThrowOperationCanceledOnCancellation 设置。
var daprPubsubClient = new DaprPublishSubscribeClientBuilder()
.UseGrpcChannelOptions(new GrpcChannelOptions { ... ThrowOperationCanceledOnCancellation = true })
.Build();
在 DaprPublishSubscribeClient 中使用取消
DaprPublishSubscribeClient 上的 API 执行异步操作并接受一个可选的 CancellationToken
参数。这遵循 .NET 用于可取消操作的标准实践。请注意,当发生取消时,无法
保证远程端点停止处理请求,只能保证客户端已停止等待完成。
当操作被取消时,它将抛出 OperationCancelledException。
通过依赖注入配置 DaprPublishSubscribeClient
使用用于在依赖注入容器中注册 DaprPublishSubscribeClient 的内置扩展方法
可以带来以下好处:一次性注册长期服务、集中复杂配置,并通过确保类似的长期资源在可能的情况下被重用(例如 HttpClient 实例)来提高性能。
有三种重载可用,为开发人员在其场景中配置客户端提供最大的灵活性。
如果尚未注册,每个重载都将代表您注册 IHttpClientFactory,并配置
DaprPublishSubscribeClientBuilder 在创建 HttpClient 实例时使用它,以便尽可能重用同一实例并避免套接字耗尽和其他问题。
在第一种方法中,开发人员不进行任何配置,DaprPublishSubscribeClient 使用默认设置进行配置。
var builder = WebApplication.CreateBuilder(args);
builder.Services.DaprPublishSubscribeClient(); //注册 `DaprPublishSubscribeClient` 以便根据需要注入
var app = builder.Build();
有时开发人员需要使用上述各种配置选项来配置创建的客户端。这是通过传入 DaprJobsClientBuiler 的重载来完成的,该重载公开了配置必要选项的方法。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprJobsClient((_, daprPubSubClientBuilder) => {
//设置 API 令牌
daprPubSubClientBuilder.UseDaprApiToken("abc123");
//指定非标准的 HTTP 端点
daprPubSubClientBuilder.UseHttpEndpoint("http://dapr.my-company.com");
});
var app = builder.Build();
最后,开发人员可能需要从另一个服务检索信息以填充这些配置值。该值可以从 DaprClient 实例、供应商特定的 SDK 或某个本地服务提供,但只要它也在 DI 中注册,就可以通过最后一个重载注入到此配置操作中:
var builder = WebApplication.CreateBuilder(args);
//注册一个从某处检索机密的虚构服务
builder.Services.AddSingleton<SecretService>();
builder.Services.AddDaprPublishSubscribeClient((serviceProvider, daprPubSubClientBuilder) => {
//从服务提供程序检索 `SecretService` 的实例
var secretService = serviceProvider.GetRequiredService<SecretService>();
var daprApiToken = secretService.GetSecret("DaprApiToken").Value;
//配置 `DaprPublishSubscribeClientBuilder`
daprPubSubClientBuilder.UseDaprApiToken(daprApiToken);
});
var app = builder.Build();
2.1.8 - Dapr 分布式锁 .NET SDK
使用 Dapr 分布式锁包,您可以在资源上创建和移除锁,以管理分布式应用程序之间的独占性。
虽然此功能在 Dapr.Client 和 Dapr.DistributedLock 包中均有实现,但两者之间的方法略有不同,未来版本将弃用 Dapr.Client 包。建议新实现使用 Dapr.DistributedLock 包。本文档将反映 Dapr.DistributedLock 包中的实现。
生命周期管理
DaprDistributedLockClient 是专门用于与 Dapr 分布式锁 API 交互的 Dapr 客户端版本。它可以与 DaprClient 和其他 Dapr 客户端一起注册,不会有任何问题。
它维护对网络资源的访问,这些资源以用于与 Dapr 边车运行时通信的 TCP 套接字形式存在。
为了获得最佳性能,建议您利用 Dapr.DistributedLock 包提供的依赖注入容器机制,以便在整个应用程序中轻松访问注入的实例。这些注入的实例是线程安全的,旨在应用程序的不同类型之间使用。通过依赖注入注册可以利用 IConfiguration 中的值或其他注入的服务,这在每个类中从头创建客户端时是不切实际的。
如果您选择手动创建 DaprDistributedLockClient 实例,建议使用 DaprClientBuilder 来创建客户端。这将确保客户端正确配置以与 Dapr 边车运行时通信。
避免为每个操作创建 DaprDistributedLockClient。
通过 DaprDistributedLockBuilder 配置 DaprDistributedLockClient
可以通过在调用 .Build() 创建客户端本身之前调用 DaprDistributedLockBuilder 类上的方法来配置 DaprDistributedLockClient。每个 DaprDistributedLockClient 的设置是独立的,在调用 .Build() 后无法更改。
var daprDistributedLockClient = new DaprDistributedLockBuilder()
.UseDaprApiToken("abc123") // 可选地指定用于向其他 Dapr 边车进行身份验证的 API 令牌
.Build();
DaprDistributedLockBuilder 包含以下设置:
- Dapr 边车的 HTTP 端点
- Dapr 边车的 gRPC 端点
- 用于配置 JSON 序列化的
JsonSerializerOptions对象 - 用于配置 gRPC 的
GrpcChannelOptions对象 - 用于向边车验证请求的 API 令牌
- 用于创建 SDK 使用的
HttpClient实例的工厂方法 - 向边车发出请求时
HttpClient实例使用的超时时间
SDK 将读取以下环境变量来配置默认值:
DAPR_HTTP_ENDPOINT:用于查找 Dapr 边车的 HTTP 端点,例如:https://dapr-api.mycompany.comDAPR_GRPC_ENDPOINT:用于查找 Dapr 边车的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.comDAPR_HTTP_PORT:如果未设置DAPR_HTTP_ENDPOINT,则用于查找 Dapr 边车的 HTTP 本地端点DAPR_GRPC_PORT:如果未设置DAPR_GRPC_ENDPOINT,则用于查找 Dapr 边车的 gRPC 本地端点DAPR_API_TOKEN:用于设置 API 令牌
配置 gRPC 通道选项
Dapr 使用 CancellationToken 进行取消依赖于 gRPC 通道选项的配置。如果您需要自己配置这些选项,请确保启用 ThrowOperationCanceledOnCancellation 设置。
var daprDistributedLockClient = new DaprDistributedLockBuilder()
.UseGrpcChannelOptions(new GrpcChannelOptions { ... ThrowOperationCanceledOnCancellation = true })
.Build();
在 DaprDistributedLockClient 中使用取消
DaprDistributedLockClient 上的 API 执行异步操作并接受可选的 CancellationToken 参数。这遵循了 .NET 可取消操作的标准实践。请注意,当发生取消时,无法保证远程端点停止处理请求,只能保证客户端已停止等待完成。
当操作被取消时,它将抛出 OperationCancelledException。
通过依赖注入配置 DaprDistributedLockClient
使用内置扩展方法在依赖注入容器中注册 DaprDistributedLockClient 可以提供一次注册长期服务的优势,集中化复杂配置,并通过确保在可能的情况下重用类似的长期资源(例如 HttpClient 实例)来提高性能。
有三种重载可用,为开发人员为其场景配置客户端提供最大的灵活性。如果尚未注册,每个重载都会代表您注册 IHttpClientFactory,并配置 DaprDistributedLockBuilder 在创建 HttpClient 实例时使用它,以便尽可能重用同一实例并避免套接字耗尽和其他问题。
在第一种方法中,开发人员不进行任何配置,DaprDistributedLockClient 使用默认设置配置。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprDistributedLock(); // 根据需要注册 `DaprDistributedLockClient` 以进行注入
var app = builder.Build();
有时开发人员需要使用上面详述的各种配置选项来配置创建的客户端。这是通过传入 DaprDistributedLockBuilder 并公开配置必要选项的方法的重载来完成的。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprDistributedLock((_, daprDistributedLockBuilder) => {
// 设置 API 令牌
daprDistributedLockBuilder.UseDaprApiToken("abc123");
// 指定非标准的 HTTP 端点
daprDistributedLockBuilder.UseHttpEndpoint("http://dapr.my-company.com");
});
var app = builder.Build();
最后,开发人员可能需要从另一个服务检索信息以填充这些配置值。该值可以从 DaprClient 实例、供应商特定的 SDK 或某些本地服务提供,但只要它也在 DI 中注册,就可以通过最后一个重载注入到此配置操作中:
var builder = WebApplication.CreateBuilder(args);
// 注册一个从某处检索机密的虚构服务
builder.Services.AddSingleton<SecretService>();
builder.Services.AddDaprDistributedLock((serviceProvider, daprDistributedLockBuilder) => {
// 从服务提供程序检索 `SecretService` 的实例
var secretService = serviceProvider.GetRequiredService<SecretService>();
var daprApiToken = secretService.GetSecret("DaprApiToken").Value;
// 配置 `DaprDistributedLockBuilder`
daprDistributedLockBuilder.UseDaprApiToken(daprApiToken);
});
var app = builder.Build();
2.1.8.1 - 操作指南:在 .NET SDK 中创建和使用 Dapr 分布式锁
前置条件
安装
要开始使用 Dapr 分布式锁 .NET SDK 客户端,请从 NuGet 安装 Dapr.Distributed Lock 包:
dotnet add package Dapr.DistributedLock
DaprDistributedLockClient 以 TCP 套接字的形式维护对网络资源的访问,用于与 Dapr 边车通信。
依赖注入
AddDaprDistributedLock() 方法会将 Dapr 客户端注册到 ASP.NET Core 依赖注入中,这是使用此包的推荐方式。此方法接受一个可选的 options 委托用于配置 DaprDistributedLockClient,以及一个 ServiceLifetime 参数,允许您为注册的服务指定不同的生命周期,而不是使用默认的 Singleton 值。
以下示例假定所有默认值均可接受,足以注册 DaprDistributedLockClient:
services.AddDaprDistributedLock();
可选的 configuration 委托用于通过在 DaprDistributedLockBuilder 上指定选项来配置 DaprDistributedLockClient,如以下示例所示:
services.AddSingleton<DefaultOptionsProvider>();
services.AddDaprDistributedLock((serviceProvider, clientBuilder) => {
// 注入服务以从中获取值
var optionsProvider = serviceProvider.GetRequiredService<DefaultOptionsProvider>();
var standardTimeout = optionsProvider.GetStandardTimeout();
// 在客户端构建器上配置值
clientBuilder.UseTimeout(standardTimeout);
});
手动实例化
除了使用依赖注入,也可以使用静态客户端构建器来构建 DaprDistributedLockClient。
为了获得最佳性能,请创建一个单一的长生命周期 DaprDistributedLockClient 实例,并在整个应用程序中提供对该共享实例的访问。DaprDistributedLockClient 实例是线程安全的,旨在共享使用。
避免为每次操作创建一个 DaprDistributedLockClient。
可以通过在调用 .Build() 创建客户端之前调用 DaprDistributedLockBuilder 类上的方法来配置 DaprDistributedLockClient。每个 DaprDistributedLockClient 的设置是独立的,在调用 .Build() 后无法更改。
var daprDistributedLockClient = new DaprDistributedLockBuilder()
.UseJsonSerializerSettings( ... ) // 配置 JSON 序列化器
.Build();
有关通过构建器配置 Dapr 分布式锁客户端时可用的选项的更多信息,请参阅 .NET 文档此处。
试用
试用 Dapr 分布式锁 .NET SDK。浏览示例,查看 Dapr 的实际运行:
| SDK 示例 | 描述 |
|---|---|
| SDK 示例 | 克隆 SDK 存储库以尝试一些示例并开始使用。 |
构建块
.NET SDK 的这一部分允许您与分布式锁 API 交互,以放置和移除锁,从而管理分布式应用程序中的资源独占性。
2.1.9 - Dapr .NET SDK 最佳实践
以信心构建
Dapr .NET SDK 提供了丰富的功能集用于构建分布式应用。本节提供在生产场景中高效使用 SDK 的实用指南——聚焦于可靠性、可维护性和开发者体验。
涵盖的主题包括:
- Dapr 构建块中的错误处理策略
- 管理实验性功能并抑制相关警告
- 利用源代码分析器和生成器来减少样板代码并及早发现问题
- 使用 Dapr.Testcontainers 运行集成测试
- 基于 Dapr 应用的通用 .NET 开发实践
错误模型指南
Dapr 操作可能因多种原因而失败——网络问题、组件配置错误或瞬态故障。SDK 提供了结构化的错误类型,帮助您区分可重试错误和致命错误。
了解如何有效使用 DaprException 及其派生类型详见此处。
实验性属性
部分 SDK 功能被标记为实验性,可能在未来的版本中发生变化。这些功能使用 [Experimental] 进行标注,默认情况下会生成构建时警告。您可以:
- 使用
#pragma warning disable选择性抑制警告 - 使用
SuppressMessage属性进行更精细的控制 - 跟踪整个代码库中的实验性功能使用情况
了解更多关于 [Experimental] 属性的使用详见此处。
源代码工具
SDK 包含基于 Roslyn 的分析器和源代码生成器,帮助您更轻松地编写更好的代码。这些工具能够:
- 对 SDK 的常见误用发出警告
- 为 Actor 注册和调用生成样板代码
- 支持 IDE 集成以提供更快的反馈
阅读更多关于如何安装和使用这些分析器的内容详见此处。
其他指南
本节旨在支持广泛的开发场景。随着应用复杂性的增长,您会发现越来越多与 .NET 中 Dapr 相关的实践和模式——从 Actor 生命周期管理到配置策略和性能调优。
了解如何使用 Dapr.Testcontainers 运行集成测试详见此处。
2.1.9.1 - Dapr .NET SDK 中的错误模型
Dapr .NET SDK 支持 Dapr 运行时实现的更丰富的错误模型。该模型为应用程序提供了一种使用额外上下文来丰富错误的方式, 使应用程序的使用者能够更好地理解问题并更快地解决它。您可以在这里阅读有关更丰富的错误模型的更多信息,并可以在这里找到实现这些错误的 Dapr proto 文件。
Dapr .NET SDK 实现了 Dapr 运行时支持的所有详细信息,在 Dapr.Common.Exceptions 命名空间中实现,并通过
DaprException 扩展方法 TryGetExtendedErrorInfo 访问。目前,此详细信息提取仅支持存在详细信息的
RpcException。
// ExtendedErrorInfo 的使用示例
try
{
// 使用 Dapr 客户端执行某些会抛出 DaprException 的操作。
}
catch (DaprException daprEx)
{
if (daprEx.TryGetExtendedErrorInfo(out DaprExtendedErrorInfo errorInfo))
{
Console.WriteLine(errorInfo.Code);
Console.WriteLine(errorInfo.Message);
foreach (DaprExtendedErrorDetail detail in errorInfo.Details)
{
Console.WriteLine(detail.ErrorType);
switch (detail.ErrorType)
{
case ExtendedErrorType.ErrorInfo:
Console.WriteLine(detail.Reason);
Console.WriteLine(detail.Domain);
break;
default:
Console.WriteLine(detail.TypeUrl);
break;
}
}
}
}
DaprExtendedErrorInfo
包含与错误关联的 Code(状态代码)和 Message(错误消息),从内部的 RpcException 解析而来。
还包含从异常的详细信息中解析的 DaprExtendedErrorDetails 集合。
DaprExtendedErrorDetail
所有详细信息都实现抽象的 DaprExtendedErrorDetail 并具有关联的 DaprExtendedErrorType。
RetryInfo
通知客户端在重试之前应等待多长时间的信息。提供一个带有 Second(秒偏移量)和 Nano(纳秒偏移量)属性的
DaprRetryDelay。
DebugInfo
服务器提供的调试信息。包含 StackEntries(包含堆栈跟踪的字符串集合)和 Detail(进一步的调试信息)。
QuotaFailure
与可能已达到的配额相关的信息,例如 API 的每日使用限制。它有一个属性 Violations,
DaprQuotaFailureViolation 的集合,每个集合包含一个 Subject(请求的主题)和 Description(有关失败的更多信息)。
PreconditionFailure
通知客户端某些必需的前提条件未满足的信息。有一个属性 Violations,DaprPreconditionFailureViolation 的集合,
每个集合具有 Subject(前提条件失败发生的主题,例如 “Azure”)、Type(前提条件类型的表示,例如 “TermsOfService”)和
Description(进一步描述,例如 “ToS must be accepted.")。
RequestInfo
服务器返回的信息,服务器可以使用该信息来标识客户端的请求。包含 RequestId 和 ServingData 属性,
RequestId 是服务器可以解释的某个字符串(例如 UID),ServingData 是构成请求的一部分的任意数据。
LocalizedMessage
包含本地化消息以及消息的区域设置。包含 Locale(区域设置,例如 “en-US”)和 Message(本地化消息)。
BadRequest
描述错误的请求字段。包含 DaprBadRequestDetailFieldViolation 集合,每个集合具有 Field(请求中的错误字段,例如 ‘first_name’)和
Description(详细说明原因的更多信息,例如 “first_name cannot contain special characters”)。
ErrorInfo
详细说明错误的原因。包含三个属性:Reason(错误的原因,应采用 UPPER_SNAKE_CASE 的形式,例如 DAPR_INVALID_KEY)、
Domain(错误所属的域,例如 ‘dapr.io’)和 Metadata,一个包含更多信息的基于键/值的集合。
Help
为客户端提供资源以对问题进行进一步研究。包含 DaprHelpDetailLink 集合,
该集合提供 Url(指向帮助或文档的 url)和 Description(对链接提供的描述)。
ResourceInfo
提供与访问的资源相关的信息。提供三个属性:ResourceType(正在访问的资源的类型,例如 “Azure service bus”)、
ResourceName(资源的名称,例如 “my-configured-service-bus”)、Owner(资源的所有者,例如 “subscriptionowner@dapr.io”)和
Description(与错误相关的资源的更多信息,例如 “missing permissions to use this resource”)。
Unknown
当详细信息类型 url 无法映射到正确的 DaprExtendedErrorDetail 实现时返回。
提供一个属性 TypeUrl(无法解析的类型 url,例如 “type.googleapis.com/Google.rpc.UnrecognizedType”)。
2.1.9.2 - Experimental Attributes
[Experimental] 属性标记某些方法Experimental Attributes
Experimental Attributes 简介
随着 .NET 8 的发布,C# 12 引入了 [Experimental] 属性,它提供了一种标准化的方式来标记仍在开发或实验阶段的 API。该属性定义在 System.Diagnostics.CodeAnalysis 命名空间中,需要一个诊断 ID 参数,用于在使用实验性 API 时生成编译器警告。
在 Dapr .NET SDK 中,我们现在使用 [Experimental] 属性而不是 [Obsolete] 来标记尚未通过稳定生命周期认证的构建块和组件。这种方法提供了更清晰的区分:
Experimental APIs — 已经可用但仍在演进的功能,尚未根据 Dapr Component Certification Lifecycle 认证为稳定。
Obsolete APIs — 真正已被弃用并将在未来版本中移除的功能。
在 Dapr .NET SDK 中的使用
在 Dapr .NET SDK 中,我们在类级别为仍处于 Component Certification Lifecycle 的 Alpha 或 Beta 阶段的构建块应用 [Experimental] 属性。该属性包括:
- 一个用于标识实验性构建块的诊断 ID
- 一个指向该构建块相关文档的 URL
例如:
using System.Diagnostics.CodeAnalysis;
namespace Dapr.Cryptography.Encryption
{
[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")]
public class DaprEncryptionClient
{
// Implementation
}
}
诊断 ID 遵循 DAPR_[BUILDING_BLOCK_NAME] 的命名约定,例如:
DAPR_CONVERSATION— 用于 Conversation 构建块DAPR_CRYPTOGRAPHY— 用于 Cryptography 构建块DAPR_JOBS— 用于 Jobs 构建块DAPR_DISTRIBUTEDLOCK— 用于 Distributed Lock 构建块
抑制 Experimental 警告
当您使用标记有 [Experimental] 属性的 API 时,编译器会生成错误。要在不将自己的代码标记为实验性的情况下构建解决方案,您需要抑制这些错误。以下是几种方法:
选项 1:使用 #pragma 指令
您可以使用 #pragma warning 指令来抑制特定代码段的警告:
// Disable experimental warning
#pragma warning disable DAPR_CRYPTOGRAPHY
// Your code using the experimental API
var client = new DaprEncryptionClient();
// Re-enable the warning
#pragma warning restore DAPR_CRYPTOGRAPHY
这种方法适用于只想抑制代码中特定部分的警告。
选项 2:项目级抑制
要为整个项目抑制警告,请将以下内容添加到您的 .csproj 文件中。
<PropertyGroup>
<NoWarn>$(NoWarn);DAPR_CRYPTOGRAPHY</NoWarn>
</PropertyGroup>
您可以包含多个用分号分隔的诊断 ID:
<PropertyGroup>
<NoWarn>$(NoWarn);DAPR_CONVERSATION;DAPR_JOBS;DAPR_DISTRIBUTEDLOCK;DAPR_CRYPTOGRAPHY</NoWarn>
</PropertyGroup>
这种方法对于需要使用实验性 API 的测试项目特别有用。
选项 3:目录级抑制
要为目录中的多个项目抑制警告,请添加一个 Directory.Build.props 文件:
<PropertyGroup>
<NoWarn>$(NoWarn);DAPR_CONVERSATION;DAPR_JOBS;DAPR_DISTRIBUTEDLOCK;DAPR_CRYPTOGRAPHY</NoWarn>
</PropertyGroup>
该文件应放置在测试项目的根目录中。您可以在 MSBuild 文档 中了解更多关于使用 Directory.Build.props 文件的信息。
Experimental API 的生命周期
随着构建块通过认证生命周期并达到 “Stable” 阶段,[Experimental] 属性将被移除。此时用户无需进行任何迁移或代码更改,只需移除之前添加的警告抑制即可。
相反,[Obsolete] 属性现在将专门保留给真正被弃用并计划移除的 API。当您看到标记有 [Obsolete] 的方法或类时,应根据属性消息中提供的迁移指导计划迁移。
最佳实践
在应用程序代码中:
- 谨慎使用实验性 API,因为它们可能会在未来版本中发生变化
- 考虑将实验性 API 的使用隔离,以便将来更容易更新
- 记录您对实验性 API 的使用,以便团队了解
在测试代码中:
- 使用项目级抑制以避免在测试代码中充斥警告抑制
- 定期审查您使用的实验性 API,并检查它们是否已稳定
在为 SDK 做贡献时:
- 对于尚未完成认证的新构建块使用
[Experimental] - 仅对真正被弃用的 API 使用
[Obsolete] - 在
UrlFormat参数中提供清晰的文档链接
- 对于尚未完成认证的新构建块使用
其他资源
2.1.9.3 - Integration testing with Dapr.Testcontainers
概述
Dapr.Testcontainers 是一个辅助包,用于使用容器针对真实的 Dapr 运行时组件编写集成测试。它封装了 Testcontainers 库来启动 Dapr 边车、控制平面服务(placement 和 scheduler)以及特定 Dapr 构建块所需的基础设施。
重要
Dapr.Testcontainers 是初始版本。API 仍在演进中,可能在未来的版本中发生变化。我们尽量保持变化最小,但在该包成熟的过程中,你应预期可能出现破坏性变更。包
Dapr.Testcontainers(核心 harness 和基础设施)Dapr.Testcontainers.Xunit(可选的 xUnit 辅助工具)
前置条件
- 一个容器运行时(Docker Desktop、Podman 或等效工具)。
- 能够拉取 Dapr 和依赖镜像的网络访问。
核心概念
Dapr.Testcontainers 围绕环境和harness来建模测试:
DaprTestEnvironment:共享基础设施(网络、placement、scheduler、可选 Redis)。当你需要多个应用共享 Dapr 控制平面或运行多应用测试时使用它。DaprHarnessBuilder:为特定构建块(工作流、作业、分布式锁或对话)创建 harness。DaprTestApplicationBuilder:使用 harness 启动你的测试应用,并将 Dapr 端点连接到配置。
基本工作流测试示例
下面的示例反映了 .NET SDK 测试套件中的工作流集成测试,展示了典型设置:
var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components");
await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync(needsActorState: true);
await environment.StartAsync();
var harness = new DaprHarnessBuilder(componentsDir)
.WithEnvironment(environment)
.BuildWorkflow();
await using var testApp = await DaprHarnessBuilder.ForHarness(harness)
.ConfigureServices(builder =>
{
builder.Services.AddDaprWorkflowBuilder(opt =>
{
opt.RegisterWorkflow<TestWorkflow>();
});
})
.BuildAndStartAsync();
using var scope = testApp.CreateScope();
var workflowClient = scope.ServiceProvider.GetRequiredService<DaprWorkflowClient>();
await workflowClient.ScheduleNewWorkflowAsync(nameof(TestWorkflow), input: 42);
配置 Dapr 运行时
DaprRuntimeOptions 让你控制 Dapr 镜像版本、App ID、日志级别和容器日志:
var options = new DaprRuntimeOptions("1.17.0")
.WithAppId("test-app")
.WithLogLevel(DaprLogLevel.Debug)
.WithContainerLogs();
var harness = new DaprHarnessBuilder(componentsDir)
.WithOptions(options)
.BuildWorkflow();
你也可以通过 DAPR_RUNTIME_VERSION 环境变量全局设置运行时版本。
xUnit 辅助工具
如果你使用 xUnit,Dapr.Testcontainers.Xunit 包包含一个辅助特性,可在运行时版本过低时跳过测试:
[MinimumDaprRuntimeFact("1.17.0")]
public async Task RequiresLatestRuntime()
{
// ...
}
后续步骤
- 查看 Dapr .NET SDK 仓库中的集成测试项目(例如
test/Dapr.IntegrationTest.Workflow)。 - 使用与你所测试的构建块匹配的 harness(工作流、作业、分布式锁、对话)。
2.1.9.4 - Dapr 源代码分析器和生成器
Dapr 支持日益丰富的可选 Roslyn 分析器和代码修复提供程序,用于检查代码中的代码质量问题。从 v1.16 版本开始,开发者可以在每个标准功能包的基础上,从 NuGet 安装额外的项目,从而在解决方案中启用这些分析器。
Note
Dapr .NET SDK 的未来版本将默认包含这些分析器,无需单独安装包。规则违规通常标记为 Info 或 Warning,因此如果分析器发现问题,不一定会中断构建。所有代码分析违规都带有前缀 “DAPR”,并通过该前缀后的数字进行唯一区分。
Note
目前,诊断标识符的前两位数字与不同的 Dapr 包一一对应,但随着更多分析器的开发,这一映射方式未来可能会发生变化。安装和配置分析器
在 v1.16 Dapr 版本发布后,以下包将可通过 NuGet 获取:
- Dapr.Actors.Analyzers
- Dapr.Jobs.Analyzers
- Dapr.Workflow.Analyzers
在您希望运行分析器的每个项目中安装每个 NuGet 包。该包将作为项目依赖项安装,分析器将在您编写代码时或作为 CI/CD 构建的一部分运行。分析器会标记现有代码中的问题,并在构建项目时警告您新出现的问题。
我们的许多分析器都有关联的代码修复,可以自动纠正问题。如果您的 IDE 支持此功能,任何可用的代码修复都将作为代码中的内联菜单选项显示。
此外,我们的大多数分析器还应该报告代码中被识别为规则关键方面的特定语法所在的行号和列号。如果您的 IDE 支持,双击任何分析器警告应该直接跳转到违反分析器规则的代码部分。
抑制特定分析器
如果您希望阻止分析器对项目的某个特定部分进行检查,可以通过多种方式单独抑制其输出。有关在项目或文件中抑制分析器的更多信息,请阅读相关的 .NET 文档。
禁用所有分析器
如果您希望在不移除任何提供分析器的包的情况下禁用项目中的所有分析器,请在 csproj 文件中将 EnableNETAnalyzers 属性设置为 false。
可用的分析器
| 诊断 ID | Dapr 包 | 类别 | 严重性 | 添加版本 | 描述 | 可用代码修复 |
|---|---|---|---|---|---|---|
| DAPR1301 | Dapr.Workflow | Usage | Warning | 1.16 | 工作流类型未向依赖注入提供程序注册 | Yes |
| DAPR1302 | Dapr.Workflow | Usage | Warning | 1.16 | 工作流活动类型未向依赖注入提供程序注册 | Yes |
| DAPR1401 | Dapr.Actors | Usage | Warning | 1.16 | Actor 计时器方法调用要求在类型上存在指定的回调方法 | No |
| DAPR1402 | Dapr.Actors | Usage | Warning | 1.16 | Actor 类型未向依赖注入注册 | Yes |
| DAPR1403 | Dapr.Actors | Interoperability | Info | 1.16 | 将 options.UseJsonSerialization 设置为 true 以支持与非 .NET actor 的互操作性 | Yes |
| DAPR1404 | Dapr.Actors | Usage | Warning | 1.16 | 调用 app.MapActorsHandlers 以映射 Dapr actor 的端点 | Yes |
| DAPR1501 | Dapr.Jobs | Usage | Warning | 1.16 | Job 调用要求为 IEndpointRouteBuilder 上每个预期的作业设置和配置 MapDaprScheduledJobHandler | No |
分析器类别
以下是分析器可以分配到的各个有效类别,这些类别是按照 .NET 分析器使用的标准类别建模的:
- Design
- Documentation
- Globalization
- Interoperability
- Maintainability
- Naming
- Performance
- Reliability
- Security
- Usage
2.1.10 - 使用 Dapr .NET SDK 开发应用程序
同时考虑多个服务
使用你喜欢的 IDE 或编辑器启动应用程序时,通常假设你只需要运行一个东西:你正在调试的应用程序。然而,开发微服务会挑战你思考本地开发流程时同时考虑多个服务。微服务应用程序包含多个你可能需要同时运行的服务,以及需要管理的依赖项(如状态存储)。
在开发流程中加入 Dapr 意味着你需要管理以下关注点:
- 你想要运行的每个服务
- 每个服务的 Dapr 边车
- Dapr 组件和配置清单
- 额外的依赖项,如状态存储
- 可选:用于 Actor 的 Dapr Placement 服务
本文档假设你正在构建一个生产应用程序,并希望创建可重复且稳健的开发实践。此处提供的指导是通用的,适用于使用 Dapr 的任何 .NET 服务器应用程序(包括 Actor)。
管理组件
对于使用 Dapr 进行本地开发,你有两种主要方法来存储组件定义:
- 使用默认位置(
~/.dapr/components) - 使用你自己的位置
在源代码仓库中创建一个文件夹来存储组件和配置,将使你能够对这些定义进行版本控制和共享。此处提供的指导假设你在应用程序源代码旁边创建了一个文件夹来存储这些文件。
开发选项
选择以下链接之一,了解你可以在本地开发场景中使用的工具。建议你熟悉其中的每一个,以了解 .NET SDK 提供的选项。
2.1.10.1 - 使用 Dapr CLI 进行 Dapr .NET SDK 开发
Dapr CLI
可以将本文视为 Docker 自托管 Dapr 指南 的 .NET 伴侣指南。
Dapr CLI 为您提供了一个良好的开发基础,它会初始化本地 Redis 容器、Zipkin 容器、placement 服务以及 Redis 的组件清单。这使您可以在全新安装且无需额外设置的情况下,使用以下构建块:
您可以使用 dapr run 来运行 .NET 服务,作为本地开发的策略。计划为每个服务运行以下命令之一以启动您的应用程序。
- 优点: 由于这是默认 Dapr 安装的一部分,因此设置起来很容易
- 缺点: 这会在您的机器上使用长时间运行的 Docker 容器,这可能并不理想
- 缺点: 这种方法的可扩展性较差,因为它需要为每个服务单独运行命令
使用 Dapr CLI
对于每个服务,您需要选择:
- 用于寻址的唯一 app-id(
app-id) - 用于 HTTP 的唯一监听端口(
port)
您还应该决定在哪里存储组件(components-path)。
以下命令可以在多个终端中运行以启动每个服务,并将相应的值替换进去。
dapr run --app-id <app-id> --app-port <port> --components-path <components-path> -- dotnet run -p <project> --urls http://localhost:<port>
说明: 此命令将使用 dapr run 来启动每个服务及其边车。命令的前半部分(在 -- 之前)将所需的配置传递给 Dapr CLI。命令的后半部分(在 -- 之后)将所需的配置传递给 dotnet run 命令。
💡 端口
由于您需要为每个服务配置唯一的端口,您可以使用此命令将该端口值传递给 Dapr 和服务两者。--urls http://localhost:<port> 将配置 ASP.NET Core 在提供的端口上监听流量。在命令行中使用配置比在其他地方硬编码监听端口更灵活。如果您的任何服务不接受 HTTP 流量,则通过删除 --app-port 和 --urls 参数来修改上述命令。
后续步骤
如果您需要调试,请使用调试器的附加功能附加到正在运行的进程之一。
如果您想扩展这种方法,请考虑构建一个脚本,为整个应用程序自动执行此过程。
2.1.10.2 - 使用 Docker Compose 进行 Dapr .NET SDK 开发
Docker Compose
可将本文视为 使用 Docker 自托管 Dapr 指南 的 .NET 伴侣指南。
docker-compose 是随 Docker Desktop 附带的 CLI 工具,可用于同时运行多个容器。它是一种将多个容器的生命周期进行自动化的方式,并为面向 Kubernetes 的应用提供类似生产环境的开发体验。
- 优点: 由于
docker-compose会为你管理容器,你可以将依赖项作为应用定义的一部分,并停止机器上长期运行的容器。 - 缺点: 投入成本最高,服务需要容器化才能开始使用。
- 缺点: 如果你不熟悉 Docker,可能会难以调试和排查故障。
使用 docker-compose
从 .NET 的角度来看,对于 Dapr 的 docker-compose 不需要专门的指导。docker-compose 运行容器,一旦你的服务进入容器中,其配置与任何其他编程技术类似。
💡 应用端口
在容器中,ASP.NET Core 应用默认监听端口 80。稍后配置--app-port 时请记住这一点。总结方法如下:
- 为每个服务创建一个
Dockerfile - 创建一个
docker-compose.yaml并将其签入源代码仓库
要了解如何编写 docker-compose.yaml,你应该从 Hello, docker-compose 示例 开始。
与使用 dapr run 在本地运行的每个服务类似,你需要选择一个唯一的 app-id。选择容器名称作为 app-id 会更容易记忆。
compose 文件至少包含:
- 容器用于通信的网络
- 每个服务的容器
- 指定了服务端口和 app-id 的
<service>-daprd边车容器 - 在容器中运行的其他依赖项(例如 redis)
- 可选:Dapr placement 容器(用于 Actor)
你还可以从 eShopOnContainers 示例应用中查看更大的示例。
2.1.10.3 - 使用 .NET Aspire 进行 Dapr .NET SDK 开发
.NET Aspire
.NET Aspire 是一种开发工具, 旨在通过提供一个框架,使第三方服务能够与您自己的软件一起轻松集成、观察和配置,从而更轻松地将外部软件包含到 .NET 应用程序中。
Aspire 通过与流行的 IDE 提供丰富的集成来简化本地开发,包括 Microsoft Visual Studio、 Visual Studio Code、 JetBrains Rider 等, 以便在启动调试器启动应用程序的同时,自动启动和配置对其他集成的访问,包括 Dapr。
虽然 Aspire 还协助将应用程序部署到各种云主机(如 Microsoft Azure 和 Amazon AWS),但部署目前超出了本指南的范围。更多信息可以在 Aspire 的文档这里找到。
可以在这里找到一个端到端演示,其中包含以下内容并演示了多个启用 Dapr 的服务之间的服务调用。
前提条件
- Dapr .NET SDK 支持 .NET 8、 .NET 9 和 .NET 10。使用支持您选择的运行时的 .NET Aspire 版本。
- 符合 OCI 标准的容器运行时,例如 Docker Desktop 或 Podman
- 安装并初始化 Dapr v1.16 或更高版本
通过 CLI 使用 .NET Aspire
我们将首先创建一个全新的 .NET 应用程序。打开您喜欢的 CLI 并导航到您希望在其中创建新 .NET 解决方案的目录。首先使用以下命令安装一个模板,该模板将创建一个空的 Aspire 应用程序:
dotnet new install Aspire.ProjectTemplates
安装完成后,继续在当前目录中创建一个空的 .NET Aspire 应用程序。-n 参数允许您指定输出解决方案的名称。如果排除该参数,.NET CLI 将改为使用输出目录的名称,例如 C:\source\aspiredemo 将导致解决方案被命名为 aspiredemo。本教程的其余部分将假设解决方案名为 aspiredemo。
dotnet new aspire -n aspiredemo
这将在您的目录中创建两个 Aspire 特定的目录和一个文件:
aspiredemo.AppHost/包含 Aspire 编排项目,用于配置应用程序中使用的每个集成。aspiredemo.ServiceDefaults/包含一系列旨在您的解决方案中共享的扩展,以帮助 Aspire 提供的弹性、服务发现和遥测功能(这些与 Dapr 本身提供的功能不同)。aspiredemo.sln是维护当前解决方案布局的文件
接下来,我们将创建两个项目,作为我们的 Dapr 应用程序并演示 Dapr 功能。在同一目录中,使用以下命令创建一个名为 FrontEndApp 的空 ASP.NET Core 项目和另一个名为 ‘BackEndApp’ 的项目。任何一个都将在当前目录下的 FrontEndApp\FrontEndApp.csproj 和 BackEndApp\BackEndApp.csproj 中创建。
dotnet new web --name FrontEndApp
接下来,我们将配置 AppHost 项目以添加必要的包以支持本地 Dapr 开发。使用以下命令导航到 AppHost 目录,并将 CommunityToolkit.Aspire.Hosting.Dapr 包从 NuGet 安装到项目中。
我们还将添加对 FrontEndApp 项目的引用,以便我们可以在注册过程中引用它。
Aspire.Hosting.Dapr,已被标记为已弃用。cd aspiredemo.AppHost
dotnet add package CommunityToolkit.Aspire.Hosting.Dapr
dotnet add reference ../FrontEndApp/
dotnet add reference ../BackEndApp/
接下来,我们需要将 Dapr 配置为与您的项目一起加载的资源。在您喜欢的 IDE 中打开该项目中的 Program.cs 文件。它应该类似于以下内容:
var builder = DistributedApplication.CreateBuilder(args);
builder.Build().Run();
如果您熟悉 ASP.NET Core 项目或其他使用 Microsoft.Extensions.DependencyInjection 功能的项目中使用的依赖注入方法,您会发现这是一个熟悉的体验。
由于我们已经添加了对 MyApp 的项目引用,因此我们需要在此配置中首先添加一个引用。在 builder.Build().Run() 行之前添加以下内容:
var backEndApp = builder
.AddProject<Projects.BackEndApp>("be")
.WithDaprSidecar();
var frontEndApp = builder
.AddProject<Projects.FrontEndApp>("fe")
.WithDaprSidecar();
由于项目引用已添加到此解决方案,您的项目在此处作为 Projects. 命名空间中的类型显示。在此教程中,为项目分配的变量名称并不重要,但如果您想使用 Aspire 的服务发现功能在此项目与另一个项目之间创建引用,则将使用该名称。
添加 .WithDaprSidecar() 将 Dapr 配置为 .NET Aspire 资源,以便当项目运行时,边车将与您的应用程序一起部署。这接受许多不同的选项,并且可以按照以下示例进行配置:
DaprSidecarOptions sidecarOptions = new()
{
AppId = "how-dapr-identifies-your-app",
AppPort = 8080, //请注意,如果您打算从 Aspire v9.0 开始配置发布订阅、actor 或工作流,则需要此参数
DaprGrpcPort = 50001,
DaprHttpPort = 3500,
MetricsPort = 9090
};
builder
.AddProject<Projects.BackEndApp>("be")
.WithReference(myApp)
.WithDaprSidecar(sidecarOptions);
最后,让我们向后端应用添加一个端点,我们可以使用 Dapr 的服务调用调用它以显示到页面以演示 Dapr 正在按预期工作。
当您在 IDE 中打开解决方案时,请确保 aspiredemo.AppHost 配置为您的启动项目,但是当您以调试配置启动它时,您会注意到您的集成控制台应该反映您预期的 Dapr 日志,并且它将对您的应用程序可用。
2.1.11 - 如何使用 Dapr .NET SDK 进行故障排除和调试
2.1.11.1 - 使用 .NET SDK 对发布订阅进行故障排查
发布订阅故障排查
发布订阅最常见的问题是应用程序中的发布订阅端点未被调用。
这个问题有几个层次,对应不同的解决方案:
- 应用程序未接收到来自 Dapr 的任何流量
- 应用程序未向 Dapr 注册发布订阅端点
- 发布订阅端点已向 Dapr 注册,但请求未到达预期的端点
步骤 1:提高日志级别
这很重要。后续步骤将取决于你查看日志输出的能力。ASP.NET Core 在默认日志设置下几乎不输出任何内容,因此你需要更改它。
按照此处的说明,调整日志详细程度以包含 ASP.NET Core 的 Information 级别日志。将 Microsoft 键设置为 Information。
步骤 2:验证你可以接收来自 Dapr 的流量
按正常方式启动应用程序(
dapr run ...)。确保你在命令行中包含了--app-port参数。Dapr 需要知道你的应用程序正在监听流量。默认情况下,ASP.NET Core 应用程序在本地开发中会在端口 5000 上监听 HTTP。等待 Dapr 完成启动
检查日志
你应该会看到类似以下的日志条目:
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/1.1 GET http://localhost:5000/.....
在初始化期间,Dapr 会向你的应用程序发出一些配置请求。如果你找不到这些请求,说明出现了问题。请通过 issue 或 Discord 寻求帮助(并附上日志)。如果你看到向你的应用程序发出的请求,则继续执行步骤 3。
步骤 3:验证端点注册
按正常方式启动应用程序(
dapr run ...)。在命令行使用
curl(或其他 HTTP 测试工具)访问/dapr/subscribe端点。
以下是一个示例命令,假设你的应用程序监听端口是 5000:
curl http://localhost:5000/dapr/subscribe -v
对于正确配置的应用程序,输出应如下所示:
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 5000 (#0)
> GET /dapr/subscribe HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Fri, 15 Jan 2021 22:31:40 GMT
< Content-Type: application/json
< Server: Kestrel
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
[{"topic":"deposit","route":"deposit","pubsubName":"pubsub"},{"topic":"withdraw","route":"withdraw","pubsubName":"pubsub"}]* Closing connection 0
特别注意 HTTP 状态码和 JSON 输出。
< HTTP/1.1 200 OK
200 状态码表示成功。
末尾包含的 JSON 数据块是 /dapr/subscribe 的输出,由 Dapr 运行时处理。在本例中,它使用的是此仓库中的 ControllerSample - 因此这是正确输出的示例。
[
{"topic":"deposit","route":"deposit","pubsubName":"pubsub"},
{"topic":"withdraw","route":"withdraw","pubsubName":"pubsub"}
]
掌握了此命令的输出后,你就可以诊断问题或继续下一步。
选项 0:响应是 200 且包含一些发布订阅条目
如果此测试的 JSON 输出中有条目,则问题出在其他地方,请继续执行步骤 2。
选项 1:响应不是 200,或不包含 JSON
如果响应不是 200 或不包含 JSON,则表示未到达 MapSubscribeHandler() 端点。
确保你在 Startup.cs 中有类似以下的代码,然后重复测试。
app.UseRouting();
app.UseCloudEvents();
app.UseEndpoints(endpoints =>
{
endpoints.MapSubscribeHandler(); // This is the Dapr subscribe handler
endpoints.MapControllers();
});
如果添加订阅处理程序未能解决问题,请在此仓库上提交 issue 并包含你的 Startup.cs 文件的内容。
选项 2:响应包含 JSON 但为空(如 [])
如果 JSON 输出是空数组(如 []),则表示订阅处理程序已注册,但未注册任何主题端点。
如果你使用控制器进行发布订阅,你应该有一个类似以下的方法:
[Topic("pubsub", "deposit")]
[HttpPost("deposit")]
public async Task<ActionResult> Deposit(...)
// Using Pub/Sub routing
[Topic("pubsub", "transactions", "event.type == \"withdraw.v2\"", 1)]
[HttpPost("withdraw")]
public async Task<ActionResult> Withdraw(...)
在此示例中,Topic 和 HttpPost 特性是必需的,但其他细节可能不同。
如果你使用路由进行发布订阅,你应该有一个类似以下的端点:
endpoints.MapPost("deposit", ...).WithTopic("pubsub", "deposit");
在此示例中,调用 WithTopic(...) 是必需的,但其他细节可能不同。
更正此代码并重新测试后,如果 JSON 输出仍然是空数组(如 []),请在此仓库上提交 issue 并包含 Startup.cs 的内容和你的发布订阅端点。
步骤 4:验证端点可达性
在此步骤中,我们将验证向发布订阅注册的条目是否可访问。上一步应该会给你一些类似以下的 JSON 输出:
[
{
"pubsubName": "pubsub",
"topic": "deposit",
"route": "deposit"
},
{
"pubsubName": "pubsub",
"topic": "deposit",
"routes": {
"rules": [
{
"match": "event.type == \"withdraw.v2\"",
"path": "withdraw"
}
]
}
}
]
保留此输出,因为我们将使用 route 信息来测试应用程序。
按正常方式启动应用程序(
dapr run ...)。在命令行使用
curl(或其他 HTTP 测试工具)访问向发布订阅端点注册的路由之一。
以下是一个示例命令,假设你的应用程序监听端口是 5000,且你的发布订阅路由之一是 withdraw:
curl http://localhost:5000/withdraw -H 'Content-Type: application/json' -d '{}' -v
以下是针对示例运行上述命令的输出:
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 5000 (#0)
> POST /withdraw HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 2
>
* upload completely sent off: 2 out of 2 bytes
< HTTP/1.1 400 Bad Request
< Date: Fri, 15 Jan 2021 22:53:27 GMT
< Content-Type: application/problem+json; charset=utf-8
< Server: Kestrel
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
{"type":"https://tools.ietf.org/html/rfc7231#section-6.5.1","title":"One or more validation errors occurred.","status":400,"traceId":"|5e9d7eee-4ea66b1e144ce9bb.","errors":{"Id":["The Id field is required."]}}* Closing connection 0
根据 HTTP 400 和 JSON 负载,此响应表示已到达端点,但由于验证错误而拒绝了请求。
你还应该查看运行中应用程序的控制台输出。这是示例输出,为清晰起见,去除了 Dapr 日志头。
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/1.1 POST http://localhost:5000/withdraw application/json 2
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
Executing endpoint 'ControllerSample.Controllers.SampleController.Withdraw (ControllerSample)'
info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[3]
Route matched with {action = "Withdraw", controller = "Sample"}. Executing controller action with signature System.Threading.Tasks.Task`1[Microsoft.AspNetCore.Mvc.ActionResult`1[ControllerSample.Account]] Withdraw(ControllerSample.Transaction, Dapr.Client.DaprClient) on controller ControllerSample.Controllers.SampleController (ControllerSample).
info: Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor[1]
Executing ObjectResult, writing value of type 'Microsoft.AspNetCore.Mvc.ValidationProblemDetails'.
info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[2]
Executed action ControllerSample.Controllers.SampleController.Withdraw (ControllerSample) in 52.1211ms
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
Executed endpoint 'ControllerSample.Controllers.SampleController.Withdraw (ControllerSample)'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished in 157.056ms 400 application/problem+json; charset=utf-8
主要感兴趣的日志条目是来自路由的条目:
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
Executing endpoint 'ControllerSample.Controllers.SampleController.Withdraw (ControllerSample)'
此条目显示:
- 路由已执行
- 路由选择了
ControllerSample.Controllers.SampleController.Withdraw (ControllerSample)'端点
现在你拥有了对此步骤进行故障排查所需的信息。
选项 0:路由选择了正确的端点
如果路由日志条目中的信息正确,则表示你的应用程序在隔离状态下运行正常。
示例:
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
Executing endpoint 'ControllerSample.Controllers.SampleController.Withdraw (ControllerSample)'
你可能希望尝试使用 Dapr CLI 直接发送发布订阅消息并比较日志输出。
示例命令:
dapr publish --pubsub pubsub --topic withdraw --data '{}'
如果执行此操作后你仍不理解问题,请在此仓库上提交 issue 并包含你的 Startup.cs 的内容。
选项 1:路由未执行
如果你在日志中未看到 Microsoft.AspNetCore.Routing.EndpointMiddleware 的条目,则表示请求由路由以外的其他组件处理。在这种情况下,问题通常是中间件行为异常。请求的其他日志可能会给你一些线索,让你了解正在发生什么。
如果你需要帮助理解问题,请在此仓库上提交 issue 并包含你的 Startup.cs 的内容。
选项 2:路由选择了错误的端点
如果你在日志中看到 Microsoft.AspNetCore.Routing.EndpointMiddleware 的条目,但它包含错误的端点,则表示你存在路由冲突。所选端点将出现在日志中,这应该会让你了解是什么导致了冲突。
如果你需要帮助理解问题,请在此仓库上提交 issue 并包含你的 Startup.cs 的内容。
2.2 - Dapr Go SDK
一个用于在 Go 中构建 Dapr 应用程序的客户端库。该客户端支持所有公共 Dapr API,同时注重地道的 Go 体验和开发者生产力。
2.2.1 - Dapr 客户端 Go SDK 入门
Dapr 客户端包允许您从 Go 应用程序与其他 Dapr 应用程序进行交互。
前置条件
导入客户端包
import "github.com/dapr/go-sdk/client"
错误处理
Dapr 错误基于 gRPC 的丰富错误模型。 以下代码展示了如何解析和处理错误详细信息的示例:
if err != nil {
st := status.Convert(err)
fmt.Printf("Code: %s\n", st.Code().String())
fmt.Printf("Message: %s\n", st.Message())
for _, detail := range st.Details() {
switch t := detail.(type) {
case *errdetails.ErrorInfo:
// 处理 ErrorInfo 详细信息
fmt.Printf("ErrorInfo:\n- Domain: %s\n- Reason: %s\n- Metadata: %v\n", t.GetDomain(), t.GetReason(), t.GetMetadata())
case *errdetails.BadRequest:
// 处理 BadRequest 详细信息
fmt.Println("BadRequest:")
for _, violation := range t.GetFieldViolations() {
fmt.Printf("- Key: %s\n", violation.GetField())
fmt.Printf("- The %q field was wrong: %s\n", violation.GetField(), violation.GetDescription())
}
case *errdetails.ResourceInfo:
// 处理 ResourceInfo 详细信息
fmt.Printf("ResourceInfo:\n- Resource type: %s\n- Resource name: %s\n- Owner: %s\n- Description: %s\n",
t.GetResourceType(), t.GetResourceName(), t.GetOwner(), t.GetDescription())
case *errdetails.Help:
// 处理 ResourceInfo 详细信息
fmt.Println("HelpInfo:")
for _, link := range t.GetLinks() {
fmt.Printf("- Url: %s\n", link.Url)
fmt.Printf("- Description: %s\n", link.Description)
}
default:
// 为您期望的其他类型的详细信息添加 case
fmt.Printf("Unhandled error detail type: %v\n", t)
}
}
}
构建块
Go SDK 允许您与所有 Dapr 构建块 进行交互。
服务调用
要在使用 Dapr 边车运行的另一个服务上调用特定方法,Dapr 客户端 Go SDK 提供了两个选项:
不使用数据调用服务:
resp, err := client.InvokeMethod(ctx, "app-id", "method-name", "post")
使用数据调用服务:
content := &dapr.DataContent{
ContentType: "application/json",
Data: []byte(`{ "id": "a123", "value": "demo", "valid": true }`),
}
resp, err = client.InvokeMethodWithContent(ctx, "app-id", "method-name", "post", content)
有关服务调用的完整指南,请访问如何:调用服务。
工作流
可以使用 Dapr Go SDK 编写和管理工作流及其活动,如下所示:
import (
...
"github.com/dapr/go-sdk/workflow"
...
func ExampleWorkflow(ctx *workflow.WorkflowContext) (any, error) {
var output string
input := "world"
if err := ctx.CallActivity(ExampleActivity, workflow.ActivityInput(input)).Await(&output); err != nil {
return nil, err
}
// 打印输出 - "hello world"
fmt.Println(output)
return nil, nil
}
func ExampleActivity(ctx workflow.ActivityContext) (any, error) {
var input int
if err := ctx.GetInput(&input); err != nil {
return "", err
}
return fmt.Sprintf("hello %s", input), nil
}
func main() {
// 创建工作流 worker
w, err := workflow.NewWorker()
if err != nil {
log.Fatalf("error creating worker: %v", err)
}
// 注册工作流
w.RegisterWorkflow(ExampleWorkflow)
// 注册活动
w.RegisterActivity(ExampleActivity)
// 启动工作流运行器
if err := w.Start(); err != nil {
log.Fatal(err)
}
// 创建工作流客户端
wfClient, err := workflow.NewClient()
if err != nil {
log.Fatal(err)
}
// 启动新工作流
id, err := wfClient.ScheduleNewWorkflow(context.Background(), "ExampleWorkflow")
if err != nil {
log.Fatal(err)
}
// 等待工作流完成
metadata, err := wfClient.WaitForWorkflowCompletion(ctx, id)
if err != nil {
log.Fatal(err)
}
// 打印完成后的工作流状态
fmt.Println(metadata.RuntimeStatus)
// 关闭 Worker
w.Shutdown()
}
状态管理
对于简单用例,Dapr 客户端提供了易于使用的 Save、Get、Delete 方法:
ctx := context.Background()
data := []byte("hello")
store := "my-store" // 在组件 YAML 中定义
// 使用键 key1 保存状态,默认选项:强一致性、最后写入获胜
if err := client.SaveState(ctx, store, "key1", data, nil); err != nil {
panic(err)
}
// 获取键 key1 的状态
item, err := client.GetState(ctx, store, "key1", nil)
if err != nil {
panic(err)
}
fmt.Printf("data [key:%s etag:%s]: %s", item.Key, item.Etag, string(item.Value))
// 删除键 key1 的状态
if err := client.DeleteState(ctx, store, "key1", nil); err != nil {
panic(err)
}
对于更细粒度的控制,Dapr Go 客户端暴露了 SetStateItem 类型,可以用于获得对状态操作的更多控制,并允许一次保存多个项目:
item1 := &dapr.SetStateItem{
Key: "key1",
Etag: &ETag{
Value: "1",
},
Metadata: map[string]string{
"created-on": time.Now().UTC().String(),
},
Value: []byte("hello"),
Options: &dapr.StateOptions{
Concurrency: dapr.StateConcurrencyLastWrite,
Consistency: dapr.StateConsistencyStrong,
},
}
item2 := &dapr.SetStateItem{
Key: "key2",
Metadata: map[string]string{
"created-on": time.Now().UTC().String(),
},
Value: []byte("hello again"),
}
item3 := &dapr.SetStateItem{
Key: "key3",
Etag: &dapr.ETag{
Value: "1",
},
Value: []byte("hello again"),
}
if err := client.SaveBulkState(ctx, store, item1, item2, item3); err != nil {
panic(err)
}
类似地,GetBulkState 方法提供了在单个操作中检索多个状态项目的方法:
keys := []string{"key1", "key2", "key3"}
items, err := client.GetBulkState(ctx, store, keys, nil,100)
以及 ExecuteStateTransaction 方法以事务方式执行多个 upsert 或删除操作。
ops := make([]*dapr.StateOperation, 0)
op1 := &dapr.StateOperation{
Type: dapr.StateOperationTypeUpsert,
Item: &dapr.SetStateItem{
Key: "key1",
Value: []byte(data),
},
}
op2 := &dapr.StateOperation{
Type: dapr.StateOperationTypeDelete,
Item: &dapr.SetStateItem{
Key: "key2",
},
}
ops = append(ops, op1, op2)
meta := map[string]string{}
err := testClient.ExecuteStateTransaction(ctx, store, meta, ops)
使用 QueryState 检索、过滤和排序存储在状态存储中的键/值数据。
// 定义查询字符串
query := `{
"filter": {
"EQ": { "value.Id": "1" }
},
"sort": [
{
"key": "value.Balance",
"order": "DESC"
}
]
}`
// 使用客户端查询状态
queryResponse, err := c.QueryState(ctx, "querystore", query)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Got %d\n", len(queryResponse))
for _, account := range queryResponse {
var data Account
err := account.Unmarshal(&data)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Account: %s has %f\n", data.ID, data.Balance)
}
注意: 查询状态 API 目前处于 alpha 阶段
有关状态管理的完整指南,请访问如何:保存和获取状态。
发布消息
要将数据发布到主题,Dapr Go 客户端提供了一个简单的方法:
data := []byte(`{ "id": "a123", "value": "abcdefg", "valid": true }`)
if err := client.PublishEvent(ctx, "component-name", "topic-name", data); err != nil {
panic(err)
}
要一次发布多条消息,可以使用 PublishEvents 方法:
events := []string{"event1", "event2", "event3"}
res := client.PublishEvents(ctx, "component-name", "topic-name", events)
if res.Error != nil {
panic(res.Error)
}
有关发布订阅的完整指南,请访问如何:发布和订阅。
工作流
您可以使用 Go SDK 创建工作流。例如,从一个简单的工作流活动开始:
func TestActivity(ctx workflow.ActivityContext) (any, error) {
var input int
if err := ctx.GetInput(&input); err != nil {
return "", err
}
// 在这里做些事情
return "result", nil
}
编写一个简单的工作流函数:
func TestWorkflow(ctx *workflow.WorkflowContext) (any, error) {
var input int
if err := ctx.GetInput(&input); err != nil {
return nil, err
}
var output string
if err := ctx.CallActivity(TestActivity, workflow.ActivityInput(input)).Await(&output); err != nil {
return nil, err
}
if err := ctx.WaitForExternalEvent("testEvent", time.Second*60).Await(&output); err != nil {
return nil, err
}
if err := ctx.CreateTimer(time.Second).Await(nil); err != nil {
return nil, nil
}
return output, nil
}
然后组合您的应用程序,使用您创建的工作流。请参阅如何:编写工作流指南以获取完整的演练。
尝试 Go SDK 工作流示例。
作业
Dapr 客户端 Go SDK 允许您调度、获取和删除作业。作业使您能够安排在特定时间或间隔执行工作。
调度作业
要调度新作业,使用 ScheduleJobAlpha1 方法:
import (
"google.golang.org/protobuf/types/known/anypb"
)
// 创建作业数据
data, err := anypb.New(&YourDataStruct{Message: "Hello, Job!"})
if err != nil {
panic(err)
}
// 使用构建器模式创建简单作业
job := client.NewJob("my-scheduled-job",
client.WithJobData(data),
client.WithJobDueTime("10s"), // 10 秒后执行
)
// 调度作业
err = client.ScheduleJobAlpha1(ctx, job)
if err != nil {
panic(err)
}
带调度和重复的作业
您可以使用带有 cron 表达式的 Schedule 字段创建重复作业:
job := client.NewJob("recurring-job",
client.WithJobData(data),
client.WithJobSchedule("0 9 * * *"), // 每天上午 9 点运行
client.WithJobRepeats(10), // 重复 10 次
client.WithJobTTL("1h"), // 作业在 1 小时后过期
)
err = client.ScheduleJobAlpha1(ctx, job)
带失败策略的作业
使用失败策略配置作业应如何处理失败:
// 带最大重试次数和间隔的常量重试策略
job := client.NewJob("resilient-job",
client.WithJobData(data),
client.WithJobDueTime("2024-01-01T10:00:00Z"),
client.WithJobConstantFailurePolicy(),
client.WithJobConstantFailurePolicyMaxRetries(3),
client.WithJobConstantFailurePolicyInterval(30*time.Second),
)
err = client.ScheduleJobAlpha1(ctx, job)
对于失败时不应重试的作业,使用 drop 策略:
job := client.NewJob("one-shot-job",
client.WithJobData(data),
client.WithJobDueTime("2024-01-01T10:00:00Z"),
client.WithJobDropFailurePolicy(),
)
err = client.ScheduleJobAlpha1(ctx, job)
获取作业
要获取有关调度作业的信息:
job, err := client.GetJobAlpha1(ctx, "my-scheduled-job")
if err != nil {
panic(err)
}
fmt.Printf("Job: %s, Schedule: %s, Repeats: %d\n",
job.Name, job.Schedule, job.Repeats)
删除作业
要取消调度作业:
err = client.DeleteJobAlpha1(ctx, "my-scheduled-job")
if err != nil {
panic(err)
}
有关作业的完整指南,请访问如何:调度和管理作业。
输出绑定
Dapr Go 客户端 SDK 提供了两种方法来调用 Dapr 定义的绑定上的操作。Dapr 支持输入、输出和双向绑定。
对于简单的仅输出绑定:
in := &dapr.InvokeBindingRequest{ Name: "binding-name", Operation: "operation-name" }
err = client.InvokeOutputBinding(ctx, in)
要使用内容和元数据调用方法:
in := &dapr.InvokeBindingRequest{
Name: "binding-name",
Operation: "operation-name",
Data: []byte("hello"),
Metadata: map[string]string{"k1": "v1", "k2": "v2"},
}
out, err := client.InvokeBinding(ctx, in)
有关输出绑定的完整指南,请访问如何:使用绑定。
Actors
使用 Dapr Go 客户端 SDK 编写 actors。
// MyActor 表示一个示例 actor 类型。
type MyActor struct {
actors.Actor
}
// MyActorMethod 是可以在 MyActor 上调用的方法。
func (a *MyActor) MyActorMethod(ctx context.Context, req *actors.Message) (string, error) {
log.Printf("Received message: %s", req.Data)
return "Hello from MyActor!", nil
}
func main() {
// 创建 Dapr 客户端
daprClient, err := client.NewClient()
if err != nil {
log.Fatal("Error creating Dapr client: ", err)
}
// 使用 Dapr 注册 actor 类型
actors.RegisterActor(&MyActor{})
// 创建 actor 客户端
actorClient := actors.NewClient(daprClient)
// 创建 actor ID
actorID := actors.NewActorID("myactor")
// 获取或创建 actor
err = actorClient.SaveActorState(context.Background(), "myactorstore", actorID, map[string]interface{}{"data": "initial state"})
if err != nil {
log.Fatal("Error saving actor state: ", err)
}
// 在 actor 上调用方法
resp, err := actorClient.InvokeActorMethod(context.Background(), "myactorstore", actorID, "MyActorMethod", &actors.Message{Data: []byte("Hello from client!")})
if err != nil {
log.Fatal("Error invoking actor method: ", err)
}
log.Printf("Response from actor: %s", resp.Data)
// 在终止之前等待几秒钟
time.Sleep(5 * time.Second)
// 删除 actor
err = actorClient.DeleteActor(context.Background(), "myactorstore", actorID)
if err != nil {
log.Fatal("Error deleting actor: ", err)
}
// 关闭 Dapr 客户端
daprClient.Close()
}
有关 actors 的完整指南,请访问 Actors 构建块文档。
密钥管理
Dapr 客户端还提供对运行时密钥的访问,这些密钥可以由任意数量的密钥存储支持(例如 Kubernetes Secrets、HashiCorp Vault 或 Azure KeyVault):
opt := map[string]string{
"version": "2",
}
secret, err := client.GetSecret(ctx, "store-name", "secret-name", opt)
身份验证
默认情况下,Dapr 依赖网络边界来限制对其 API 的访问。但是,如果目标 Dapr API 配置了基于令牌的身份验证,用户可以通过两种方式使用该令牌配置 Go Dapr 客户端:
环境变量
如果定义了 DAPR_API_TOKEN 环境变量,Dapr 将自动使用它来增强其 Dapr API 调用以确保身份验证。
显式方法
此外,用户还可以在任何 Dapr 客户端实例上显式设置 API 令牌。当用户代码需要为不同的 Dapr API 端点创建多个客户端时,此方法很有用。
func main() {
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
defer client.Close()
client.WithAuthToken("your-Dapr-API-token-here")
}
有关密钥的完整指南,请访问如何:检索密钥。
分布式锁
Dapr 客户端使用锁提供对资源的互斥访问。使用锁,您可以:
- 提供对数据库行、表或整个数据库的访问
- 按顺序锁定从队列读取消息
package main
import (
"fmt"
dapr "github.com/dapr/go-sdk/client"
)
func main() {
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
defer client.Close()
resp, err := client.TryLockAlpha1(ctx, "lockstore", &dapr.LockRequest{
LockOwner: "random_id_abc123",
ResourceID: "my_file_name",
ExpiryInSeconds: 60,
})
fmt.Println(resp.Success)
}
有关分布式锁的完整指南,请访问如何:使用锁。
配置
使用 Dapr 客户端 Go SDK,您可以消费作为只读键/值对返回的配置项,并订阅配置项更改。
配置获取
items, err := client.GetConfigurationItem(ctx, "example-config", "mykey")
if err != nil {
panic(err)
}
fmt.Printf("get config = %s\n", (*items).Value)
配置订阅
go func() {
if err := client.SubscribeConfigurationItems(ctx, "example-config", []string{"mySubscribeKey1", "mySubscribeKey2", "mySubscribeKey3"}, func(id string, items map[string]*dapr.ConfigurationItem) {
for k, v := range items {
fmt.Printf("get updated config key = %s, value = %s \n", k, v.Value)
}
subscribeID = id
}); err != nil {
panic(err)
}
}()
有关配置的完整指南,请访问如何:从存储管理配置。
密码学
使用 Dapr 客户端 Go SDK,您可以使用高级别 Encrypt 和 Decrypt 密码学 API 在处理数据流时加密和解密文件。
要加密:
// 使用 Dapr 加密数据
out, err := client.Encrypt(context.Background(), rf, dapr.EncryptOptions{
// 这是 3 个必需参数
ComponentName: "mycryptocomponent",
KeyName: "mykey",
Algorithm: "RSA",
})
if err != nil {
panic(err)
}
要解密:
// 使用 Dapr 解密数据
out, err := client.Decrypt(context.Background(), rf, dapr.EncryptOptions{
// 唯一必需的选项是组件名称
ComponentName: "mycryptocomponent",
})
有关密码学的完整指南,请访问如何:使用密码学 API。
相关链接
2.2.2 - Dapr Service(回调)SDK for Go 入门
除了 Dapr API 客户端之外,Dapr Go SDK 还提供了 service 包,用于引导启动你的 Dapr 回调服务。这些服务可以基于 gRPC 或 HTTP 进行开发:
2.2.2.1 - Dapr HTTP Service SDK for Go 入门
前置条件
首先导入 Dapr Go service/http 包:
daprd "github.com/dapr/go-sdk/service/http"
创建并启动服务
要创建一个 HTTP Dapr 服务,首先需要使用特定地址创建一个 Dapr 回调实例:
s := daprd.NewService(":8080")
或者使用地址和现有的 http.ServeMux,以便合并现有的服务器实现:
mux := http.NewServeMux()
mux.HandleFunc("/", myOtherHandler)
s := daprd.NewServiceWithMux(":8080", mux)
创建服务实例后,您可以为该服务"附加"任意数量的事件、绑定和服务调用逻辑处理器,如下所示。一旦定义了逻辑,就可以启动服务了:
if err := s.Start(); err != nil && err != http.ErrServerClosed {
log.Fatalf("error: %v", err)
}
事件处理
要处理来自特定主题的事件,需要在启动服务之前添加至少一个主题事件处理器:
sub := &common.Subscription{
PubsubName: "messages",
Topic: "topic1",
Route: "/events",
}
err := s.AddTopicEventHandler(sub, eventHandler)
if err != nil {
log.Fatalf("error adding topic subscription: %v", err)
}
处理器方法本身可以是任何符合预期签名的方法:
func eventHandler(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
log.Printf("event - PubsubName:%s, Topic:%s, ID:%s, Data: %v", e.PubsubName, e.Topic, e.ID, e.Data)
// do something with the event
return true, nil
}
或者,您可以使用路由规则根据 CloudEvent 的内容将消息发送到不同的处理器。
sub := &common.Subscription{
PubsubName: "messages",
Topic: "topic1",
Route: "/important",
Match: `event.type == "important"`,
Priority: 1,
}
err := s.AddTopicEventHandler(sub, importantHandler)
if err != nil {
log.Fatalf("error adding topic subscription: %v", err)
}
您也可以创建一个实现了 TopicEventSubscriber 接口的自定义类型来处理事件:
type EventHandler struct {
// any data or references that your event handler needs.
}
func (h *EventHandler) Handle(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
log.Printf("event - PubsubName:%s, Topic:%s, ID:%s, Data: %v", e.PubsubName, e.Topic, e.ID, e.Data)
// do something with the event
return true, nil
}
然后可以使用 AddTopicEventSubscriber 方法添加 EventHandler:
sub := &common.Subscription{
PubsubName: "messages",
Topic: "topic1",
}
eventHandler := &EventHandler{
// initialize any fields
}
if err := s.AddTopicEventSubscriber(sub, eventHandler); err != nil {
log.Fatalf("error adding topic subscription: %v", err)
}
服务调用处理器
要处理服务调用,需要在启动服务之前添加至少一个服务调用处理器:
if err := s.AddServiceInvocationHandler("/echo", echoHandler); err != nil {
log.Fatalf("error adding invocation handler: %v", err)
}
处理器方法本身可以是任何符合预期签名的方法:
func echoHandler(ctx context.Context, in *common.InvocationEvent) (out *common.Content, err error) {
log.Printf("echo - ContentType:%s, Verb:%s, QueryString:%s, %+v", in.ContentType, in.Verb, in.QueryString, string(in.Data))
// do something with the invocation here
out = &common.Content{
Data: in.Data,
ContentType: in.ContentType,
DataTypeURL: in.DataTypeURL,
}
return
}
绑定调用处理器
if err := s.AddBindingInvocationHandler("/run", runHandler); err != nil {
log.Fatalf("error adding binding handler: %v", err)
}
处理器方法本身可以是任何符合预期签名的方法:
func runHandler(ctx context.Context, in *common.BindingEvent) (out []byte, err error) {
log.Printf("binding - Data:%v, Meta:%v", in.Data, in.Metadata)
// do something with the invocation here
return nil, nil
}
相关链接
2.2.2.2 - Dapr 服务(回调)SDK for Go 入门
Dapr gRPC 服务 SDK for Go
前置条件
首先导入 Dapr Go service/grpc 包:
daprd "github.com/dapr/go-sdk/service/grpc"
创建和启动服务
要创建一个 gRPC Dapr 服务,首先,使用特定地址创建一个 Dapr 回调实例:
s, err := daprd.NewService(":50001")
if err != nil {
log.Fatalf("failed to start the server: %v", err)
}
或者使用地址和一个已有的 net.Listener,以便与现有的服务监听器结合:
list, err := net.Listen("tcp", "localhost:0")
if err != nil {
log.Fatalf("gRPC listener creation failed: %s", err)
}
s := daprd.NewServiceWithListener(list)
一旦创建了服务实例,您就可以为该服务"附加"任意数量的事件、绑定和服务调用逻辑处理器,如下所示。逻辑定义完成后,就可以启动服务了:
if err := s.Start(); err != nil {
log.Fatalf("server error: %v", err)
}
事件处理
要处理来自特定主题的事件,您需要在启动服务之前添加至少一个主题事件处理器:
sub := &common.Subscription{
PubsubName: "messages",
Topic: "topic1",
}
if err := s.AddTopicEventHandler(sub, eventHandler); err != nil {
log.Fatalf("error adding topic subscription: %v", err)
}
处理器方法本身可以是任何具有预期签名的方法:
func eventHandler(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
log.Printf("event - PubsubName:%s, Topic:%s, ID:%s, Data: %v", e.PubsubName, e.Topic, e.ID, e.Data)
// do something with the event
return true, nil
}
或者,您可以使用路由规则,根据 CloudEvent 的内容将消息发送到不同的处理器。
sub := &common.Subscription{
PubsubName: "messages",
Topic: "topic1",
Route: "/important",
Match: `event.type == "important"`,
Priority: 1,
}
err := s.AddTopicEventHandler(sub, importantHandler)
if err != nil {
log.Fatalf("error adding topic subscription: %v", err)
}
您还可以创建一个实现 TopicEventSubscriber 接口的自定义类型来处理您的事件:
type EventHandler struct {
// any data or references that your event handler needs.
}
func (h *EventHandler) Handle(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
log.Printf("event - PubsubName:%s, Topic:%s, ID:%s, Data: %v", e.PubsubName, e.Topic, e.ID, e.Data)
// do something with the event
return true, nil
}
然后可以使用 AddTopicEventSubscriber 方法添加 EventHandler:
sub := &common.Subscription{
PubsubName: "messages",
Topic: "topic1",
}
eventHandler := &EventHandler{
// initialize any fields
}
if err := s.AddTopicEventSubscriber(sub, eventHandler); err != nil {
log.Fatalf("error adding topic subscription: %v", err)
}
服务调用处理器
要处理服务调用,您需要在启动服务之前添加至少一个服务调用处理器:
if err := s.AddServiceInvocationHandler("echo", echoHandler); err != nil {
log.Fatalf("error adding invocation handler: %v", err)
}
处理器方法本身可以是任何具有预期签名的方法:
func echoHandler(ctx context.Context, in *common.InvocationEvent) (out *common.Content, err error) {
log.Printf("echo - ContentType:%s, Verb:%s, QueryString:%s, %+v", in.ContentType, in.Verb, in.QueryString, string(in.Data))
// do something with the invocation here
out = &common.Content{
Data: in.Data,
ContentType: in.ContentType,
DataTypeURL: in.DataTypeURL,
}
return
}
绑定调用处理器
要处理绑定调用,您需要在启动服务之前添加至少一个绑定调用处理器:
if err := s.AddBindingInvocationHandler("run", runHandler); err != nil {
log.Fatalf("error adding binding handler: %v", err)
}
处理器方法本身可以是任何具有预期签名的方法:
func runHandler(ctx context.Context, in *common.BindingEvent) (out []byte, err error) {
log.Printf("binding - Data:%v, Meta:%v", in.Data, in.Metadata)
// do something with the invocation here
return nil, nil
}
相关链接
2.3 - Dapr Java SDK
Dapr 提供了多种软件包来协助 Java 应用程序的开发。使用它们,你可以通过 Dapr 创建 Java 客户端、服务器和虚拟 Actor。
前提条件
导入 Dapr Java SDK
接下来,导入 Java SDK 软件包以开始使用。选择你首选的构建工具以了解如何导入。
对于 Maven 项目,将以下内容添加到你的 pom.xml 文件中:
<project>
...
<dependencies>
...
<!-- Dapr's core SDK with all features, except Actors. -->
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk</artifactId>
<version>1.16.0</version>
</dependency>
<!-- Dapr's SDK for Actors (optional). -->
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-actors</artifactId>
<version>1.16.0</version>
</dependency>
<!-- Dapr's SDK integration with SpringBoot (optional). -->
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-springboot</artifactId>
<version>1.16.0</version>
</dependency>
...
</dependencies>
...
</project>
对于 Gradle 项目,将以下内容添加到你的 build.gradle 文件中:
dependencies {
...
// Dapr's core SDK with all features, except Actors.
compile('io.dapr:dapr-sdk:1.16.0')
// Dapr's SDK for Actors (optional).
compile('io.dapr:dapr-sdk-actors:1.16.0')
// Dapr's SDK integration with SpringBoot (optional).
compile('io.dapr:dapr-sdk-springboot:1.16.0')
}
如果你还使用了 Spring Boot,可能会遇到一个常见问题:Dapr SDK 使用的 OkHttp 版本与 Spring Boot 的 Bill of Materials 中指定的版本冲突。
你可以通过在项目中指定与 Dapr SDK 使用的版本兼容的 OkHttp 版本来解决此问题:
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>1.16.0</version>
</dependency>
试用
测试 Dapr Java SDK。通过 Java 快速入门和教程来查看 Dapr 的实际效果:
| SDK 示例 | 描述 |
|---|---|
| 快速入门 | 在几分钟内使用 Java SDK 体验 Dapr 的 API 构建块。 |
| SDK 示例 | 克隆 SDK 仓库以尝试一些示例并开始使用。 |
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
try (DaprClient client = (new DaprClientBuilder()).build()) {
// sending a class with message; BINDING_OPERATION="create"
client.invokeBinding(BINDING_NAME, BINDING_OPERATION, myClass).block();
// sending a plain string
client.invokeBinding(BINDING_NAME, BINDING_OPERATION, message).block();
}
- 有关输出绑定的完整指南,请访问 操作指南:输出绑定。
- 访问 Java SDK 示例 以获取代码示例和尝试输出绑定的说明。
可用软件包
2.3.1 - AI
2.3.1.1 - 操作指南:使用 Java SDK 编写和管理 Dapr Conversation AI
在本次演示中,我们将介绍如何使用 Conversation API 与大语言模型(LLM)进行对话。该 API 会返回给定提示词的 LLM 响应。通过提供的 conversation ai 示例,你将:
- 使用 Conversation AI 示例 提供提示词
- 过滤个人身份信息(PII)。
此示例使用自托管模式下 dapr init 的默认配置。
前置条件
- Dapr CLI 和已初始化的环境。
- Java JDK 11(或更高版本):
- Oracle JDK,或
- OpenJDK
- Apache Maven,版本 3.x。
- Docker Desktop
设置环境
克隆 Java SDK 仓库 并进入该目录。
git clone https://github.com/dapr/java-sdk.git
cd java-sdk
运行以下命令以安装使用 Dapr Java SDK 运行 Conversation AI 示例所需的依赖。
mvn clean install -DskipTests
从 Java SDK 根目录进入示例目录。
cd examples
运行 Dapr 边车。
dapr run --app-id conversationapp --dapr-grpc-port 51439 --dapr-http-port 3500 --app-port 8080
现在,Dapr 正在
http://localhost:3500监听 HTTP 请求,在http://localhost:51439监听 gRPC 请求。
向 Conversation AI API 发送包含个人身份信息(PII)的提示词
在 DemoConversationAI 中,包含使用 DaprPreviewClient 下的 converse 方法发送提示词的步骤。
public class DemoConversationAI {
/**
* 启动客户端的 main 方法。
*
* @param args 输入参数(未使用)。
*/
public static void main(String[] args) {
try (DaprPreviewClient client = new DaprClientBuilder().buildPreviewClient()) {
System.out.println("Sending the following input to LLM: Hello How are you? This is the my number 672-123-4567");
ConversationInput daprConversationInput = new ConversationInput("Hello How are you? "
+ "This is the my number 672-123-4567");
// 组件名称是在 conversation.yaml 文件的 metadata 块中提供的名称。
Mono<ConversationResponse> responseMono = client.converse(new ConversationRequest("echo",
List.of(daprConversationInput))
.setContextId("contextId")
.setScrubPii(true).setTemperature(1.1d));
ConversationResponse response = responseMono.block();
System.out.printf("Conversation output: %s", response.getConversationOutputs().get(0).getResult());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
使用以下命令运行 DemoConversationAI。
java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.conversation.DemoConversationAI
示例输出
== APP == Conversation output: Hello How are you? This is the my number <ISBN>
如输出所示,发送到 API 的号码已被混淆并以 <ISBN> 的形式返回。
上面的示例使用了一个“echo”
组件进行测试,该组件只是简单地返回输入消息。
当与 OpenAI 或 Claude 等 LLM 集成时,你将收到有意义的响应,而不是回显的输入。
后续步骤
2.3.2 - Dapr 客户端 Java SDK 入门
Dapr 客户端包允许您从 Java 应用程序与其他 Dapr 应用程序进行交互。
注意
如果您还没有,请尝试其中一个快速入门,快速了解如何将 Dapr Java SDK 与 API 构建块一起使用。前置条件
初始化客户端
您可以像这样初始化 Dapr 客户端:
DaprClient client = new DaprClientBuilder().build()
这将连接到默认的 Dapr gRPC 端点 localhost:50001。有关使用环境变量和系统属性配置客户端的信息,请参阅属性。
错误处理
最初,Dapr 中的错误遵循标准 gRPC 错误模型。然而,为了提供更详细和有信息量的错误消息,在 1.13 版本中引入了增强的错误模型,该模型与 gRPC 更丰富的错误模型保持一致。作为响应,Java SDK 扩展了 DaprException 以包含 Dapr 中添加的错误详细信息。
使用 Dapr Java SDK 时处理 DaprException 并使用错误详细信息的示例:
...
try {
client.publishEvent("unknown_pubsub", "mytopic", "mydata").block();
} catch (DaprException exception) {
System.out.println("Dapr exception's error code: " + exception.getErrorCode());
System.out.println("Dapr exception's message: " + exception.getMessage());
// DaprException 现在包含 `getStatusDetails()` 以包含有关 Dapr 运行时错误的更多详细信息。
System.out.println("Dapr exception's reason: " + exception.getStatusDetails().get(
DaprErrorDetails.ErrorDetailType.ERROR_INFO,
"reason",
TypeRef.STRING));
}
...
构建块
Java SDK 允许您与所有 Dapr 构建块 进行交互。
调用服务
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
try (DaprClient client = (new DaprClientBuilder()).build()) {
// 调用 'GET' 方法 (HTTP),跳过序列化:使用 Mono<byte[]> 返回类型
// 对于 gRPC,请在下面设置 HttpExtension.NONE 参数
response = client.invokeMethod(SERVICE_TO_INVOKE, METHOD_TO_INVOKE, "{\"name\":\"World!\"}", HttpExtension.GET, byte[].class).block();
// 调用 'POST' 方法 (HTTP),跳过序列化:使用 Mono<byte[]> 返回类型
response = client.invokeMethod(SERVICE_TO_INVOKE, METHOD_TO_INVOKE, "{\"id\":\"100\", \"FirstName\":\"Value\", \"LastName\":\"Value\"}", HttpExtension.POST, byte[].class).block();
System.out.println(new String(response));
// 调用 'POST' 方法 (HTTP),使用序列化:使用 Mono<Employee> 返回类型
Employee newEmployee = new Employee("Nigel", "Guitarist");
Employee employeeResponse = client.invokeMethod(SERVICE_TO_INVOKE, "employees", newEmployee, HttpExtension.POST, Employee.class).block();
}
- 有关服务调用的完整指南,请访问如何:调用服务。
- 访问 Java SDK 示例获取代码示例和尝试服务调用的说明
保存和获取应用程序状态
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.State;
import reactor.core.publisher.Mono;
try (DaprClient client = (new DaprClientBuilder()).build()) {
// 保存状态
client.saveState(STATE_STORE_NAME, FIRST_KEY_NAME, myClass).block();
// 获取状态
State<MyClass> retrievedMessage = client.getState(STATE_STORE_NAME, FIRST_KEY_NAME, MyClass.class).block();
// 删除状态
client.deleteState(STATE_STORE_NAME, FIRST_KEY_NAME).block();
}
- 有关状态操作的完整列表,请访问如何:获取和保存状态。
- 访问 Java SDK 示例获取代码示例和尝试状态管理的说明
发布和订阅消息
发布消息
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.Metadata;
import static java.util.Collections.singletonMap;
try (DaprClient client = (new DaprClientBuilder()).build()) {
client.publishEvent(PUBSUB_NAME, TOPIC_NAME, message, singletonMap(Metadata.TTL_IN_SECONDS, MESSAGE_TTL_IN_SECONDS)).block();
}
订阅消息
import com.fasterxml.jackson.databind.ObjectMapper;
import io.dapr.Topic;
import io.dapr.client.domain.BulkSubscribeAppResponse;
import io.dapr.client.domain.BulkSubscribeAppResponseEntry;
import io.dapr.client.domain.BulkSubscribeAppResponseStatus;
import io.dapr.client.domain.BulkSubscribeMessage;
import io.dapr.client.domain.BulkSubscribeMessageEntry;
import io.dapr.client.domain.CloudEvent;
import io.dapr.springboot.annotations.BulkSubscribe;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
public class SubscriberController {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Topic(name = "testingtopic", pubsubName = "${myAppProperty:messagebus}")
@PostMapping(path = "/testingtopic")
public Mono<Void> handleMessage(@RequestBody(required = false) CloudEvent<?> cloudEvent) {
return Mono.fromRunnable(() -> {
try {
System.out.println("Subscriber got: " + cloudEvent.getData());
System.out.println("Subscriber got: " + OBJECT_MAPPER.writeValueAsString(cloudEvent));
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
@Topic(name = "testingtopic", pubsubName = "${myAppProperty:messagebus}",
rule = @Rule(match = "event.type == 'myevent.v2'", priority = 1))
@PostMapping(path = "/testingtopicV2")
public Mono<Void> handleMessageV2(@RequestBody(required = false) CloudEvent envelope) {
return Mono.fromRunnable(() -> {
try {
System.out.println("Subscriber got: " + cloudEvent.getData());
System.out.println("Subscriber got: " + OBJECT_MAPPER.writeValueAsString(cloudEvent));
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
@BulkSubscribe()
@Topic(name = "testingtopicbulk", pubsubName = "${myAppProperty:messagebus}")
@PostMapping(path = "/testingtopicbulk")
public Mono<BulkSubscribeAppResponse> handleBulkMessage(
@RequestBody(required = false) BulkSubscribeMessage<CloudEvent<String>> bulkMessage) {
return Mono.fromCallable(() -> {
if (bulkMessage.getEntries().size() == 0) {
return new BulkSubscribeAppResponse(new ArrayList<BulkSubscribeAppResponseEntry>());
}
System.out.println("Bulk Subscriber received " + bulkMessage.getEntries().size() + " messages.");
List<BulkSubscribeAppResponseEntry> entries = new ArrayList<BulkSubscribeAppResponseEntry>();
for (BulkSubscribeMessageEntry<?> entry : bulkMessage.getEntries()) {
try {
System.out.printf("Bulk Subscriber message has entry ID: %s\n", entry.getEntryId());
CloudEvent<?> cloudEvent = (CloudEvent<?>) entry.getEvent();
System.out.printf("Bulk Subscriber got: %s\n", cloudEvent.getData());
entries.add(new BulkSubscribeAppResponseEntry(entry.getEntryId(), BulkSubscribeAppResponseStatus.SUCCESS));
} catch (Exception e) {
e.printStackTrace();
entries.add(new BulkSubscribeAppResponseEntry(entry.getEntryId(), BulkSubscribeAppResponseStatus.RETRY));
}
}
return new BulkSubscribeAppResponse(entries);
});
}
}
批量发布消息
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.BulkPublishResponse;
import io.dapr.client.domain.BulkPublishResponseFailedEntry;
import java.util.ArrayList;
import java.util.List;
class Solution {
public void publishMessages() {
try (DaprClient client = (new DaprClientBuilder()).build()) {
// 创建要发布的消息列表
List<String> messages = new ArrayList<>();
for (int i = 0; i < NUM_MESSAGES; i++) {
String message = String.format("This is message #%d", i);
messages.add(message);
System.out.println("Going to publish message : " + message);
}
// 使用批量发布 API 发布消息列表
BulkPublishResponse<String> res = client.publishEvents(PUBSUB_NAME, TOPIC_NAME, "text/plain", messages).block()
}
}
}
- 有关发布消息和订阅主题的完整指南,请访问如何:发布和订阅。
- 访问 Java SDK 示例获取代码示例和尝试发布订阅的说明
与输出绑定交互
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
try (DaprClient client = (new DaprClientBuilder()).build()) {
// 使用消息发送类;BINDING_OPERATION="create"
client.invokeBinding(BINDING_NAME, BINDING_OPERATION, myClass).block();
// 发送纯字符串
client.invokeBinding(BINDING_NAME, BINDING_OPERATION, message).block();
}
- 有关输出绑定的完整指南,请访问如何:输出绑定。
- 访问 Java SDK 示例获取代码示例和尝试输出绑定的说明。
与输入绑定交互
import org.springframework.web.bind.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@RestController
@RequestMapping("/")
public class myClass {
private static final Logger log = LoggerFactory.getLogger(myClass);
@PostMapping(path = "/checkout")
public Mono<String> getCheckout(@RequestBody(required = false) byte[] body) {
return Mono.fromRunnable(() ->
log.info("Received Message: " + new String(body)));
}
}
- 有关输入绑定的完整指南,请访问如何:输入绑定。
- 访问 Java SDK 示例获取代码示例和尝试输入绑定的说明。
获取密钥
import com.fasterxml.jackson.databind.ObjectMapper;
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import java.util.Map;
try (DaprClient client = (new DaprClientBuilder()).build()) {
Map<String, String> secret = client.getSecret(SECRET_STORE_NAME, secretKey).block();
System.out.println(JSON_SERIALIZER.writeValueAsString(secret));
}
- 有关密钥的完整指南,请访问如何:检索密钥。
- 访问 Java SDK 示例获取代码示例和尝试检索密钥的说明
Actor
Actor 是一个隔离的、独立的计算和状态单元,具有单线程执行。Dapr 提供基于虚拟 Actor 模式的 actor 实现,该模式提供单线程编程模型,其中 actor 在不使用时会被垃圾回收。使用 Dapr 的实现,您可以根据 Actor 模型编写 Dapr actor,Dapr 利用底层平台提供的可扩展性和可靠性。
import io.dapr.actors.ActorMethod;
import io.dapr.actors.ActorType;
import reactor.core.publisher.Mono;
@ActorType(name = "DemoActor")
public interface DemoActor {
void registerReminder();
@ActorMethod(name = "echo_message")
String say(String something);
void clock(String message);
@ActorMethod(returns = Integer.class)
Mono<Integer> incrementAndGet(int delta);
}
- 有关 actor 的完整指南,请访问如何:在 Dapr 中使用虚拟 actor。
- 访问 Java SDK 示例获取代码示例和尝试 actor 的说明
获取和订阅应用程序配置
请注意,这是一个预览 API,因此只能通过 DaprPreviewClient 接口访问,而不能通过普通的 DaprClient 接口访问
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.DaprPreviewClient;
import io.dapr.client.domain.ConfigurationItem;
import io.dapr.client.domain.GetConfigurationRequest;
import io.dapr.client.domain.SubscribeConfigurationRequest;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
try (DaprPreviewClient client = (new DaprClientBuilder()).buildPreviewClient()) {
// 获取单个键的配置
Mono<ConfigurationItem> item = client.getConfiguration(CONFIG_STORE_NAME, CONFIG_KEY).block();
// 获取多个键的配置
Mono<Map<String, ConfigurationItem>> items =
client.getConfiguration(CONFIG_STORE_NAME, CONFIG_KEY_1, CONFIG_KEY_2);
// 订阅配置更改
Flux<SubscribeConfigurationResponse> outFlux = client.subscribeConfiguration(CONFIG_STORE_NAME, CONFIG_KEY_1, CONFIG_KEY_2);
outFlux.subscribe(configItems -> configItems.forEach(...));
// 取消订阅配置更改
Mono<UnsubscribeConfigurationResponse> unsubscribe = client.unsubscribeConfiguration(SUBSCRIPTION_ID, CONFIG_STORE_NAME)
}
- 有关配置操作的完整列表,请访问如何:从存储管理配置。
- 访问 Java SDK 示例获取代码示例和尝试不同配置操作的说明。
查询已保存的状态
请注意,这是一个预览 API,因此只能通过 DaprPreviewClient 接口访问,而不能通过普通的 DaprClient 接口访问
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.DaprPreviewClient;
import io.dapr.client.domain.QueryStateItem;
import io.dapr.client.domain.QueryStateRequest;
import io.dapr.client.domain.QueryStateResponse;
import io.dapr.client.domain.query.Query;
import io.dapr.client.domain.query.Sorting;
import io.dapr.client.domain.query.filters.EqFilter;
try (DaprClient client = builder.build(); DaprPreviewClient previewClient = builder.buildPreviewClient()) {
String searchVal = args.length == 0 ? "searchValue" : args[0];
// 创建 JSON 数据
Listing first = new Listing();
first.setPropertyType("apartment");
first.setId("1000");
...
Listing second = new Listing();
second.setPropertyType("row-house");
second.setId("1002");
...
Listing third = new Listing();
third.setPropertyType("apartment");
third.setId("1003");
...
Listing fourth = new Listing();
fourth.setPropertyType("apartment");
fourth.setId("1001");
...
Map<String, String> meta = new HashMap<>();
meta.put("contentType", "application/json");
// 保存状态
SaveStateRequest request = new SaveStateRequest(STATE_STORE_NAME).setStates(
new State<>("1", first, null, meta, null),
new State<>("2", second, null, meta, null),
new State<>("3", third, null, meta, null),
new State<>("4", fourth, null, meta, null)
);
client.saveBulkState(request).block();
// 创建查询和查询状态请求
Query query = new Query()
.setFilter(new EqFilter<>("propertyType", "apartment"))
.setSort(Arrays.asList(new Sorting("id", Sorting.Order.DESC)));
QueryStateRequest request = new QueryStateRequest(STATE_STORE_NAME)
.setQuery(query);
// 使用预览客户端调用查询状态 API
QueryStateResponse<MyData> result = previewClient.queryState(request, MyData.class).block();
// 查看查询状态响应
System.out.println("Found " + result.getResults().size() + " items.");
for (QueryStateItem<Listing> item : result.getResults()) {
System.out.println("Key: " + item.getKey());
System.out.println("Data: " + item.getValue());
}
}
- 有关查询状态的完整说明,请访问如何:查询状态。
- 访问 Java SDK 示例获取完整的代码示例。
分布式锁
package io.dapr.examples.lock.grpc;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.DaprPreviewClient;
import io.dapr.client.domain.LockRequest;
import io.dapr.client.domain.UnlockRequest;
import io.dapr.client.domain.UnlockResponseStatus;
import reactor.core.publisher.Mono;
public class DistributedLockGrpcClient {
private static final String LOCK_STORE_NAME = "lockstore";
/**
* 执行各种方法以检查不同的 API。
*
* @param args 参数
* @throws Exception 抛出异常
*/
public static void main(String[] args) throws Exception {
try (DaprPreviewClient client = (new DaprClientBuilder()).buildPreviewClient()) {
System.out.println("Using preview client...");
tryLock(client);
unlock(client);
}
}
/**
* 尝试获取锁。
*
* @param client DaprPreviewClient 对象
*/
public static void tryLock(DaprPreviewClient client) {
System.out.println("*******trying to get a free distributed lock********");
try {
LockRequest lockRequest = new LockRequest(LOCK_STORE_NAME, "resouce1", "owner1", 5);
Mono<Boolean> result = client.tryLock(lockRequest);
System.out.println("Lock result -> " + (Boolean.TRUE.equals(result.block()) ? "SUCCESS" : "FAIL"));
} catch (Exception ex) {
System.out.println(ex.getMessage());
}
}
/**
* 解锁。
*
* @param client DaprPreviewClient 对象
*/
public static void unlock(DaprPreviewClient client) {
System.out.println("*******unlock a distributed lock********");
try {
UnlockRequest unlockRequest = new UnlockRequest(LOCK_STORE_NAME, "resouce1", "owner1");
Mono<UnlockResponseStatus> result = client.unlock(unlockRequest);
System.out.println("Unlock result ->" + result.block().name());
} catch (Exception ex) {
System.out.println(ex.getMessage());
}
}
}
- 有关分布式锁的完整说明,请访问如何:使用锁
- 访问 Java SDK 示例获取完整的代码示例。
工作流
package io.dapr.examples.workflows;
import io.dapr.workflows.client.DaprWorkflowClient;
import io.dapr.workflows.client.WorkflowState;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* 有关设置说明,请参阅 README。
*/
public class DemoWorkflowClient {
/**
* 主方法。
*
* @param args 输入参数(未使用)。
* @throws InterruptedException 如果程序已中断。
*/
public static void main(String[] args) throws InterruptedException {
DaprWorkflowClient client = new DaprWorkflowClient();
try (client) {
String separatorStr = "*******";
System.out.println(separatorStr);
String instanceId = client.scheduleNewWorkflow(DemoWorkflow.class, "input data");
System.out.printf("Started new workflow instance with random ID: %s%n", instanceId);
System.out.println(separatorStr);
System.out.println("**GetWorkflowMetadata:Running Workflow**");
WorkflowState workflowMetadata = client.getWorkflowState(instanceId, true);
System.out.printf("Result: %s%n", workflowMetadata);
System.out.println(separatorStr);
System.out.println("**WaitForWorkflowStart**");
try {
WorkflowState waitForWorkflowStartResult =
client.waitForWorkflowStart(instanceId, Duration.ofSeconds(60), true);
System.out.printf("Result: %s%n", waitForWorkflowStartResult);
} catch (TimeoutException ex) {
System.out.printf("waitForWorkflowStart has an exception:%s%n", ex);
}
System.out.println(separatorStr);
System.out.println("**SendExternalMessage**");
client.raiseEvent(instanceId, "TestEvent", "TestEventPayload");
System.out.println(separatorStr);
System.out.println("** Registering parallel Events to be captured by allOf(t1,t2,t3) **");
client.raiseEvent(instanceId, "event1", "TestEvent 1 Payload");
client.raiseEvent(instanceId, "event2", "TestEvent 2 Payload");
client.raiseEvent(instanceId, "event3", "TestEvent 3 Payload");
System.out.printf("Events raised for workflow with instanceId: %s\n", instanceId);
System.out.println(separatorStr);
System.out.println("** Registering Event to be captured by anyOf(t1,t2,t3) **");
client.raiseEvent(instanceId, "e2", "event 2 Payload");
System.out.printf("Event raised for workflow with instanceId: %s\n", instanceId);
System.out.println(separatorStr);
System.out.println("**waitForWorkflowCompletion**");
try {
WorkflowState waitForWorkflowCompletionResult =
client.waitForWorkflowCompletion(instanceId, Duration.ofSeconds(60), true);
System.out.printf("Result: %s%n", waitForWorkflowCompletionResult);
} catch (TimeoutException ex) {
System.out.printf("waitForWorkflowCompletion has an exception:%s%n", ex);
}
System.out.println(separatorStr);
System.out.println("**purgeWorkflow**");
boolean purgeResult = client.purgeWorkflow(instanceId);
System.out.printf("purgeResult: %s%n", purgeResult);
System.out.println(separatorStr);
System.out.println("**raiseEvent**");
String eventInstanceId = client.scheduleNewWorkflow(DemoWorkflow.class);
System.out.printf("Started new workflow instance with random ID: %s%n", eventInstanceId);
client.raiseEvent(eventInstanceId, "TestException", null);
System.out.printf("Event raised for workflow with instanceId: %s\n", eventInstanceId);
System.out.println(separatorStr);
String instanceToTerminateId = "terminateMe";
client.scheduleNewWorkflow(DemoWorkflow.class, null, instanceToTerminateId);
System.out.printf("Started new workflow instance with specified ID: %s%n", instanceToTerminateId);
TimeUnit.SECONDS.sleep(5);
System.out.println("Terminate this workflow instance manually before the timeout is reached");
client.terminateWorkflow(instanceToTerminateId, null);
System.out.println(separatorStr);
String restartingInstanceId = "restarting";
client.scheduleNewWorkflow(DemoWorkflow.class, null, restartingInstanceId);
System.out.printf("Started new workflow instance with ID: %s%n", restartingInstanceId);
System.out.println("Sleeping 30 seconds to restart the workflow");
TimeUnit.SECONDS.sleep(30);
System.out.println("**SendExternalMessage: RestartEvent**");
client.raiseEvent(restartingInstanceId, "RestartEvent", "RestartEventPayload");
System.out.println("Sleeping 30 seconds to terminate the eternal workflow");
TimeUnit.SECONDS.sleep(30);
client.terminateWorkflow(restartingInstanceId, null);
}
System.out.println("Exiting DemoWorkflowClient.");
System.exit(0);
}
}
- 有关工作流的完整指南,请访问:
- 了解有关如何将 Java SDK 与工作流一起使用的更多信息。
边车 API
等待边车
DaprClient 还提供了一个辅助方法来等待边车变得健康(仅限组件)。使用此方法时,请务必指定超时时间(以毫秒为单位)并使用 block() 来等待响应式操作的结果。
// 在尝试使用 Dapr 组件之前,等待 Dapr 边车报告健康。
try (DaprClient client = new DaprClientBuilder().build()) {
System.out.println("Waiting for Dapr sidecar ...");
client.waitForSidecar(10000).block(); // 以毫秒为单位指定超时时间
System.out.println("Dapr sidecar is ready.");
...
}
// 在此处执行 Dapr 组件操作,例如获取密钥或保存状态。
关闭边车
try (DaprClient client = new DaprClientBuilder().build()) {
logger.info("Sending shutdown request.");
client.shutdown().block();
logger.info("Ensuring dapr has stopped.");
...
}
了解更多有关可添加到 Java 应用程序的 Dapr Java SDK 包的信息。
安全
应用 API 令牌身份验证
像发布订阅、输入绑定或作业这样的构建块需要 Dapr 向您的应用程序发出传入调用,您可以使用 Dapr 应用 API 令牌身份验证来保护这些请求。这确保只有 Dapr 可以调用您应用程序的端点。
了解两种令牌
Dapr 使用两种不同的令牌来保护通信。有关这两种令牌的详细信息,请参阅属性:
DAPR_API_TOKEN(您的应用 → Dapr 边车):使用DaprClient时由 Java SDK 自动处理APP_API_TOKEN(Dapr → 您的应用):需要在应用程序中进行服务器端验证
下面的示例展示如何为 APP_API_TOKEN 实施服务器端验证。
实施服务器端令牌验证
使用 gRPC 协议时,实施服务器拦截器来捕获元数据。
import io.grpc.Context;
import io.grpc.Contexts;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
public class SubscriberGrpcService extends AppCallbackGrpc.AppCallbackImplBase {
public static final Context.Key<Metadata> METADATA_KEY = Context.key("grpc-metadata");
// gRPC 拦截器来捕获元数据
public static class MetadataInterceptor implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
Context contextWithMetadata = Context.current().withValue(METADATA_KEY, headers);
return Contexts.interceptCall(contextWithMetadata, call, headers, next);
}
}
// 您的服务方法放在这里...
}
在构建 gRPC 服务器时注册拦截器:
Server server = ServerBuilder.forPort(port)
.intercept(new SubscriberGrpcService.MetadataInterceptor())
.addService(new SubscriberGrpcService())
.build();
server.start();
然后,在您的服务方法中,从元数据中提取令牌:
@Override
public void onTopicEvent(DaprAppCallbackProtos.TopicEventRequest request,
StreamObserver<DaprAppCallbackProtos.TopicEventResponse> responseObserver) {
try {
// 从上下文中提取元数据
Context context = Context.current();
Metadata metadata = METADATA_KEY.get(context);
if (metadata != null) {
String apiToken = metadata.get(
Metadata.Key.of("dapr-api-token", Metadata.ASCII_STRING_MARSHALLER));
// 相应地验证令牌
}
// 处理请求
// ...
} catch (Throwable e) {
responseObserver.onError(e);
}
}
与 HTTP 端点一起使用
对于基于 HTTP 的端点,从标头中提取令牌:
@RestController
public class SubscriberController {
@PostMapping(path = "/endpoint")
public Mono<Void> handleRequest(
@RequestBody(required = false) byte[] body,
@RequestHeader Map<String, String> headers) {
return Mono.fromRunnable(() -> {
try {
// 从标头中提取令牌
String apiToken = headers.get("dapr-api-token");
// 相应地验证令牌
// 处理请求
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
}
示例
有关使用发布订阅、绑定和作业的工作示例:
相关链接
有关 SDK 属性的完整列表以及如何配置它们,请访问属性。
2.3.2.1 - 属性
属性
Dapr Java SDK 提供了一组控制 SDK 行为的全局属性。这些属性可以通过环境变量或系统属性进行配置。系统属性可以在运行 Java 应用程序时使用 -D 标志进行设置。
这些属性会影响整个 SDK,包括客户端和运行时。它们控制的方面包括:
- 边车连接(端点、端口)
- 安全设置(TLS、API 令牌)
- 性能调优(超时、连接池)
- 协议设置(gRPC、HTTP)
- 字符串编码
环境变量
以下环境变量可用于配置 Dapr Java SDK:
边车端点
当设置这些变量时,客户端将自动使用它们连接到 Dapr 边车。
| 环境变量 | 描述 | 默认值 |
|---|---|---|
DAPR_GRPC_ENDPOINT | Dapr 边车的 gRPC 端点 | localhost:50001 |
DAPR_HTTP_ENDPOINT | Dapr 边车的 HTTP 端点 | localhost:3500 |
DAPR_GRPC_PORT | Dapr 边车的 gRPC 端口(已弃用,DAPR_GRPC_ENDPOINT 优先级更高) | 50001 |
DAPR_HTTP_PORT | Dapr 边车的 HTTP 端口(已弃用,DAPR_HTTP_ENDPOINT 优先级更高) | 3500 |
API 令牌
Dapr 支持两种类型的 API 令牌来保护通信:
| 环境变量 | 描述 | 默认值 |
|---|---|---|
DAPR_API_TOKEN | 用于验证从你的应用向 Dapr 边车发起请求的 API 令牌。当使用 DaprClient 时,Java SDK 会自动在请求中包含此令牌。 | null |
APP_API_TOKEN | 用于验证从 Dapr 向你的应用发起请求的 API 令牌。当设置此变量时,Dapr 在调用你的应用程序时(例如发布订阅订阅者、输入绑定或任务触发器),会在 dapr-api-token 请求头/元数据中包含此令牌。你的应用程序必须验证此令牌。 | null |
有关实现示例,请参阅 App API Token Authentication。更多详情请参阅 Dapr API token authentication。
gRPC 配置
TLS 设置
为了安全地进行 gRPC 通信,你可以使用以下环境变量配置 TLS 设置:
| 环境变量 | 描述 | 默认值 |
|---|---|---|
DAPR_GRPC_TLS_INSECURE | 当设置为 “true” 时,启用不安全的 TLS 模式,该模式仍然使用 TLS 但不验证证书。这会使用 InsecureTrustManagerFactory 来信任所有证书。应仅用于测试或安全环境。 | false |
DAPR_GRPC_TLS_CA_PATH | CA 证书文件的路径。用于与具有自签名证书的服务器建立 TLS 连接。 | null |
DAPR_GRPC_TLS_CERT_PATH | 用于客户端认证的 TLS 证书文件路径。 | null |
DAPR_GRPC_TLS_KEY_PATH | 用于客户端认证的 TLS 私钥文件路径。 | null |
Keepalive 设置
使用以下环境变量配置 gRPC keepalive 行为:
| 环境变量 | 描述 | 默认值 |
|---|---|---|
DAPR_GRPC_ENABLE_KEEP_ALIVE | 是否启用 gRPC keepalive | false |
DAPR_GRPC_KEEP_ALIVE_TIME_SECONDS | gRPC keepalive 时间(秒) | 10 |
DAPR_GRPC_KEEP_ALIVE_TIMEOUT_SECONDS | gRPC keepalive 超时(秒) | 5 |
DAPR_GRPC_KEEP_ALIVE_WITHOUT_CALLS | 是否在没有调用时保持 gRPC 连接存活 | true |
入站消息设置
使用以下环境变量配置 gRPC 入站消息设置:
| 环境变量 | 描述 | 默认值 |
|---|---|---|
DAPR_GRPC_MAX_INBOUND_MESSAGE_SIZE_BYTES | Dapr 的 gRPC 最大入站消息大小(字节)。此值设置应用程序可以接收的 gRPC 消息的最大大小 | 4194304 |
DAPR_GRPC_MAX_INBOUND_METADATA_SIZE_BYTES | Dapr 的 gRPC 最大入站元数据大小(字节) | 8192 |
HTTP 客户端配置
这些属性控制用于与 Dapr 边车通信的 HTTP 客户端的行为:
| 环境变量 | 描述 | 默认值 |
|---|---|---|
DAPR_HTTP_CLIENT_READ_TIMEOUT_SECONDS | HTTP 客户端读取操作的超时时间(秒)。这是等待 Dapr 边车响应的最长时间。 | 60 |
DAPR_HTTP_CLIENT_MAX_REQUESTS | 可以同时执行的最大 HTTP 请求数。超过此限制后,请求将在内存中排队等待正在运行的调用完成。 | 1024 |
DAPR_HTTP_CLIENT_MAX_IDLE_CONNECTIONS | HTTP 连接池中的最大空闲连接数。这是池中可以保持空闲的最大连接数。 | 128 |
API 配置
这些属性控制通过 SDK 发起的 API 调用的行为:
| 环境变量 | 描述 | 默认值 |
|---|---|---|
DAPR_API_MAX_RETRIES | 向 Dapr 边车发起 API 调用时,可重试异常的最大重试次数 | 0 |
DAPR_API_TIMEOUT_MILLISECONDS | 向 Dapr 边车发起 API 调用的超时时间(毫秒)。值为 0 表示无超时。 | 0 |
字符串编码
| 环境变量 | 描述 | 默认值 |
|---|---|---|
DAPR_STRING_CHARSET | SDK 中用于字符串编码/解码的字符集。必须是有效的 Java 字符集名称。 | UTF-8 |
系统属性
所有环境变量都可以使用 -D 标志设置为系统属性。以下是可用的系统属性的完整列表:
| 系统属性 | 描述 | 默认值 |
|---|---|---|
dapr.sidecar.ip | Dapr 边车的 IP 地址 | localhost |
dapr.http.port | Dapr 边车的 HTTP 端口 | 3500 |
dapr.grpc.port | Dapr 边车的 gRPC 端口 | 50001 |
dapr.grpc.tls.cert.path | gRPC TLS 证书的路径 | null |
dapr.grpc.tls.key.path | gRPC TLS 密钥的路径 | null |
dapr.grpc.tls.ca.path | gRPC TLS CA 证书的路径 | null |
dapr.grpc.tls.insecure | 是否使用不安全的 TLS 模式 | false |
dapr.grpc.endpoint | 远程边车的 gRPC 端点 | null |
dapr.grpc.enable.keep.alive | 是否启用 gRPC keepalive | false |
dapr.grpc.keep.alive.time.seconds | gRPC keepalive 时间(秒) | 10 |
dapr.grpc.keep.alive.timeout.seconds | gRPC keepalive 超时(秒) | 5 |
dapr.grpc.keep.alive.without.calls | 是否在没有调用时保持 gRPC 连接存活 | true |
dapr.http.endpoint | 远程边车的 HTTP 端点 | null |
dapr.api.maxRetries | API 调用的最大重试次数 | 0 |
dapr.api.timeoutMilliseconds | API 调用的超时时间(毫秒) | 0 |
dapr.api.token | 用于身份验证的 API 令牌 | null |
dapr.string.charset | SDK 中使用的字符串编码 | UTF-8 |
dapr.http.client.readTimeoutSeconds | HTTP 客户端读取的超时时间(秒) | 60 |
dapr.http.client.maxRequests | 最大并发 HTTP 请求数 | 1024 |
dapr.http.client.maxIdleConnections | 最大空闲 HTTP 连接数 | 128 |
属性解析顺序
属性按以下顺序解析:
- 覆盖值(如果在创建 Properties 实例时提供)
- 系统属性(通过
-D设置) - 环境变量
- 默认值
SDK 按顺序检查每个来源。如果某个值对属性类型无效(例如,数值属性为非数字),SDK 将记录警告并尝试下一个来源。例如:
# 无效的布尔值 - 将被忽略
java -Ddapr.grpc.enable.keep.alive=not-a-boolean -jar myapp.jar
# 有效的布尔值 - 将被使用
export DAPR_GRPC_ENABLE_KEEP_ALIVE=false
在这种情况下,将使用环境变量,因为系统属性值无效。但是,如果两个值都有效,系统属性优先:
# 有效的布尔值 - 将被使用
java -Ddapr.grpc.enable.keep.alive=true -jar myapp.jar
# 有效的布尔值 - 将被忽略
export DAPR_GRPC_ENABLE_KEEP_ALIVE=false
可以通过 DaprClientBuilder 以两种方式设置覆盖值:
- 使用单个属性覆盖(大多数情况下推荐):
import io.dapr.config.Properties;
// 设置单个属性覆盖
DaprClient client = new DaprClientBuilder()
.withPropertyOverride(Properties.GRPC_ENABLE_KEEP_ALIVE, "true")
.build();
// 或设置多个属性覆盖
DaprClient client = new DaprClientBuilder()
.withPropertyOverride(Properties.GRPC_ENABLE_KEEP_ALIVE, "true")
.withPropertyOverride(Properties.HTTP_CLIENT_READ_TIMEOUT_SECONDS, "120")
.build();
- 使用 Properties 实例(当你需要一次设置多个属性时很有用):
// 创建属性覆盖的映射
Map<String, String> overrides = new HashMap<>();
overrides.put("dapr.grpc.enable.keep.alive", "true");
overrides.put("dapr.http.client.readTimeoutSeconds", "120");
// 创建带有覆盖的 Properties 实例
Properties properties = new Properties(overrides);
// 在创建客户端时使用这些属性
DaprClient client = new DaprClientBuilder()
.withProperties(properties)
.build();
对于大多数用例,你会使用系统属性或环境变量。覆盖值主要用于在同一应用程序中需要为 SDK 的不同实例设置不同的属性值时。
代理配置
你可以使用系统属性为 Java 应用程序配置代理设置。这些是 Java 网络层(java.net 包)的标准 Java 系统属性,并非 Dapr 特有。它们会被 Java 的网络栈使用,包括 Dapr SDK 使用的 HTTP 客户端。
有关 Java 代理配置的详细信息,包括所有可用属性及其用法,请参阅 Java Networking Properties documentation。
例如,以下是配置代理的方法:
# 配置 HTTP 代理 - 替换为你的实际代理服务器详细信息
java -Dhttp.proxyHost=your-proxy-server.com -Dhttp.proxyPort=8080 -jar myapp.jar
# 配置 HTTPS 代理 - 替换为你的实际代理服务器详细信息
java -Dhttps.proxyHost=your-proxy-server.com -Dhttps.proxyPort=8443 -jar myapp.jar
将 your-proxy-server.com 替换为你的实际代理服务器主机名或 IP 地址,并调整端口号以匹配你的代理服务器配置。
这些代理设置会影响 Java 应用程序发起的所有 HTTP/HTTPS 连接,包括与 Dapr 边车的连接。
2.3.3 - 工作流
2.3.3.1 - 操作指南:在 Java SDK 中编写和管理 Dapr 工作流
让我们创建一个 Dapr 工作流并通过控制台调用它。借助提供的工作流示例,你将:
- 使用 Java 工作流 worker 执行工作流实例
- 利用 Java 工作流客户端和 API 调用来启动和终止工作流实例
本示例使用自托管模式下通过 dapr init 初始化的默认配置。
前置条件
- Dapr CLI 和已初始化的环境。
- Java JDK 11(或更高版本):
- Oracle JDK,或
- OpenJDK
- Apache Maven,版本 3.x。
- 验证你使用的是最新的 proto 绑定
设置环境
克隆 Java SDK 仓库并进入该目录。
git clone https://github.com/dapr/java-sdk.git
cd java-sdk
运行以下命令以安装使用 Dapr Java SDK 运行此工作流示例所需的要求。
mvn clean install
从 Java SDK 根目录进入 Dapr Workflow 示例。
cd examples
运行 DemoWorkflowWorker
DemoWorkflowWorker 类在 Dapr 的工作流运行时引擎中注册 DemoWorkflow 的实现。在 DemoWorkflowWorker.java 文件中,你可以找到 DemoWorkflowWorker 类和 main 方法:
public class DemoWorkflowWorker {
public static void main(String[] args) throws Exception {
// 在运行时中注册工作流。
WorkflowRuntime.getInstance().registerWorkflow(DemoWorkflow.class);
System.out.println("Start workflow runtime");
WorkflowRuntime.getInstance().startAndBlock();
System.exit(0);
}
}
在上述代码中:
WorkflowRuntime.getInstance().registerWorkflow()将DemoWorkflow作为工作流注册到 Dapr Workflow 运行时中。WorkflowRuntime.getInstance().start()在 Dapr Workflow 运行时内构建并启动引擎。
在终端中,执行以下命令以启动 DemoWorkflowWorker:
dapr run --app-id demoworkflowworker --resources-path ./components/workflows --dapr-grpc-port 50001 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.DemoWorkflowWorker
预期输出
You're up and running! Both Dapr and your app logs will appear here.
...
== APP == Start workflow runtime
== APP == Sep 13, 2023 9:02:03 AM com.microsoft.durabletask.DurableTaskGrpcWorker startAndBlock
== APP == INFO: Durable Task worker is connecting to sidecar at 127.0.0.1:50001.
运行 DemoWorkflowClient
DemoWorkflowClient 启动已注册到 Dapr 的工作流实例。
public class DemoWorkflowClient {
// ...
public static void main(String[] args) throws InterruptedException {
DaprWorkflowClient client = new DaprWorkflowClient();
try (client) {
String separatorStr = "*******";
System.out.println(separatorStr);
String instanceId = client.scheduleNewWorkflow(DemoWorkflow.class, "input data");
System.out.printf("Started new workflow instance with random ID: %s%n", instanceId);
System.out.println(separatorStr);
System.out.println("**GetInstanceMetadata:Running Workflow**");
WorkflowState workflowMetadata = client.getWorkflowState(instanceId, true);
System.out.printf("Result: %s%n", workflowMetadata);
System.out.println(separatorStr);
System.out.println("**WaitForWorkflowStart**");
try {
WorkflowState waitForWorkflowStartResult =
client.waitForWorkflowStart(instanceId, Duration.ofSeconds(60), true);
System.out.printf("Result: %s%n", waitForWorkflowStartResult);
} catch (TimeoutException ex) {
System.out.printf("waitForWorkflowStart has an exception:%s%n", ex);
}
System.out.println(separatorStr);
System.out.println("**SendExternalMessage**");
client.raiseEvent(instanceId, "TestEvent", "TestEventPayload");
System.out.println(separatorStr);
System.out.println("** Registering parallel Events to be captured by allOf(t1,t2,t3) **");
client.raiseEvent(instanceId, "event1", "TestEvent 1 Payload");
client.raiseEvent(instanceId, "event2", "TestEvent 2 Payload");
client.raiseEvent(instanceId, "event3", "TestEvent 3 Payload");
System.out.printf("Events raised for workflow with instanceId: %s\n", instanceId);
System.out.println(separatorStr);
System.out.println("** Registering Event to be captured by anyOf(t1,t2,t3) **");
client.raiseEvent(instanceId, "e2", "event 2 Payload");
System.out.printf("Event raised for workflow with instanceId: %s\n", instanceId);
System.out.println(separatorStr);
System.out.println("**waitForWorkflowCompletion**");
try {
WorkflowState waitForWorkflowCompletionResult =
client.waitForWorkflowCompletion(instanceId, Duration.ofSeconds(60), true);
System.out.printf("Result: %s%n", waitForWorkflowCompletionResult);
} catch (TimeoutException ex) {
System.out.printf("waitForWorkflowCompletion has an exception:%s%n", ex);
}
System.out.println(separatorStr);
System.out.println("**purgeWorkflow**");
boolean purgeResult = client.purgeWorkflow(instanceId);
System.out.printf("purgeResult: %s%n", purgeResult);
System.out.println(separatorStr);
System.out.println("**raiseEvent**");
String eventInstanceId = client.scheduleNewWorkflow(DemoWorkflow.class);
System.out.printf("Started new workflow instance with random ID: %s%n", eventInstanceId);
client.raiseEvent(eventInstanceId, "TestException", null);
System.out.printf("Event raised for workflow with instanceId: %s\n", eventInstanceId);
System.out.println(separatorStr);
String instanceToTerminateId = "terminateMe";
client.scheduleNewWorkflow(DemoWorkflow.class, null, instanceToTerminateId);
System.out.printf("Started new workflow instance with specified ID: %s%n", instanceToTerminateId);
TimeUnit.SECONDS.sleep(5);
System.out.println("Terminate this workflow instance manually before the timeout is reached");
client.terminateWorkflow(instanceToTerminateId, null);
System.out.println(separatorStr);
String restartingInstanceId = "restarting";
client.scheduleNewWorkflow(DemoWorkflow.class, null, restartingInstanceId);
System.out.printf("Started new workflow instance with ID: %s%n", restartingInstanceId);
System.out.println("Sleeping 30 seconds to restart the workflow");
TimeUnit.SECONDS.sleep(30);
System.out.println("**SendExternalMessage: RestartEvent**");
client.raiseEvent(restartingInstanceId, "RestartEvent", "RestartEventPayload");
System.out.println("Sleeping 30 seconds to terminate the eternal workflow");
TimeUnit.SECONDS.sleep(30);
client.terminateWorkflow(restartingInstanceId, null);
}
System.out.println("Exiting DemoWorkflowClient.");
System.exit(0);
}
}
在第二个终端窗口中,运行以下命令以启动工作流:
java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.DemoWorkflowClient
预期输出
*******
Started new workflow instance with random ID: 0b4cc0d5-413a-4c1c-816a-a71fa24740d4
*******
**GetInstanceMetadata:Running Workflow**
Result: [Name: 'io.dapr.examples.workflows.DemoWorkflow', ID: '0b4cc0d5-413a-4c1c-816a-a71fa24740d4', RuntimeStatus: RUNNING, CreatedAt: 2023-09-13T13:02:30.547Z, LastUpdatedAt: 2023-09-13T13:02:30.699Z, Input: '"input data"', Output: '']
*******
**WaitForWorkflowStart**
Result: [Name: 'io.dapr.examples.workflows.DemoWorkflow', ID: '0b4cc0d5-413a-4c1c-816a-a71fa24740d4', RuntimeStatus: RUNNING, CreatedAt: 2023-09-13T13:02:30.547Z, LastUpdatedAt: 2023-09-13T13:02:30.699Z, Input: '"input data"', Output: '']
*******
**SendExternalMessage**
*******
** Registering parallel Events to be captured by allOf(t1,t2,t3) **
Events raised for workflow with instanceId: 0b4cc0d5-413a-4c1c-816a-a71fa24740d4
*******
** Registering Event to be captured by anyOf(t1,t2,t3) **
Event raised for workflow with instanceId: 0b4cc0d5-413a-4c1c-816a-a71fa24740d4
*******
**WaitForWorkflowCompletion**
Result: [Name: 'io.dapr.examples.workflows.DemoWorkflow', ID: '0b4cc0d5-413a-4c1c-816a-a71fa24740d4', RuntimeStatus: FAILED, CreatedAt: 2023-09-13T13:02:30.547Z, LastUpdatedAt: 2023-09-13T13:02:55.054Z, Input: '"input data"', Output: '']
*******
**purgeWorkflow**
purgeResult: true
*******
**raiseEvent**
Started new workflow instance with random ID: 7707d141-ebd0-4e54-816e-703cb7a52747
Event raised for workflow with instanceId: 7707d141-ebd0-4e54-816e-703cb7a52747
*******
Started new workflow instance with specified ID: terminateMe
Terminate this workflow instance manually before the timeout is reached
*******
Started new workflow instance with ID: restarting
Sleeping 30 seconds to restart the workflow
**SendExternalMessage: RestartEvent**
Sleeping 30 seconds to terminate the eternal workflow
Exiting DemoWorkflowClient.
发生了什么?
- 当你运行
dapr run时,工作流 worker 将工作流(DemoWorkflow)及其活动注册到 Dapr Workflow 引擎中。 - 当你运行
java时,工作流客户端通过以下活动启动了工作流实例。你可以在运行dapr run的终端中查看相应的输出。- 工作流启动,引发三个并行任务,并等待它们完成。
- 工作流客户端调用活动,并将 “Hello Activity” 消息发送到控制台。
- 工作流超时并被清除。
- 工作流客户端使用随机 ID 启动新的工作流实例,使用另一个名为
terminateMe的工作流实例将其终止,并使用名为restarting的工作流重启它。 - 然后工作流客户端退出。
后续步骤
高级特性
任务执行键
任务执行键是由 durabletask-java 库生成的唯一标识符。它们存储在 WorkflowActivityContext 中,可用于跟踪和管理工作流活动的执行。它们特别适用于:
- 幂等性:确保同一任务的活动不会被执行多次
- 状态管理:跟踪活动执行的状态
- 错误处理:以受控方式管理重试和失败
以下是在工作流活动中使用任务执行键的示例:
public class TaskExecutionKeyActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
// 获取此活动的任务执行键
String taskExecutionKey = ctx.getTaskExecutionKey();
// 使用该键来实现幂等性或状态管理
// 例如,检查此任务是否已执行
if (isTaskAlreadyExecuted(taskExecutionKey)) {
return getPreviousResult(taskExecutionKey);
}
// 执行活动逻辑
Object result = executeActivityLogic();
// 使用任务执行键存储结果
storeResult(taskExecutionKey, result);
return result;
}
}
2.3.4 - 作业
2.3.4.1 - 如何:使用 Java SDK 编写和管理 Dapr Jobs
在本演示中,我们将调度一个 Dapr Job。调度的 Job 将触发同一应用中注册的端点。使用提供的 Jobs 示例,你将:
本示例使用自托管模式下 dapr init 的默认配置。
前置条件
- Dapr CLI 和已初始化的环境。
- Java JDK 11(或更高版本):
- Oracle JDK,或
- OpenJDK
- Apache Maven,版本 3.x。
- Docker Desktop
设置环境
克隆 Java SDK 仓库 并进入该目录。
git clone https://github.com/dapr/java-sdk.git
cd java-sdk
运行以下命令以安装使用 Dapr Java SDK 运行 jobs 示例所需的要求。
mvn clean install -DskipTests
从 Java SDK 根目录进入示例目录。
cd examples
运行 Dapr sidecar。
dapr run --app-id jobsapp --dapr-grpc-port 51439 --dapr-http-port 3500 --app-port 8080
现在,Dapr 正在
http://localhost:3500监听 HTTP 请求,在http://localhost:51439监听内部 Jobs gRPC 请求。
调度和获取 Job
在 DemoJobsClient 中有调度 Job 的步骤。使用 DaprPreviewClient 调用 scheduleJob
将向 Dapr Runtime 调度一个 Job。
public class DemoJobsClient {
/**
* The main method of this app to schedule and get jobs.
*/
public static void main(String[] args) throws Exception {
try (DaprPreviewClient client = new DaprClientBuilder().withPropertyOverrides(overrides).buildPreviewClient()) {
// Schedule a job.
System.out.println("**** Scheduling a Job with name dapr-jobs-1 *****");
ScheduleJobRequest scheduleJobRequest = new ScheduleJobRequest("dapr-job-1",
JobSchedule.fromString("* * * * * *")).setData("Hello World!".getBytes());
client.scheduleJob(scheduleJobRequest).block();
System.out.println("**** Scheduling job dapr-jobs-1 completed *****");
}
}
}
调用 getJob 以检索先前创建并调度的 Job 详细信息。
client.getJob(new GetJobRequest("dapr-job-1")).block()
使用以下命令运行 DemoJobsClient。
java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.jobs.DemoJobsClient
示例输出
**** Scheduling a Job with name dapr-jobs-1 *****
**** Scheduling job dapr-jobs-1 completed *****
**** Retrieving a Job with name dapr-jobs-1 *****
设置一个在 Job 触发时被调用的端点
DemoJobsSpringApplication 类启动一个 Spring Boot 应用,该应用注册 JobsController 中指定的端点
该端点充当对调度的 Job 请求的回调。
@RestController
public class JobsController {
/**
* Handles jobs callback from Dapr.
*
* @param jobName name of the job.
* @param payload data from the job if payload exists.
* @return Empty Mono.
*/
@PostMapping("/job/{jobName}")
public Mono<Void> handleJob(@PathVariable("jobName") String jobName,
@RequestBody(required = false) byte[] payload) {
System.out.println("Job Name: " + jobName);
System.out.println("Job Payload: " + new String(payload));
return Mono.empty();
}
}
参数:
jobName:被触发的 Job 的名称。payload:与 Job 关联的可选负载数据(作为字节数组)。
使用以下命令运行 Spring Boot 应用。
java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.jobs.DemoJobsSpringApplication
示例输出
Job Name: dapr-job-1
Job Payload: Hello World!
删除一个调度的 Job
public class DemoJobsClient {
/**
* The main method of this app deletes a job that was previously scheduled.
*/
public static void main(String[] args) throws Exception {
try (DaprPreviewClient client = new DaprClientBuilder().buildPreviewClient()) {
// Delete a job.
System.out.println("**** Delete a Job with name dapr-jobs-1 *****");
client.deleteJob(new DeleteJobRequest("dapr-job-1")).block();
}
}
}
后续步骤
2.3.5 - Dapr 与 Spring Boot 入门
通过结合 Dapr 和 Spring Boot,我们可以创建独立于基础设施的 Java 应用程序,这些应用程序可以部署到不同的环境中,支持广泛的本地和云提供商服务。
首先,我们将从一个涵盖 DaprClient 和 Testcontainers 集成的简单集成开始,然后使用 Spring 和 Spring Boot 机制和编程模型来利用底层的 Dapr API。这有助于团队移除连接到特定环境的基础设施(数据库、键值存储、消息代理、配置/密钥存储等)所需的客户端和驱动程序等依赖项。
注意
Spring Boot 集成需要 Spring Boot 3.x+ 才能工作。这不适用于 Spring Boot 2.x。 Spring Boot 集成仍处于 alpha 阶段。我们需要您的帮助和反馈来使其毕业。 请加入 #java-sdk discord 频道 讨论或在 dapr/java-sdk 中提出问题。将 Dapr 和 Spring Boot 集成添加到您的项目
如果您已经有 Spring Boot 应用程序,可以直接将以下依赖项添加到您的项目中:
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter</artifactId>
<version>1.16.0</version>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter-test</artifactId>
<version>1.16.0</version>
<scope>test</scope>
</dependency>
您可以在此处找到最新发布的版本。
通过添加这些依赖项,您可以:
- 在应用程序内自动装配
DaprClient以供使用 - 使用 Spring Data 和 Messaging 抽象以及编程模型,这些模型在底层使用 Dapr API
- 通过依赖 Testcontainers 来引导 Dapr 控制平面服务和默认组件,从而改进您的内部开发循环
一旦这些依赖项在您的应用程序中,您就可以依赖 Spring Boot 自动配置来自动装配 DaprClient 实例:
@Autowired
private DaprClient daprClient;
这将连接到默认的 Dapr gRPC 端点 localhost:50001,要求您在应用程序外部启动 Dapr。
注意
默认情况下,以下属性是为 DaprClient 和 DaprWorkflowClient 预配置的:
dapr.client.httpEndpoint=http://localhost
dapr.client.httpPort=3500
dapr.client.grpcEndpoint=localhost
dapr.client.grpcPort=50001
dapr.client.apiToken=<your remote api token>
这些值是默认使用的,但您可以在 application.properties 文件中覆盖它们以适应您的环境。请注意,同时支持 kebab-case 和 camelCase。
您可以在应用程序的任何位置使用 DaprClient 与 Dapr API 交互,例如从 REST 端点内部:
@RestController
public class DemoRestController {
@Autowired
private DaprClient daprClient;
@PostMapping("/store")
public void storeOrder(@RequestBody Order order){
daprClient.saveState("kvstore", order.orderId(), order).block();
}
}
record Order(String orderId, Integer amount){}
如果您希望在 Spring Boot 应用程序外部避免管理 Dapr,可以依赖 Testcontainers 在应用程序旁边引导 Dapr 以用于开发目的。
为此,我们可以创建一个使用 Testcontainers 来引导使用 Dapr API 开发应用程序所需的所有内容的测试配置。
使用 Testcontainers 和 Dapr 集成,我们让 @TestConfiguration 为我们的应用程序引导 Dapr。
请注意,对于此示例,我们正在使用一个名为 kvstore 的 Statestore 组件配置 Dapr,该组件连接到也由 Testcontainers 引导的 PostgreSQL 实例。
@TestConfiguration(proxyBeanMethods = false)
public class DaprTestContainersConfig {
@Bean
@ServiceConnection
public DaprContainer daprContainer(Network daprNetwork, PostgreSQLContainer<?> postgreSQLContainer){
return new DaprContainer("daprio/daprd:1.16.0-rc.5")
.withAppName("producer-app")
.withNetwork(daprNetwork)
.withComponent(new Component("kvstore", "state.postgresql", "v1", STATE_STORE_PROPERTIES))
.withComponent(new Component("kvbinding", "bindings.postgresql", "v1", BINDING_PROPERTIES))
.dependsOn(postgreSQLContainer);
}
}
在测试类路径中,您可以添加一个使用此配置进行测试的新 Spring Boot 应用程序:
@SpringBootApplication
public class TestProducerApplication {
public static void main(String[] args) {
SpringApplication
.from(ProducerApplication::main)
.with(DaprTestContainersConfig.class)
.run(args);
}
}
现在您可以使用以下命令启动应用程序:
mvn spring-boot:test-run
运行此命令将启动应用程序,使用提供的测试配置,其中包括 Testcontainers 和 Dapr 集成。在日志中,您应该能够看到为您的应用程序启动了 daprd 和 placement 服务容器。
除了之前的配置(DaprTestContainersConfig)之外,您的测试不应该测试 Dapr 本身,只测试应用程序暴露的 REST 端点。
利用 Spring 和 Spring Boot 编程模型与 Dapr
Java SDK 允许您与所有 Dapr 构建块 进行接口。
但是,如果您想利用 Spring 和 Spring Boot 编程模型,可以使用 dapr-spring-boot-starter 集成。
这包括 Spring Data(KeyValueTemplate 和 CrudRepository)的实现以及用于生产和消费消息的 DaprMessagingTemplate
(类似于 Spring Kafka、Spring Pulsar 和 Spring AMQP for RabbitMQ)和 Dapr 工作流。
使用 Spring Data CrudRepository 和 KeyValueTemplate
您可以使用众所周知的 Spring Data 构造,这些构造依赖于基于 Dapr 的实现。 使用 Dapr,您不需要添加任何与基础设施相关的驱动程序或客户端,使您的 Spring 应用程序更轻量,并且与其运行的环境解耦。
在底层,这些实现使用 Dapr Statestore 和 Binding API。
配置参数
使用 Spring Data 抽象,您可以配置 Dapr 将使用哪些 statestore 和绑定来连接到可用的基础设施。 这可以通过设置以下属性来完成:
dapr.statestore.name=kvstore
dapr.statestore.binding=kvbinding
然后您可以像这样 @Autowire KeyValueTemplate 或 CrudRepository:
@RestController
@EnableDaprRepositories
public class OrdersRestController {
@Autowired
private OrderRepository repository;
@PostMapping("/orders")
public void storeOrder(@RequestBody Order order){
repository.save(order);
}
@GetMapping("/orders")
public Iterable<Order> getAll(){
return repository.findAll();
}
}
其中 OrderRepository 在一个扩展 Spring Data CrudRepository 接口的接口中定义:
public interface OrderRepository extends CrudRepository<Order, String> {}
请注意,@EnableDaprRepositories 注释完成了在 CrudRespository 接口下连接 Dapr API 的所有魔术。
因为 Dapr 允许用户从同一个应用程序与不同的 StateStores 交互,作为用户,您需要提供以下 bean 作为 Spring Boot @Configuration:
@Configuration
@EnableConfigurationProperties({DaprStateStoreProperties.class})
public class ProducerAppConfiguration {
@Bean
public KeyValueAdapterResolver keyValueAdapterResolver(DaprClient daprClient, ObjectMapper mapper, DaprStateStoreProperties daprStatestoreProperties) {
String storeName = daprStatestoreProperties.getName();
String bindingName = daprStatestoreProperties.getBinding();
return new DaprKeyValueAdapterResolver(daprClient, mapper, storeName, bindingName);
}
@Bean
public DaprKeyValueTemplate daprKeyValueTemplate(KeyValueAdapterResolver keyValueAdapterResolver) {
return new DaprKeyValueTemplate(keyValueAdapterResolver);
}
}
使用 Spring Messaging 生产和消费事件
类似于 Spring Kafka、Spring Pulsar 和 Spring AMQP,您可以使用 DaprMessagingTemplate 将消息发布到配置的基础设施。要消费消息,您可以使用 @Topic 注释(很快将重命名为 @DaprListener)。
要发布事件/消息,您可以在 Spring 应用程序中 @Autowired DaprMessagingTemplate。
对于此示例,我们将发布 Order 事件,并将消息发送到名为 topic 的主题。
@Autowired
private DaprMessagingTemplate<Order> messagingTemplate;
@PostMapping("/orders")
public void storeOrder(@RequestBody Order order){
repository.save(order);
messagingTemplate.send("topic", order);
}
与 CrudRepository 类似,我们需要指定要使用哪个 PubSub 代理来发布和消费我们的消息。
dapr.pubsub.name=pubsub
因为使用 Dapr 您可以连接到多个 PubSub 代理,您需要提供以下 bean 以让 Dapr 知道您的 DaprMessagingTemplate 将使用哪个 PubSub 代理:
@Bean
public DaprMessagingTemplate<Order> messagingTemplate(DaprClient daprClient,
DaprPubSubProperties daprPubSubProperties) {
return new DaprMessagingTemplate<>(daprClient, daprPubSubProperties.getName());
}
最后,因为 Dapr PubSub 需要在您的应用程序和 Dapr 之间建立双向连接,所以您需要使用几个参数扩展您的 Testcontainers 配置:
@Bean
@ServiceConnection
public DaprContainer daprContainer(Network daprNetwork, PostgreSQLContainer<?> postgreSQLContainer, RabbitMQContainer rabbitMQContainer){
return new DaprContainer("daprio/daprd:1.16.0-rc.5")
.withAppName("producer-app")
.withNetwork(daprNetwork)
.withComponent(new Component("kvstore", "state.postgresql", "v1", STATE_STORE_PROPERTIES))
.withComponent(new Component("kvbinding", "bindings.postgresql", "v1", BINDING_PROPERTIES))
.withComponent(new Component("pubsub", "pubsub.rabbitmq", "v1", rabbitMqProperties))
.withAppPort(8080)
.withAppChannelAddress("host.testcontainers.internal")
.dependsOn(rabbitMQContainer)
.dependsOn(postgreSQLContainer);
}
现在,在 Dapr 配置中,我们包含了一个 pubsub 组件,它将连接到由 Testcontainers 启动的 RabbitMQ 实例。
我们还设置了两个重要参数 .withAppPort(8080) 和 .withAppChannelAddress("host.testcontainers.internal"),这允许 Dapr
在代理中发布消息时联系回应用程序。
要监听事件/消息,您需要在应用程序中暴露一个负责接收消息的端点。
如果您暴露 REST 端点,可以使用 @Topic 注释让 Dapr 知道它也需要将事件/消息转发到哪里:
@PostMapping("subscribe")
@Topic(pubsubName = "pubsub", name = "topic")
public void subscribe(@RequestBody CloudEvent<Order> cloudEvent){
events.add(cloudEvent);
}
在引导应用程序时,Dapr 将注册要转发到您的应用程序暴露的 subscribe 端点的消息订阅。
如果您正在为这些订阅者编写测试,您需要确保 Testcontainers 知道您的应用程序将在端口 8080 上运行, 因此使用 Testcontainers 启动的容器知道您的应用程序在哪里:
@BeforeAll
public static void setup(){
org.testcontainers.Testcontainers.exposeHostPorts(8080);
}
您可以在此处查看并运行完整的示例源代码。
后续步骤
了解有关可添加到您的 Java 应用程序的 Dapr Java SDK 包的更多信息。
查看如何指南,使用 Spring Boot 和 Testcontainers 进行 Dapr 工作流以获得本地工作流开发体验。
相关链接
2.3.5.1 - 操作指南:使用 Spring Boot 编写和管理 Dapr 工作流
遵循与 Spring Data 和 Spring Messaging 相同的方法,dapr-spring-boot-starter 为 Spring Boot 用户带来了 Dapr 工作流集成。
使用 Dapr 工作流,您可以在 Java 代码中定义复杂的编排(工作流)。Dapr Spring Boot Starter 通过将 Workflows 和 WorkflowActivitys 作为 Spring Bean 进行管理,使您的开发更加便捷。
为了启用自动 bean 发现,您需要在 @SpringBootApplication 上添加 @EnableDaprWorkflows 注解:
@SpringBootApplication
@EnableDaprWorkflows
public class MySpringBootApplication {
...
}
通过添加此注解,所有的 Workflows 和 WorkflowActivitys bean 都会被 Spring 自动发现并注册到工作流引擎中。
创建工作流和活动
在您的 Spring Boot 应用程序中,您可以定义任意数量的工作流。为此,您需要创建新的 Workflow 接口实现。
@Component
public class MyWorkflow implements Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
<工作流逻辑>
};
}
}
在工作流定义内部,您可以执行服务间交互、调度定时器或接收外部事件。
通过将所有 WorkflowActivitys 作为托管 bean,您可以使用 Spring 的 @Autowired 机制来注入工作流活动实现其功能所需的任何 bean。例如 @RestTemplate:
@Component
public class MyWorkflowActivity implements WorkflowActivity {
@Autowired
private RestTemplate restTemplate;
创建和与工作流交互
要创建和与工作流实例交互,您可以使用同样支持 @Autowired 的 DaprWorkflowClient。
@Autowired
private DaprWorkflowClient daprWorkflowClient;
应用程序现在可以调度新的工作流实例并触发事件。
String instanceId = daprWorkflowClient.scheduleNewWorkflow(MyWorkflow.class, payload);
以及
daprWorkflowClient.raiseEvent(instanceId, "MyEvenet", event);
后续步骤和资源
查看 Baeldung 关于 Dapr 工作流和 Dapr 发布订阅的博客文章,其中包含完整的工作示例。
查看 Dapr 工作流文档,了解如何使用 Dapr 工作流的更多信息。
2.4 - JavaScript SDK
一个用于在 JavaScript 和 TypeScript 中构建 Dapr 应用程序的客户端库。该客户端抽象了服务调用、状态管理、发布订阅、密钥管理等公开 Dapr API,并提供简单直观的 API 用于构建应用程序。
安装
要开始使用 JavaScript SDK,请从 NPM 安装 Dapr JavaScript SDK 软件包:
npm install --save @dapr/dapr
结构
Dapr JavaScript SDK 包含两个主要组件:
- DaprServer:管理所有 Dapr 边车与应用程序之间的通信。
- DaprClient:管理所有应用程序与 Dapr 边车之间的通信。
上述通信可配置为使用 gRPC 或 HTTP 协议。
![]() | ![]() |
快速入门
为了帮助您快速入门,请查看以下资源:
2.4.1 - JavaScript 客户端 SDK
简介
Dapr 客户端允许您与 Dapr 边车通信,并访问其面向客户端的功能,如发布事件、调用输出绑定、状态管理、密钥管理等。
前置条件
安装和导入 Dapr JS SDK
- 使用
npm安装 SDK:
npm i @dapr/dapr --save
- 导入库:
import { DaprClient, DaprServer, HttpMethod, CommunicationProtocolEnum } from "@dapr/dapr";
const daprHost = "127.0.0.1"; // Dapr 边车主机
const daprPort = "3500"; // 此示例服务器的 Dapr 边车端口
const serverHost = "127.0.0.1"; // 此示例服务器的应用主机
const serverPort = "50051"; // 此示例服务器的应用端口
// HTTP 示例
const client = new DaprClient({ daprHost, daprPort });
// GRPC 示例
const client = new DaprClient({ daprHost, daprPort, communicationProtocol: CommunicationProtocolEnum.GRPC });
运行
要运行示例,您可以使用两种不同的协议与 Dapr 边车交互:HTTP(默认)或 gRPC。
使用 HTTP(默认)
import { DaprClient } from "@dapr/dapr";
const client = new DaprClient({ daprHost, daprPort });
# 使用 dapr run
dapr run --app-id example-sdk --app-protocol http -- npm run start
# 或,使用 npm script
npm run start:dapr-http
使用 gRPC
由于 HTTP 是默认协议,您需要调整通信协议以使用 gRPC。您可以通过向客户端或服务器构造函数传递额外的参数来实现。
import { DaprClient, CommunicationProtocol } from "@dapr/dapr";
const client = new DaprClient({ daprHost, daprPort, communicationProtocol: CommunicationProtocol.GRPC });
# 使用 dapr run
dapr run --app-id example-sdk --app-protocol grpc -- npm run start
# 或,使用 npm script
npm run start:dapr-grpc
环境变量
Dapr 边车端点
您可以使用 DAPR_HTTP_ENDPOINT 和 DAPR_GRPC_ENDPOINT 环境变量来分别设置 Dapr 边车的 HTTP 和 gRPC 端点。当设置这些变量时,构造函数的选项参数中不必设置 daprHost 和 daprPort,客户端会自动从提供的端点中解析它们。
import { DaprClient, CommunicationProtocol } from "@dapr/dapr";
// 使用 HTTP,当设置了 DAPR_HTTP_ENDPOINT 时
const client = new DaprClient();
// 使用 gRPC,当设置了 DAPR_GRPC_ENDPOINT 时
const client = new DaprClient({ communicationProtocol: CommunicationProtocol.GRPC });
如果设置了环境变量,但向构造函数传递了 daprHost 和 daprPort 值,后者将优先于环境变量。
Dapr API 令牌
您可以使用 DAPR_API_TOKEN 环境变量来设置 Dapr API 令牌。当设置此变量时,构造函数的选项参数中不必设置 daprApiToken,客户端会自动获取它。
通用
增加主体大小
您可以使用 DaprClient 的选项来增加应用程序用于与边车通信的主体大小。
import { DaprClient, CommunicationProtocol } from "@dapr/dapr";
// 允许使用 10Mb 的主体大小
// 默认为 4Mb
const client = new DaprClient({
daprHost,
daprPort,
communicationProtocol: CommunicationProtocol.HTTP,
maxBodySizeMb: 10,
});
代理请求
通过代理请求,我们可以利用 Dapr 通过其边车架构带来的独特能力,如服务发现、日志记录等,使我们能够立即"升级"我们的 gRPC 服务。gRPC 代理的此功能在 社区会议 41 中进行了演示。
创建代理
要执行 gRPC 代理,只需调用 client.proxy.create() 方法创建代理:
// 像往常一样,为我们的 dapr 边车创建一个客户端
// 此客户端负责确保边车已启动、我们可以通信等
const clientSidecar = new DaprClient({ daprHost, daprPort, communicationProtocol: CommunicationProtocol.GRPC });
// 创建一个允许我们使用 gRPC 代码的代理
const clientProxy = await clientSidecar.proxy.create<GreeterClient>(GreeterClient);
现在我们可以按照 GreeterClient 接口中定义的方法调用(在本例中,该接口来自 Hello World 示例)
幕后(技术工作原理)

- gRPC 服务在 Dapr 中启动。我们通过
--app-port告诉 Dapr 此 gRPC 服务器运行在哪个端口,并使用--app-id <APP_ID_HERE>为其提供唯一的 Dapr 应用 ID - 我们现在可以通过连接到边车的客户端调用 Dapr 边车
- 在调用 Dapr 边车时,我们提供一个名为
dapr-app-id的元数据键,其值为在 Dapr 中启动的 gRPC 服务器(例如在我们的示例中为server) - Dapr 现在将调用转发到配置的 gRPC 服务器
构建块
JavaScript 客户端 SDK 允许您与所有 Dapr 构建块 交互,重点关注客户端到边车的功能。
调用 API
调用服务
import { DaprClient, HttpMethod } from "@dapr/dapr";
const daprHost = "127.0.0.1";
const daprPort = "3500";
async function start() {
const client = new DaprClient({ daprHost, daprPort });
const serviceAppId = "my-app-id";
const serviceMethod = "say-hello";
// POST 请求
const response = await client.invoker.invoke(serviceAppId, serviceMethod, HttpMethod.POST, { hello: "world" });
// 带请求头的 POST 请求
const response = await client.invoker.invoke(
serviceAppId,
serviceMethod,
HttpMethod.POST,
{ hello: "world" },
{ headers: { "X-User-ID": "123" } },
);
// GET 请求
const response = await client.invoker.invoke(serviceAppId, serviceMethod, HttpMethod.GET);
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
有关服务调用的完整指南,请访问 操作指南:调用服务。
状态管理 API
保存、获取和删除应用状态
import { DaprClient } from "@dapr/dapr";
const daprHost = "127.0.0.1";
const daprPort = "3500";
async function start() {
const client = new DaprClient({ daprHost, daprPort });
const serviceStoreName = "my-state-store-name";
// 保存状态
const response = await client.state.save(
serviceStoreName,
[
{
key: "first-key-name",
value: "hello",
metadata: {
foo: "bar",
},
},
{
key: "second-key-name",
value: "world",
},
],
{
metadata: {
ttlInSeconds: "3", // 这应该覆盖状态项中的 ttl
},
},
);
// 获取状态
const response = await client.state.get(serviceStoreName, "first-key-name");
// 批量获取状态
const response = await client.state.getBulk(serviceStoreName, ["first-key-name", "second-key-name"]);
// 状态事务
await client.state.transaction(serviceStoreName, [
{
operation: "upsert",
request: {
key: "first-key-name",
value: "new-data",
},
},
{
operation: "delete",
request: {
key: "second-key-name",
},
},
]);
// 删除状态
const response = await client.state.delete(serviceStoreName, "first-key-name");
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
有关状态操作的完整列表,请访问 操作指南:获取和保存状态。
查询状态 API
import { DaprClient } from "@dapr/dapr";
async function start() {
const client = new DaprClient({ daprHost, daprPort });
const res = await client.state.query("state-mongodb", {
filter: {
OR: [
{
EQ: { "person.org": "Dev Ops" },
},
{
AND: [
{
EQ: { "person.org": "Finance" },
},
{
IN: { state: ["CA", "WA"] },
},
],
},
],
},
sort: [
{
key: "state",
order: "DESC",
},
],
page: {
limit: 10,
},
});
console.log(res);
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
发布订阅 API
发布消息
import { DaprClient } from "@dapr/dapr";
const daprHost = "127.0.0.1";
const daprPort = "3500";
async function start() {
const client = new DaprClient({ daprHost, daprPort });
const pubSubName = "my-pubsub-name";
const topic = "topic-a";
// 以 text/plain 格式向主题发布消息
// 注意,除非明确指定,内容类型是从消息类型推断的
const response = await client.pubsub.publish(pubSubName, topic, "hello, world!");
// 如果发布失败,response 包含错误
console.log(response);
// 以 application/json 格式向主题发布消息
await client.pubsub.publish(pubSubName, topic, { hello: "world" });
// 以纯文本形式发布 JSON 消息
const options = { contentType: "text/plain" };
await client.pubsub.publish(pubSubName, topic, { hello: "world" }, options);
// 以 application/cloudevents+json 格式向主题发布消息
// 您也可以使用 cloudevent SDK 创建云事件 https://github.com/cloudevents/sdk-javascript
const cloudEvent = {
specversion: "1.0",
source: "/some/source",
type: "example",
id: "1234",
};
await client.pubsub.publish(pubSubName, topic, cloudEvent);
// 以原始负载形式发布云事件
const options = { metadata: { rawPayload: true } };
await client.pubsub.publish(pubSubName, topic, "hello, world!", options);
// 以 text/plain 格式向主题发布多条消息
await client.pubsub.publishBulk(pubSubName, topic, ["message 1", "message 2", "message 3"]);
// 以 application/json 格式向主题发布多条消息
await client.pubsub.publishBulk(pubSubName, topic, [
{ hello: "message 1" },
{ hello: "message 2" },
{ hello: "message 3" },
]);
// 使用显式批量发布消息发布多条消息
const bulkPublishMessages = [
{
entryID: "entry-1",
contentType: "application/json",
event: { hello: "foo message 1" },
},
{
entryID: "entry-2",
contentType: "application/cloudevents+json",
event: { ...cloudEvent, data: "foo message 2", datacontenttype: "text/plain" },
},
{
entryID: "entry-3",
contentType: "text/plain",
event: "foo message 3",
},
];
await client.pubsub.publishBulk(pubSubName, topic, bulkPublishMessages);
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
绑定 API
调用输出绑定
输出绑定
import { DaprClient } from "@dapr/dapr";
const daprHost = "127.0.0.1";
const daprPort = "3500";
async function start() {
const client = new DaprClient({ daprHost, daprPort });
const bindingName = "my-binding-name";
const bindingOperation = "create";
const message = { hello: "world" };
const response = await client.binding.send(bindingName, bindingOperation, message);
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
有关输出绑定的完整指南,请访问 操作指南:使用绑定。
密钥 API
检索密钥
import { DaprClient } from "@dapr/dapr";
const daprHost = "127.0.0.1";
const daprPort = "3500";
async function start() {
const client = new DaprClient({ daprHost, daprPort });
const secretStoreName = "my-secret-store";
const secretKey = "secret-key";
// 从密钥存储中检索单个密钥
const response = await client.secret.get(secretStoreName, secretKey);
// 从密钥存储中检索所有密钥
const response = await client.secret.getBulk(secretStoreName);
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
有关密钥的完整指南,请访问 操作指南:检索密钥。
配置 API
获取配置键
import { DaprClient } from "@dapr/dapr";
const daprHost = "127.0.0.1";
async function start() {
const client = new DaprClient({
daprHost,
daprPort: process.env.DAPR_GRPC_PORT,
communicationProtocol: CommunicationProtocolEnum.GRPC,
});
const config = await client.configuration.get("config-store", ["key1", "key2"]);
console.log(config);
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
示例输出:
{
items: {
key1: { key: 'key1', value: 'foo', version: '', metadata: {} },
key2: { key: 'key2', value: 'bar2', version: '', metadata: {} }
}
}
订阅配置更新
import { DaprClient } from "@dapr/dapr";
const daprHost = "127.0.0.1";
async function start() {
const client = new DaprClient({
daprHost,
daprPort: process.env.DAPR_GRPC_PORT,
communicationProtocol: CommunicationProtocolEnum.GRPC,
});
// 订阅键 "key1" 和 "key2" 的配置存储更改
const stream = await client.configuration.subscribeWithKeys("config-store", ["key1", "key2"], async (data) => {
console.log("订阅收到来自配置存储的更新:", data);
});
// 等待 60 秒并取消订阅。
await new Promise((resolve) => setTimeout(resolve, 60000));
stream.stop();
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
示例输出:
订阅收到来自配置存储的更新: {
items: { key2: { key: 'key2', value: 'bar', version: '', metadata: {} } }
}
订阅收到来自配置存储的更新: {
items: { key1: { key: 'key1', value: 'foobar', version: '', metadata: {} } }
}
加密 API
JavaScript SDK 中仅在 gRPC 客户端上支持加密 API。
import { createReadStream, createWriteStream } from "node:fs";
import { readFile, writeFile } from "node:fs/promises";
import { pipeline } from "node:stream/promises";
import { DaprClient, CommunicationProtocolEnum } from "@dapr/dapr";
const daprHost = "127.0.0.1";
const daprPort = "50050"; // 此示例服务器的 Dapr 边车端口
async function start() {
const client = new DaprClient({
daprHost,
daprPort,
communicationProtocol: CommunicationProtocolEnum.GRPC,
});
// 使用流加密和解密消息
await encryptDecryptStream(client);
// 使用缓冲区加密和解密消息
await encryptDecryptBuffer(client);
}
async function encryptDecryptStream(client: DaprClient) {
// 首先,加密消息
console.log("== 使用流加密消息");
console.log("将 plaintext.txt 加密为 ciphertext.out");
await pipeline(
createReadStream("plaintext.txt"),
await client.crypto.encrypt({
componentName: "crypto-local",
keyName: "symmetric256",
keyWrapAlgorithm: "A256KW",
}),
createWriteStream("ciphertext.out"),
);
// 解密消息
console.log("== 使用流解密消息");
console.log("将 ciphertext.out 解密为 plaintext.out");
await pipeline(
createReadStream("ciphertext.out"),
await client.crypto.decrypt({
componentName: "crypto-local",
}),
createWriteStream("plaintext.out"),
);
}
async function encryptDecryptBuffer(client: DaprClient) {
// 读取 "plaintext.txt" 以便我们有一些内容
const plaintext = await readFile("plaintext.txt");
// 首先,加密消息
console.log("== 使用缓冲区加密消息");
const ciphertext = await client.crypto.encrypt(plaintext, {
componentName: "crypto-local",
keyName: "my-rsa-key",
keyWrapAlgorithm: "RSA",
});
await writeFile("test.out", ciphertext);
// 解密消息
console.log("== 使用缓冲区解密消息");
const decrypted = await client.crypto.decrypt(ciphertext, {
componentName: "crypto-local",
});
// 内容应该相等
if (plaintext.compare(decrypted) !== 0) {
throw new Error("解密的消息与原始消息不匹配");
}
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
有关加密的完整指南,请访问 操作指南:加密。
分布式锁 API
尝试锁定和解锁 API
import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";
import { LockStatus } from "@dapr/dapr/types/lock/UnlockResponse";
const daprHost = "127.0.0.1";
const daprPortDefault = "3500";
async function start() {
const client = new DaprClient({ daprHost, daprPort });
const storeName = "redislock";
const resourceId = "resourceId";
const lockOwner = "owner1";
let expiryInSeconds = 1000;
console.log(`在 ${storeName}、${resourceId} 上获取锁,所有者:${lockOwner}`);
const lockResponse = await client.lock.lock(storeName, resourceId, lockOwner, expiryInSeconds);
console.log(lockResponse);
console.log(`在 ${storeName}、${resourceId} 上解锁,所有者:${lockOwner}`);
const unlockResponse = await client.lock.unlock(storeName, resourceId, lockOwner);
console.log("解锁 API 响应: " + getResponseStatus(unlockResponse.status));
}
function getResponseStatus(status: LockStatus) {
switch (status) {
case LockStatus.Success:
return "Success";
case LockStatus.LockDoesNotExist:
return "LockDoesNotExist";
case LockStatus.LockBelongsToOthers:
return "LockBelongsToOthers";
default:
return "InternalError";
}
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
有关分布式锁的完整指南,请访问 操作指南:使用分布式锁。
工作流 API
工作流管理
import { DaprClient } from "@dapr/dapr";
async function start() {
const client = new DaprClient();
// 启动新的工作流实例
const instanceId = await client.workflow.start("OrderProcessingWorkflow", {
Name: "Paperclips",
TotalCost: 99.95,
Quantity: 4,
});
console.log(`已启动工作流实例 ${instanceId}`);
// 获取工作流实例
const workflow = await client.workflow.get(instanceId);
console.log(
`工作流 ${workflow.workflowName},创建于 ${workflow.createdAt.toUTCString()},状态为 ${
workflow.runtimeStatus
}`,
);
console.log(`其他属性:${JSON.stringify(workflow.properties)}`);
// 暂停工作流实例
await client.workflow.pause(instanceId);
console.log(`已暂停工作流实例 ${instanceId}`);
// 恢复工作流实例
await client.workflow.resume(instanceId);
console.log(`已恢复工作流实例 ${instanceId}`);
// 终止工作流实例
await client.workflow.terminate(instanceId);
console.log(`已终止工作流实例 ${instanceId}`);
// 清除工作流实例
await client.workflow.purge(instanceId);
console.log(`已清除工作流实例 ${instanceId}`);
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
相关链接
2.4.2 - JavaScript Server SDK
简介
Dapr Server 将允许您接收来自 Dapr 边车的通信,并访问其面向服务器的功能,例如:订阅事件、接收输入绑定等。
前置条件
- 已安装 Dapr CLI
- 已初始化 Dapr 环境
- 最新 LTS 版本的 Node 或更高版本
安装和导入 Dapr 的 JS SDK
- 使用
npm安装 SDK:
npm i @dapr/dapr --save
- 导入库:
import { DaprServer, CommunicationProtocolEnum } from "@dapr/dapr";
const daprHost = "127.0.0.1"; // Dapr Sidecar Host
const daprPort = "3500"; // Dapr Sidecar Port of this Example Server
const serverHost = "127.0.0.1"; // App Host of this Example Server
const serverPort = "50051"; // App Port of this Example Server
// HTTP Example
const server = new DaprServer({
serverHost,
serverPort,
communicationProtocol: CommunicationProtocolEnum.HTTP, // DaprClient to use same communication protocol as DaprServer, in case DaprClient protocol not mentioned explicitly
clientOptions: {
daprHost,
daprPort,
},
});
// GRPC Example
const server = new DaprServer({
serverHost,
serverPort,
communicationProtocol: CommunicationProtocolEnum.GRPC,
clientOptions: {
daprHost,
daprPort,
},
});
运行
要运行示例,您可以使用两种不同的协议与 Dapr sidecar 交互:HTTP(默认)或 gRPC。
使用 HTTP(内置 express webserver)
import { DaprServer } from "@dapr/dapr";
const server = new DaprServer({
serverHost: appHost,
serverPort: appPort,
clientOptions: {
daprHost,
daprPort,
},
});
// initialize subscribtions, ... before server start
// the dapr sidecar relies on these
await server.start();
# Using dapr run
dapr run --app-id example-sdk --app-port 50051 --app-protocol http -- npm run start
# or, using npm script
npm run start:dapr-http
ℹ️ Note: The
app-portis required here, as this is where our server will need to bind to. Dapr will check for the application to bind to this port, before finishing start-up.
使用 HTTP(自备 express webserver)
您不一定要使用 Dapr sidecar 的内置 web 服务器与应用程序通信,您也可以自带实例。这在构建 REST API 后端并希望直接集成 Dapr 的场景中非常有用。
注意,目前仅适用于 express。
💡 Note: when using a custom web-server, the SDK will configure server properties like max body size, and add new routes to it. The routes are unique on their own to avoid any collisions with your application, but it’s not guaranteed to not collide.
import { DaprServer, CommunicationProtocolEnum } from "@dapr/dapr";
import express from "express";
const myApp = express();
myApp.get("/my-custom-endpoint", (req, res) => {
res.send({ msg: "My own express app!" });
});
const daprServer = new DaprServer({
serverHost: "127.0.0.1", // App Host
serverPort: "50002", // App Port
serverHttp: myApp,
clientOptions: {
daprHost
daprPort
}
});
// Initialize subscriptions before the server starts, the Dapr sidecar uses it.
// This will also initialize the app server itself (removing the need for `app.listen` to be called).
await daprServer.start();
配置好上述内容后,您可以像往常一样调用自定义端点:
const res = await fetch(`http://127.0.0.1:50002/my-custom-endpoint`);
const json = await res.json();
使用 gRPC
由于 HTTP 是默认协议,您需要调整通信协议以使用 gRPC。您可以通过向客户端或服务器构造函数传递额外参数来实现这一点。
import { DaprServer, CommunicationProtocol } from "@dapr/dapr";
const server = new DaprServer({
serverHost: appHost,
serverPort: appPort,
communicationProtocol: CommunicationProtocolEnum.GRPC,
clientOptions: {
daprHost,
daprPort,
},
});
// initialize subscribtions, ... before server start
// the dapr sidecar relies on these
await server.start();
# Using dapr run
dapr run --app-id example-sdk --app-port 50051 --app-protocol grpc -- npm run start
# or, using npm script
npm run start:dapr-grpc
ℹ️ Note: The
app-portis required here, as this is where our server will need to bind to. Dapr will check for the application to bind to this port, before finishing start-up.
构建块
JavaScript Server SDK 允许您与所有 Dapr 构建块 进行交互,主要专注于 Sidecar 到应用程序的功能。
Invocation API
Listen to an Invocation
import { DaprServer, DaprInvokerCallbackContent } from "@dapr/dapr";
const daprHost = "127.0.0.1"; // Dapr Sidecar Host
const daprPort = "3500"; // Dapr Sidecar Port of this Example Server
const serverHost = "127.0.0.1"; // App Host of this Example Server
const serverPort = "50051"; // App Port of this Example Server "
async function start() {
const server = new DaprServer({
serverHost,
serverPort,
clientOptions: {
daprHost,
daprPort,
},
});
const callbackFunction = (data: DaprInvokerCallbackContent) => {
console.log("Received body: ", data.body);
console.log("Received metadata: ", data.metadata);
console.log("Received query: ", data.query);
console.log("Received headers: ", data.headers); // only available in HTTP
};
await server.invoker.listen("hello-world", callbackFunction, { method: HttpMethod.GET });
// You can now invoke the service with your app id and method "hello-world"
await server.start();
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
For a full guide on service invocation visit How-To: Invoke a service.
PubSub API
Subscribe to messages
订阅消息可以通过多种方式完成,以提供在主题上接收消息的灵活性:
- 通过
subscribe方法直接订阅 - 通过
subscribeWithOptions方法带选项直接订阅 - 之后通过
susbcribeOnEvent方法订阅
每次事件到达时,我们将其正文作为 data 传递,将标头作为 headers 传递,这些标头可能包含事件发布者的属性(例如,来自 IoT Hub 的设备 ID)
Dapr 要求在启动时设置订阅,但在 JS SDK 中,我们也允许之后添加事件处理程序,为您提供编程的灵活性。
下面提供了一个示例
import { DaprServer } from "@dapr/dapr";
const daprHost = "127.0.0.1"; // Dapr Sidecar Host
const daprPort = "3500"; // Dapr Sidecar Port of this Example Server
const serverHost = "127.0.0.1"; // App Host of this Example Server
const serverPort = "50051"; // App Port of this Example Server "
async function start() {
const server = new DaprServer({
serverHost,
serverPort,
clientOptions: {
daprHost,
daprPort,
},
});
const pubSubName = "my-pubsub-name";
const topic = "topic-a";
// Configure Subscriber for a Topic
// Method 1: Direct subscription through the `subscribe` method
await server.pubsub.subscribe(pubSubName, topic, async (data: any, headers: object) =>
console.log(`Received Data: ${JSON.stringify(data)} with headers: ${JSON.stringify(headers)}`),
);
// Method 2: Direct susbcription with options through the `subscribeWithOptions` method
await server.pubsub.subscribeWithOptions(pubSubName, topic, {
callback: async (data: any, headers: object) =>
console.log(`Received Data: ${JSON.stringify(data)} with headers: ${JSON.stringify(headers)}`),
});
// Method 3: Subscription afterwards through the `susbcribeOnEvent` method
// Note: we use default, since if no route was passed (empty options) we utilize "default" as the route name
await server.pubsub.subscribeWithOptions("pubsub-redis", "topic-options-1", {});
server.pubsub.subscribeToRoute("pubsub-redis", "topic-options-1", "default", async (data: any, headers: object) => {
console.log(`Received Data: ${JSON.stringify(data)} with headers: ${JSON.stringify(headers)}`);
});
// Start the server
await server.start();
}
For a full list of state operations visit How-To: Publish & subscribe.
Subscribe with SUCCESS/RETRY/DROP status
Dapr 支持重试逻辑的状态码来指定消息处理后应该发生什么。
⚠️ JS SDK 允许在同一主题上设置多个回调,我们按
RETRY>DROP>SUCCESS的顺序处理状态优先级,并默认为SUCCESS
⚠️ 确保在应用程序中配置弹性以处理
RETRY消息
在 JS SDK 中,我们通过 DaprPubSubStatusEnum 枚举支持这些消息。为了确保 Dapr 将重试,我们还配置了弹性策略。
components/resiliency.yaml
apiVersion: dapr.io/v1alpha1
kind: Resiliency
metadata:
name: myresiliency
spec:
policies:
retries:
# Global Retry Policy for Inbound Component operations
DefaultComponentInboundRetryPolicy:
policy: constant
duration: 500ms
maxRetries: 10
targets:
components:
messagebus:
inbound:
retry: DefaultComponentInboundRetryPolicy
src/index.ts
import { DaprServer, DaprPubSubStatusEnum } from "@dapr/dapr";
const daprHost = "127.0.0.1"; // Dapr Sidecar Host
const daprPort = "3500"; // Dapr Sidecar Port of this Example Server
const serverHost = "127.0.0.1"; // App Host of this Example Server
const serverPort = "50051"; // App Port of this Example Server "
async function start() {
const server = new DaprServer({
serverHost,
serverPort,
clientOptions: {
daprHost,
daprPort,
},
});
const pubSubName = "my-pubsub-name";
const topic = "topic-a";
// Process a message successfully
await server.pubsub.subscribe(pubSubName, topic, async (data: any, headers: object) => {
return DaprPubSubStatusEnum.SUCCESS;
});
// Retry a message
// Note: this example will keep on retrying to deliver the message
// Note 2: each component can have their own retry configuration
// e.g., https://docs.dapr.io/reference/components-reference/supported-pubsub/setup-redis-pubsub/
await server.pubsub.subscribe(pubSubName, topic, async (data: any, headers: object) => {
return DaprPubSubStatusEnum.RETRY;
});
// Drop a message
await server.pubsub.subscribe(pubSubName, topic, async (data: any, headers: object) => {
return DaprPubSubStatusEnum.DROP;
});
// Start the server
await server.start();
}
Subscribe to messages rule based
Dapr 支持根据规则将消息路由到不同的处理程序(路由)。
例如,您正在编写一个需要根据消息的"type"处理消息的应用程序,使用 Dapr,您可以将它们发送到不同的路由
handlerType1和handlerType2,默认路由为handlerDefault
import { DaprServer } from "@dapr/dapr";
const daprHost = "127.0.0.1"; // Dapr Sidecar Host
const daprPort = "3500"; // Dapr Sidecar Port of this Example Server
const serverHost = "127.0.0.1"; // App Host of this Example Server
const serverPort = "50051"; // App Port of this Example Server "
async function start() {
const server = new DaprServer({
serverHost,
serverPort,
clientOptions: {
daprHost,
daprPort,
},
});
const pubSubName = "my-pubsub-name";
const topic = "topic-a";
// Configure Subscriber for a Topic with rule set
// Note: the default route and match patterns are optional
await server.pubsub.subscribe("pubsub-redis", "topic-1", {
default: "/default",
rules: [
{
match: `event.type == "my-type-1"`,
path: "/type-1",
},
{
match: `event.type == "my-type-2"`,
path: "/type-2",
},
],
});
// Add handlers for each route
server.pubsub.subscribeToRoute("pubsub-redis", "topic-1", "default", async (data) => {
console.log(`Handling Default`);
});
server.pubsub.subscribeToRoute("pubsub-redis", "topic-1", "type-1", async (data) => {
console.log(`Handling Type 1`);
});
server.pubsub.subscribeToRoute("pubsub-redis", "topic-1", "type-2", async (data) => {
console.log(`Handling Type 2`);
});
// Start the server
await server.start();
}
Susbcribe with Wildcards
支持通配符 * 和 +(请确保验证 pubsub 组件是否支持它),可以按如下方式订阅:
import { DaprServer } from "@dapr/dapr";
const daprHost = "127.0.0.1"; // Dapr Sidecar Host
const daprPort = "3500"; // Dapr Sidecar Port of this Example Server
const serverHost = "127.0.0.1"; // App Host of this Example Server
const serverPort = "50051"; // App Port of this Example Server "
async function start() {
const server = new DaprServer({
serverHost,
serverPort,
clientOptions: {
daprHost,
daprPort,
},
});
const pubSubName = "my-pubsub-name";
// * Wildcard
await server.pubsub.subscribe(pubSubName, "/events/*", async (data: any, headers: object) =>
console.log(`Received Data: ${JSON.stringify(data)}`),
);
// + Wildcard
await server.pubsub.subscribe(pubSubName, "/events/+/temperature", async (data: any, headers: object) =>
console.log(`Received Data: ${JSON.stringify(data)}`),
);
// Start the server
await server.start();
}
Bulk Subscribe to messages
批量订阅受支持,可通过以下 API 使用:
- 通过
subscribeBulk方法批量订阅:maxMessagesCount和maxAwaitDurationMs是可选的;如果未提供,将使用相关组件的默认值。
在监听消息时,应用程序从 Dapr 批量接收消息。然而,与常规订阅一样,回调函数一次接收一条消息,用户可以选择返回 DaprPubSubStatusEnum 值来确认成功、重试或丢弃消息。默认行为是返回成功响应。
有关更多详细信息,请参阅本文档。
import { DaprServer } from "@dapr/dapr";
const pubSubName = "orderPubSub";
const topic = "topicbulk";
const daprHost = process.env.DAPR_HOST || "127.0.0.1";
const daprHttpPort = process.env.DAPR_HTTP_PORT || "3502";
const serverHost = process.env.SERVER_HOST || "127.0.0.1";
const serverPort = process.env.APP_PORT || 5001;
async function start() {
const server = new DaprServer({
serverHost,
serverPort,
clientOptions: {
daprHost,
daprPort: daprHttpPort,
},
});
// Publish multiple messages to a topic with default config.
await client.pubsub.subscribeBulk(pubSubName, topic, (data) =>
console.log("Subscriber received: " + JSON.stringify(data)),
);
// Publish multiple messages to a topic with specific maxMessagesCount and maxAwaitDurationMs.
await client.pubsub.subscribeBulk(
pubSubName,
topic,
(data) => {
console.log("Subscriber received: " + JSON.stringify(data));
return DaprPubSubStatusEnum.SUCCESS; // If App doesn't return anything, the default is SUCCESS. App can also return RETRY or DROP based on the incoming message.
},
{
maxMessagesCount: 100,
maxAwaitDurationMs: 40,
},
);
}
Dead Letter Topics
Dapr 支持死信主题。这意味着当消息处理失败时,它将被发送到死信队列。例如,当消息在 /my-queue 上处理失败时,它将被发送到 /my-queue-failed。
例如,当消息在 /my-queue 上处理失败时,它将被发送到 /my-queue-failed。
您可以将以下选项与 subscribeWithOptions 方法一起使用:
deadletterTopic:指定死信主题名称(注意:如果未提供,我们将创建一个名为deadletter的主题)deadletterCallback:作为死信处理程序触发的方法
在 JS SDK 中实现死信支持可以通过以下方式完成:
- 将
deadletterCallback作为选项传递 - 通过
subscribeToRoute手动订阅路由
下面提供了一个示例
import { DaprServer } from "@dapr/dapr";
const daprHost = "127.0.0.1"; // Dapr Sidecar Host
const daprPort = "3500"; // Dapr Sidecar Port of this Example Server
const serverHost = "127.0.0.1"; // App Host of this Example Server
const serverPort = "50051"; // App Port of this Example Server "
async function start() {
const server = new DaprServer({
serverHost,
serverPort,
clientOptions: {
daprHost,
daprPort,
},
});
const pubSubName = "my-pubsub-name";
// Method 1 (direct subscribing through subscribeWithOptions)
await server.pubsub.subscribeWithOptions("pubsub-redis", "topic-options-5", {
callback: async (data: any) => {
throw new Error("Triggering Deadletter");
},
deadLetterCallback: async (data: any) => {
console.log("Handling Deadletter message");
},
});
// Method 2 (subscribe afterwards)
await server.pubsub.subscribeWithOptions("pubsub-redis", "topic-options-1", {
deadletterTopic: "my-deadletter-topic",
});
server.pubsub.subscribeToRoute("pubsub-redis", "topic-options-1", "default", async () => {
throw new Error("Triggering Deadletter");
});
server.pubsub.subscribeToRoute("pubsub-redis", "topic-options-1", "my-deadletter-topic", async () => {
console.log("Handling Deadletter message");
});
// Start server
await server.start();
}
Bindings API
Receive an Input Binding
import { DaprServer } from "@dapr/dapr";
const daprHost = "127.0.0.1";
const daprPort = "3500";
const serverHost = "127.0.0.1";
const serverPort = "5051";
async function start() {
const server = new DaprServer({
serverHost,
serverPort,
clientOptions: {
daprHost,
daprPort,
},
});
const bindingName = "my-binding-name";
const response = await server.binding.receive(bindingName, async (data: any) =>
console.log(`Got Data: ${JSON.stringify(data)}`),
);
await server.start();
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
For a full guide on output bindings visit How-To: Use bindings.
Configuration API
💡 The configuration API is currently only available through gRPC
Getting a configuration value
import { DaprServer } from "@dapr/dapr";
const daprHost = "127.0.0.1";
const daprPort = "3500";
const serverHost = "127.0.0.1";
const serverPort = "5051";
async function start() {
const client = new DaprClient({
daprHost,
daprPort,
communicationProtocol: CommunicationProtocolEnum.GRPC,
});
const config = await client.configuration.get("config-redis", ["myconfigkey1", "myconfigkey2"]);
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
Subscribing to Key Changes
import { DaprServer } from "@dapr/dapr";
const daprHost = "127.0.0.1";
const daprPort = "3500";
const serverHost = "127.0.0.1";
const serverPort = "5051";
async function start() {
const client = new DaprClient({
daprHost,
daprPort,
communicationProtocol: CommunicationProtocolEnum.GRPC,
});
const stream = await client.configuration.subscribeWithKeys("config-redis", ["myconfigkey1", "myconfigkey2"], () => {
// Received a key update
});
// When you are ready to stop listening, call the following
await stream.close();
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
相关链接
2.4.3 - JavaScript SDK for Actors
Dapr Actor 包允许你从 JavaScript 应用程序与 Dapr 虚拟 Actor 交互。下面的示例演示了如何使用 JavaScript SDK 与虚拟 Actor 进行交互。
有关 Dapr Actor 的更深入概述,请访问 Actor 概述页面。
前置条件
- 已安装 Dapr CLI
- 已初始化 Dapr 环境
- 最新的 Node LTS 版本或更高版本
- 已安装 JavaScript NPM 包
场景
下面的代码示例大致描述了停车场车位监控系统的场景,你可以在 Mark Russinovich 的这个视频中看到。
一个停车场包含数百个停车位,每个停车位都有一个传感器,向中央监控系统提供更新。停车位传感器(我们的 Actor)检测停车位是被占用还是可用。
要立即亲自运行此示例,请克隆源代码,可以在 JavaScript SDK 示例目录中找到。
Actor 接口
Actor 接口定义了 Actor 实现和调用 Actor 的客户端之间共享的合约。在下面的示例中,我们为停车场传感器创建了一个接口。每个传感器有 2 个方法:carEnter 和 carLeave,它们定义了停车位的状态:
export default interface ParkingSensorInterface {
carEnter(): Promise<void>;
carLeave(): Promise<void>;
}
Actor 实现
Actor 实现通过扩展基类型 AbstractActor 并实现 Actor 接口(在此例中为 ParkingSensorInterface)来定义一个类。
以下代码描述了一个 Actor 实现以及一些辅助方法。
import { AbstractActor } from "@dapr/dapr";
import ParkingSensorInterface from "./ParkingSensorInterface";
export default class ParkingSensorImpl extends AbstractActor implements ParkingSensorInterface {
async carEnter(): Promise<void> {
// 实现更新该停车位被占用的状态。
}
async carLeave(): Promise<void> {
// 实现更新该停车位可用的状态。
}
private async getInfo(): Promise<object> {
// 实现从停车位传感器请求更新。
}
/**
* @override
*/
async onActivate(): Promise<void> {
// 由 AbstractActor 调用的初始化逻辑。
}
}
配置 Actor 运行时
要配置 Actor 运行时,请使用 DaprClientOptions。各种参数及其默认值记录在 操作指南:在 Dapr 中使用虚拟 Actor 中。
注意,超时和间隔应格式化为 time.ParseDuration 字符串。
import { CommunicationProtocolEnum, DaprClient, DaprServer } from "@dapr/dapr";
// 使用 DaprClientOptions 配置 Actor 运行时。
const clientOptions = {
daprHost: daprHost,
daprPort: daprPort,
communicationProtocol: CommunicationProtocolEnum.HTTP,
actor: {
actorIdleTimeout: "1h",
actorScanInterval: "30s",
drainOngoingCallTimeout: "1m",
drainRebalancedActors: true,
reentrancy: {
enabled: true,
maxStackDepth: 32,
},
remindersStoragePartitions: 0,
},
};
// 在创建 DaprServer 和 DaprClient 时使用这些选项。
// 注意,DaprServer 在内部创建一个 DaprClient,需要使用 clientOptions 进行配置。
const server = new DaprServer({ serverHost, serverPort, clientOptions });
const client = new DaprClient(clientOptions);
注册 Actor
使用 DaprServer 包初始化并注册你的 Actor:
import { DaprServer } from "@dapr/dapr";
import ParkingSensorImpl from "./ParkingSensorImpl";
const daprHost = "127.0.0.1";
const daprPort = "50000";
const serverHost = "127.0.0.1";
const serverPort = "50001";
const server = new DaprServer({
serverHost,
serverPort,
clientOptions: {
daprHost,
daprPort,
},
});
await server.actor.init(); // 让服务器知道我们需要 Actor
server.actor.registerActor(ParkingSensorImpl); // 注册 Actor
await server.start(); // 启动服务器
// 要获取已注册的 Actor,你可以调用 `getRegisteredActors`:
const resRegisteredActors = await server.actor.getRegisteredActors();
console.log(`Registered Actors: ${JSON.stringify(resRegisteredActors)}`);
调用 Actor 方法
注册 Actor 后,使用 ActorProxyBuilder 创建一个实现 ParkingSensorInterface 的 Proxy 对象。你可以通过直接调用 Proxy 对象上的方法来调用 Actor 方法。在内部,它会转换为对 Actor API 的网络调用并获取结果。
import { ActorId, DaprClient } from "@dapr/dapr";
import ParkingSensorImpl from "./ParkingSensorImpl";
import ParkingSensorInterface from "./ParkingSensorInterface";
const daprHost = "127.0.0.1";
const daprPort = "50000";
const client = new DaprClient({ daprHost, daprPort });
// 创建一个新的 Actor 构建器。它可用于创建多个同类型的 Actor。
const builder = new ActorProxyBuilder<ParkingSensorInterface>(ParkingSensorImpl, client);
// 创建一个新的 Actor 实例。
const actor = builder.build(new ActorId("my-actor"));
// 或者,使用随机 ID
// const actor = builder.build(ActorId.createRandomId());
// 调用方法。
await actor.carEnter();
在 Actor 中使用状态
import { AbstractActor } from "@dapr/dapr";
import ActorStateInterface from "./ActorStateInterface";
export default class ActorStateExample extends AbstractActor implements ActorStateInterface {
async setState(key: string, value: any): Promise<void> {
await this.getStateManager().setState(key, value);
await this.getStateManager().saveState();
}
async removeState(key: string): Promise<void> {
await this.getStateManager().removeState(key);
await this.getStateManager().saveState();
}
// 使用特定类型的 getState
async getState<T>(key: string): Promise<T | null> {
return await this.getStateManager<T>().getState(key);
}
// 不使用类型作为 `any` 的 getState
async getState(key: string): Promise<any> {
return await this.getStateManager().getState(key);
}
}
Actor 定时器和提醒
JS SDK 支持通过注册定时器或提醒来安排对自己的周期性工作的 Actor。定时器和提醒之间的主要区别在于,Dapr Actor 运行时在停用后不保留有关定时器的任何信息,但使用 Dapr Actor 状态提供程序持久化提醒信息。
这种区别允许用户在轻量级但无状态的定时器和资源密集但有状态的提醒之间进行权衡。
定时器和提醒的调度接口是相同的。有关调度配置的更深入信息,请参阅 Actor 定时器和提醒文档。
Actor 定时器
// ...
const actor = builder.build(new ActorId("my-actor"));
// 注册定时器
await actor.registerActorTimer(
"timer-id", // 定时器的唯一名称。
"cb-method", // 定时器触发时要执行的回调方法。
Temporal.Duration.from({ seconds: 2 }), // DueTime
Temporal.Duration.from({ seconds: 1 }), // Period
Temporal.Duration.from({ seconds: 1 }), // TTL
50, // 要发送到定时器回调的状态。
);
// 删除定时器
await actor.unregisterActorTimer("timer-id");
Actor 提醒
// ...
const actor = builder.build(new ActorId("my-actor"));
// 注册提醒,它有一个默认回调:`receiveReminder`
await actor.registerActorReminder(
"reminder-id", // 提醒的唯一名称。
Temporal.Duration.from({ seconds: 2 }), // DueTime
Temporal.Duration.from({ seconds: 1 }), // Period
Temporal.Duration.from({ seconds: 1 }), // TTL
100, // 要发送到提醒回调的状态。
);
// 删除提醒
await actor.unregisterActorReminder("reminder-id");
要处理回调,你需要在你的 Actor 中覆盖默认的 receiveReminder 实现。例如,从我们最初的 Actor 实现:
export default class ParkingSensorImpl extends AbstractActor implements ParkingSensorInterface {
// ...
/**
* @override
*/
async receiveReminder(state: any): Promise<void> {
// 在这里处理逻辑
}
// ...
}
有关 Actor 的完整指南,请访问 操作指南:在 Dapr 中使用虚拟 Actor。
2.4.4 - JavaScript SDK 中的日志记录
简介
JavaScript SDK 内置了基于 Console 的日志记录器。SDK 会发出各种内部日志,以帮助用户了解事件链并排查问题。SDK 的使用者可以自定义日志的详细程度,也可以提供自己的日志记录器实现。
配置日志级别
日志共有五个级别,按重要性降序排列 - error、warn、info、verbose 和 debug。将日志设置到某个级别意味着日志记录器将发出所有重要性不低于该级别的日志。例如,设置为 verbose 级别意味着 SDK 将不会发出 debug 级别的日志。默认日志级别为 info。
Dapr Client
import { CommunicationProtocolEnum, DaprClient, LogLevel } from "@dapr/dapr";
// create a client instance with log level set to verbose.
const client = new DaprClient({
daprHost,
daprPort,
communicationProtocol: CommunicationProtocolEnum.HTTP,
logger: { level: LogLevel.Verbose },
});
有关如何使用 Client 的更多详细信息,请参阅 JavaScript Client。
DaprServer
import { CommunicationProtocolEnum, DaprServer, LogLevel } from "@dapr/dapr";
// create a server instance with log level set to error.
const server = new DaprServer({
serverHost,
serverPort,
clientOptions: {
daprHost,
daprPort,
logger: { level: LogLevel.Error },
},
});
有关如何使用 Server 的更多详细信息,请参阅 JavaScript Server。
自定义 LoggerService
JavaScript SDK 使用内置的 Console 进行日志记录。要使用 Winston 或 Pino 等自定义日志记录器,可以实现 LoggerService 接口。
基于 Winston 的日志记录:
创建 LoggerService 的新实现。
import { LoggerService } from "@dapr/dapr";
import * as winston from "winston";
export class WinstonLoggerService implements LoggerService {
private logger;
constructor() {
this.logger = winston.createLogger({
transports: [new winston.transports.Console(), new winston.transports.File({ filename: "combined.log" })],
});
}
error(message: any, ...optionalParams: any[]): void {
this.logger.error(message, ...optionalParams);
}
warn(message: any, ...optionalParams: any[]): void {
this.logger.warn(message, ...optionalParams);
}
info(message: any, ...optionalParams: any[]): void {
this.logger.info(message, ...optionalParams);
}
verbose(message: any, ...optionalParams: any[]): void {
this.logger.verbose(message, ...optionalParams);
}
debug(message: any, ...optionalParams: any[]): void {
this.logger.debug(message, ...optionalParams);
}
}
将新实现传递给 SDK。
import { CommunicationProtocolEnum, DaprClient, LogLevel } from "@dapr/dapr";
import { WinstonLoggerService } from "./WinstonLoggerService";
const winstonLoggerService = new WinstonLoggerService();
// create a client instance with log level set to verbose and logger service as winston.
const client = new DaprClient({
daprHost,
daprPort,
communicationProtocol: CommunicationProtocolEnum.HTTP,
logger: { level: LogLevel.Verbose, service: winstonLoggerService },
});
2.4.6 - 如何:在 JavaScript SDK 中编写和管理 Dapr 工作流
让我们创建一个 Dapr 工作流并通过控制台调用它。借助提供的工作流示例,你将:
- 使用 JavaScript 工作流 worker 执行工作流实例
- 利用 JavaScript 工作流客户端与 API 调用来启动和终止工作流实例
本示例使用了在自托管模式下通过 dapr init 获得的默认配置。
前提条件
- Dapr CLI 和已初始化的环境。
- Node.js 和 npm。
- Docker Desktop
- 验证你正在使用最新的 proto 绑定
设置环境
克隆 JavaScript SDK 仓库并进入该目录。
git clone https://github.com/dapr/js-sdk
cd js-sdk
从 JavaScript SDK 根目录进入 Dapr Workflow 示例。
cd examples/workflow/authoring
运行以下命令以安装使用 Dapr JavaScript SDK 运行此工作流示例所需的所有依赖。
npm install
运行 activity-sequence.ts
activity-sequence 文件向 Dapr Workflow 运行时注册了一个工作流和一个 activity。该工作流是按顺序执行的多个 activity 组成的序列。我们使用 DaprWorkflowClient 调度新的工作流实例并等待其完成。
const daprHost = "localhost";
const daprPort = "50001";
const workflowClient = new DaprWorkflowClient({
daprHost,
daprPort,
});
const workflowRuntime = new WorkflowRuntime({
daprHost,
daprPort,
});
const hello = async (_: WorkflowActivityContext, name: string) => {
return `Hello ${name}!`;
};
const sequence: TWorkflow = async function* (ctx: WorkflowContext): any {
const cities: string[] = [];
const result1 = yield ctx.callActivity(hello, "Tokyo");
cities.push(result1);
const result2 = yield ctx.callActivity(hello, "Seattle");
cities.push(result2);
const result3 = yield ctx.callActivity(hello, "London");
cities.push(result3);
return cities;
};
workflowRuntime.registerWorkflow(sequence).registerActivity(hello);
// 将 worker 启动包装在 try-catch 块中以处理启动期间的任何错误
try {
await workflowRuntime.start();
console.log("Workflow runtime started successfully");
} catch (error) {
console.error("Error starting workflow runtime:", error);
}
// 调度新的编排
try {
const id = await workflowClient.scheduleNewWorkflow(sequence);
console.log(`Orchestration scheduled with ID: ${id}`);
// 等待编排完成
const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30);
console.log(`Orchestration completed! Result: ${state?.serializedOutput}`);
} catch (error) {
console.error("Error scheduling or waiting for orchestration:", error);
}
在以上代码中:
workflowRuntime.registerWorkflow(sequence)将sequence注册为 Dapr Workflow 运行时中的一个工作流。await workflowRuntime.start();在 Dapr Workflow 运行时中构建并启动引擎。await workflowClient.scheduleNewWorkflow(sequence)向 Dapr Workflow 运行时调度一个新的工作流实例。await workflowClient.waitForWorkflowCompletion(id, undefined, 30)等待工作流实例完成。
在终端中执行以下命令以启动 activity-sequence.ts:
npm run start:dapr:activity-sequence
预期输出
You're up and running! Both Dapr and your app logs will appear here.
...
== APP == Orchestration scheduled with ID: dc040bea-6436-4051-9166-c9294f9d2201
== APP == Waiting 30 seconds for instance dc040bea-6436-4051-9166-c9294f9d2201 to complete...
== APP == Received "Orchestrator Request" work item with instance id 'dc040bea-6436-4051-9166-c9294f9d2201'
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Rebuilding local state with 0 history event...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Processing 2 new history event(s): [ORCHESTRATORSTARTED=1, EXECUTIONSTARTED=1]
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Waiting for 1 task(s) and 0 event(s) to complete...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Returning 1 action(s)
== APP == Received "Activity Request" work item
== APP == Activity hello completed with output "Hello Tokyo!" (14 chars)
== APP == Received "Orchestrator Request" work item with instance id 'dc040bea-6436-4051-9166-c9294f9d2201'
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Rebuilding local state with 3 history event...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Processing 2 new history event(s): [ORCHESTRATORSTARTED=1, TASKCOMPLETED=1]
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Waiting for 1 task(s) and 0 event(s) to complete...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Returning 1 action(s)
== APP == Received "Activity Request" work item
== APP == Activity hello completed with output "Hello Seattle!" (16 chars)
== APP == Received "Orchestrator Request" work item with instance id 'dc040bea-6436-4051-9166-c9294f9d2201'
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Rebuilding local state with 6 history event...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Processing 2 new history event(s): [ORCHESTRATORSTARTED=1, TASKCOMPLETED=1]
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Waiting for 1 task(s) and 0 event(s) to complete...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Returning 1 action(s)
== APP == Received "Activity Request" work item
== APP == Activity hello completed with output "Hello London!" (15 chars)
== APP == Received "Orchestrator Request" work item with instance id 'dc040bea-6436-4051-9166-c9294f9d2201'
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Rebuilding local state with 9 history event...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Processing 2 new history event(s): [ORCHESTRATORSTARTED=1, TASKCOMPLETED=1]
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Orchestration completed with status COMPLETED
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Returning 1 action(s)
INFO[0006] dc040bea-6436-4051-9166-c9294f9d2201: 'sequence' completed with a COMPLETED status. app_id=activity-sequence-workflow instance=kaibocai-devbox scope=wfengine.backend type=log ver=1.12.3
== APP == Instance dc040bea-6436-4051-9166-c9294f9d2201 completed
== APP == Orchestration completed! Result: ["Hello Tokyo!","Hello Seattle!","Hello London!"]
后续步骤
2.5 - Dapr PHP SDK
Dapr 提供了一个 SDK 来帮助开发 PHP 应用程序。使用它,你可以用 Dapr 创建 PHP 客户端、服务器和虚拟 Actor。
设置
前提条件
可选前提条件
初始化项目
在你想要创建服务的目录中,运行 composer init 并回答问题。
使用 composer require dapr/php-sdk 以及你可能希望使用的任何其他依赖项进行安装。
配置服务
创建一个 config.php,复制以下内容:
<?php
use Dapr\Actors\Generators\ProxyFactory;
use Dapr\Middleware\Defaults\{Response\ApplicationJson,Tracing};
use Psr\Log\LogLevel;
use function DI\{env,get};
return [
// 设置日志级别
'dapr.log.level' => LogLevel::WARNING,
// 在每个请求上生成一个新的代理 - 建议在开发中使用
'dapr.actors.proxy.generation' => ProxyFactory::GENERATED,
// 在此处添加任何订阅
'dapr.subscriptions' => [],
// 如果此服务将托管任何 Actor,请在此添加它们
'dapr.actors' => [],
// 如果此服务将托管任何 Actor,配置 Dapr 应将 Actor 视为空闲的时间
'dapr.actors.idle_timeout' => null,
// 如果此服务将托管任何 Actor,配置 Dapr 检查空闲 Actor 的频率
'dapr.actors.scan_interval' => null,
// 如果此服务将托管任何 Actor,配置 Dapr 在排空期间等待 Actor 完成的时间
'dapr.actors.drain_timeout' => null,
// 如果此服务将托管任何 Actor,配置 Dapr 是否应等待 Actor 完成
'dapr.actors.drain_enabled' => null,
// 你通常不需要更改此项,但如果需要可以在此设置
'dapr.port' => env('DAPR_HTTP_PORT', '3500'),
// 在此处添加任何自定义序列化例程
'dapr.serializers.custom' => [],
// 在此处添加任何自定义反序列化例程
'dapr.deserializers.custom' => [],
// 以下内容无效,因为它是默认中间件并按指定顺序处理
'dapr.http.middleware.request' => [get(Tracing::class)],
'dapr.http.middleware.response' => [get(ApplicationJson::class), get(Tracing::class)],
];
创建服务
创建 index.php 并放入以下内容:
<?php
require_once __DIR__.'/vendor/autoload.php';
use Dapr\App;
$app = App::create(configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions(__DIR__ . '/config.php'));
$app->get('/hello/{name}', function(string $name) {
return ['hello' => $name];
});
$app->start();
试用
使用 dapr init 初始化 dapr,然后使用 dapr run -a dev -p 3000 -- php -S 0.0.0.0:3000 启动项目。
现在你可以打开 Web 浏览器并访问 http://localhost:3000/hello/world
将 world 替换为你的名字、宠物的名字或任何你想要的内容。
恭喜,你已经创建了你的第一个 Dapr 服务!我很期待看到你用它做什么!
更多信息
2.5.1 - 虚拟 Actor
如果你不熟悉 Actor 模式,了解 Actor 模式的最佳位置是 Actor 概述。
在 PHP SDK 中,Actor 有两个方面,即客户端和 Actor(也称为运行时)。作为 Actor 的客户端,
你将通过 ActorProxy 类与远程 Actor 交互。该类使用若干配置策略之一动态地生成代理类。
在编写 Actor 时,状态可以为你管理。你可以钩入 Actor 生命周期,并定义提醒和定时器。 这为你处理适合 Actor 模式的各类问题提供了相当大的能力。
Actor 代理
每当您需要与 Actor 通信时,都需要获取一个代理对象来执行此操作。代理负责 序列化您的请求、反序列化响应并将其返回给您,同时遵守指定接口定义的契约。
为了创建代理,首先需要一个接口来定义您与 Actor 发送和接收的内容和方式。 例如,如果您想与一个仅跟踪计数的计数 Actor 通信,您可以将接口 定义如下:
<?php
#[\Dapr\Actors\Attributes\DaprType('Counter')]
interface ICount {
function increment(int $amount = 1): void;
function get_count(): int;
}
将此接口放在 Actor 和客户端都可以访问的共享库中(如果两者都用 PHP 编写),这是一个好主意。DaprType
属性告诉 DaprClient 要发送到的 Actor 名称。它应该与实现的 DaprType 匹配,尽管
如果需要,您可以覆盖该类型。
<?php
$app->run(function(\Dapr\Actors\ActorProxy $actorProxy) {
$actor = $actorProxy->get(ICount::class, 'actor-id');
$actor->increment(10);
});
编写 Actor
要创建 Actor,您需要实现之前定义的接口,并添加 DaprType 属性。所有
Actor 必须实现 IActor,不过有一个 Actor 基类实现了样板代码,使您的实现
更加简单。
以下是计数 Actor:
<?php
#[\Dapr\Actors\Attributes\DaprType('Count')]
class Counter extends \Dapr\Actors\Actor implements ICount {
function __construct(string $id, private CountState $state) {
parent::__construct($id);
}
function increment(int $amount = 1): void {
$this->state->count += $amount;
}
function get_count(): int {
return $this->state->count;
}
}
最重要的是构造函数。它至少接受一个名为 id 的参数,该参数是 Actor 的 id。
任何其他参数都由 DI 容器注入,包括您想要使用的任何 ActorState。
Actor 生命周期
Actor 通过构造函数在每个针对该 Actor 类型的请求上进行实例化。您可以使用它来计算 临时状态或处理您所需的任何特定于请求的启动,例如设置其他客户端或 连接。
实例化 Actor 后,可能会调用 on_activation() 方法。on_activation() 方法在 Actor “唤醒”
或首次创建时调用。它不会在每次请求时调用。
接下来,调用 Actor 方法。这可能来自定时器、提醒或客户端。您可以执行任何需要 完成的工作和/或抛出异常。
最后,工作结果返回给调用者。一段时间后(取决于您如何配置
服务),Actor 将被停用,并将调用 on_deactivation()。如果主机宕机、
daprd 崩溃或发生其他一些阻止其成功调用的错误,则可能不会调用此方法。
Actor 状态
Actor 状态是扩展 ActorState 的"普通旧 PHP 对象"(POPO)。ActorState 基类提供了一些
有用的方法。以下是示例实现:
<?php
class CountState extends \Dapr\Actors\ActorState {
public int $count = 0;
}
注册 Actor
Dapr 期望在启动时知道服务可以托管哪些 Actor。您需要将其添加到配置中:
如果你想利用预编译的依赖注入,你需要使用一个工厂:
<?php
// in config.php
return [
'dapr.actors' => fn() => [Counter::class],
];
启动应用程序所需的全部内容:
<?php
require_once __DIR__ . '/vendor/autoload.php';
$app = \Dapr\App::create(
configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions('config.php')->enableCompilation(__DIR__)
);
$app->start();
<?php
// in config.php
return [
'dapr.actors' => [Counter::class]
];
启动应用程序所需的全部内容:
<?php
require_once __DIR__ . '/vendor/autoload.php';
$app = \Dapr\App::create(configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions('config.php'));
$app->start();
2.5.1.1 - 生产环境参考:Actor
代理模式
Actor 代理有四种不同的处理模式。每种模式呈现不同的权衡,你需要在开发和生产环境中进行权衡。
<?php
\Dapr\Actors\Generators\ProxyFactory::GENERATED;
\Dapr\Actors\Generators\ProxyFactory::GENERATED_CACHED;
\Dapr\Actors\Generators\ProxyFactory::ONLY_EXISTING;
\Dapr\Actors\Generators\ProxyFactory::DYNAMIC;
可以通过 dapr.actors.proxy.generation 配置键进行设置。
这是默认模式。在此模式下,每个请求都会生成一个类并通过 eval 执行。它主要用于开发,不应在生产环境中使用。
这与 ProxyModes::GENERATED 相同,只是类存储在临时文件中,因此不需要在每次请求时重新生成。它不知道何时更新缓存的类,因此不建议在开发中使用,但在无法手动生成文件时提供此选项。
在此模式下,如果代理类不存在,将抛出异常。当你不想在生产环境中生成代码时,这很有用。你需要确保生成并预加载/自动加载该类。
生成代理
你可以创建一个 composer 脚本按需生成代理,以利用 ONLY_EXISTING 模式。
创建一个 ProxyCompiler.php
<?php
class ProxyCompiler {
private const PROXIES = [
MyActorInterface::class,
MyOtherActorInterface::class,
];
private const PROXY_LOCATION = __DIR__.'/proxies/';
public static function compile() {
try {
$app = \Dapr\App::create();
foreach(self::PROXIES as $interface) {
$output = $app->run(function(\DI\FactoryInterface $factory) use ($interface) {
return \Dapr\Actors\Generators\FileGenerator::generate($interface, $factory);
});
$reflection = new ReflectionClass($interface);
$dapr_type = $reflection->getAttributes(\Dapr\Actors\Attributes\DaprType::class)[0]->newInstance()->type;
$filename = 'dapr_proxy_'.$dapr_type.'.php';
file_put_contents(self::PROXY_LOCATION.$filename, $output);
echo "Compiled: $interface";
}
} catch (Exception $ex) {
echo "Failed to generate proxy for $interface\n{$ex->getMessage()} on line {$ex->getLine()} in {$ex->getFile()}\n";
}
}
}
然后为生成的代理添加一个 psr-4 自动加载器,并在 composer.json 中添加一个脚本:
{
"autoload": {
"psr-4": {
"Dapr\\Proxies\\": "path/to/proxies"
}
},
"scripts": {
"compile-proxies": "ProxyCompiler::compile"
}
}
最后,配置 dapr 仅使用生成的代理:
<?php
// in config.php
return [
'dapr.actors.proxy.generation' => ProxyFactory::ONLY_EXISTING,
];
在此模式下,代理满足接口约定,但实际上并不实现接口本身(意味着 instanceof 将返回 false)。此模式利用 PHP 的一些特性来工作,适用于代码无法被 eval 或生成的场景。
请求
对于任何模式,创建 actor 代理的开销都非常小。创建 actor 代理对象时不会发起任何请求。
当你在代理对象上调用方法时,只有你实现的方法由你的 actor 实现提供服务。get_id() 在本地处理,而 get_reminder()、delete_reminder() 等由 daprd 处理。
Actor 实现
PHP 中的每个 actor 实现都必须实现 \Dapr\Actors\IActor 并使用 \Dapr\Actors\ActorTrait trait。这允许快速反射并提供一些快捷方式。使用 \Dapr\Actors\Actor 抽象基类会自动为你完成这些,但如果你需要覆盖默认行为,可以通过实现接口并使用该 trait 来实现。
激活和停用
当 actor 激活时,会将一个令牌文件写入临时目录(在 Linux 上默认为 '/tmp/dapr_' + sha256(concat(Dapr type, id)),在 Windows 上为 '%temp%/dapr_' + sha256(concat(Dapr type, id)))。该文件会一直保留,直到 actor 停用或主机关闭。这确保了当 Dapr 在主机上激活 actor 时,on_activation 只被调用一次。
性能
在使用 php-fpm 和 nginx 或 Windows 上的 IIS 的生产环境中,actor 方法调用非常快。虽然 actor 在每个请求上都会被构造,但 actor 状态键仅在需要时加载,而不是在每个请求期间加载。但是,单独加载每个键会有一些开销。可以通过在状态中存储数据数组来缓解这个问题,以可用性换取速度。不建议从一开始就这样做,而是作为需要时的优化手段。
状态版本控制
ActorState 对象中的变量名直接对应存储中的键名。这意味着如果你更改变量的类型或名称,可能会遇到错误。为了解决这个问题,你可能需要对状态对象进行版本控制。为此,你需要覆盖状态的加载和存储方式。有很多方法可以解决这个问题,其中一种解决方案可能如下所示:
<?php
class VersionedState extends \Dapr\Actors\ActorState {
/**
* @var int 存储中状态的当前版本。我们给出当前版本的默认值。
* 但是,它在存储中可能有不同的值。
*/
public int $state_version = self::VERSION;
/**
* @var int 数据的当前版本
*/
private const VERSION = 3;
/**
* 在你的 actor 激活时调用。
*/
public function upgrade() {
if($this->state_version < self::VERSION) {
$value = parent::__get($this->get_versioned_key('key', $this->state_version));
// 更新数据结构后更新值
parent::__set($this->get_versioned_key('key', self::VERSION), $value);
$this->state_version = self::VERSION;
$this->save_state();
}
}
// 如果你在上面的方法中根据需要升级所有键,则不需要在加载/保存时遍历以前的
// 键,你只需获取键的当前版本。
private function get_previous_version(int $version): int {
return $this->has_previous_version($version) ? $version - 1 : $version;
}
private function has_previous_version(int $version): bool {
return $version >= 0;
}
private function walk_versions(int $version, callable $callback, callable $predicate): mixed {
$value = $callback($version);
if($predicate($value) || !$this->has_previous_version($version)) {
return $value;
}
return $this->walk_versions($this->get_previous_version($version), $callback, $predicate);
}
private function get_versioned_key(string $key, int $version) {
return $this->has_previous_version($version) ? $version.$key : $key;
}
public function __get(string $key): mixed {
return $this->walk_versions(
self::VERSION,
fn($version) => parent::__get($this->get_versioned_key($key, $version)),
fn($value) => isset($value)
);
}
public function __isset(string $key): bool {
return $this->walk_versions(
self::VERSION,
fn($version) => parent::__isset($this->get_versioned_key($key, $version)),
fn($isset) => $isset
);
}
public function __set(string $key,mixed $value): void {
// 可选:你可以取消设置键的以前版本
parent::__set($this->get_versioned_key($key, self::VERSION), $value);
}
public function __unset(string $key) : void {
// 取消设置此版本和所有以前版本
$this->walk_versions(
self::VERSION,
fn($version) => parent::__unset($this->get_versioned_key($key, $version)),
fn() => false
);
}
}
有很多可以优化的地方,直接在生产环境中使用它并不是一个好主意,但你可以了解它的工作原理。其中大部分将取决于你的用例,这也是 SDK 中没有类似功能的原因。例如,在此示例实现中,保留以前的值是为了防止升级期间可能出现的错误;保留以前的值允许再次运行升级,但你可能希望删除以前的值。
2.5.2 - 使用 PHP 进行发布订阅
使用 Dapr,你可以发布任何内容,包括云事件。SDK 包含一个简单的云事件实现,但你也可以直接传递一个符合云事件规范的数组,或者使用其他库。
<?php
$app->post('/publish', function(\Dapr\Client\DaprClient $daprClient) {
$daprClient->publishEvent(pubsubName: 'pubsub', topicName: 'my-topic', data: ['something' => 'happened']);
});
有关发布/订阅的更多信息,请查看操作指南。
数据内容类型
PHP SDK 允许在构造自定义云事件或发布原始数据时设置数据内容类型。
<?php
$event = new \Dapr\PubSub\CloudEvent();
$event->data = $xml;
$event->data_content_type = 'application/xml';
<?php
/**
* @var \Dapr\Client\DaprClient $daprClient
*/
$daprClient->publishEvent(pubsubName: 'pubsub', topicName: 'my-topic', data: $raw_data, contentType: 'application/octet-stream');
Binary data
二进制数据仅支持 <code>application/octet-steam</code>。
接收云事件
在你的订阅处理程序中,你可以让 DI 容器向你的控制器中注入 Dapr\PubSub\CloudEvent 或 array。前者会进行一些验证以确保你有一个正确的事件。如果你需要直接访问数据,或者事件不符合规范,请使用 array。
2.5.3 - 应用
在 PHP 中,没有默认的路由器。因此,提供了 \Dapr\App 类。它在底层使用
Nikic’s FastRoute。不过,您可以根据需要自由使用任何路由器或
框架。只需查看 App 类中的 add_dapr_routes() 方法,即可了解 actors 和
订阅是如何实现的。
每个应用都应从 App::create() 开始,它接受两个参数,第一个是现有的 DI 容器(如果
您有的话),第二个是回调函数,用于钩入 ContainerBuilder 并添加您自己的配置。
在此基础上,您应该定义路由,然后调用 $app->start() 来执行当前请求的路由。
<?php
// app.php
require_once __DIR__ . '/vendor/autoload.php';
$app = \Dapr\App::create(configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions('config.php'));
// 添加一个 GET /test/{id} 的控制器,返回 id
$app->get('/test/{id}', fn(string $id) => $id);
$app->start();
从控制器返回
您可以从控制器返回任何内容,它将被序列化为 json 对象。您也可以请求 Psr Response 对象并返回该对象,从而允许您自定义标头,并对整个响应进行控制:
<?php
$app = \Dapr\App::create(configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions('config.php'));
// 添加一个 GET /test/{id} 的控制器,返回 id
$app->get('/test/{id}',
fn(
string $id,
\Psr\Http\Message\ResponseInterface $response,
\Nyholm\Psr7\Factory\Psr17Factory $factory) => $response->withBody($factory->createStream($id)));
$app->start();
将应用作为客户端使用
当您只想将 Dapr 用作客户端时,例如在现有代码中,可以调用 $app->run()。在这些情况下,通常
不需要自定义配置,不过,您可能希望使用编译后的 DI 容器,特别是在生产环境中:
<?php
// app.php
require_once __DIR__ . '/vendor/autoload.php';
$app = \Dapr\App::create(configure: fn(\DI\ContainerBuilder $builder) => $builder->enableCompilation(__DIR__));
$result = $app->run(fn(\Dapr\DaprClient $client) => $client->get('/invoke/other-app/method/my-method'));
在其他框架中使用
提供了 DaprClient 对象,实际上,App 对象使用的所有便捷方法都是基于 DaprClient 构建的。
<?php
require_once __DIR__ . '/vendor/autoload.php';
$clientBuilder = \Dapr\Client\DaprClient::clientBuilder();
// 您可以自定义(反)序列化,或注释掉以使用默认的 JSON 序列化器。
$clientBuilder = $clientBuilder->withSerializationConfig($yourSerializer)->withDeserializationConfig($yourDeserializer);
// 您还可以传入一个 logger
$clientBuilder = $clientBuilder->withLogger($myLogger);
// 以及更改边车的 url,例如,使用 https
$clientBuilder = $clientBuilder->useHttpClient('https://localhost:3800')
在调用之前,您可以调用多个函数
2.5.3.1 - 单元测试
单元测试和集成测试是 PHP SDK 的一等公民。通过使用 DI 容器、mock、stub 以及提供的 \Dapr\Mocks\TestClient,你可以编写非常细粒度的测试。
测试 Actor
在测试 Actor 时,我们关注两件事:
- 基于初始状态的返回结果
- 基于初始状态的最终状态
下面是一个测试非常简单的 actor 的示例,该 actor 更新其状态并返回特定值:
<?php
// TestState.php
class TestState extends \Dapr\Actors\ActorState
{
public int $number;
}
// TestActor.php
#[\Dapr\Actors\Attributes\DaprType('TestActor')]
class TestActor extends \Dapr\Actors\Actor
{
public function __construct(string $id, private TestState $state)
{
parent::__construct($id);
}
public function oddIncrement(): bool
{
if ($this->state->number % 2 === 0) {
return false;
}
$this->state->number += 1;
return true;
}
}
// TheTest.php
class TheTest extends \PHPUnit\Framework\TestCase
{
private \DI\Container $container;
public function setUp(): void
{
parent::setUp();
// 创建一个默认应用并从中提取 DI 容器
$app = \Dapr\App::create(
configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions(
['dapr.actors' => [TestActor::class]],
[\Dapr\DaprClient::class => \DI\autowire(\Dapr\Mocks\TestClient::class)]
));
$app->run(fn(\DI\Container $container) => $this->container = $container);
}
public function testIncrementsWhenOdd()
{
$id = uniqid();
$runtime = $this->container->get(\Dapr\Actors\ActorRuntime::class);
$client = $this->getClient();
// 从 http://localhost:1313/reference/api/actors_api/ 返回当前状态
$client->register_get("/actors/TestActor/$id/state/number", code: 200, data: 3);
// 确保从 http://localhost:1313/reference/api/actors_api/ 递增
$client->register_post(
"/actors/TestActor/$id/state",
code: 204,
response_data: null,
expected_request: [
[
'operation' => 'upsert',
'request' => [
'key' => 'number',
'value' => 4,
],
],
]
);
$result = $runtime->resolve_actor(
'TestActor',
$id,
fn($actor) => $runtime->do_method($actor, 'oddIncrement', null)
);
$this->assertTrue($result);
}
private function getClient(): \Dapr\Mocks\TestClient
{
return $this->container->get(\Dapr\DaprClient::class);
}
}
<?php
// TestState.php
class TestState extends \Dapr\Actors\ActorState
{
public int $number;
}
// TestActor.php
#[\Dapr\Actors\Attributes\DaprType('TestActor')]
class TestActor extends \Dapr\Actors\Actor
{
public function __construct(string $id, private TestState $state)
{
parent::__construct($id);
}
public function oddIncrement(): bool
{
if ($this->state->number % 2 === 0) {
return false;
}
$this->state->number += 1;
return true;
}
}
// TheTest.php
class TheTest extends \PHPUnit\Framework\TestCase
{
public function testNotIncrementsWhenEven() {
$container = new \DI\Container();
$state = new TestState($container, $container);
$state->number = 4;
$id = uniqid();
$actor = new TestActor($id, $state);
$this->assertFalse($actor->oddIncrement());
$this->assertSame(4, $state->number);
}
}
测试事务
在基于事务构建时,你可能需要测试如何处理失败的事务。为此,你需要注入故障并确保事务符合你的预期。
<?php
// MyState.php
#[\Dapr\State\Attributes\StateStore('statestore', \Dapr\consistency\EventualFirstWrite::class)]
class MyState extends \Dapr\State\TransactionalState {
public string $value = '';
}
// SomeService.php
class SomeService {
public function __construct(private MyState $state) {}
public function doWork() {
$this->state->begin();
$this->state->value = "hello world";
$this->state->commit();
}
}
// TheTest.php
class TheTest extends \PHPUnit\Framework\TestCase {
private \DI\Container $container;
public function setUp(): void
{
parent::setUp();
$app = \Dapr\App::create(configure: fn(\DI\ContainerBuilder $builder)
=> $builder->addDefinitions([\Dapr\DaprClient::class => \DI\autowire(\Dapr\Mocks\TestClient::class)]));
$this->container = $app->run(fn(\DI\Container $container) => $container);
}
private function getClient(): \Dapr\Mocks\TestClient {
return $this->container->get(\Dapr\DaprClient::class);
}
public function testTransactionFailure() {
$client = $this->getClient();
// 从 https://docs.dapr.io/zh-hans/reference/api/state_api/ 创建响应
$client->register_post('/state/statestore/bulk', code: 200, response_data: [
[
'key' => 'value',
// 没有先前的值
],
], expected_request: [
'keys' => ['value'],
'parallelism' => 10
]);
$client->register_post('/state/statestore/transaction',
code: 200,
response_data: null,
expected_request: [
'operations' => [
[
'operation' => 'upsert',
'request' => [
'key' => 'value',
'value' => 'hello world'
]
]
]
]
);
$state = new MyState($this->container, $this->container);
$service = new SomeService($state);
$service->doWork();
$this->assertSame('hello world', $state->value);
}
}
<?php
// MyState.php
#[\Dapr\State\Attributes\StateStore('statestore', \Dapr\consistency\EventualFirstWrite::class)]
class MyState extends \Dapr\State\TransactionalState {
public string $value = '';
}
// SomeService.php
class SomeService {
public function __construct(private MyState $state) {}
public function doWork() {
$this->state->begin();
$this->state->value = "hello world";
$this->state->commit();
}
}
// TheTest.php
class TheTest extends \PHPUnit\Framework\TestCase {
public function testTransactionFailure() {
$state = $this->createStub(MyState::class);
$service = new SomeService($state);
$service->doWork();
$this->assertSame('hello world', $state->value);
}
}
2.5.4 - 使用 PHP 进行状态管理
Dapr 为在应用程序中使用状态提供了一种优秀的模块化方法。学习基础知识的最佳方式是访问 操作指南。
元数据
许多状态组件允许您向组件传递元数据以控制组件行为的特定方面。PHP SDK 允许您通过以下方式传递该元数据:
<?php
// 使用 state manager
$app->run(
fn(\Dapr\State\StateManager $stateManager) =>
$stateManager->save_state('statestore', new \Dapr\State\StateItem('key', 'value', metadata: ['port' => '112'])));
// 使用 DaprClient
$app->run(fn(\Dapr\Client\DaprClient $daprClient) => $daprClient->saveState(storeName: 'statestore', key: 'key', value: 'value', metadata: ['port' => '112']))
这是向 Cassandra 传递端口元数据的示例。
每个状态操作都允许传递元数据。
一致性/并发
在 PHP SDK 中,有四个类代表 Dapr 中四种不同类型的一致性和并发:
<?php
[
\Dapr\consistency\StrongLastWrite::class,
\Dapr\consistency\StrongFirstWrite::class,
\Dapr\consistency\EventualLastWrite::class,
\Dapr\consistency\EventualFirstWrite::class,
]
将其中之一传递给 StateManager 方法或使用 StateStore() 属性,您可以定义状态存储应如何处理冲突。
并行度
执行批量读取或开始事务时,您可以指定并行度数量。如果 Dapr 必须一次读取一个键,它将从底层存储中"最多"一次读取该数量的键。这有助于控制状态存储上的负载,但会降低性能。默认值为 10。
前缀
硬编码的键名称很有用,但为什么不使状态对象更具可重用性呢?在提交事务或将对象保存到状态时,您可以传递一个应用于对象中每个键的前缀。
<?php
class TransactionObject extends \Dapr\State\TransactionalState {
public string $key;
}
$app->run(function (TransactionObject $object ) {
$object->begin(prefix: 'my-prefix-');
$object->key = 'value';
// 提交到键 `my-prefix-key`
$object->commit();
});
<?php
class StateObject {
public string $key;
}
$app->run(function(\Dapr\State\StateManager $stateManager) {
$stateManager->load_object($obj = new StateObject(), prefix: 'my-prefix-');
// 原始值来自 `my-prefix-key`
$obj->key = 'value';
// 保存到 `my-prefix-key`
$stateManager->save_object($obj, prefix: 'my-prefix-');
});
2.5.5 - 自定义序列化
Dapr 使用 JSON 序列化,因此在发送/接收数据时会丢失(复杂)类型信息。
序列化
当从控制器返回对象、将对象传递给 DaprClient,或将对象存储在状态存储中时,
只有公共属性会被扫描和序列化。你可以通过实现 \Dapr\Serialization\ISerialize 来自定义此行为。
例如,如果你想创建一个序列化为字符串的 ID 类型,可以像这样实现:
<?php
class MyId implements \Dapr\Serialization\Serializers\ISerialize
{
public string $id;
public function serialize(mixed $value,\Dapr\Serialization\ISerializer $serializer): mixed
{
// $value === $this
return $this->id;
}
}
这适用于我们拥有完全所有权的任何类型,但它不适用于来自库或 PHP 本身的类。 为此,你需要向 DI 容器注册自定义序列化器:
<?php
// 在 config.php 中
class SerializeSomeClass implements \Dapr\Serialization\Serializers\ISerialize
{
public function serialize(mixed $value,\Dapr\Serialization\ISerializer $serializer) : mixed
{
// 序列化 $value 并返回结果
}
}
return [
'dapr.serializers.custom' => [SomeClass::class => new SerializeSomeClass()],
];
反序列化
反序列化的工作方式完全相同,只是接口是 \Dapr\Deserialization\Deserializers\IDeserialize。
2.6 - Dapr Python SDK
Dapr 提供了多种子包来帮助开发 Python 应用程序。使用它们,你可以使用 Dapr 创建 Python 客户端、服务器和虚拟 actor。
前提条件
- 已安装 Dapr CLI
- 已初始化 Dapr 环境
- 已安装 Python 3.9+
安装
要开始使用 Python SDK,请安装主要的 Dapr Python SDK 软件包。
pip install dapr
注意: 开发包将包含与 Dapr 运行时预发布版本兼容的功能和行为。在安装 dapr-dev 软件包之前,请确保卸载任何稳定版本的 Python SDK。
pip install dapr-dev
可用的子包
SDK 导入
Python SDK 导入是随主 SDK 安装包含的子包,但在使用时需要导入。Dapr Python SDK 提供的最常见导入包括:
了解有关 所有 可用的 Dapr Python SDK 导入 的更多信息。
SDK 扩展
SDK 扩展主要用作接收发布订阅事件、以编程方式创建发布订阅订阅以及处理输入绑定事件的实用工具。虽然你可以在不使用扩展的情况下完成所有这些任务,但使用 Python SDK 扩展会更加方便。
了解有关 Dapr Python SDK 扩展 的更多信息。
试用
克隆 Python SDK 仓库。
git clone https://github.com/dapr/python-sdk.git
浏览 Python 快速入门、教程和示例,查看 Dapr 的实际运行情况:
| SDK 示例 | 描述 |
|---|---|
| 快速入门 | 在几分钟内使用 Python SDK 体验 Dapr 的 API 构建块。 |
| SDK 示例 | 克隆 SDK 仓库以试用一些示例并开始使用。 |
| 绑定教程 | 了解 Dapr Python SDK 如何与其他 Dapr SDK 配合工作以启用绑定。 |
| 分布式计算器教程 | 使用 Dapr Python SDK 处理方法调用和状态持久化功能。 |
| Hello World 教程 | 了解如何使用 Python SDK 在本地机器上启动和运行 Dapr。 |
| Hello Kubernetes 教程 | 在 Kubernetes 集群中使用 Dapr Python SDK 启动和运行。 |
| 可观测性教程 | 使用 Python SDK 探索 Dapr 的指标收集、链路追踪、日志记录和健康检查功能。 |
| 发布订阅教程 | 了解 Dapr Python SDK 如何与其他 Dapr SDK 配合工作以启用发布订阅应用程序。 |
更多信息
2.6.1 - Dapr 客户端 Python SDK 入门
Dapr 客户端包允许你从 Python 应用程序与其他 Dapr 应用程序进行交互。
注意
如果还没有,请尝试其中一个快速入门,快速了解如何将 Dapr Python SDK 与 API 构建块一起使用。前提条件
在开始之前,安装 Dapr Python 包。
导入客户端包
dapr 包包含 DaprClient,用于创建和使用客户端。
from dapr.clients import DaprClient
初始化客户端
你可以通过多种方式初始化 Dapr 客户端:
默认值:
当不使用任何参数初始化客户端时,它将使用 Dapr 边车实例的默认值(127.0.0.1:50001)。
from dapr.clients import DaprClient
with DaprClient() as d:
# 使用客户端
初始化时指定端点:
当在构造函数中作为参数传递时,gRPC 端点优先于任何配置或环境变量。
from dapr.clients import DaprClient
with DaprClient("mydomain:50051?tls=true") as d:
# 使用客户端
配置选项:
Dapr 边车端点
你可以使用标准化的 DAPR_GRPC_ENDPOINT 环境变量来指定 gRPC 端点。设置此变量后,可以在不使用任何参数的情况下初始化客户端:
export DAPR_GRPC_ENDPOINT="mydomain:50051?tls=true"
from dapr.clients import DaprClient
with DaprClient() as d:
# 客户端将使用环境变量中指定的端点
旧的环境变量 DAPR_RUNTIME_HOST、DAPR_HTTP_PORT 和 DAPR_GRPC_PORT 也受支持,但 DAPR_GRPC_ENDPOINT 优先。
Dapr API 令牌
如果你的 Dapr 实例配置为需要 DAPR_API_TOKEN 环境变量,你可以在环境中设置它,客户端将自动使用它。
你可以在这里阅读更多关于 Dapr API 令牌身份验证的信息。
健康检查超时
在客户端初始化时,会针对 Dapr 边车(/healthz/outbound)执行健康检查。
客户端将等待边车启动并运行后再继续。
默认健康检查超时为 60 秒,但可以通过设置 DAPR_HEALTH_TIMEOUT 环境变量来覆盖。
重试和超时
如果从边车收到特定的错误代码,Dapr 客户端可以重试请求。这可以通过 DAPR_API_MAX_RETRIES 环境变量配置,并且会自动拾取,不需要任何代码更改。
DAPR_API_MAX_RETRIES 的默认值是 0,表示不会进行重试。
你可以通过创建 dapr.clients.retry.RetryPolicy 对象并将其传递给 DaprClient 构造函数来微调更多重试参数:
from dapr.clients.retry import RetryPolicy
retry = RetryPolicy(
max_attempts=5,
initial_backoff=1,
max_backoff=20,
backoff_multiplier=1.5,
retryable_http_status_codes=[408, 429, 500, 502, 503, 504],
retryable_grpc_status_codes=[StatusCode.UNAVAILABLE, StatusCode.DEADLINE_EXCEEDED, ]
)
with DaprClient(retry_policy=retry) as d:
...
或者对于 actors:
factory = ActorProxyFactory(retry_policy=RetryPolicy(max_attempts=3))
proxy = ActorProxy.create('DemoActor', ActorId('1'), DemoActorInterface, factory)
超时可以通过环境变量 DAPR_API_TIMEOUT_SECONDS 为所有调用设置。默认值为 60 秒。
注意:你可以单独控制服务调用的超时,方法是将
timeout参数传递给invoke_method方法。
错误处理
最初,Dapr 中的错误遵循标准 gRPC 错误模型。然而,为了提供更详细和更有信息量的错误消息,在 1.13 版本中引入了一个增强的错误模型,该模型与 gRPC 更丰富的错误模型保持一致。作为响应,Python SDK 实现了 DaprGrpcError,这是一个自定义异常类,旨在改善开发人员体验。
值得注意的是,对于所有 gRPC 状态异常,使用 DaprGrpcError 的转换正在进行中。截至目前,并非 SDK 中的每个 API 调用都已更新以利用此自定义异常。我们正在积极进行此增强,并欢迎社区的贡献。
使用 Dapr python-SDK 时处理 DaprGrpcError 异常的示例:
try:
d.save_state(store_name=storeName, key=key, value=value)
except DaprGrpcError as err:
print(f'Status code: {err.code()}')
print(f"Message: {err.message()}")
print(f"Error code: {err.error_code()}")
print(f"Error info(reason): {err.error_info.reason}")
print(f"Resource info (resource type): {err.resource_info.resource_type}")
print(f"Resource info (resource name): {err.resource_info.resource_name}")
print(f"Bad request (field): {err.bad_request.field_violations[0].field}")
print(f"Bad request (description): {err.bad_request.field_violations[0].description}")
构建块
Python SDK 允许你与所有 Dapr 构建块 进行交互。
调用服务
Dapr Python SDK 提供了一个简单的 API,可以通过 HTTP 或 gRPC(已弃用)调用服务。可以通过设置 DAPR_API_METHOD_INVOCATION_PROTOCOL 环境变量来选择协议,未设置时默认为 HTTP。Dapr 中的 GRPC 服务调用已弃用,建议使用 GRPC 代理作为替代。
from dapr.clients import DaprClient
with DaprClient() as d:
# 调用方法(gRPC 或 HTTP GET)
resp = d.invoke_method('service-to-invoke', 'method-to-invoke', data='{"message":"Hello World"}')
# 对于其他 HTTP 动词,必须指定动词
# 调用 'POST' 方法(仅 HTTP)
resp = d.invoke_method('service-to-invoke', 'method-to-invoke', data='{"id":"100", "FirstName":"Value", "LastName":"Value"}', http_verb='post')
HTTP api 调用的基本端点在 DAPR_HTTP_ENDPOINT 环境变量中指定。
如果未设置此变量,端点值将从 DAPR_RUNTIME_HOST 和 DAPR_HTTP_PORT 变量派生,其默认值分别为 127.0.0.1 和 3500。
gRPC 调用的基本端点是用于客户端初始化的端点(上文解释)。
- 有关服务调用的完整指南,请访问如何:调用服务。
- 访问 Python SDK 示例,获取代码示例和说明以尝试服务调用。
保存和获取应用程序状态
from dapr.clients import DaprClient
with DaprClient() as d:
# 保存状态
d.save_state(store_name="statestore", key="key1", value="value1")
# 获取状态
data = d.get_state(store_name="statestore", key="key1").data
# 删除状态
d.delete_state(store_name="statestore", key="key1")
- 有关状态操作的完整列表,请访问如何:获取和保存状态。
- 访问 Python SDK 示例,获取代码示例和说明以尝试状态管理。
查询应用程序状态(Alpha)
from dapr import DaprClient
query = '''
{
"filter": {
"EQ": { "state": "CA" }
},
"sort": [
{
"key": "person.id",
"order": "DESC"
}
]
}
'''
with DaprClient() as d:
resp = d.query_state(
store_name='state_store',
query=query,
states_metadata={"metakey": "metavalue"}, # 可选
)
- 有关状态存储查询选项的完整列表,请访问如何:查询状态。
- 访问 Python SDK 示例,获取代码示例和说明以尝试状态存储查询。
发布和订阅
发布消息
from dapr.clients import DaprClient
with DaprClient() as d:
resp = d.publish_event(pubsub_name='pubsub', topic_name='TOPIC_A', data='{"message":"Hello World"}')
发送带有 json 有效负载的 CloudEvents 消息:
from dapr.clients import DaprClient
import json
with DaprClient() as d:
cloud_event = {
'specversion': '1.0',
'type': 'com.example.event',
'source': 'my-service',
'id': 'myid',
'data': {'id': 1, 'message': 'hello world'},
'datacontenttype': 'application/json',
}
# 将数据内容类型设置为 'application/cloudevents+json'
resp = d.publish_event(
pubsub_name='pubsub',
topic_name='TOPIC_CE',
data=json.dumps(cloud_event),
data_content_type='application/cloudevents+json',
)
发布带有纯文本有效负载的 CloudEvents 消息:
from dapr.clients import DaprClient
import json
with DaprClient() as d:
cloud_event = {
'specversion': '1.0',
'type': 'com.example.event',
'source': 'my-service',
'id': "myid",
'data': 'hello world',
'datacontenttype': 'text/plain',
}
# 将数据内容类型设置为 'application/cloudevents+json'
resp = d.publish_event(
pubsub_name='pubsub',
topic_name='TOPIC_CE',
data=json.dumps(cloud_event),
data_content_type='application/cloudevents+json',
)
订阅消息
from cloudevents.sdk.event import v1
from dapr.ext.grpc import App
import json
app = App()
# 主题的默认订阅
@app.subscribe(pubsub_name='pubsub', topic='TOPIC_A')
def mytopic(event: v1.Event) -> None:
data = json.loads(event.Data())
print(f'Received: id={data["id"]}, message="{data ["message"]}"'
' content_type="{event.content_type}"',flush=True)
# 使用发布/订阅路由的特定处理程序
@app.subscribe(pubsub_name='pubsub', topic='TOPIC_A',
rule=Rule("event.type == \"important\"", 1))
def mytopic_important(event: v1.Event) -> None:
data = json.loads(event.Data())
print(f'Received: id={data["id"]}, message="{data ["message"]}"'
' content_type="{event.content_type}"',flush=True)
- 有关发布/订阅的更多信息,请访问如何:发布和订阅。
- 访问 Python SDK 示例,获取代码示例和说明以尝试发布/订阅。
流式消息订阅
你可以使用 subscribe 或 subscribe_handler 方法创建对 PubSub 主题的流式订阅。
subscribe 方法返回一个可迭代的 Subscription 对象,允许你使用 for 循环(例如 for message in subscription)或调用 next_message 方法从流中拉取消息。这将在等待消息时阻塞主线程。
完成后,你应该调用 close 方法来终止订阅并停止接收消息。
subscribe_with_handler 方法接受一个回调函数,该函数对从流中接收到的每条消息执行。
它在单独的线程中运行,因此不会阻塞主线程。回调应返回一个 TopicEventResponse(例如 TopicEventResponse('success')),指示消息是成功处理、应该重试还是应该丢弃。该方法将根据返回的状态自动管理消息确认。对 subscribe_with_handler 方法的调用返回一个关闭函数,完成后应调用该函数以终止订阅。
以下是使用 subscribe 方法的示例:
import time
from dapr.clients import DaprClient
from dapr.clients.grpc.subscription import StreamInactiveError, StreamCancelledError
counter = 0
def process_message(message):
global counter
counter += 1
# 在此处处理消息
print(f'Processing message: {message.data()} from {message.topic()}...')
return 'success'
def main():
with DaprClient() as client:
global counter
subscription = client.subscribe(
pubsub_name='pubsub', topic='TOPIC_A', dead_letter_topic='TOPIC_A_DEAD'
)
try:
for message in subscription:
if message is None:
print('No message received. The stream might have been cancelled.')
continue
try:
response_status = process_message(message)
if response_status == 'success':
subscription.respond_success(message)
elif response_status == 'retry':
subscription.respond_retry(message)
elif response_status == 'drop':
subscription.respond_drop(message)
if counter >= 5:
break
except StreamInactiveError:
print('Stream is inactive. Retrying...')
time.sleep(1)
continue
except StreamCancelledError:
print('Stream was cancelled')
break
except Exception as e:
print(f'Error occurred during message processing: {e}')
finally:
print('Closing subscription...')
subscription.close()
if __name__ == '__main__':
main()
以下是使用 subscribe_with_handler 方法的示例:
import time
from dapr.clients import DaprClient
from dapr.clients.grpc._response import TopicEventResponse
counter = 0
def process_message(message):
# 在此处处理消息
global counter
counter += 1
print(f'Processing message: {message.data()} from {message.topic()}...')
return TopicEventResponse('success')
def main():
with (DaprClient() as client):
# 这将启动一个新线程来监听消息
# 并在 `process_message` 函数中处理它们
close_fn = client.subscribe_with_handler(
pubsub_name='pubsub', topic='TOPIC_A', handler_fn=process_message,
dead_letter_topic='TOPIC_A_DEAD'
)
while counter < 5:
time.sleep(1)
print("Closing subscription...")
close_fn()
if __name__ == '__main__':
main()
- 有关发布/订阅的更多信息,请访问如何:发布和订阅。
- 访问 Python SDK 示例,获取代码示例和说明以尝试流式发布/订阅。
对话(Alpha)
注意
Dapr 对话 API 目前处于 alpha 阶段。从 1.15 版本开始,Dapr 为开发者提供了通过对话 API与大型语言模型(LLM)安全可靠地交互的能力。
from dapr.clients import DaprClient
from dapr.clients.grpc.conversation import ConversationInput
with DaprClient() as d:
inputs = [
ConversationInput(content="What's Dapr?", role='user', scrub_pii=True),
ConversationInput(content='Give a brief overview.', role='user', scrub_pii=True),
]
metadata = {
'model': 'foo',
'key': 'authKey',
'cacheTTL': '10m',
}
response = d.converse_alpha1(
name='echo', inputs=inputs, temperature=0.7, context_id='chat-123', metadata=metadata
)
for output in response.outputs:
print(f'Result: {output.result}')
与输出绑定交互
from dapr.clients import DaprClient
with DaprClient() as d:
resp = d.invoke_binding(binding_name='kafkaBinding', operation='create', data='{"message":"Hello World"}')
- 有关输出绑定的完整指南,请访问如何:使用绑定。
- 访问 Python SDK 示例,获取代码示例和说明以尝试输出绑定。
检索密钥
from dapr.clients import DaprClient
with DaprClient() as d:
resp = d.get_secret(store_name='localsecretstore', key='secretKey')
- 有关密钥的完整指南,请访问如何:检索密钥。
- 访问 Python SDK 示例,获取代码示例和说明以尝试检索密钥
配置
获取配置
from dapr.clients import DaprClient
with DaprClient() as d:
# 获取配置
configuration = d.get_configuration(store_name='configurationstore', keys=['orderId'], config_metadata={})
订阅配置
import asyncio
from time import sleep
from dapr.clients import DaprClient
async def executeConfiguration():
with DaprClient() as d:
storeName = 'configurationstore'
key = 'orderId'
# 等待边车在 20 秒内启动。
d.wait(20)
# 按键订阅配置。
configuration = await d.subscribe_configuration(store_name=storeName, keys=[key], config_metadata={})
while True:
if configuration != None:
items = configuration.get_items()
for key, item in items:
print(f"Subscribe key={key} value={item.value} version={item.version}", flush=True)
else:
print("Nothing yet")
sleep(5)
asyncio.run(executeConfiguration())
- 通过如何:管理配置指南了解有关管理配置的更多信息。
- 访问 Python SDK 示例,获取代码示例和说明以尝试配置。
分布式锁
from dapr.clients import DaprClient
def main():
# 锁参数
store_name = 'lockstore' # 在 components/lockstore.yaml 中定义
resource_id = 'example-lock-resource'
client_id = 'example-client-id'
expiry_in_seconds = 60
with DaprClient() as dapr:
print('Will try to acquire a lock from lock store named [%s]' % store_name)
print('The lock is for a resource named [%s]' % resource_id)
print('The client identifier is [%s]' % client_id)
print('The lock will will expire in %s seconds.' % expiry_in_seconds)
with dapr.try_lock(store_name, resource_id, client_id, expiry_in_seconds) as lock_result:
assert lock_result.success, 'Failed to acquire the lock. Aborting.'
print('Lock acquired successfully!!!')
# 此时锁已释放 - 通过 `with` 子句的魔法 ;)
unlock_result = dapr.unlock(store_name, resource_id, client_id)
print('We already released the lock so unlocking will not work.')
print('We tried to unlock it anyway and got back [%s]' % unlock_result.status)
- 了解有关使用分布式锁的更多信息:如何:使用锁。
- 访问 Python SDK 示例,获取代码示例和说明以尝试分布式锁。
加密
from dapr.clients import DaprClient
message = 'The secret is "passw0rd"'
def main():
with DaprClient() as d:
resp = d.encrypt(
data=message.encode(),
options=EncryptOptions(
component_name='crypto-localstorage',
key_name='rsa-private-key.pem',
key_wrap_algorithm='RSA',
),
)
encrypt_bytes = resp.read()
resp = d.decrypt(
data=encrypt_bytes,
options=DecryptOptions(
component_name='crypto-localstorage',
key_name='rsa-private-key.pem',
),
)
decrypt_bytes = resp.read()
print(decrypt_bytes.decode()) # The secret is "passw0rd"
- 有关状态操作的完整列表,请访问如何:使用加密 API。
- 访问 Python SDK 示例,获取代码示例和说明以尝试加密
相关链接
2.6.2 - Dapr Actor Python SDK 入门
Dapr actor 包使您能够从 Python 应用程序与 Dapr virtual actors 交互。
前置条件
- 已安装 Dapr CLI
- 已初始化 Dapr 环境
- 已安装 Python 3.9+
- 已安装 Dapr Python 包
Actor 接口
接口定义了 actor 实现与调用 actor 的客户端之间共享的 actor 契约。由于客户端可能依赖该接口,因此通常将其定义在与 actor 实现分离的程序集中。
from dapr.actor import ActorInterface, actormethod
class DemoActorInterface(ActorInterface):
@actormethod(name="GetMyData")
async def get_my_data(self) -> object:
...
Actor 服务
actor 服务托管 virtual actor。它由一个派生自基类型 Actor 并实现 actor 接口中定义的接口的类来实现。
可以使用以下 Dapr actor 扩展之一创建 actor:
Actor 客户端
actor 客户端包含 actor 客户端的实现,该实现调用 actor 接口中定义的 actor 方法。
import asyncio
from dapr.actor import ActorProxy, ActorId
from demo_actor_interface import DemoActorInterface
async def main():
# 创建代理客户端
proxy = ActorProxy.create('DemoActor', ActorId('1'), DemoActorInterface)
# 在客户端上调用方法
resp = await proxy.GetMyData()
示例
请访问此页面查看可运行的 actor 示例。
Mock Actor 测试
Dapr Python SDK 提供了创建 mock actor 的功能,用于对 actor 方法进行单元测试,并查看它们如何与 actor 状态交互。
示例用法
from dapr.actor.runtime.mock_actor import create_mock_actor
class MyActor(Actor, MyActorInterface):
async def save_state(self, data) -> None:
await self._state_manager.set_state('mystate', data)
await self._state_manager.save_state()
mock_actor = create_mock_actor(MyActor, "id")
await mock_actor.save_state(5)
assert mockactor._state_manager._mock_state['mystate'] == 5 #True
Mock actor 通过将 actor 类和 actor ID(字符串)传递给 create_mock_actor 函数来创建。此函数返回一个 actor 实例,其中许多内部方法已被覆盖。Mock actor 不与 Dapr 交互来执行保存状态或管理定时器等任务,而是使用内存状态来模拟这些行为。
可以通过以下变量访问此状态:
重要提示:由于下文详细讨论的类型提示问题,这些变量对类型提示器/linter 等工具不可见,它们会认为这些是无效变量。您需要使用 #type: ignore 来满足任何此类系统的要求。
_state_manager._mock_state()
一个[str, object]字典,其中存储了所有 actor 状态。通过_state_manager.save_state(key, value)或任何其他 statemanager 方法保存的任何变量都作为该键值对存储在字典中。通过try_get_state或任何其他 statemanager 方法加载的任何值都从此字典中获取。_state_manager._mock_timers()
一个[str, ActorTimerData]字典,其中保存活动的 actor 定时器。任何会添加或移除定时器的 actor 方法都会从此字典中添加或弹出相应的ActorTimerData对象。_state_manager._mock_reminders()
一个 [str, ActorReminderData] 字典,其中保存活动的 actor 提醒。任何会添加或移除定时器的 actor 方法都会从此字典中添加或弹出相应的 ActorReminderData 对象。
注意:定时器和提醒永远不会实际触发。这些字典的存在仅为了测试应该添加或移除定时器/提醒的方法。如果您需要测试它们应该激活的回调,您应该使用适当的值直接调用它们:
result = await mock_actor.recieve_reminder(name, state, due_time, period, _ttl)
# 直接测试结果,或通过查询 `_state_manager._mock_state` 测试副作用(如更改状态)
用法和限制
为了允许更细粒度的控制,_on_activate 方法不会像 Dapr 初始化新的 Actor 实例时那样自动调用。您应该在测试中根据需要手动调用它。
Mock actor 系统当前的一个限制是它不调用 _on_pre_actor_method 和 _on_post_actor_method 方法。您始终可以手动调用这些方法作为测试的一部分。
__init__、register_timer、unregister_timer、register_reminder、unregister_reminder 方法都会被 MockActor 类覆盖,该类通过 create_mock_actor 作为 mixin 应用。如果您的 actor 本身覆盖了这些方法,这些修改本身将被覆盖,actor 可能不会按您的预期运行。
注意:__init__ 是一个特殊情况,您应该将其定义为
def __init__(self, ctx, actor_id):
super().__init__(ctx, actor_id)
Mock actor 可以正常工作,但如果您在 __init__ 中添加了任何额外逻辑,它将被覆盖。值得注意的是,在初始化时应用逻辑的正确方法是通过 _on_activate(也可以与 mock actor 安全使用),而不是 __init__。
如果您有一个确实覆盖了默认 Dapr actor 方法的 actor,您可以创建 MockActor 类(来自 MockActor.py)的自定义子类,该类实现您拥有的任何自定义逻辑,同时与 _mock_state、_mock_timers 和 _mock_reminders 正常交互,然后通过您自己定义的 create_mock_actor 函数将该自定义类作为 mixin 应用。
Actor _runtime_ctx 变量设置为 None。所有正常的 actor 方法都已覆盖,不会调用它,但如果您的代码本身直接与 _runtime_ctx 交互,测试可能会失败。
Actor _state_manager 被 MockStateManager 实例覆盖。它具有与基本 ActorStateManager 相同的所有方法和功能,除了使用各种 _mock 变量而不是 _runtime_ctx 来存储数据。如果您的代码实现了自己的自定义状态管理器,它将被覆盖,测试可能会失败。
类型提示
由于 Python 缺乏用于类型提示类型交集的统一方法(参见:python/typing #213),类型提示不幸地不适用于 Mock Actor。返回类型被类型提示为"Actor 子类 T 的实例",而实际上应该被类型提示为"MockActor 子类 T 的实例"或"类型交集 [Actor 子类 T, MockActor] 的实例"(值得注意的是,MockActor 本身是 Actor 的子类)。
这意味着,例如,如果您在代码编辑器中将鼠标悬停在 mockactor._state_manager 上,它将显示为 ActorStateManager 的实例(而不是 MockStateManager),各种 IDE 辅助功能(如 VSCode 的 Go to Definition,它会将您带到 ActorStateManager 的定义而不是 MockStateManager)将无法正常工作。
目前,这个问题无法修复,因此仅仅需要注意它,因为它可能会引起混淆。如果将来能够准确地为这种情况进行类型提示,欢迎随时打开关于实现它的问题。
2.6.3 - Dapr Python SDK 扩展
2.6.3.1 - Dapr Python gRPC 服务扩展入门
Dapr Python SDK 提供了一个内置的 gRPC 服务器扩展 dapr.ext.grpc,用于创建 Dapr 服务。
安装
您可以通过以下命令下载并安装 Dapr gRPC 服务器扩展:
pip install dapr-ext-grpc
注意
开发包包含的功能和行为将与 Dapr 运行时的预发布版本兼容。在安装 <code>dapr-dev</code> 包之前,请确保卸载 Python SDK 扩展的任何稳定版本。
pip3 install dapr-ext-grpc-dev
示例
App 对象可用于创建服务器。
监听服务调用请求
InvokeMethodReqest 和 InvokeMethodResponse 对象可用于处理传入请求。
一个简单的监听并响应请求的服务如下所示:
from dapr.ext.grpc import App, InvokeMethodRequest, InvokeMethodResponse
app = App()
@app.method(name='my-method')
def mymethod(request: InvokeMethodRequest) -> InvokeMethodResponse:
print(request.metadata, flush=True)
print(request.text(), flush=True)
return InvokeMethodResponse(b'INVOKE_RECEIVED', "text/plain; charset=UTF-8")
app.run(50051)
完整示例可在此处找到。
订阅主题
在订阅主题时,您可以指示 Dapr 已接受传递的事件,还是应该丢弃该事件或稍后重试。
from typing import Optional
from cloudevents.sdk.event import v1
from dapr.ext.grpc import App
from dapr.clients.grpc._response import TopicEventResponse
app = App()
# 主题的默认订阅
@app.subscribe(pubsub_name='pubsub', topic='TOPIC_A')
def mytopic(event: v1.Event) -> Optional[TopicEventResponse]:
print(event.Data(),flush=True)
# 返回 None(或不显式返回)等效于
# 返回 TopicEventResponse("success")。
# 您也可以返回 TopicEventResponse("retry") 让 dapr 记录
# 该消息并稍后重试传递,或返回 TopicEventResponse("drop")
# 让其丢弃该消息
return TopicEventResponse("success")
# 使用发布订阅路由的特定处理程序
@app.subscribe(pubsub_name='pubsub', topic='TOPIC_A',
rule=Rule("event.type == \"important\"", 1))
def mytopic_important(event: v1.Event) -> None:
print(event.Data(),flush=True)
# 禁用主题验证的处理程序
@app.subscribe(pubsub_name='pubsub-mqtt', topic='topic/#', disable_topic_validation=True,)
def mytopic_wildcard(event: v1.Event) -> None:
print(event.Data(),flush=True)
app.run(50051)
完整示例可在此处找到。
设置输入绑定触发器
from dapr.ext.grpc import App, BindingRequest
app = App()
@app.binding('kafkaBinding')
def binding(request: BindingRequest):
print(request.text(), flush=True)
app.run(50051)
完整示例可在此处找到。
相关链接
2.6.3.2 - Dapr Python SDK 与 FastAPI 集成
Dapr Python SDK 通过 dapr-ext-fastapi 扩展提供与 FastAPI 的集成。
安装
您可以通过以下命令下载并安装 Dapr FastAPI 扩展:
pip install dapr-ext-fastapi
注意
开发版包将包含与 Dapr 运行时预发布版本兼容的功能和行为。在安装 <code>dapr-dev</code> 包之前,请确保卸载任何稳定版本的 Python SDK 扩展。
pip install dapr-ext-fastapi-dev
示例
订阅不同类型的事件
import uvicorn
from fastapi import Body, FastAPI
from dapr.ext.fastapi import DaprApp
from pydantic import BaseModel
class RawEventModel(BaseModel):
body: str
class User(BaseModel):
id: int
name: str
class CloudEventModel(BaseModel):
data: User
datacontenttype: str
id: str
pubsubname: str
source: str
specversion: str
topic: str
traceid: str
traceparent: str
tracestate: str
type: str
app = FastAPI()
dapr_app = DaprApp(app)
# 允许处理任何结构的事件(最简单,但最不健壮)
# dapr publish --publish-app-id sample --topic any_topic --pubsub pubsub --data '{"id":"7", "desc": "good", "size":"small"}'
@dapr_app.subscribe(pubsub='pubsub', topic='any_topic')
def any_event_handler(event_data = Body()):
print(event_data)
# 为了健壮性,根据发布者是否使用 CloudEvents 选择以下方式之一
# 处理使用 CloudEvents 发送的事件
# dapr publish --publish-app-id sample --topic cloud_topic --pubsub pubsub --data '{"id":"7", "name":"Bob Jones"}'
@dapr_app.subscribe(pubsub='pubsub', topic='cloud_topic')
def cloud_event_handler(event_data: CloudEventModel):
print(event_data)
# 处理不使用 CloudEvents 发送的原始事件
# curl -X "POST" http://localhost:3500/v1.0/publish/pubsub/raw_topic?metadata.rawPayload=true -H "Content-Type: application/json" -d '{"body": "345"}'
@dapr_app.subscribe(pubsub='pubsub', topic='raw_topic')
def raw_event_handler(event_data: RawEventModel):
print(event_data)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=30212)
创建 actor
from fastapi import FastAPI
from dapr.ext.fastapi import DaprActor
from demo_actor import DemoActor
app = FastAPI(title=f'{DemoActor.__name__}Service')
# 添加 Dapr Actor 扩展
actor = DaprActor(app)
@app.on_event("startup")
async def startup_event():
# 注册 DemoActor
await actor.register_actor(DemoActor)
@app.get("/GetMyData")
def get_my_data():
return "{'message': 'myData'}"
2.6.3.3 - Dapr Python SDK 与 Flask 集成
Dapr Python SDK 通过 flask-dapr 扩展提供与 Flask 的集成。
安装
你可以使用以下命令下载并安装 Dapr Flask 扩展:
pip install flask-dapr
注意
开发包将包含与 Dapr runtime 预发布版本兼容的功能和行为。在安装 <code>dapr-dev</code> 包之前,请确保卸载任何稳定版本的 Python SDK 扩展。
pip install flask-dapr-dev
示例
from flask import Flask
from flask_dapr.actor import DaprActor
from dapr.conf import settings
from demo_actor import DemoActor
app = Flask(f'{DemoActor.__name__}Service')
# 启用 DaprActor Flask 扩展
actor = DaprActor(app)
# 注册 DemoActor
actor.register_actor(DemoActor)
# 设置方法路由
@app.route('/GetMyData', methods=['GET'])
def get_my_data():
return {'message': 'myData'}, 200
# 运行应用
if __name__ == '__main__':
app.run(port=settings.HTTP_APP_PORT)
2.6.3.4 - Dapr Python SDK 与 Dapr 工作流扩展集成
Dapr Python SDK 提供了一个内置的 Dapr 工作流扩展 dapr.ext.workflow,用于创建 Dapr 服务。
安装
您可以通过以下命令下载并安装 Dapr 工作流扩展:
pip install dapr-ext-workflow
Note
开发版包将包含与 Dapr 运行时预发布版本兼容的功能和行为。在安装 <code>dapr-dev</code> 包之前,请确保已卸载任何稳定版本的 Python SDK 扩展。
pip install dapr-ext-workflow-dev
示例
from time import sleep
import dapr.ext.workflow as wf
wfr = wf.WorkflowRuntime()
@wfr.workflow(name='random_workflow')
def task_chain_workflow(ctx: wf.DaprWorkflowContext, wf_input: int):
try:
result1 = yield ctx.call_activity(step1, input=wf_input)
result2 = yield ctx.call_activity(step2, input=result1)
except Exception as e:
yield ctx.call_activity(error_handler, input=str(e))
raise
return [result1, result2]
@wfr.activity(name='step1')
def step1(ctx, activity_input):
print(f'Step 1: Received input: {activity_input}.')
# Do some work
return activity_input + 1
@wfr.activity
def step2(ctx, activity_input):
print(f'Step 2: Received input: {activity_input}.')
# Do some work
return activity_input * 2
@wfr.activity
def error_handler(ctx, error):
print(f'Executing error handler: {error}.')
# Do some compensating work
if __name__ == '__main__':
wfr.start()
sleep(10) # wait for workflow runtime to start
wf_client = wf.DaprWorkflowClient()
instance_id = wf_client.schedule_new_workflow(workflow=task_chain_workflow, input=42)
print(f'Workflow started. Instance ID: {instance_id}')
state = wf_client.wait_for_workflow_completion(instance_id)
print(f'Workflow completed! Status: {state.runtime_status}')
wfr.shutdown()
- 了解有关编写和管理工作流的更多信息:
- 访问 Python SDK 示例 获取代码示例和尝试 Dapr 工作流的说明:
后续步骤
Dapr 工作流 Python SDK 入门2.6.3.4.1 - Dapr Workflow Python SDK 入门
让我们创建一个 Dapr 工作流并通过控制台调用它。借助提供的工作流示例,你将:
- 运行一个 Python 控制台应用程序,该程序演示包含活动、子工作流和外部事件的工作流编排
- 了解如何处理重试、超时以及工作流状态管理
- 使用 Python 工作流 SDK 来启动、暂停、恢复和清理工作流实例
本示例使用自托管模式下通过 dapr init 初始化的默认配置。
在 Python 示例项目中,simple.py 文件包含应用程序的设置,包括:
- 工作流定义
- 工作流活动定义
- 工作流和工作流活动的注册
前置条件
- 已安装 Dapr CLI
- 已初始化 Dapr 环境
- 已安装 Python 3.9+
- 已安装 Dapr Python 包和工作流扩展
- 验证你使用的是最新的 proto 绑定
设置环境
首先克隆 [Python SDK 仓库]。
git clone https://github.com/dapr/python-sdk.git
从 Python SDK 根目录导航到 Dapr Workflow 示例。
cd examples/workflow
运行以下命令,安装使用 Dapr Python SDK 运行此工作流示例所需的所有依赖。
pip3 install -r workflow/requirements.txt
在本地运行应用程序
要运行 Dapr 应用程序,你需要启动 Python 程序和一个 Dapr 边车。在终端中运行:
dapr run --app-id wf-simple-example --dapr-grpc-port 50001 --resources-path components -- python3 simple.py
注意: 由于 Windows 上未定义 Python3.exe,你可能需要使用
python simple.py而不是python3 simple.py。
预期输出
- "== APP == Hi Counter!"
- "== APP == New counter value is: 1!"
- "== APP == New counter value is: 11!"
- "== APP == Retry count value is: 0!"
- "== APP == Retry count value is: 1! This print statement verifies retry"
- "== APP == Appending 1 to child_orchestrator_string!"
- "== APP == Appending a to child_orchestrator_string!"
- "== APP == Appending a to child_orchestrator_string!"
- "== APP == Appending 2 to child_orchestrator_string!"
- "== APP == Appending b to child_orchestrator_string!"
- "== APP == Appending b to child_orchestrator_string!"
- "== APP == Appending 3 to child_orchestrator_string!"
- "== APP == Appending c to child_orchestrator_string!"
- "== APP == Appending c to child_orchestrator_string!"
- "== APP == Get response from hello_world_wf after pause call: Suspended"
- "== APP == Get response from hello_world_wf after resume call: Running"
- "== APP == New counter value is: 111!"
- "== APP == New counter value is: 1111!"
- "== APP == Workflow completed! Result: "Completed"
发生了什么?
当你运行应用程序时,会演示几个关键的工作流功能:
工作流和活动注册:应用程序使用 Python 装饰器自动向运行时注册工作流和活动。这种基于装饰器的方法提供了一种简洁、声明式的方式来定义你的工作流组件:
@wfr.workflow(name='hello_world_wf') def hello_world_wf(ctx: DaprWorkflowContext, wf_input): # Workflow definition... @wfr.activity(name='hello_act') def hello_act(ctx: WorkflowActivityContext, wf_input): # Activity definition...运行时设置:应用程序初始化工作流运行时和客户端:
wfr = WorkflowRuntime() wfr.start() wf_client = DaprWorkflowClient()活动执行:工作流执行一系列活动来递增计数器:
@wfr.workflow(name='hello_world_wf') def hello_world_wf(ctx: DaprWorkflowContext, wf_input): yield ctx.call_activity(hello_act, input=1) yield ctx.call_activity(hello_act, input=10)重试逻辑:工作流演示了使用重试策略进行错误处理:
retry_policy = RetryPolicy( first_retry_interval=timedelta(seconds=1), max_number_of_attempts=3, backoff_coefficient=2, max_retry_interval=timedelta(seconds=10), retry_timeout=timedelta(seconds=100), ) yield ctx.call_activity(hello_retryable_act, retry_policy=retry_policy)子工作流:子工作流使用自己的重试策略执行:
yield ctx.call_child_workflow(child_retryable_wf, retry_policy=retry_policy)外部事件处理:工作流等待一个带有超时的外部事件:
event = ctx.wait_for_external_event(event_name) timeout = ctx.create_timer(timedelta(seconds=30)) winner = yield when_any([event, timeout])工作流生命周期管理:示例演示如何暂停和恢复工作流:
wf_client.pause_workflow(instance_id=instance_id) metadata = wf_client.get_workflow_state(instance_id=instance_id) # ... check status ... wf_client.resume_workflow(instance_id=instance_id)事件触发:恢复后,工作流触发一个事件:
wf_client.raise_workflow_event( instance_id=instance_id, event_name=event_name, data=event_data )完成与清理:最后,工作流等待完成并进行清理:
state = wf_client.wait_for_workflow_completion( instance_id, timeout_in_seconds=30 ) wf_client.purge_workflow(instance_id=instance_id)
后续步骤
2.6.4 -
title: “Conversation API(Python)- 推荐用法” linkTitle: “对话” weight: 11000 type: docs description: 推荐在 Python 中配合或不配合工具使用 Dapr Conversation API 的模式,包括多轮流程与安全指引。
Dapr Conversation API 当前仍处于 alpha 阶段。本文给出在 Python SDK 中高效使用它的推荐最小模式:
- 纯请求(无工具)
- 携带工具的请求(将函数用作工具)
- 带工具执行的多轮流程
- 异步变体
- 执行工具调用时的重要安全注意事项
前提条件
- 已安装 Dapr CLI
- 已初始化 Dapr 环境
- 已安装 Python 3.9+
- 已安装 Dapr Python 包
- 已在 Dapr 环境中配置好 LLM 组件(例如 OpenAI 或 Azure OpenAI)
如需完整的端到端流程与提供程序配置,参见:
- Conversation 相关 SDK 示例:
基础对话(无工具)
from dapr.clients import DaprClient
from dapr.clients.grpc import conversation
# 构建单轮 Alpha2 输入
user_msg = conversation.create_user_message("什么是 Dapr?")
alpha2_input = conversation.ConversationInputAlpha2(messages=[user_msg])
with DaprClient() as client:
resp = client.converse_alpha2(
name="echo", # 替换为你的 LLM 组件名称
inputs=[alpha2_input],
temperature=1,
)
for msg in resp.to_assistant_messages():
if msg.of_assistant.content:
print(msg.of_assistant.content[0].text)
要点:
- 使用
conversation.create_user_message构造消息。 - 将其包进
ConversationInputAlpha2(messages=[...]),再传给converse_alpha2。 - 使用
response.to_assistant_messages()遍历 assistant 输出。
工具:基于装饰器(推荐)
基于装饰器的工具方式更整洁、也更顺手。定义函数时应写清晰的类型提示与详细 docstring;这对 LLM 理解何时以及如何调用该工具很重要;
然后使用 @conversation.tool 进行装饰。注册后的工具可以传给 LLM,并通过工具调用来执行。
from dapr.clients import DaprClient
from dapr.clients.grpc import conversation
@conversation.tool
def get_weather(location: str, unit: str = 'fahrenheit') -> str:
"""获取指定地点的当前天气。"""
# 请替换为真实实现
return f"Weather in {location} (unit={unit})"
user_msg = conversation.create_user_message("巴黎的天气怎么样?")
alpha2_input = conversation.ConversationInputAlpha2(messages=[user_msg])
with DaprClient() as client:
response = client.converse_alpha2(
name="openai", # 你的 LLM 组件
inputs=[alpha2_input],
tools=conversation.get_registered_tools(), # 由 @conversation.tool 注册的工具
tool_choice='auto',
temperature=1,
)
# 检查 assistant 消息,包括其中的工具调用
for msg in response.to_assistant_messages():
if msg.of_assistant.tool_calls:
for tc in msg.of_assistant.tool_calls:
print(f"Tool call: {tc.function.name} args={tc.function.arguments}")
elif msg.of_assistant.content:
print(msg.of_assistant.content[0].text)
说明:
- 使用
conversation.get_registered_tools()收集所有通过@conversation.tool装饰的函数。 - 绑定器会依据函数签名校验并强制转换参数。类型注解要准确。
使用工具的最小多轮流程
这是处理带工具对话时最常用的循环:
警告
不要在不信任所有已注册工具的情况下,盲目自动执行 LLM 返回的工具调用。工具名和参数都应视为不可信输入。
- 请校验输入并施加防护(工具白名单、参数 schema、副作用约束)。
- 对异步或 I/O 密集型工具,优先使用
conversation.execute_registered_tool_async(..., timeout=...),并设置保守的超时时间。 - 在敏感场景中,考虑在执行前增加策略层或用户确认步骤。
- 记录并监控工具使用;当校验失败时应默认拒绝执行。
from dapr.clients import DaprClient
from dapr.clients.grpc import conversation
@conversation.tool
def get_weather(location: str, unit: str = 'fahrenheit') -> str:
return f"Weather in {location} (unit={unit})"
history: list[conversation.ConversationMessage] = [
conversation.create_user_message("旧金山的天气怎么样?")]
with DaprClient() as client:
# 第 1 轮
resp1 = client.converse_alpha2(
name="openai",
inputs=[conversation.ConversationInputAlpha2(messages=history)],
tools=conversation.get_registered_tools(),
tool_choice='auto',
temperature=1,
)
# 追加 assistant 消息;执行工具调用;再追加工具结果
for msg in resp1.to_assistant_messages():
history.append(msg)
for tc in msg.of_assistant.tool_calls:
# 重要:生产环境中必须校验输入并施加防护
tool_output = conversation.execute_registered_tool(
tc.function.name, tc.function.arguments
)
history.append(
conversation.create_tool_message(
tool_id=tc.id, name=tc.function.name, content=str(tool_output)
)
)
# 第 2 轮(LLM 会看到工具结果)
history.append(conversation.create_user_message("我需要带伞吗?"))
resp2 = client.converse_alpha2(
name="openai",
inputs=[conversation.ConversationInputAlpha2(messages=history)],
tools=conversation.get_registered_tools(),
temperature=1,
)
for msg in resp2.to_assistant_messages():
history.append(msg)
if not msg.of_assistant.tool_calls and msg.of_assistant.content:
print(msg.of_assistant.content[0].text)
提示:
- 始终把 assistant 消息追加到 history。
- 执行每个工具调用时,都要先校验,再把工具输出作为 tool message 追加进去。
- 下一轮请求会带上这些工具结果,LLM 才能基于它们继续推理。
将函数用作工具:其他方式
当装饰器方式不方便时,还有两种选择。
A)从带类型的函数自动生成 schema:
from enum import Enum
from dapr.clients.grpc import conversation
class Units(Enum):
CELSIUS = 'celsius'
FAHRENHEIT = 'fahrenheit'
def get_weather(location: str, unit: Units = Units.FAHRENHEIT) -> str:
return f"Weather in {location}"
fn = conversation.ConversationToolsFunction.from_function(get_weather)
weather_tool = conversation.ConversationTools(function=fn)
B)手写 JSON Schema(兜底方式):
from dapr.clients.grpc import conversation
fn = conversation.ConversationToolsFunction(
name='get_weather',
description='Get current weather',
parameters={
'type': 'object',
'properties': {
'location': {'type': 'string'},
'unit': {'type': 'string', 'enum': ['celsius', 'fahrenheit']},
},
'required': ['location'],
},
)
weather_tool = conversation.ConversationTools(function=fn)
异步变体
按需使用异步客户端与异步工具执行辅助方法。
import asyncio
from dapr.aio.clients import DaprClient as AsyncDaprClient
from dapr.clients.grpc import conversation
@conversation.tool
def get_time() -> str:
return '2025-01-01T12:00:00Z'
async def main():
async with AsyncDaprClient() as client:
msg = conversation.create_user_message('现在几点?')
inp = conversation.ConversationInputAlpha2(messages=[msg])
resp = await client.converse_alpha2(
name='openai', inputs=[inp], tools=conversation.get_registered_tools()
)
for m in resp.to_assistant_messages():
if m.of_assistant.content:
print(m.of_assistant.content[0].text)
asyncio.run(main())
如果你需要异步执行工具(例如网络 I/O),请实现异步函数,并结合超时参数使用 conversation.execute_registered_tool_async。
安全与验证(必读)
LLM 可能会建议调用工具。所有由模型提供的参数都必须视为不可信输入。
建议:
- 仅将可信函数注册为工具。为清晰性与自动 schema 生成,优先使用
@conversation.tool装饰器。 - 使用精确的类型注解和 docstring。SDK 会把函数签名转换成 JSON schema,并在参数绑定时执行类型强制转换,同时拒绝意外或无效字段。
- 对可能产生副作用的工具(文件系统、网络、子进程)添加防护。可考虑白名单、沙箱和配额限制。
- 执行前校验参数。例如清理文件路径,或限制 URL / 域名范围。
- 考虑超时和并发控制。对于异步工具,可向
execute_registered_tool_async(..., timeout=...)传入超时。 - 记录并监控工具使用。默认拒绝:若校验失败,就不要执行工具,并以安全方式告知用户。
另请参阅 dapr/clients/grpc/conversation.py 中的内联说明(如 tool()、ConversationTools、execute_registered_tool),了解参数绑定与错误处理细节。
关键辅助方法(速查)
本节汇总了示例中使用到的 dapr.clients.grpc.conversation 辅助工具。
create_user_message(text: str) -> ConversationMessage
- 为 Alpha2 构建 user 角色消息。可用于 history 列表。
- 示例:
history.append(conversation.create_user_message("Hello"))
create_system_message(text: str) -> ConversationMessage
- 构建 system 消息,用于约束 assistant 的行为。
- 示例:
history = [conversation.create_system_message("You are a concise assistant.")]
create_assistant_message(text: str) -> ConversationMessage
- 适合在测试或受控流程中注入 assistant 文本。
create_tool_message(tool_id: str, name: str, content: Any) -> ConversationMessage
- 将工具输出转换为下一轮可供 LLM 读取的 tool message。
- content 可以是任意对象;SDK 会安全地将其转为字符串。
- 示例:
history.append(conversation.create_tool_message(tool_id=tc.id, name=tc.function.name, content=conversation.execute_registered_tool(tc.function.name, tc.function.arguments)))
get_registered_tools() -> list[ConversationTools]
- 返回当前进程内注册表中的全部工具。
- 包括以下方式创建的工具:
@conversation.tool装饰器(默认自动注册),以及ConversationToolsFunction.from_function且register=True(默认)。
- 在
converse_alpha2(..., tools=...)中传入该列表。
register_tool(name: str, t: ConversationTools) / unregister_tool(name: str)
- 手动管理工具注册表(例如高级场景、测试、清理)。
- 名称必须唯一;在长生命周期进程中,记得注销以避免冲突。
execute_registered_tool(name: str, params: Mapping|Sequence|str|None) -> Any
- 按名称同步执行已注册工具。
- params 可接受 kwargs(mapping)、args(sequence)、JSON 字符串或 None。若传入 JSON 字符串(LLM 常见返回形式),SDK 会自动解析。
- 参数会依据函数签名或 schema 进行校验与强制转换;多余或无效字段会直接报错。
- 安全性:params 必须视为不可信输入;对副作用操作要加防护。
execute_registered_tool_async(name: str, params: Mapping|Sequence|str|None, *, timeout: float|None=None) -> Any
- 异步版本。支持超时;对 I/O 密集型工具尤其建议使用。
- 适用于异步工具,或配合 aio 客户端使用。
ConversationToolsFunction.from_function(func: Callable, register: bool = True) -> ConversationToolsFunction
- 从带类型的 Python 函数(注解 + 可选 docstring)推导 JSON schema,并可选择直接注册为工具。
- 常见用法:
spec = conversation.ConversationToolsFunction.from_function(my_func);随后可依赖自动注册,也可用ConversationTools(function=spec)包装后调用register_tool(spec.name, tool),或直接把[tool]传给tools=。
ConversationResponseAlpha2.to_assistant_messages() -> list[ConversationMessage]
- 便捷方法,用于把响应输出转换为 assistant 的
ConversationMessage对象,便于直接追加到 history(包括存在的tool_calls)。
- 便捷方法,用于把响应输出转换为 assistant 的
提示:@conversation.tool 装饰器是创建工具最简单的方式。它会根据函数自动生成 schema,支持可选的命名空间或名称覆盖,并自动注册工具(若想延后注册,可设置 register=False)。
2.7 - Dapr Rust SDK
Note
Dapr Rust SDK 目前处于 Alpha 阶段。目前正在努力将其推向稳定版本,可能会涉及破坏性变更。一个帮助使用 Rust 构建 Dapr 应用程序的客户端库。该客户端旨在支持所有公共 Dapr API,同时专注于惯用的 Rust 体验和开发者生产力。
2.7.1 - 开始使用 Dapr 客户端 Rust SDK
Dapr 客户端包允许您从 Rust 应用程序与其他 Dapr 应用程序进行交互。
注意
Dapr Rust SDK 目前处于 Alpha 阶段。我们正在努力将其推向稳定版本,期间可能会涉及破坏性变更。前置条件
导入客户端包
将 Dapr 添加到您的 cargo.toml
[dependencies]
dapr = "0.16"
您可以直接引用 dapr::Client 或将完整路径绑定到新名称,如下所示:
use dapr::Client as DaprClient;
实例化 Dapr 客户端
let addr = "https://127.0.0.1".to_string();
let mut client = dapr::Client::<dapr::client::TonicClient>::connect(addr,
port).await?;
或者,如果您想指定自定义端口,可以使用此 connect 方法:
let mut client = dapr::Client::<dapr::client::TonicClient>::connect_with_port(addr, "3500".to_string()).await?;
构建块
Rust SDK 允许您与 Dapr 构建块 进行交互。
服务调用 (gRPC)
要在运行 Dapr 边车的另一个服务上调用特定方法,Dapr 客户端提供两个选项:
调用 (gRPC) 服务
let response = client
.invoke_service("service-to-invoke", "method-to-invoke", Some(data))
.await
.unwrap();
有关服务调用的完整指南,请访问如何操作:调用服务。
状态管理
Dapr 客户端提供对这些状态管理方法的访问:save_state、get_state、delete_state,可以像这样使用:
let store_name = String::from("statestore");
let key = String::from("hello");
let val = String::from("world").into_bytes();
// save key-value pair in the state store
client
.save_state(store_name, key, val, None, None, None)
.await?;
let get_response = client
.get_state("statestore", "hello", None)
.await?;
// delete a value from the state store
client
.delete_state("statestore", "hello", None)
.await?;
可以使用 save_bulk_states 方法发送多个状态。
有关状态管理的完整指南,请访问如何操作:保存和获取状态。
发布消息
要将数据发布到主题,Dapr 客户端提供了一个简单的方法:
let pubsub_name = "pubsub-name".to_string();
let pubsub_topic = "topic-name".to_string();
let pubsub_content_type = "text/plain".to_string();
let data = "content".to_string().into_bytes();
client
.publish_event(pubsub_name, pubsub_topic, pubsub_content_type, data, None)
.await?;
有关发布订阅的完整指南,请访问如何操作:发布和订阅。
相关链接
3 - 错误码
3.1 - 错误概述
错误码是一个数字或字母数字代码,用于指示错误的性质,并在可能的情况下说明错误发生的原因。
Dapr 错误码是使用 Dapr API 进行 HTTP 和 gRPC 请求时 80 多种常见错误的标准化字符串。这些代码同时具备以下功能:
- 在请求的 JSON 响应体中返回。
- 启用后,在运行时日志中以 debug 级别记录。
- 如果在 Kubernetes 中运行,错误码会记录在边车中。
- 如果在自托管模式下运行,你可以启用并运行 debug 日志。
错误格式
Dapr 错误码由前缀、类别和错误简写组成。例如:
| 前缀 | 类别 | 错误简写 |
|---|---|---|
| ERR_ | PUBSUB_ | NOT_FOUND |
一些最常见的返回错误包括:
- ERR_ACTOR_TIMER_CREATE
- ERR_PURGE_WORKFLOW
- ERR_STATE_STORE_NOT_FOUND
- ERR_HEALTH_NOT_READY
状态存储未找到时返回的错误可能如下所示:
{
"error": "Bad Request",
"error_msg": "{\"errorCode\":\"ERR_STATE_STORE_NOT_FOUND\",\"message\":\"state store <name> is not found\",\"details\":[{\"@type\":\"type.googleapis.com/google.rpc.ErrorInfo\",\"domain\":\"dapr.io\",\"metadata\":{\"appID\":\"nodeapp\"},\"reason\":\"DAPR_STATE_NOT_FOUND\"}]}",
"status": 400
}
返回的错误包含:
- 错误码:
ERR_STATE_STORE_NOT_FOUND - 描述问题的错误消息:
state store <name> is not found - 发生错误的应用程序 ID:
nodeapp - 错误原因:
DAPR_STATE_NOT_FOUND
Dapr 错误码指标
指标帮助你查看运行时内部错误发生的具体时间。错误码指标通过 error_code_total 端点收集。该端点默认禁用。你可以通过在配置文件中使用 recordErrorCodes 字段来启用它。
演示
观看在 Diagrid Dapr v1.15 庆祝活动上呈现的演示,了解如何启用错误码指标以及如何处理运行时中返回的错误码。
下一步
查看所有 Dapr 错误码列表3.2 - 错误码参考指南
以下表格列出了 Dapr 运行时返回的错误码。
错误码在 HTTP 请求的响应体中返回,或在 gRPC 状态响应的 ErrorInfo 部分中返回(如果存在)。
一项正在推进的工作是,根据 Richer Error Model 丰富所有 gRPC 错误响应。尚无对应 gRPC 代码的错误码表示这些错误尚未更新到此模型。
Actors API
| HTTP Code | gRPC Code | Description |
|---|---|---|
ERR_ACTOR_INSTANCE_MISSING | 缺少 actor 实例 | |
ERR_ACTOR_INVOKE_METHOD | 调用 actor 方法时出错 | |
ERR_ACTOR_RUNTIME_NOT_FOUND | 未找到 actor 运行时 | |
ERR_ACTOR_STATE_GET | 获取 actor 状态时出错 | |
ERR_ACTOR_STATE_TRANSACTION_SAVE | 保存 actor 事务时出错 | |
ERR_ACTOR_REMINDER_CREATE | 创建 actor reminder 时出错 | |
ERR_ACTOR_REMINDER_DELETE | 删除 actor reminder 时出错 | |
ERR_ACTOR_REMINDER_GET | 获取 actor reminder 时出错 | |
ERR_ACTOR_REMINDER_NON_HOSTED | 在非托管 actor 类型上执行 reminder 操作 | |
ERR_ACTOR_TIMER_CREATE | 创建 actor timer 时出错 | |
ERR_ACTOR_NO_APP_CHANNEL | 应用通道未初始化 | |
ERR_ACTOR_STACK_DEPTH | 超出最大 actor 调用栈深度 | |
ERR_ACTOR_NO_PLACEMENT | 未配置 Placement 服务 | |
ERR_ACTOR_RUNTIME_CLOSED | Actor 运行时已关闭 | |
ERR_ACTOR_NAMESPACE_REQUIRED | 在 Kubernetes 模式下运行时,actor 必须配置命名空间 | |
ERR_ACTOR_NO_ADDRESS | 未找到 actor 的地址 |
工作流 API
| HTTP Code | gRPC Code | Description |
|---|---|---|
ERR_GET_WORKFLOW | 获取工作流时出错 | |
ERR_START_WORKFLOW | 启动工作流时出错 | |
ERR_PAUSE_WORKFLOW | 暂停工作流时出错 | |
ERR_RESUME_WORKFLOW | 恢复工作流时出错 | |
ERR_TERMINATE_WORKFLOW | 终止工作流时出错 | |
ERR_PURGE_WORKFLOW | 清除工作流时出错 | |
ERR_RAISE_EVENT_WORKFLOW | 在工作流中引发事件时出错 | |
ERR_WORKFLOW_COMPONENT_MISSING | 缺少工作流组件 | |
ERR_WORKFLOW_COMPONENT_NOT_FOUND | 未找到工作流组件 | |
ERR_WORKFLOW_EVENT_NAME_MISSING | 缺少工作流事件名称 | |
ERR_WORKFLOW_NAME_MISSING | 未配置工作流名称 | |
ERR_INSTANCE_ID_INVALID | 工作流实例 ID 无效。(仅允许字母数字和下划线字符) | |
ERR_INSTANCE_ID_NOT_FOUND | 未找到工作流实例 ID | |
ERR_INSTANCE_ID_PROVIDED_MISSING | 缺少工作流实例 ID | |
ERR_INSTANCE_ID_TOO_LONG | 工作流实例 ID 过长 |
状态管理 API
| HTTP Code | gRPC Code | Description |
|---|---|---|
ERR_STATE_TRANSACTION | 状态事务出错 | |
ERR_STATE_SAVE | 保存状态时出错 | |
ERR_STATE_GET | 获取状态时出错 | |
ERR_STATE_DELETE | 删除状态时出错 | |
ERR_STATE_BULK_DELETE | 批量删除状态时出错 | |
ERR_STATE_BULK_GET | 批量获取状态时出错 | |
ERR_NOT_SUPPORTED_STATE_OPERATION | 事务中不支持该操作 | |
ERR_STATE_QUERY | DAPR_STATE_QUERY_FAILED | 查询状态时出错 |
ERR_STATE_STORE_NOT_FOUND | DAPR_STATE_NOT_FOUND | 未找到状态存储 |
ERR_STATE_STORE_NOT_CONFIGURED | DAPR_STATE_NOT_CONFIGURED | 状态存储未配置 |
ERR_STATE_STORE_NOT_SUPPORTED | DAPR_STATE_TRANSACTIONS_NOT_SUPPORTED | 状态存储不支持事务 |
ERR_STATE_STORE_NOT_SUPPORTED | DAPR_STATE_QUERYING_NOT_SUPPORTED | 状态存储不支持查询 |
ERR_STATE_STORE_TOO_MANY_TRANSACTIONS | DAPR_STATE_TOO_MANY_TRANSACTIONS | 单个事务中的操作数过多 |
ERR_MALFORMED_REQUEST | DAPR_STATE_ILLEGAL_KEY | 键无效 |
Configuration API
| HTTP Code | gRPC Code | Description |
|---|---|---|
ERR_CONFIGURATION_GET | 获取配置时出错 | |
ERR_CONFIGURATION_STORE_NOT_CONFIGURED | 配置存储未配置 | |
ERR_CONFIGURATION_STORE_NOT_FOUND | 未找到配置存储 | |
ERR_CONFIGURATION_SUBSCRIBE | 订阅配置时出错 | |
ERR_CONFIGURATION_UNSUBSCRIBE | 取消订阅配置时出错 |
密码学 API
| HTTP Code | gRPC Code | Description |
|---|---|---|
ERR_CRYPTO | 密码学操作出错 | |
ERR_CRYPTO_KEY | 获取密码学密钥时出错 | |
ERR_CRYPTO_PROVIDER_NOT_FOUND | 未找到密码学提供程序 | |
ERR_CRYPTO_PROVIDERS_NOT_CONFIGURED | 密码学提供程序未配置 |
Secrets API
| HTTP Code | gRPC Code | Description |
|---|---|---|
ERR_SECRET_GET | 获取 secret 时出错 | |
ERR_SECRET_STORE_NOT_FOUND | 未找到 secret 存储 | |
ERR_SECRET_STORES_NOT_CONFIGURED | Secret 存储未配置 | |
ERR_PERMISSION_DENIED | 策略拒绝权限 |
发布订阅和消息错误
| HTTP Code | gRPC Code | Description |
|---|---|---|
ERR_PUBSUB_EMPTY | DAPR_PUBSUB_NAME_EMPTY | Pubsub 名称为空 |
ERR_PUBSUB_NOT_FOUND | DAPR_PUBSUB_NOT_FOUND | 未找到 Pubsub |
ERR_PUBSUB_NOT_FOUND | DAPR_PUBSUB_TEST_NOT_FOUND | 未找到 Pubsub |
ERR_PUBSUB_NOT_CONFIGURED | DAPR_PUBSUB_NOT_CONFIGURED | Pubsub 未配置 |
ERR_TOPIC_NAME_EMPTY | DAPR_PUBSUB_TOPIC_NAME_EMPTY | 主题名称为空 |
ERR_PUBSUB_FORBIDDEN | DAPR_PUBSUB_FORBIDDEN | 应用 ID 被禁止访问主题 |
ERR_PUBSUB_PUBLISH_MESSAGE | DAPR_PUBSUB_PUBLISH_MESSAGE | 发布消息时出错 |
ERR_PUBSUB_REQUEST_METADATA | DAPR_PUBSUB_METADATA_DESERIALIZATION | 反序列化元数据时出错 |
ERR_PUBSUB_CLOUD_EVENTS_SER | DAPR_PUBSUB_CLOUD_EVENT_CREATION | 创建 CloudEvent 时出错 |
ERR_PUBSUB_EVENTS_SER | DAPR_PUBSUB_MARSHAL_ENVELOPE | 封送 Cloud Event 信封时出错 |
ERR_PUBSUB_EVENTS_SER | DAPR_PUBSUB_MARSHAL_EVENTS | 将事件封送为字节时出错 |
ERR_PUBSUB_EVENTS_SER | DAPR_PUBSUB_UNMARSHAL_EVENTS | 反封送事件时出错 |
ERR_PUBLISH_OUTBOX | 向 outbox 发布消息时出错 |
对话 API
| HTTP Code | gRPC Code | Description |
|---|---|---|
ERR_CONVERSATION_INVALID_PARMS | 对话组件参数无效 | |
ERR_CONVERSATION_INVOKE | 调用对话时出错 | |
ERR_CONVERSATION_MISSING_INPUTS | 缺少对话输入 | |
ERR_CONVERSATION_NOT_FOUND | 未找到对话 |
服务调用 / 直接消息 API
| HTTP Code | gRPC Code | Description |
|---|---|---|
ERR_DIRECT_INVOKE | 调用服务时出错 |
Bindings API
| HTTP Code | gRPC Code | Description |
|---|---|---|
ERR_INVOKE_OUTPUT_BINDING | 调用输出绑定时出错 |
分布式锁 API
| HTTP Code | gRPC Code | Description |
|---|---|---|
ERR_LOCK_STORE_NOT_CONFIGURED | 锁存储未配置 | |
ERR_LOCK_STORE_NOT_FOUND | 未找到锁存储 | |
ERR_TRY_LOCK | 获取锁时出错 | |
ERR_UNLOCK | 释放锁时出错 |
健康检查
| HTTP Code | gRPC Code | Description |
|---|---|---|
ERR_HEALTH_NOT_READY | Dapr 未就绪 | |
ERR_HEALTH_APPID_NOT_MATCH | Dapr App ID 不匹配 | |
ERR_OUTBOUND_HEALTH_NOT_READY | Dapr 出站未就绪 |
通用
| HTTP Code | gRPC Code | Description |
|---|---|---|
ERR_API_UNIMPLEMENTED | API 未实现 | |
ERR_APP_CHANNEL_NIL | 应用通道为 nil | |
ERR_BAD_REQUEST | 错误请求 | |
ERR_BODY_READ | 读取请求体时出错 | |
ERR_INTERNAL | 内部错误 | |
ERR_MALFORMED_REQUEST | 格式错误的请求 | |
ERR_MALFORMED_REQUEST_DATA | 格式错误的请求数据 | |
ERR_MALFORMED_RESPONSE | 格式错误的响应 |
Scheduler/Jobs API
| HTTP Code | gRPC Code | Description |
|---|---|---|
DAPR_SCHEDULER_SCHEDULE_JOB | DAPR_SCHEDULER_SCHEDULE_JOB | 调度任务时出错 |
DAPR_SCHEDULER_JOB_NAME | DAPR_SCHEDULER_JOB_NAME | 作业名称只能设置在 URL 中 |
DAPR_SCHEDULER_JOB_NAME_EMPTY | DAPR_SCHEDULER_JOB_NAME_EMPTY | 作业名称为空 |
DAPR_SCHEDULER_GET_JOB | DAPR_SCHEDULER_GET_JOB | 获取任务时出错 |
DAPR_SCHEDULER_LIST_JOBS | DAPR_SCHEDULER_LIST_JOBS | 列出任务时出错 |
DAPR_SCHEDULER_DELETE_JOB | DAPR_SCHEDULER_DELETE_JOB | 删除任务时出错 |
DAPR_SCHEDULER_EMPTY | DAPR_SCHEDULER_EMPTY | 必需参数为空 |
DAPR_SCHEDULER_SCHEDULE_EMPTY | DAPR_SCHEDULER_SCHEDULE_EMPTY | 未提供任务的调度计划 |
通用
| HTTP Code | gRPC Code | Description |
|---|---|---|
ERROR | ERROR | 通用错误 |
后续步骤
3.3 - 处理 HTTP 错误代码
对于发往 Dapr 运行时的 HTTP 调用,当遇到错误时,会在响应正文中返回一个错误 JSON。该 JSON 包含一个错误代码和一条描述性错误消息。
{
"errorCode": "ERR_STATE_GET",
"message": "Requested state key does not exist in state store."
}
相关
3.4 - 处理 gRPC 错误码
最初,错误遵循标准 gRPC 错误模型。然而,为了提供更详细和更有信息量的错误消息,定义了一个增强的错误模型,该模型与 gRPC 更丰富的错误模型保持一致。
注意
并非所有 Dapr 错误都已转换为更丰富的 gRPC 错误模型。标准 gRPC 错误模型
标准 gRPC 错误模型是 gRPC 中的一种错误报告方法。每个错误响应包含一个错误码和一个错误消息。错误码是标准化的,反映了常见的错误条件。
标准 gRPC 错误响应示例:
ERROR:
Code: InvalidArgument
Message: input key/keyPrefix 'bad||keyname' can't contain '||'
更丰富的 gRPC 错误模型
更丰富的 gRPC 错误模型通过提供有关错误的额外上下文和详细信息来扩展标准错误模型。该模型包括标准的错误 code 和 message,以及一个 details 部分,其中可以包含各种类型的信息,例如 ErrorInfo、ResourceInfo 和 BadRequest 详细信息。
更丰富的 gRPC 错误响应示例:
ERROR:
Code: InvalidArgument
Message: input key/keyPrefix 'bad||keyname' can't contain '||'
Details:
1) {
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"domain": "dapr.io",
"reason": "DAPR_STATE_ILLEGAL_KEY"
}
2) {
"@type": "type.googleapis.com/google.rpc.ResourceInfo",
"resourceName": "statestore",
"resourceType": "state"
}
3) {
"@type": "type.googleapis.com/google.rpc.BadRequest",
"fieldViolations": [
{
"field": "bad||keyname",
"description": "input key/keyPrefix 'bad||keyname' can't contain '||'"
}
]
}
对于 HTTP 客户端,Dapr 将 gRPC 错误模型转换为类似的 JSON 格式结构。响应包括一个 errorCode、一个 message 和一个 details 数组,该数组反映了在更丰富的 gRPC 模型中找到的结构。
HTTP 错误响应示例:
{
"errorCode": "ERR_MALFORMED_REQUEST",
"message": "api error: code = InvalidArgument desc = input key/keyPrefix 'bad||keyname' can't contain '||'",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"domain": "dapr.io",
"metadata": null,
"reason": "DAPR_STATE_ILLEGAL_KEY"
},
{
"@type": "type.googleapis.com/google.rpc.ResourceInfo",
"description": "",
"owner": "",
"resource_name": "statestore",
"resource_type": "state"
},
{
"@type": "type.googleapis.com/google.rpc.BadRequest",
"field_violations": [
{
"field": "bad||keyname",
"description": "api error: code = InvalidArgument desc = input key/keyPrefix 'bad||keyname' can't contain '||'"
}
]
}
]
}
你可以在这里找到所有可能的状态详情的规范。
相关链接
4 - 本地开发
4.1 - IDE 支持
4.1.1 - Visual Studio Code 与 Dapr 集成
4.1.1.1 - Dapr Visual Studio Code 扩展概述
弃用通知
该扩展先前由 Microsoft 支持,但现在已被弃用。该扩展将在 Visual Studio Code marketplace 中保持可用,但将不再接收更新或支持。用于本地开发的已弃用 Dapr Visual Studio Code 扩展 为用户提供了多种功能,以更好地管理其 Dapr 应用程序,并对所有支持的 Dapr 语言(包括 .NET、Go、PHP、Python 和 Java)的应用程序进行调试。
功能
脚手架 Dapr 调试任务
Dapr 扩展帮助你使用 Visual Studio Code 的内置调试功能通过 Dapr 调试应用程序。
使用 Dapr: Scaffold Dapr Tasks 命令面板操作,你可以更新现有的 task.json 和 launch.json 文件,以便在开始调试时启动和配置 Dapr 边车。
- 确保已为你的应用设置了启动配置。(了解更多)
- 使用
Ctrl+Shift+P打开命令面板 - 选择
Dapr: Scaffold Dapr Tasks - 使用
F5或通过运行视图运行你的应用和 Dapr 边车。
脚手架 Dapr 组件
将 Dapr 添加到应用程序时,你可能需要一个专用的组件目录,与作为 dapr init 一部分初始化的默认组件分离。
要创建包含默认 statestore、pubsub 和 zipkin 组件的专用组件文件夹,请使用 Dapr: Scaffold Dapr Components 命令面板操作。
- 在 Visual Studio Code 中打开应用程序目录
- 使用
Ctrl+Shift+P打开命令面板 - 选择
Dapr: Scaffold Dapr Components - 使用
dapr run --resources-path ./components -- ...运行应用程序
查看正在运行的 Dapr 应用程序
应用程序视图显示在本地机器上运行的 Dapr 应用程序。

调用 Dapr 应用程序
在应用程序视图中,用户可以右键单击并通过 GET 或 POST 方法调用 Dapr 应用程序,可选择指定有效负载。

向 Dapr 应用程序发布事件
在应用程序视图中,用户可以右键单击并向正在运行的 Dapr 应用程序发布消息,指定主题和有效负载。
用户还可以向所有正在运行的应用程序发布消息。

其他资源
同时调试多个 Dapr 应用程序
使用 VS Code 扩展,你可以通过多目标调试同时调试多个 Dapr 应用程序。
社区通话演示
观看此视频了解如何使用 Dapr VS Code 扩展:
4.1.1.2 - 如何操作:使用 Visual Studio Code 调试 Dapr 应用程序
弃用通知
该扩展此前由 Microsoft 支持,但现已弃用。该扩展在 Visual Studio Code marketplace 中仍然可用,但将不再接收更新或支持。手动调试
开发 Dapr 应用程序时,通常使用 Dapr CLI 启动 daprized 服务,类似这样:
dapr run --app-id nodeapp --app-port 3000 --dapr-http-port 3500 app.js
将调试器附加到服务的一种方法是首先在命令行中使用正确参数运行 daprd,然后启动代码并附加调试器。虽然这是一个完全可接受的解决方案,但它确实需要一些额外的步骤,并且需要向可能想要克隆你的仓库并点击"播放"按钮开始调试的开发者提供一些指导。
如果你的应用程序是一组微服务,每个都有 Dapr 边车,在 Visual Studio Code 中一起调试它们将非常有用。本页面将使用 hello world 快速入门来展示如何配置 VSCode 以使用 VSCode 调试来调试多个 Dapr 应用程序。
前提条件
- 安装 Dapr 扩展。稍后你将使用它提供的 任务。
- 可选:克隆 hello world 快速入门
步骤 1:配置 launch.json
文件 .vscode/launch.json 包含 VS Code 调试运行的 启动配置。该文件定义了当用户开始调试时将启动什么以及如何配置。Visual Studio Code marketplace中提供了每种编程语言的配置。
在 hello world 快速入门的情况下,启动了两个应用程序,每个都有自己的 Dapr 边车。一个用 Node.JS 编写,另一个用 Python 编写。你会注意到每个配置都包含一个 daprd run preLaunchTask 和一个 daprd stop postDebugTask。
{
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Nodeapp with Dapr",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/node/app.js",
"preLaunchTask": "daprd-debug-node",
"postDebugTask": "daprd-down-node"
},
{
"type": "python",
"request": "launch",
"name": "Pythonapp with Dapr",
"program": "${workspaceFolder}/python/app.py",
"console": "integratedTerminal",
"preLaunchTask": "daprd-debug-python",
"postDebugTask": "daprd-down-python"
}
]
}
如果你使用的端口不是代码中内置的默认端口,请在 launch.json 调试配置中设置 DAPR_HTTP_PORT 和 DAPR_GRPC_PORT 环境变量。与 daprd tasks.json 中的 httpPort 和 grpcPort 匹配。例如,launch.json:
{
// 设置非默认的 HTTP 和 gRPC 端口
"env": {
"DAPR_HTTP_PORT": "3502",
"DAPR_GRPC_PORT": "50002"
},
}
tasks.json:
{
// 与 launch.json 中设置的端口匹配
"httpPort": 3502,
"grpcPort": 50002
}
每个配置都需要 request、type 和 name。这些参数帮助 VSCode 识别 .vscode/tasks.json 文件中的任务配置。
type定义使用的语言。根据语言的不同,它可能需要在 marketplace 中找到的扩展,例如 Python 扩展。name是配置的唯一名称。当在项目中调用多个配置时,这用于复合配置。${workspaceFolder}是 VS Code 变量引用。这是在 VS Code 中打开的工作区路径。preLaunchTask和postDebugTask参数指的是在启动应用程序之前和之后运行的程序配置。有关如何配置这些参数,请参阅步骤 2。
有关 VSCode 调试参数的更多信息,请参阅 VS Code 启动属性。
步骤 2:配置 tasks.json
对于 .vscode/launch.json 中定义的每个任务,在 .vscode/tasks.json 中必须存在相应的任务定义。
对于快速入门,每个服务都需要一个任务来使用 daprd 类型启动 Dapr 边车,以及一个使用 daprd-down 停止边车的任务。参数 appId、httpPort、metricsPort、label 和 type 是必需的。其他可选参数可用,请参阅此处的参考表。
{
"version": "2.0.0",
"tasks": [
{
"label": "daprd-debug-node",
"type": "daprd",
"appId": "nodeapp",
"appPort": 3000,
"httpPort": 3500,
"metricsPort": 9090
},
{
"label": "daprd-down-node",
"type": "daprd-down",
"appId": "nodeapp"
},
{
"label": "daprd-debug-python",
"type": "daprd",
"appId": "pythonapp",
"httpPort": 53109,
"grpcPort": 53317,
"metricsPort": 9091
},
{
"label": "daprd-down-python",
"type": "daprd-down",
"appId": "pythonapp"
}
]
}
步骤 3:在 launch.json 中配置复合启动
复合启动配置可以在 .vscode/launch.json 中定义,它是一组并行启动的两个或多个启动配置。可选地,可以指定 preLaunchTask 并在单独的调试会话开始之前运行。
对于此示例,复合配置是:
{
"version": "2.0.0",
"configurations": [...],
"compounds": [
{
"name": "Node/Python Dapr",
"configurations": ["Nodeapp with Dapr","Pythonapp with Dapr"]
}
]
}
步骤 4:启动调试会话
现在,你可以在 VS Code 调试器中找到在上一步中定义的复合命令名称,以调试模式运行应用程序:

你现在正在使用 Dapr 调试多个应用程序!
Daprd 参数表
以下是 VS Code 任务支持的参数。这些参数等同于 daprd 参数,详见此参考:
| 参数 | 描述 | 必需 | 示例 |
|---|---|---|---|
allowedOrigins | 允许的 HTTP 源(默认为 “*") | 否 | "allowedOrigins": "*" |
appId | 应用程序的唯一 ID。用于服务发现、状态封装和发布订阅消费者 ID | 是 | "appId": "divideapp" |
appMaxConcurrency | 限制应用程序的并发性。有效值是任何大于 0 的数字 | 否 | "appMaxConcurrency": -1 |
appPort | 此参数告诉 Dapr 你的应用程序正在监听哪个端口 | 是 | "appPort": 4000 |
appProtocol | 告诉 Dapr 你的应用程序使用哪个协议。有效选项是 http、grpc、https、grpcs、h2c。默认是 http。 | 否 | "appProtocol": "http" |
args | 设置传递给 Dapr 应用程序的参数列表 | 否 | “args”: [] |
componentsPath | 组件目录的路径。如果为空,则不会加载组件。 | 否 | "componentsPath": "./components" |
config | 告诉 Dapr 使用哪个配置资源 | 否 | "config": "./config" |
controlPlaneAddress | Dapr 控制平面的地址 | 否 | "controlPlaneAddress": "http://localhost:1366/" |
enableProfiling | 启用性能分析 | 否 | "enableProfiling": false |
enableMtls | 为 daprd 到 daprd 通信通道启用自动 mTLS | 否 | "enableMtls": false |
grpcPort | Dapr API 要监听的 gRPC 端口(默认为 “50001”) | 如果有多个应用则为是 | "grpcPort": 50004 |
httpPort | Dapr API 的 HTTP 端口 | 是 | "httpPort": 3502 |
internalGrpcPort | Dapr 内部 API 要监听的 gRPC 端口 | 否 | "internalGrpcPort": 50001 |
logAsJson | 将此参数设置为 true 会以 JSON 格式输出日志。默认为 false | 否 | "logAsJson": false |
logLevel | 设置 Dapr 边车的日志级别。允许的值是 debug、info、warn、error。默认是 info | 否 | "logLevel": "debug" |
metricsPort | 设置边车指标服务器的端口。默认为 9090 | 如果有多个应用则为是 | "metricsPort": 9093 |
mode | Dapr 的运行模式(默认为 “standalone”) | 否 | "mode": "standalone" |
placementHostAddress | Dapr Actor Placement 服务器的地址 | 否 | "placementHostAddress": "http://localhost:1313/" |
profilePort | 分析服务器的端口(默认为 “7777”) | 否 | "profilePort": 7777 |
sentryAddress | Sentry CA 服务的地址 | 否 | "sentryAddress": "http://localhost:1345/" |
type | 告诉 VS Code 这将是一个 daprd 任务类型 | 是 | "type": "daprd" |
相关链接
4.1.1.3 - 使用 Dev Containers 开发 Dapr 应用程序
弃用通知
该扩展此前由 Microsoft 支持,但现已弃用。该扩展仍将在 Visual Studio Code marketplace 中提供,但将不再接收更新或支持。Visual Studio Code Dev Containers 扩展允许您使用独立的 Docker 容器作为完整的开发环境,而无需在本地文件系统中安装任何额外的软件包、库或工具。
Dapr 为 C# 和 JavaScript/TypeScript 提供了预构建的 Dev Containers;您可以选择适合您需求的容器,即可获得现成的开发环境。请注意,这些预构建的容器会自动更新到最新的 Dapr 版本。
我们还发布了一个 Dev Container 功能,可在任何 Dev Container 中安装 Dapr CLI。
设置开发环境
前置条件
使用 Dev Container 功能添加 Dapr CLI
您可以使用 Dev Container 功能 在任何 Dev Container 中安装 Dapr CLI。
为此,请编辑您的 devcontainer.json 并在 "features" 部分添加两个对象:
"features": {
// 安装 Dapr CLI
"ghcr.io/dapr/cli/dapr-cli:0": {},
// 启用 Docker(通过 Docker-in-Docker)
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
// 或者,使用 Docker-outside-of-Docker(使用主机上的 Docker)
//"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {},
}
保存 JSON 文件并(重新)构建承载您的开发环境的容器后,您将拥有 Dapr CLI(和 Docker)可用,并且可以通过在容器中运行以下命令来安装 Dapr:
dapr init
示例:为 Dapr 创建 Java Dev Container
这是一个基于官方 Java 17 Dev Container 镜像创建用于开发使用 Dapr 的 Java 应用程序的 Dev Container 示例。
将此内容放在项目中的 .devcontainer/devcontainer.json 文件中:
// 有关格式详细信息,请参阅 https://aka.ms/devcontainer.json。有关配置选项,请参阅
// README at: https://github.com/devcontainers/templates/tree/main/src/java
{
"name": "Java",
// 或使用 Dockerfile 或 Docker Compose 文件。更多信息:https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/java:0-17",
"features": {
"ghcr.io/devcontainers/features/java:1": {
"version": "none",
"installMaven": "false",
"installGradle": "false"
},
// 安装 Dapr CLI
"ghcr.io/dapr/cli/dapr-cli:0": {},
// 启用 Docker(通过 Docker-in-Docker)
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
// 或者,使用 Docker-outside-of-Docker(使用主机上的 Docker)
//"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {},
}
// 使用 'forwardPorts' 使容器内的端口列表在本地可用。
// "forwardPorts": [],
// 使用 'postCreateCommand' 在容器创建后运行命令。
// "postCreateCommand": "java -version",
// 配置特定于工具的属性。
// "customizations": {},
// 取消注释以 root 用户身份连接。更多信息:https://aka.ms/dev-containers-non-root。
// "remoteUser": "root"
}
然后,使用 VS Code 命令面板(CTRL + SHIFT + P 或 Mac 上的 CMD + SHIFT + P),选择 Dev Containers: Rebuild and Reopen in Container。
使用预构建的 Dev Container(C# 和 JavaScript/TypeScript)
- 在 VS Code 中打开您的应用程序工作区
- 在命令面板中(
CTRL + SHIFT + P或 Mac 上的CMD + SHIFT + P)键入并选择Dev Containers: Add Development Container Configuration Files...
- 键入
dapr以过滤列表到可用的 Dapr 远程容器,并选择与您的应用程序匹配的语言容器。请注意,您可能需要选择Show All Definitions...
- 按照提示在容器中重新打开您的工作区。

示例
观看此视频,了解如何将 Dapr Dev Containers 与您的应用程序一起使用。
4.1.2 - IntelliJ
在开发 Dapr 应用程序时,通常使用 Dapr CLI 启动您的"Dapr 化"服务,类似这样:
dapr run --app-id nodeapp --app-port 3000 --dapr-http-port 3500 app.js
这会使用默认的组件 yaml 文件(在 dapr init 时创建),以便您的服务可以与本地 Redis 容器交互。这对刚入门来说很好,但如果您想将调试器附加到您的服务并单步调试代码呢?这是您可以不调用应用程序而使用 dapr cli 的地方。
将调试器附加到服务的一种方法是首先从命令行运行 dapr run --,然后启动代码并附加调试器。虽然这是一个完全可以接受的解决方案,但它确实需要一些额外的步骤(比如在终端和 IDE 之间切换),以及一些可能想要克隆您的仓库并点击"播放"按钮开始调试的开发者说明。
本文档解释如何直接从 IntelliJ 使用 dapr。作为先决条件,请确保您已通过 dapr init 初始化了 Dapr 的开发环境。
让我们开始吧!
将 Dapr 添加为"外部工具"
首先,在直接修改配置文件之前退出 IntelliJ。
IntelliJ 配置文件位置
对于 2020.1 及更高版本,工具的配置文件应位于:
%USERPROFILE%\AppData\Roaming\JetBrains\IntelliJIdea2020.1\tools\
$HOME/.config/JetBrains/IntelliJIdea2020.1/tools/
~/Library/Application\ Support/JetBrains/IntelliJIdea2020.1/tools/
对于 2019.3 或更早版本,配置文件位置不同。有关更多详细信息,请参阅此处。
如果需要,请更改路径中 IntelliJ 的版本。
在 <CONFIG PATH>/tools/External\ Tools.xml 中创建或编辑文件(如果需要,更改路径中的 IntelliJ 版本)。<CONFIG PATH> 取决于操作系统,如上所示。
添加新的 <tool></tool> 条目:
<toolSet name="External Tools">
...
<!-- 1. 每个工具都有自己的 app-id,因此为每个要调试的应用程序创建一个 -->
<tool name="dapr for DemoService in examples" description="Dapr sidecar" showInMainMenu="false" showInEditor="false" showInProject="false" showInSearchPopup="false" disabled="false" useConsole="true" showConsoleOnStdOut="true" showConsoleOnStdErr="true" synchronizeAfterRun="true">
<exec>
<!-- 2. 对于 Linux 或 MacOS 使用:/usr/local/bin/dapr -->
<option name="COMMAND" value="C:\dapr\dapr.exe" />
<!-- 3. 选择不与其他 daprd 命令条目冲突的应用程序、http 和 grpc 端口(placement 地址不应更改)。 -->
<option name="PARAMETERS" value="run -app-id demoservice -app-port 3000 -dapr-http-port 3005 -dapr-grpc-port 52000" />
<!-- 4. 使用 `components` 文件夹所在的文件夹 -->
<option name="WORKING_DIRECTORY" value="C:/Code/dapr/java-sdk/examples" />
</exec>
</tool>
...
</toolSet>
(可选)您还可以为可以在许多项目中重用的边车工具创建新条目:
<toolSet name="External Tools">
...
<!-- 1. 具有应用端口的应用程序的可重用条目。 -->
<tool name="dapr with app-port" description="Dapr sidecar" showInMainMenu="false" showInEditor="false" showInProject="false" showInSearchPopup="false" disabled="false" useConsole="true" showConsoleOnStdOut="true" showConsoleOnStdErr="true" synchronizeAfterRun="true">
<exec>
<!-- 2. 对于 Linux 或 MacOS 使用:/usr/local/bin/dapr -->
<option name="COMMAND" value="c:\dapr\dapr.exe" />
<!-- 3. 提示用户 4 次(按顺序):app id、app port、Dapr 的 http port、Dapr 的 grpc port。 -->
<option name="PARAMETERS" value="run --app-id $Prompt$ --app-port $Prompt$ --dapr-http-port $Prompt$ --dapr-grpc-port $Prompt$" />
<!-- 4. 使用 `components` 文件夹所在的文件夹 -->
<option name="WORKING_DIRECTORY" value="$ProjectFileDir$" />
</exec>
</tool>
<!-- 1. 没有 app-port 的应用程序的可重用条目。 -->
<tool name="dapr without app-port" description="Dapr sidecar" showInMainMenu="false" showInEditor="false" showInProject="false" showInSearchPopup="false" disabled="false" useConsole="true" showConsoleOnStdOut="true" showConsoleOnStdErr="true" synchronizeAfterRun="true">
<exec>
<!-- 2. 对于 Linux 或 MacOS 使用:/usr/local/bin/dapr -->
<option name="COMMAND" value="c:\dapr\dapr.exe" />
<!-- 3. 提示用户 3 次(按顺序):app id、Dapr 的 http port、Dapr 的 grpc port。 -->
<option name="PARAMETERS" value="run --app-id $Prompt$ --dapr-http-port $Prompt$ --dapr-grpc-port $Prompt$" />
<!-- 4. 使用 `components` 文件夹所在的文件夹 -->
<option name="WORKING_DIRECTORY" value="$ProjectFileDir$" />
</exec>
</tool>
...
</toolSet>
创建或编辑运行配置
现在,为要调试的应用程序创建或编辑运行配置。可以在 main() 函数旁边的菜单中找到它。

现在,添加程序参数和环境变量。这些需要与上面"外部工具"中的条目定义的端口匹配。
- 本示例的命令行参数:
-p 3000 - 本示例的环境变量:
DAPR_HTTP_PORT=3005;DAPR_GRPC_PORT=52000

开始调试
完成上述一次性配置后,在 IntelliJ 中使用 Dapr 调试 Java 应用程序需要两个步骤:
- 通过 IntelliJ 中的
Tools->External Tool启动dapr。

- 以调试模式启动您的应用程序。

总结
调试完成后,请确保在 IntelliJ 中同时停止 dapr 和您的应用程序。
注意:由于您使用 dapr run CLI 命令启动了服务,dapr list 命令将在当前使用 Dapr 运行的应用程序列表中显示来自 IntelliJ 的运行。
祝您调试愉快!
相关链接
- IntelliJ 配置目录位置的更改
4.2 - 多应用运行
4.2.1 - Multi-App Run 概述
Note
适用于 Kubernetes 的 Multi-App Run 目前为预览功能。假设您想在本地运行多个应用程序以进行联合测试,类似于生产环境的场景。Multi-App Run 允许您同时启动和停止一组应用程序,可以选择:
- 使用进程在本地/自托管环境中运行,或
- 通过构建容器镜像并部署到 Kubernetes 集群
- 您可以使用本地 Kubernetes 集群或部署到云
Multi-App Run 模板文件描述了如何启动多个应用程序,就像您执行了多个单独的 CLI run 命令一样。默认情况下,此模板文件名为 dapr.yaml。
Multi-App Run 模板文件
当您执行 dapr run -f . 时,它会启动当前目录中存在的多应用模板文件(名为 dapr.yaml)以运行所有应用程序。
您可以使用首选名称来命名模板文件,而不是使用默认名称。例如 dapr run -f ./<your-preferred-file-name>.yaml。
以下示例包括一些您可以为应用程序自定义的模板属性。在该示例中,您可以同时启动 2 个应用程序,其应用 ID 为 processor 和 emit-metrics。
version: 1
apps:
- appID: processor
appDirPath: ../apps/processor/
appPort: 9081
daprHTTPPort: 3510
command: ["go","run", "app.go"]
- appID: emit-metrics
appDirPath: ../apps/emit-metrics/
daprHTTPPort: 3511
env:
DAPR_HOST_ADD: localhost
command: ["go","run", "app.go"]
有关更深入的示例和模板属性的解释,请参阅 多应用模板。
资源和配置文件的位置
在使用 Multi-App Run 时,您可以选择放置应用程序资源和配置文件的位置。
指向一个文件位置(使用约定)
您可以在 ~/.dapr 根目录下设置所有应用程序资源和配置。当所有应用程序共享相同的资源路径时(例如在本地机器上测试时),这很有帮助。
为每个应用程序指定单独的文件位置(使用约定)
使用 Multi-App Run 时,每个应用程序目录可以有一个 .dapr 文件夹,其中包含 config.yaml 文件和 resources 目录。否则,如果应用程序目录中不存在 .dapr 目录,则使用默认的 ~/.dapr/resources/ 和 ~/.dapr/config.yaml 位置。
如果您决定在每个应用程序目录中添加 .dapr 目录,其中包含 /resources 目录和 config.yaml 文件,则可以为每个应用程序指定不同的资源路径。此方法通过使用默认的 ~/.dapr 保持约定。
指向单独的位置(自定义)
您还可以将每个应用目录的 .dapr 目录命名为 .dapr 以外的名称,例如 webapp 或 backend。如果您想明确资源或应用程序目录路径,这很有帮助。
日志
运行模板为每个应用程序及其关联的 daprd 进程提供两个日志目标字段:
appLogDestination:此字段配置应用程序的日志目标。可能的值为console、file和fileAndConsole。默认值为fileAndConsole,其中应用程序日志默认写入控制台和文件。daprdLogDestination:此字段配置daprd进程的日志目标。可能的值为console、file和fileAndConsole。默认值为file,其中daprd日志默认写入文件。
日志文件格式
应用程序和 daprd 的日志捕获在单独的文件中。这些日志文件在应用程序目录(模板中的 appDirPath)下的 .dapr/logs 目录下自动创建。这些日志文件名遵循以下模式:
<appID>_app_<timestamp>.log(app日志的文件名格式)<appID>_daprd_<timestamp>.log(daprd日志的文件名格式)
即使您决定将资源文件夹重命名为 .dapr 以外的名称,日志文件也只会写入 .dapr/logs 文件夹(在应用程序目录中创建)。
观看演示
Multi-App Run 模板文件
当您执行 dapr run -k -f . 或 dapr run -k -f dapr.yaml 时,dapr.yaml Multi-App Run 模板文件中定义的应用程序将在 Kubernetes 默认命名空间中启动。
注意: 目前,Multi-App Run 模板只能在默认 Kubernetes 命名空间中启动应用程序。
Kubernetes 的必要默认服务和部署定义会在 .dapr/deploy 文件夹中为 dapr.yaml 模板中的每个应用程序生成。
如果在 dapr.yaml 模板中为某个应用程序将 createService 字段设置为 true,则会在该应用程序的 .dapr/deploy 文件夹中生成 service.yaml 文件。
否则,只为设置了 containerImage 字段的每个应用程序生成 deployment.yaml 文件。
service.yaml 和 deployment.yaml 文件用于在 Kubernetes 中的 default 命名空间中部署应用程序。此功能专门针对在 Kubernetes 中的开发/测试环境中运行多个应用程序。
您可以使用任何首选名称来命名模板文件,而不是使用默认名称。例如:
dapr run -k -f ./<your-preferred-file-name>.yaml
以下示例包括一些您可以为应用程序自定义的模板属性。在该示例中,您可以同时启动 2 个应用程序,其应用 ID 为 nodeapp 和 pythonapp。
version: 1
common:
apps:
- appID: nodeapp
appDirPath: ./nodeapp/
appPort: 3000
containerImage: ghcr.io/dapr/samples/hello-k8s-node:latest
containerImagePullPolicy: Always
createService: true
env:
APP_PORT: 3000
- appID: pythonapp
appDirPath: ./pythonapp/
containerImage: ghcr.io/dapr/samples/hello-k8s-python:latest
注意:
- 如果未指定
containerImage字段,dapr run -k -f会产生错误。- containerImagePullPolicy 表示始终为此应用程序下载新的容器镜像。
createService字段在 Kubernetes 中定义一个基本服务(ClusterIP 或 LoadBalancer),该服务针对模板中指定的--app-port。如果未指定createService,则应用程序无法从集群外部访问。
有关更深入的示例和模板属性的解释,请参阅 多应用模板。
日志
运行模板为每个应用程序及其关联的 daprd 进程提供两个日志目标字段:
appLogDestination:此字段配置应用程序的日志目标。可能的值为console、file和fileAndConsole。默认值为fileAndConsole,其中应用程序日志默认写入控制台和文件。daprdLogDestination:此字段配置daprd进程的日志目标。可能的值为console、file和fileAndConsole。默认值为file,其中daprd日志默认写入文件。
日志文件格式
应用程序和 daprd 的日志捕获在单独的文件中。这些日志文件在应用程序目录(模板中的 appDirPath)下的 .dapr/logs 目录下自动创建。这些日志文件名遵循以下模式:
<appID>_app_<timestamp>.log(app日志的文件名格式)<appID>_daprd_<timestamp>.log(daprd日志的文件名格式)
即使您决定将资源文件夹重命名为 .dapr 以外的名称,日志文件也只会写入 .dapr/logs 文件夹(在应用程序目录中创建)。
观看演示
后续步骤
4.2.2 - 操作指南:使用多应用运行模板文件
注意
Kubernetes 的多应用运行目前为预览功能。多应用运行模板文件是一个 YAML 文件,可用于一次运行多个应用程序。在本指南中,您将学习如何:
- 使用多应用模板
- 查看已启动的应用程序
- 停止多应用模板
- 构建多应用模板文件结构
使用多应用模板
您可以通过以下两种方式之一使用多应用模板文件:
通过提供目录路径执行
当您提供目录路径时,CLI 将尝试在目录中查找名为 dapr.yaml(默认名称)的多应用运行模板文件。如果未找到该文件,CLI 将返回错误。
执行以下 CLI 命令以读取多应用运行模板文件,默认名称为 dapr.yaml:
# 如果给定目录路径,模板文件需要默认命名为 `dapr.yaml`
dapr run -f <dir_path>
dapr run -f <dir_path> -k
通过提供文件路径执行
如果多应用运行模板文件的名称不是 dapr.yaml,那么您可以向命令提供相对或绝对文件路径:
dapr run -f ./path/to/<your-preferred-file-name>.yaml
dapr run -f ./path/to/<your-preferred-file-name>.yaml -k
查看已启动的应用程序
多应用模板运行后,您可以使用以下命令查看已启动的应用程序:
dapr list
dapr list -k
停止多应用模板
随时使用以下任一命令停止多应用运行模板:
# 如果给定目录路径,模板文件需要默认命名为 `dapr.yaml`
dapr stop -f <dir_path>
或:
dapr stop -f ./path/to/<your-preferred-file-name>.yaml
# 如果给定目录路径,模板文件需要默认命名为 `dapr.yaml`
dapr stop -f <dir_path> -k
或:
dapr stop -f ./path/to/<your-preferred-file-name>.yaml -k
模板文件结构
多应用运行模板文件可以包含以下属性。以下是一个示例模板,展示了配置了部分属性的两个应用程序。
version: 1
common: # 可选部分,用于在应用程序之间共享的变量
resourcesPath: ./app/components # 任何在应用程序之间共享的 dapr 资源
env: # 任何在应用程序之间共享的环境变量
DEBUG: true
apps:
- appID: webapp # 可选
appDirPath: .dapr/webapp/ # 必需
resourcesPath: .dapr/resources # 已弃用
resourcesPaths: .dapr/resources # 逗号分隔的资源路径。(可选)可按约定保留为默认值。
appChannelAddress: 127.0.0.1 # 应用程序监听的网络地址。(可选)可按约定保留为默认值。
configFilePath: .dapr/config.yaml # (可选)也可以按约定默认,如果未找到文件则忽略。
appProtocol: http
appPort: 8080
appHealthCheckPath: "/healthz"
command: ["python3", "app.py"]
appLogDestination: file # (可选),可以是 file、console 或 fileAndConsole。默认为 fileAndConsole。
daprdLogDestination: file # (可选),可以是 file、console 或 fileAndConsole。默认为 file。
- appID: backend # 可选
appDirPath: .dapr/backend/ # 必需
appProtocol: grpc
appPort: 3000
unixDomainSocket: "/tmp/test-socket"
env:
DEBUG: false
command: ["./backend"]
模板文件中所有路径适用以下规则:
- 如果路径是绝对路径,则按原样使用。
- common 部分下的所有相对路径应相对于模板文件路径提供。
- apps 部分下的
appDirPath应相对于模板文件路径提供。 - apps 部分下的所有其他相对路径应相对于
appDirPath提供。
version: 1
common: # 可选部分,用于在应用程序之间共享的变量
env: # 任何在应用程序之间共享的环境变量
DEBUG: true
apps:
- appID: webapp # 可选
appDirPath: .dapr/webapp/ # 必需
appChannelAddress: 127.0.0.1 # 应用程序监听的网络地址。(可选)可按约定保留为默认值。
appProtocol: http
appPort: 8080
appHealthCheckPath: "/healthz"
appLogDestination: file # (可选),可以是 file、console 或 fileAndConsole。默认为 fileAndConsole。
daprdLogDestination: file # (可选),可以是 file、console 或 fileAndConsole。默认为 file。
containerImage: ghcr.io/dapr/samples/hello-k8s-node:latest # (可选)部署到 Kubernetes 开发/测试环境时要使用的容器镜像 URI。
containerImagePullPolicy: IfNotPresent # (可选),如果本地不存在容器镜像则下载,否则使用本地镜像。
createService: true # (可选)部署到开发/测试环境时为应用程序创建 Kubernetes 服务。
- appID: backend # 可选
appDirPath: .dapr/backend/ # 必需
appProtocol: grpc
appPort: 3000
unixDomainSocket: "/tmp/test-socket"
env:
DEBUG: false
模板文件中所有路径适用以下规则:
- 如果路径是绝对路径,则按原样使用。
- apps 部分下的
appDirPath应相对于模板文件路径提供。 - app 部分下的所有相对路径应相对于
appDirPath提供。
模板属性
多应用运行模板的属性与 dapr run CLI 标志一致,在 CLI 参考文档中列出。
| 属性 | 必需 | 详情 | 示例 |
|---|---|---|---|
appDirPath | 是 | 应用程序代码的路径 | ./webapp/、./backend/ |
appID | 否 | 应用程序的 app ID。如果未提供,将从 appDirPath 派生 | webapp、backend |
resourcesPath | 否 | 已弃用。Dapr 资源的路径。可按约定使用默认值 | ./app/components、./webapp/components |
resourcesPaths | 否 | Dapr 资源的逗号分隔路径。可按约定使用默认值 | ./app/components、./webapp/components |
appChannelAddress | 否 | 应用程序监听的网络地址。可按约定保留为默认值。 | 127.0.0.1 |
configFilePath | 否 | 应用程序配置文件的路径 | ./webapp/config.yaml |
appProtocol | 否 | Dapr 用于与应用程序通信的协议。 | http、grpc |
appPort | 否 | 应用程序监听的端口 | 8080、3000 |
daprHTTPPort | 否 | Dapr HTTP 端口 | |
daprGRPCPort | 否 | Dapr GRPC 端口 | |
daprInternalGRPCPort | 否 | Dapr 内部 API 监听的 gRPC 端口;用于从本地 DNS 组件解析值时使用 | |
metricsPort | 否 | Dapr 将其指标信息发送到的端口 | |
unixDomainSocket | 否 | Unix 域套接字目录挂载的路径。如果指定,与 Dapr 边车的通信使用 Unix 域套接字,与使用 TCP 端口相比,具有更低的延迟和更高的吞吐量。在 Windows 上不可用。 | /tmp/test-socket |
profilePort | 否 | 性能分析服务器监听的端口 | |
enableProfiling | 否 | 通过 HTTP 端点启用性能分析 | |
apiListenAddresses | 否 | Dapr API 监听地址 | |
logLevel | 否 | 日志详细程度。 | |
appMaxConcurrency | 否 | 应用程序的并发级别;默认为无限制 | |
placementHostAddress | 否 | Dapr placement 服务器的逗号分隔地址列表 | 127.0.0.1:50057,127.0.0.1:50058 |
schedulerHostAddress | 否 | Dapr Scheduler Service 主机地址 | localhost:50006 |
appSSL | 否 | 当 Dapr 调用应用程序时启用 HTTPS | |
maxBodySize | 否 | 请求正文的最大大小(以 MB 为单位)。使用大小单位设置值(例如,16Mi 表示 16MB)。默认为 4Mi | |
readBufferSize | 否 | HTTP 读取缓冲区的最大大小(以 KB 为单位)。这也限制了 HTTP 标头的最大大小。使用大小单位设置值,例如 32Ki 将支持最大 32KB 的标头。默认为 4Ki(4KB) | |
enableAppHealthCheck | 否 | 在应用程序上启用应用程序运行状况检查 | true、false |
appHealthCheckPath | 否 | 运行状况检查文件的路径 | /healthz |
appHealthProbeInterval | 否 | 探测应用程序运行状况的间隔(以秒为单位) | |
appHealthProbeTimeout | 否 | 应用程序运行状况探测的超时时间(以毫秒为单位) | |
appHealthThreshold | 否 | 应用程序被视为不健康之前的连续失败次数 | |
enableApiLogging | 否 | 启用从应用程序到 Dapr 的所有 API 调用的日志记录 | |
runtimePath | 否 | Dapr 运行时安装路径 | |
env | 否 | 映射到环境变量;应用于每个应用程序的环境变量将覆盖在应用程序之间共享的环境变量 | DEBUG、DAPR_HOST_ADD |
appLogDestination | 否 | 用于输出应用程序日志的日志目标;其值可以是 file、console 或 fileAndConsole。默认为 fileAndConsole | file、console、fileAndConsole |
daprdLogDestination | 否 | 用于输出 daprd 日志的日志目标;其值可以是 file、console 或 fileAndConsole。默认为 file | file、console、fileAndConsole |
后续步骤
多应用运行模板的属性与 dapr run -k CLI 标志一致,在 CLI 参考文档中列出。
| 属性 | 必需 | 详情 | 示例 |
|---|---|---|---|
appDirPath | 是 | 应用程序代码的路径 | ./webapp/、./backend/ |
appID | 否 | 应用程序的 app ID。如果未提供,将从 appDirPath 派生 | webapp、backend |
appChannelAddress | 否 | 应用程序监听的网络地址。可按约定保留为默认值。 | 127.0.0、localhost |
appProtocol | 否 | Dapr 用于与应用程序通信的协议。 | http、grpc |
appPort | 否 | 应用程序监听的端口 | 8080、3000 |
daprHTTPPort | 否 | Dapr HTTP 端口 | |
daprGRPCPort | 否 | Dapr GRPC 端口 | |
daprInternalGRPCPort | 否 | Dapr 内部 API 监听的 gRPC 端口;用于从本地 DNS 组件解析值时使用 | |
metricsPort | 否 | Dapr 将其指标信息发送到的端口 | |
unixDomainSocket | 否 | Unix 域套接字目录挂载的路径。如果指定,与 Dapr 边车的通信使用 Unix 域套接字,与使用 TCP 端口相比,具有更低的延迟和更高的吞吐量。在 Windows 上不可用。 | /tmp/test-socket |
profilePort | 否 | 性能分析服务器监听的端口 | |
enableProfiling | 否 | 通过 HTTP 端点启用性能分析 | |
apiListenAddresses | 否 | Dapr API 监听地址 | |
logLevel | 否 | 日志详细程度。 | |
appMaxConcurrency | 否 | 应用程序的并发级别;默认为无限制 | |
placementHostAddress | 否 | Dapr placement 服务器的逗号分隔地址列表 | 127.0.0.1:50057,127.0.0.1:50058 |
schedulerHostAddress | 否 | Dapr Scheduler Service 主机地址 | 127.0.0.1:50006 |
appSSL | 否 | 当 Dapr 调用应用程序时启用 HTTPS | |
maxBodySize | 否 | 请求正文的最大大小(以 MB 为单位)。使用大小单位设置值(例如,16Mi 表示 16MB)。默认为 4Mi | 16Mi |
readBufferSize | 否 | HTTP 读取缓冲区的最大大小(以 KB 为单位)。这也限制了 HTTP 标头的最大大小。使用大小单位设置值,例如 32Ki 将支持最大 32KB 的标头。默认为 4Ki(4KB) | 32Ki |
enableAppHealthCheck | 否 | 在应用程序上启用应用程序运行状况检查 | true、false |
appHealthCheckPath | 否 | 运行状况检查文件的路径 | /healthz |
appHealthProbeInterval | 否 | 探测应用程序运行状况的间隔(以秒为单位) | |
appHealthProbeTimeout | 否 | 应用程序运行状况探测的超时时间(以毫秒为单位) | |
appHealthThreshold | 否 | 应用程序被视为不健康之前的连续失败次数 | |
enableApiLogging | 否 | 启用从应用程序到 Dapr 的所有 API 调用的日志记录 | |
env | 否 | 映射到环境变量;应用于每个应用程序的环境变量将覆盖在应用程序之间共享的环境变量 | DEBUG、DAPR_HOST_ADD |
appLogDestination | 否 | 用于输出应用程序日志的日志目标;其值可以是 file、console 或 fileAndConsole。默认为 fileAndConsole | file、console、fileAndConsole |
daprdLogDestination | 否 | 用于输出 daprd 日志的日志目标;其值可以是 file、console 或 fileAndConsole。默认为 file | file、console、fileAndConsole |
containerImage | 否 | 部署到 Kubernetes 开发/测试环境时要使用的容器镜像 URI。 | ghcr.io/dapr/samples/hello-k8s-python:latest |
containerImagePullPolicy | 否 | 容器镜像拉取策略(默认为 Always)。 | Always、IfNotPresent、Never |
createService | 否 | 部署到开发/测试环境时为应用程序创建 Kubernetes 服务。 | true、false |
后续步骤
4.3 - 操作指南:在 Dapr 应用中使用 gRPC 接口
Dapr 为本地调用实现了 HTTP 和 gRPC 两种 API。gRPC 适用于低延迟、高性能场景,并提供了基于 proto 客户端的语言集成。
Dapr 运行时实现了一个 proto 服务,应用程序可以通过 gRPC 与其通信。
除了通过 gRPC 调用 Dapr 外,Dapr 还支持作为代理进行 gRPC 服务间调用。在 gRPC 服务调用操作指南中了解更多。
本指南演示如何使用 Go SDK 应用程序配置并通过 gRPC 调用 Dapr。
配置 Dapr 通过 gRPC 与应用程序通信
在自托管模式下运行时,使用 --app-protocol 标志告知 Dapr 使用 gRPC 与应用程序通信。
dapr run --app-protocol grpc --app-port 5005 node app.js
这会告知 Dapr 通过端口 5005 上的 gRPC 与应用程序通信。
在 Kubernetes 上,在部署 YAML 中设置以下注解:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: default
labels:
app: myapp
spec:
replicas: 1
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
annotations:
dapr.io/enabled: "true"
dapr.io/app-id: "myapp"
dapr.io/app-protocol: "grpc"
dapr.io/app-port: "5005"
...
通过 gRPC 调用 Dapr
以下步骤展示如何创建 Dapr 客户端并调用其 SaveStateData 操作。
导入包:
package main import ( "context" "log" "os" dapr "github.com/dapr/go-sdk/client" )创建客户端:
// 仅用于此演示 ctx := context.Background() data := []byte("ping") // 创建客户端 client, err := dapr.NewClient() if err != nil { log.Panic(err) } defer client.Close()- 调用
SaveState方法:
// 使用键 key1 保存状态 err = client.SaveState(ctx, "statestore", "key1", data) if err != nil { log.Panic(err) } log.Println("data saved")- 调用
现在你可以探索 Dapr 客户端上的所有不同方法。
使用 Dapr 创建 gRPC 应用
以下步骤展示如何创建一个应用程序,该应用程序暴露一个可供 Dapr 通信的服务器。
导入包:
package main import ( "context" "fmt" "log" "net" "github.com/golang/protobuf/ptypes/any" "github.com/golang/protobuf/ptypes/empty" commonv1pb "github.com/dapr/dapr/pkg/proto/common/v1" pb "github.com/dapr/dapr/pkg/proto/runtime/v1" "google.golang.org/grpc" )实现接口:
// server 是我们的用户应用程序 type server struct { pb.UnimplementedAppCallbackServer } // EchoMethod 是一个简单的演示方法,用于调用 func (s *server) EchoMethod() string { return "pong" } // 当远程服务通过 Dapr 调用应用程序时,会调用此方法 // 载荷包含一个用于标识方法的 Method、一组元数据属性和一个可选载荷 func (s *server) OnInvoke(ctx context.Context, in *commonv1pb.InvokeRequest) (*commonv1pb.InvokeResponse, error) { var response string switch in.Method { case "EchoMethod": response = s.EchoMethod() } return &commonv1pb.InvokeResponse{ ContentType: "text/plain; charset=UTF-8", Data: &any.Any{Value: []byte(response)}, }, nil } // Dapr 将调用此方法以获取应用程序希望订阅的主题列表。在此示例中,我们告知 Dapr // 订阅一个名为 TopicA 的主题 func (s *server) ListTopicSubscriptions(ctx context.Context, in *empty.Empty) (*pb.ListTopicSubscriptionsResponse, error) { return &pb.ListTopicSubscriptionsResponse{ Subscriptions: []*pb.TopicSubscription{ {Topic: "TopicA"}, }, }, nil } // Dapr 将调用此方法以获取将调用应用程序的绑定列表。在此示例中,我们告知 Dapr // 使用名为 storage 的绑定调用我们的应用程序 func (s *server) ListInputBindings(ctx context.Context, in *empty.Empty) (*pb.ListInputBindingsResponse, error) { return &pb.ListInputBindingsResponse{ Bindings: []string{"storage"}, }, nil } // 每当从已注册绑定触发新事件时,都会调用此方法。消息携带绑定名称、载荷和可选元数据 func (s *server) OnBindingEvent(ctx context.Context, in *pb.BindingEventRequest) (*pb.BindingEventResponse, error) { fmt.Println("Invoked from binding") return &pb.BindingEventResponse{}, nil } // 当消息已发布到已订阅的主题时,将触发此方法。Dapr 以 CloudEvents 0.3 信封发送已发布的消息。 func (s *server) OnTopicEvent(ctx context.Context, in *pb.TopicEventRequest) (*pb.TopicEventResponse, error) { fmt.Println("Topic message arrived") return &pb.TopicEventResponse{}, nil }创建服务器:
func main() { // 创建监听器 lis, err := net.Listen("tcp", ":50001") if err != nil { log.Fatalf("failed to listen: %v", err) } // 创建 grpc 服务器 s := grpc.NewServer() pb.RegisterAppCallbackServer(s, &server{}) fmt.Println("Client starting...") // 然后启动... if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }这会为你的应用程序在端口 50001 上创建一个 gRPC 服务器。
运行应用程序
要在本地运行,请使用 Dapr CLI:
dapr run --app-id goapp --app-port 50001 --app-protocol grpc go run main.go
在 Kubernetes 上,在 Pod 规范模板中设置所需的 dapr.io/app-protocol: "grpc" 和 dapr.io/app-port: "50001 注解,如上所述。
其他语言
你可以将 Dapr 与 Protobuf 支持的任何语言一起使用,而不仅仅是当前可用的生成 SDK。
使用 protoc 工具,你可以为其他语言(如 Ruby、C++、Rust 等)生成 Dapr 客户端。
相关主题
4.4 - Dapr SDK 中的序列化
Dapr SDK 为两种用例提供序列化。首先,是通过请求和响应负载发送的 API 对象。其次,是需要持久化的对象。对于这两种情况,每种语言 SDK 都提供了默认的序列化方法。
| Language SDK | 默认序列化器 |
|---|---|
| .NET | 对于远程 actor 使用 DataContracts,其他情况使用 System.Text.Json。了解更多关于 .NET 序列化的信息 here |
| Java | 用于 JSON 序列化的 DefaultObjectSerializer |
| JavaScript | JSON |
服务调用
using var client = (new DaprClientBuilder()).Build();
await client.InvokeMethodAsync("myappid", "saySomething", "My Message");
DaprClient client = (new DaprClientBuilder()).build();
client.invokeMethod("myappid", "saySomething", "My Message", HttpExtension.POST).block();
在上面的示例中,应用 myappid 接收到一个 saySomething 方法的 POST 请求,请求负载为 "My Message" — 由于序列化器会将输入的 String 序列化为 JSON,所以它被引号包裹。
POST /saySomething HTTP/1.1
Host: localhost
Content-Type: text/plain
Content-Length: 12
"My Message"
状态管理
using var client = (new DaprClientBuilder()).Build();
var state = new Dictionary<string, string>
{
{ "key": "MyKey" },
{ "value": "My Message" }
};
await client.SaveStateAsync("MyStateStore", "MyKey", state);
DaprClient client = (new DaprClientBuilder()).build();
client.saveState("MyStateStore", "MyKey", "My Message").block();
在此示例中,My Message 被保存。它没有被引号包裹,因为 Dapr 的 API 在保存前会在内部解析 JSON 请求对象。
在此示例中,My Message 被保存。它没有被引号包裹,因为 Dapr 的 API 在发送前会在内部序列化字符串。
发布订阅
using var client = (new DaprClientBuilder()).Build();
await client.PublishEventAsync("MyPubSubName", "TopicName", "My Message");
事件被发布,内容被序列化为 byte[] 并发送到 Dapr 边车。订阅者将其作为 CloudEvent 接收。Cloud 事件将 data 定义为字符串。Dapr SDK 也为 CloudEvent 对象提供了内置的反序列化器。
public async Task<IActionResult> HandleMessage(string message)
{
//ASP.NET Core 自动将 UTF-8 编码的字节反序列化为字符串
return new Ok();
}
或者
app.MapPost("/TopicName", [Topic("MyPubSubName", "TopicName")] (string message) => {
return Results.Ok();
}
DaprClient client = (new DaprClientBuilder()).build();
client.publishEvent("TopicName", "My Message").block();
事件被发布,内容被序列化为 byte[] 并发送到 Dapr 边车。订阅者将其作为 CloudEvent 接收。Cloud 事件将 data 定义为 String。Dapr SDK 也为 CloudEvent 对象提供了内置的反序列化器。
@PostMapping(path = "/TopicName")
public void handleMessage(@RequestBody(required = false) byte[] body) {
// Dapr 的事件符合 CloudEvent 规范
CloudEvent event = CloudEvent.deserialize(body);
}
绑定
对于输出绑定,对象被序列化为 byte[],而输入绑定按原样接收原始 byte[] 并将其反序列化为预期的对象类型。
- 输出绑定:
using var client = (new DaprClientBuilder()).Build();
await client.InvokeBindingAsync("sample", "My Message");
- 输入绑定(控制器):
[ApiController]
public class SampleController : ControllerBase
{
[HttpPost("propagate")]
public ActionResult<string> GetValue([FromBody] int itemId)
{
Console.WriteLine($"Received message: {itemId}");
return $"itemID:{itemId}";
}
}
- 输入绑定(最小 API):
app.MapPost("value", ([FromBody] int itemId) =>
{
Console.WriteLine($"Received message: {itemId}");
return ${itemID:{itemId}";
});
- 输出绑定:
DaprClient client = (new DaprClientBuilder()).build();
client.invokeBinding("sample", "My Message").block();
- 输入绑定:
@PostMapping(path = "/sample")
public void handleInputBinding(@RequestBody(required = false) byte[] body) {
String message = (new DefaultObjectSerializer()).deserialize(body, String.class);
System.out.println(message);
}
它应该打印:
My Message
Actor 方法调用
Actor 方法调用的对象序列化和反序列化与服务方法调用的方式相同,唯一的区别是应用不需要反序列化请求或序列化响应,因为这一切都由 SDK 透明地完成。
对于 Actor 方法,SDK 仅支持具有零个或一个参数的方法。
根据您使用的是强类型还是弱类型客户端,.NET SDK 提供了不同的序列化选项:强类型客户端使用 DataContracts,而弱类型客户端可以选择 DataContracts 或 System.Text.JSON。您可以参考 [本文档](https://docs.dapr.io/zh-hans/developing-applications/sdks/dotnet/dotnet-actors/dotnet-actors-serialization/) 了解各种序列化方式之间的差异和需要注意的细节。- 使用弱类型客户端和 System.Text.JSON 调用 Actor 的方法:
var proxy = this.ProxyFactory.Create(ActorId.CreateRandom(), "DemoActor");
await proxy.SayAsync("My message");
- 实现 Actor 的方法:
public Task SayAsync(string message)
{
Console.WriteLine(message);
return Task.CompletedTask;
}
- 调用 Actor 的方法:
public static void main() {
ActorProxyBuilder builder = new ActorProxyBuilder("DemoActor");
String result = actor.invokeActorMethod("say", "My Message", String.class).block();
}
- 实现 Actor 的方法:
public String say(String something) {
System.out.println(something);
return "OK";
}
它应该打印:
My Message
Actor 的状态管理
Actor 也可以具有状态。在这种情况下,状态管理器将使用状态序列化器对对象进行序列化和反序列化,并对应用透明地处理这些操作。
public Task SayAsync(string message)
{
// 从键读取状态
var previousMessage = await this.StateManager.GetStateAsync<string>("lastmessage");
// 在序列化后为键设置新状态
await this.StateManager.SetStateAsync("lastmessage", message);
return previousMessage;
}
public String actorMethod(String message) {
// 从键读取状态并反序列化为 String
String previousMessage = super.getActorStateManager().get("lastmessage", String.class).block();
// 在序列化后为键设置新状态
super.getActorStateManager().set("lastmessage", message).block();
return previousMessage;
}
默认序列化器
Dapr 的默认序列化器是一个 JSON 序列化器,具有以下预期:
- 使用基本 JSON 数据类型以实现跨语言和跨平台兼容性:string、number、array、boolean、null 和另一个 JSON 对象。应用可序列化对象中的每个复杂属性类型(DateTime,例如),都应表示为 JSON 的基本类型之一。
- 使用默认序列化器持久化的数据也应保存为 JSON 对象,无需额外的引号或编码。下面的示例展示了字符串和 JSON 对象在 Redis 存储中的样子。
redis-cli MGET "ActorStateIT_StatefulActorService||StatefulActorTest||1581130928192||message
"This is a message to be saved and retrieved."
redis-cli MGET "ActorStateIT_StatefulActorService||StatefulActorTest||1581130928192||mydata
{"value":"My data value."}
- 自定义序列化器必须将对象序列化为
byte[]。 - 自定义序列化器必须将
byte[]反序列化为对象。 - 当用户提供自定义序列化器时,应将其作为
byte[]传输或持久化。持久化时,还要编码为 Base64 字符串。大多数 JSON 库原生支持此操作。
redis-cli MGET "ActorStateIT_StatefulActorService||StatefulActorTest||1581130928192||message
"VGhpcyBpcyBhIG1lc3NhZ2UgdG8gYmUgc2F2ZWQgYW5kIHJldHJpZXZlZC4="
redis-cli MGET "ActorStateIT_StatefulActorService||StatefulActorTest||1581130928192||mydata
"eyJ2YWx1ZSI6Ik15IGRhdGEgdmFsdWUuIn0="
5 - 调试 Dapr 应用程序和 Dapr 控制平面
5.1 - 在 Kubernetes 模式下调试 Dapr
5.1.1 - 在 Kubernetes 上调试 Dapr 控制平面
概述
有时有必要了解 Dapr 控制平面(即 Kubernetes 服务)内部发生了什么,包括 dapr-sidecar-injector、dapr-operator、dapr-placement 和 dapr-sentry,尤其是当你诊断 Dapr 应用程序并怀疑 Dapr 本身是否存在问题时。此外,你可能正在为 Kubernetes 上的 Dapr 开发新功能并希望调试代码。
本指南将介绍如何使用 Dapr 调试二进制文件在 Kubernetes 集群上调试 Dapr 服务。
调试 Dapr Kubernetes 服务
前置条件
1. 构建 Dapr 调试二进制文件
为了调试 Dapr Kubernetes 服务,需要重新构建所有 Dapr 二进制文件和 Docker 镜像以禁用编译器优化。为此,执行以下命令:
git clone https://github.com/dapr/dapr.git
cd dapr
make release GOOS=linux GOARCH=amd64 DEBUG=1
在 Windows 上下载 MingGW 并使用
ming32-make.exe代替make。
在上述命令中,‘DEBUG’ 被指定为 ‘1’ 以禁用编译器优化。‘GOOS=linux’ 和 ‘GOARCH=amd64’ 也是必需的,因为二进制文件将在下一步被打包到基于 Linux 的 Docker 镜像中。
二进制文件可以在 ‘dapr’ 目录下的 ‘dist/linux_amd64/debug’ 子目录中找到。
2. 构建 Dapr 调试 Docker 镜像
使用以下命令将调试二进制文件打包到 Docker 镜像中。在此之前,你需要登录到 docker.io 账户,如果你还没有账户,可能需要考虑从 “https://hub.docker.com/" 注册一个。
export DAPR_TAG=dev
export DAPR_REGISTRY=<your docker.io id>
docker login
make docker-push DEBUG=1
一旦 Dapr Docker 镜像构建完成并推送到 Docker hub,你就可以在 Kubernetes 集群中重新安装 Dapr 了。
3. 安装 Dapr 调试二进制文件
如果 Kubernetes 集群中已经安装了 Dapr,请先卸载:
dapr uninstall -k
我们将使用 ‘helm’ 来安装 Dapr 调试二进制文件。在以下章节中,我们将以 Dapr operator 为例演示如何在 Kubernetes 环境中配置、安装和调试 Dapr 服务。
首先使用这些选项配置一个 values 文件:
global:
registry: docker.io/<your docker.io id>
tag: "dev-linux-amd64"
dapr_operator:
debug:
enabled: true
initialDelaySeconds: 3000
注意
如果需要调试 Dapr 服务的启动时间,需要考虑将initialDelaySeconds 配置为一个很长的时间值,例如 “3000” 秒。如果不是这种情况,将其配置为一个短时间值,例如 “3” 秒。然后进入本指南开始时从 GitHub 克隆的 ‘dapr’ 目录(如果还没有的话),并执行以下命令:
helm install dapr charts/dapr --namespace dapr-system --values values.yml --wait
4. 转发调试端口
要调试目标 Dapr 服务(在此例中为 Dapr operator),其预配置的调试端口需要对你的 IDE 可见。为了实现这一点,我们首先需要找到目标 Dapr 服务的 pod:
$ kubectl get pods -n dapr-system -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
dapr-dashboard-64b46f98b6-dl2n9 1/1 Running 0 61s 172.17.0.9 minikube <none> <none>
dapr-operator-7878f94fcd-6bfx9 1/1 Running 1 61s 172.17.0.7 minikube <none> <none>
dapr-placement-server-0 1/1 Running 1 61s 172.17.0.8 minikube <none> <none>
dapr-sentry-68c7d4c7df-sc47x 1/1 Running 0 61s 172.17.0.6 minikube <none> <none>
dapr-sidecar-injector-56c8f489bb-t2st9 1/1 Running 0 61s 172.17.0.10 minikube <none> <none>
然后使用 kubectl 的 port-forward 命令将内部调试端口暴露给外部 IDE:
$ kubectl port-forward dapr-operator-7878f94fcd-6bfx9 40000:40000 -n dapr-system
Forwarding from 127.0.0.1:40000 -> 40000
Forwarding from [::1]:40000 -> 40000
完成了。现在你可以指向端口 40000 并从你喜欢的 IDE 启动远程调试会话。
相关链接
5.1.2 - 在 Kubernetes 上调试 daprd
概述
有时需要了解 Dapr 边车(daprd)的运行状态,它作为边车运行在你的应用旁边,尤其是在诊断 Dapr 应用并怀疑 Dapr 本身存在问题时。此外,你可能正在为 Kubernetes 上的 Dapr 开发新功能,并希望调试你的代码。
本指南介绍如何在 Kubernetes Pod 中使用内置的 Dapr 调试功能来调试 Dapr 边车。要了解如何在 Kubernetes 中查看日志和排查 Dapr 问题,请参阅配置和查看 Dapr 日志指南
前置条件
以调试模式初始化 Dapr
如果 Dapr 已经安装在你的 Kubernetes 集群中,请先卸载它:
dapr uninstall -k
我们将使用 ‘helm’ 来安装 Dapr 调试二进制文件。更多信息请参考使用 Helm 安装。
首先配置一个名为 values.yml 的 values 文件,包含以下选项:
global:
registry: docker.io/<your docker.io id>
tag: "dev-linux-amd64"
然后进入从你克隆的 dapr/dapr 仓库中的 ‘dapr’ 目录,并执行以下命令:
helm install dapr charts/dapr --namespace dapr-system --values values.yml --wait
要为 daprd 启用调试模式,你需要在应用的部署文件中添加一个额外的注解 dapr.io/enable-debug。让我们以 quickstarts/hello-kubernetes 为例。修改 ‘deploy/node.yaml’ 如下:
diff --git a/hello-kubernetes/deploy/node.yaml b/hello-kubernetes/deploy/node.yaml
index 23185a6..6cdb0ae 100644
--- a/hello-kubernetes/deploy/node.yaml
+++ b/hello-kubernetes/deploy/node.yaml
@@ -33,6 +33,7 @@ spec:
dapr.io/enabled: "true"
dapr.io/app-id: "nodeapp"
dapr.io/app-port: "3000"
+ dapr.io/enable-debug: "true"
spec:
containers:
- name: node
注解 dapr.io/enable-debug 将提示 Dapr 注入器以调试模式注入 Dapr 边车。你还可以使用注解 dapr.io/debug-port 指定调试端口,否则默认端口为 “40000”。
使用以下命令部署应用。完整指南请参考 Dapr Kubernetes 快速入门:
kubectl apply -f ./deploy/node.yaml
使用以下命令找出目标应用的 pod 名称:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nodeapp-78866448f5-pqdtr 1/2 Running 0 14s
然后使用 kubectl 的 port-forward 命令将内部调试端口暴露给外部 IDE:
$ kubectl port-forward nodeapp-78866448f5-pqdtr 40000:40000
Forwarding from 127.0.0.1:40000 -> 40000
Forwarding from [::1]:40000 -> 40000
完成。现在你可以指向端口 40000 并从你喜欢的 IDE 启动到 daprd 的远程调试会话。
常用 kubectl 命令
在 Kubernetes 上调试 daprd 和应用时使用以下常用 kubectl 命令。
获取所有 pods、事件和服务:
kubectl get all
kubectl get all --n <namespace>
kubectl get all --all-namespaces
分别获取每一项:
kubectl get pods
kubectl get events --n <namespace>
kubectl get events --sort-by=.metadata.creationTimestamp --n <namespace>
kubectl get services
检查日志:
kubectl logs <podId> daprd
kubectl logs <podId> <myAppContainerName>
kuebctl logs <deploymentId> daprd
kubectl logs <deploymentId> <myAppContainerName>
kubectl describe pod <podId>
kubectl describe deploy <deployId>
kubectl describe replicaset <replicasetId>
通过运行以下命令重启 pod:
kubectl delete pod <podId>
这将导致 replicaset 控制器在删除后重启 pod。
观看演示
请参阅 Dapr Community Call #36 中关于在 Kubernetes 上排查 Dapr 问题的演示。
相关链接
5.2 - 调试在 Docker Compose 中运行的 Dapr 应用
本文的目标是演示一种方法,在保持与 docker compose 环境中部署的其他应用程序集成的同时,调试一个或多个 daprized 应用程序(通过你的 IDE,在本地)。
让我们来看一个 docker compose 文件的最小示例,它只包含两个服务:
nodeapp- 你的应用程序nodeapp-dapr- 你的nodeapp服务的 dapr 边车进程
compose.yml
services:
nodeapp:
build: ./node
ports:
- "50001:50001"
networks:
- hello-dapr
nodeapp-dapr:
image: "daprio/daprd:edge"
command: [
"./daprd",
"--app-id", "nodeapp",
"--app-port", "3000",
"--resources-path", "./components"
]
volumes:
- "./components/:/components"
depends_on:
- nodeapp
network_mode: "service:nodeapp"
networks:
hello-dapr
当你使用 docker compose -f compose.yml up 运行这个 docker 文件时,它将部署到 Docker 并正常运行。
但是,我们如何在保持与运行的 dapr 边车进程集成的同时调试 nodeapp,以及通过 Docker compose 文件部署的其他任何内容呢?
让我们首先引入一个名为 compose.debug.yml 的第二个 docker compose 文件。当运行 up 命令时,这第二个 compose 文件将与第一个 compose 文件协同工作。
compose.debug.yml
services:
nodeapp: # 通过移除端口并将其从网络中隔离来隔离 nodeapp
ports: !reset []
networks: !reset
- ""
nodeapp-dapr:
command: ["./daprd",
"--app-id", "nodeapp",
"--app-port", "8080", # 这必须与你在 IDE 中调试时应用程序暴露的端口相匹配
"--resources-path", "./components",
"--app-channel-address", "host.docker.internal"] # 使边车在主机上查找 App Channel
network_mode: !reset "" # 重置 network_mode...
networks: # ...以便边车可以进入正常网络
- hello-dapr
ports:
- "3500:3500" # 将 HTTP 端口暴露给主机
- "50001:50001" # 将 GRPC 端口暴露给主机(Dapr Workflows 依赖于 GRPC 通道)
接下来,确保你的 nodeapp 在你选择的 IDE 中运行/调试,并在你在上面的 compose.debug.yml 中指定的同一端口上暴露 - 在上面的示例中,这设置为端口 8080。
接下来,停止你可能已启动的任何现有 compose 会话,并运行以下命令来将两个 docker compose 文件组合运行:
docker compose -f compose.yml -f compose.debug.yml up
现在你应该发现,dapr 边车和你的调试应用程序将具有双向通信,就像它们在 Docker compose 环境中正常运行一样。
注意:需要强调的是,docker compose 环境中的 nodeapp 服务实际上仍在运行,但它已从 docker 网络中移除,因此实际上已被孤立,没有任何东西可以与之通信。
演示:观看此视频,了解如何使用 Docker Compose 调试本地 Dapr 应用
6 - 集成
6.1 - 与 AWS 的集成
6.1.1 - AWS 认证
利用 AWS 服务的 Dapr 组件(例如 DynamoDB、SQS、S3)通过 AWS SDK 使用标准化的配置属性。了解更多关于 AWS SDK 如何处理凭据的信息。
您可以使用 AWS SDK 的默认提供程序链或下面列出的预定义 AWS 认证配置文件之一来配置认证。通过测试和检查 Dapr 运行时日志来确认正确初始化,从而验证您的组件配置。
术语
- ARN (Amazon Resource Name,Amazon 资源名称): 用于指定 AWS 资源的唯一标识符。格式:
arn:partition:service:region:account-id:resource。示例:arn:aws:iam::123456789012:role/example-role。 - IAM (Identity and Access Management,身份和访问管理): 用于安全管理 AWS 资源访问的 AWS 服务。
认证配置文件
访问密钥 ID 和秘密访问密钥
使用静态访问密钥和秘密密钥凭据,可以通过组件元数据字段或通过默认 AWS 配置实现。
重要
在以下场景中,建议通过默认 AWS 配置加载凭据:
- 在 EKS (AWS Kubernetes) 上与您的应用程序一起运行 Dapr 边车 (
daprd)。 - 使用附加到 IAM 策略的节点或 Pod,这些策略定义了 AWS 资源访问权限。
| 属性 | 必填 | 描述 | 示例 |
|---|---|---|---|
region | Y | 要连接的 AWS 区域。 | “us-east-1” |
accessKey | N | AWS 访问密钥 ID。在 Dapr v1.17 中将是必填项。 | “AKIAIOSFODNN7EXAMPLE” |
secretKey | N | AWS 秘密访问密钥,与 accessKey 一起使用。在 Dapr v1.17 中将是必填项。 | “wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY” |
sessionToken | N | AWS 会话令牌,与 accessKey 和 secretKey 一起使用。对于 IAM 用户密钥通常不需要。 |
承担 IAM 角色
此配置文件允许 Dapr 承担特定的 IAM 角色。通常在 Dapr 边车运行在 EKS 上或附加到 IAM 策略的节点/ Pod 上时使用。目前由 Kafka 和 PostgreSQL 组件支持。
| 属性 | 必填 | 描述 | 示例 |
|---|---|---|---|
region | Y | 要连接的 AWS 区域。 | “us-east-1” |
assumeRoleArn | N | 具有 AWS 资源访问权限的 IAM 角色的 ARN。在 Dapr v1.17 中将是必填项。 | “arn:aws:iam::123456789:role/mskRole” |
sessionName | N | 角色承担的会话名称。默认为 "DaprDefaultSession"。 | “MyAppSession” |
来自环境变量的凭据
使用环境变量进行认证。这对于边车注入器不配置环境变量的自托管模式下的 Dapr 特别有用。
此认证配置文件不需要元数据字段。
IAM Roles Anywhere
IAM Roles Anywhere 将基于 IAM 角色的认证扩展到外部工作负载。它通过使用加密签名证书消除了对长期凭据的需求,该证书使用 Dapr PKI 建立在信任关系中。Dapr SPIFFE 身份 X.509 证书用于向 AWS 服务进行认证,Dapr 在会话生命周期的一半时处理凭据轮换。
要配置此认证配置文件:
- 在信任 AWS 账户中使用 Dapr 证书包作为
External certificate bundle创建信任锚点。 - 创建一个具有必要资源权限策略以及 Roles Anywhere AWS 服务的信任实体的 IAM 角色。在此处,您可以指定允许的 SPIFFE 身份。
- 在 Roles Anywhere 服务下创建一个 IAM 配置文件,链接 IAM 角色。
| 属性 | 必填 | 描述 | 示例 |
|---|---|---|---|
trustAnchorArn | Y | AWS 账户中授予 Dapr 证书颁发机构信任的信任锚点的 ARN。 | arn:aws:rolesanywhere:us-west-1:012345678910:trust-anchor/01234568-0123-0123-0123-012345678901 |
trustProfileArn | Y | 信任 AWS 账户中 AWS IAM 配置文件的 ARN。 | arn:aws:rolesanywhere:us-west-1:012345678910:profile/01234568-0123-0123-0123-012345678901 |
assumeRoleArn | Y | 信任 AWS 账户中要承担的 AWS IAM 角色的 ARN。 | arn:aws:iam:012345678910:role/exampleIAMRoleName |
附加字段
某些 AWS 组件包含其他可选字段:
| 属性 | 必填 | 描述 | 示例 |
|---|---|---|---|
endpoint | N | 端点通常由 AWS SDK 内部处理。但是,在某些情况下,在本地设置它可能是有意义的 - 例如,针对 DynamoDB Local 进行开发时。 |
此外,支持 AWS 认证配置文件的非原生 AWS 组件(如 Kafka 和 PostgreSQL)具有用于触发 AWS 认证逻辑的元数据字段。请务必查看特定组件文档。
在组件清单文件中显式指定凭据的替代方案
在生产场景中,建议使用以下解决方案:
如果在 AWS EKS 上运行,您可以将 IAM 角色链接到 Kubernetes 服务账户,您的 Pod 可以使用该角色。
所有这些解决方案都解决了相同的问题:它们允许 Dapr 运行时进程(或边车)动态检索凭据,因此不需要显式凭据。这提供了几个好处,例如自动密钥轮换,以及避免管理机密。
Kiam 和 Kube2IAM 都通过拦截对实例元数据服务的调用来工作。
使用 AWS EKS Pod Identity 设置 Dapr
EKS Pod 身份提供了为您的应用程序管理凭据的能力,类似于 Amazon EC2 实例配置文件为 Amazon EC2 实例提供凭据的方式。您可以将 IAM 角色与 Kubernetes 服务账户关联,并配置您的 Pod 使用该服务账户,而不是创建和分发 AWS 凭据到容器或使用 Amazon EC2 实例的角色。
要查看有关如何使用 AWS EKS Pod Identity 从 EKS 授权 Pod 访问 AWS Secrets Manager 的综合示例,请遵循此存储库中的示例。
在 AWS EC2 上以独立模式运行时使用实例配置文件
如果以独立模式直接在 AWS EC2 实例上运行 Dapr,您可以使用实例配置文件。
- 配置 IAM 角色。
- 将其附加到 ec2 实例的实例配置文件。
然后,Dapr 在不需要在 Dapr 组件清单中指定凭据的情况下向 AWS 进行认证。
在独立模式下本地运行 dapr 时向 AWS 进行认证
当以独立模式运行 Dapr(或直接运行 Dapr 运行时)时,您可以将环境变量注入到进程中,如下例所示:
FOO=bar daprd --app-id myapp
如果您已在本地配置了命名的 AWS 配置文件,可以通过指定 “AWS_PROFILE” 环境变量来告诉 Dapr(或 Dapr 运行时)使用哪个配置文件:
AWS_PROFILE=myprofile dapr run...
或
AWS_PROFILE=myprofile daprd...
您可以使用任何支持的环境变量以这种方式配置 Dapr。
在 Windows 上,需要在启动 dapr 或 daprd 命令之前设置环境变量,不支持像 Linux/MacOS 那样内联设置。
如果使用基于 AWS SSO 的配置文件,则向 AWS 进行认证
如果您使用 AWS SSO 向 AWS 进行认证,适用于 Go 的 AWS SDK(v1 和 v2)为 AWS SSO 凭据提供程序提供原生支持。这意味着您可以直接使用 AWS SSO 配置文件,而无需其他实用程序。
有关适用于 Go 的 AWS SDK 中 AWS SSO 支持的更多信息,请参阅 AWS 博客文章。
后续步骤
参考 AWS 组件规范 >>相关链接
6.2 - 与 Azure 的集成
6.2.1 - 对 Azure 进行身份验证
6.2.1.1 - 向 Azure 进行身份验证
关于使用 Microsoft Entra ID 进行身份验证
Microsoft Entra ID 是 Azure 的标识和访问管理 (IAM) 解决方案,用于对用户和服务进行身份验证和授权。它构建在 OAuth 2.0 等开放标准之上,允许服务(应用程序)获取访问令牌以向 Azure 服务发出请求,包括 Azure Storage、Azure Service Bus、Azure Key Vault、Azure Cosmos DB、Azure Database for Postgres、Azure SQL 等。
身份验证选项
应用程序可以通过多种方法使用 Microsoft Entra ID 进行身份验证并获取访问令牌以向 Azure 服务发出请求:
- 工作负载标识联合 - 配置 Microsoft Entra ID 租户以信任外部标识提供者的推荐方式。这包括来自 Kubernetes 或 AKS 集群的服务账户。了解有关工作负载标识联合的更多信息。
- 系统分配和用户分配的托管标识 - 不如工作负载标识联合精细,但保留了部分优势。了解有关系统分配和用户分配的托管标识的更多信息。
- [客户端 ID 和密钥]({{ < ref howto-aad.md >}}) - 不推荐,因为它要求您在应用程序级别维护和关联凭据。
- Pod 标识 - 已弃用的方法,用于对在 Kubernetes Pod 上运行的应用程序进行身份验证,在 Pod 级别进行。不应再使用此方法。
如果您刚刚开始,建议使用工作负载标识联合。
托管标识和工作负载标识联合
使用托管标识 (MI),您的应用程序可以使用 Microsoft Entra ID 进行身份验证并获取访问令牌以向 Azure 服务发出请求。当您的应用程序在支持的 Azure 服务(例如 Azure VM、Azure Container Apps、Azure Web Apps 等)上运行时,可以在基础设施级别为您的应用程序分配标识。您还可以设置 Microsoft Entra ID 以使用联合标识凭据直接将联合信任委托给您的 Dapr 应用程序标识。这允许您配置对 Microsoft 资源的访问权限,即使不在 Microsoft 基础设施上运行也是如此。要了解如何配置 Dapr 以使用联合标识,请参阅使用联合标识凭据进行身份验证部分。 这是通过系统分配或用户分配的托管标识或工作负载标识联合完成的。
使用托管标识后,您的代码不必处理凭据,这可以:
- 消除安全管理凭据的挑战
- 允许开发团队和运营团队之间更好地分离关注点
- 减少有权访问凭据的人数
- 简化操作方面——尤其是在使用多个环境时
虽然某些 Dapr Azure 组件提供替代身份验证方法,例如基于"共享密钥"或"访问令牌"的系统,但只要有可能,您应该始终尝试使用 Microsoft Entra ID 对 Dapr 组件进行身份验证。这提供了许多好处,包括:
建议在 Azure Kubernetes Service 上运行的应用程序利用工作负载标识联合自动为各个 Pod 提供标识。
基于角色的访问控制
在对受支持的服务使用 Azure 基于角色的访问控制 (RBAC) 时,可以微调授予应用程序的权限。例如,您可以限制对数据子集的访问或使访问变为只读。
审核
使用 Microsoft Entra ID 可提供改进的访问审核体验。租户管理员可以查看审核日志以跟踪身份验证请求。
(可选) 使用证书进行身份验证
虽然 Microsoft Entra ID 允许您使用 MI,但您仍然可以选择使用证书进行身份验证。
对其他 Azure 环境的支持
默认情况下,Dapr 组件配置为与"公有云"中的 Azure 资源进行交互。如果您的应用程序部署到另一个云,例如 Azure China 或 Azure Government(“主权云”),可以通过将 azureEnvironment 元数据属性设置为支持的值之一,为支持的组件启用该功能:
- Azure 公有云(默认):
"AzurePublicCloud" - Azure 中国:
"AzureChinaCloud" - Azure 政府:
"AzureUSGovernmentCloud"
对主权云的支持是实验性的。
凭据元数据字段
要使用 Microsoft Entra ID 进行身份验证,您需要将以下凭据作为值添加到您的 Dapr 组件的元数据中。
元数据选项
根据您如何将凭据传递给 Dapr 服务,您有多个元数据选项。
使用客户端凭据进行身份验证
| 字段 | 必填 | 详情 | 示例 |
|---|---|---|---|
azureTenantId | Y | Microsoft Entra ID 租户的 ID | "cd4b2887-304c-47e1-b4d5-65447fdd542b" |
azureClientId | Y | 客户端 ID (应用程序 ID) | "c7dd251f-811f-4ba2-a905-acd4d3f8f08b" |
azureClientSecret | Y | 客户端密钥 (应用程序密码) | "Ecy3XG7zVZK3/vl/a2NSB+a1zXLa8RnMum/IgD0E" |
在 Kubernetes 上运行时,您还可以对上述任何或所有值使用对 Kubernetes 机密的引用。
使用证书进行身份验证
| 字段 | 必填 | 详情 | 示例 |
|---|---|---|---|
azureTenantId | Y | Microsoft Entra ID 租户的 ID | "cd4b2887-304c-47e1-b4d5-65447fdd542b" |
azureClientId | Y | 客户端 ID (应用程序 ID) | "c7dd251f-811f-4ba2-a905-acd4d3f8f08b" |
azureCertificate | azureCertificate 和 azureCertificateFile 之一 | 证书和私钥 (PFX/PKCS#12 格式) | "-----BEGIN PRIVATE KEY-----\n MIIEvgI... \n -----END PRIVATE KEY----- \n -----BEGIN CERTIFICATE----- \n MIICoTC... \n -----END CERTIFICATE-----" |
azureCertificateFile | azureCertificate 和 azureCertificateFile 之一 | 包含证书和私钥的 PFX/PKCS#12 文件的路径 | "/path/to/file.pem" |
azureCertificatePassword | N | 证书的密码(如果已加密) | "password" |
在 Kubernetes 上运行时,您还可以对上述任何或所有值使用对 Kubernetes 机密的引用。
使用托管标识 (MI) 进行身份验证
| 字段 | 必填 | 详情 | 示例 |
|---|---|---|---|
azureClientId | N | 客户端 ID (应用程序 ID) | "c7dd251f-811f-4ba2-a905-acd4d3f8f08b" |
使用托管标识,通常推荐使用 azureClientId 字段。使用系统分配的标识时,该字段是可选的,但在使用用户分配的标识时可能是必需的。
在 AKS 上使用工作负载标识进行身份验证
在 Azure Kubernetes Service (AKS) 上运行时,您可以使用工作负载标识对组件进行身份验证。请参阅 Azure AKS 文档中关于为 Kubernetes 资源启用工作负载标识的内容。
使用联合标识凭据进行身份验证
您可以使用 Microsoft Entra ID 中的联合标识凭据直接将联合信任委托给您的 Dapr 安装,无论其运行在何处。这允许您跨不同云一致地针对 Dapr 应用程序的 SPIFFE ID 轻松配置访问规则。
为了联合信任,您必须运行启用了 JWT 颁发和 OIDC 发现的 Dapr Sentry。可以使用以下 Dapr Sentry helm 值配置这些功能:
jwt:
# 通过 Sentry 启用 JWT 令牌颁发
enabled: true
# JWT 令牌的颁发者值
issuer: "<your-issuer-domain>"
oidc:
enabled: true
server:
# OIDC HTTP 服务器的端口
port: 9080
tls:
# 为 OIDC HTTP 服务器启用 TLS
enabled: true
# OIDC HTTP 服务器的 TLS 证书文件
certFile: "<path-to-tls-cert.pem>"
# OIDC HTTP 服务器的 TLS 证书文件
keyFile: "<path-to-tls-key.pem>"
警告
issuer 值必须与您在 Microsoft Entra ID 中创建联合标识凭据时提供的值完全匹配。提供这些设置后,在提供的 OIDC HTTP 端口上的 Dapr Sentry 安装上会公开以下端点:
/.well-known/openid-configuration
/jwks.json
您还需要提供 Dapr 运行时配置以请求具有 Azure 受众 api://AzureADTokenExchange 的 JWT 令牌。
在独立模式下运行时,可以使用标志 --sentry-request-jwt-audiences=api://AzureADTokenExchange 提供。
在 Kubernetes 上运行时,可以通过使用注解 "dapr.io/sentry-request-jwt-audiences": "api://AzureADTokenExchange" 修饰应用程序 Kubernetes 清单来提供。
这可确保 Sentry 服务颁发具有正确受众的 JWT 令牌,这是 Microsoft Entra ID 验证令牌所必需的。
为了使 Microsoft Entra ID 能够访问 OIDC 端点,您必须在公共地址上公开它们。您必须确保提供这些端点的域与配置 Dapr Sentry 时提供的颁发者相同。
现在您可以在 Microsoft Entra ID 中创建联合凭据。
cat > creds.json <<EOF
{
"name": "DaprAppIDSpiffe",
"issuer": "https://<your-issuer-domain>",
"subject": spiffe://public/ns/<dapr-app-id-namespace>/<dapr-app-id>",
"audiences": ["api://AzureADTokenExchange"],
"description": "Credential for Dapr App ID"
}
EOF
export APP_ID=$(az ad app create --display-name my-dapr-app --enable-access-token-issuance --enable-id-token-issuance | jq .id)
az ad sp create --id $APP_ID
az ad app federated-credential create --id $APP_ID --parameters ./creds.json
现在您已经有了 Microsoft Entra ID 应用程序注册的联合凭据,可以为其服务主体分配所需的角色。
下面是分配"Storage Blob Data Owner"角色的示例。
az role assignment create --assignee-object-id $APP_ID --assignee-principal-type ServicePrincipal --role "Storage Blob Data Owner" --scope "/subscriptions/$SUBSCRIPTION/resourceGroups/$GROUP/providers/Microsoft.Storage/storageAccounts/$ACCOUNT_NAME"
要配置 Dapr 组件以使用联合凭据访问 Azure 资源,首先需要获取您的 clientId 和 tenantId:
CLIENT_ID=$(az ad app show --id $APP_ID --query appId --output tsv)
TENANT_ID=$(az account show --query tenantId --output tsv)
然后您可以创建 Azure Dapr 组件并只需提供这些值:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: azureblob
spec:
type: state.azure.blobstorage
version: v2
initTimeout: 10s # 增加初始化超时以允许 Azure 有足够的时间执行令牌交换
metadata:
- name: clientId
value: $CLIENT_ID
- name: tenantId
value: $TENANT_ID
- name: accountName
value: $ACCOUNT_NAME
- name: containerName
value: $CONTAINER_NAME
Dapr 运行时使用这些详细信息通过 Microsoft Entra ID 进行身份验证,使用 Dapr Sentry 颁发的 JWT 令牌交换访问令牌以访问 Azure 资源。
使用 Azure CLI 凭据进行身份验证(仅限开发)
重要提示: 此身份验证方法仅建议用于开发。
此身份验证方法在本地计算机上开发时可能很有用。您需要:
- 已安装 Azure CLI
- 已成功使用
az login命令进行身份验证
当 Dapr 在有 Azure CLI 凭据可用的主机上运行时,如果没有配置其他身份验证方法,组件可以使用这些凭据自动进行身份验证。
使用此身份验证方法不需要设置任何元数据选项。
在 Dapr 组件中的使用示例
在此示例中,您将设置一个使用 Microsoft Entra ID 进行身份验证的 Azure Key Vault 机密存储组件。
要使用客户端密钥,在组件目录中创建一个名为 azurekeyvault.yaml 的文件,填入上述设置过程中的详细信息:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: azurekeyvault
namespace: default
spec:
type: secretstores.azure.keyvault
version: v1
metadata:
- name: vaultName
value: "[your_keyvault_name]"
- name: azureTenantId
value: "[your_tenant_id]"
- name: azureClientId
value: "[your_client_id]"
- name: azureClientSecret
value : "[your_client_secret]"
如果您想使用保存在本地磁盘上的证书,请改用:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: azurekeyvault
namespace: default
spec:
type: secretstores.azure.keyvault
version: v1
metadata:
- name: vaultName
value: "[your_keyvault_name]"
- name: azureTenantId
value: "[your_tenant_id]"
- name: azureClientId
value: "[your_client_id]"
- name: azureCertificateFile
value : "[pfx_certificate_file_fully_qualified_local_path]"
在 Kubernetes 中,您将客户端密钥或证书存储到 Kubernetes 机密存储中,然后在 YAML 文件中引用它们。
要使用客户端密钥:
使用以下命令创建 Kubernetes 机密:
kubectl create secret generic [your_k8s_secret_name] --from-literal=[your_k8s_secret_key]=[your_client_secret][your_client_secret]是如上生成的应用程序客户端密钥[your_k8s_secret_name]是 Kubernetes 机密存储中的机密名称[your_k8s_secret_key]是 Kubernetes 机密存储中的机密键
创建一个
azurekeyvault.yaml组件文件。组件 yaml 使用
auth属性引用 Kubernetes 机密存储,secretKeyRef引用存储在 Kubernetes 机密存储中的客户端密钥。apiVersion: dapr.io/v1alpha1 kind: Component metadata: name: azurekeyvault namespace: default spec: type: secretstores.azure.keyvault version: v1 metadata: - name: vaultName value: "[your_keyvault_name]" - name: azureTenantId value: "[your_tenant_id]" - name: azureClientId value: "[your_client_id]" - name: azureClientSecret secretKeyRef: name: "[your_k8s_secret_name]" key: "[your_k8s_secret_key]" auth: secretStore: kubernetes应用
azurekeyvault.yaml组件:kubectl apply -f azurekeyvault.yaml
要使用证书:
使用以下命令创建 Kubernetes 机密:
kubectl create secret generic [your_k8s_secret_name] --from-file=[your_k8s_secret_key]=[pfx_certificate_file_fully_qualified_local_path][pfx_certificate_file_fully_qualified_local_path]是您之前获得的 PFX 文件的路径[your_k8s_secret_name]是 Kubernetes 机密存储中的机密名称[your_k8s_secret_key]是 Kubernetes 机密存储中的机密键
创建一个
azurekeyvault.yaml组件文件。组件 yaml 使用
auth属性引用 Kubernetes 机密存储,secretKeyRef引用存储在 Kubernetes 机密存储中的证书。apiVersion: dapr.io/v1alpha1 kind: Component metadata: name: azurekeyvault namespace: default spec: type: secretstores.azure.keyvault version: v1 metadata: - name: vaultName value: "[your_keyvault_name]" - name: azureTenantId value: "[your_tenant_id]" - name: azureClientId value: "[your_client_id]" - name: azureCertificate secretKeyRef: name: "[your_k8s_secret_name]" key: "[your_k8s_secret_key]" auth: secretStore: kubernetes应用
azurekeyvault.yaml组件:kubectl apply -f azurekeyvault.yaml
后续步骤
生成新的 Microsoft Entra ID 应用程序和服务主体 >>参考
6.2.1.2 - 如何使用工作负载身份联合
本指南将帮助你配置 Kubernetes 集群,以在 Azure 上运行 Dapr 并使用工作负载身份联合。
它是什么?
工作负载身份联合 是一种让应用程序向 Azure 进行身份验证的方式,无需在发布过程中存储或管理凭据。
通过使用工作负载身份联合,任何在 Kubernetes 和 AKS 上运行并面向 Azure 的 Dapr 组件都可以透明地进行身份验证,无需额外配置。
指南
我们将演示如何针对 AKS 集群配置 Azure Key Vault 资源。你可以根据需要调整本指南,将其应用于不同的 Dapr Azure 组件。
在本操作指南中,我们将使用此 Dapr AKS secrets 示例应用。
前提条件
- 已启用工作负载身份的 AKS 集群
- Microsoft Entra ID 租户
1 - 启用工作负载身份联合
按照 在 AKS 集群上启用工作负载身份联合的 Azure 文档 进行操作。
该操作指南将引导你配置 Azure Entra ID 租户以信任来自 AKS 集群颁发者的身份。它还会指导你设置一个 Kubernetes 服务账户,该账户与你创建的 Azure 托管标识相关联。
完成后,返回此处继续执行步骤 2。
2 - 向 Azure Key Vault 添加密钥
在你创建的 Azure Key Vault 中,添加一个名为 dapr 的密钥,其值为 Hello Dapr!。
3 - 配置 Azure Key Vault dapr 组件
此时,你应该拥有一个名称类似于 workload-identity-sa0a1b2c 的 Kubernetes 服务账户。
将以下内容应用到你的 Kubernetes 集群,记得将 your-key-vault 替换为你的密钥保管库名称:
---
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: demo-secret-store # 请勿更改此名称,我们的应用将查找它。
spec:
type: secretstores.azure.keyvault
version: v1
metadata:
- name: vaultName
value: your-key-vault # 替换
你会注意到,我们在组件定义中没有提供任何与身份验证相关的详细信息。这是有意为之,因为 Dapr 能够利用 Kubernetes 服务账户向 Azure 进行透明身份验证。
4 - 部署测试应用程序
前往 工作负载身份联合示例应用程序 并准备镜像的构建。
确保镜像已推送到你的 AKS 集群可见并有权拉取的注册表。
接下来,为我们的示例 AKS secrets 应用容器创建一个部署,同时包含一个 Dapr 边车。
记得将 dapr-wif-k8s-service-account 替换为你的服务账户名称,将 dapraksworkloadidentityfederation 替换为你的集群可以解析的镜像:
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: aks-dapr-wif-secrets
labels:
app: aks-dapr-wif-secrets
spec:
replicas: 1
selector:
matchLabels:
app: aks-dapr-wif-secrets
template:
metadata:
labels:
app: aks-dapr-wif-secrets
azure.workload.identity/use: "true" # 重要
annotations:
dapr.io/enabled: "true" # 启用 Dapr
dapr.io/app-id: "aks-dapr-wif-secrets"
spec:
serviceAccountName: dapr-wif-k8s-service-account # 记得替换
containers:
- name: workload-id-demo
image: dapraksworkloadidentityfederation # 记得替换
imagePullPolicy: Always
应用程序启动并运行后,应该输出以下内容:
Fetched Secret: Hello dapr!
6.2.1.3 - 操作指南:生成新的 Microsoft Entra ID 应用程序和服务主体
前置条件
使用 Azure CLI 登录 Azure
在新的终端中,运行以下命令:
az login
az account set -s [your subscription id]
创建 Microsoft Entra ID 应用程序
使用以下命令创建 Microsoft Entra ID 应用程序:
# 应用程序 / 服务主体的友好名称
APP_NAME="dapr-application"
# 创建应用程序
APP_ID=$(az ad app create --display-name "${APP_NAME}" | jq -r .appId)
选择您希望传递凭据的方式。
要创建 client secret(客户端密钥),请运行以下命令。
az ad app credential reset \
--id "${APP_ID}" \
--years 2
这将基于 base64 字符集生成一个随机的 40 字符长度的密码。此密码有效期为 2 年,之后您需要轮换它。
保存返回的输出值;Dapr 需要这些值来向 Azure 进行身份验证。预期输出:
{
"appId": "<your-app-id>",
"password": "<your-password>",
"tenant": "<your-azure-tenant>"
}
将返回的值添加到 Dapr 组件的元数据时:
appId是azureClientId的值password是azureClientSecret的值(这是随机生成的)tenant是azureTenantId的值
对于 PFX (PKCS#12) 证书,请运行以下命令来创建自签名证书:
az ad app credential reset \
--id "${APP_ID}" \
--create-cert
注意: 自签名证书仅建议用于开发环境。在生产环境中,您应该使用由 CA 签名并通过
--cert标志导入的证书。
上述命令的输出应如下所示:
保存返回的输出值;Dapr 需要这些值来向 Azure 进行身份验证。预期输出:
{
"appId": "<your-app-id>",
"fileWithCertAndPrivateKey": "<file-path>",
"password": null,
"tenant": "<your-azure-tenant>"
}
将返回的值添加到 Dapr 组件的元数据时:
appId是azureClientId的值tenant是azureTenantId的值fileWithCertAndPrivateKey指示自签名 PFX 证书和私钥的位置。使用该文件的内容作为azureCertificate(或将其写入服务器上的文件并使用azureCertificateFile)
注意: 虽然生成的文件具有
.pem扩展名,但它包含以 PFX (PKCS#12) 编码的证书和私钥。
创建服务主体
创建 Microsoft Entra ID 应用程序后,为该应用程序创建服务主体。使用此服务主体,您可以授予其对 Azure 资源的访问权限。
要创建服务主体,请运行以下命令:
SERVICE_PRINCIPAL_ID=$(az ad sp create \
--id "${APP_ID}" \
| jq -r .id)
echo "Service Principal ID: ${SERVICE_PRINCIPAL_ID}"
预期输出:
Service Principal ID: 1d0ccf05-5427-4b5e-8eb4-005ac5f9f163
上面返回的值是 服务主体 ID,它不同于 Microsoft Entra ID 应用程序 ID(客户端 ID)。服务主体 ID 在 Azure 租户内定义,用于授予应用程序对 Azure 资源的访问权限。
您将使用服务主体 ID 来授予应用程序访问 Azure 资源的权限。
同时,客户端 ID 由您的应用程序用于身份验证。您将在 Dapr 清单中使用客户端 ID 来配置与 Azure 服务的身份验证。
请记住,刚刚创建的服务主体默认情况下无权访问任何 Azure 资源。需要根据需要授予对每个资源的访问权限,如组件文档中所述。
后续步骤
使用托管标识 >>6.2.1.4 - 如何:使用托管标识
使用托管标识时,身份验证会自动进行,因为你的应用程序运行在已启用系统托管标识或用户分配标识的 Azure 服务之上。
要开始使用,你需要在各种 Azure 服务中启用托管标识作为服务选项/功能,这与 Dapr 无关。启用此功能会在底层为 Microsoft Entra ID(以前称为 Azure Active Directory ID)创建一个标识(或应用程序)。
然后,你的 Dapr 服务可以利用该标识与 Microsoft Entra ID 进行身份验证,这一过程是透明的,无需你指定任何凭据。
在本指南中,你将学习如何:
- 通过官方 Azure 文档向你的标识授予对正在使用的 Azure 服务的访问权限
- 在你的组件中设置系统托管标识或用户分配标识
大概就是这么多了。
注意
在你的组件 YAML 中,仅在使用用户分配标识时才需要azureClientId 属性。否则,你可以省略此属性以默认使用系统托管标识。向服务授予权限
为特定的 Azure 资源(由资源范围标识)设置所需的 Microsoft Entra ID 角色分配或自定义权限,以应用于你的系统托管标识或用户分配标识。
你可以将托管标识设置到新的或现有的 Azure 资源上。具体说明取决于所使用的服务。请查看以下官方文档以获取最合适的说明:
- Azure Kubernetes Service (AKS)
- Azure Container Apps (ACA)
- Azure App Service(包括 Azure Web Apps 和 Azure Functions)
- Azure Virtual Machines (VM)
- Azure Virtual Machines Scale Sets (VMSS)
- Azure Container Instance (ACI)
在将系统托管标识分配给你的 Azure 资源后,你将获得类似以下的凭据:
{
"principalId": "<object-id>",
"tenantId": "<tenant-id>",
"type": "SystemAssigned",
"userAssignedIdentities": null
}
从返回的值中,请注意 principalId 值,这是为你的标识创建的服务主体 ID。使用该值授予你的 Azure 资源组件访问该标识的权限。
Azure Container Apps 中的托管标识
每个容器应用都有完全不同的系统托管标识,这使得在多个应用之间管理所需的角色分配变得非常困难。
相反,_强烈建议_使用用户分配标识并将其附加到所有应加载该组件的应用上。然后,你应该将组件范围限定为这些相同的应用。
在组件中设置标识
默认情况下,Dapr Azure 组件会查找其运行环境的系统托管标识并以此身份进行身份验证。通常,对于给定组件,除了服务名称、存储帐户名称以及 Azure 服务所需的任何其他属性(在文档中列出)之外,使用系统托管标识不需要其他必需属性。
对于用户分配标识,除了所使用服务所需的基本属性外,你还需要在组件中指定 azureClientId(用户分配标识 ID)。确保用户分配标识已附加到运行 Dapr 的 Azure 服务上,否则你将无法使用该标识。
注意
如果边车加载的组件未指定azureClientId,它只会尝试系统分配标识。如果组件指定了 azureClientId 属性,它只会尝试具有该 ID 的特定用户分配标识。以下示例演示如何在 Azure KeyVault secrets 组件中设置系统托管标识或用户分配标识。
如果你使用 Azure KeyVault 组件设置系统托管标识,YAML 将如下所示:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: azurekeyvault
spec:
type: secretstores.azure.keyvault
version: v1
metadata:
- name: vaultName
value: mykeyvault
在此示例中,系统托管标识会查找服务标识并与 mykeyvault 保管库通信。接下来,向你的系统托管标识授予对所需服务的访问权限。
如果你使用 Azure KeyVault 组件设置用户分配标识,YAML 将如下所示:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: azurekeyvault
spec:
type: secretstores.azure.keyvault
version: v1
metadata:
- name: vaultName
value: mykeyvault
- name: azureClientId
value: someAzureIdentityClientIDHere
一旦你使用 azureClientId 属性设置了组件 YAML,你就可以向你的用户分配标识授予对服务的访问权限。
对于 Kubernetes 或 AKS 中的组件配置,请参阅 Workload Identity 指南。
故障排除
如果你收到错误或托管标识未按预期工作,请检查以下项目是否为真:
系统托管标识或用户分配标识在目标资源上没有所需的权限。
用户分配标识未附加到正在加载组件的 Azure 服务(容器应用或 Pod)。这种情况特别可能发生在:
- 你有一个未限定范围的组件(一个由环境中所有容器应用或 AKS 集群中所有部署加载的组件)。
- 你只将用户分配标识附加到一个容器应用或 AKS 中的一个部署(使用 Azure Workload Identity)。
在此场景中,由于标识未附加到 AKS 中的每个其他容器应用或部署,因此通过
azureClientId引用用户分配标识的组件会失败。
最佳实践: 使用用户分配标识时,请务必将组件范围限定到特定应用!
后续步骤
参考 Azure 组件规范 >>6.2.2 - Azure API Management 的 Dapr 集成策略
Azure API Management 是为后端服务(包括使用 Dapr 构建的服务)创建一致且现代化的 API 网关的一种方式。你可以在自托管的 API Management 网关中启用 Dapr 支持,以允许它们:
- 将请求转发到 Dapr 服务
- 向 Dapr 发布订阅主题发送消息
- 触发 Dapr 输出绑定
试一试 Dapr 与 Azure API Management 集成示例。
详细了解 Dapr 集成策略6.2.3 - Dapr extension for Azure Functions runtime
Dapr 通过一个扩展与 Azure Functions runtime 集成,使函数能够与 Dapr 无缝交互。
- Azure Functions 提供事件驱动的编程模型。
- Dapr 提供云原生构建块。
该扩展将两者结合,用于无服务器和事件驱动应用。
试用 Dapr for Azure Functions 扩展6.2.4 - 适用于 Azure Kubernetes Service (AKS) 的 Dapr 扩展
在 AKS 上安装 Dapr 的推荐方法是使用 AKS Dapr 扩展。该扩展提供以下功能:
- 通过 Azure CLI 命令行参数支持所有原生 Dapr 配置功能
- 可选择启用 Dapr 运行时的自动次版本升级
注意
如果你通过 AKS 扩展安装 Dapr,最佳实践是继续使用该扩展进行 Dapr 的未来管理,而不是使用 Dapr CLI。同时使用这两种工具可能会导致冲突并产生意外行为。使用 AKS 的 Dapr 扩展的前提条件:
详细了解适用于 AKS 的 Dapr 扩展6.3 - Diagrid 集成
6.3.1 - Conductor: Enterprise Dapr for Kubernetes

Diagrid Conductor 快速安全地连接到所有运行 Dapr 和 Daprized applications 的 Kubernetes 集群,提供卓越的运维、安全可靠性和洞察协作能力。
自动化 Dapr 管理
一键安装、升级和修补 Dapr,支持选择性应用更新和自动回滚,确保您始终保持最新状态。
Advisor:发现并自动化最佳实践
获取并应用生产环境最佳实践,通过持续检查防止配置错误,提升安全性、可靠性和性能。
资源使用报告和跟踪
通过研究历史资源使用行为,推荐应用资源优化方案,在 CPU 和内存方面实现显著的成本节约。
应用可视化
应用图通过提供服务和基础设施组件的动态概览,促进开发与运维之间的协作。
Learn more about Diagrid Conductor6.4 - 如何:使用 KEDA 自动扩缩 Dapr 应用
Dapr 采用构建块 API 方式,并提供众多发布订阅组件,这使得编写消息处理应用变得轻而易举。由于 Dapr 可以在多种环境中运行(例如虚拟机、物理机、云或边缘 Kubernetes),因此 Dapr 应用的自动扩缩由托管层管理。
对于 Kubernetes,Dapr 与 KEDA 集成,KEDA 是一个面向 Kubernetes 的事件驱动自动扩缩器。Dapr 的许多发布订阅组件与 KEDA 提供的扩缩器重叠,因此可以轻松配置 Dapr 在 Kubernetes 上的部署,基于背压使用 KEDA 进行自动扩缩。
在本指南中,你将配置一个可扩缩的 Dapr 应用,以及 Kafka topic 上的背压。但是,你可以将此方法应用于 Dapr 提供的_任意_ 发布订阅组件。
注意
如果你使用的是 Azure Container Apps,请参阅官方 Azure 文档了解使用 KEDA 扩缩器扩缩 Dapr 应用。安装 KEDA
要安装 KEDA,请按照 KEDA 网站上的部署 KEDA 说明进行操作。
安装并部署 Kafka
如果你没有 Kafka 服务,可以使用 Helm 将其安装到 Kubernetes 集群中:
helm repo add confluentinc https://confluentinc.github.io/cp-helm-charts/
helm repo update
kubectl create ns kafka
helm install kafka confluentinc/cp-helm-charts -n kafka \
--set cp-schema-registry.enabled=false \
--set cp-kafka-rest.enabled=false \
--set cp-kafka-connect.enabled=false
查看 Kafka 部署状态:
kubectl rollout status deployment.apps/kafka-cp-control-center -n kafka
kubectl rollout status deployment.apps/kafka-cp-ksql-server -n kafka
kubectl rollout status statefulset.apps/kafka-cp-kafka -n kafka
kubectl rollout status statefulset.apps/kafka-cp-zookeeper -n kafka
安装完成后,部署 Kafka 客户端并等待其就绪:
kubectl apply -n kafka -f deployment/kafka-client.yaml
kubectl wait -n kafka --for=condition=ready pod kafka-client --timeout=120s
创建 Kafka topic
创建本示例使用的 topic(demo-topic):
kubectl -n kafka exec -it kafka-client -- kafka-topics \
--zookeeper kafka-cp-zookeeper-headless:2181 \
--topic demo-topic \
--create \
--partitions 10 \
--replication-factor 3 \
--if-not-exists
topic 的
partitions数量与 KEDA 为你的部署创建的最大副本数相关。
部署 Dapr 发布订阅组件
为 Kubernetes 部署 Dapr Kafka 发布订阅组件。将以下 YAML 粘贴到名为 kafka-pubsub.yaml 的文件中:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: autoscaling-pubsub
spec:
type: pubsub.kafka
version: v1
metadata:
- name: brokers
value: kafka-cp-kafka.kafka.svc.cluster.local:9092
- name: authRequired
value: "false"
- name: consumerID
value: autoscaling-subscriber
以上 YAML 定义了你的应用订阅的发布订阅组件,即你之前创建的 topic(demo-topic)。
如果你按照 Kafka Helm 安装说明 操作,可以保留 brokers 值不变。否则,请将此值更改为你的 Kafka brokers 连接字符串。
注意为 consumerID 设置的 autoscaling-subscriber 值。此值稍后用于确保 KEDA 和你的部署使用相同的 Kafka partition offset。
现在,将组件部署到集群:
kubectl apply -f kafka-pubsub.yaml
部署 KEDA Kafka 扩缩器
部署 KEDA 扩缩对象,它会:
- 监控指定 Kafka topic 上的 lag
- 配置 Kubernetes Horizontal Pod Autoscaler (HPA) 以扩缩你的 Dapr 部署
将以下内容粘贴到名为 kafka_scaler.yaml 的文件中,并在所需位置配置你的 Dapr 部署:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: subscriber-scaler
spec:
scaleTargetRef:
name: <REPLACE-WITH-DAPR-DEPLOYMENT-NAME>
pollingInterval: 15
minReplicaCount: 0
maxReplicaCount: 10
triggers:
- type: kafka
metadata:
topic: demo-topic
bootstrapServers: kafka-cp-kafka.kafka.svc.cluster.local:9092
consumerGroup: autoscaling-subscriber
lagThreshold: "5"
让我们回顾一下上述文件中的几个元数据值:
| 值 | 描述 |
|---|---|
scaleTargetRef/name | Deployment 中定义的应用的 Dapr ID(dapr.io/id 注解的值)。 |
pollingInterval | KEDA 检查 Kafka 当前 topic partition offset 的频率(秒)。 |
minReplicaCount | KEDA 为你的部署创建的最少副本数。如果你的应用启动时间较长,最好将其设置为 1 以确保部署始终至少运行一个副本。否则设置为 0,KEDA 会为你创建第一个副本。 |
maxReplicaCount | 你的部署的最大副本数。根据 Kafka partition offset 的工作原理,你不应将此值设置为高于 topic partitions 的总数。 |
triggers/metadata/topic | 应设置为你的 Dapr 部署订阅的同一 topic(本例中为 demo-topic)。 |
triggers/metadata/bootstrapServers | 应设置为 kafka-pubsub.yaml 文件中使用的同一 broker 连接字符串。 |
triggers/metadata/consumerGroup | 应设置为 kafka-pubsub.yaml 文件中 consumerID 的同一值。 |
重要
将 Dapr 服务订阅和 KEDA 扩缩器配置的连接字符串、topic 和消费者组设置为相同的值对于确保自动扩缩正常工作至关重要。将 KEDA 扩缩器部署到 Kubernetes:
kubectl apply -f kafka_scaler.yaml
全部完成!
查看 KEDA 扩缩器工作
现在 ScaledObject KEDA 对象已配置,你的部署将根据 Kafka topic 的 lag 进行扩缩。了解有关为 Kafka topics 配置 KEDA 的更多信息。
按照 KEDA 扩缩器清单中的定义,你现在可以开始向 Kafka topic demo-topic 发布消息,并观察当 lag 阈值高于 5 时 pod 自动扩缩。使用 Dapr Publish CLI 命令向 Kafka Dapr 组件发布消息。
后续步骤
6.5 - 如何:在 GitHub Actions 工作流中使用 Dapr CLI
Dapr 可以通过 GitHub Marketplace 中的 Dapr tool installer 与 GitHub Actions 集成。该安装程序将 Dapr CLI 添加到你的工作流,使你能够在各个环境中部署、管理和升级 Dapr。
通过 Dapr 工具安装程序安装 Dapr CLI
将以下安装程序代码片段复制并粘贴到应用程序的 YAML 文件中:
- name: Dapr tool installer
uses: dapr/setup-dapr@v1
dapr/setup-dapr action 会在 macOS、Linux 和 Windows 运行器上安装指定版本的 Dapr CLI。安装完成后,你可以运行任何 Dapr CLI 命令 来管理 Dapr 环境。
有关所有输入的详细信息,请参阅 action.yml 元数据文件。
示例
例如,对于使用 Azure Kubernetes Service (AKS) 的 Dapr 扩展 的应用程序,应用程序 YAML 将如下所示:
- name: Install Dapr
uses: dapr/setup-dapr@v1
with:
version: '1.18.0'
- name: Initialize Dapr
shell: bash
run: |
# Get the credentials to K8s to use with dapr init
az aks get-credentials --resource-group ${{ env.RG_NAME }} --name "${{ steps.azure-deployment.outputs.aksName }}"
# Initialize Dapr
# Group the Dapr init logs so these lines can be collapsed.
echo "::group::Initialize Dapr"
dapr init --kubernetes --wait --runtime-version ${{ env.DAPR_VERSION }}
echo "::endgroup::"
dapr status --kubernetes
working-directory: ./demos/demo3
后续步骤
- 了解更多关于 GitHub Actions 的信息。
6.6 - 操作指南:使用 Dapr Kubernetes Operator
您可以使用 Dapr Kubernetes Operator 来管理 Dapr 控制平面。使用 operator 可以自动化管理 Kubernetes 模式下 Dapr 控制平面生命周期所需的任务。
安装和使用 Dapr Kubernetes Operator6.7 - 如何:与 Kratix 集成
作为 Kratix Marketplace 的一部分,Dapr 可用于构建满足您需求的定制平台。
注意
Dapr Helm chart 会生成静态公私钥对,并发布在仓库中。此 promise 应仅_本地_用于演示目的。如果您希望将此 promise 用于演示以外的用途,建议使用您自己的凭证密钥手动更新 promise 中的所有密钥。只需安装 Dapr Promise 即可开始使用,它会在所有匹配的集群上安装 Dapr。
安装 Dapr Promise6.8 - 操作指南:与 Argo CD 集成
Argo CD 是一个用于 Kubernetes 的声明式、GitOps 持续交付工具。它使您能够通过跟踪 Git 仓库中所需的应用程序状态并自动将其同步到集群来管理 Kubernetes 部署。
与 Dapr 集成
您可以使用 Argo CD 来管理 Dapr 控制平面组件和启用 Dapr 的应用程序的部署。通过采用 GitOps 方法,您可以确保 Dapr 的配置和应用程序在您的各个环境中以一致的方式部署、版本化和审计。Argo CD 可以轻松配置为部署存储在 Git 仓库中的 Helm charts、manifests 和 Dapr 组件。
示例代码
一个演示使用 Argo CD 部署 Dapr 的示例项目可在 https://github.com/dapr/samples/tree/master/dapr-argocd 获取。
7 - 组件
7.1 - 可插拔组件
7.1.1 - 可插拔组件概述
可插拔组件是指不包含在运行时中的组件,与 dapr init 附带内置组件不同。您可以配置 Dapr 使用可插拔组件,这些组件利用构建块 API,但注册方式与内置 Dapr 组件不同。

可插拔组件与内置组件
Dapr 提供了两种注册和创建组件的方式:
- 包含在运行时中的内置组件,位于 components-contrib 仓库。
- 可独立部署和注册的可插拔组件。
虽然两种注册选项都利用了 Dapr 的构建块 API,但每种方式的实现过程不同。
| 组件详情 | 内置组件 | 可插拔组件 |
|---|---|---|
| 语言 | 只能用 Go 编写 | 可以用任何 gRPC 支持的语言编写 |
| 运行位置 | 作为 Dapr 运行时可执行文件的一部分 | 作为独立的进程或容器中的 Pod 运行。与 Dapr 本身分开运行。 |
| 向 Dapr 注册 | 集成到 Dapr 代码库中 | 通过 Unix 域套接字(使用 gRPC)向 Dapr 注册 |
| 分发方式 | 随 Dapr 版本发布。组件新功能的添加与 Dapr 版本保持一致 | 独立于 Dapr 本身分发。可以根据需要随时添加新功能,并遵循自己的发布周期。 |
| 组件激活方式 | Dapr 启动时自动运行组件 | 用户手动启动组件 |
为什么创建可插拔组件?
可插拔组件在以下场景中非常有用:
- 您需要一个私有组件。
- 您希望组件与 Dapr 发布流程分开。
- 您不太熟悉 Go,或者用 Go 实现组件不是理想选择。
功能特性
实现可插拔组件
要实现可插拔组件,您需要在组件中实现 gRPC 服务。实现 gRPC 服务需要三个步骤:
- 找到 proto 定义文件
- 创建服务脚手架
- 定义服务
了解更多关于如何开发和实现可插拔组件的信息。
利用多个构建块实现组件
除了从同一组件实现多个 gRPC 服务(例如 StateStore、QueriableStateStore、TransactionalStateStore 等)外,可插拔组件还可以暴露其他组件接口的实现。这意味着单个可插拔组件可以同时充当状态存储、发布订阅和输入或输出绑定。换句话说,您可以将多个组件接口实现到一个可插拔组件中,并将其暴露为 gRPC 服务。
虽然在同一可插拔组件上暴露多个组件接口可以降低部署多个组件的运维负担,但它会使组件的实现和调试变得更加困难。如有疑问,请遵循"关注点分离"原则,仅在必要时将多个组件接口合并到同一个可插拔组件中。
将可插拔组件投入运维
内置组件和可插拔组件有一个共同点:两者都需要组件规范 。内置组件无需任何额外步骤即可使用:Dapr 自动准备好使用它们。
相比之下,可插拔组件在与 Dapr 通信之前需要额外步骤。您需要首先运行组件,并促进 Dapr-组件通信以启动注册过程。
后续步骤
7.1.2 - 如何操作:实现可插拔组件
在本指南中,您将了解为何以及如何实现可插拔组件。要了解如何配置和注册可插拔组件,请参阅如何操作:注册可插拔组件
实现可插拔组件
为了实现可插拔组件,您需要在组件中实现一个 gRPC 服务。实现 gRPC 服务需要三个步骤:
查找 proto 定义文件
为每个支持的服务接口(状态存储、发布订阅、绑定、密钥存储)都提供了 Proto 定义。
目前,支持以下组件 API:
- 状态存储
- 发布订阅
- 绑定
- 密钥存储
| 组件 | 类型 | gRPC 定义 | 内置参考实现 | 文档 |
|---|---|---|---|---|
| 状态存储 | state | state.proto | Redis | 概念, 如何操作, api 规范 |
| 发布订阅 | pubsub | pubsub.proto | Redis | 概念, 如何操作, api 规范 |
| 绑定 | bindings | bindings.proto | Kafka | 概念, 输入如何操作, 输出如何操作, api 规范 |
| 密钥存储 | secretstores | secretstore.proto | Hashicorp/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.CommonProtosNuGet 包。
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。
可以通过以下方式直接返回错误:
- 在方法返回的
Mono或Flux中调用.error()方法 - 提供适当的异常作为参数。
您也可以引发异常,只要它被捕获并反馈到生成的 Mono 或 Flux 中。
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)
后续步骤
- 使用此示例代码开始开发 .NET 可插拔组件
- 查看可插拔组件概述
- 了解如何注册您的可插拔组件
7.1.3 - 可插拔组件 SDK
Dapr SDK 是创建可插拔组件的最简单方式。选择你喜欢的语言,在几分钟内开始创建组件。
可插拔组件 SDK
| 语言 | 状态 |
|---|---|
| Go | 开发中 |
| .NET | 开发中 |
7.1.3.1 - Dapr 可插拔组件 .NET SDK 入门
Dapr 提供 NuGet 包以帮助开发 .NET 可插拔组件。
前置条件
- .NET 6 SDK 或更高版本
- Dapr 1.9 CLI 或更高版本
- 已初始化的 Dapr 环境
- Linux、Mac 或 Windows(配合 WSL)
注意
在 Windows 上开发 Dapr 可插拔组件需要 WSL,因为某些开发平台在"原生" Windows 上不完全支持 Unix 域套接字。创建项目
创建可插拔组件始于一个空的 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 的应用。这可能是目前为组件生成容器的最简单方法。
注意
目前,.NET 7 SDK 需要本地计算机上的 Docker Desktop、一个特殊的 NuGet 包以及本地计算机上的 Docker Desktop 来构建容器。.NET SDK 的未来版本计划消除这些要求。
本地计算机上可以同时安装多个版本的 .NET SDK。
将 Microsoft.NET.Build.Containers NuGet 包添加到组件项目。
dotnet add package Microsoft.NET.Build.Containers
将应用发布为容器:
dotnet publish --os linux --arch x64 /t:PublishContainer -c Release
注意
确保架构参数--arch x64 与组件的最终部署目标相匹配。默认情况下,生成的容器架构与本地计算机的架构相匹配。例如,如果本地计算机基于 ARM64(例如 M1 或 M2 Mac)并且省略了该参数,则会生成 ARM64 容器,该容器可能与期望 AMD64 容器的部署目标不兼容。有关更多配置选项,例如控制容器名称、标签和基础镜像,请参阅 .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> .
注意
Dockerfile 中 COPY 操作的路径相对于构建镜像时传递的 Docker 上下文,而 Docker 上下文本身会根据所构建项目的需求而变化(例如,如果它有引用的项目)。在上面的示例中,假设 Docker 上下文是组件项目目录。演示
观看此视频,了解 使用 .NET 构建可插拔组件的演示:
后续步骤
- 了解可插拔组件 .NET SDK 的高级步骤
- 了解有关使用可插拔组件 .NET SDK 的更多信息:
7.1.3.1.1 - 实现 .NET 输入/输出绑定组件
创建绑定组件只需要几个基本步骤。
添加绑定命名空间
添加绑定相关命名空间的 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();
注意
同时实现IInputBinding 和 IOutputBinding 的组件将被同时注册为输入和输出绑定。后续步骤
- 了解可插拔组件 .NET SDK 的高级步骤
- 了解有关使用可插拔组件 .NET SDK 的更多信息:
7.1.3.1.2 - 实现 .NET 发布订阅组件
创建发布订阅组件只需几个基本步骤。
添加发布订阅命名空间
为发布订阅相关的命名空间添加 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();
后续步骤
- 了解可插拔组件 .NET SDK 的高级步骤
- 了解有关使用可插拔组件 .NET SDK 的更多信息:
7.1.3.1.3 - 实现 .NET 状态存储组件
创建状态存储组件只需要几个基本步骤。
添加状态存储命名空间
添加状态存储相关命名空间的 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 接口的方法,但包含多个请求值。
注意
对于未实现IBulkStateStore 的状态存储,Dapr 运行时将通过单独调用其操作来模拟批量状态存储操作。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 运行时对某些状态存储操作导致的某些错误条件有额外的处理。状态存储可以通过从其操作逻辑中抛出特定异常来指示此类情况:
| 异常 | 适用操作 | 描述 |
|---|---|---|
ETagInvalidException | Delete、Set、Bulk Delete、Bulk Set | 当 ETag 无效时 |
ETagMismatchException | Delete、Set、Bulk Delete、Bulk Set | 当 ETag 与预期值不匹配时 |
BulkDeleteRowMismatchException | Bulk Delete | 当受影响的行数与预期行数不匹配时 |
后续步骤
- 了解可插拔组件 .NET SDK 的高级步骤
- 了解有关使用可插拔组件 .NET SDK 的更多信息:
7.1.3.1.4 - Dapr 可插拔组件 .NET SDK 的高级用法
尽管大多数人通常不需要,但这些指南展示了配置 .NET 可插拔组件的高级方法。
7.1.3.1.4.1 - .NET Dapr 可插拔组件中的多个服务
可插拔组件可以托管多种类型的多个组件。您可能需要这样做:
- 为了最小化集群中运行的边车数量
- 为了分组可能共享库和实现的相关组件,例如:
- 一个既作为通用状态存储公开的数据库,和
- 允许更特定操作的输出绑定。
每个 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: []
后续步骤
- 了解有关组件生命周期的更多信息
- 了解有关应用程序环境的更多信息
- 了解有关使用可插拔组件 .NET SDK 的更多信息:
7.1.3.1.4.2 - .NET Dapr 可插拔组件的应用环境
.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)
{
// ...
}
// ...
}
警告
不建议使用IServiceCollection.AddScoped()。此类实例的生命周期绑定到单个 gRPC 方法调用,这与单个组件实例的生命周期不匹配。日志
.NET Dapr 可插拔组件可以使用标准 .NET 日志机制。DaprPluggableComponentsApplication 暴露了一个 ILoggingBuilder,可以通过它进行配置。
注意
与 ASP.NET 一样,日志记录器服务(例如ILogger<T>)已预先注册。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)
{
// ...
}
// ...
}
后续步骤
- 了解组件生命周期的更多信息
- 了解多服务的更多信息
- 了解如何使用可插拔组件 .NET SDK:
7.1.3.1.4.3 - .NET Dapr 可插拔组件的生命周期
有两种方式注册组件:
- 组件作为单例运行,其生命周期由 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;
}
// ...
}
后续步骤
- 了解有关应用程序环境的更多信息
- 了解有关多个服务的更多信息
- 了解有关使用可插拔组件 .NET SDK 的更多信息:
7.1.3.2 - Dapr 可插拔组件 Go SDK 入门
Dapr 提供了用于帮助开发 Go 可插拔组件的软件包。
前置条件
- Go 1.20 或更高版本
- Dapr 1.9 CLI 或更高版本
- 已初始化的 Dapr 环境
- Linux、Mac 或 Windows(需使用 WSL)
注意
在 Windows 上开发 Dapr 可插拔组件需要 WSL。并非所有语言和 SDK 在"原生"Windows 上都支持 Unix 域套接字。应用程序创建
创建可插拔组件首先需要创建一个空的 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> .
注意
Dockerfile 中 COPY 操作的路径是相对于构建镜像时传递的 Docker 上下文而言的,而 Docker 上下文本身将根据所构建应用程序的需求而变化。在上面的示例中,假设 Docker 上下文是组件应用程序目录。后续步骤
- 可插拔组件 Go SDK 的高级技巧
- 了解有关实现的更多信息:
7.1.3.2.1 - 实现 Go 输入/输出绑定组件
创建绑定组件只需几个基本步骤。
导入绑定包
创建文件 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()
}
后续步骤
- 可插拔组件 Go SDK 的高级技巧
- 了解更多关于实现:
7.1.3.2.2 - 实现 Go 发布订阅组件
创建发布订阅组件只需要几个基本步骤。
导入发布订阅包
创建文件 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()
}
后续步骤
- 可插拔组件 Go SDK 的高级技巧
- 了解有关实现的更多信息:
7.1.3.2.3 - 实现 Go 状态存储组件
创建状态存储组件只需要几个基本步骤。
导入状态存储包
创建文件 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 | 当受影响的行数与预期行数不匹配时 |
后续步骤
- 可插拔组件 Go SDK 的高级技术
- 了解有关实现的更多信息:
7.1.3.2.4 - 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-a 和 service-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: []
后续步骤
7.2 - 如何编写中间件组件
Dapr 允许通过链接一系列中间件组件来定义自定义处理管道。在本指南中,你将学习如何创建中间件组件。要了解如何配置现有中间件组件,请参阅配置中间件组件。
编写自定义 HTTP 中间件
Dapr 中的 HTTP 中间件封装了标准的 Go net/http 处理函数。
你的中间件需要实现一个中间件接口,该接口定义了一个 GetHandler 方法,该方法返回一个 http.Handler 回调和一个 error:
type Middleware interface {
GetHandler(metadata middleware.Metadata) (func(next http.Handler) http.Handler, error)
}
处理程序接收一个 next 回调,该回调应该被调用以继续处理请求。
你的处理程序实现可以包括入站逻辑、出站逻辑,或两者兼有:
func (m *customMiddleware) GetHandler(metadata middleware.Metadata) (func(next http.Handler) http.Handler, error) {
var err error
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Inbound logic
// ...
// Call the next handler
next.ServeHTTP(w, r)
// Outbound logic
// ...
}
}, err
}


