This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

使用 Dapr 进行应用程序开发

提供使用 Dapr 构建应用程序的工具、技巧和相关信息

1 - 构建模块

Dapr 提供的功能,旨在解决分布式应用程序开发中的常见挑战

概念部分中,您可以查看 Dapr 构建模块的高级概述

展示不同 Dapr API 构建模块的图示

1.1 - 服务调用

实现直接且安全的服务间方法调用

1.1.1 - 服务调用概述

服务调用API模块的概述

通过服务调用,您的应用程序可以使用标准的gRPCHTTP协议可靠且安全地与其他应用程序进行通信。

在许多基于微服务的应用程序中,多个服务需要能够相互通信。这种服务间通信要求应用程序开发人员处理以下问题:

  • 服务发现。 如何找到我的不同服务?
  • 标准化服务间的API调用。 如何在服务之间调用方法?
  • 安全的服务间通信。 如何通过加密安全地调用其他服务并对方法应用访问控制?
  • 缓解请求超时或失败。 如何处理重试和瞬态错误?
  • 实现可观测性和追踪。 如何使用追踪查看带有指标的调用图以诊断生产中的问题?

服务调用API

Dapr通过提供一个类似反向代理的服务调用API来解决这些挑战,该API内置了服务发现,并利用了分布式追踪、指标、错误处理、加密等功能。

Dapr采用sidecar架构。要使用Dapr调用应用程序:

  • 您在Dapr实例上使用invoke API。
  • 每个应用程序与其自己的Dapr实例通信。
  • Dapr实例相互发现并通信。

以下概述视频和演示展示了Dapr服务调用的工作原理。

下图概述了Dapr的服务调用在两个集成Dapr的应用程序之间的工作原理。

显示服务调用步骤的图示
  1. 服务A发起一个HTTP或gRPC调用,目标是服务B。调用发送到本地Dapr sidecar。
  2. Dapr使用正在运行的名称解析组件在给定的托管平台上发现服务B的位置。
  3. Dapr将消息转发到服务B的Dapr sidecar
    • 注意:所有Dapr sidecar之间的调用都通过gRPC进行以提高性能。只有服务与Dapr sidecar之间的调用可以是HTTP或gRPC。
  4. 服务B的Dapr sidecar将请求转发到服务B上的指定端点(或方法)。服务B然后运行其业务逻辑代码。
  5. 服务B向服务A发送响应。响应发送到服务B的sidecar。
  6. Dapr将响应转发到服务A的Dapr sidecar。
  7. 服务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协议提供服务调用请求的轮询负载均衡,例如在单台机器或多台联网的物理机器上。

下图显示了其工作原理的示例。如果您有一个应用程序实例,应用程序ID为FrontEnd,以及三个应用程序实例,应用程序ID为Cart,并且您从FrontEnd应用程序调用Cart应用程序,Dapr在三个实例之间进行轮询。这些实例可以在同一台机器上或不同的机器上。

显示服务调用步骤的图示

注意:应用程序ID在_应用程序_中是唯一的,而不是应用程序实例。无论该应用程序存在多少个实例(由于扩展),它们都将共享相同的应用程序ID。

可交换的服务发现

Dapr可以在多种托管平台上运行。为了启用可交换的服务发现,Dapr使用名称解析组件。例如,Kubernetes名称解析组件使用Kubernetes DNS服务来解析在集群中运行的其他应用程序的位置。

自托管机器可以使用mDNS名称解析组件。作为替代方案,您可以使用SQLite名称解析组件在单节点环境中运行Dapr,并用于本地开发场景。属于集群的Dapr sidecar将其信息存储在本地机器上的SQLite数据库中。

Consul名称解析组件特别适合多机部署,并且可以在任何托管环境中使用,包括Kubernetes、多台虚拟机或自托管。

HTTP服务调用的流式处理

您可以在HTTP服务调用中将数据作为流处理。这可以在使用Dapr通过HTTP调用另一个服务时提供性能和内存利用率的改进,尤其是在请求或响应体较大的情况下。

下图演示了数据流的六个步骤。

显示表中描述的服务调用步骤的图示
  1. 请求:“应用程序A"到"Dapr sidecar A”
  2. 请求:“Dapr sidecar A"到"Dapr sidecar B”
  3. 请求:“Dapr sidecar B"到"应用程序B”
  4. 响应:“应用程序B"到"Dapr sidecar B”
  5. 响应:“Dapr sidecar B"到"Dapr sidecar A”
  6. 响应:“Dapr sidecar A"到"应用程序A”

示例架构

按照上述调用顺序,假设您有如Hello World教程中描述的应用程序,其中一个Python应用程序调用一个Node.js应用程序。在这种情况下,Python应用程序将是"服务A",Node.js应用程序将是"服务B"。

下图再次显示了本地机器上的1-7序列,显示了API调用:

  1. Node.js应用程序的Dapr应用程序ID为nodeapp。Python应用程序通过POST http://localhost:3500/v1.0/invoke/nodeapp/method/neworder调用Node.js应用程序的neworder方法,该请求首先发送到Python应用程序的本地Dapr sidecar。
  2. Dapr使用名称解析组件(在这种情况下是自托管时的mDNS)发现Node.js应用程序的位置,该组件在您的本地机器上运行。
  3. Dapr使用刚刚接收到的位置将请求转发到Node.js应用程序的sidecar。
  4. Node.js应用程序的sidecar将请求转发到Node.js应用程序。Node.js应用程序执行其业务逻辑,记录传入消息,然后将订单ID持久化到Redis(图中未显示)。
  5. Node.js应用程序通过Node.js sidecar向Python应用程序发送响应。
  6. Dapr将响应转发到Python Dapr sidecar。
  7. Python应用程序接收响应。

试用服务调用

快速入门和教程

Dapr文档包含多个利用服务调用构建模块的快速入门,适用于不同的示例架构。为了直观地理解服务调用API及其功能,我们建议从我们的快速入门开始:

快速入门/教程描述
服务调用快速入门这个快速入门让您直接与服务调用构建模块进行交互。
Hello World教程本教程展示了如何在本地机器上运行服务调用和状态管理构建模块。
Hello World Kubernetes教程本教程演示了如何在Kubernetes中使用Dapr,并涵盖了服务调用和状态管理构建模块。

直接在您的应用程序中开始使用服务调用

想跳过快速入门?没问题。您可以直接在应用程序中试用服务调用构建模块,以安全地与其他服务通信。在Dapr安装完成后,您可以通过以下方式开始使用服务调用API。

使用以下方式调用服务:

  • HTTP和gRPC服务调用(推荐的设置方法)
  • 直接调用API - 除了代理,还有一个选项可以直接调用服务调用API以调用GET端点。只需将您的地址URL更新为localhost:<dapr-http-port>,您就可以直接调用API。您还可以在上面链接的HTTP代理文档中阅读更多关于此的信息。
  • SDKs - 如果您正在使用Dapr SDK,您可以直接通过SDK使用服务调用。选择您需要的SDK,并使用Dapr客户端调用服务。有关更多信息,请阅读Dapr SDKs

为了快速测试,尝试使用Dapr CLI进行服务调用:

  • Dapr CLI命令 - 一旦设置了Dapr CLI,使用dapr invoke --method <method-name>命令以及方法标志和感兴趣的方法。有关更多信息,请阅读Dapr CLI

下一步

1.1.2 - 操作指南:使用HTTP调用服务

通过服务调用实现服务之间的通信

本文演示了如何部署服务,每个服务都有一个唯一的应用程序ID,以便其他服务可以通过HTTP进行服务调用来发现并调用它们的端点。

示例服务的服务调用图示

为服务选择一个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。sidecar编程模型鼓励每个应用程序与其自己的Dapr实例交互。Dapr的sidecar会自动发现并相互通信。

以下是利用Dapr SDK进行服务调用的代码示例。

# 依赖
import random
from time import sleep
import logging
import requests

# 代码
logging.basicConfig(level = logging.INFO) 
while True:
    sleep(random.randrange(50, 5000) / 1000)
    orderId = random.randint(1, 1000)
    # 调用服务
    result = requests.post(
       url='%s/orders' % (base_url),
       data=json.dumps(order),
       headers=headers
    )    
    logging.info('Order requested: ' + str(orderId))
    logging.info('Result: ' + str(result))
// 依赖
import axios from "axios";

// 代码
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);
        });
    }
}

// 调用服务
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();
// 依赖
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;

// 代码
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);

               // 使用Dapr SDK调用方法
               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);
   	    }
       }
   }
}
// 依赖
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;

// 代码
@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;

            // 创建一个Map来表示请求体
            Map<String, Object> requestBody = new HashMap<>();
            requestBody.put("orderId", orderId);
            // 根据需要向requestBody Map添加其他字段

            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)
		}

		// 将目标应用程序ID添加为头的一部分
		req.Header.Add("dapr-app-id", "order-processor")

		// 调用服务
		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:

  1. 将URL中的地址更改为localhost:<dapr-http-port>
  2. 添加一个dapr-app-id头来指定目标服务的ID,或者通过HTTP基本认证传递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

命名空间

支持命名空间的平台上运行时,您可以在应用程序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 sidecar。这为开发人员带来了以下 Dapr 服务调用 的优势:

  1. 双向认证
  2. 跟踪
  3. 指标
  4. 访问控制列表
  5. 网络级别的弹性
  6. 基于 API 令牌的认证

Dapr 支持代理所有类型的 gRPC 调用,包括一元和流式调用

第一步:运行 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,我们为应用分配了一个唯一的 ID,server,通过 --app-id 标志指定。

第二步:调用服务

以下示例展示了如何使用 Dapr 从 gRPC 客户端发现 Greeter 服务。 注意,客户端不是直接通过端口 50051 调用目标服务,而是通过端口 50007 调用其本地 Dapr sidecar,这样就提供了所有的服务调用功能,包括服务发现、跟踪、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 规范

第三步:查看跟踪和日志

上面的示例展示了如何直接调用本地或 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 时,由于其长时间存在的特性,弹性策略仅应用于“初始握手”。因此:

  • 如果流在初始握手后中断,Dapr 不会自动重新建立。您的应用将被通知流已结束,并需要重新创建它。
  • 重试策略仅影响初始连接“握手”。如果您的弹性策略包括重试,Dapr 将检测到建立与目标应用的初始连接失败,并将重试直到成功(或直到策略中定义的重试次数耗尽)。
  • 同样,弹性策略中定义的超时仅适用于初始“握手”。连接建立后,超时不再影响流。

相关链接

社区通话演示

观看此视频了解如何使用 Dapr 的 gRPC 代理功能:

1.1.4 - 如何:使用HTTP调用非Dapr端点

从Dapr应用程序中通过服务调用访问非Dapr端点

本文介绍如何通过Dapr使用HTTP调用非Dapr端点。

通过Dapr的服务调用API,您可以与使用或不使用Dapr的端点进行通信。使用Dapr调用非Dapr端点不仅提供了一致的API,还带来了以下Dapr服务调用的优势:

  • 应用弹性策略
  • 通过跟踪和指标实现调用的可观测性
  • 通过访问控制实现安全性
  • 利用中间件管道组件
  • 服务发现
  • 使用请求头进行身份验证

通过HTTP调用外部服务或非Dapr端点

有时您可能需要调用非Dapr的HTTP端点,例如:

  • 您可能只在应用程序的一部分中使用Dapr,尤其是在涉及旧系统时
  • 您可能无法访问代码以将现有应用程序迁移到Dapr
  • 您需要调用外部的HTTP服务

通过定义HTTPEndpoint资源,您可以声明性地配置与非Dapr端点的交互方式。然后,您可以使用服务调用URL来访问非Dapr端点。或者,您可以直接在服务调用URL中使用非Dapr的完全限定域名(FQDN)端点URL。

HttpEndpoint、FQDN URL和appId的优先级

在进行服务调用时,Dapr运行时遵循以下优先级顺序:

  1. 是否为命名的HTTPEndpoint资源?
  2. 是否为带有http://https://前缀的FQDN URL?
  3. 是否为appID

服务调用与非Dapr HTTP端点

下图概述了Dapr在调用非Dapr端点时的工作流程。

显示服务调用到非Dapr端点步骤的图示
  1. 服务A发起一个HTTP调用,目标是服务B(一个非Dapr端点)。调用被发送到本地的Dapr sidecar。
  2. Dapr通过HTTPEndpoint或FQDN URL定位服务B的位置,然后将消息转发给服务B。
  3. 服务B向服务A的Dapr sidecar发送响应。
  4. 服务A接收响应。

使用HTTPEndpoint资源或FQDN URL调用非Dapr端点

在与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
    

使用appId调用启用Dapr的应用程序

AppID用于通过appIDmy-method调用Dapr应用程序。阅读如何:使用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

相关链接

社区电话演示

观看此视频以了解如何使用服务调用来调用非Dapr端点。

1.1.5 - 指南:跨命名空间进行服务调用

在不同命名空间之间进行服务调用

在本文中,您将学习如何在不同命名空间之间进行服务调用。默认情况下,service-invocation支持通过简单引用应用程序ID(如nodeapp)来调用同一命名空间内的服务:

localhost:3500/v1.0/invoke/nodeapp/method/neworder

service-invocation也支持跨命名空间的调用。在所有支持的平台上,Dapr应用程序ID遵循包含目标命名空间的有效FQDN格式。您可以同时指定:

  • 应用程序ID(如nodeapp),以及
  • 应用程序所在的命名空间(如production)。

示例 1

调用位于production命名空间中nodeappneworder方法:

localhost:3500/v1.0/invoke/nodeapp.production/method/neworder

在使用service-invocation调用不同命名空间中的应用程序时,您需要使用命名空间来限定它。这在Kubernetes集群中的跨命名空间调用中非常有用。

示例 2

调用位于production命名空间中myappping方法:

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 - 消息发布与订阅

服务之间安全且可扩展的消息传递

1.2.1 - 发布-订阅模式概述

pubsub API 的基本构建块概述

发布-订阅模式(pubsub)使微服务能够通过消息进行事件驱动的架构通信。

  • 生产者或发布者将消息写入输入通道并发送到主题,而不关心哪个应用程序会接收它们。
  • 消费者或订阅者订阅主题并从输出通道接收消息,而不关心哪个服务生成了这些消息。

消息代理会将每条消息从发布者的输入通道复制到所有对该消息感兴趣的订阅者的输出通道。这种模式在需要将微服务解耦时特别有用。



pubsub API

在 Dapr 中,pubsub API:

  • 提供一个平台无关的 API 来发送和接收消息。
  • 确保消息至少被传递一次。
  • 与多种消息代理和队列系统集成。

您可以在运行时配置 Dapr pubsub 组件来使用特定的消息代理,这种可插拔性使您的服务更具可移植性和灵活性。

在 Dapr 中使用 pubsub 时:

  1. 您的服务通过网络调用 Dapr pubsub 构建块 API。
  2. pubsub 构建块调用封装特定消息代理的 Dapr pubsub 组件。
  3. 为了接收主题上的消息,Dapr 代表您的服务订阅 pubsub 组件,并在消息到达时将其传递到您的服务的端点。

以下概述视频和演示展示了 Dapr pubsub 的工作原理。

在下图中,“shipping”服务和“email”服务都已订阅由“cart”服务发布的主题。每个服务加载指向相同 pubsub 消息代理组件的 pubsub 组件配置文件;例如:Redis Streams、NATS Streaming、Azure Service Bus 或 GCP pubsub。

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

查看 Dapr 支持的 pubsub 组件的完整列表

特性

pubsub API 构建块为您的应用程序带来了多个特性。

使用 Cloud Events 发送消息

为了启用消息路由并为服务之间的每条消息提供额外的上下文,Dapr 使用 CloudEvents 1.0 规范 作为其消息格式。任何应用程序通过 Dapr 发送到主题的消息都会自动包装在 Cloud Events 信封中,使用 Content-Type 头值 作为 datacontenttype 属性。

有关更多信息,请阅读 使用 CloudEvents 进行消息传递,或 发送不带 CloudEvents 的原始消息

与不使用 Dapr 和 CloudEvents 的应用程序通信

如果您的一个应用程序使用 Dapr 而另一个不使用,您可以为发布者或订阅者禁用 CloudEvent 包装。这允许在无法一次性采用 Dapr 的应用程序中部分采用 Dapr pubsub。

有关更多信息,请阅读 如何在没有 CloudEvents 的情况下使用 pubsub

设置消息内容类型

发布消息时,指定发送数据的内容类型很重要。除非指定,否则 Dapr 将假定为 text/plain

  • HTTP 客户端:可以在 Content-Type 头中设置内容类型
  • gRPC 客户端和 SDK:有一个专用的内容类型参数

消息传递

原则上,Dapr 认为消息一旦被订阅者处理并以非错误响应进行响应,就已成功传递。为了更细粒度的控制,Dapr 的 pubsub API 还提供了明确的状态,定义在响应负载中,订阅者可以用这些状态向 Dapr 指示特定的处理指令(例如,RETRYDROP)。

使用主题订阅接收消息

Dapr 应用程序可以通过支持相同功能的三种订阅类型订阅已发布的主题:声明式、流式和编程式。

订阅类型描述
声明式订阅在外部文件中定义。声明式方法消除了代码中的 Dapr 依赖性,并允许现有应用程序订阅主题,而无需更改代码。
流式订阅在用户代码中定义。流式订阅是动态的,意味着它们允许在运行时添加或删除订阅。它们不需要应用程序中的订阅端点(这是编程式和声明式订阅所需的),使其易于在代码中配置。流式订阅也不需要应用程序配置 sidecar 来接收消息。由于消息被发送到消息处理程序代码,因此流式订阅中没有路由或批量订阅的概念。
编程式订阅在用户代码中定义。编程式方法实现了静态订阅,并需要在代码中有一个端点。

有关更多信息,请阅读 关于订阅类型的订阅

重新加载主题订阅

要重新加载以编程方式或声明式定义的主题订阅,需要重新启动 Dapr sidecar。 通过启用 HotReload 功能门,可以使 Dapr sidecar 动态重新加载更改的声明式主题订阅,而无需重新启动。 主题订阅的热重载目前是一个预览功能。 重新加载订阅时,正在传输的消息不受影响。

消息路由

Dapr 提供 基于内容的路由 模式。Pubsub 路由 是此模式的实现,允许开发人员使用表达式根据其内容将 CloudEvents 路由到应用程序中的不同 URI/路径和事件处理程序。如果没有路由匹配,则使用可选的默认路由。随着您的应用程序扩展以支持多个事件版本或特殊情况,这很有用。

此功能适用于声明式和编程式订阅方法。

有关消息路由的更多信息,请阅读 Dapr pubsub API 参考

使用死信主题处理失败的消息

有时,由于各种可能的问题,例如生产者或消费者应用程序中的错误条件或导致应用程序代码出现问题的意外状态更改,消息无法被处理。Dapr 允许开发人员设置死信主题来处理无法传递到应用程序的消息。此功能适用于所有 pubsub 组件,并防止消费者应用程序无休止地重试失败的消息。有关更多信息,请阅读 死信主题

启用外发模式

Dapr 使开发人员能够使用外发模式在事务性状态存储和任何消息代理之间实现单一事务。有关更多信息,请阅读 如何启用事务性外发消息

命名空间消费者组

Dapr 通过 命名空间消费者组 解决大规模多租户问题。只需在组件元数据中包含 "{namespace}" 值,即可允许具有相同 app-id 的多个命名空间的应用程序发布和订阅相同的消息代理。

至少一次保证

Dapr 保证消息传递的至少一次语义。当应用程序使用 pubsub API 向主题发布消息时,Dapr 确保消息至少一次传递给每个订阅者。

即使消息传递失败,或者您的应用程序崩溃,Dapr 也会尝试重新传递消息,直到成功传递。

所有 Dapr pubsub 组件都支持至少一次保证。

消费者组和竞争消费者模式

Dapr 处理消费者组和竞争消费者模式的负担。在竞争消费者模式中,使用单个消费者组的多个应用程序实例竞争消息。当副本使用相同的 app-id 而没有显式消费者组覆盖时,Dapr 强制执行竞争消费者模式。

当同一应用程序的多个实例(具有相同的 app-id)订阅一个主题时,Dapr 将每条消息仅传递给该应用程序的一个实例。此概念在下图中进行了说明。



同样,如果两个不同的应用程序(具有不同的 app-id)订阅同一主题,Dapr 将每条消息仅传递给每个应用程序的一个实例

并非所有 Dapr pubsub 组件都支持竞争消费者模式。目前,以下(非详尽)pubsub 组件支持此功能:

为增强安全性设置主题范围

默认情况下,与 pubsub 组件实例关联的所有主题消息对配置了该组件的每个应用程序都是可用的。您可以使用 Dapr 主题范围限制哪个应用程序可以发布或订阅主题。有关更多信息,请阅读:pubsub 主题范围

消息生存时间(TTL)

Dapr 可以在每条消息的基础上设置超时消息,这意味着如果消息未从 pubsub 组件中读取,则消息将被丢弃。此超时消息可防止未读消息的积累。如果消息在队列中的时间超过配置的 TTL,则标记为死信。有关更多信息,请阅读 pubsub 消息 TTL

发布和订阅批量消息

Dapr 支持在单个请求中发送和接收多条消息。当编写需要发送或接收大量消息的应用程序时,使用批量操作可以通过减少请求总数来实现高吞吐量。有关更多信息,请阅读 pubsub 批量消息

使用 StatefulSets 扩展订阅者

在 Kubernetes 上运行时,使用 StatefulSets 结合 {podName} 标记,订阅者可以为每个实例拥有一个粘性 consumerID。请参阅 如何使用 StatefulSets 水平扩展订阅者

试用 pubsub

快速入门和教程

想要测试 Dapr pubsub API 吗?通过以下快速入门和教程来查看 pubsub 的实际应用:

快速入门/教程描述
Pubsub 快速入门使用发布和订阅 API 发送和接收消息。
Pubsub 教程演示如何使用 Dapr 启用 pubsub 应用程序。使用 Redis 作为 pubsub 组件。

直接在您的应用中开始使用 pubsub

想要跳过快速入门?没问题。您可以直接在应用程序中试用 pubsub 构建块来发布消息并订阅主题。在 安装 Dapr 后,您可以从 pubsub 如何指南 开始使用 pubsub API。

下一步

1.2.2 - 如何:发布消息并订阅主题

学习如何使用一个服务向主题发送消息,并在另一个服务中订阅该主题

既然您已经了解了Dapr pubsub构建块的功能,接下来我们来看看如何在您的服务中应用它。下面的代码示例描述了一个使用两个服务处理订单的应用程序,每个服务都有Dapr sidecar:

  • 一个结账服务,使用Dapr订阅消息队列中的主题。
  • 一个订单处理服务,使用Dapr向RabbitMQ发布消息。
示例服务的状态管理图

Dapr会自动将用户的负载封装在符合CloudEvents v1.0的格式中,并使用Content-Type头的值作为datacontenttype属性。了解更多关于CloudEvents的消息。

以下示例展示了如何在您的应用程序中发布和订阅名为orders的主题。

设置Pub/Sub组件

第一步是设置pubsub组件:

当您运行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标志来覆盖此文件。

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

要将其部署到Kubernetes集群中,请填写以下YAML中的pub/sub组件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提供了三种方法来订阅主题:

  • 声明式,在外部文件中定义订阅。
  • 流式,在用户代码中定义订阅。
  • 编程式,在用户代码中定义订阅。

声明式、流式和编程式订阅文档中了解更多信息。此示例演示了声明式订阅。

创建一个名为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的事件订阅,针对pubsub组件order-pub-sub

  • route字段指示Dapr将所有主题消息发送到应用程序中的/checkout端点。
  • scopes字段指定此订阅适用于ID为orderprocessingcheckout的应用程序。

subscription.yaml放在与您的pubsub.yaml组件相同的目录中。当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.controller
{
    [ApiController]
    public class CheckoutServiceController : Controller
    {
         //订阅一个主题 
        [Topic("order-pub-sub", "orders")]
        [HttpPost("checkout")]
        public void getCheckout([FromBody] int orderId)
        {
            Console.WriteLine("订阅者接收到 : " + orderId);
        }
    }
}

导航到包含上述代码的目录,然后运行以下命令以启动Dapr sidecar和订阅者应用程序:

dapr run --app-id checkout --app-port 6002 --dapr-http-port 3602 --dapr-grpc-port 60002 --app-protocol https dotnet run
//依赖项
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;

//代码
@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("订阅者接收到: " + cloudEvent.getData());
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
    }
}

导航到包含上述代码的目录,然后运行以下命令以启动Dapr sidecar和订阅者应用程序:

dapr run --app-id checkout --app-port 6002 --dapr-http-port 3602 --dapr-grpc-port 60002 mvn spring-boot:run
#依赖项
from cloudevents.sdk.event import v1
from dapr.ext.grpc import App
import logging
import json

#代码
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('订阅者接收到: ' + str(data))

app.run(6002)

导航到包含上述代码的目录,然后运行以下命令以启动Dapr sidecar和订阅者应用程序:

dapr run --app-id checkout --app-port 6002 --dapr-http-port 3602 --app-protocol grpc -- python3 CheckoutService.py
//依赖项
import (
	"log"
	"net/http"
	"context"

	"github.com/dapr/go-sdk/service/common"
	daprd "github.com/dapr/go-sdk/service/http"
)

//代码
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("添加主题订阅时出错: %v", err)
	}
	if err := s.Start(); err != nil && err != http.ErrServerClosed {
		log.Fatalf("监听时出错: %v", err)
	}
}

func eventHandler(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
	log.Printf("订阅者接收到: %s", e.Data)
	return false, nil
}

导航到包含上述代码的目录,然后运行以下命令以启动Dapr sidecar和订阅者应用程序:

dapr run --app-id checkout --app-port 6002 --dapr-http-port 3602 --dapr-grpc-port 60002 go run CheckoutService.go
//依赖项
import { DaprServer, CommunicationProtocolEnum } from '@dapr/dapr'; 

//代码
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(`订阅者接收到: ${JSON.stringify(orderId)}`)
    });
    await server.start();
}

导航到包含上述代码的目录,然后运行以下命令以启动Dapr sidecar和订阅者应用程序:

dapr run --app-id checkout --app-port 6002 --dapr-http-port 3602 --dapr-grpc-port 60002 npm start

发布消息

启动一个名为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 Microsoft.AspNetCore.Mvc;
using System.Threading;

//代码
namespace EventService
{
    class Program
    {
        static async Task Main(string[] args)
        {
           string PUBSUB_NAME = "order-pub-sub";
           string TOPIC_NAME = "orders";
           while(true) {
                System.Threading.Thread.Sleep(5000);
                Random random = new Random();
                int orderId = random.Next(1,1000);
                CancellationTokenSource source = new CancellationTokenSource();
                CancellationToken cancellationToken = source.Token;
                using var client = new DaprClientBuilder().Build();
                //使用Dapr SDK发布主题
                await client.PublishEventAsync(PUBSUB_NAME, TOPIC_NAME, orderId, cancellationToken);
                Console.WriteLine("发布的数据: " + orderId);
		        }
        }
    }
}

导航到包含上述代码的目录,然后运行以下命令以启动Dapr sidecar和发布者应用程序:

dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 --app-protocol https dotnet run
//依赖项
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;

//代码
@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("发布的数据:" + orderId);
		}
	}
}

导航到包含上述代码的目录,然后运行以下命令以启动Dapr sidecar和发布者应用程序:

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
import json
from dapr.clients import DaprClient

#代码
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('发布的数据: ' + str(orderId))

导航到包含上述代码的目录,然后运行以下命令以启动Dapr sidecar和发布者应用程序:

dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --app-protocol grpc python3 OrderProcessingService.py
//依赖项
import (
	"context"
	"log"
	"math/rand"
	"time"
	"strconv"
	dapr "github.com/dapr/go-sdk/client"
)

//代码
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("发布的数据: " + strconv.Itoa(orderId))
	}
}

导航到包含上述代码的目录,然后运行以下命令以启动Dapr sidecar和发布者应用程序:

dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 go run OrderProcessingService.go
//依赖项
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("发布的数据:" + orderId)
    //使用Dapr SDK发布主题
    await client.pubsub.publish(PUBSUB_NAME, TOPIC_NAME, orderId);
}

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

main();

导航到包含上述代码的目录,然后运行以下命令以启动Dapr sidecar和发布者应用程序:

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的pubsub消息传递。

下一步

1.2.3 - 使用 CloudEvents 进行消息传递

了解 Dapr 使用 CloudEvents 的原因,它们在 Dapr 发布订阅中的工作原理,以及如何创建 CloudEvents。

为了实现消息路由并为每条消息提供额外的上下文,Dapr 采用 CloudEvents 1.0 规范 作为其消息格式。通过 Dapr 发送到主题的任何消息都会自动被包装在 CloudEvents 信封中,使用 Content-Type 头部值 作为 datacontenttype 属性。

Dapr 使用 CloudEvents 为事件负载提供额外的上下文,从而实现以下功能:

  • 跟踪
  • 事件数据的正确反序列化的内容类型
  • 发送应用程序的验证

您可以选择以下三种方法之一通过发布订阅发布 CloudEvent:

  1. 发送一个发布订阅事件,然后由 Dapr 包装在 CloudEvent 信封中。
  2. 通过覆盖标准 CloudEvent 属性来替换 Dapr 提供的特定 CloudEvents 属性。
  3. 将您自己的 CloudEvent 信封作为发布订阅事件的一部分编写。

Dapr 生成的 CloudEvents 示例

向 Dapr 发送发布操作会自动将其包装在一个包含以下字段的 CloudEvent 信封中:

  • id
  • source
  • specversion
  • type
  • traceparent
  • traceid
  • tracestate
  • topic
  • pubsubname
  • time
  • datacontenttype (可选)

以下示例演示了 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 的示例,以下显示了在 CloudEvent 消息中以 JSON 序列化的 XML 内容的数据:

{
  "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: 覆盖 id
  • cloudevent.source: 覆盖 source
  • cloudevent.type: 覆盖 type
  • cloudevent.traceid: 覆盖 traceid
  • cloudevent.tracestate: 覆盖 tracestate
  • cloudevent.traceparent: 覆盖 traceparent

使用这些元数据属性替换 CloudEvents 属性的能力适用于所有发布订阅组件。

示例

例如,要替换代码中上述 CloudEvent 示例中的 sourceid 值:

with DaprClient() as client:
    order = {'orderId': i}
    # 使用 Dapr 发布订阅发布事件/消息
    result = client.publish_event(
        pubsub_name='order_pub_sub',
        topic_name='orders',
        publish_metadata={'cloudevent.id': 'd99b228f-6c73-4e78-8c4d-3f80a043d317', 'cloudevent.source': 'payment'}
    )
var order = new Order(i);
using var client = new DaprClientBuilder().Build();

// 覆盖 cloudevent 元数据
var metadata = new Dictionary<string,string>() {
    { "cloudevent.source", "payment" },
    { "cloudevent.id", "d99b228f-6c73-4e78-8c4d-3f80a043d317" }
}

// 使用 Dapr 发布订阅发布事件/消息
await client.PublishEventAsync("order_pub_sub", "orders", order, metadata);
Console.WriteLine("Published data: " + order);

await Task.Delay(TimeSpan.FromSeconds(1));

然后 JSON 负载反映新的 sourceid 值:

{
  "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"
}

发布您自己的 CloudEvent

如果您想使用自己的 CloudEvent,请确保将 datacontenttype 指定为 application/cloudevents+json

如果应用程序编写的 CloudEvent 不包含 CloudEvent 规范中最低要求的字段,则消息将被拒绝。如果缺少,Dapr 会将以下字段添加到 CloudEvent 中:

  • time
  • traceid
  • traceparent
  • tracestate
  • topic
  • pubsubname
  • source
  • type
  • specversion

您可以向自定义 CloudEvent 添加不属于官方 CloudEvent 规范的其他字段。Dapr 将按原样传递这些字段。

示例

发布一个 CloudEvent 到 orders 主题:

dapr publish --publish-app-id orderprocessing --pubsub order-pub-sub --topic orders --data '{\"orderId\": \"100\"}'

发布一个 CloudEvent 到 orders 主题:

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"}}'

发布一个 CloudEvent 到 orders 主题:

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'

事件去重

使用 Dapr 创建的 CloudEvents 时,信封中包含一个 id 字段,应用程序可以使用该字段执行消息去重。Dapr 不会自动进行去重处理。Dapr 支持使用本身具备消息去重功能的消息代理。

下一步

1.2.4 - 发布和订阅非CloudEvents消息

了解何时可能不使用CloudEvents以及如何禁用它们。

在将Dapr集成到您的应用程序时,由于兼容性原因或某些应用程序不使用Dapr,某些服务可能仍需要通过不封装在CloudEvents中的pub/sub消息进行通信。这些消息被称为“原始”pub/sub消息。Dapr允许应用程序发布和订阅原始事件,这些事件未封装在CloudEvent中以实现兼容性。

发布原始消息

Dapr应用程序可以将原始事件发布到pub/sub主题中,而不需要CloudEvent封装,以便与非Dapr应用程序兼容。

显示当订阅者不使用Dapr或CloudEvent时如何使用Dapr发布的图示

要禁用CloudEvent封装,请在发布请求中将rawPayload元数据设置为true。这样,订阅者可以接收这些消息而无需解析CloudEvent架构。

curl -X "POST" http://localhost:3500/v1.0/publish/pubsub/TOPIC_A?metadata.rawPayload=true -H "Content-Type: application/json" -d '{"order-number": "345"}'
from dapr.clients import DaprClient

with DaprClient() as d:
    req_data = {
        'order-number': '345'
    }
    # 创建一个带有内容类型和主体的类型化消息
    resp = d.publish_event(
        pubsub_name='pubsub',
        topic_name='TOPIC_A',
        data=json.dumps(req_data),
        publish_metadata={'rawPayload': 'true'}
    )
    # 打印请求
    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']);
});

订阅原始消息

Dapr应用程序还可以订阅来自不使用CloudEvent封装的现有pub/sub主题的原始事件。

显示当发布者不使用Dapr或CloudEvent时如何使用Dapr订阅的图示

以编程方式订阅原始事件

在以编程方式订阅时,添加rawPayload的额外元数据条目,以便Dapr sidecar自动将负载封装到与当前Dapr SDK兼容的CloudEvent中。

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();

声明式订阅原始事件

同样,您可以通过在订阅规范中添加rawPayload元数据条目来声明式地订阅原始事件。

apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
  name: myevent-subscription
spec:
  topic: deathStarStatus
  routes: 
    default: /dsstatus
  pubsubname: pubsub
  metadata:
    rawPayload: "true"
scopes:
- app1
- app2

下一步

1.2.5 - 操作指南:将消息路由到不同的事件处理程序

学习如何根据 CloudEvent 字段将主题中的消息路由到不同的事件处理程序

pubsub 路由实现了基于内容的路由,这是一种使用 DSL 而不是命令式应用程序代码的消息模式。通过 pubsub 路由,您可以根据 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 格式。
  • 约束:
    • 可选

必需属性

以下属性在所有 CloudEvents 中是必需的:

id

  • 类型: String
  • 描述: 标识事件。生产者 必须 确保 source + id 对于每个不同的事件都是唯一的。如果由于网络错误而重新发送重复事件,则它可能具有相同的 id。消费者可以假设具有相同 sourceid 的事件是重复的。
  • 约束:
    • 必需
    • 必须是非空字符串
    • 必须在生产者范围内唯一
  • 示例:
    • 由生产者维护的事件计数器
    • UUID

source

  • 类型: URI-reference

  • 描述: 标识事件发生的上下文。通常包括以下信息:

    • 事件源的类型
    • 发布事件的组织
    • 产生事件的过程

    URI 中编码的数据的确切语法和语义由事件生产者定义。

    生产者 必须 确保 source + id 对于每个不同的事件都是唯一的。

    应用程序可以:

    • 为每个不同的生产者分配一个唯一的 source,以便更容易生成唯一的 ID,并防止其他生产者具有相同的 source
    • 使用 UUID、URN、DNS 权威或应用程序特定方案创建唯一的 source 标识符。

    一个 source 可能包含多个生产者。在这种情况下,生产者 必须 合作以确保 source + id 对于每个不同的事件都是唯一的。

  • 约束:

    • 必需
    • 必须是非空 URI-reference
    • 推荐使用绝对 URI
  • 示例:

    • 具有 DNS 权威的互联网范围唯一 URI:
    • 具有 UUID 的全球唯一 URN:
      • urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66
    • 应用程序特定标识符:
      • /cloudevents/spec/pull/123
      • /sensors/tn-1234567/alerts
      • 1-555-123-4567

specversion

  • 类型: String

  • 描述: 事件使用的 CloudEvents 规范版本。这使得上下文的解释成为可能。合规的事件生产者 必须 在引用此版本的规范时使用 1.0 值。

    目前,此属性仅包含“主要”和“次要”版本号。这允许在不更改此属性值的情况下对规范进行补丁更改。

    注意:对于“候选发布”版本,可能会使用后缀进行测试。

  • 约束:

    • 必需
    • 必须是非空字符串

type

  • 类型: String
  • 描述: 包含描述与原始事件相关的事件类型的值。通常,此属性用于路由、可观察性、策略执行等。格式由生产者定义,可能包括 type 的版本信息。有关更多信息,请参阅CloudEvents 的版本控制
  • 约束:
    • 必需
    • 必须是非空字符串
    • 应该以反向 DNS 名称为前缀。前缀域决定了定义此事件类型语义的组织。
  • 示例:
    • com.github.pull_request.opened
    • com.example.object.deleted.v2

可选属性

以下属性在 CloudEvents 中是可选的。有关可选定义的更多信息,请参阅符号约定部分。

datacontenttype

  • 类型: String 根据 RFC 2046

  • 描述: data 值的内容类型。此属性使 data 能够携带任何类型的内容,其中格式和编码可能与所选事件格式不同。

    例如,使用 JSON 信封格式呈现的事件可能在 data 中携带 XML 负载。此属性被设置为 "application/xml",通知消费者。

    不同 datacontenttype 值的数据内容呈现规则在事件格式规范中定义。例如,JSON 事件格式在第 3.1 节中定义了关系。

    对于某些二进制模式协议绑定,此字段直接映射到相应协议的内容类型元数据属性。您可以在相应协议中找到二进制模式和内容类型元数据映射的规范规则。

    在某些事件格式中,您可以省略 datacontenttype 属性。例如,如果 JSON 格式事件没有 datacontenttype 属性,则意味着 data 是符合 "application/json" 媒体类型的 JSON 值。换句话说:没有 datacontenttype 的 JSON 格式事件与 datacontenttype="application/json" 的事件完全等效。

    当将没有 datacontenttype 属性的事件消息转换为不同格式或协议绑定时,目标 datacontenttype 应明确设置为源的隐含 datacontenttype

  • 约束:

    • 可选
    • 如果存在,必须符合 RFC 2046 中指定的格式
  • 有关媒体类型示例,请参阅 IANA 媒体类型

dataschema

  • 类型: URI
  • 描述: 标识 data 遵循的模式。与模式不兼容的更改应通过不同的 URI 反映。有关更多信息,请参阅CloudEvents 的版本控制
  • 约束:
    • 可选
    • 如果存在,必须是非空 URI

subject

  • 类型: String

  • 描述: 这描述了事件生产者(由 source 标识)上下文中的事件主题。在发布-订阅场景中,订阅者通常会订阅由 source 发出的事件。如果 source 上下文具有内部子结构,则仅 source 标识符可能不足以作为任何特定事件的限定符。

    在上下文元数据中(而不是仅在 data 负载中)识别事件的主题在通用订阅过滤场景中很有帮助,其中中间件无法解释 data 内容。在上述示例中,订阅者可能只对名称以 ‘.jpg’ 或 ‘.jpeg’ 结尾的 blob 感兴趣。使用 subject 属性,您可以为该事件子集构建简单而高效的字符串后缀过滤器。

  • 约束:

    • 可选
    • 如果存在,必须是非空字符串
  • 示例:
    订阅者可能会注册对在 blob 存储容器中创建新 blob 时的兴趣。在这种情况下:

    • 事件 source 标识订阅范围(存储容器)
    • 事件 type 标识“blob 创建”事件
    • 事件 id 唯一标识事件实例,以区分同名 blob 的单独创建事件。

    新创建的 blob 的名称在 subject 中传递:

time

  • 类型: Timestamp
  • 描述: 事件发生的时间戳。如果无法确定事件发生的时间,则此属性可以由 CloudEvents 生产者设置为其他时间(例如当前时间)。然而,所有相同 source 的生产者 必须 在这方面保持一致。换句话说,要么他们都使用事件发生的实际时间,要么他们都使用相同的算法来确定使用的值。
  • 约束:
    • 可选
    • 如果存在,必须符合 RFC 3339 中指定的格式

社区电话演示

观看此视频以了解如何使用 pubsub 进行消息路由:

下一步

1.2.6 - 声明式、流式和编程式订阅类型

了解更多关于允许您订阅消息主题的订阅类型。

发布/订阅 API 订阅类型

Dapr 应用程序可以通过三种订阅类型来订阅已发布的主题,这三种类型支持相同的功能:声明式、流式和编程式。

订阅类型描述
声明式订阅在外部文件中定义。声明式方法将 Dapr 的依赖从代码中移除,允许现有应用程序无需更改代码即可订阅主题。
流式订阅在应用程序代码中定义。流式订阅是动态的,允许在运行时添加或删除订阅。它们不需要在应用程序中设置订阅端点(这是编程式和声明式订阅所需的),使其在代码中易于配置。流式订阅也不需要应用程序配置 sidecar 来接收消息。
编程式订阅在应用程序代码中定义。编程式方法实现了静态订阅,并需要在代码中设置一个端点。

下面的示例演示了通过 orders 主题在 checkout 应用程序和 orderprocessing 应用程序之间的发布/订阅消息。示例首先以声明式,然后以编程式演示了相同的 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 发布/订阅组件中指定的主题。

 //订阅一个主题 
[HttpPost("orders")]
public void getCheckout([FromBody] int orderId)
{
    Console.WriteLine("Subscriber received : " + orderId);
}
import io.dapr.client.domain.CloudEvent;

 //订阅一个主题
@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

#订阅一个主题 
@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' }));

// 监听声明式路由
app.post('/orders', (req, res) => {
  console.log(req.body);
  res.sendStatus(200);
});
//订阅一个主题
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 拉取。这意味着不需要端点来订阅主题,并且可以在没有任何应用程序配置在 sidecar 上的情况下进行订阅。 可以同时订阅任意数量的发布/订阅和主题。 由于消息被发送到给定的消息处理代码,因此没有路由或批量订阅的概念。

注意: 每个应用程序一次只能订阅一个发布/订阅/主题对。

下面的示例展示了不同的流式订阅主题的方法。

您可以使用 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
    # 在此处处理消息
    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

                # 处理消息
                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):
    # 在此处处理消息
    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='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()

了解更多关于使用 Python SDK 客户端的流式订阅。

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。
	defer sub.Close()

	for {
		msg, err := sub.Receive()
		if err != nil {
			panic(err)
		}

		// 处理事件

		// 我们 _必须_ 始终表示消息处理的结果,否则
		// 消息将不会被视为已处理,并将被重新传递或
		// 死信。
		// 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。
	defer stop()

	<-make(chan struct{})
}

func eventHandler(e *common.TopicEvent) common.SubscriptionResponseStatus {
	// 在此处处理消息
    // common.SubscriptionResponseStatusRetry
    // common.SubscriptionResponseStatusDrop
			common.SubscriptionResponseStatusDrop, status)
	}

	return common.SubscriptionResponseStatusSuccess
}

演示

观看 此视频以了解流式订阅的概述

编程式订阅

动态编程式方法在代码中返回 routes JSON 结构,与声明式方法的 route YAML 结构不同。

注意: 编程式订阅仅在应用程序启动时读取一次。您不能 动态 添加新的编程式订阅,只能在编译时添加新的。

在下面的示例中,您在应用程序代码中定义了在上面的声明式 YAML 订阅中找到的值。

[Topic("pubsub", "orders")]
[HttpPost("/orders")]
public async Task<ActionResult<Order>>Checkout(Order order, [FromServices] DaprClient daprClient)
{
    // 逻辑
    return order;
}

// Dapr 订阅在 [Topic] 中将 orders 主题路由到此路由
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"`
}

// 处理 /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))
}

下一步

1.2.7 - 死信主题

通过订阅死信主题来处理无法投递的消息

介绍

在某些情况下,应用程序可能由于各种原因无法处理消息。例如,可能会出现获取处理消息所需数据的临时问题,或者应用程序的业务逻辑失败并返回错误。死信主题用于处理这些无法投递的消息,并将其转发到订阅应用程序。这可以减轻应用程序处理失败消息的负担,使开发人员可以编写代码从死信主题中读取消息,修复后重新发送,或者选择放弃这些消息。

死信主题通常与重试策略和处理死信主题消息的订阅一起使用。

当配置了死信主题时,任何无法投递到应用程序的消息都会被放置在死信主题中,以便转发到处理这些消息的订阅。这可以是同一个应用程序或完全不同的应用程序。

即使底层系统不支持,Dapr 也为其所有的 pubsub 组件启用了死信主题。例如,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 组件应用 重试策略

此示例显示了如何为 pubsub pubsub 组件设置名为 pubsubRetry 的常量重试策略,每 5 秒应用一次,最多尝试投递 10 次。

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 - 如何:设置 pub/sub 命名空间消费者组

了解如何在组件中使用基于元数据的命名空间消费者组

您已经配置了 Dapr 的 pub/sub API 构建块,并且您的应用程序正在使用集中式消息代理顺利地发布和订阅主题。如果您想为应用程序执行简单的 A/B 测试、蓝/绿部署,甚至金丝雀部署,该怎么办?即使使用 Dapr,这也可能很困难。

Dapr 通过其 pub/sub 命名空间消费者组机制解决了大规模的多租户问题。

没有命名空间消费者组

假设您有一个 Kubernetes 集群,其中两个应用程序(App1 和 App2)部署在同一个命名空间(namespace-a)中。App2 发布到一个名为 order 的主题,而 App1 订阅名为 order 的主题。这将创建两个以您的应用程序命名的消费者组(App1 和 App2)。

显示基本 pubsub 过程的图示。

为了在使用集中式消息代理时进行简单的测试和部署,您创建了另一个命名空间,其中包含两个具有相同 app-id 的应用程序,App1 和 App2。

Dapr 使用单个应用程序的 app-id 创建消费者组,因此消费者组名称将保持为 App1 和 App2。

显示没有 Dapr 命名空间消费者组的多租户复杂性的图示。

为了避免这种情况,您需要在代码中“潜入”一些东西来更改 app-id,具体取决于您运行的命名空间。这种方法既麻烦又容易出错。

使用命名空间消费者组

Dapr 不仅允许您使用 UUID 和 pod 名称的 consumerID 更改消费者组的行为,还提供了一个存在于 pub/sub 组件元数据中的 命名空间机制。例如,使用 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 会自动识别其运行的命名空间并为您填充命名空间值,就像由运行时注入的动态元数据值一样。

演示

观看 此视频以了解 pub/sub 多租户的概述

下一步

1.2.9 - 如何:使用StatefulSets水平扩展订阅者

学习如何使用StatefulSet进行订阅,并通过一致的消费者ID水平扩展

与在Deployments中Pod是临时的不同,StatefulSets通过为每个Pod保持固定的身份,使得在Kubernetes上可以部署有状态应用程序。

以下是一个使用Dapr的StatefulSet示例:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: python-subscriber
spec:
  selector:
    matchLabels:
      app: python-subscriber  # 必须匹配.spec.template.metadata.labels
  serviceName: "python-subscriber"
  replicas: 3
  template:
    metadata:
      labels:
        app: python-subscriber # 必须匹配.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订阅pubsub主题时,应用程序可以定义一个consumerID,这个ID决定了订阅者在队列或主题中的位置。利用StatefulSets中Pod的固定身份,您可以为每个Pod分配一个唯一的consumerID,从而实现订阅者应用程序的水平扩展。Dapr会跟踪每个Pod的名称,并可以在组件中使用{podName}标记来声明。

当扩展某个主题的订阅者数量时,每个Dapr组件都有特定的设置来决定其行为。通常,对于多个消费者有两种选择:

  • 广播:发布到主题的每条消息将被所有订阅者接收。
  • 共享:一条消息仅由一个订阅者接收(而不是所有订阅者)。

Kafka通过consumerID为每个订阅者分配独立的位置。当实例重新启动时,它会使用相同的consumerID继续从上次的位置处理消息,而不会遗漏任何消息。以下组件示例展示了如何让多个Pod使用Kafka组件:

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"

下一步

1.2.10 - 限制 Pub/sub 主题访问

通过范围控制将 pub/sub 主题限制为特定应用程序

介绍

命名空间或组件范围可以用来限制组件的访问权限,使其仅对特定应用程序可用。这些应用程序范围的设置确保只有具有特定 ID 的应用程序才能使用该组件。

除了这种通用的组件范围外,还可以对 pub/sub 组件进行以下限制:

  • 哪些主题可以被使用(发布或订阅)
  • 哪些应用程序被允许发布到特定主题
  • 哪些应用程序被允许订阅特定主题

这被称为 pub/sub 主题范围控制

为每个 pub/sub 组件定义 pub/sub 范围。您可能有一个名为 pubsub 的 pub/sub 组件,它有一组范围,另一个 pubsub2 则有不同的范围。

要使用此主题范围,可以为 pub/sub 组件设置三个元数据属性:

  • 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(默认行为),则所有主题都是有效的。如果存在,subscriptionScopespublishingScopes 仍然生效。
    • publishingScopessubscriptionScopes 可以与 allowedTopics 结合使用以添加细粒度限制
  • spec.metadata.protectedTopics
    • 一个为所有应用程序保护的主题的逗号分隔列表。
    • 如果一个主题被标记为保护,则必须通过 publishingScopessubscriptionScopes 明确授予应用程序发布或订阅权限才能发布/订阅该主题。

这些元数据属性可用于所有 pub/sub 组件。以下示例使用 Redis 作为 pub/sub 组件。

示例 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"

下表显示了哪些应用程序被允许发布到主题:

topic1topic2topic3
app1
app2
app3

下表显示了哪些应用程序被允许订阅主题:

topic1topic2topic3
app1
app2
app3

注意:如果应用程序未列出(例如 subscriptionScopes 中的 app1),则允许其订阅所有主题。因为未使用 allowedTopics,且 app1 没有任何订阅范围,它也可以使用上面未列出的其他主题。

示例 2:限制允许的主题

如果 Dapr 应用程序向其发送消息,则会创建一个主题。在某些情况下,这种主题创建应该受到管理。例如:

  • Dapr 应用程序在生成主题名称时的错误可能导致创建无限数量的主题
  • 精简主题名称和总数,防止主题无限增长

在这些情况下可以使用 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"

注意:第三个应用程序未列出,因为如果应用程序未在范围内指定,则允许其使用所有主题。

下表显示了哪个应用程序被允许发布到主题:

ABC
app1
app2
app3

下表显示了哪个应用程序被允许订阅主题:

ABC
app1
app2
app3

示例 4:将主题标记为保护

如果您的主题涉及敏感数据,则每个新应用程序必须在 publishingScopessubscriptionScopes 中明确列出,以确保其无法读取或写入该主题。或者,您可以将主题指定为“保护”(使用 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 未列在 publishingScopessubscriptionScopes 中,它也无法与这些主题交互。

下表显示了哪个应用程序被允许发布到主题:

ABC
app1
app2
app3

下表显示了哪个应用程序被允许订阅主题:

ABC
app1
app2
app3

演示

下一步

1.2.11 - 消息生存时间 (TTL)

在发布/订阅消息中使用生存时间。

介绍

Dapr 支持为每条消息设置生存时间 (TTL)。这意味着应用程序可以为每条消息指定生存时间,过期后订阅者将不会收到这些消息。

所有 Dapr 发布/订阅组件 都兼容消息 TTL,因为 Dapr 在运行时内处理 TTL 逻辑。只需在发布消息时设置 ttlInSeconds 元数据即可。

在某些组件中,例如 Kafka,可以通过 retention.ms 在主题中配置生存时间,详见文档。使用 Dapr 的消息 TTL,使用 Kafka 的应用程序现在可以为每条消息设置生存时间,而不仅限于每个主题。

原生消息 TTL 支持

当发布/订阅组件原生支持消息生存时间时,Dapr 仅转发生存时间配置而不添加额外逻辑,保持行为的可预测性。这在组件以不同方式处理过期消息时非常有用。例如,在 Azure Service Bus 中,过期消息会被存储在死信队列中,而不是简单地删除。

支持的组件

Azure Service Bus

Azure Service Bus 支持实体级别的生存时间。这意味着消息有默认的生存时间,但也可以在发布时设置为更短的时间跨度。Dapr 传播消息的生存时间元数据,并让 Azure Service Bus 直接处理过期。

非 Dapr 订阅者

如果消息由不使用 Dapr 的订阅者消费,过期消息不会自动丢弃,因为过期是由 Dapr 运行时在 Dapr sidecar 接收到消息时处理的。然而,订阅者可以通过在云事件中添加逻辑来处理 expiration 属性,以编程方式丢弃过期消息,该属性遵循 RFC3339 格式。

当非 Dapr 订阅者使用诸如 Azure Service Bus 等原生处理消息 TTL 的组件时,他们不会收到过期消息。在这种情况下,不需要额外的逻辑。

示例

消息 TTL 可以在发布请求的元数据中设置:

curl -X "POST" http://localhost:3500/v1.0/publish/pubsub/TOPIC_A?metadata.ttlInSeconds=120 -H "Content-Type: application/json" -d '{"order-number": "345"}'
from dapr.clients import DaprClient

with DaprClient() as d:
    req_data = {
        'order-number': '345'
    }
    # 创建一个带有内容类型和主体的类型化消息
    resp = d.publish_event(
        pubsub_name='pubsub',
        topic='TOPIC_A',
        data=json.dumps(req_data),
        publish_metadata={'ttlInSeconds': '120'}
    )
    # 打印请求
    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', ['ttlInSeconds' => '120']);
});

请参阅本指南以获取发布/订阅 API 的参考。

下一步

1.2.12 - 批量发布和订阅消息

了解如何在Dapr中使用批量发布和订阅API。

通过批量发布和订阅API,您可以在单个请求中发布和订阅多个消息。在开发需要发送或接收大量消息的应用程序时,使用批量操作可以通过减少Dapr sidecar、应用程序和底层pubsub代理之间的请求总数来提高吞吐量。

批量发布消息

批量发布消息时的限制

批量发布API允许您通过单个请求将多个消息发布到一个主题。它是非事务性的,这意味着在一个批量请求中,某些消息可能会成功发布,而某些可能会失败。如果有消息发布失败,批量发布操作将返回失败消息的列表。

批量发布操作不保证消息的顺序。

示例

import io.dapr.client.DaprClientBuilder;
import io.dapr.client.DaprPreviewClient;
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 (DaprPreviewClient client = (new DaprClientBuilder()).buildPreviewClient()) {
      // 创建要发布的消息列表
      List<String> messages = new ArrayList<>();
      for (int i = 0; i < 10; i++) {
        String message = String.format("这是消息 #%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("从dapr返回的响应为空");
}
if (res.FailedEntries.Count > 0)
{
    Console.WriteLine("某些事件发布失败!");
    foreach (var failedEntry in res.FailedEntries)
    {
        Console.WriteLine("EntryId: " + failedEntry.Entry.EntryId + " 错误信息: " +
                          failedEntry.ErrorMessage);
    }
}
else
{
    Console.WriteLine("所有事件已发布!");
}
import requests
import json

base_url = "http://localhost:3500/v1.0-alpha1/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-alpha1/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-alpha1/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-alpha1/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("批量订阅者收到: %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("订阅者收到: " + JSON.stringify(data)));

    // 使用特定的maxMessagesCount和maxAwaitDurationMs向主题发布多个消息。
    await client.pubsub.bulkSubscribeWithConfig(pubSubName, topic, (data) => console.log("订阅者收到: " + 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($"收到 {bulkMessages.Entries.Count()} 条消息");
        foreach (var message in bulkMessages.Entries)
        {
            try
            {
                logger.LogInformation($"收到一条数据为 '{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; }
    }
}

目前,您只能使用HTTP客户端在Python中进行批量订阅。

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已订阅: ' + json.dumps(subscriptions))
    return jsonify(subscriptions)


# 定义处理传入消息的端点
@app.route('/checkout', methods=['POST'])
def checkout():
    messages = request.json
    print(messages)
    for message in messages:
        print(f"收到消息: {message}")
    return json.dumps({'success': True}), 200, {'ContentType': 'application/json'}

if __name__ == '__main__':
    app.run(port=5000)

组件如何处理发布和订阅批量消息

对于事件发布/订阅,涉及两种网络传输。

  1. 从/到应用程序到/从Dapr
  2. 从/到Dapr到/从pubsub代理

这些是可以进行优化的机会。当优化时,进行批量请求,从而减少总体调用次数,从而提高吞吐量并提供更好的延迟。

启用批量发布和/或批量订阅时,应用程序和Dapr sidecar之间的通信(上面第1点)针对所有组件进行了优化。

从Dapr sidecar到pubsub代理的优化取决于许多因素,例如:

  • 代理必须本质上支持批量pubsub
  • Dapr组件必须更新以支持代理提供的批量API的使用

目前,以下组件已更新以支持此级别的优化:

组件批量发布批量订阅
Kafka
Azure Servicebus
Azure Eventhubs

演示

观看以下关于批量pubsub的演示和演讲。

KubeCon Europe 2023 演讲

Dapr社区电话#77 演讲

相关链接

1.3 - 工作流

在各种微服务中进行逻辑编排

1.3.1 - 工作流概述

Dapr 工作流概述

Dapr 工作流让开发人员能够可靠地编写业务逻辑和集成。由于 Dapr 工作流是有状态的,它们支持长时间运行和容错应用程序,非常适合编排微服务。Dapr 工作流与其他 Dapr 构建块(如服务调用、发布订阅、状态管理和绑定)无缝协作。

Dapr 工作流的耐用性和弹性功能包括:

  • 提供内置的工作流运行时以驱动 Dapr 工作流执行。
  • 提供用于在代码中编写工作流的 SDK,支持多种编程语言。
  • 提供用于管理工作流(启动、查询、暂停/恢复、触发事件、终止、清除)的 HTTP 和 gRPC API。
  • 通过工作流组件与其他工作流运行时集成。
显示 Dapr 工作流基础的图示

Dapr 工作流可以应用于以下场景:

  • 涉及库存管理、支付系统和运输服务之间编排的订单处理。
  • 协调多个部门和参与者任务的人力资源入职工作流。
  • 在全国餐饮连锁店中协调数字菜单更新的推出。
  • 涉及基于 API 的分类和存储的图像处理工作流。

功能

工作流和活动

使用 Dapr 工作流,您可以编写活动,然后在工作流中编排这些活动。工作流活动是:

  • 工作流中的基本工作单元
  • 用于调用其他(Dapr)服务、与状态存储交互以及发布订阅代理。

了解更多关于工作流活动的信息。

子工作流

除了活动之外,您还可以编写工作流以调度其他工作流作为子工作流。子工作流具有独立于启动它的父工作流的实例 ID、历史记录和状态,除了终止父工作流会终止由其创建的所有子工作流这一事实。子工作流还支持自动重试策略。

了解更多关于子工作流的信息。

定时器和提醒

与 Dapr actor 相同,您可以为任何时间范围安排类似提醒的持久延迟。

了解更多关于工作流定时器提醒

使用 HTTP 调用管理工作流

当您使用工作流代码创建应用程序并使用 Dapr 运行它时,您可以调用驻留在应用程序中的特定工作流。每个单独的工作流可以:

  • 通过 POST 请求启动或终止
  • 通过 POST 请求触发以传递命名事件
  • 通过 POST 请求暂停然后恢复
  • 通过 POST 请求从您的状态存储中清除
  • 通过 GET 请求查询工作流状态

了解更多关于如何使用 HTTP 调用管理工作流的信息。

工作流模式

Dapr 工作流简化了微服务架构中复杂的、有状态的协调需求。以下部分描述了可以从 Dapr 工作流中受益的几种应用程序模式。

了解更多关于不同类型的工作流模式

工作流 SDK

Dapr 工作流 编写 SDK 是特定语言的 SDK,包含用于实现工作流逻辑的类型和函数。工作流逻辑存在于您的应用程序中,并由运行在 Dapr sidecar 中的 Dapr 工作流引擎通过 gRPC 流进行编排。

支持的 SDK

您可以使用以下 SDK 编写工作流。

语言栈
Pythondapr-ext-workflow
JavaScriptDaprWorkflowClient
.NETDapr.Workflow
Javaio.dapr.workflows
Goworkflow

试用工作流

快速入门和教程

想要测试工作流?通过以下快速入门和教程来查看工作流的实际应用:

快速入门/教程描述
工作流快速入门运行一个包含四个工作流活动的工作流应用程序,查看 Dapr 工作流的实际应用
工作流 Python SDK 示例了解如何使用 Python dapr-ext-workflow 包创建和调用 Dapr 工作流。
工作流 JavaScript SDK 示例了解如何使用 JavaScript SDK 创建和调用 Dapr 工作流。
工作流 .NET SDK 示例了解如何使用 ASP.NET Core web API 创建和调用 Dapr 工作流。
工作流 Java SDK 示例了解如何使用 Java io.dapr.workflows 包创建和调用 Dapr 工作流。
工作流 Go SDK 示例了解如何使用 Go workflow 包创建和调用 Dapr 工作流。

直接在您的应用中开始使用工作流

想要跳过快速入门?没问题。您可以直接在您的应用程序中试用工作流构建块。在Dapr 安装完成后,您可以开始使用工作流,从如何编写工作流开始。

限制

  • 状态存储: 由于某些数据库选择的底层限制,通常是 NoSQL 数据库,您可能会遇到存储内部状态的限制。例如,CosmosDB 在单个请求中最多只能有 100 个状态的单操作项限制。

观看演示

观看此视频以了解 Dapr 工作流的概述

下一步

工作流功能和概念 >>

相关链接

1.3.2 - 功能和概念

详细了解 Dapr 工作流的功能和概念

在您已经从高层次了解了工作流构建块之后,让我们深入探讨 Dapr 工作流引擎和 SDK 所包含的功能和概念。Dapr 工作流在所有支持的语言中都提供了几个核心功能和概念。

工作流

Dapr 工作流是您编写的函数,用于定义一系列按特定顺序执行的任务。Dapr 工作流引擎负责调度和执行这些任务,包括管理故障和重试。如果托管工作流的应用程序在多台机器上扩展,工作流引擎还可以在多台机器上负载均衡工作流及其任务的执行。

工作流可以调度多种类型的任务,包括:

  • 用于执行自定义逻辑的活动
  • 用于将工作流休眠任意时间长度的持久计时器
  • 用于将较大的工作流分解为较小部分的子工作流
  • 用于阻塞工作流直到接收到外部事件信号的外部事件等待器。这些任务在其相应的部分中有更详细的描述。

工作流标识

每个您定义的工作流都有一个类型名称,工作流的每次执行都需要一个唯一的_实例 ID_。工作流实例 ID 可以由您的应用程序代码生成,这在工作流对应于业务实体(如文档或作业)时很有用,或者可以是自动生成的 UUID。工作流的实例 ID 对于调试以及使用工作流 API管理工作流非常有用。

在任何给定时间,只能存在一个具有给定 ID 的工作流实例。然而,如果一个工作流实例完成或失败,其 ID 可以被新的工作流实例重用。但请注意,新工作流实例实际上会在配置的状态存储中替换旧的实例。

工作流重放

Dapr 工作流通过使用一种称为事件溯源的技术来维护其执行状态。工作流引擎不是将工作流的当前状态存储为快照,而是管理一个仅追加的历史事件日志,描述工作流所采取的各种步骤。当使用工作流 SDK 时,这些历史事件会在工作流“等待”计划任务的结果时自动存储。

当工作流“等待”计划任务时,它会从内存中卸载自己,直到任务完成。一旦任务完成,工作流引擎会再次调度工作流函数运行。此时的工作流函数执行被称为_重放_。

当工作流函数被重放时,它会从头开始再次运行。然而,当它遇到已经完成的任务时,工作流引擎不会再次调度该任务,而是:

  1. 将已完成任务的存储结果返回给工作流。
  2. 继续执行直到下一个“等待”点。

这种“重放”行为会持续到工作流函数完成或因错误而失败。

通过这种重放技术,工作流能够从任何“等待”点恢复执行,就像它从未从内存中卸载过一样。即使是先前运行的局部变量的值也可以恢复,而无需工作流引擎了解它们存储了什么数据。这种恢复状态的能力使 Dapr 工作流具有_持久性_和_容错性_。

无限循环和永恒工作流

工作流重放部分所述,工作流维护其所有操作的仅写事件溯源历史日志。为了避免资源使用失控,工作流必须限制其调度的操作数量。例如,确保您的工作流不会:

  • 在其实现中使用无限循环
  • 调度数千个任务。

您可以使用以下两种技术来编写可能需要调度极大量任务的工作流:

  1. 使用 continue-as-new API
    每个工作流 SDK 都公开了一个 continue-as-new API,工作流可以调用该 API 以使用新的输入和历史记录重新启动自己。continue-as-new API 特别适合实现“永恒工作流”,如监控代理,否则将使用 while (true) 类构造实现。使用 continue-as-new 是保持工作流历史记录小的好方法。

    continue-as-new API 会截断现有历史记录,并用新的历史记录替换它。

  2. 使用子工作流
    每个工作流 SDK 都公开了一个用于创建子工作流的 API。子工作流的行为与任何其他工作流相同,只是它由父工作流调度。子工作流具有:

    • 自己的历史记录
    • 在多台机器上分布工作流函数执行的好处。

    如果一个工作流需要调度数千个或更多任务,建议将这些任务分布在子工作流中,以免单个工作流的历史记录大小过大。

更新工作流代码

由于工作流是长时间运行且持久的,因此更新工作流代码必须非常小心。如工作流确定性限制部分所述,工作流代码必须是确定性的。如果系统中有任何未完成的工作流实例,更新工作流代码必须保留这种确定性。否则,更新工作流代码可能会导致下次这些工作流执行时出现运行时故障。

查看已知限制

工作流活动

工作流活动是工作流中的基本工作单元,是在业务流程中被编排的任务。例如,您可能会创建一个工作流来处理订单。任务可能涉及检查库存、向客户收费和创建发货。每个任务将是一个单独的活动。这些活动可以串行执行、并行执行或两者的某种组合。

与工作流不同,活动在您可以在其中执行的工作类型上没有限制。活动经常用于进行网络调用或运行 CPU 密集型操作。活动还可以将数据返回给工作流。

Dapr 工作流引擎保证每个被调用的活动在工作流的执行过程中至少执行一次。由于活动仅保证至少一次执行,建议尽可能将活动逻辑实现为幂等。

子工作流

除了活动之外,工作流还可以调度其他工作流作为_子工作流_。子工作流具有独立于启动它的父工作流的实例 ID、历史记录和状态。

子工作流有许多好处:

  • 您可以将大型工作流拆分为一系列较小的子工作流,使您的代码更易于维护。
  • 您可以在多个计算节点上同时分布工作流逻辑,这在您的工作流逻辑需要协调大量任务时很有用。
  • 您可以通过保持父工作流的历史记录较小来减少内存使用和 CPU 开销。

子工作流的返回值是其输出。如果子工作流因异常而失败,则该异常会像活动任务失败时一样显示给父工作流。子工作流还支持自动重试策略。

终止父工作流会终止由工作流实例创建的所有子工作流。有关更多信息,请参阅终止工作流 API

持久计时器

Dapr 工作流允许您为任何时间范围安排类似提醒的持久延迟,包括分钟、天甚至年。这些_持久计时器_可以由工作流安排以实现简单的延迟或为其他异步任务设置临时超时。更具体地说,持久计时器可以设置为在特定日期触发或在指定持续时间后触发。持久计时器的最大持续时间没有限制,它们在内部由内部 actor 提醒支持。例如,跟踪服务 30 天免费订阅的工作流可以使用在工作流创建后 30 天触发的持久计时器实现。工作流在等待持久计时器触发时可以安全地从内存中卸载。

重试策略

工作流支持活动和子工作流的持久重试策略。工作流重试策略与Dapr 弹性策略在以下方面是分开的和不同的。

  • 工作流重试策略由工作流作者在代码中配置,而 Dapr 弹性策略由应用程序操作员在 YAML 中配置。
  • 工作流重试策略是持久的,并在应用程序重启时保持其状态,而 Dapr 弹性策略不是持久的,必须在应用程序重启后重新应用。
  • 工作流重试策略由活动和子工作流中的未处理错误/异常触发,而 Dapr 弹性策略由操作超时和连接故障触发。

重试在内部使用持久计时器实现。这意味着工作流在等待重试触发时可以安全地从内存中卸载,从而节省系统资源。这也意味着重试之间的延迟可以任意长,包括分钟、小时甚至天。

可以同时使用工作流重试策略和 Dapr 弹性策略。例如,如果工作流活动使用 Dapr 客户端调用服务,则 Dapr 客户端使用配置的弹性策略。有关示例的更多信息,请参阅快速入门:服务到服务的弹性。但是,如果活动本身因任何原因失败,包括耗尽弹性策略的重试次数,则工作流的弹性策略会启动。

由于工作流重试策略是在代码中配置的,因此具体的开发者体验可能会因工作流 SDK 的版本而异。通常,工作流重试策略可以通过以下参数进行配置。

参数描述
最大尝试次数执行活动或子工作流的最大次数。
首次重试间隔第一次重试前的等待时间。
退避系数用于确定退避增长率的系数。例如,系数为 2 会使每次后续重试的等待时间加倍。
最大重试间隔每次后续重试前的最大等待时间。
重试超时重试的总体超时,无论配置的最大尝试次数如何。

外部事件

有时工作流需要等待由外部系统引发的事件。例如,如果订单处理工作流中的总成本超过某个阈值,审批工作流可能需要人类明确批准订单请求。另一个例子是一个问答游戏编排工作流,它在等待所有参与者提交他们对问答问题的答案时暂停。这些中间执行输入被称为_外部事件_。

外部事件具有_名称_和_负载_,并传递给单个工作流实例。工作流可以创建“等待外部事件”任务,订阅外部事件并_等待_这些任务以阻止执行,直到接收到事件。然后,工作流可以读取这些事件的负载,并决定采取哪些下一步。外部事件可以串行或并行处理。外部事件可以由其他工作流或工作流代码引发。

工作流还可以等待同名的多个外部事件信号,在这种情况下,它们会以先进先出 (FIFO) 的方式分派给相应的工作流任务。如果工作流接收到外部事件信号,但尚未创建“等待外部事件”任务,则事件将保存到工作流的历史记录中,并在工作流请求事件后立即消费。

了解有关外部系统交互的更多信息。

工作流后端

Dapr 工作流依赖于 Go 的持久任务框架(即 durabletask-go)作为执行工作流的核心引擎。该引擎设计为支持多种后端实现。例如,durabletask-go 仓库包括一个 SQLite 实现,Dapr 仓库包括一个 actor 实现。

默认情况下,Dapr 工作流支持 actor 后端,该后端稳定且可扩展。然而,您可以选择 Dapr 工作流中支持的其他后端。例如,SQLite(待定未来版本)可以是本地开发和测试的后端选项。

后端实现在很大程度上与您看到的工作流核心引擎或编程模型解耦。后端主要影响:

  • 如何存储工作流状态
  • 如何在副本之间协调工作流执行

从这个意义上说,它类似于 Dapr 的状态存储抽象,但专为工作流设计。无论使用哪个后端,所有 API 和编程模型功能都是相同的。

清除

工作流状态可以从状态存储中清除,清除其所有历史记录并删除与特定工作流实例相关的所有元数据。清除功能用于已运行到 COMPLETEDFAILEDTERMINATED 状态的工作流。

工作流 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>("GetRandomString");
// 这样做!!
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>("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("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 - 工作流模式

编写不同类型的工作流模式

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'步骤 1: 接收到输入: {activity_input}.')
    # 执行一些操作
    return activity_input + 1


def step2(ctx, activity_input):
    print(f'步骤 2: 接收到输入: {activity_input}.')
    # 执行一些操作
    return activity_input * 2


def step3(ctx, activity_input):
    print(f'步骤 3: 接收到输入: {activity_input}.')
    # 执行一些操作
    return activity_input ^ 2


def error_handler(ctx, error):
    print(f'执行错误处理程序: {error}.')
    # 执行一些补偿操作

注意 工作流重试策略将在 Python SDK 的未来版本中提供。

import { DaprWorkflowClient, WorkflowActivityContext, WorkflowContext, WorkflowRuntime, TWorkflow } from "@dapr/dapr";

async function start() {
  // 更新 gRPC 客户端和工作者以使用本地地址和端口
  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);

  // 将工作者启动包装在 try-catch 块中以处理启动期间的任何错误
  try {
    await workflowRuntime.start();
    console.log("工作流运行时启动成功");
  } catch (error) {
    console.error("启动工作流运行时时出错:", error);
  }

  // 调度新的编排
  try {
    const id = await workflowClient.scheduleNewWorkflow(sequence);
    console.log(`编排已调度,ID: ${id}`);

    // 等待编排完成
    const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30);

    console.log(`编排完成!结果: ${state?.serializedOutput}`);
  } catch (error) {
    console.error("调度或等待编排时出错:", error);
  }

  await workflowRuntime.stop();
  await workflowClient.stop();

  // 停止 dapr sidecar
  process.exit(0);
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});
// 支持长时间中断的指数退避重试策略
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) // 任务失败会作为 TaskFailedException 显示
{
    // 重试过期 - 应用自定义补偿逻辑
    await context.CallActivityAsync<long[]>("MyCompensation", options: retryOptions);
    throw;
}

注意 在上面的示例中,"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("步骤 1: 接收到输入: %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("步骤 2: 接收到输入: %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("步骤 3: 接收到输入: %s", input)
	return int(math.Pow(float64(input), 2)), nil
}

如您所见,工作流被表达为所选编程语言中的简单语句序列。这使得组织中的任何工程师都可以快速理解端到端的流程,而不必了解端到端的系统架构。

在幕后,Dapr 工作流运行时:

  • 负责执行工作流并确保其运行到完成。
  • 自动保存进度。
  • 如果工作流进程本身因任何原因失败,自动从上次完成的步骤恢复工作流。
  • 允许在目标编程语言中自然地表达错误处理,使您可以轻松实现补偿逻辑。
  • 提供内置的重试配置原语,以简化为工作流中的各个步骤配置复杂重试策略的过程。

扇出/扇入

在扇出/扇入设计模式中,您可以在多个工作者上同时执行多个任务,等待它们完成,并对结果进行一些聚合。

显示扇出/扇入工作流模式如何工作的图示

除了前一个模式中提到的挑战外,在手动实现扇出/扇入模式时还有几个重要问题需要考虑:

  • 如何控制并行度?
  • 如何知道何时触发后续聚合步骤?
  • 如果并行步骤的数量是动态的怎么办?

Dapr 工作流提供了一种将扇出/扇入模式表达为简单函数的方法,如以下示例所示:

import time
from typing import List
import dapr.ext.workflow as wf


def batch_processing_workflow(ctx: wf.DaprWorkflowContext, wf_input: int):
    # 获取一批 N 个工作项以并行处理
    work_batch = yield ctx.call_activity(get_work_batch, input=wf_input)

    # 调度 N 个并行任务以处理工作项并等待所有任务完成
    parallel_tasks = [ctx.call_activity(process_work_item, input=work_item) for work_item in work_batch]
    outputs = yield wf.when_all(parallel_tasks)

    # 聚合结果并将其发送到另一个活动
    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'处理工作项: {work_item}.')
    time.sleep(5)
    result = work_item * 2
    print(f'工作项 {work_item} 已处理. 结果: {result}.')
    return result


def process_results(ctx, final_result: int):
    print(f'最终结果: {final_result}.')
import {
  Task,
  DaprWorkflowClient,
  WorkflowActivityContext,
  WorkflowContext,
  WorkflowRuntime,
  TWorkflow,
} from "@dapr/dapr";

// 将整个代码包装在一个立即调用的异步函数中
async function start() {
  // 更新 gRPC 客户端和工作者以使用本地地址和端口
  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(`生成 ${count} 个工作项...`);

    const workItems: string[] = Array.from({ length: count }, (_, i) => `工作项 ${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(`处理工作项: ${item}`);

    // 模拟一些需要可变时间的工作
    const sleepTime = Math.random() * 5000;
    await sleep(sleepTime);

    // 返回给定工作项的结果,在这种情况下也是一个随机数
    // 有关工作流中随机数的更多信息,请查看
    // https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-code-constraints?tabs=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);

  // 将工作者启动包装在 try-catch 块中以处理启动期间的任何错误
  try {
    await workflowRuntime.start();
    console.log("工作者启动成功");
  } catch (error) {
    console.error("启动工作者时出错:", error);
  }

  // 调度新的编排
  try {
    const id = await workflowClient.scheduleNewWorkflow(workflow);
    console.log(`编排已调度,ID: ${id}`);

    // 等待编排完成
    const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30);

    console.log(`编排完成!结果: ${state?.serializedOutput}`);
  } catch (error) {
    console.error("调度或等待编排时出错:", error);
  }

  // 停止工作者和客户端
  await workflowRuntime.stop();
  await workflowClient.stop();

  // 停止 dapr sidecar
  process.exit(0);
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});
// 获取要并行处理的 N 个工作项的列表。
object[] workBatch = await context.CallActivityAsync<object[]>("GetWorkBatch", null);

// 调度并行任务,但不等待它们完成。
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);
}

// 一切都已调度。在此处等待,直到所有并行任务完成。
await Task.WhenAll(parallelTasks);

// 聚合所有 N 个输出并发布结果。
int sum = parallelTasks.Sum(t => t.Result);
await context.CallActivityAsync("PostResults", sum);
public class FaninoutWorkflow extends Workflow {
    @Override
    public WorkflowStub create() {
        return ctx -> {
            // 获取要并行处理的 N 个工作项的列表。
            Object[] workBatch = ctx.callActivity("GetWorkBatch", Object[].class).await();
            // 调度并行任务,但不等待它们完成。
            List<Task<Integer>> tasks = Arrays.stream(workBatch)
                    .map(workItem -> ctx.callActivity("ProcessWorkItem", workItem, int.class))
                    .collect(Collectors.toList());
            // 一切都已调度。在此处等待,直到所有并行任务完成。
            List<Integer> results = ctx.allOf(tasks).await();
            // 聚合所有 N 个输出并发布结果。
            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("处理工作项: %d\n", workItem)
	time.Sleep(time.Second * 5)
	result := workItem * 2
	fmt.Printf("工作项 %d 已处理. 结果: %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("最终结果: %d\n", finalResult)
	return finalResult, nil
}

此示例的关键要点是:

  • 扇出/扇入模式可以使用普通编程构造表达为简单函数
  • 并行任务的数量可以是静态的或动态的
  • 工作流本身能够聚合并行执行的结果

此外,工作流的执行是持久的。如果一个工作流启动了 100 个并行任务执行,并且只有 40 个在进程崩溃前完成,工作流会自动重新启动并仅调度剩余的 60 个任务。

可以进一步使用简单的、特定语言的构造来限制并发度。下面的示例代码说明了如何将扇出的程度限制为仅 5 个并发活动执行:


// 回顾之前的示例...
// 获取要并行处理的 N 个工作项的列表。
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);

以这种方式限制并发度对于限制对共享资源的争用可能很有用。例如,如果活动需要调用具有自身并发限制的外部资源(如数据库或外部 API),则确保不超过指定数量的活动同时调用该资源可能很有用。

异步 HTTP API

异步 HTTP API 通常使用异步请求-回复模式实现。传统上实现此模式涉及以下步骤:

  1. 客户端向 HTTP API 端点(启动 API)发送请求
  2. 启动 API 将消息写入后端队列,从而触发长时间运行操作的开始
  3. 在调度后端操作后,启动 API 立即向客户端返回 HTTP 202 响应,其中包含可用于轮询状态的标识符
  4. 状态 API 查询包含长时间运行操作状态的数据库
  5. 客户端重复轮询 状态 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,这意味着客户端可以停止轮询更新。

监控

监控模式是一个通常包括以下步骤的重复过程:

  1. 检查系统状态
  2. 根据该状态采取某些行动 - 例如发送通知
  3. 休眠一段时间
  4. 重复

下图提供了此模式的粗略说明。

显示监控模式如何工作的图示

根据业务需求,可能只有一个监控器,也可能有多个监控器,每个业务实体(例如股票)一个。此外,休眠时间可能需要根据情况进行更改。这些要求使得使用基于 cron 的调度系统不切实际。

Dapr 工作流通过允许您实现_永恒工作流_本地支持此模式。Dapr 工作流公开了一个 continue-as-new API,工作流作者可以使用该 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):
    # 轮询与此 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  # 在健康状态下检查频率较低
    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  # 在不健康状态下检查频率较高

    yield ctx.create_timer(fire_at=ctx.current_utc_datetime + timedelta(minutes=next_sleep_interval))

    # 使用新的 JobStatus 输入从头开始重新启动
    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") {
      // 在健康状态下检查频率较低
      // 设置持续时间为 1 小时
      duration = 60 * 60;
    } else {
      yield ctx.callActivity(alertActivity, "job unhealthy");
      // 在不健康状态下检查频率较高
      // 设置持续时间为 5 分钟
      duration = 5 * 60;
    }

    // 将工作流置于休眠状态,直到确定的时间
    ctx.createTimer(duration);

    // 使用更新的状态从头开始重新启动
    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;

        // 在健康状态下检查频率较低
        nextSleepInterval = TimeSpan.FromMinutes(60);
    }
    else
    {
        if (myEntityState.IsHealthy)
        {
            myEntityState.IsHealthy = false;
            await context.CallActivityAsync("SendAlert", myEntityState);
        }

        // 在不健康状态下检查频率较高
        nextSleepInterval = TimeSpan.FromMinutes(5);
    }

    // 将工作流置于休眠状态,直到确定的时间
    await context.CreateTimer(nextSleepInterval);

    // 使用更新的状态从头开始重新启动
    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) {
        // 在健康状态下检查频率较低
        nextSleepInterval = Duration.ofMinutes(60);
      } else {

        ctx.callActivity(DemoWorkflowAlertActivity.class.getName()).await();

        // 在不健康状态下检查频率较高
        nextSleepInterval = Duration.ofMinutes(5);
      }

      // 将工作流置于休眠状态,直到确定的时间
      try {
        ctx.createTimer(nextSleepInterval);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }

      // 使用更新的状态从头开始重新启动
      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 来优雅地终止自身。

外部系统交互

在某些情况下,工作流可能需要暂停并等待外部系统执行某些操作。例如,工作流可能需要暂停并等待接收到付款。在这种情况下,支付系统可能会在收到付款时将事件发布到 pub/sub 主题,并且该主题上的侦听器可以使用触发事件工作流 API向工作流触发事件。

另一个非常常见的场景是工作流需要暂停并等待人类,例如在批准采购订单时。Dapr 工作流通过外部事件功能支持此事件模式。

以下是涉及人类的采购订单工作流示例:

  1. 收到采购订单时触发工作流。
  2. 工作流中的规则确定需要人类执行某些操作。例如,采购订单成本超过某个自动批准阈值。
  3. 工作流发送请求人类操作的通知。例如,它向指定的审批人发送带有批准链接的电子邮件。
  4. 工作流暂停并等待人类通过点击链接批准或拒绝订单。
  5. 如果在指定时间内未收到批准,工作流将恢复并执行某些补偿逻辑,例如取消订单。

下图说明了此流程。

显示外部系统交互模式如何与人类交互的图示

以下示例代码显示了如何使用 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):
    # 低于 $1000 的订单自动批准
    if order.cost < 1000:
        return "Auto-approved"

    # $1000 或以上的订单需要经理批准
    yield ctx.call_activity(send_approval_request, input=order)

    # 必须在 24 小时内收到批准,否则将被取消。
    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"

    # 订单已获批准
    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'*** 发送审批请求: {order}')


def place_order(_, order: Order) -> None:
    print(f'*** 下订单: {order}')
import {
  Task,
  DaprWorkflowClient,
  WorkflowActivityContext,
  WorkflowContext,
  WorkflowRuntime,
  TWorkflow,
} from "@dapr/dapr";
import * as readlineSync from "readline-sync";

// 将整个代码包装在一个立即调用的异步函数中
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));
  }

  // 更新 gRPC 客户端和工作者以使用本地地址和端口
  const daprHost = "localhost";
  const daprPort = "50001";
  const workflowClient = new DaprWorkflowClient({
    daprHost,
    daprPort,
  });
  const workflowRuntime = new WorkflowRuntime({
    daprHost,
    daprPort,
  });

  // 发送审批请求给经理的活动函数
  const sendApprovalRequest = async (_: WorkflowActivityContext, order: Order) => {
    // 模拟一些需要时间的工作
    await sleep(3000);
    console.log(`发送审批请求: ${order.product}`);
  };

  // 下订单的活动函数
  const placeOrder = async (_: WorkflowActivityContext, order: Order) => {
    console.log(`下订单: ${order.product}`);
  };

  // 表示采购订单工作流的编排函数
  const purchaseOrderWorkflow: TWorkflow = async function* (ctx: WorkflowContext, order: Order): any {
    // 低于 $1000 的订单自动批准
    if (order.cost < 1000) {
      return "Auto-approved";
    }

    // $1000 或以上的订单需要经理批准
    yield ctx.callActivity(sendApprovalRequest, order);

    // 必须在 24 小时内收到批准,否则将被取消。
    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);

  // 将工作者启动包装在 try-catch 块中以处理启动期间的任何错误
  try {
    await workflowRuntime.start();
    console.log("工作者启动成功");
  } catch (error) {
    console.error("启动工作者时出错:", error);
  }

  // 调度新的编排
  try {
    const cost = readlineSync.questionInt("输入订单金额:");
    const approver = readlineSync.question("输入审批人:");
    const timeout = readlineSync.questionInt("输入订单超时时间(秒):");
    const order = new Order(cost, "MyProduct", 1);
    const id = await workflowClient.scheduleNewWorkflow(purchaseOrderWorkflow, order);
    console.log(`编排已调度,ID: ${id}`);

    // 异步提示批准
    promptForApproval(approver, workflowClient, id);

    // 等待编排完成
    const state = await workflowClient.waitForWorkflowCompletion(id, undefined, timeout + 2);

    console.log(`编排完成!结果: ${state?.serializedOutput}`);
  } catch (error) {
    console.error("调度或等待编排时出错:", error);
  }

  // 停止工作者和客户端
  await workflowRuntime.stop();
  await workflowClient.stop();

  // 停止 dapr sidecar
  process.exit(0);
}

async function promptForApproval(approver: string, workflowClient: DaprWorkflowClient, id: string) {
  if (readlineSync.keyInYN("按 [Y] 批准订单... Y/是, N/否")) {
    const approvalEvent = { approver: approver };
    await workflowClient.raiseEvent(id, "approval_received", approvalEvent);
  } else {
    return "订单被拒绝";
  }
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});
public override async Task<OrderResult> RunAsync(WorkflowContext context, OrderPayload order)
{
    // ...(其他步骤)...

    // 需要对超过某个阈值的订单进行批准
    if (order.TotalCost > OrderApprovalThreshold)
    {
        try
        {
            // 请求人类批准此订单
            await context.CallActivityAsync(nameof(RequestApprovalActivity), order);

            // 暂停并等待人类批准订单
            ApprovalResult approvalResult = await context.WaitForExternalEventAsync<ApprovalResult>(
                eventName: "ManagerApproval",
                timeout: TimeSpan.FromDays(3));
            if (approvalResult == ApprovalResult.Rejected)
            {
                // 订单被拒绝,在此结束工作流
                return new OrderResult(Processed: false);
            }
        }
        catch (TaskCanceledException)
        {
            // 批准超时会导致自动取消订单
            return new OrderResult(Processed: false);
        }
    }

    // ...(其他步骤)...

    // 以成功结果结束工作流
    return new OrderResult(Processed: true);
}

注意 在上面的示例中,RequestApprovalActivity 是要调用的工作流活动的名称,ApprovalResult 是由工作流应用程序定义的枚举。为了简洁起见,这些定义未包含在示例代码中。

public class ExternalSystemInteractionWorkflow extends Workflow {
    @Override
    public WorkflowStub create() {
        return ctx -> {
            // ...其他步骤...
            Integer orderCost = ctx.getInput(int.class);
            // 需要对超过某个阈值的订单进行批准
            if (orderCost > ORDER_APPROVAL_THRESHOLD) {
                try {
                    // 请求人类批准此订单
                    ctx.callActivity("RequestApprovalActivity", orderCost, Void.class).await();
                    // 暂停并等待人类批准订单
                    boolean approved = ctx.waitForExternalEvent("ManagerApproval", Duration.ofDays(3), boolean.class).await();
                    if (!approved) {
                        // 订单被拒绝,在此结束工作流
                        ctx.complete("Process reject");
                    }
                } catch (TaskCanceledException e) {
                    // 批准超时会导致自动取消订单
                    ctx.complete("Process cancel");
                }
            }
            // ...其他步骤...

            // 以成功结果结束工作流
            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
	}
	// 低于 $1000 的订单自动批准
	if order.Cost < 1000 {
		return "Auto-approved", nil
	}
	// $1000 或以上的订单需要经理批准
	if err := ctx.CallActivity(SendApprovalRequest, workflow.ActivityInput(order)).Await(nil); err != nil {
		return "", err
	}
	// 必须在 24 小时内收到批准,否则将被取消
	var approval Approval
	if err := ctx.WaitForExternalEvent("approval_received", time.Hour*24).Await(&approval); err != nil {
		// 假设发生了超时 - 无论如何;一个错误。
		return "error/cancelled", err
	}
	// 订单已获批准
	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("*** 发送审批请求: %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("*** 下订单: %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);
  }
// 向等待的工作流触发工作流事件
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")
}

外部事件不必由人类直接触发。它们也可以由其他系统触发。例如,工作流可能需要暂停并等待接收到付款。在这种情况下,支付系统可能会在收到付款时将事件发布到 pub/sub 主题,并且该主题上的侦听器可以使用触发事件工作流 API 向工作流触发事件。

下一步

工作流架构 >>

相关链接

1.3.4 - 工作流架构

Dapr 工作流引擎架构

Dapr 工作流 允许开发者使用多种编程语言的普通代码定义工作流。工作流引擎运行在 Dapr sidecar 内部,并协调作为应用程序一部分部署的工作流代码。本文描述了:

  • Dapr 工作流引擎的架构
  • 工作流引擎如何与应用程序代码交互
  • 工作流引擎如何融入整体 Dapr 架构
  • 不同的工作流后端如何与工作流引擎协作

有关如何在应用程序中编写 Dapr 工作流的更多信息,请参见 如何:编写工作流

Dapr 工作流引擎的内部支持来自于 Dapr 的 actor 运行时。下图展示了 Kubernetes 模式下的 Dapr 工作流架构:

展示 Kubernetes 模式下工作流架构如何工作的图示

要使用 Dapr 工作流构建块,您需要在应用程序中使用 Dapr 工作流 SDK 编写工作流代码,该 SDK 内部通过 gRPC 流连接到 sidecar。这会注册工作流和任何工作流活动,或工作流可以调度的任务。

引擎直接嵌入在 sidecar 中,并通过 durabletask-go 框架库实现。此框架允许您更换不同的存储提供者,包括为 Dapr 创建的存储提供者,该提供者在幕后利用内部 actor。由于 Dapr 工作流使用 actor,您可以将工作流状态存储在状态存储中。

Sidecar 交互

当工作流应用程序启动时,它使用工作流编写 SDK 向 Dapr sidecar 发送 gRPC 请求,并根据 服务器流式 RPC 模式 获取工作流工作项流。这些工作项可以是从“启动一个新的 X 工作流”(其中 X 是工作流的类型)到“调度活动 Y,输入 Z 以代表工作流 X 运行”的任何内容。

工作流应用程序执行相应的工作流代码,然后将执行结果通过 gRPC 请求发送回 sidecar。

Dapr 工作流引擎协议

所有交互都通过单个 gRPC 通道进行,并由应用程序发起,这意味着应用程序不需要打开任何入站端口。这些交互的细节由特定语言的 Dapr 工作流编写 SDK 内部处理。

工作流和 actor sidecar 交互的区别

如果您熟悉 Dapr actor,您可能会注意到工作流与 actor 的 sidecar 交互方式有一些不同。

Actor工作流
Actor 可以使用 HTTP 或 gRPC 与 sidecar 交互。工作流仅使用 gRPC。由于工作流 gRPC 协议的复杂性,实现工作流时需要一个 SDK。
Actor 操作从 sidecar 推送到应用程序代码。这需要应用程序在特定的 应用端口 上监听。对于工作流,操作是由应用程序使用流协议从 sidecar 拉取的。应用程序不需要监听任何端口即可运行工作流。
Actor 明确地向 sidecar 注册自己。工作流不向 sidecar 注册自己。嵌入的引擎不跟踪工作流类型。这一责任被委托给工作流应用程序及其 SDK。

工作流分布式追踪

工作流引擎使用 durabletask-go 核心通过 Open Telemetry SDKs 写入分布式追踪。这些追踪由 Dapr sidecar 自动捕获并导出到配置的 Open Telemetry 提供者,例如 Zipkin。

引擎管理的每个工作流实例都表示为一个或多个跨度。有一个单一的父跨度表示完整的工作流执行,以及各种任务的子跨度,包括活动任务执行和持久计时器的跨度。

工作流活动代码目前无法访问追踪上下文。

内部工作流 actor

在 Dapr sidecar 内部注册了两种类型的 actor,以支持工作流引擎:

  • dapr.internal.{namespace}.{appID}.workflow
  • dapr.internal.{namespace}.{appID}.activity

{namespace} 值是 Dapr 命名空间,如果没有配置命名空间,则默认为 default{appID} 值是应用程序的 ID。例如,如果您有一个名为 “wfapp” 的工作流应用程序,那么工作流 actor 的类型将是 dapr.internal.default.wfapp.workflow,活动 actor 的类型将是 dapr.internal.default.wfapp.activity

下图展示了在 Kubernetes 场景中内部工作流 actor 如何操作:

展示跨集群内部注册的 actor 的图示

与用户定义的 actor 一样,内部工作流 actor 由 actor 放置服务分布在集群中。它们也维护自己的状态并使用提醒。然而,与存在于应用程序代码中的 actor 不同,这些 内部 actor 嵌入在 Dapr sidecar 中。应用程序代码完全不知道这些 actor 的存在。

工作流 actor

工作流 actor 负责管理应用程序中运行的所有工作流的状态和放置。每当创建一个工作流实例时,就会激活一个新的工作流 actor 实例。工作流 actor 的 ID 是工作流的 ID。这个内部 actor 存储工作流的状态,并通过 actor 放置服务确定工作流代码执行的节点。

每个工作流 actor 使用以下键在配置的状态存储中保存其状态:

描述
inbox-NNNNNN工作流的收件箱实际上是一个驱动工作流执行的 消息 的 FIFO 队列。示例消息包括工作流创建消息、活动任务完成消息等。每条消息都存储在状态存储中的一个键中,名称为 inbox-NNNNNN,其中 NNNNNN 是一个 6 位数,表示消息的顺序。这些状态键在相应的消息被工作流消费后被移除。
history-NNNNNN工作流的历史是一个有序的事件列表,表示工作流的执行历史。历史中的每个键保存单个历史事件的数据。像一个只追加的日志一样,工作流历史事件只会被添加而不会被移除(除非工作流执行“继续为新”操作,这会清除所有历史并使用新输入重新启动工作流)。
customStatus包含用户定义的工作流状态值。每个工作流 actor 实例只有一个 customStatus 键。
metadata以 JSON blob 形式包含有关工作流的元信息,包括收件箱的长度、历史的长度以及表示工作流生成的 64 位整数(用于实例 ID 被重用的情况)。长度信息用于确定在加载或保存工作流状态更新时需要读取或写入哪些键。

下图展示了工作流 actor 的典型生命周期。

Dapr 工作流 Actor 流程图

总结:

  1. 当工作流 actor 收到新消息时被激活。
  2. 新消息触发相关的工作流代码(在您的应用程序中)运行,并将执行结果返回给工作流 actor。
  3. 一旦收到结果,actor 会根据需要调度任何任务。
  4. 调度后,actor 在状态存储中更新其状态。
  5. 最后,actor 进入空闲状态,直到收到另一条消息。在此空闲时间内,sidecar 可能决定从内存中卸载工作流 actor。

活动 actor

活动 actor 负责管理所有工作流活动调用的状态和放置。每当工作流调度一个活动任务时,就会激活一个新的活动 actor 实例。活动 actor 的 ID 是工作流的 ID 加上一个序列号(序列号从 0 开始)。例如,如果一个工作流的 ID 是 876bf371,并且是工作流调度的第三个活动,它的 ID 将是 876bf371::2,其中 2 是序列号。

每个活动 actor 将单个键存储到状态存储中:

描述
activityState键包含活动调用负载,其中包括序列化的活动输入数据。此键在活动调用完成后自动删除。

下图展示了活动 actor 的典型生命周期。

工作流活动 Actor 流程图

活动 actor 是短暂的:

  1. 当工作流 actor 调度一个活动任务时,活动 actor 被激活。
  2. 活动 actor 然后立即调用工作流应用程序以调用相关的活动代码。
  3. 一旦活动代码完成运行并返回其结果,活动 actor 将执行结果的消息发送给父工作流 actor。
  4. 一旦结果被发送,工作流被触发以继续其下一步。

提醒使用和执行保证

Dapr 工作流通过使用 actor 提醒 来确保工作流的容错性,以从瞬态系统故障中恢复。在调用应用程序工作流代码之前,工作流或活动 actor 将创建一个新的提醒。如果应用程序代码执行没有中断,提醒将被删除。然而,如果托管相关工作流或活动的节点或 sidecar 崩溃,提醒将重新激活相应的 actor 并重试执行。

展示调用工作流 actor 过程的图示

状态存储使用

Dapr 工作流在内部使用 actor 来驱动工作流的执行。像任何 actor 一样,这些内部工作流 actor 将其状态存储在配置的状态存储中。任何支持 actor 的状态存储都隐式支持 Dapr 工作流。

工作流 actor 部分所述,工作流通过追加到历史日志中增量保存其状态。工作流的历史日志分布在多个状态存储键中,以便每个“检查点”只需追加最新的条目。

每个检查点的大小由工作流在进入空闲状态之前调度的并发操作数决定。顺序工作流 因此将对状态存储进行较小的批量更新,而 扇出/扇入工作流 将需要更大的批量。批量的大小还受到工作流 调用活动子工作流 时输入和输出大小的影响。

工作流 actor 状态存储交互图示

不同的状态存储实现可能隐式对您可以编写的工作流类型施加限制。例如,Azure Cosmos DB 状态存储将项目大小限制为 2 MB 的 UTF-8 编码 JSON(来源)。活动或子工作流的输入或输出负载作为状态存储中的单个记录存储,因此 2 MB 的项目限制意味着工作流和活动的输入和输出不能超过 2 MB 的 JSON 序列化数据。

同样,如果状态存储对批量事务的大小施加限制,这可能会限制工作流可以调度的并行操作数。

工作流状态可以从状态存储中清除,包括其所有历史记录。每个 Dapr SDK 都公开用于清除特定工作流实例的所有元数据的 API。

工作流可扩展性

由于 Dapr 工作流在内部使用 actor 实现,Dapr 工作流具有与 actor 相同的可扩展性特征。放置服务:

  • 不区分工作流 actor 和您在应用程序中定义的 actor
  • 将使用与 actor 相同的算法对工作流进行负载均衡

工作流的预期可扩展性由以下因素决定:

  • 用于托管工作流应用程序的机器数量
  • 运行工作流的机器上可用的 CPU 和内存资源
  • 为 actor 配置的状态存储的可扩展性
  • actor 放置服务和提醒子系统的可扩展性

目标应用程序中工作流代码的实现细节也在个别工作流实例的可扩展性中起作用。每个工作流实例一次在单个节点上执行,但工作流可以调度在其他节点上运行的活动和子工作流。

工作流还可以调度这些活动和子工作流以并行运行,允许单个工作流可能将计算任务分布在集群中的所有可用节点上。

跨多个 Dapr 实例扩展的工作流和活动 actor 图示

工作流不控制负载在集群中的具体分布方式。例如,如果一个工作流调度 10 个活动任务并行运行,所有 10 个任务可能在多达 10 个不同的计算节点上运行,也可能在少至一个计算节点上运行。实际的扩展行为由 actor 放置服务决定,该服务管理表示工作流每个任务的 actor 的分布。

工作流后端

工作流后端负责协调和保存工作流的状态。在任何给定时间,只能支持一个后端。您可以将工作流后端配置为一个组件,类似于 Dapr 中的任何其他组件。配置要求:

  1. 指定工作流后端的类型。
  2. 提供特定于该后端的配置。

例如,以下示例演示了如何定义一个 actor 后端组件。Dapr 工作流目前默认仅支持 actor 后端,用户不需要定义 actor 后端组件即可使用它。

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: actorbackend
spec:
  type: workflowbackend.actor
  version: v1

工作流延迟

为了提供关于持久性和弹性的保证,Dapr 工作流频繁地写入状态存储并依赖提醒来驱动执行。因此,Dapr 工作流可能不适合对延迟敏感的工作负载。预期的高延迟来源包括:

  • 在持久化工作流状态时来自状态存储的延迟。
  • 在使用大型历史记录重新加载工作流时来自状态存储的延迟。
  • 集群中过多活动提醒导致的延迟。
  • 集群中高 CPU 使用率导致的延迟。

有关工作流 actor 设计如何影响执行延迟的更多详细信息,请参见 提醒使用和执行保证部分

下一步

编写工作流 >>

相关链接

1.3.5 - 如何:编写一个工作流

学习如何使用Dapr工作流引擎开发和编写工作流

本文提供了如何编写由Dapr工作流引擎执行的工作流的高级概述。

以代码形式编写工作流

Dapr工作流逻辑是通过通用编程语言实现的,这使您可以:

  • 使用您喜欢的编程语言(无需学习新的DSL或YAML模式)。
  • 访问语言的标准库。
  • 构建您自己的库和抽象。
  • 使用调试器并检查本地变量。
  • 为您的工作流编写单元测试,就像应用程序逻辑的其他部分一样。

Dapr sidecar不加载任何工作流定义。相反,sidecar仅负责驱动工作流的执行,而所有具体的工作流任务则由应用程序的一部分来处理。

编写工作流任务

工作流任务是工作流中的基本工作单元,是在业务流程中被编排的任务。

定义您希望工作流执行的工作流任务。任务是一个函数定义,可以接受输入并返回输出。以下示例创建了一个名为hello_act的任务,用于打印当前计数器的值。hello_act是一个从WorkflowActivityContext类派生的函数。

def hello_act(ctx: WorkflowActivityContext, input):
    global counter
    counter += input
    print(f'New counter value is: {counter}!', flush=True)

查看上下文中的hello_act工作流任务。

定义您希望工作流执行的工作流任务。任务被封装在实现工作流任务的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>();
    }

    //...

}

查看完整的ProcessPaymentActivity.cs工作流任务示例。

定义您希望工作流执行的工作流任务。任务被封装在实现工作流任务的公共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;
  }
}

查看上下文中的Java SDK工作流任务示例。

定义您希望工作流执行的每个工作流任务。任务输入可以通过ctx.GetInput从上下文中解组。任务应定义为接受ctx workflow.ActivityContext参数并返回接口和错误。

func TestActivity(ctx workflow.ActivityContext) (any, error) {
	var input int
	if err := ctx.GetInput(&input); err != nil {
		return "", err
	}
	
	// Do something here
	return "result", nil
}

查看上下文中的Go SDK工作流任务示例。

编写工作流

接下来,在工作流中注册并调用任务。

hello_world_wf函数是从一个名为DaprWorkflowContext的类派生的,具有输入和输出参数类型。它还包括一个yield语句,该语句完成工作流的繁重工作并调用工作流任务。

def hello_world_wf(ctx: DaprWorkflowContext, input):
    print(f'{input}')
    yield ctx.call_activity(hello_act, input=1)
    yield ctx.call_activity(hello_act, input=10)
    yield ctx.wait_for_external_event("event1")
    yield ctx.call_activity(hello_act, input=100)
    yield ctx.call_activity(hello_act, input=1000)

查看上下文中的hello_world_wf工作流。

接下来,使用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, intput: TInput): TOutput => {
      const wfActivityContext = new WorkflowActivityContext(ctx);
      return fn(wfActivityContext, intput);
    };
    this.worker.addNamedActivity(name, activityWrapper);
    return this;
  }

  // Start the workflow runtime processing items and block.
  public async start() {
    await this.worker.start();
  }

}

查看上下文中的WorkflowRuntime

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);
        }
    }

查看OrderProcessingWorkflow.cs中的完整工作流示例。

接下来,使用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);
  }
}

查看上下文中的Java SDK工作流。

定义您的工作流函数,参数为ctx *workflow.WorkflowContext,返回任何和错误。从您的工作流中调用您定义的任务。

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工作流。

编写应用程序

最后,使用工作流编写应用程序。

在以下示例中,对于使用Python SDK的基本Python hello world应用程序,您的项目代码将包括:

  • 一个名为DaprClient的Python包,用于接收Python SDK功能。
  • 一个带有扩展的构建器,称为:
  • API调用。在下面的示例中,这些调用启动、暂停、恢复、清除和终止工作流。
from dapr.ext.workflow import WorkflowRuntime, DaprWorkflowContext, WorkflowActivityContext
from dapr.clients import DaprClient

# ...

def main():
    with DaprClient() as d:
        host = settings.DAPR_RUNTIME_HOST
        port = settings.DAPR_GRPC_PORT
        workflowRuntime = WorkflowRuntime(host, port)
        workflowRuntime = WorkflowRuntime()
        workflowRuntime.register_workflow(hello_world_wf)
        workflowRuntime.register_activity(hello_act)
        workflowRuntime.start()

        # Start workflow
        print("==========Start Counter Increase as per Input:==========")
        start_resp = d.start_workflow(instance_id=instanceId, workflow_component=workflowComponent,
                        workflow_name=workflowName, input=inputData, workflow_options=workflowOptions)
        print(f"start_resp {start_resp.instance_id}")

        # ...

        # Pause workflow
        d.pause_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        print(f"Get response from {workflowName} after pause call: {getResponse.runtime_status}")

        # Resume workflow
        d.resume_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        print(f"Get response from {workflowName} after resume call: {getResponse.runtime_status}")
        
        sleep(1)
        # Raise workflow
        d.raise_workflow_event(instance_id=instanceId, workflow_component=workflowComponent,
                    event_name=eventName, event_data=eventData)

        sleep(5)
        # Purge workflow
        d.purge_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        try:
            getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        except DaprInternalError as err:
            if nonExistentIDError in err._message:
                print("Instance Successfully Purged")

        # Kick off another workflow for termination purposes 
        start_resp = d.start_workflow(instance_id=instanceId, workflow_component=workflowComponent,
                        workflow_name=workflowName, input=inputData, workflow_options=workflowOptions)
        print(f"start_resp {start_resp.instance_id}")

        # Terminate workflow
        d.terminate_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        sleep(1)
        getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        print(f"Get response from {workflowName} after terminate call: {getResponse.runtime_status}")

        # Purge workflow
        d.purge_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        try:
            getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        except DaprInternalError as err:
            if nonExistentIDError in err._message:
                print("Instance Successfully Purged")

        workflowRuntime.shutdown()

if __name__ == '__main__':
    main()

以下示例是一个使用JavaScript SDK的基本JavaScript应用程序。在此示例中,您的项目代码将包括:

  • 一个带有扩展的构建器,称为:
  • API调用。在下面的示例中,这些调用启动、终止、获取状态、暂停、恢复、引发事件和清除工作流。
import { TaskHubGrpcClient } from "@microsoft/durabletask-js";
import { WorkflowState } from "./WorkflowState";
import { generateApiTokenClientInterceptors, generateEndpoint, getDaprApiToken } from "../internal/index";
import { TWorkflow } from "../../types/workflow/Workflow.type";
import { getFunctionName } from "../internal";
import { WorkflowClientOptions } from "../../types/workflow/WorkflowClientOption";

/** DaprWorkflowClient类定义了管理工作流实例的客户端操作。 */

export default class DaprWorkflowClient {
  private readonly _innerClient: TaskHubGrpcClient;

  /** 初始化DaprWorkflowClient的新实例。
   */
  constructor(options: Partial<WorkflowClientOptions> = {}) {
    const grpcEndpoint = generateEndpoint(options);
    options.daprApiToken = getDaprApiToken(options);
    this._innerClient = this.buildInnerClient(grpcEndpoint.endpoint, options);
  }

  private buildInnerClient(hostAddress: string, options: Partial<WorkflowClientOptions>): TaskHubGrpcClient {
    let innerOptions = options?.grpcOptions;
    if (options.daprApiToken !== undefined && options.daprApiToken !== "") {
      innerOptions = {
        ...innerOptions,
        interceptors: [generateApiTokenClientInterceptors(options), ...(innerOptions?.interceptors ?? [])],
      };
    }
    return new TaskHubGrpcClient(hostAddress, innerOptions);
  }

  /**
   * 使用DurableTask客户端调度新的工作流。
   */
  public async scheduleNewWorkflow(
    workflow: TWorkflow | string,
    input?: any,
    instanceId?: string,
    startAt?: Date,
  ): Promise<string> {
    if (typeof workflow === "string") {
      return await this._innerClient.scheduleNewOrchestration(workflow, input, instanceId, startAt);
    }
    return await this._innerClient.scheduleNewOrchestration(getFunctionName(workflow), input, instanceId, startAt);
  }

  /**
   * 终止与提供的实例ID关联的工作流。
   *
   * @param {string} workflowInstanceId - 要终止的工作流实例ID。
   * @param {any} output - 为终止的工作流实例设置的可选输出。
   */
  public async terminateWorkflow(workflowInstanceId: string, output: any) {
    await this._innerClient.terminateOrchestration(workflowInstanceId, output);
  }

  /**
   * 从配置的持久存储中获取工作流实例元数据。
   */
  public async getWorkflowState(
    workflowInstanceId: string,
    getInputsAndOutputs: boolean,
  ): Promise<WorkflowState | undefined> {
    const state = await this._innerClient.getOrchestrationState(workflowInstanceId, getInputsAndOutputs);
    if (state !== undefined) {
      return new WorkflowState(state);
    }
  }

  /**
   * 等待工作流开始运行
   */
  public async waitForWorkflowStart(
    workflowInstanceId: string,
    fetchPayloads = true,
    timeoutInSeconds = 60,
  ): Promise<WorkflowState | undefined> {
    const state = await this._innerClient.waitForOrchestrationStart(
      workflowInstanceId,
      fetchPayloads,
      timeoutInSeconds,
    );
    if (state !== undefined) {
      return new WorkflowState(state);
    }
  }

  /**
   * 等待工作流完成运行
   */
  public async waitForWorkflowCompletion(
    workflowInstanceId: string,
    fetchPayloads = true,
    timeoutInSeconds = 60,
  ): Promise<WorkflowState | undefined> {
    const state = await this._innerClient.waitForOrchestrationCompletion(
      workflowInstanceId,
      fetchPayloads,
      timeoutInSeconds,
    );
    if (state != undefined) {
      return new WorkflowState(state);
    }
  }

  /**
   * 向等待的工作流实例发送事件通知消息
   */
  public async raiseEvent(workflowInstanceId: string, eventName: string, eventPayload?: any) {
    this._innerClient.raiseOrchestrationEvent(workflowInstanceId, eventName, eventPayload);
  }

  /**
   * 从工作流状态存储中清除工作流实例状态。
   */
  public async purgeWorkflow(workflowInstanceId: string): Promise<boolean> {
    const purgeResult = await this._innerClient.purgeOrchestration(workflowInstanceId);
    if (purgeResult !== undefined) {
      return purgeResult.deletedInstanceCount > 0;
    }
    return false;
  }

  /**
   * 关闭内部DurableTask客户端并关闭GRPC通道。
   */
  public async stop() {
    await this._innerClient.stop();
  }
}

在以下Program.cs示例中,对于使用.NET SDK的基本ASP.NET订单处理应用程序,您的项目代码将包括:

  • 一个名为Dapr.Workflow的NuGet包,用于接收.NET SDK功能
  • 一个带有扩展方法的构建器,称为AddDaprWorkflow
    • 这将允许您注册工作流和工作流任务(工作流可以调度的任务)
  • HTTP API调用
    • 一个用于提交新订单
    • 一个用于检查现有订单的状态
using Dapr.Workflow;
//...

// Dapr工作流作为服务配置的一部分注册
builder.Services.AddDaprWorkflow(options =>
{
    // 请注意,也可以将lambda函数注册为工作流或任务实现,而不是类。
    options.RegisterWorkflow<OrderProcessingWorkflow>();

    // 这些是由工作流调用的任务。
    options.RegisterActivity<NotifyActivity>();
    options.RegisterActivity<ReserveInventoryActivity>();
    options.RegisterActivity<ProcessPaymentActivity>();
});

WebApplication app = builder.Build();

// POST启动新的订单工作流实例
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获取订单工作流的状态以报告状态
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工作流的hello-world应用程序将包括:

  • 一个名为io.dapr.workflows.client的Java包,用于接收Java SDK客户端功能。
  • 导入io.dapr.workflows.Workflow
  • 扩展WorkflowDemoWorkflow
  • 使用输入和输出创建工作流。
  • 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;

/**
 * DemoWorkflow的服务器端实现。
 */
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();
      // ...
    };
  }
}

查看上下文中的完整Java SDK工作流示例。

如以下示例所示,使用Go SDK和Dapr工作流的hello-world应用程序将包括:

  • 一个名为client的Go包,用于接收Go SDK客户端功能。
  • TestWorkflow方法
  • 使用输入和输出创建工作流。
  • API调用。在下面的示例中,这些调用启动并调用工作流任务。
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/dapr/go-sdk/client"
	"github.com/dapr/go-sdk/workflow"
)

var stage = 0

const (
	workflowComponent = "dapr"
)

func main() {
	w, err := workflow.NewWorker()
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Worker initialized")

	if err := w.RegisterWorkflow(TestWorkflow); err != nil {
		log.Fatal(err)
	}
	fmt.Println("TestWorkflow registered")

	if err := w.RegisterActivity(TestActivity); err != nil {
		log.Fatal(err)
	}
	fmt.Println("TestActivity registered")

	// Start workflow runner
	if err := w.Start(); err != nil {
		log.Fatal(err)
	}
	fmt.Println("runner started")

	daprClient, err := client.NewClient()
	if err != nil {
		log.Fatalf("failed to intialise client: %v", err)
	}
	defer daprClient.Close()
	ctx := context.Background()

	// Start workflow test
	respStart, err := daprClient.StartWorkflow(ctx, &client.StartWorkflowRequest{
		InstanceID:        "a7a4168d-3a1c-41da-8a4f-e7f6d9c718d9",
		WorkflowComponent: workflowComponent,
		WorkflowName:      "TestWorkflow",
		Options:           nil,
		Input:             1,
		SendRawInput:      false,
	})
	if err != nil {
		log.Fatalf("failed to start workflow: %v", err)
	}
	fmt.Printf("workflow started with id: %v\n", respStart.InstanceID)

	// Pause workflow test
	err = daprClient.PauseWorkflow(ctx, &client.PauseWorkflowRequest{
		InstanceID:        "a7a4168d-3a1c-41da-8a4f-e7f6d9c718d9",
		WorkflowComponent: workflowComponent,
	})

	if err != nil {
		log.Fatalf("failed to pause workflow: %v", err)
	}

	respGet, err := daprClient.GetWorkflow(ctx, &client.GetWorkflowRequest{
		InstanceID:        "a7a4168d-3a1c-41da-8a4f-e7f6d9c718d9",
		WorkflowComponent: workflowComponent,
	})
	if err != nil {
		log.Fatalf("failed to get workflow: %v", err)
	}

	if respGet.RuntimeStatus != workflow.StatusSuspended.String() {
		log.Fatalf("workflow not paused: %v", respGet.RuntimeStatus)
	}

	fmt.Printf("workflow paused\n")

	// Resume workflow test
	err = daprClient.ResumeWorkflow(ctx, &client.ResumeWorkflowRequest{
		InstanceID:        "a7a4168d-3a1c-41da-8a4f-e7f6d9c718d9",
		WorkflowComponent: workflowComponent,
	})

	if err != nil {
		log.Fatalf("failed to resume workflow: %v", err)
	}

	respGet, err = daprClient.GetWorkflow(ctx, &client.GetWorkflowRequest{
		InstanceID:        "a7a4168d-3a1c-41da-8a4f-e7f6d9c718d9",
		WorkflowComponent: workflowComponent,
	})
	if err != nil {
		log.Fatalf("failed to get workflow: %v", err)
	}

	if respGet.RuntimeStatus != workflow.StatusRunning.String() {
		log.Fatalf("workflow not running")
	}

	fmt.Println("workflow resumed")

	fmt.Printf("stage: %d\n", stage)

	// Raise Event Test

	err = daprClient.RaiseEventWorkflow(ctx, &client.RaiseEventWorkflowRequest{
		InstanceID:        "a7a4168d-3a1c-41da-8a4f-e7f6d9c718d9",
		WorkflowComponent: workflowComponent,
		EventName:         "testEvent",
		EventData:         "testData",
		SendRawData:       false,
	})

	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)

	respGet, err = daprClient.GetWorkflow(ctx, &client.GetWorkflowRequest{
		InstanceID:        "a7a4168d-3a1c-41da-8a4f-e7f6d9c718d9",
		WorkflowComponent: workflowComponent,
	})
	if err != nil {
		log.Fatalf("failed to get workflow: %v", err)
	}

	fmt.Printf("workflow status: %v\n", respGet.RuntimeStatus)

	// Purge workflow test
	err = daprClient.PurgeWorkflow(ctx, &client.PurgeWorkflowRequest{
		InstanceID:        "a7a4168d-3a1c-41da-8a4f-e7f6d9c718d9",
		WorkflowComponent: workflowComponent,
	})
	if err != nil {
		log.Fatalf("failed to purge workflow: %v", err)
	}

	respGet, err = daprClient.GetWorkflow(ctx, &client.GetWorkflowRequest{
		InstanceID:        "a7a4168d-3a1c-41da-8a4f-e7f6d9c718d9",
		WorkflowComponent: workflowComponent,
	})
	if err != nil && respGet != nil {
		log.Fatal("failed to purge workflow")
	}

	fmt.Println("workflow purged")

	fmt.Printf("stage: %d\n", stage)

	// Terminate workflow test
	respStart, err = daprClient.StartWorkflow(ctx, &client.StartWorkflowRequest{
		InstanceID:        "a7a4168d-3a1c-41da-8a4f-e7f6d9c718d9",
		WorkflowComponent: workflowComponent,
		WorkflowName:      "TestWorkflow",
		Options:           nil,
		Input:             1,
		SendRawInput:      false,
	})
	if err != nil {
		log.Fatalf("failed to start workflow: %v", err)
	}

	fmt.Printf("workflow started with id: %s\n", respStart.InstanceID)

	err = daprClient.TerminateWorkflow(ctx, &client.TerminateWorkflowRequest{
		InstanceID:        "a7a4168d-3a1c-41da-8a4f-e7f6d9c718d9",
		WorkflowComponent: workflowComponent,
	})
	if err != nil {
		log.Fatalf("failed to terminate workflow: %v", err)
	}

	respGet, err = daprClient.GetWorkflow(ctx, &client.GetWorkflowRequest{
		InstanceID:        "a7a4168d-3a1c-41da-8a4f-e7f6d9c718d9",
		WorkflowComponent: workflowComponent,
	})
	if err != nil {
		log.Fatalf("failed to get workflow: %v", err)
	}
	if respGet.RuntimeStatus != workflow.StatusTerminated.String() {
		log.Fatal("failed to terminate workflow")
	}

	fmt.Println("workflow terminated")

	err = daprClient.PurgeWorkflow(ctx, &client.PurgeWorkflowRequest{
		InstanceID:        "a7a4168d-3a1c-41da-8a4f-e7f6d9c718d9",
		WorkflowComponent: workflowComponent,
	})

	respGet, err = daprClient.GetWorkflow(ctx, &client.GetWorkflowRequest{
		InstanceID:        "a7a4168d-3a1c-41da-8a4f-e7f6d9c718d9",
		WorkflowComponent: workflowComponent,
	})
	if err == nil || respGet != nil {
		log.Fatalf("failed to purge workflow: %v", err)
	}

	fmt.Println("workflow purged")

	stage = 0
	fmt.Println("workflow client test")

	wfClient, err := workflow.NewClient()
	if err != nil {
		log.Fatalf("[wfclient] faield to initialize: %v", err)
	}

	id, err := wfClient.ScheduleNewWorkflow(ctx, "TestWorkflow", workflow.WithInstanceID("a7a4168d-3a1c-41da-8a4f-e7f6d9c718d9"), workflow.WithInput(1))
	if err != nil {
		log.Fatalf("[wfclient] failed to start workflow: %v", err)
	}

	fmt.Printf("[wfclient] started workflow with id: %s\n", id)

	metadata, err := wfClient.FetchWorkflowMetadata(ctx, id)
	if err != nil {
		log.Fatalf("[wfclient] failed to get worfklow: %v", err)
	}

	fmt.Printf("[wfclient] workflow status: %v\n", metadata.RuntimeStatus.String())

	if stage != 1 {
		log.Fatalf("Workflow assertion failed while validating the wfclient. Stage 1 expected, current: %d", stage)
	}

	fmt.Printf("[wfclient] stage: %d\n", stage)

	// raise event

	if err := wfClient.RaiseEvent(ctx, id, "testEvent", workflow.WithEventPayload("testData")); err != nil {
		log.Fatalf("[wfclient] failed to raise event: %v", err)
	}

	fmt.Println("[wfclient] event raised")

	// Sleep to allow the workflow to advance
	time.Sleep(time.Second)

	if stage != 2 {
		log.Fatalf("Workflow assertion failed while validating the wfclient. Stage 2 expected, current: %d", stage)
	}

	fmt.Printf("[wfclient] stage: %d\n", stage)

	// stop workflow
	if err := wfClient.TerminateWorkflow(ctx, id); err != nil {
		log.Fatalf("[wfclient] failed to terminate workflow: %v", err)
	}

	fmt.Println("[wfclient] workflow terminated")

	if err := wfClient.PurgeWorkflow(ctx, id); err != nil {
		log.Fatalf("[wfclient] failed to purge workflow: %v", err)
	}

	fmt.Println("[wfclient] workflow purged")

	// stop workflow runtime
	if err := w.Shutdown(); err != nil {
		log.Fatalf("failed to shutdown runtime: %v", err)
	}

	fmt.Println("workflow worker successfully shutdown")
}

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
	}

	err := ctx.WaitForExternalEvent("testEvent", time.Second*60).Await(&output)
	if err != nil {
		return nil, err
	}

	if err := ctx.CallActivity(TestActivity, workflow.ActivityInput(input)).Await(&output); err != nil {
		return nil, err
	}

	return output, nil
}

func TestActivity(ctx workflow.ActivityContext) (any, error) {
	var input int
	if err := ctx.GetInput(&input); err != nil {
		return "", err
	}

	stage += input

	return fmt.Sprintf("Stage: %d", stage), nil
}

查看上下文中的完整Go SDK工作流示例。

下一步

现在您已经编写了一个工作流,学习如何管理它。

管理工作流 >>

相关链接

1.3.6 - 如何:管理工作流

管理和运行工作流

现在您已经在应用程序中编写了工作流及其活动,您可以使用HTTP API调用来启动、终止和获取工作流的信息。有关更多信息,请阅读工作流API参考

在代码中管理您的工作流。在编写工作流指南中的工作流示例中,工作流通过以下API在代码中注册:

  • start_workflow: 启动工作流的一个实例
  • get_workflow: 获取工作流状态的信息
  • pause_workflow: 暂停或挂起一个工作流实例,稍后可以恢复
  • resume_workflow: 恢复一个暂停的工作流实例
  • raise_workflow_event: 在工作流上触发一个事件
  • purge_workflow: 删除与特定工作流实例相关的所有元数据
  • terminate_workflow: 终止或停止特定的工作流实例
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"

# 启动工作流
start_resp = d.start_workflow(instance_id=instanceId, workflow_component=workflowComponent,
                        workflow_name=workflowName, input=inputData, workflow_options=workflowOptions)

# 获取工作流信息
getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)

# 暂停工作流
d.pause_workflow(instance_id=instanceId, workflow_component=workflowComponent)

# 恢复工作流
d.resume_workflow(instance_id=instanceId, workflow_component=workflowComponent)

# 在工作流上触发一个事件
d.raise_workflow_event(instance_id=instanceId, workflow_component=workflowComponent,
                    event_name=eventName, event_data=eventData)

# 清除工作流
d.purge_workflow(instance_id=instanceId, workflow_component=workflowComponent)

# 终止工作流
d.terminate_workflow(instance_id=instanceId, workflow_component=workflowComponent)

在代码中管理您的工作流。在编写工作流指南中的工作流示例中,工作流通过以下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.workflowName}, 创建于 ${workflow.createdAt.toUTCString()}, 状态为 ${
      workflow.runtimeStatus
    }`,
  );
  console.log(`附加属性: ${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(`已启动工作流实例 ${instanceId}`);
  await printWorkflowStatus(client, instanceId);

  // 暂停一个工作流实例
  await client.workflow.pause(instanceId);
  console.log(`已暂停工作流实例 ${instanceId}`);
  await printWorkflowStatus(client, instanceId);

  // 恢复一个工作流实例
  await client.workflow.resume(instanceId);
  console.log(`已恢复工作流实例 ${instanceId}`);
  await printWorkflowStatus(client, instanceId);

  // 终止一个工作流实例
  await client.workflow.terminate(instanceId);
  console.log(`已终止工作流实例 ${instanceId}`);
  await printWorkflowStatus(client, instanceId);

  // 等待工作流完成,30秒!
  await new Promise((resolve) => setTimeout(resolve, 30000));
  await printWorkflowStatus(client, instanceId);

  // 清除一个工作流实例
  await client.workflow.purge(instanceId);
  console.log(`已清除工作流实例 ${instanceId}`);
  // 这将抛出一个错误,因为工作流实例不再存在。
  await printWorkflowStatus(client, instanceId);
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

在代码中管理您的工作流。在编写工作流指南中的OrderProcessingWorkflow示例中,工作流在代码中注册。您现在可以启动、终止并获取正在运行的工作流的信息:

string orderId = "exampleOrderId";
string workflowComponent = "dapr";
string workflowName = "OrderProcessingWorkflow";
OrderPayload input = new OrderPayload("Paperclips", 99.95);
Dictionary<string, string> workflowOptions; // 这是一个可选参数

// 启动工作流。这将返回一个"StartWorkflowResponse",其中包含特定工作流实例的实例ID。
StartWorkflowResponse startResponse = await daprClient.StartWorkflowAsync(orderId, workflowComponent, workflowName, input, workflowOptions);

// 获取工作流的信息。此响应包含工作流的状态、启动时间等信息!
GetWorkflowResponse getResponse = await daprClient.GetWorkflowAsync(orderId, workflowComponent, eventName);

// 终止工作流
await daprClient.TerminateWorkflowAsync(orderId, workflowComponent);

// 触发一个事件(一个传入的采购订单),您的工作流将等待此事件。这将返回等待购买的项目。
await daprClient.RaiseWorkflowEventAsync(orderId, workflowComponent, workflowName, input);

// 暂停
await daprClient.PauseWorkflowAsync(orderId, workflowComponent);

// 恢复
await daprClient.ResumeWorkflowAsync(orderId, workflowComponent);

// 清除工作流,删除与关联实例的所有收件箱和历史信息
await daprClient.PurgeWorkflowAsync(orderId, workflowComponent);

在代码中管理您的工作流。在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启动您的工作流,请运行:

POST http://localhost:3500/v1.0/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678

请注意,工作流实例ID只能包含字母数字字符、下划线和破折号。

终止工作流

要使用ID 12345678终止您的工作流,请运行:

POST http://localhost:3500/v1.0/workflows/dapr/12345678/terminate

触发一个事件

对于支持订阅外部事件的工作流组件,例如Dapr工作流引擎,您可以使用以下“触发事件”API将命名事件传递给特定的工作流实例。

POST http://localhost:3500/v1.0/workflows/<workflowComponentName>/<instanceID>/raiseEvent/<eventName>

eventName可以是任何自定义的事件名称。

暂停或恢复工作流

为了计划停机时间、等待输入等,您可以暂停然后恢复工作流。要暂停ID为12345678的工作流,直到触发恢复,请运行:

POST http://localhost:3500/v1.0/workflows/dapr/12345678/pause

要恢复ID为12345678的工作流,请运行:

POST http://localhost:3500/v1.0/workflows/dapr/12345678/resume

清除工作流

清除API可用于从底层状态存储中永久删除工作流元数据,包括任何存储的输入、输出和工作流历史记录。这通常对于实施数据保留策略和释放资源很有用。

只有处于COMPLETED、FAILED或TERMINATED状态的工作流实例可以被清除。如果工作流处于其他状态,调用清除将返回错误。

POST http://localhost:3500/v1.0/workflows/dapr/12345678/purge

获取工作流信息

要获取ID为12345678的工作流信息(输出和输入),请运行:

GET http://localhost:3500/v1.0/workflows/dapr/12345678

工作流API参考指南中了解更多关于这些HTTP调用的信息。

下一步

1.4 - 状态管理

创建持久运行的有状态服务

1.4.1 - 状态管理概述

状态管理API模块概述

您的应用程序可以利用Dapr的状态管理API在支持的状态存储中保存、读取和查询键/值对。通过状态存储组件,您可以构建有状态且长时间运行的应用程序,例如购物车或游戏的会话状态。如下图所示:

  • 使用HTTP POST来保存或查询键/值对。
  • 使用HTTP GET来读取特定键并返回其值。

以下视频和演示概述了Dapr状态管理的工作原理。

功能

通过状态管理API模块,您的应用程序可以利用一些通常复杂且容易出错的功能,包括:

  • 设置并发控制和数据一致性的选项。
  • 执行批量更新操作CRUD,包括多个事务操作。
  • 查询和过滤键/值数据。

以下是状态管理API的一些功能:

可插拔的状态存储

Dapr的数据存储被设计为组件,可以在不更改服务代码的情况下进行替换。查看支持的状态存储以获取更多信息。

可配置的状态存储行为

使用Dapr,您可以在状态操作请求中附加元数据,描述您期望请求如何被处理。您可以附加:

  • 并发性要求
  • 一致性要求

默认情况下,您的应用程序应假设数据存储是最终一致的,并使用最后写入胜出并发模式

并非所有存储都是平等的。为了确保您的应用程序的可移植性,您可以查询存储的元数据能力,并使您的代码适应不同的存储能力。

并发

Dapr支持使用ETags的乐观并发控制(OCC)。当请求状态值时,Dapr总是将ETag属性附加到返回的状态中。当用户代码:

  • 更新状态时,期望通过请求体附加ETag。
  • 删除状态时,期望通过If-Match头附加ETag。

当提供的ETag与状态存储中的ETag匹配时,写入操作成功。

为什么Dapr选择乐观并发控制(OCC)

在许多应用程序中,数据更新冲突很少见,因为客户端通常根据业务上下文分区以操作不同的数据。然而,如果您的应用程序选择使用ETags,不匹配的ETags可能导致请求被拒绝。建议您在代码中使用重试策略,以在使用ETags时补偿冲突。

如果您的应用程序在写入请求中省略ETags,Dapr在处理请求时会跳过ETag检查。这使得最后写入胜出模式成为可能,与使用ETags的首次写入胜出模式相比。

阅读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只能使用一个状态存储组件作为状态存储。阅读state API参考actors API参考以了解有关actor状态存储的更多信息。

actor状态的生存时间(TTL)

在保存actor状态时,您应始终设置TTL元数据字段(ttlInSeconds)或在您选择的SDK中使用等效的API调用,以确保状态最终被移除。阅读actors概述以获取更多信息。

状态加密

Dapr支持应用程序状态的自动客户端加密,并支持密钥轮换。这在所有Dapr状态存储上都支持。有关更多信息,请阅读如何:加密应用程序状态主题。

应用程序之间的共享状态

不同应用程序在共享状态时的需求各不相同。在一种情况下,您可能希望将所有状态封装在给定应用程序中,并让Dapr为您管理访问。在另一种情况下,您可能希望两个应用程序在同一状态上工作,以获取和保存相同的键。

Dapr使状态能够:

  • 隔离到一个应用程序。
  • 在应用程序之间的状态存储中共享。
  • 在不同状态存储之间的多个应用程序之间共享。

有关更多详细信息,请阅读如何:在应用程序之间共享状态

启用外发模式

Dapr使开发人员能够使用外发模式在事务性状态存储和任何消息代理之间实现单一事务。有关更多信息,请阅读如何启用事务性外发消息

查询状态

有两种方法可以查询状态:

  • 使用Dapr运行时提供的状态管理查询API。
  • 使用存储的原生SDK直接查询状态存储。

查询API

使用_可选的_状态管理查询API,您可以查询状态存储中保存的键/值数据,无论底层数据库或存储技术如何。使用状态管理查询API,您可以过滤、排序和分页键/值数据。有关更多详细信息,请阅读如何:查询状态

直接查询状态存储

Dapr在不进行任何转换的情况下保存和检索状态值。您可以直接从底层状态存储查询和聚合状态。例如,要获取与应用程序ID “myApp” 相关的所有状态键,请在Redis中使用:

KEYS "myApp*"
查询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。

下一步

1.4.2 - 操作指南:保存和获取状态

使用键值对持久化状态

状态管理是新应用程序、遗留应用程序、单体应用程序或微服务应用程序的常见需求之一。处理和测试不同的数据库库,以及处理重试和故障,可能既困难又耗时。

在本指南中,您将学习如何使用键/值状态API来保存、获取和删除应用程序的状态。

下面的代码示例描述了一个处理订单的应用程序,该应用程序使用Dapr sidecar。订单处理服务通过Dapr将状态存储在Redis状态存储中。

示例服务的状态管理图示

设置状态存储

状态存储组件是Dapr用于与数据库通信的资源。

在本指南中,我们将使用Redis状态存储,但您也可以选择支持列表中的其他状态存储。

当您在selfhost模式下运行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: ""

请参阅如何在Kubernetes上设置不同的状态存储以获取更多信息。

保存和检索单个状态

以下示例展示了如何使用Dapr状态管理API保存和检索单个键/值对。


// 依赖项
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Dapr.Client;
using Microsoft.AspNetCore.Mvc;
using System.Threading;
using System.Text.Json;

// 代码
namespace EventService
{
    class Program
    {
        static async Task Main(string[] args)
        {
            string DAPR_STORE_NAME = "statestore";
            while(true) {
                System.Threading.Thread.Sleep(5000);
                using var client = new DaprClientBuilder().Build();
                Random random = new Random();
                int 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);
            }
        }
    }
}

要为上述示例应用程序启动一个Dapr sidecar,运行类似以下的命令:

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);
		}
	}

}

要为上述示例应用程序启动一个Dapr sidecar,运行类似以下的命令:

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.data.decode('utf-8'))

要为上述示例应用程序启动一个Dapr sidecar,运行类似以下的命令:

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("获取后的结果:", string(result.Value))
		time.Sleep(2 * time.Second)
	}
}

要为上述示例应用程序启动一个Dapr sidecar,运行类似以下的命令:

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);
}

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

main();

要为上述示例应用程序启动一个Dapr sidecar,运行类似以下的命令:

dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 npm start

启动一个Dapr sidecar:

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

重新启动您的sidecar并尝试再次检索状态,以观察状态与应用程序分开持久化。

启动一个Dapr sidecar:

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'

重新启动您的sidecar并尝试再次检索状态,以观察状态与应用程序分开持久化。

删除状态

以下是利用Dapr SDK删除状态的代码示例。

// 依赖项
using Dapr.Client;

// 代码
namespace EventService
{
    class Program
    {
        static async Task Main(string[] args)
        {
            string DAPR_STORE_NAME = "statestore";
            // 使用Dapr SDK删除状态
            using var client = new DaprClientBuilder().Build();
            await client.DeleteStateAsync(DAPR_STORE_NAME, "order_1", cancellationToken: cancellationToken);
        }
    }
}

要为上述示例应用程序启动一个Dapr sidecar,运行类似以下的命令:

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 sidecar,运行类似以下的命令:

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 sidecar,运行类似以下的命令:

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 sidecar,运行类似以下的命令:

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 sidecar,运行类似以下的命令:

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;
// 代码
namespace EventService
{
    class Program
    {
        static async Task Main(string[] args)
        {
            string DAPR_STORE_NAME = "statestore";
            // 使用Dapr SDK检索多个状态
            using var client = new DaprClientBuilder().Build();
            IReadOnlyList<BulkStateItem> multipleStateResult = await client.GetBulkStateAsync(DAPR_STORE_NAME, new List<string> { "order_1", "order_2" }, parallelism: 1);
        }
    }
}

要为上述示例应用程序启动一个Dapr sidecar,运行类似以下的命令:

dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 dotnet run

上述示例返回一个BulkStateItem,其中包含您保存到状态的值的序列化格式。如果您希望SDK在每个批量响应项中反序列化值,您可以使用以下代码:

// 依赖项
using Dapr.Client;
// 代码
namespace EventService
{
    class Program
    {
        static async Task Main(string[] args)
        {
            string DAPR_STORE_NAME = "statestore";
            // 使用Dapr SDK检索多个状态
            using var client = new DaprClientBuilder().Build();
            IReadOnlyList<BulkStateItem<Widget>> mulitpleStateResult = await client.GetBulkStateAsync<Widget>(DAPR_STORE_NAME, new List<string> { "widget_1", "widget_2" }, parallelism: 1);
        }
    }

    class Widget
    {
        string Size { get; set; }
        string Color { get; set; }        
    }
}
// 依赖项
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 sidecar,运行类似以下的命令:

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('批量获取后的结果: ' + str(result)) 

要为上述示例应用程序启动一个Dapr sidecar,运行类似以下的命令:

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("从GetBulkState获取的项:", string(item.Value))
		}
	}
} 

要为上述示例应用程序启动一个Dapr sidecar,运行类似以下的命令:

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 sidecar,运行类似以下的命令:

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 System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Dapr.Client;
using Microsoft.AspNetCore.Mvc;
using System.Threading;
using System.Text.Json;

// 代码
namespace EventService
{
    class Program
    {
        static async Task Main(string[] args)
        {
            string DAPR_STORE_NAME = "statestore";
            while(true) {
                System.Threading.Thread.Sleep(5000);
                Random random = new Random();
                int orderId = random.Next(1,1000);
                using var client = new DaprClientBuilder().Build();
                var requests = new List<StateTransactionRequest>()
                {
                    new StateTransactionRequest("order_3", JsonSerializer.SerializeToUtf8Bytes(orderId.ToString()), StateOperationType.Upsert),
                    new StateTransactionRequest("order_2", null, StateOperationType.Delete)
                };
                CancellationTokenSource source = new CancellationTokenSource();
                CancellationToken cancellationToken = source.Token;
                // 使用Dapr SDK执行状态事务
                await client.ExecuteStateTransactionAsync(DAPR_STORE_NAME, requests, cancellationToken: cancellationToken);
                Console.WriteLine("订单请求: " + orderId);
                Console.WriteLine("结果: " + result);
            }
        }
    }
}

要为上述示例应用程序启动一个Dapr sidecar,运行类似以下的命令:

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("订单请求: " + orderId);
		}
	}

}

要为上述示例应用程序启动一个Dapr sidecar,运行类似以下的命令:

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('订单请求: ' + str(orderId))
    logging.info('结果: ' + str(result))

要为上述示例应用程序启动一个Dapr sidecar,运行类似以下的命令:

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("获取后的结果:", string(result.Value))
		time.Sleep(2 * time.Second)
	}
}

要为上述示例应用程序启动一个Dapr sidecar,运行类似以下的命令:

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 sidecar,运行类似以下的命令:

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 - 操作指南:查询状态

使用查询API查询状态存储

通过状态查询API,您可以从状态存储组件中检索、过滤和排序键/值数据。查询API并不是完整查询语言的替代品。

尽管状态存储是键/值存储,value可能是一个包含自身层次结构、键和值的JSON文档。查询API允许您使用这些键/值来检索相应的文档。

查询状态

您可以通过HTTP POST/PUT或gRPC提交查询请求。请求的主体是一个包含以下三个部分的JSON对象:

  • filter
  • sort
  • page

filter

filter用于指定查询条件,结构类似于树形,每个节点表示一个操作,可能是单一或多个操作数。

支持以下操作:

操作符操作数描述
EQkey:valuekey 等于 value
NEQkey:valuekey 不等于 value
GTkey:valuekey 大于 value
GTEkey:valuekey 大于等于 value
LTkey:valuekey 小于 value
LTEkey:valuekey 小于等于 value
INkey:[]valuekey 等于 value[0] 或 value[1] 或 … 或 value[n]
AND[]operationoperation[0] 且 operation[1] 且 … 且 operation[n]
OR[]operationoperation[0] 或 operation[1] 或 … 或 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"表示降序
      如果省略order,默认是升序。

page

page包含limittoken参数。

  • limit设置每页返回的记录数。
  • token是组件返回的分页令牌,用于获取后续查询的结果。

在后台,此查询请求被转换为本地查询语言并由状态存储组件执行。

示例数据和查询

让我们来看一些从简单到复杂的真实示例。

作为数据集,考虑一个包含员工ID、组织、州和城市的员工记录集合。注意,这个数据集是一个键/值对数组,其中:

  • key是唯一ID
  • value是包含员工记录的JSON对象。

为了更好地说明功能,组织名称(org)和员工ID(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运行时完成并存储为加密数据,因此这实际上阻止了服务器端查询。

您可以在相关链接部分找到更多信息。

相关链接

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

查看如何在Kubernetes上设置不同的状态存储

强一致性和最终一致性

在强一致性模式下,Dapr确保底层状态存储:

  • 在数据写入所有副本后才返回响应。
  • 在写入或删除状态之前从法定人数接收确认。

对于读取请求,Dapr确保在副本之间一致地返回最新的数据。默认情况下是最终一致性,除非在请求状态API时另有指定。

以下示例展示了如何使用强一致性保存、获取和删除状态。示例用Python编写,但适用于任何编程语言。

保存状态

import requests
import json

store_name = "redis-store" # 在状态存储组件yaml文件中指定的状态存储名称
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" # 在状态存储组件yaml文件中指定的状态存储名称
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" # 在状态存储组件yaml文件中指定的状态存储名称
dapr_state_url = "http://localhost:3500/v1.0/state/{}".format(store_name)
response = requests.delete(dapr_state_url + "/key1", headers={"consistency":"strong"})

如果没有指定concurrency选项,默认是后写胜出并发模式。

先写胜出和后写胜出

Dapr允许开发者在使用数据存储时选择两种常见的并发模式:

  • 先写胜出:在应用程序的多个实例同时写入同一个键的情况下很有用。
  • 后写胜出:Dapr的默认模式。

Dapr使用版本号来确定特定键是否已更新。您可以:

  1. 在读取键的数据时保留版本号。
  2. 在更新(如写入和删除)时使用版本号。

如果自从检索版本号以来版本信息已更改,将抛出错误,要求您执行另一次读取以获取最新的版本信息和状态。

Dapr利用ETags来确定状态的版本号。ETags从状态请求中以ETag头返回。使用ETags,您的应用程序知道自上次检查以来资源已更新,因为在ETag不匹配时会出错。

以下示例展示了如何:

  • 获取ETag。
  • 使用ETag保存状态。
  • 删除状态。

以下示例用Python编写,但适用于任何编程语言。

import requests
import json

store_name = "redis-store" # 在状态存储组件yaml文件中指定的状态存储名称
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

# 此方法保存状态,如果保存状态失败则返回false
def save_state(data):
    try:
        store_name = "redis-store" # 在状态存储组件yaml文件中指定的状态存储名称
        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

# 此方法获取状态并返回响应,ETag在头中 -->
def get_state(key):
    response = requests.get("http://localhost:3500/v1.0/state/<state_store_name>/{}".format(key), headers={"concurrency":"first-write"})
    return response

# 当保存状态成功时退出。如果存在ETag不匹配,success将为False -->
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 - 操作指南:启用事务性 Outbox 模式

在状态存储和发布/订阅消息代理之间提交单个事务

事务性 Outbox 模式是一种广为人知的设计模式,用于发送应用程序状态变化的通知。它通过一个跨越数据库和消息代理的单一事务来传递通知。

开发人员在尝试自行实现此模式时会遇到许多技术难题,通常需要编写复杂且容易出错的中央协调管理器,这些管理器最多支持一种或两种数据库和消息代理的组合。

例如,您可以使用 Outbox 模式来:

  1. 向账户数据库写入新的用户记录。
  2. 发送账户成功创建的通知消息。

通过 Dapr 的 Outbox 支持,您可以在调用 Dapr 的事务 API时通知订阅者应用程序的状态何时被创建或更新。

下图概述了 Outbox 功能的工作原理:

  1. 服务 A 使用事务将状态保存/更新到状态存储。
  2. 在同一事务下将消息写入消息代理。当消息成功传递到消息代理时,事务完成,确保状态和消息一起被事务化。
  3. 消息代理将消息主题传递给任何订阅者 - 在此情况下为服务 B。
显示 Outbox 模式步骤的图示

要求

Outbox 功能可以与 Dapr 支持的任何事务性状态存储一起使用。所有发布/订阅代理都支持 Outbox 功能。

了解更多关于您可以使用的事务方法。

启用 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

元数据字段

名称必需默认值描述
outboxPublishPubsubN/A设置发布状态更改时传递通知的发布/订阅组件的名称
outboxPublishTopicN/A设置接收在配置了 outboxPublishPubsub 的发布/订阅上的状态更改的主题。消息体将是 insertupdate 操作的状态事务项
outboxPubsuboutboxPublishPubsub设置 Dapr 用于协调状态和发布/订阅事务的发布/订阅组件。如果未设置,则使用配置了 outboxPublishPubsub 的发布/订阅组件。如果您希望将用于发送通知状态更改的发布/订阅组件与用于协调事务的组件分开,这将很有用
outboxDiscardWhenMissingStatefalse通过将 outboxDiscardWhenMissingState 设置为 true,如果 Dapr 无法在数据库中找到状态且不重试,则 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 模式消息。此事务添加了一个名为 outbox.projection 的元数据键,值设置为 true。当添加到事务中保存的状态数组时,此负载在写入状态时被忽略,数据用作发送到上游订阅者的负载。

要正确使用,key 值必须在状态存储上的操作和消息投影之间匹配。如果键不匹配,则整个事务失败。

如果您为同一键启用了两个或多个 outbox.projection 状态项,则使用第一个定义的项,其他项将被忽略。

了解更多关于默认和自定义 CloudEvent 消息。

在以下 Python SDK 的状态事务示例中,值 "2" 被保存到数据库,但值 "3" 被发布到最终用户主题。

DAPR_STORE_NAME = "statestore"

async def main():
    client = DaprClient()

    # 定义第一个状态操作以保存值 "2"
    op1 = StateItem(
        key="key1",
        value=b"2"
    )

    # 定义第二个状态操作以带有元数据发布值 "3"
    op2 = StateItem(
        key="key1",
        value=b"3",
        options=StateOptions(
            metadata={
                "outbox.projection": "true"
            }
        )
    )

    # 创建状态操作列表
    ops = [op1, op2]

    # 执行状态事务
    await client.state.transaction(DAPR_STORE_NAME, operations=ops)
    print("状态事务已执行。")

通过将元数据项 "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("状态事务已执行。");
}

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("状态事务已执行。");
    }
}

通过将元数据项 "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"
            StateOperation<String> op1 = new StateOperation<>(
                    StateOperationType.UPSERT,
                    "key1",
                    "2"
            );

            // 定义第二个状态操作以带有元数据发布值 "3"
            Map<String, String> metadata = new HashMap<>();
            metadata.put("outbox.projection", "true");

            StateOperation<String> op2 = new StateOperation<>(
                    StateOperationType.UPSERT,
                    "key1",
                    "3",
                    metadata
            );

            // 创建状态操作列表
            List<StateOperation<?>> ops = new ArrayList<>();
            ops.add(op1);
            ops.add(op2);

            // 执行状态事务
            client.executeStateTransaction(DAPR_STORE_NAME, ops).block();
            System.out.println("状态事务已执行。");
        } 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('状态事务已执行。')
        except Exception as e:
            print('执行状态事务时出错:', 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("状态事务已执行。");
    }

    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()) {

            // 定义值 "2"
            String value = "2";

            // 覆盖 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");

            // 定义状态操作
            List<StateOperation<?>> ops = new ArrayList<>();
            StateOperation<String> op1 = new StateOperation<>(
                    StateOperationType.UPSERT,
                    "key1",
                    value,
                    metadata
            );
            ops.add(op1);

            // 执行状态事务
            String storeName = "your-state-store-name";
            daprClient.executeStateTransaction(storeName, ops).block();
            System.out.println("状态事务已执行。");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
func main() {
	// 创建 Dapr 客户端
	client, err := dapr.NewClient()
	if err != nil {
		log.Fatalf("创建 Dapr 客户端失败: %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("执行状态事务失败: %v", err)
	}

	log.Println("状态事务已执行。")
}
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",
        }
      }'

演示

观看此视频以了解 Outbox 模式的概述

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 提供基于 AES 的自动客户端加密,采用 Galois/Counter Mode (GCM),支持 128、192 和 256 位的密钥。

除了自动加密,Dapr 还支持主加密密钥和次加密密钥,使开发人员和运维团队更容易启用密钥轮换策略。此功能由所有 Dapr 状态存储支持。

加密密钥始终从 secret 中获取,不能在 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 的 secret 中获取加密密钥,其中包含名为 mykey 的实际加密密钥。

实际的加密密钥必须是有效的、十六进制编码的加密密钥。虽然支持 192 位和 256 位密钥,但建议使用 128 位加密密钥。如果加密密钥无效,Dapr 会报错并退出。

例如,您可以使用以下命令生成一个随机的、十六进制编码的 128 位(16 字节)密钥:

openssl rand 16 | hexdump -v -e '/1 "%02x"'
# 结果将类似于 "cb321007ad11a9d23f963bff600d58e0"

注意,secret 存储不必支持密钥。

密钥轮换

为了支持密钥轮换,Dapr 提供了一种指定次加密密钥的方法:

metadata:
- name: primaryEncryptionKey
    secretKeyRef:
      name: mysecret
      key: mykey
- name: secondaryEncryptionKey
    secretKeyRef:
      name: mysecret2
      key: mykey2

当 Dapr 启动时,它会获取 metadata 部分中列出的包含加密密钥的 secrets。Dapr 自动识别哪个状态项是用哪个密钥加密的,因为它会将 secretKeyRef.name 字段附加到实际状态密钥的末尾。

要轮换密钥,

  1. 更改 primaryEncryptionKey 以指向包含新密钥的 secret。
  2. 将旧的主加密密钥移至 secondaryEncryptionKey

新数据将使用新密钥加密,任何检索到的旧数据将使用次密钥解密。

使用旧密钥加密的数据项的任何更新都将使用新密钥重新加密。

相关链接

1.4.8 - 与后端状态存储交互

指导如何与特定的后端状态存储进行交互

请查看操作部分,了解支持的状态存储列表,并学习如何配置状态存储组件

1.4.8.1 - Azure Cosmos DB

使用 Azure Cosmos DB 作为状态存储

Dapr 在保存和检索状态时不对状态值进行转换。Dapr 要求所有状态存储实现遵循特定的键格式规范(参见状态管理规范)。您可以直接与底层存储交互以操作状态数据,例如:

  • 查询状态。
  • 创建聚合视图。
  • 进行备份。

连接到 Azure Cosmos DB

要连接到您的 Cosmos DB 实例,您可以:

按应用程序 ID 列出键

要获取与应用程序 “myapp” 关联的所有状态键,请使用查询:

SELECT * FROM states WHERE CONTAINS(states.id, 'myapp||')

上述查询返回所有 id 包含 “myapp||” 的文档,这是状态键的前缀。

获取特定状态数据

要通过键 “balance” 获取应用程序 “myapp” 的状态数据,请使用查询:

SELECT * FROM states WHERE states.id = 'myapp||balance'

读取返回文档的 value 字段。要获取状态版本/ETag,请使用命令:

SELECT states._etag FROM states WHERE states.id = 'myapp||balance'

读取 actor 状态

要获取与实例 ID 为 “leroy” 的 actor 类型 “cat” 关联的所有状态键,该 actor 属于 ID 为 “mypets” 的应用程序,请使用命令:

SELECT * FROM states WHERE CONTAINS(states.id, 'mypets||cat||leroy||')

要获取特定的 actor 状态,例如 “food”,请使用命令:

SELECT * FROM states WHERE states.id = 'mypets||cat||leroy||food'

1.4.8.2 - Redis

使用 Redis 作为状态存储

Dapr 在保存和检索状态时不对状态值进行转换。Dapr 要求所有状态存储实现遵循特定的键格式规范(参见状态管理规范)。您可以直接与底层存储交互以操作状态数据,例如:

  • 查询状态。
  • 创建聚合视图。
  • 进行备份。

连接到 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,表示不断递增的版本。

例如,要通过键 “balance” 获取应用程序 “myapp” 的状态数据,请使用命令:

HGET myapp||balance data

要获取状态版本/ETag,请使用命令:

HGET myapp||balance version

读取 actor 状态

要获取与应用程序 ID 为 “mypets” 的 actor 类型 “cat” 的实例 ID 为 “leroy” 关联的所有状态键,请使用命令:

KEYS mypets||cat||leroy*

要获取特定的 actor 状态,例如 “food”,请使用命令:

HGET mypets||cat||leroy||food value

1.4.8.3 - SQL server

使用 SQL server 作为后端状态存储

Dapr 在保存和检索状态时不对状态值进行转换。Dapr 要求所有状态存储实现遵循特定的键格式(参见状态管理规范)。您可以直接与底层存储交互来操作状态数据,例如:

  • 查询状态。
  • 创建聚合视图。
  • 进行备份。

连接到 SQL Server

连接到 SQL Server 实例的最简单方法是使用:

按应用程序 ID 列出键

要获取与应用程序 “myapp” 关联的所有状态键,请使用以下查询:

SELECT * FROM states WHERE [Key] LIKE 'myapp||%'

上述查询返回所有 ID 包含 “myapp||” 的行,这是状态键的前缀。

获取特定状态数据

要通过键 “balance” 获取应用程序 “myapp” 的状态数据,请使用以下查询:

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 状态

要获取与 actor 类型 “cat” 的实例 ID “leroy” 关联的所有状态键,该 actor 属于 ID 为 “mypets” 的应用程序,请使用以下命令:

SELECT * FROM states WHERE [Key] LIKE 'mypets||cat||leroy||%'

要获取特定的 actor 状态,例如 “food”,请使用以下命令:

SELECT * FROM states WHERE [Key] = 'mypets||cat||leroy||food'

1.4.9 - 状态生存时间 (TTL)

管理具有 TTL 的状态。

Dapr 允许为每个状态设置生存时间 (TTL)。这意味着应用程序可以为存储的每个状态指定一个生存时间,过期后将无法检索这些状态。

对于支持的状态存储,只需在发布消息时设置 ttlInSeconds 元数据。其他状态存储将忽略此值。对于某些状态存储,您可以为每个表或容器指定默认的过期时间。

原生状态 TTL 支持

当状态存储组件原生支持状态 TTL 时,Dapr 会直接传递 TTL 配置,而不添加额外的逻辑,从而保持行为的一致性。这在组件以不同方式处理过期状态时尤为有用。

如果未指定 TTL,将保留状态存储的默认行为。

显式持久化绕过全局定义的 TTL

对于允许为所有数据指定默认 TTL 的状态存储,持久化状态的方式包括:

  • 通过 Dapr 组件设置全局 TTL 值,或
  • 在 Dapr 之外创建状态存储并设置全局 TTL 值。

如果未指定特定的 TTL,数据将在全局 TTL 时间段后过期,这不是由 Dapr 直接控制的。

此外,所有状态存储还支持显式持久化数据的选项。这意味着您可以忽略默认的数据库策略(可能是在 Dapr 之外或通过 Dapr 组件设置的),以无限期保留特定的数据库记录。您可以通过将 ttlInSeconds 设置为 -1 来实现,这表示忽略任何设置的 TTL 值。

支持的组件

请参阅状态存储组件指南中的 TTL 列。

示例

您可以在状态存储请求的元数据中设置状态 TTL:

# 依赖

from dapr.clients import DaprClient

# 代码

DAPR_STORE_NAME = "statestore"

with DaprClient() as client:
        client.save_state(DAPR_STORE_NAME, "order_1", str(orderId), state_metadata={
            'ttlInSeconds': '120'
        }) 

要启动 Dapr sidecar 并运行上述示例应用程序,您可以运行类似以下的命令:

dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 -- python3 OrderProcessingService.py
// 依赖

using Dapr.Client;

// 代码

await client.SaveStateAsync(storeName, stateKeyName, state, metadata: new Dictionary<string, string>() { 
    { 
        "ttlInSeconds", "120" 
    } 
});

要启动 Dapr sidecar 并运行上述示例应用程序,您可以运行类似以下的命令:

dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 dotnet run
// 依赖

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

// 代码

md := map[string]string{"ttlInSeconds": "120"}
if err := client.SaveState(ctx, store, "key1", []byte("hello world"), md); err != nil {
   panic(err)
}

要启动 Dapr sidecar 并运行上述示例应用程序,您可以运行类似以下的命令:

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'

相关链接

1.5 - Bindings

与外部系统交互或被外部系统触发

1.5.1 - bindings 概述

bindings API 模块的概述

通过 Dapr 的 bindings API,您可以利用外部系统的事件来触发应用程序,并与外部系统交互。使用 bindings API,您可以:

  • 避免连接到消息系统并进行轮询的复杂性(如队列和消息总线)。
  • 专注于业务逻辑,而不是系统交互的实现细节。
  • 使您的代码不依赖于特定的 SDK 或库。
  • 处理重试和故障恢复。
  • 在运行时可以切换不同的 bindings。
  • 构建具有特定环境 bindings 设置的可移植应用程序,而无需更改代码。

例如,通过 bindings,您的应用程序可以响应传入的 Twilio/SMS 消息,而无需:

  • 添加或配置第三方 Twilio SDK
  • 担心从 Twilio 轮询(或使用 WebSockets 等)
显示 bindings 的图示

在上图中:

  • 输入 binding 触发您应用程序上的一个方法。
  • 在组件上执行输出 binding 操作,例如 "create"

bindings 的开发独立于 Dapr 运行时。您可以查看并贡献 bindings

输入 bindings

通过输入 bindings,您可以在外部资源发生事件时触发您的应用程序。请求中可以发送可选的负载和元数据。

以下概述视频和演示展示了 Dapr 输入 binding 的工作原理。

要接收来自输入 binding 的事件:

  1. 定义描述 binding 类型及其元数据(如连接信息)的组件 YAML。
  2. 使用以下方式监听传入事件:
    • HTTP 端点
    • gRPC proto 库获取传入事件。

阅读使用输入 bindings 创建事件驱动应用程序指南以开始使用输入 bindings。

输出 bindings

通过输出 bindings,您可以调用外部资源。调用请求中可以发送可选的负载和元数据。

以下概述视频和演示展示了 Dapr 输出 binding 的工作原理。

要调用输出 binding:

  1. 定义描述 binding 类型及其元数据(如连接信息)的组件 YAML。
  2. 使用 HTTP 端点或 gRPC 方法调用 binding,并附带可选负载。
  3. 指定输出操作。输出操作取决于您使用的 binding 组件,可以包括:
    • "create"
    • "update"
    • "delete"
    • "exec"

阅读使用输出 bindings 与外部资源交互指南以开始使用输出 bindings。

binding 方向(可选)

您可以提供 direction 元数据字段以指示 binding 组件支持的方向。这可以使 Dapr sidecar 避免“等待应用程序准备就绪”状态,减少 Dapr sidecar 与应用程序之间的生命周期依赖:

  • "input"
  • "output"
  • "input, output"

查看 bindings direction 元数据的完整示例。

试用 bindings

快速入门和教程

想要测试 Dapr bindings API?通过以下快速入门和教程来查看 bindings 的实际应用:

快速入门/教程描述
bindings 快速入门使用输入 bindings 处理外部系统的事件,并使用输出 bindings 调用操作。
bindings 教程演示如何使用 Dapr 创建到其他组件的输入和输出 bindings。使用 bindings 连接到 Kafka。

直接在您的应用程序中开始使用 bindings

想要跳过快速入门?没问题。您可以直接在应用程序中试用 bindings 模块,以调用输出 bindings 和触发输入 bindings。在Dapr 安装完成后,您可以从输入 bindings 如何指南开始使用 bindings API。

下一步

1.5.2 - 操作指南:使用输入绑定触发应用程序

使用Dapr输入绑定触发事件驱动的应用程序

当外部资源发生事件时,您可以通过输入绑定来触发您的应用程序。外部资源可以是队列、消息管道、云服务、文件系统等。请求中可以发送可选的负载和元数据。

输入绑定非常适合用于事件驱动的处理、数据管道或一般的事件响应和后续处理。Dapr输入绑定允许您:

  • 在不需要特定SDK或库的情况下接收事件
  • 在不更改代码的情况下替换绑定
  • 专注于业务逻辑而不是事件资源的实现
示例服务的绑定图示

本指南使用Kafka绑定作为示例。您可以从绑定组件列表中找到您偏好的绑定规范。在本指南中:

  1. 示例调用/binding端点,使用checkout作为要调用的绑定名称。
  2. 负载需要放在data字段中,可以是任何可序列化为JSON的值。
  3. operation字段指定绑定需要执行的操作。例如,Kafka绑定支持create操作

创建绑定

创建一个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代理连接设置
  - name: brokers
    value: localhost:9092
  # 消费者配置:主题和消费者组
  - name: topics
    value: sample
  - name: consumerGroup
    value: group1
  # 发布者配置:主题
  - 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代理连接设置
  - name: brokers
    value: localhost:9092
  # 消费者配置:主题和消费者组
  - name: topics
    value: sample
  - name: consumerGroup
    value: group1
  # 发布者配置:主题
  - name: publishTopic
    value: sample
  - name: authRequired
    value: false
  - name: direction
    value: input

监听传入事件(输入绑定)

配置您的应用程序以接收传入事件。如果您使用HTTP,您需要:

  • 监听一个POST端点,其名称与binding.yaml文件中的metadata.name指定的绑定名称相同。
  • 确保您的应用程序允许Dapr对该端点进行OPTIONS请求。

以下是利用Dapr SDK展示输入绑定的代码示例。

//依赖项
using System.Collections.Generic;
using System.Threading.Tasks;
using System;
using Microsoft.AspNetCore.Mvc;

//代码
namespace CheckoutService.controller
{
    [ApiController]
    public class CheckoutServiceController : Controller
    {
        [HttpPost("/checkout")]
        public ActionResult<string> getCheckout([FromBody] int orderId)
        {
            Console.WriteLine("Received Message: " + orderId);
            return "CID" + orderId;
        }
    }
}
//依赖项
import org.springframework.web.bind.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;

//代码
@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)));
        }
}
#依赖项
import logging
from dapr.ext.grpc import App, BindingRequest

#代码
app = App()

@app.binding('checkout')
def getCheckout(request: BindingRequest):
    logging.basicConfig(level = logging.INFO)
    logging.info('Received Message : ' + request.text())

app.run(6002)
//依赖项
import (
	"encoding/json"
	"log"
	"net/http"
	"github.com/gorilla/mux"
)

//代码
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)
}
//依赖项 
import { DaprServer, CommunicationProtocolEnum } from '@dapr/dapr'; 

//代码
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中设置以下元数据属性来覆盖此设置:

name: mybinding
spec:
  type: binding.rabbitmq
  metadata:
  - name: route
    value: /onevent

事件投递保证

事件投递保证由绑定实现控制。根据绑定实现,事件投递可以是精确一次或至少一次。

参考资料

1.5.3 - 操作指南:使用输出绑定与外部资源交互

通过输出绑定调用外部系统

使用输出绑定,您可以与外部资源进行交互。在调用请求中,您可以发送可选的负载和元数据。

示例服务的绑定图示

本指南以Kafka绑定为例。您可以从绑定组件列表中选择您偏好的绑定规范。在本指南中:

  1. 示例中调用了/binding端点,使用checkout作为要调用的绑定名称。
  2. 负载放在必需的data字段中,可以是任何JSON可序列化的值。
  3. operation字段指定绑定需要执行的操作。例如,Kafka绑定支持create操作

创建绑定

创建一个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代理连接设置
  - 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代理连接设置
  - 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实例上调用输出绑定端点。

//依赖项
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Dapr.Client;
using Microsoft.AspNetCore.Mvc;
using System.Threading;

//代码
namespace EventService
{
    class Program
    {
        static async Task Main(string[] args)
        {
            string BINDING_NAME = "checkout";
            string BINDING_OPERATION = "create";
            while(true)
            {
                System.Threading.Thread.Sleep(5000);
                Random random = new Random();
                int orderId = random.Next(1,1000);
                using var client = new DaprClientBuilder().Build();
                //使用Dapr SDK调用输出绑定
                await client.InvokeBindingAsync(BINDING_NAME, BINDING_OPERATION, orderId);
                Console.WriteLine("发送消息: " + orderId);
            }
        }
    }
}
//依赖项
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;

//代码
@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("发送消息: " + orderId);
		}
	}
}
#依赖项
import random
from time import sleep    
import requests
import logging
import json
from dapr.clients import DaprClient

#代码
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('发送消息: ' + str(orderId))
    
//依赖项
import (
	"context"
	"log"
	"math/rand"
	"time"
	"strconv"
	dapr "github.com/dapr/go-sdk/client"

)

//代码
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("发送消息: " + strconv.Itoa(orderId))
	}
}
    
//依赖项
import { DaprClient, CommunicationProtocolEnum } from "@dapr/dapr";

//代码
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("发送消息: " + 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

将代码和数据封装在可重用的actor对象中,这是一种常见的微服务设计模式

1.6.1 - Actors 概述

Actors API 构建模块概述

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 Actors 与 Dapr Workflow

Dapr actors 基于状态管理和服务调用 API 创建具有身份的有状态、长时间运行的对象。Dapr Workflow 和 Dapr Actors 是相关的,workflow 基于 actor 提供更高层次的抽象来编排一组 actor,实现常见的 workflow 模式并代表您管理 actor 的生命周期。

Dapr actors 旨在提供一种在分布式系统中封装状态和行为的方法。actor 可以按需由客户端应用程序激活。当 actor 被激活时,它被分配一个唯一的身份,这使得它能够在多次调用中保持其状态。这使得 actor 在构建有状态、可扩展和容错的分布式应用程序时非常有用。

另一方面,Dapr Workflow 提供了一种定义和编排涉及多个服务和组件的复杂 workflow 的方法。workflow 允许您定义需要按特定顺序执行的一系列步骤或任务,并可用于实现业务流程、事件驱动的 workflow 和其他类似场景。

如上所述,Dapr Workflow 基于 Dapr Actors 管理其激活和生命周期。

何时使用 Dapr Actors

与任何其他技术决策一样,您应该根据要解决的问题来决定是否使用 actor。例如,如果您正在构建一个聊天应用程序,您可能会使用 Dapr actors 来实现聊天室和用户之间的单个聊天会话,因为每个聊天会话都需要维护自己的状态并且具有可扩展性和容错性。

一般来说,如果您的问题空间涉及大量(数千个或更多)小型、独立和隔离的状态和逻辑单元,可以考虑使用 actor 模式来建模您的问题或场景。

  • 您的问题空间涉及大量(数千个或更多)小型、独立和隔离的状态和逻辑单元。
  • 您希望使用单线程对象,这些对象不需要外部组件的显著交互,包括跨一组 actor 查询状态。
  • 您的 actor 实例不会通过发出 I/O 操作来阻塞调用者,导致不可预测的延迟。

何时使用 Dapr Workflow

当您需要定义和编排涉及多个服务和组件的复杂 workflow 时,您可以使用 Dapr Workflow。例如,使用前面提到的聊天应用程序示例,您可能会使用 Dapr Workflow 来定义应用程序的整体 workflow,例如如何注册新用户、如何发送和接收消息以及应用程序如何处理错误和异常。

了解有关 Dapr Workflow 的更多信息以及如何在应用程序中使用 workflow。

Actor 类型和 Actor ID

actor 被唯一定义为 actor 类型的一个实例,类似于对象是类的一个实例。例如,您可能有一个实现计算器功能的 actor 类型。可能有许多此类型的 actor 分布在集群的各个节点上。

每个 actor 都由一个 actor ID 唯一标识。actor ID 可以是您选择的任何字符串值。如果您不提供 actor ID,Dapr 会为您生成一个随机字符串作为 ID。

功能

命名空间化的 Actors

Dapr 支持命名空间化的 actor。actor 类型可以部署到不同的命名空间中。您可以在同一命名空间中调用这些 actor 的实例。

了解有关命名空间化的 actor 及其工作原理的更多信息。

Actor 生命周期

由于 Dapr actors 是虚拟的,因此不需要显式创建或销毁。Dapr actor 运行时:

  1. 一旦收到该 actor ID 的初始请求,就会自动激活 actor。
  2. 垃圾收集未使用的 actor 的内存对象。
  3. 维护 actor 的存在信息,以防它稍后被重新激活。

actor 的状态超出了对象的生命周期,因为状态存储在为 Dapr 运行时配置的状态提供者中。

了解有关 actor 生命周期的更多信息。

分布和故障转移

为了提供可扩展性和可靠性,actor 实例在整个集群中分布,Dapr 在整个集群中分布 actor 实例,并自动将它们迁移到健康的节点。

了解有关 Dapr actor 放置的更多信息。

Actor 通信

您可以通过 HTTP 调用 actor 方法,如下面的通用示例所示。

  1. 服务调用 sidecar 上的 actor API。
  2. 使用来自放置服务的缓存分区信息,sidecar 确定哪个 actor 服务实例将托管 actor ID 3。调用被转发到适当的 sidecar。
  3. pod 2 中的 sidecar 实例调用服务实例以调用 actor 并执行 actor 方法。

了解有关调用 actor 方法的更多信息。

并发

Dapr actor 运行时为访问 actor 方法提供了一个简单的轮流访问模型。轮流访问极大地简化了并发系统,因为不需要同步机制来进行数据访问。

状态

事务性状态存储可以用于存储 actor 状态。无论您是否打算在 actor 中存储任何状态,您都必须在状态存储组件的元数据部分中将属性 actorStateStore 的值指定为 true。actor 状态以特定方案存储在事务性状态存储中,允许进行一致的查询。所有 actor 只能使用单个状态存储组件作为状态存储。阅读状态 API 参考actors API 参考以了解有关 actor 状态存储的更多信息。

Actor 定时器和提醒

actor 可以通过注册定时器或提醒来安排定期工作。

定时器和提醒的功能非常相似。主要区别在于 Dapr actor 运行时在停用后不保留有关定时器的任何信息,而是使用 Dapr actor 状态提供者持久化有关提醒的信息。

这种区别允许用户在轻量级但无状态的定时器与更耗资源但有状态的提醒之间进行权衡。

以下概述视频和演示展示了 actor 定时器和提醒如何工作。

下一步

Actors 功能和概念 >>

相关链接

1.6.2 - Actor 的运行时特性

了解 Dapr 中 Actor 的特性和概念

在您已经从高层次上了解了 Actor 构建块之后,让我们深入探讨 Dapr 中 Actor 的特性和概念。

Actor 的生命周期

Dapr 中的 Actor 是虚拟的,这意味着它们的生命周期与内存中的表示无关。因此,不需要显式地创建或销毁它们。Dapr 的 Actor 运行时会在首次收到某个 Actor ID 的请求时自动激活该 Actor。如果某个 Actor 在一段时间内未被使用,Dapr 的 Actor 运行时会对其进行垃圾回收,但会保留其存在的信息,以便在需要时重新激活。

调用 Actor 方法、定时器和提醒会重置 Actor 的空闲时间。例如,提醒的触发会保持 Actor 的活跃状态。

  • Actor 的提醒会在无论其活跃与否的情况下触发。如果提醒触发了一个不活跃的 Actor,它会先激活该 Actor。
  • Actor 的定时器触发会重置空闲时间;然而,定时器仅在 Actor 活跃时触发。

Dapr 运行时用于判断 Actor 是否可以被垃圾回收的空闲超时和扫描间隔是可配置的。当 Dapr 运行时调用 Actor 服务以获取支持的 Actor 类型时,可以传递此信息。

这种虚拟 Actor 生命周期的抽象带来了一些注意事项,尽管 Dapr 的 Actor 实现有时会偏离这种模型。

Actor 在首次向其 Actor ID 发送消息时会自动激活(即构建 Actor 对象)。经过一段时间后,Actor 对象会被垃圾回收。将来再次使用该 Actor ID 会导致构建新的 Actor 对象。Actor 的状态超越对象的生命周期,因为状态存储在为 Dapr 运行时配置的状态提供者中。

分布和故障转移

为了提供可扩展性和可靠性,Actor 实例分布在整个集群中,Dapr 会根据需要自动将它们从故障节点迁移到健康节点。

Actor 分布在 Actor 服务的实例中,这些实例分布在集群中的节点上。每个服务实例包含给定 Actor 类型的一组 Actor。

Actor 放置服务

Dapr 的 Actor 运行时通过 Actor Placement 服务为您管理分布方案和键范围设置。当创建服务的新实例时:

  1. Sidecar 调用 Actor 服务以检索注册的 Actor 类型和配置设置。
  2. 相应的 Dapr 运行时注册它可以创建的 Actor 类型。
  3. Placement 服务计算给定 Actor 类型的所有实例的分区。

每个 Actor 类型的分区数据表在环境中运行的每个 Dapr 实例中更新和存储,并且可以随着 Actor 服务的新实例的创建和销毁而动态变化。

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

这简化了一些选择,但也带来了一些考虑:

  • 默认情况下,Actor 随机放置到 Pod 中,导致均匀分布。
  • 由于 Actor 是随机放置的,因此应预期 Actor 操作总是需要网络通信,包括方法调用数据的序列化和反序列化,从而产生延迟和开销。

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 目前支持 .NETJavaPython 的 Actor SDK。

有关更多详细信息,请参阅 Dapr Actor 特性

并发

Dapr 的 Actor 运行时为访问 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 运行时配置行为

您可以使用以下配置参数来调整 Dapr actor 的默认运行时行为。

参数描述默认值
entities此主机支持的 actor 类型。N/A
actorIdleTimeout空闲 actor 的停用超时时间。每隔 actorScanInterval 时间间隔检查一次。60 分钟
actorScanInterval指定扫描空闲 actor 的时间间隔。超过 actorIdleTimeout 的 actor 将被停用。30 秒
drainOngoingCallTimeout在重新平衡 actor 时,指定当前活动 actor 方法的完成超时时间。如果没有正在进行的方法调用,则忽略此项。60 秒
drainRebalancedActors如果设置为 true,Dapr 将在 drainOngoingCallTimeout 时间内等待当前 actor 调用完成,然后再尝试停用 actor。true
reentrancy (ActorReentrancyConfig)配置 actor 的重入行为。如果未提供,则重入功能被禁用。禁用,false
remindersStoragePartitions配置 actor 的提醒分区数量。如果未提供,所有提醒将作为 actor 状态存储中的单个记录保存。0
entitiesConfig使用配置数组单独配置每个 actor 类型。任何在单个实体配置中指定的实体也必须在顶级 entities 字段中列出。N/A

示例

// 在 Startup.cs 中
public void ConfigureServices(IServiceCollection services)
{
    // 使用 DI 注册 actor 运行时
    services.AddActors(options =>
    {
        // 注册 actor 类型并配置 actor 设置
        options.Actors.RegisterActor<MyActor>();

        // 配置默认设置
        options.ActorIdleTimeout = TimeSpan.FromMinutes(60);
        options.ActorScanInterval = TimeSpan.FromSeconds(30);
        options.DrainOngoingCallTimeout = TimeSpan.FromSeconds(60);
        options.DrainRebalancedActors = true;
        options.RemindersStoragePartitions = 7;
        options.ReentrancyConfig = new() { Enabled = false };

        // 为特定 actor 类型添加配置。
        // 此 actor 类型必须在基础级别的 'entities' 字段中有匹配值。如果没有,配置将被忽略。
        // 如果有匹配的实体,这里的值将用于覆盖根配置中指定的任何值。
        // 在此示例中,`ReentrantActor` 启用了重入;然而,'MyActor' 将不启用重入。
        options.Actors.RegisterActor<ReentrantActor>(typeOptions: new()
        {
            ReentrancyConfig = new()
            {
                Enabled = true,
            }
        });
    });

    // 注册用于 actor 的其他服务
    services.AddSingleton<BankService>();
}

查看 .NET SDK 文档以注册 actor

import { CommunicationProtocolEnum, DaprClient, DaprServer } from "@dapr/dapr";

// 使用 DaprClientOptions 配置 actor 运行时。
const clientOptions = {
  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, daprHost, daprPort, clientOptions);

const client = new DaprClient(daprHost, daprPort, CommunicationProtocolEnum.HTTP, clientOptions);

查看使用 JavaScript SDK 编写 actor 的文档

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),
        remindersStoragePartitions=7
    )
)

查看使用 Python SDK 运行 actor 的文档

// 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);
ActorRuntime.getInstance().getConfig().setRemindersStoragePartitions(7);

查看使用 Java SDK 编写 actor 的文档

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{
		{
            // 为特定 actor 类型添加配置。
            // 此 actor 类型必须在基础级别的 'entities' 字段中有匹配值。如果没有,配置将被忽略。
            // 如果有匹配的实体,这里的值将用于覆盖根配置中指定的任何值。
            // 在此示例中,`reentrantActorType` 启用了重入;然而,'defaultActorType' 将不启用重入。
			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)
}

查看使用 Go SDK 的 actor 示例

下一步

启用 actor reminder 分区 >>

相关链接

1.6.4 - 命名空间中的actor

了解命名空间中的actor

在Dapr中,命名空间用于提供隔离,从而支持多租户。通过为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>

查看您的状态存储组件规格以了解其提供的功能。

下一步

1.6.5 - actor 定时器和提醒

为您的 actor 设置定时器和提醒并执行错误处理

actor 可以通过注册定时器或提醒来安排周期性工作。

定时器和提醒的功能非常相似。主要区别在于 Dapr actor 运行时在停用后不会保留任何关于定时器的信息,而是使用 Dapr actor 状态提供程序持久化提醒的信息。

这种区别允许用户在轻量但无状态的定时器与更资源密集但有状态的提醒之间进行选择。

定时器和提醒的调度配置是相同的,概述如下:


dueTime 是一个可选参数,用于设置第一次调用回调的时间或时间间隔。如果省略 dueTime,则在定时器/提醒注册后立即调用回调。

支持的格式:

  • RFC3339 日期格式,例如 2020-10-02T15:00:00Z
  • time.Duration 格式,例如 2h30m
  • ISO 8601 持续时间 格式,例如 PT2H30M

period 是一个可选参数,用于设置两次连续回调调用之间的时间间隔。当以 ISO 8601-1 持续时间 格式指定时,您还可以配置重复次数以限制回调调用的总次数。 如果省略 period,则回调只会被调用一次。

支持的格式:


ttl 是一个可选参数,用于设置定时器/提醒将过期和删除的时间或时间间隔。如果省略 ttl,则不应用任何限制。

支持的格式:

  • RFC3339 日期格式,例如 2020-10-02T15:00:00Z
  • time.Duration 格式,例如 2h30m
  • ISO 8601 持续时间 格式。示例:PT2H30M

actor 运行时验证调度配置的正确性,并在输入无效时返回错误。

当您同时指定 period 中的重复次数和 ttl 时,定时器/提醒将在任一条件满足时停止。

actor 定时器

您可以在 actor 上注册一个基于定时器执行的回调。

Dapr actor 运行时确保回调方法遵循基于轮次的并发保证。这意味着在此回调完成执行之前,不会有其他 actor 方法或定时器/提醒回调正在进行。

Dapr actor 运行时在回调完成时保存对 actor 状态所做的更改。如果在保存状态时发生错误,该 actor 对象将被停用,并激活一个新实例。

当 actor 作为垃圾回收的一部分被停用时,所有定时器都会停止。之后不会调用任何定时器回调。此外,Dapr actor 运行时不会保留关于停用前正在运行的定时器的任何信息。actor 需要在将来重新激活时注册所需的任何定时器。

您可以通过调用如下所示的 HTTP/gRPC 请求或通过 Dapr SDK 为 actor 创建定时器。

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 持续时间格式)的定时器。它还将调用次数限制为 10 次。这意味着它将触发 10 次:首先在注册后立即触发,然后每隔 3 秒触发一次。

{
  "period":"R10/PT3S",
}

以下请求体配置了一个 period 为 3 秒(ISO 8601 持续时间格式)和 ttl 为 20 秒的定时器。这意味着它在注册后立即触发,然后每隔 3 秒触发一次,持续 20 秒。

{
  "period":"PT3S",
  "ttl":"20s"
}

以下请求体配置了一个 dueTime 为 10 秒、period 为 3 秒和 ttl 为 10 秒的定时器。它还将调用次数限制为 4 次。这意味着它将在 10 秒后首次触发,然后每隔 3 秒触发一次,持续 10 秒,但总共不超过 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 被明确删除或调用次数耗尽。具体来说,提醒在 actor 停用和故障转移期间被触发,因为 Dapr actor 运行时使用 Dapr actor 状态提供程序持久化关于 actor 提醒的信息。

您可以通过调用如下所示的 HTTP/gRPC 请求或通过 Dapr SDK 为 actor 创建持久提醒。

POST/PUT http://localhost:3500/v1.0/actors/<actorType>/<actorId>/reminders/<name>

提醒的请求结构与 actor 的相同。请参阅 actor 定时器示例

检索 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 提醒将重试最多三次,每次尝试之间的退避间隔为一秒。可能会根据任何可选应用的 actor 弹性策略进行额外的重试。

有关更多详细信息,请参阅 api 规范

错误处理

当 actor 的方法成功完成时,运行时将继续按照指定的定时器或提醒计划调用该方法。然而,如果方法抛出异常,运行时会捕获它并在 Dapr sidecar 日志中记录错误消息,而不进行重试。

为了允许 actor 从故障中恢复并在崩溃或重启后重试,您可以通过配置状态存储(如 Redis 或 Azure Cosmos DB)来持久化 actor 的状态。

如果方法的调用失败,定时器不会被移除。定时器仅在以下情况下被移除:

  • sidecar 崩溃
  • 执行次数用尽
  • 您明确删除它

提醒数据序列化格式

actor 提醒数据默认序列化为 JSON。从 Dapr v1.13 开始,支持通过 Placement 和 Scheduler 服务为工作流的内部提醒数据使用 protobuf 序列化格式。根据吞吐量和负载大小,这可以显著提高性能,为开发人员提供更高的吞吐量和更低的延迟。

另一个好处是将较小的数据存储在 actor 底层数据库中,这在使用某些云数据库时可以实现成本优化。使用 protobuf 序列化的限制是提醒数据不再可查询。

以 protobuf 格式保存的提醒数据无法在 Dapr 1.12.x 及更早版本中读取。建议在 Dapr v1.13 中测试此功能,并验证它在您的数据库中按预期工作,然后再投入生产。

在 Kubernetes 上启用 protobuf 序列化

要在 Kubernetes 上为 actor 提醒使用 protobuf 序列化,请使用以下 Helm 值:

--set dapr_placement.maxActorApiLevel=20

在自托管环境中启用 protobuf 序列化

要在自托管环境中为 actor 提醒使用 protobuf 序列化,请使用以下 daprd 标志:

--max-api-level=20

下一步

配置 actor 运行时行为 >>

相关链接

1.6.6 - 如何启用actor提醒分区

为您的应用程序启用actor提醒分区

actor提醒在sidecar重启后仍然持久化并继续触发。注册了多个提醒的应用程序可能会遇到以下问题:

  • 提醒注册和注销的吞吐量低
  • 基于state存储单个记录大小限制的提醒注册数量有限

为了解决这些问题,应用程序可以通过在state存储中将数据分布在多个键中来启用actor提醒分区。

  1. actors\|\|<actor type>\|\|metadata中使用一个元数据记录来存储给定actor类型的持久化配置。
  2. 多个记录存储同一actor类型的提醒子集。
actors||<actor type>||metadata{ "id": <actor metadata identifier>, "actorRemindersMetadata": { "partitionCount": <number of partitions for reminders> } }
actors||<actor type>||<actor metadata identifier>||reminders||1[ <reminder 1-1>, <reminder 1-2>, ... , <reminder 1-n> ]
actors||<actor type>||<actor metadata identifier>||reminders||2[ <reminder 1-1>, <reminder 1-2>, ... , <reminder 1-m> ]

如果您需要更改分区数量,Dapr的sidecar将自动重新分配提醒集。

配置actor运行时以分区actor提醒

与其他actor配置元素类似,actor运行时通过actor的GET /dapr/config端点提供适当的配置来分区actor提醒。选择您偏好的语言以获取actor运行时配置示例。

// 在Startup.cs中
public void ConfigureServices(IServiceCollection services)
{
    // 使用DI注册actor运行时
    services.AddActors(options =>
    {
        // 注册actor类型并配置actor设置
        options.Actors.RegisterActor<MyActor>();

        // 配置默认设置
        options.ActorIdleTimeout = TimeSpan.FromMinutes(60);
        options.ActorScanInterval = TimeSpan.FromSeconds(30);
        options.RemindersStoragePartitions = 7;
    });

    // 注册用于actor的其他服务
    services.AddSingleton<BankService>();
}

查看.NET SDK中注册actor的文档

import { CommunicationProtocolEnum, DaprClient, DaprServer } from "@dapr/dapr";

// 使用DaprClientOptions配置actor运行时。
const clientOptions = {
  actor: {
    remindersStoragePartitions: 0,
  },
};

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");

查看使用JavaScript SDK编写actor的文档

from datetime import timedelta

ActorRuntime.set_actor_config(
    ActorRuntimeConfig(
        actor_idle_timeout=timedelta(hours=1),
        actor_scan_interval=timedelta(seconds=30),
        remindersStoragePartitions=7
    )
)

查看使用Python SDK运行actor的文档

// 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().setRemindersStoragePartitions(7);

查看使用Java SDK编写actor的文档

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"`
	RemindersStoragePartitions int      `json:"remindersStoragePartitions,omitempty"`
}

var daprConfigResponse = daprConfig{
	[]string{defaultActorType},
	actorIdleTimeout,
	actorScanInterval,
	drainOngoingCallTimeout,
	drainRebalancedActors,
	7,
}

func configHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(daprConfigResponse)
}

查看使用Go SDK的actor示例

以下是一个有效的提醒分区配置示例:

{
	"entities": [ "MyActorType", "AnotherActorType" ],
	"remindersStoragePartitions": 7
}

处理配置更改

为了配置actor提醒分区,Dapr将actor类型元数据持久化在actor的state存储中。这允许配置更改在全局范围内应用,而不仅仅是在单个sidecar实例中。

此外,您只能增加分区数量,不能减少。这允许Dapr在滚动重启时自动重新分配数据,其中一个或多个分区配置可能处于活动状态。

演示

观看此视频以获取actor提醒分区的演示

1.6.7 - 操作指南:使用脚本与虚拟actor交互

通过调用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方法调用返回的数据,将在响应体中。

有关更多详细信息,请参阅Actors API规范

使用actors保存状态

您可以通过HTTP/gRPC端点与Dapr交互,利用Dapr的actor状态管理功能来可靠地保存状态。

要使用actors,您的状态存储必须支持多项事务。这意味着您的状态存储组件需要实现TransactionalStore接口。

查看支持事务/actors的组件列表。所有actors只能使用一个状态存储组件来保存状态。

下一步

actor重入 >>

相关链接

1.6.8 - 如何:在Dapr中启用和使用actor重入

了解更多关于actor重入的信息

虚拟actor模式的一个核心原则是actor的单线程执行特性。没有重入时,Dapr运行时会锁定所有actor请求。第二个请求必须等到第一个请求完成后才能启动。这意味着actor不能调用自身,也不能被另一个actor调用,即使它们属于同一调用链。

重入通过允许同一链或上下文的请求重新进入已锁定的actor来解决这个问题。这在以下场景中非常有用:

  • 一个actor想要调用自身的方法
  • actor在工作流中用于执行任务,然后回调到协调actor。

重入允许的调用链示例如下:

Actor A -> Actor A
Actor A -> Actor B -> Actor A

通过重入,您可以执行更复杂的actor调用,而不影响虚拟actor的单线程特性。

显示协调工作流actor调用工作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中的actor

相关链接

1.7 - Secret 管理

安全地从应用程序访问 Secret

1.7.1 - Secrets 管理概述

Secrets 管理 API 构建块概述

应用程序通常使用专用的 secret 存储来保存敏感信息。例如,您可以使用存储在 secret 存储中的连接字符串、密钥、令牌和其他应用程序级别的 secret 来对数据库、服务和外部系统进行身份验证,例如 AWS Secrets Manager, Azure Key Vault, Hashicorp Vault 等

为了访问这些 secret 存储,应用程序需要导入 secret 存储的 SDK。在多云场景中,这种情况更具挑战性,因为可能会使用不同供应商特定的 secret 存储。

Secrets 管理 API

Dapr 的专用 secrets 构建块 API 使开发人员更容易从 secret 存储中使用应用程序 secret。要使用 Dapr 的 secret 存储构建块,您需要:

  1. 为特定的 secret 存储解决方案设置一个组件。
  2. 在应用程序代码中使用 Dapr secrets API 检索 secret。
  3. 可选地,在 Dapr 组件文件中引用 secret。

以下概述视频和演示展示了 Dapr secrets 管理的工作原理。

功能

Secrets 管理 API 构建块为您的应用程序带来了多种功能。

在不更改应用程序代码的情况下配置 secret

您可以在应用程序代码中调用 secrets API,从 Dapr 支持的 secret 存储中检索和使用 secret。观看此视频以了解如何在应用程序中使用 secrets 管理 API 的示例。

例如,下图显示了一个应用程序从配置的云 secret 存储中请求名为 “mysecret” 的 secret,该 secret 存储名为 “vault”。

应用程序还可以使用 secrets API 从 Kubernetes secret 存储中访问 secret。默认情况下,Dapr 在 Kubernetes 模式下启用了内置的 Kubernetes secret 存储,可以通过以下方式部署:

  • 使用 Helm 的默认设置,或
  • 运行 dapr init -k

如果您使用其他 secret 存储,可以通过在 deployment.yaml 文件中添加注释 dapr.io/disable-builtin-k8s-secret-store: "true" 来禁用(不配置)Dapr Kubernetes secret 存储。默认值为 false

在下面的示例中,应用程序从 Kubernetes secret 存储中检索相同的 secret “mysecret”。

在 Azure 中,您可以配置 Dapr 使用托管身份通过 Azure Key Vault 检索 secret。在下面的示例中:

  1. 配置了一个 Azure Kubernetes Service (AKS) 集群 以使用托管身份。
  2. Dapr 使用 pod 身份 代表应用程序从 Azure Key Vault 检索 secret。

在上述示例中,应用程序代码无需更改即可获取相同的 secret。Dapr 通过 secrets 管理构建块 API 使用 secret 管理组件。

尝试使用 secrets API 通过我们的快速入门或教程之一。

在 Dapr 组件中引用 secret 存储

在配置 Dapr 组件(如 state 存储)时,通常需要在组件文件中包含凭据。或者,您可以将凭据放在 Dapr 支持的 secret 存储中,并在 Dapr 组件中引用该 secret。这是首选方法和推荐的最佳实践,尤其是在生产环境中。

有关更多信息,请阅读在组件中引用 secret 存储

限制对 secret 的访问

为了对 secret 的访问提供更细粒度的控制,Dapr 提供了定义范围和限制访问权限的能力。了解更多关于使用 secret 范围的信息。

尝试 secrets 管理

快速入门和教程

想要测试 Dapr secrets 管理 API 吗?通过以下快速入门和教程来查看 Dapr secrets 的实际应用:

快速入门/教程描述
Secrets 管理快速入门使用 secrets 管理 API 从配置的 secret 存储中在应用程序代码中检索 secret。
Secret Store 教程演示如何使用 Dapr Secrets API 访问 secret 存储。

直接在您的应用中开始管理 secret

想要跳过快速入门?没问题。您可以直接在应用程序中尝试使用 secret 管理构建块来检索和管理 secret。在安装 Dapr后,您可以从secrets 使用指南开始使用 secrets 管理 API。

下一步

1.7.2 - 如何检索 Secret

使用 Secret 存储构建块安全地检索 Secret

在了解了Dapr Secret 构建块的功能后,接下来学习如何在服务中使用它。本指南将演示如何调用 Secret API,并从配置的 Secret 存储中将 Secret 检索到应用程序代码中。

示例服务的 Secret 管理示意图。

配置 Secret 存储

在应用程序代码中检索 Secret 之前,您需要先配置一个 Secret 存储组件。此示例配置了一个使用本地 JSON 文件存储 Secret 的 Secret 存储。

在项目目录中,创建一个名为 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  # Secret 文件的路径
  - name: nestedSeparator
    value: ":"

更多信息:

获取 Secret

通过调用 Dapr sidecar 的 Secret API 来获取 Secret:

curl http://localhost:3601/v1.0/secrets/localsecretstore/secret

查看完整的 API 参考

从代码中调用 Secret API

现在您已经设置了本地 Secret 存储,可以通过 Dapr 从应用程序代码中获取 Secret。以下是利用 Dapr SDK 检索 Secret 的代码示例。

// 依赖项
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Dapr.Client;
using Microsoft.AspNetCore.Mvc;
using System.Threading;
using System.Text.Json;

// 代码
namespace EventService
{
    class Program
    {
        static async Task Main(string[] args)
        {
            string SECRET_STORE_NAME = "localsecretstore";
            using var client = new DaprClientBuilder().Build();
            // 使用 Dapr SDK 获取 Secret
            var secret = await client.GetSecretAsync(SECRET_STORE_NAME, "secret");
            Console.WriteLine($"Result: {string.Join(", ", secret)}");
        }
    }
}
// 依赖项
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;

// 代码
@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();
        // 使用 Dapr SDK 获取 Secret
        Map<String, String> secret = client.getSecret(SECRET_STORE_NAME, "secret").block();
        log.info("Result: " + JSON_SERIALIZER.writeValueAsString(secret));
    }
}
# 依赖项 
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 = "localsecretstore"
key = 'secret'

with DaprClient() as client:
    # 使用 Dapr SDK 获取 Secret
    secret = client.get_secret(store_name=DAPR_STORE_NAME, key=key)
    logging.info('Result: ')
    logging.info(secret.secret)
    # 使用 Dapr SDK 获取批量 Secret
    secret = client.get_bulk_secret(store_name=DAPR_STORE_NAME)
    logging.info('Result for bulk secret: ')
    logging.info(sorted(secret.secrets.items()))
// 依赖项 
import (
	"context"
	"log"

	dapr "github.com/dapr/go-sdk/client"
)

// 代码
func main() {
	client, err := dapr.NewClient()
	SECRET_STORE_NAME := "localsecretstore"
	if err != nil {
		panic(err)
	}
	defer client.Close()
	ctx := context.Background()
     // 使用 Dapr SDK 获取 Secret
	secret, err := client.GetSecret(ctx, SECRET_STORE_NAME, "secret", nil)
	if secret != nil {
		log.Println("Result : ")
		log.Println(secret)
	}
    // 使用 Dapr SDK 获取批量 Secret
	secretBulk, err := client.GetBulkSecret(ctx, SECRET_STORE_NAME, nil)

	if secret != nil {
		log.Println("Result for bulk: ")
		log.Println(secretBulk)
	}
}
// 依赖项 
import { DaprClient, HttpMethod, CommunicationProtocolEnum } from '@dapr/dapr'; 

// 代码
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";
    // 使用 Dapr SDK 获取 Secret
    var secret = await client.secret.get(SECRET_STORE_NAME, "secret");
    console.log("Result: " + secret);
    // 使用 Dapr SDK 获取批量 Secret
    secret = await client.secret.getBulk(SECRET_STORE_NAME);
    console.log("Result for bulk: " + secret);
}

main();

相关链接

1.7.3 - 如何使用:配置 secret 访问范围

通过设置访问范围限制应用程序从 secret 存储中读取的 secret

当您为应用程序配置了 secret 存储后,Dapr 应用程序默认可以访问该存储中定义的所有 secret。

您可以通过在应用程序配置中定义 secret 访问范围策略,来限制 Dapr 应用程序对特定 secret 的访问权限。

secret 访问范围策略适用于任何secret 存储,包括:

  • 本地 secret 存储
  • Kubernetes secret 存储
  • 公有云 secret 存储

有关如何设置secret 存储的详细信息,请阅读如何:检索 secret

观看此视频以了解如何在应用程序中使用 secret 访问范围的演示。

场景 1:拒绝访问 secret 存储中的所有 secret

在此示例中,所有 secret 访问都被拒绝给运行在 Kubernetes 集群上的应用程序,该集群配置了名为 mycustomsecretstoreKubernetes secret 存储。除了用户定义的自定义存储外,示例还配置了 Kubernetes 默认存储(名为 kubernetes),以确保所有 secret 都被拒绝访问。了解有关 Kubernetes 默认 secret 存储的更多信息

定义以下 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 secret 存储的应用程序,请按照这些说明,并将以下注释添加到应用程序 pod:

dapr.io/config: appconfig

配置完成后,应用程序将无法访问 Kubernetes secret 存储中的任何 secret。

场景 2:仅允许访问 secret 存储中的某些 secret

此示例使用名为 vault 的 secret 存储。这可以是设置在应用程序上的 Hashicorp secret 存储组件。要允许 Dapr 应用程序仅访问 vault secret 存储中的 secret1secret2,请定义以下 appconfig.yaml

apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: appconfig
spec:
  secrets:
    scopes:
      - storeName: vault
        defaultAccess: deny
        allowedSecrets: ["secret1", "secret2"]

vault secret 存储的默认访问是 deny,但应用程序可以根据 allowedSecrets 列表访问特定的 secret。了解如何将配置应用于 sidecar

场景 3:拒绝访问 secret 存储中的某些敏感 secret

定义以下 config.yaml

apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: appconfig
spec:
  secrets:
    scopes:
      - storeName: vault
        defaultAccess: allow # 这是默认值,可以省略
        deniedSecrets: ["secret1", "secret2"]

此示例配置明确拒绝访问名为 vault 的 secret 存储中的 secret1secret2,同时允许访问所有其他 secret。了解如何将配置应用于 sidecar

权限优先级

allowedSecretsdeniedSecrets 列表的设置优先于 defaultAccess 策略。

场景defaultAccessallowedSecretsdeniedSecrets权限
1 - 仅默认访问deny/allowdeny/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 - 配置

管理应用程序配置并接收更改通知

1.8.1 - 配置概述

配置API构建模块的概述

在开发应用程序时,配置是一个常见的任务。通常,我们会使用配置存储来管理这些配置数据。配置项通常具有动态特性,并且与应用程序的需求紧密相关。

例如,应用程序的配置可能包括:

  • 密钥名称
  • 各种标识符
  • 分区或消费者ID
  • 数据库名称等

通常,配置项以键/值对的形式存储在状态存储或数据库中。开发人员或运维人员可以在运行时更改配置存储中的应用程序配置。一旦进行了更改,服务会被通知以加载新的配置。

从应用程序API的角度来看,配置数据是只读的,配置存储的更新通过运维工具进行。使用Dapr的配置API,您可以:

  • 获取以只读键/值对形式返回的配置项
  • 订阅配置项的变更通知

试用配置

快速入门

想要测试Dapr配置API?通过以下快速入门来了解配置API的实际应用:

快速入门描述
配置快速入门使用配置API获取配置项或订阅配置更改。

直接在应用中开始使用配置API

想要跳过快速入门?没问题。您可以直接在应用程序中尝试配置构建模块以读取和管理配置数据。在Dapr安装完成后,您可以从配置操作指南开始使用配置API。

观看演示

观看使用Dapr配置构建模块的演示

下一步

请参阅以下指南:

1.8.2 - 操作指南:从存储中管理配置

学习如何获取应用程序配置并订阅更改

本示例使用Redis配置存储组件来演示如何检索配置项。

示例服务获取配置的图示

在存储中创建配置项

在支持的配置存储中创建一个配置项。这可以是一个简单的键值项,使用您选择的任何键。本示例使用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:

  • 对于Kubernetes使用kubectl
  • 使用Dapr CLI运行时。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: configstore
spec:
  type: configuration.redis
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    value: <PASSWORD>

检索配置项

获取配置项

以下示例展示了如何使用Dapr配置API获取已保存的配置项。

//依赖项
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Dapr.Client;

//代码
namespace ConfigurationApi
{
    public class Program
    {
        private static readonly string CONFIG_STORE_NAME = "configstore";

        public static async Task Main(string[] args)
        {
            using var client = new DaprClientBuilder().Build();
            var configuration = await client.GetConfiguration(CONFIG_STORE_NAME, new List<string>() { "orderId1", "orderId2" });
            Console.WriteLine($"Got key=\n{configuration[0].Key} -> {configuration[0].Value}\n{configuration[1].Key} -> {configuration[1].Value}");
        }
    }
}
//依赖项
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;

//代码
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());
      }
    }
}
#依赖项
from dapr.clients import DaprClient
#代码
with DaprClient() as d:
        CONFIG_STORE_NAME = 'configstore'
        keys = ['orderId1', 'orderId2']
        #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尚不支持通过HTTP协议的配置API
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 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));

启动一个Dapr sidecar:

dapr run --app-id orderprocessing --dapr-http-port 3601

在另一个终端中,获取之前保存的配置项:

curl http://localhost:3601/v1.0/configuration/configstore?key=orderId1

启动一个Dapr sidecar:

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;

const string DAPR_CONFIGURATION_STORE = "configstore";
var CONFIGURATION_KEYS = new List<string> { "orderId1", "orderId2" };
var client = new DaprClientBuilder().Build();

// 订阅配置更改
SubscribeConfigurationResponse subscribe = await client.SubscribeConfiguration(DAPR_CONFIGURATION_STORE, CONFIGURATION_ITEMS);

// 打印配置更改
await foreach (var items in subscribe.Source)
{
  // 应用程序订阅配置更改时的首次调用仅返回订阅ID
  if (items.Keys.Count == 0)
  {
    Console.WriteLine("App subscribed to config changes with subscription id: " + subscribe.Id);
    subscriptionId = subscribe.Id;
    continue;
  }
  var cfg = System.Text.Json.JsonSerializer.Serialize(items);
  Console.WriteLine("Configuration update " + cfg);
}

导航到包含上述代码的目录,然后运行以下命令以启动Dapr sidecar和订阅者应用程序:

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;

namespace ConfigurationApi
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Starting application.");
            CreateHostBuilder(args).Build().Run();
            Console.WriteLine("Closing application.");
        }

        /// <summary>
        /// 创建WebHost Builder。
        /// </summary>
        /// <param name="args">参数。</param>
        /// <returns>返回IHostbuilder。</returns>
        public static IHostBuilder CreateHostBuilder(string[] args)
        {
            var client = new DaprClientBuilder().Build();
            return Host.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration(config =>
                {
                    // 获取初始值并继续监视其更改。
                    config.AddDaprConfigurationStore("configstore", new List<string>() { "orderId1","orderId2" }, client, TimeSpan.FromSeconds(20));
                    config.AddStreamingDaprConfigurationStore("configstore", new List<string>() { "orderId1","orderId2" }, client, TimeSpan.FromSeconds(20));

                })
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
        }
    }
}

导航到包含上述代码的目录,然后运行以下命令以启动Dapr sidecar和订阅者应用程序:

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;

//代码
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()) {
      // 订阅配置更改
      List<String> keys = new ArrayList<>();
      keys.add("orderId1");
      keys.add("orderId2");
      Flux<SubscribeConfigurationResponse> subscription = client.subscribeConfiguration(DAPR_CONFIGURATON_STORE,keys);

      // 读取配置更改20秒
      subscription.subscribe((response) -> {
          // 首次响应包含订阅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 sidecar和订阅者应用程序:

dapr run --app-id orderprocessing -- -- mvn spring-boot:run
#依赖项
from dapr.clients import DaprClient
#代码

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 sidecar和订阅者应用程序:

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 sidecar和订阅者应用程序:

dapr run --app-id orderprocessing -- go run main.go
import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

// JS SDK尚不支持通过HTTP协议的配置API
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));
      }
    );
    // 取消订阅配置更新并在20秒后退出应用程序
    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 sidecar和订阅者应用程序:

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;

const string DAPR_CONFIGURATION_STORE = "configstore";
var client = new DaprClientBuilder().Build();

// 取消订阅配置更新并退出应用程序
async Task unsubscribe(string subscriptionId)
{
  try
  {
    await client.UnsubscribeConfiguration(DAPR_CONFIGURATION_STORE, subscriptionId);
    Console.WriteLine("App unsubscribed from config changes");
    Environment.Exit(0);
  }
  catch (Exception ex)
  {
    Console.WriteLine("Error unsubscribing from config updates: " + ex.Message);
  }
}
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;

//代码
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()) {
      // 取消订阅配置更改
      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尚不支持通过HTTP协议的配置API
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(() => {
      // 取消订阅配置更新
      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 - 分布式锁概述

分布式锁API构建模块概述

介绍

锁用于确保资源的互斥访问。例如,您可以使用锁来:

  • 独占访问数据库的行、表或整个数据库
  • 顺序锁定从队列中读取消息

任何需要更新的共享资源都可以被锁定。锁通常用于改变状态的操作,而不是读取操作。

每个锁都有一个名称。应用程序决定锁定哪些资源。通常,同一应用程序的多个实例使用这个命名锁来独占访问资源并进行更新。

例如,在竞争消费者模式中,应用程序的多个实例访问一个队列。您可以选择在应用程序执行其业务逻辑时锁定队列。

在下图中,同一应用程序的两个实例,App1,使用Redis锁组件来锁定共享资源。

  • 第一个应用程序实例获取命名锁并获得独占访问权。
  • 第二个应用程序实例无法获取锁,因此在锁被释放之前不允许访问资源,释放方式可以是:
    • 通过应用程序显式调用解锁API,或
    • 由于租约超时而在一段时间后自动释放。

*此API目前处于Alpha状态。

特性

资源的互斥访问

在任何给定时刻,只有一个应用程序实例可以持有命名锁。锁的范围限定在Dapr应用程序ID内。

使用租约防止死锁

Dapr分布式锁使用基于租约的锁定机制。如果应用程序获取锁后遇到异常,无法释放锁,则锁将在一段时间后通过租约自动释放。这防止了在应用程序故障时发生资源死锁。

演示

观看此视频以了解分布式锁API的概述

下一步

请参阅以下指南:

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 - 作业概述

作业API构建模块概述

许多应用程序需要作业调度,或者需要在未来执行某些操作。作业API是一个用于管理和安排这些未来作业的工具,可以在特定时间或间隔执行。

作业API不仅帮助您安排作业,Dapr内部还利用调度服务来安排actor提醒。

在Dapr中,作业包括:

查看示例场景。

显示调度器控制平面服务和作业API的图示

工作原理

作业API是一个作业调度器,而不是作业的执行者。设计上保证作业至少执行一次,注重可靠性和可扩展性,而非精确性。这意味着:

  • 保证: 作业不会在计划时间之前被调用。
  • 不保证: 作业在到期时间之后被调用的具体时间。

所有计划作业的详细信息和用户相关数据都存储在调度器服务的Etcd数据库中。 您可以使用作业来:

  • 延迟您的pubsub消息传递 您可以在未来的特定时间发布消息(例如:一周后,或特定的UTC日期/时间)。
  • 调度应用程序之间的服务调用方法。

场景

作业调度在以下场景中可能会有所帮助:

  • 自动化数据库备份: 确保数据库每天备份以防止数据丢失。安排一个备份脚本在每晚2点运行,创建数据库备份并将其存储在安全位置。

  • 定期数据处理和ETL(提取、转换、加载): 处理和转换来自各种来源的原始数据并将其加载到数据仓库中。安排ETL作业在特定时间运行(例如:每小时、每天)以获取新数据、处理并更新数据仓库中的信息。

  • 电子邮件通知和报告: 通过电子邮件接收每日销售报告和每周性能摘要。安排一个作业生成所需的报告并在每天早上6点通过电子邮件发送每日报告,每周一早上8点发送每周摘要。

  • 维护任务和系统更新: 执行定期维护任务,如清理临时文件、更新软件和检查系统健康状况。安排各种维护脚本在非高峰时段运行,如周末或深夜,以尽量减少对用户的干扰。

  • 金融交易的批处理: 处理需要在每个工作日结束时批处理和结算的大量交易。安排批处理作业在每个工作日下午5点运行,汇总当天的交易并执行必要的结算和对账。

Dapr的作业API确保这些场景中表示的任务在没有人工干预的情况下始终如一地执行,提高效率并减少错误风险。

特性

作业API提供了多种特性,使您可以轻松调度作业。

在多个副本之间调度作业

调度器服务支持在多个副本之间扩展作业调度,同时保证作业仅由一个调度器服务实例触发。

试用作业API

您可以在应用程序中试用作业API。在Dapr安装完成后,您可以开始使用作业API,从如何:调度作业指南开始。

下一步

1.10.2 - 操作指南:调度和处理触发的作业

学习如何使用作业API来调度和处理触发的作业

现在您已经了解了作业构建块提供的功能,让我们来看一个如何使用API的示例。下面的代码示例描述了一个为数据库备份应用程序调度作业并在触发时处理它们的应用程序,也就是作业因到达其到期时间而被返回到应用程序的时间。

启动调度器服务

当您在本地托管模式或Kubernetes上运行dapr init时,Dapr调度器服务会启动。

设置作业API

在您的代码中,配置并调度应用程序内的作业。

以下.NET SDK代码示例调度名为prod-db-backup的作业。作业数据包含有关您将定期备份的数据库的信息。在本示例中,您将:

  • 定义在示例其余部分中使用的类型
  • 在应用程序启动期间注册一个端点,以处理服务上的所有作业触发调用
  • 向Dapr注册作业

在以下示例中,您将创建记录,序列化并与作业一起注册,以便在将来作业被触发时可以使用这些信息:

  • 备份任务的名称(db-backup
  • 备份任务的Metadata,包括:
    • 数据库名称(DBName
    • 数据库位置(BackupLocation

创建一个ASP.NET Core项目,并从NuGet添加最新版本的Dapr.Jobs

注意: 虽然您的项目不严格需要使用Microsoft.NET.Sdk.Web SDK来创建作业,但在撰写本文档时,只有调度作业的服务会接收到其触发调用。由于这些调用期望有一个可以处理作业触发的端点,并且需要Microsoft.NET.Sdk.Web SDK,因此建议您为此目的使用ASP.NET Core项目。

首先定义类型以持久化我们的备份作业数据,并将我们自己的JSON属性名称属性应用于属性,以便它们与其他语言示例保持一致。

//定义我们将用来表示作业数据的类型
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);

接下来,作为应用程序设置的一部分,设置一个处理程序,该处理程序将在作业在您的应用程序上被触发时调用。此处理程序负责根据提供的作业名称识别应如何处理作业。

这通过在ASP.NET Core中注册一个处理程序来实现,路径为/job/<job-name>,其中<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();

//注册一个端点以接收和处理触发的作业
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5));
app.MapDaprScheduledJobHandler((string jobName, DaprJobDetails jobDetails, ILogger logger, CancellationToken cancellationToken) => {
  logger?.LogInformation("Received trigger invocation for job '{jobName}'", jobName);
  switch (jobName)
  {
    case "prod-db-backup":
      // 反序列化作业负载元数据
      var jobData = JsonSerializer.Deserialize<BackupJobData>(jobDetails.Payload);
      
      // 处理备份操作 - 我们假设这在您的代码中已实现
      await BackupDatabaseAsync(jobData, cancellationToken);
      break;
  }
}, cancellationTokenSource.Token);

await app.RunAsync();

最后,作业本身需要在Dapr中注册,以便可以在以后触发。您可以通过将DaprJobsClient注入到类中并作为应用程序的入站操作的一部分执行此操作,但为了本示例的目的,它将放在您上面开始的Program.cs文件的底部。因为您将使用依赖注入注册的DaprJobsClient,所以首先创建一个范围以便可以访问它。

//创建一个范围以便可以访问注册的DaprJobsClient
await using scope = app.Services.CreateAsyncScope();
var daprJobsClient = scope.ServiceProvider.GetRequiredService<DaprJobsClient>();

//创建我们希望与未来作业触发一起呈现的负载
var jobData = new BackupJobData("db-backup", new BackupMetadata("my-prod-db", "/backup-dir")); 

//将我们的负载序列化为UTF-8字节
var serializedJobData = JsonSerializer.SerializeToUtf8Bytes(jobData);

//调度我们的备份作业每分钟运行一次,但只重复10次
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() {
    // 初始化服务器
	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)
		}
	}()
    // ...

    // 设置备份位置
	jobData, err := json.Marshal(&api.DBBackup{
		Task: "db-backup",
		Metadata: api.Metadata{
			DBName:         "my-prod-db",
			BackupLocation: "/backup-dir",
		},
	},
	)
	// ...
}

作业是通过设置Schedule和所需的Repeats数量来调度的。这些设置决定了作业应被触发并发送回应用程序的最大次数。

在此示例中,在触发时间,即根据Schedule@every 1s,此作业被触发并最多发送回应用程序Repeats10)次。

    // ...
    // 设置作业
	job := daprc.Job{
		Name:     "prod-db-backup",
		Schedule: "@every 1s",
		Repeats:  10,
		Data: &anypb.Any{
			Value: jobData,
		},
	}

在触发时间,调用prodDBBackupHandler函数,在触发时间执行此作业的所需业务逻辑。例如:

HTTP

当您使用Dapr的作业API创建作业时,Dapr会自动假定在/job/<job-name>有一个可用的端点。例如,如果您调度一个名为test的作业,Dapr期望您的应用程序在/job/test监听作业事件。确保您的应用程序为此端点设置了一个处理程序,以便在作业被触发时处理它。例如:

注意:以下示例是用Go编写的,但适用于任何编程语言。


func main() {
    ...
    http.HandleFunc("/job/", handleJob)
	http.HandleFunc("/job/<job-name>", specificJob)
    ...
}

func specificJob(w http.ResponseWriter, r *http.Request) {
    // 处理特定触发的作业
}

func handleJob(w http.ResponseWriter, r *http.Request) {
    // 处理触发的作业
}

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) {
    // 处理触发的作业
}

此函数在您的gRPC服务器上下文中处理触发的作业。当您设置服务器时,确保注册回调服务器,当作业被触发时将调用此函数:

...
js := &JobService{}
rtv1.RegisterAppCallbackAlphaServer(server, js)

在此设置中,您可以完全控制如何接收和处理触发的作业,因为它们直接通过此gRPC方法路由。

SDKs

对于SDK用户,处理触发的作业更简单。当作业被触发时,Dapr会自动将作业路由到您在服务器初始化期间设置的事件处理程序。例如,在Go中,您可以这样注册事件处理程序:

...
if err = server.AddJobEventHandler("prod-db-backup", prodDBBackupHandler); err != nil {
    log.Fatalf("failed to register job event handler: %v", err)
}

Dapr负责底层路由。当作业被触发时,您的prodDBBackupHandler函数将被调用,并带有触发的作业数据。以下是处理触发作业的示例:

// ...

// 在作业触发时调用此函数
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 sidecar

一旦您在应用程序中设置了作业API,在终端窗口中使用以下命令运行Dapr sidecar。

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.11 - 互动

通过提示有效使用大型语言模型(LLMs)

1.11.1 - 会话概述

会话API功能概述

Dapr的会话API简化了与大型语言模型(LLM)进行大规模、安全、可靠交互的复杂性。无论您是缺乏必要本地SDK的开发者,还是只想专注于LLM交互提示的多语言开发团队,会话API都提供了一个统一的API接口来与底层LLM提供商进行对话。

显示用户应用与Dapr的LLM组件通信流程的图示。

除了启用关键的性能和安全功能(如提示缓存个人信息清理),您还可以将会话API与Dapr的其他功能结合使用,例如:

  • 弹性断路器和重试机制,以应对限制和令牌错误,或
  • 中间件,用于验证与LLM之间的请求

Dapr通过为您的LLM交互提供指标,增强了系统的可观测性。

功能

以下功能适用于所有支持的会话组件

提示缓存

提示缓存通过存储和重用在多个API调用中经常重复的提示来优化性能。Dapr将这些频繁使用的提示存储在本地缓存中,从而显著减少延迟和成本,使您的集群、pod或其他组件可以重用,而无需为每个新请求重新处理信息。

个人信息清理

个人信息清理功能能够识别并删除会话响应中的任何形式的敏感用户信息。只需在输入和输出数据上启用此功能,即可保护您的隐私,清除可能用于识别个人的敏感细节。

演示

观看在Diagrid的Dapr v1.15庆祝活动中展示的演示,了解会话API如何使用.NET SDK工作。

试用会话

快速入门和教程

想要测试Dapr会话API?通过以下快速入门和教程来查看其实际应用:

快速入门/教程描述
会话快速入门TODO

直接在您的应用中开始使用会话API

想跳过快速入门?没问题。您可以直接在您的应用中试用会话模块。在Dapr安装完成后,您可以从操作指南开始使用会话API。

下一步

1.11.2 - 操作指南:使用 conversation API 与 LLM 对话

学习如何简化与大型语言模型交互的复杂性

让我们开始使用 conversation API。在本指南中,您将学习如何:

  • 配置一个可用的 Dapr 组件(echo),以便与 conversation API 搭配使用。
  • 将 conversation 客户端集成到您的应用程序中。
  • 使用 dapr run 启动连接。

配置 conversation 组件

创建一个名为 conversation.yaml 的新配置文件,并将其保存到应用程序目录中的组件或配置子文件夹中。

为您的 conversation.yaml 文件选择 合适的 conversation 组件规范

在这个场景中,我们使用一个简单的 echo 组件。

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: echo
spec:
  type: conversation.echo
  version: v1

集成 conversation 客户端

以下示例使用 HTTP 客户端向 Dapr 的 sidecar HTTP 端点发送 POST 请求。您也可以使用 Dapr SDK 客户端

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("Received the following from the LLM:");
foreach (var resp in response.Outputs)
{
    Console.WriteLine($"\t{resp.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{
		Message: "Please write a witty haiku about the Dapr distributed programming framework at dapr.io",
		// Role:     nil, // Optional
		// ScrubPII: nil, // Optional
	}

	fmt.Printf("conversation input: %s\n", input.Message)

	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.message);

    let response = client.converse_alpha1(request).await?;

    println!("conversation output: {:?}", response.outputs[0].result);
    Ok(())
}

启动 conversation 连接

使用 dapr run 命令启动连接。例如,在这个场景中,我们在一个应用程序上运行 dapr run,其应用程序 ID 为 conversation,并指向 ./config 目录中的 conversation 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 -- go run ./main.go

预期输出

  - '== APP == conversation output: Please write a witty haiku about the Dapr distributed programming framework at dapr.io'
dapr run --app-id=conversation --resources-path ./config --dapr-grpc-port 3500 -- cargo run --example conversation

预期输出

  - 'conversation input: hello world'
  - 'conversation output: hello world'

相关链接

尝试使用支持的 SDK 仓库中提供的完整示例来体验 conversation API。

下一步

1.12 -

type: docs title: “加密技术” linkTitle: “加密技术” weight: 100 description: “在不暴露密钥的情况下执行加密操作,确保应用程序的安全性”


1.12.1 - 加密概述

Dapr 加密概述

使用加密构建块,您可以安全且一致地利用加密技术。Dapr 提供的 API 允许您在密钥库或 Dapr sidecar 中执行加密和解密操作,而无需将加密密钥暴露给您的应用程序。

为什么需要加密?

加密技术在应用程序中被广泛使用,正确实施可以在数据泄露时提高安全性。在某些情况下,您可能需要使用加密技术以符合行业法规(如金融领域)或法律要求(如 GDPR 等隐私法规)。

然而,正确使用加密技术可能很复杂。您需要:

  • 选择合适的算法和选项
  • 学习正确的密钥管理和保护方法
  • 在希望限制对加密密钥材料的访问时,处理操作复杂性

安全的一个重要要求是限制对加密密钥的访问,这通常被称为“原始密钥材料”。Dapr 可以与密钥库集成,如 Azure Key Vault(未来将支持更多组件),这些密钥库将密钥存储在安全的环境中,并在库中执行加密操作,而不将密钥暴露给您的应用程序或 Dapr。

或者,您可以配置 Dapr 为您管理加密密钥,在 sidecar 中执行操作,同样不将原始密钥材料暴露给您的应用程序。

Dapr 中的加密

使用 Dapr,您可以在不将加密密钥暴露给应用程序的情况下执行加密操作。

显示 Dapr 加密如何与您的应用程序协作的图示

通过使用加密构建块,您可以:

  • 更轻松地以安全的方式执行加密操作。Dapr 提供了防止使用不安全算法或不安全选项的保护措施。
  • 将密钥保存在应用程序之外。应用程序从未看到“原始密钥材料”,但可以请求库使用密钥执行操作。当使用 Dapr 的加密引擎时,操作在 Dapr sidecar 中安全地执行。
  • 实现更好的关注点分离。通过使用外部库或加密组件,只有授权团队可以访问私钥材料。
  • 更轻松地管理和轮换密钥。密钥在库中管理并在应用程序之外,它们可以在不需要开发人员参与(甚至不需要重启应用程序)的情况下轮换。
  • 启用更好的审计日志记录,以监控何时在库中使用密钥执行操作。

功能

加密组件

Dapr 加密构建块包括两种组件:

  • 允许与管理服务或库(“密钥库”)交互的组件。
    类似于 Dapr 在各种 secret 存储或 state 存储之上的“抽象层”,这些组件允许与各种密钥库(如 Azure Key Vault)交互(未来 Dapr 版本中会有更多)。通过这些组件,对私钥的加密操作在库中执行,Dapr 从未看到您的私钥。

  • 基于 Dapr 自身加密引擎的组件。
    当密钥库不可用时,您可以利用基于 Dapr 自身加密引擎的组件。这些组件名称中带有 .dapr.,在 Dapr sidecar 中执行加密操作,密钥存储在文件、Kubernetes secret 或其他来源中。虽然 Dapr 知道私钥,但它们仍然对您的应用程序不可用。

这两种组件,无论是利用密钥库还是使用 Dapr 中的加密引擎,都提供相同的抽象层。这允许您的解决方案根据需要在各种库和/或加密组件之间切换。例如,您可以在开发期间使用本地存储的密钥,而在生产中使用云库。

加密 API

加密 API 允许使用 Dapr Crypto Scheme v1 加密和解密数据。这是一种有见地的加密方案,旨在使用现代、安全的加密标准,并以流的方式高效处理数据(甚至是大文件)。

试用加密

快速入门和教程

想要测试 Dapr 加密 API 吗?通过以下快速入门和教程,看看加密如何实际运作:

快速入门/教程描述
加密快速入门使用加密 API 使用 RSA 和 AES 密钥加密和解密消息和大文件。

直接在您的应用程序中开始使用加密

想要跳过快速入门?没问题。您可以直接在应用程序中试用加密构建块来加密和解密您的应用程序。在 安装 Dapr 后,您可以从 加密操作指南 开始使用加密 API。

演示

观看此 Dapr 社区电话 #83 中的加密 API 演示视频

下一步

使用加密 API >>

相关链接

1.12.2 - 如何:使用加密API

学习如何加密和解密文件

在您了解了Dapr作为加密构建块之后,让我们通过使用SDK来学习如何使用加密API。

加密

在您的项目中使用Dapr SDK和gRPC API,您可以加密数据流,例如文件或字符串:

# 当传递数据(缓冲区或字符串)时,`encrypt`会返回一个包含加密信息的缓冲区
def encrypt_decrypt_string(dapr: DaprClient):
    message = 'The secret is "passw0rd"'

    # 加密消息
    resp = dapr.encrypt(
        data=message.encode(),
        options=EncryptOptions(
            # 加密组件的名称(必需)
            component_name=CRYPTO_COMPONENT_NAME,
            # 存储在加密组件中的密钥(必需)
            key_name=RSA_KEY_NAME,
            # 用于包装密钥的算法,必须由上述密钥支持。
            # 选项包括:"RSA", "AES"
            key_wrap_algorithm='RSA',
        ),
    )

    # 该方法返回一个可读流,我们将其完整读取到内存中
    encrypt_bytes = resp.read()
    print(f'加密后的消息长度为 {len(encrypt_bytes)} 字节')

在您的项目中使用Dapr SDK和gRPC API,您可以加密缓冲区或字符串中的数据:

// 当传递数据(缓冲区或字符串)时,`encrypt`会返回一个包含加密信息的缓冲区
const ciphertext = await client.crypto.encrypt(plaintext, {
    // Dapr组件的名称(必需)
    componentName: "mycryptocomponent",
    // 存储在组件中的密钥名称(必需)
    keyName: "mykey",
    // 用于包装密钥的算法,必须由上述密钥支持。
    // 选项包括:"RSA", "AES"
    keyWrapAlgorithm: "RSA",
});

API也可以与流一起使用,以更高效地加密来自流的数据。下面的示例使用流加密文件,并写入另一个文件:

// `encrypt`可以用作双工流
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"; //更改此以匹配您的加密组件
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("已写入", n, "字节")

以下示例使用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(
            # 加密组件的名称(必需)
            component_name=CRYPTO_COMPONENT_NAME,
            # 存储在加密组件中的密钥(必需)
            key_name=RSA_KEY_NAME,
        ),
    )

    # 该方法返回一个可读流,我们将其完整读取到内存中
    decrypt_bytes = resp.read()
    print(f'解密后的消息长度为 {len(decrypt_bytes)} 字节')

    print(decrypt_bytes.decode())
    assert message == decrypt_bytes.decode()

使用Dapr SDK,您可以解密缓冲区中的数据或使用流。

// 当传递数据作为缓冲区时,`decrypt`会返回一个包含解密信息的缓冲区
const plaintext = await client.crypto.decrypt(ciphertext, {
    // 唯一必需的选项是组件名称
    componentName: "mycryptocomponent",
});

// `decrypt`也可以用作双工流
await pipeline(
    fs.createReadStream("ciphertext.out"),
    await client.crypto.decrypt({
        // 唯一必需的选项是组件名称
        componentName: "mycryptocomponent",
    }),
    fs.createWriteStream("plaintext.out"),
);

要解密字符串,请在您的项目中使用’解密Async’ gRPC API。

在以下示例中,我们将获取一个字节数组(例如上面的示例)并将其解密为UTF-8编码的字符串。

public async Task<string> DecryptBytesAsync(byte[] encryptedBytes)
{
  using var client = new DaprClientBuilder().Build();

  const string componentName = "azurekeyvault"; //更改此以匹配您的加密组件
  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",
})

下一步

加密组件规范

2 - 错误代码

在使用Dapr时可能遇到的错误代码和信息

2.1 - 错误概述

Dapr 错误概述

错误代码是用于指示错误性质的数字或字母数字代码,并在可能的情况下,说明其发生的原因。

Dapr 错误代码是标准化的字符串,适用于 Dapr API 中 HTTP 和 gRPC 请求的 80 多种常见错误。这些代码会:

  • 在请求的 JSON 响应体中返回。
  • 启用后,会在运行时的调试级别日志中记录。
    • 如果您在 Kubernetes 中运行,错误代码会记录在 sidecar 中。
    • 如果您在自托管中运行,可以启用并查看调试日志。

错误格式

Dapr 错误代码由前缀、类别和错误本身的简写组成。例如:

前缀类别错误简写
ERR_PUBSUB_NOT_FOUND

一些最常见的返回错误包括:

  • ERR_ACTOR_TIMER_CREATE
  • ERR_PURGE_WORKFLOW
  • ERR_STATE_STORE_NOT_FOUND
  • ERR_HEALTH_NOT_READY

注意: 查看 Dapr 中错误代码的完整列表。

对于未找到的状态存储返回的错误可能如下所示:

{
  "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 错误代码的列表

2.2 - 错误代码参考指南

Dapr 中 gRPC 和 HTTP 错误代码列表及其描述

以下表格列出了 Dapr 运行时返回的错误代码。 错误代码会在 HTTP 请求的响应体中或 gRPC 状态响应的 ErrorInfo 部分返回(如果存在)。 我们正在努力根据 更丰富的错误模型 来改进所有 gRPC 错误响应。没有对应 gRPC 代码的错误代码表示这些错误尚未更新到此模型。

演员 API

HTTP 代码gRPC 代码描述
ERR_ACTOR_INSTANCE_MISSING缺少演员实例
ERR_ACTOR_INVOKE_METHOD调用演员方法时发生错误
ERR_ACTOR_RUNTIME_NOT_FOUND找不到演员运行时
ERR_ACTOR_STATE_GET获取演员状态时发生错误
ERR_ACTOR_STATE_TRANSACTION_SAVE保存演员事务时发生错误
ERR_ACTOR_REMINDER_CREATE创建演员提醒时发生错误
ERR_ACTOR_REMINDER_DELETE删除演员提醒时发生错误
ERR_ACTOR_REMINDER_GET获取演员提醒时发生错误
ERR_ACTOR_REMINDER_NON_HOSTED非托管演员类型的提醒操作
ERR_ACTOR_TIMER_CREATE创建演员计时器时发生错误
ERR_ACTOR_NO_APP_CHANNEL应用通道未初始化
ERR_ACTOR_STACK_DEPTH超过演员调用堆栈的最大深度
ERR_ACTOR_NO_PLACEMENT未配置放置服务
ERR_ACTOR_RUNTIME_CLOSED演员运行时已关闭
ERR_ACTOR_NAMESPACE_REQUIRED在 Kubernetes 模式下运行时,演员必须配置命名空间
ERR_ACTOR_NO_ADDRESS找不到演员的地址

工作流 API

HTTP 代码gRPC 代码描述
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 代码gRPC 代码描述
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_QUERYDAPR_STATE_QUERY_FAILED查询状态时出错
ERR_STATE_STORE_NOT_FOUNDDAPR_STATE_NOT_FOUND找不到状态存储
ERR_STATE_STORE_NOT_CONFIGUREDDAPR_STATE_NOT_CONFIGURED未配置状态存储
ERR_STATE_STORE_NOT_SUPPORTEDDAPR_STATE_TRANSACTIONS_NOT_SUPPORTED状态存储不支持事务
ERR_STATE_STORE_NOT_SUPPORTEDDAPR_STATE_QUERYING_NOT_SUPPORTED状态存储不支持查询
ERR_STATE_STORE_TOO_MANY_TRANSACTIONSDAPR_STATE_TOO_MANY_TRANSACTIONS每个事务的操作过多
ERR_MALFORMED_REQUESTDAPR_STATE_ILLEGAL_KEY无效的键

配置 API

HTTP 代码gRPC 代码描述
ERR_CONFIGURATION_GET获取配置时出错
ERR_CONFIGURATION_STORE_NOT_CONFIGURED未配置配置存储
ERR_CONFIGURATION_STORE_NOT_FOUND找不到配置存储
ERR_CONFIGURATION_SUBSCRIBE订阅配置时出错
ERR_CONFIGURATION_UNSUBSCRIBE取消订阅配置时出错

加密 API

HTTP 代码gRPC 代码描述
ERR_CRYPTO加密操作出错
ERR_CRYPTO_KEY检索加密密钥时出错
ERR_CRYPTO_PROVIDER_NOT_FOUND找不到加密提供者
ERR_CRYPTO_PROVIDERS_NOT_CONFIGURED未配置加密提供者

密钥管理 API

HTTP 代码gRPC 代码描述
ERR_SECRET_GET获取密钥时出错
ERR_SECRET_STORE_NOT_FOUND找不到密钥存储
ERR_SECRET_STORES_NOT_CONFIGURED未配置密钥存储
ERR_PERMISSION_DENIED策略拒绝权限

发布/订阅和消息传递错误

HTTP 代码gRPC 代码描述
ERR_PUBSUB_EMPTYDAPR_PUBSUB_NAME_EMPTY发布/订阅名称为空
ERR_PUBSUB_NOT_FOUNDDAPR_PUBSUB_NOT_FOUND找不到发布/订阅
ERR_PUBSUB_NOT_FOUNDDAPR_PUBSUB_TEST_NOT_FOUND找不到发布/订阅
ERR_PUBSUB_NOT_CONFIGUREDDAPR_PUBSUB_NOT_CONFIGURED未配置发布/订阅
ERR_TOPIC_NAME_EMPTYDAPR_PUBSUB_TOPIC_NAME_EMPTY主题名称为空
ERR_PUBSUB_FORBIDDENDAPR_PUBSUB_FORBIDDEN禁止访问主题的应用 ID
ERR_PUBSUB_PUBLISH_MESSAGEDAPR_PUBSUB_PUBLISH_MESSAGE发布消息时出错
ERR_PUBSUB_REQUEST_METADATADAPR_PUBSUB_METADATA_DESERIALIZATION反序列化元数据时出错
ERR_PUBSUB_CLOUD_EVENTS_SERDAPR_PUBSUB_CLOUD_EVENT_CREATION创建 CloudEvent 时出错
ERR_PUBSUB_EVENTS_SERDAPR_PUBSUB_MARSHAL_ENVELOPE编组 Cloud Event 信封时出错
ERR_PUBSUB_EVENTS_SERDAPR_PUBSUB_MARSHAL_EVENTS将事件编组为字节时出错
ERR_PUBSUB_EVENTS_SERDAPR_PUBSUB_UNMARSHAL_EVENTS解组事件时出错
ERR_PUBLISH_OUTBOX将消息发布到 outbox 时出错

对话 API

HTTP 代码gRPC 代码描述
ERR_CONVERSATION_INVALID_PARMS对话组件的参数无效
ERR_CONVERSATION_INVOKE调用对话时出错
ERR_CONVERSATION_MISSING_INPUTS对话缺少输入
ERR_CONVERSATION_NOT_FOUND找不到对话

服务调用 / 直接消息传递 API

HTTP 代码gRPC 代码描述
ERR_DIRECT_INVOKE调用服务时出错

绑定 API

HTTP 代码gRPC 代码描述
ERR_INVOKE_OUTPUT_BINDING调用输出绑定时出错

分布式锁 API

HTTP 代码gRPC 代码描述
ERR_LOCK_STORE_NOT_CONFIGURED未配置锁存储
ERR_LOCK_STORE_NOT_FOUND找不到锁存储
ERR_TRY_LOCK获取锁时出错
ERR_UNLOCK释放锁时出错

健康检查

HTTP 代码gRPC 代码描述
ERR_HEALTH_NOT_READYDapr 未准备好
ERR_HEALTH_APPID_NOT_MATCHDapr 应用 ID 不匹配
ERR_OUTBOUND_HEALTH_NOT_READYDapr 出站未准备好

通用

HTTP 代码gRPC 代码描述
ERR_API_UNIMPLEMENTEDAPI 未实现
ERR_APP_CHANNEL_NIL应用通道为 nil
ERR_BAD_REQUEST错误请求
ERR_BODY_READ读取请求体时出错
ERR_INTERNAL内部错误
ERR_MALFORMED_REQUEST请求格式错误
ERR_MALFORMED_REQUEST_DATA请求数据格式错误
ERR_MALFORMED_RESPONSE响应格式错误

调度/作业 API

HTTP 代码gRPC 代码描述
DAPR_SCHEDULER_SCHEDULE_JOBDAPR_SCHEDULER_SCHEDULE_JOB调度作业时出错
DAPR_SCHEDULER_JOB_NAMEDAPR_SCHEDULER_JOB_NAME作业名称应仅在 URL 中设置
DAPR_SCHEDULER_JOB_NAME_EMPTYDAPR_SCHEDULER_JOB_NAME_EMPTY作业名称为空
DAPR_SCHEDULER_GET_JOBDAPR_SCHEDULER_GET_JOB获取作业时出错
DAPR_SCHEDULER_LIST_JOBSDAPR_SCHEDULER_LIST_JOBS列出作业时出错
DAPR_SCHEDULER_DELETE_JOBDAPR_SCHEDULER_DELETE_JOB删除作业时出错
DAPR_SCHEDULER_EMPTYDAPR_SCHEDULER_EMPTY必需的参数为空
DAPR_SCHEDULER_SCHEDULE_EMPTYDAPR_SCHEDULER_SCHEDULE_EMPTY未提供作业的调度

通用

HTTP 代码gRPC 代码描述
ERRORERROR通用错误

下一步

2.3 - 处理 HTTP 错误代码

Dapr HTTP 错误代码的详细参考及其处理方法

在向 Dapr 运行时发出的 HTTP 调用中,如果发生错误,响应体会返回一个错误的 JSON。该 JSON 包含错误代码和描述性错误信息。

{
    "errorCode": "ERR_STATE_GET",
    "message": "请求的状态键在状态存储中不存在。"
}

相关内容

2.4 - 处理 gRPC 错误代码

关于 Dapr gRPC 错误及其处理方法的信息

最初,错误是按照 标准 gRPC 错误模型 进行处理的。然而,为了提供更详细且信息丰富的错误消息,定义了一个增强的错误模型,与 gRPC 的 更丰富的错误模型 保持一致。

标准 gRPC 错误模型

标准 gRPC 错误模型 是 gRPC 中的一种错误报告方法。每个错误响应都包含一个错误代码和一条错误消息。错误代码是标准化的,反映了常见的错误情况。

标准 gRPC 错误响应示例:

ERROR:
  Code: InvalidArgument
  Message: 输入键/键前缀 'bad||keyname' 不能包含 '||'

更丰富的 gRPC 错误模型

更丰富的 gRPC 错误模型 通过提供关于错误的额外上下文和详细信息来扩展标准错误模型。此模型包括标准错误 codemessage,以及一个 details 部分,可以包含各种类型的信息,如 ErrorInfoResourceInfoBadRequest 详细信息。

更丰富的 gRPC 错误响应示例:

ERROR:
  Code: InvalidArgument
  Message: 输入键/键前缀 'bad||keyname' 不能包含 '||'
  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": "输入键/键前缀 'bad||keyname' 不能包含 '||'"
    	    }
    	  ]
    	}

对于 HTTP 客户端,Dapr 会将 gRPC 错误模型转换为类似的 JSON 格式结构。响应包括一个 errorCode、一个 message 和一个 details 数组,反映了更丰富的 gRPC 模型中的结构。

HTTP 错误响应示例:

{
    "errorCode": "ERR_MALFORMED_REQUEST",
    "message": "api error: code = InvalidArgument desc = 输入键/键前缀 'bad||keyname' 不能包含 '||'",
    "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 = 输入键/键前缀 'bad||keyname' 不能包含 '||'"
                }
            ]
        }
    ]
}

您可以在这里找到所有可能状态详细信息的规范。

相关链接

3 - Dapr 软件开发工具包 (SDKs)

使用您喜欢的语言与 Dapr 一起工作

Dapr SDKs 是将 Dapr 集成到应用程序中的最简单方法。选择您喜欢的语言,几分钟内即可开始使用 Dapr。

SDK 包

选择您偏好的语言以了解有关客户端、服务扩展、actor 和工作流包的更多信息。

  • 客户端: Dapr 客户端允许您调用 Dapr 构建块 API 并执行每个构建块的操作。
  • 服务扩展: Dapr 服务扩展使您能够创建可被其他服务调用的服务并订阅主题。
  • actor: Dapr actor SDK 允许您构建具有方法、状态、计时器和持久性提醒的虚拟 actor。
  • 工作流: Dapr 工作流使您能够可靠地编写长时间运行的业务逻辑和集成。

SDK 语言

语言状态客户端服务扩展actor工作流
.NET稳定ASP.NET Core
Python稳定gRPC
FastAPI
Flask
Java稳定Spring Boot
Quarkus
Go稳定
PHP稳定
JavaScript稳定
C++开发中
Rust开发中

进一步阅读

3.1 - Dapr .NET SDK

用于开发 Dapr 应用程序的 .NET SDK 包

Dapr 提供多种包以协助 .NET 应用程序的开发。通过这些包,您可以使用 Dapr 创建 .NET 客户端、服务器和虚拟 actor。

先决条件

安装

要开始使用 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 sidecar 和其他 Dapr 应用程序交互的 .NET 客户端。

服务器

使用 Dapr SDK 编写 .NET 服务器和服务。包括对 ASP.NET 的支持。

Actors

在 .NET 中创建具有状态、提醒/计时器和方法的虚拟 actor。

工作流

创建和管理与其他 Dapr API 一起工作的工作流。

作业

创建和管理 .NET 中作业的调度和编排。

AI

在 .NET 中创建和管理 AI 操作

更多信息

了解更多关于本地开发选项的信息,或浏览 NuGet 包以添加到您现有的 .NET 应用程序中。

开发

了解 .NET Dapr 应用程序的本地开发选项

NuGet 包

用于将 .NET SDK 添加到您的 .NET 应用程序的 Dapr 包。


3.1.1 - 开始使用 Dapr 客户端 .NET SDK

如何使用 Dapr .NET SDK 快速上手

Dapr 客户端包使您能够从 .NET 应用程序与其他 Dapr 应用程序进行交互。

构建块

.NET SDK 允许您与所有 Dapr 构建块进行接口交互。

调用服务

HTTP

您可以使用 DaprClientSystem.Net.Http.HttpClient 来调用服务。

using var client = new DaprClientBuilder().
                UseTimeout(TimeSpan.FromSeconds(2)). // 可选:设置超时
                Build(); 

// 调用名为 "deposit" 的 POST 方法,输入类型为 "Transaction"
var data = new { id = "17", amount = 99m };
var account = await client.InvokeMethodAsync<object, Account>("routing", "deposit", data, cancellationToken);
Console.WriteLine("返回: id:{0} | 余额:{1}", account.Id, account.Balance);
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("返回: id:{0} | 余额:{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 client = new DaprClientBuilder().Build();

var state = new Widget() { Size = "small", Color = "yellow", };
await client.SaveStateAsync(storeName, stateKeyName, state, cancellationToken: cancellationToken);
Console.WriteLine("状态已保存!");

state = await client.GetStateAsync<Widget>(storeName, stateKeyName, cancellationToken: cancellationToken);
Console.WriteLine($"获取状态: {state.Size} {state.Color}");

await client.DeleteStateAsync(storeName, stateKeyName, cancellationToken: cancellationToken);
Console.WriteLine("状态已删除!");

查询状态 (Alpha)

var query = "{" +
                "\"filter\": {" +
                    "\"EQ\": { \"value.Id\": \"1\" }" +
                "}," +
                "\"sort\": [" +
                    "{" +
                        "\"key\": \"value.Balance\"," +
                        "\"order\": \"DESC\"" +
                    "}" +
                "]" +
            "}";

var client = new DaprClientBuilder().Build();
var queryResponse = await client.QueryStateAsync<Account>("querystore", query, cancellationToken: cancellationToken);

Console.WriteLine($"获取 {queryResponse.Results.Count}");
foreach (var account in queryResponse.Results)
{
    Console.WriteLine($"账户: {account.Data.Id} 余额 {account.Data.Balance}");
}

发布消息

var client = new DaprClientBuilder().Build();

var eventData = new { Id = "17", Amount = 10m, };
await client.PublishEventAsync(pubsubName, "deposit", eventData, cancellationToken);
Console.WriteLine("已发布存款事件!");

与输出绑定交互

using var client = new DaprClientBuilder().Build();

// Twilio SendGrid 绑定的示例负载
var email = new 
{
    metadata = new 
    {
        emailTo = "customer@example.com",
        subject = "来自 Dapr SendGrid 绑定的邮件",    
    }, 
    data =  "<h1>测试 Dapr 绑定</h1>这是一个测试。<br>再见!",
};
await client.InvokeBindingAsync("send-email", "create", email);

检索秘密

var client = new DaprClientBuilder().Build();

// 检索基于键值对的秘密 - 返回一个 Dictionary<string, string>
var secrets = await client.GetSecretAsync("mysecretstore", "key-value-pair-secret");
Console.WriteLine($"获取秘密键: {string.Join(", ", secrets.Keys)}");
var client = new DaprClientBuilder().Build();

// 检索基于键值对的秘密 - 返回一个 Dictionary<string, string>
var secrets = await client.GetSecretAsync("mysecretstore", "key-value-pair-secret");
Console.WriteLine($"获取秘密键: {string.Join(", ", secrets.Keys)}");

// 检索单值秘密 - 返回一个 Dictionary<string, string>
// 包含一个以秘密名称为键的单个值
var data = await client.GetSecretAsync("mysecretstore", "single-value-secret");
var value = data["single-value-secret"]
Console.WriteLine("获取了一个秘密值,我不会打印它,因为它是秘密!");

获取配置键

var client = new DaprClientBuilder().Build();

// 检索特定的一组键。
var specificItems = await client.GetConfiguration("configstore", new List<string>() { "key1", "key2" });
Console.WriteLine($"这是我的值:\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($"我得到了 {configItems.Count} 个条目!");
foreach (var item in configItems)
{
    Console.WriteLine($"{item.Key} -> {item.Value}")
}

订阅配置键

var client = new DaprClientBuilder().Build();

// 订阅配置 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;

namespace LockService
{
    class Program
    {
        [Obsolete("分布式锁 API 处于 Alpha 阶段,一旦稳定可以移除。")]
        static async Task Main(string[] args)
        {
            var daprLockName = "lockstore";
            var 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("成功");
                }
                else
                {
                    Console.WriteLine($"锁定 {fileName} 失败。");
                }
            }
        }
    }
}

解锁现有锁

using System;
using Dapr.Client;

namespace LockService
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var daprLockName = "lockstore";
            var client = new DaprClientBuilder().Build();

            var response = await client.Unlock(DAPR_LOCK_NAME, "my_file_name", "random_id_abc123"));
            Console.WriteLine(response.status);
        }
    }
}

管理工作流实例 (Alpha)

var daprClient = new DaprClientBuilder().Build();

string instanceId = "MyWorkflowInstance1";
string workflowComponentName = "dapr"; // 或者,这可以是 yaml 中定义的工作流组件的名称
string workflowName = "MyWorkflowDefinition";
var input = new { name = "Billy", age = 30 }; // 任何 JSON 可序列化的值都可以

// 启动工作流
var startResponse = await daprClient.StartWorkflowAsync(instanceId, workflowComponentName, workflowName, input);

// 终止工作流
await daprClient.TerminateWorkflowAsync(instanceId, workflowComponentName);

// 获取工作流元数据
var getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponentName, workflowName);

Sidecar APIs

Sidecar 健康

.NET SDK 提供了一种轮询 sidecar 健康状态的方法,以及一个等待 sidecar 准备就绪的便捷方法。

轮询健康状态

当 sidecar 和您的应用程序都启动(完全初始化)时,此健康端点返回 true。

var client = new DaprClientBuilder().Build();

var isDaprReady = await client.CheckHealthAsync();

if (isDaprReady) 
{
    // 执行依赖 Dapr 的代码。
}

轮询健康状态(出站)

当 Dapr 初始化了其所有组件时,此健康端点返回 true,但可能尚未完成与您的应用程序的通信通道设置。

当您希望在启动路径中利用 Dapr 组件时,这种方法最好,例如,从 secretstore 加载秘密。

var client = new DaprClientBuilder().Build();

var isDaprComponentsReady = await client.CheckOutboundHealthAsync();

if (isDaprComponentsReady) 
{
    // 执行依赖 Dapr 组件的代码。
}

等待 sidecar

DaprClient 还提供了一个辅助方法来等待 sidecar 变得健康(仅限组件)。使用此方法时,建议包含一个 CancellationToken 以允许请求超时。以下是 DaprSecretStoreConfigurationProvider 中使用此方法的示例。

// 在尝试使用 Dapr 组件之前,等待 Dapr sidecar 报告健康。
using (var tokenSource = new CancellationTokenSource(sidecarWaitTimeout))
{
    await client.WaitForSidecarAsync(tokenSource.Token);
}

// 在此处执行 Dapr 组件操作,例如获取秘密。

关闭 sidecar

var client = new DaprClientBuilder().Build();
await client.ShutdownSidecarAsync();

相关链接

3.1.1.1 - DaprClient 使用

使用 DaprClient 的基本提示和建议

生命周期管理

DaprClient 使用 TCP 套接字来访问网络资源,与 Dapr sidecar 进行通信。它实现了 IDisposable 接口,以便快速清理资源。

依赖注入

通过 AddDaprClient() 方法可以在 ASP.NET Core 中注册 Dapr 客户端。此方法接受一个可选的配置委托,用于配置 DaprClient,以及一个 ServiceLifetime 参数,允许您为注册的资源指定不同的生命周期,默认是 Singleton

以下示例展示了如何使用默认值注册 DaprClient

services.AddDaprClient();

您可以通过配置委托在 DaprClientBuilder 上指定选项来配置 DaprClient,例如:

services.AddDaprClient(daprBuilder => {
    daprBuilder.UseJsonSerializerOptions(new JsonSerializerOptions {
            WriteIndented = true,
            MaxDepth = 8
        });
    daprBuilder.UseTimeout(TimeSpan.FromSeconds(30));
});

另一个重载允许访问 DaprClientBuilderIServiceProvider,以便进行更高级的配置,例如从依赖注入容器中获取服务:

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 对象的设置是独立的,创建后无法更改。

var daprClient = new DaprClientBuilder()
    .UseJsonSerializerSettings( ... ) // 配置 JSON 序列化器
    .Build();

默认情况下,DaprClientBuilder 会按以下顺序优先获取配置值:

  • 直接提供给 DaprClientBuilder 方法的值(例如 UseTimeout(TimeSpan.FromSeconds(30))
  • 从可选的 IConfiguration 中提取的值,与环境变量名称匹配
  • 从环境变量中提取的值
  • 默认值

DaprClientBuilder 上配置

DaprClientBuilder 提供以下方法来设置配置选项:

  • UseHttpEndpoint(string): 设置 Dapr sidecar 的 HTTP 端点
  • UseGrpcEndpoint(string): 设置 Dapr sidecar 的 gRPC 端点
  • UseGrpcChannelOptions(GrpcChannelOptions): 设置 gRPC 通道选项
  • UseHttpClientFactory(IHttpClientFactory): 配置 DaprClient 使用的 HttpClient 工厂
  • UseJsonSerializationOptions(JsonSerializerOptions): 配置 JSON 序列化
  • UseDaprApiToken(string): 为 Dapr sidecar 的身份验证提供令牌
  • UseTimeout(TimeSpan): 指定与 Dapr sidecar 通信时的超时值

IConfiguration 配置

除了直接从环境变量获取配置值,您还可以通过 IConfiguration 提供这些值。

例如,在多租户环境中,您可能需要为环境变量添加前缀。以下示例展示了如何从环境变量中获取这些值到 IConfiguration,并移除前缀:

var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddEnvironmentVariables("test_"); // 获取所有以 "test_" 开头的环境变量,并移除前缀
builder.Services.AddDaprClient();

从环境变量配置

SDK 会读取以下环境变量来配置默认值:

  • DAPR_HTTP_ENDPOINT: Dapr sidecar 的 HTTP 端点,例如:https://dapr-api.mycompany.com
  • DAPR_GRPC_ENDPOINT: Dapr sidecar 的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.com
  • DAPR_HTTP_PORT: 如果未设置 DAPR_HTTP_ENDPOINT,则用于查找本地 HTTP 端点
  • DAPR_GRPC_PORT: 如果未设置 DAPR_GRPC_ENDPOINT,则用于查找本地 gRPC 端点
  • DAPR_API_TOKEN: 设置 API 令牌

配置 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 类型(数值类型、字符串、DateTime)清晰映射的 .NET 类型
  • 为顶级消息、事件或状态值创建自己的类,以便将来可以添加属性
  • 设计具有 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 属性检查更多异常详细信息。

3.1.2 - Dapr actors .NET SDK

快速掌握使用 Dapr actors .NET SDK 的方法

借助 Dapr actor 包,您可以在 .NET 应用程序中轻松与 Dapr 的虚拟 actor 进行交互。

要开始,请参阅 Dapr actors 指南。

3.1.2.1 - IActorProxyFactory 接口

了解如何使用 IActorProxyFactory 接口创建 actor 客户端

在使用 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

确定 actor

IActorProxyFactory 的所有 API 都需要提供 actor 的 类型id 以便与其通信。对于强类型客户端,您还需要提供其接口之一。

  • actor 类型 在整个应用程序中唯一标识 actor 实现。
  • actor id 唯一标识该类型的一个实例。

如果您没有 actor id 并希望与新实例通信,可以使用 ActorId.CreateRandom() 创建一个随机 id。随机 id 是一个加密强标识符,运行时将在您与其交互时创建一个新的 actor 实例。

您可以使用 ActorReference 类型在消息中传递 actor 类型和 actor id,以便与其他 actor 进行交换。

两种风格的 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: 远程 actor 方法异常,详细信息:异常:NotImplementedException,方法名称:ExceptionExample,行号:14,异常 uuid:d291a006-84d5-42c4-b39e-d6300e9ac38b

下一步

了解如何使用 ActorHost 编写和运行 actor

3.1.2.2 - 编写和运行actor

了解如何使用.NET SDK编写和运行actor

编写actor

ActorHost

ActorHost

  • 是所有actor构造函数所需的参数
  • 由运行时提供的
  • 必须传递给基类的构造函数
  • 包含允许该actor实例与运行时通信的所有状态信息
internal class MyActor : Actor, IMyActor, IRemindable
{
    public MyActor(ActorHost host) // 在构造函数中接收ActorHost
        : base(host) // 将ActorHost传递给基类的构造函数
    {
    }
}

由于ActorHost包含actor特有的状态信息,您不需要将其实例传递给代码的其他部分。建议仅在测试中创建您自己的ActorHost实例。

依赖注入

actor支持通过依赖注入将额外的参数传递到构造函数中。您定义的任何其他参数都将从依赖注入容器中获取其值。

internal class MyActor : Actor, IMyActor, IRemindable
{
    public MyActor(ActorHost host, BankService bank) // 在构造函数中接收BankService
        : base(host)
    {
        ...
    }
}

一个actor类型应该只有一个public构造函数。actor系统使用ActivatorUtilities模式来创建actor实例。

您可以在Startup.cs中注册类型以进行依赖注入以使其可用。阅读更多关于注册类型的不同方法

// 在Startup.cs中
public void ConfigureServices(IServiceCollection services)
{
    ...

    // 使用依赖注入注册额外的类型。
    services.AddSingleton<BankService>();
}

每个actor实例都有其自己的依赖注入范围,并在执行操作后在内存中保留一段时间。在此期间,与actor关联的依赖注入范围也被视为活动状态。该范围将在actor被停用时释放。

如果actor在构造函数中注入IServiceProvider,actor将接收到与其范围关联的IServiceProvider的引用。IServiceProvider可以用于将来动态解析服务。

internal class MyActor : Actor, IMyActor, IRemindable
{
    public MyActor(ActorHost host, IServiceProvider services) // 在构造函数中接收IServiceProvider
        : base(host)
    {
        ...
    }
}

使用此模式时,避免创建许多实现IDisposable瞬态服务。由于与actor关联的范围可能被视为有效时间较长,您可能会在内存中积累许多服务。有关更多信息,请参阅依赖注入指南

IDisposable和actor

actor可以实现IDisposableIAsyncDisposable。建议您依赖依赖注入进行资源管理,而不是在应用程序代码中实现释放功能。仅在确实必要的情况下提供释放支持。

日志记录

在actor类内部,您可以通过基类Actor上的属性访问ILogger实例。此实例连接到ASP.NET Core日志系统,应该用于actor内部的所有日志记录。阅读更多关于日志记录。您可以配置各种不同的日志格式和输出接收器。

使用_结构化日志记录_和_命名占位符_,如下例所示:

public Task<MyData> GetDataAsync()
{
    this.Logger.LogInformation("获取状态时间为 {CurrentTime}", DateTime.UtcNow);
    return this.StateManager.GetStateAsync<MyData>("my_data");
}

记录日志时,避免使用格式字符串,如:$"获取状态时间为 {DateTime.UtcNow}"

日志记录应使用命名占位符语法,这提供了更好的性能和与日志系统的集成。

使用显式actor类型名称

默认情况下,客户端看到的actor的_类型_是从actor实现类的_名称_派生的。默认名称将是类名(不包括命名空间)。

如果需要,您可以通过将ActorAttribute属性附加到actor实现类来指定显式类型名称。

[Actor(TypeName = "MyCustomActorTypeName")]
internal class MyActor : Actor, IMyActor
{
    // ...
}

在上面的例子中,名称将是MyCustomActorTypeName

无需更改注册actor类型与运行时的代码,通过属性提供值是唯一需要的。

在服务器上托管actor

注册actor

actor注册是Startup.csConfigureServices的一部分。您可以通过ConfigureServices方法使用依赖注入注册服务。注册actor类型集是actor服务注册的一部分。

ConfigureServices中,您可以:

  • 注册actor运行时(AddActors
  • 注册actor类型(options.Actors.RegisterActor<>
  • 配置actor运行时设置options
  • 注册额外的服务类型以进行actor的依赖注入(services
// 在Startup.cs中
public void ConfigureServices(IServiceCollection services)
{
    // 使用DI注册actor运行时
    services.AddActors(options =>
    {
        // 注册actor类型并配置actor设置
        options.Actors.RegisterActor<MyActor>();
        
        // 配置默认设置
        options.ActorIdleTimeout = TimeSpan.FromMinutes(10);
        options.ActorScanInterval = TimeSpan.FromSeconds(35);
        options.DrainOngoingCallTimeout = TimeSpan.FromSeconds(35);
        options.DrainRebalancedActors = true;
    });

    // 注册额外的服务以供actor使用
    services.AddSingleton<BankService>();
}

配置JSON选项

actor运行时使用System.Text.Json进行:

  • 将数据序列化到状态存储
  • 处理来自弱类型客户端的请求

默认情况下,actor运行时使用基于JsonSerializerDefaults.Web的设置。

您可以在ConfigureServices中配置JsonSerializerOptions

// 在Startup.cs中
public void ConfigureServices(IServiceCollection services)
{
    services.AddActors(options =>
    {
        ...
        
        // 自定义JSON选项
        options.JsonSerializerOptions = ...
    });
}

actor和路由

ASP.NET Core对actor的托管支持使用端点路由系统。.NET SDK不支持使用早期ASP.NET Core版本的传统路由系统托管actor。

由于actor使用端点路由,actor的HTTP处理程序是中间件管道的一部分。以下是设置包含actor的中间件管道的Configure方法的最小示例。

// 在Startup.cs中
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        // 注册与Dapr运行时接口的actor处理程序。
        endpoints.MapActorsHandlers();
    });
}

UseRoutingUseEndpoints调用是配置路由所必需的。通过在端点中间件中添加MapActorsHandlers将actor配置为管道的一部分。

这是一个最小示例,actor功能可以与以下内容共存:

  • 控制器
  • Razor页面
  • Blazor
  • gRPC服务
  • Dapr pub/sub处理程序
  • 其他端点,如健康检查

问题中间件

某些中间件可能会干扰Dapr请求到actor处理程序的路由。特别是,UseHttpsRedirection对于Dapr的默认配置是有问题的。Dapr默认通过未加密的HTTP发送请求,这将被UseHttpsRedirection中间件阻止。此中间件目前不能与Dapr一起使用。

// 在Startup.cs中
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    // 无效 - 这将阻止非HTTPS请求
    app.UseHttpsRedirection();
    // 无效 - 这将阻止非HTTPS请求

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        // 注册与Dapr运行时接口的actor处理程序。
        endpoints.MapActorsHandlers();
    });
}

下一步

尝试运行和使用虚拟actor示例

3.1.2.3 - .NET SDK 中的 Actor 序列化

使用 .NET 中的远程 Actor 序列化类型的必要步骤

Actor 序列化

Dapr actor 包使您能够在 .NET 应用程序中使用 Dapr 虚拟 actor,您可以选择使用弱类型或强类型客户端。每种方式都有不同的序列化方法。本文档将回顾这些差异,并传达一些在任一场景中需要理解的关键基本规则。

请注意,由于序列化方法的不同,弱类型和强类型 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 默认选项,除非通过自定义选项配置覆盖,这意味着将应用以下内容:

  • 属性名称的反序列化以不区分大小写的方式进行
  • 属性名称的序列化使用 驼峰命名法,除非属性被 [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 state 的类型,这不是必需的,因为您不打算独立于 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>,因此您将枚举的名称保存为值而不是数字值,但如果您更改枚举名称,这将引入破坏性更改,因为名称将不再与 state 中的内容匹配。

请注意,如果您选择使用此方法,您应该为所有枚举成员装饰 [EnumMeber] 属性,以便为每个枚举值一致地应用值,而不是随意地。没有任何东西会在构建或运行时验证这一点,但这被认为是最佳实践操作。

在这种情况下,如何在仍然更改枚举成员名称的同时指定持久化的精确值?使用自定义 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] 属性中的值,为我们提供了一种机制来重构我们的应用程序,而无需为 state 中现有的枚举值制定复杂的版本控制方案。

{"event":  "Conference",  "season":  "fall"}

强类型 Dapr Actor 客户端

在本节中,您将学习如何配置类和记录,以便在使用强类型 actor 客户端时,它们在运行时能够正确序列化和反序列化。这些客户端是使用 .NET 接口实现的,并且与使用其他语言编写的 Dapr actor 兼容。

此 actor 客户端使用称为 数据契约序列化器 的引擎序列化数据,该引擎将您的 C# 类型转换为 XML 文档。此序列化框架并不是 Dapr 特有的,而是由 .NET 团队在 .NET GitHub 仓库 中单独维护的。

在发送或接收原始类型(如字符串或整数)时,此序列化会透明地进行,您无需进行任何准备。然而,当处理您创建的复杂类型时,有一些重要规则需要考虑,以便此过程顺利进行。

可序列化类型

使用数据契约序列化器时需要牢记几个重要注意事项:

  • 默认情况下,所有类型、读/写属性(构造后)和标记为公开可见的字段都会被序列化
  • 所有类型必须公开一个无参数构造函数或用 DataContractAttribute 属性装饰
  • 仅在使用 DataContractAttribute 属性时支持仅初始化的设置器
  • 只读字段、没有 Get 和 Set 方法的属性以及具有私有 Get 和 Set 方法的内部或属性在序列化期间会被忽略
  • 通过使用 KnownTypesAttribute 属性,支持使用其他复杂类型的类型的序列化,这些复杂类型本身未标记为 DataContractAttribute 属性
  • 如果类型标记为 DataContractAttribute 属性,则您希望序列化和反序列化的所有成员也必须用 DataMemberAttribute 属性装饰,否则它们将被设置为默认值

反序列化如何工作?

反序列化使用的方法取决于类型是否用 DataContractAttribute 属性装饰。如果没有此属性,则使用无参数构造函数创建类型的实例。然后使用各自的设置器将每个属性和字段映射到类型中,并将实例返回给调用者。

如果类型标记为 [DataContract],则序列化器会使用反射读取类型的元数据,并根据它们是否标记为 DataMemberAttribute 属性来确定应包含哪些属性或字段,因为这是基于选择加入的。然后在内存中分配一个未初始化的对象(避免使用任何构造函数,无论是否有参数),然后直接在每个映射的属性或字段上设置值,即使是私有的或使用仅初始化的设置器。在整个过程中会根据需要调用序列化回调,然后将对象返回给调用者。

强烈建议使用序列化属性,因为它们提供了更多灵活性来覆盖名称和命名空间,并且通常使用更多现代 C# 功能。虽然默认序列化器可以依赖于原始类型,但不建议用于您自己的任何类型,无论它们是类、结构还是记录。建议如果您用 DataContractAttribute 属性装饰类型,还要显式装饰您希望序列化或反序列化的每个成员的 DataMemberAttribute 属性。

.NET 类

只要遵循本页和 数据契约序列化器 文档中详细说明的其他规则,类在数据契约序列化器中是完全支持的。

这里最重要的是要记住,您必须要么有一个公共无参数构造函数,要么用适当的属性装饰它。让我们通过一些示例来真正澄清什么会起作用,什么不会。

在以下示例中,我们展示了一个名为 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>

所以让我们调整一下——让我们添加我们自己的构造函数,并仅在成员上使用仅初始化的设置器。这将无法正确序列化和反序列化,不是因为使用了仅初始化的设置器,而是因为缺少无参数构造函数。

// 无法正确序列化!
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 属性装饰了类型,这意味着我可以在属性上使用仅初始化的设置器。

[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; } 
}

以及使用我们的序列化属性(再次选择仅初始化的设置器,因为我们使用的是序列化属性):

[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 结构体

只要它们标记为 DataContractAttribute 属性,并且您希望序列化的成员标记为 DataMemberAttribute 属性,结构体就可以被数据契约序列化器支持。此外,为了支持反序列化,结构体还需要有一个无参数构造函数。即使您定义了自己的无参数构造函数(在 C# 10 中启用),这也能正常工作。

[DataContract]
public struct Doodad
{
    [DataMember]
    public int Count { get; set; }
}

.NET 记录

记录是在 C# 9 中引入的,在序列化方面遵循与类完全相同的规则。我们建议您应该用 DataContractAttribute 属性装饰所有记录,并用 DataMemberAttribute 属性装饰您希望序列化的成员,以便在使用此或其他较新的 C# 功能时不会遇到反序列化问题。因为记录类默认使用仅初始化的设置器来设置属性,并鼓励使用主构造函数,所以将这些属性应用于您的类型可以确保序列化器能够正确处理您的类型。

通常,记录以使用新主构造函数概念的简单单行语句呈现:

public record Doodad(Guid Id, string Name, int Count);

这将抛出一个错误,鼓励使用序列化属性,因为在 Dapr actor 方法调用中使用它时没有可用的无参数构造函数,也没有用上述属性装饰。

在这里,我们添加了一个显式的无参数构造函数,它不会抛出错误,但在反序列化期间不会设置任何值,因为它们是使用仅初始化的设置器创建的。因为这没有使用 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/en-us/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))],这告诉数据契约序列化器 DerivedClassBaseClass 的可能实现,它可能需要序列化或反序列化。如果没有此属性,当序列化器遇到一个实际上是 DerivedClass 类型的 BaseClass 实例时,它将不知道如何处理派生类型,这可能导致序列化异常。通过将所有可能的派生类型指定为已知类型,您可以确保序列化器能够正确处理类型及其成员。

有关使用 [KnownType] 的更多信息和示例,请参阅官方文档

3.1.2.4 - 如何:在 .NET SDK 中运行和使用虚拟 actor

通过此示例尝试 .NET Dapr 虚拟 actor

Dapr actor 包使您能够从 .NET 应用程序中与 Dapr 虚拟 actor 交互。在本指南中,您将学习如何:

  • 创建一个 actor (MyActor)。
  • 在客户端应用程序上调用其方法。
MyActor --- MyActor.Interfaces
         |
         +- MyActorService
         |
         +- MyActorClient

接口项目 (\MyActor\MyActor.Interfaces)

此项目包含 actor 的接口定义。actor 接口可以在任何项目中定义,名称不限。接口定义了 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 客户端的实现,该客户端调用在 actor 接口中定义的 MyActor 的方法。

准备工作

步骤 0:准备

我们将创建 3 个项目,请选择一个空目录开始,并在您选择的终端中打开它。

步骤 1:创建 actor 接口

actor 接口定义了 actor 实现和调用 actor 的客户端共享的 actor 合约。

actor 接口定义如下要求:

  • actor 接口必须继承 Dapr.Actors.IActor 接口
  • actor 方法的返回类型必须是 TaskTask<object>
  • actor 方法最多可以有一个参数

创建接口项目并添加依赖项

# 创建 actor 接口
dotnet new classlib -o MyActor.Interfaces

cd MyActor.Interfaces

# 添加 Dapr.Actors nuget 包。请使用 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 运行时。

创建 actor 服务项目并添加依赖项

# 创建 ASP.Net Web 服务以托管 Dapr actor
dotnet new web -o MyActorService

cd MyActorService

# 添加 Dapr.Actors.AspNetCore nuget 包。请使用 nuget.org 上的最新包版本
dotnet add package Dapr.Actors.AspNetCore

# 添加 actor 接口引用
dotnet add reference ../MyActor.Interfaces/MyActor.Interfaces.csproj

cd ..

添加 actor 实现

实现 IMyActor 接口并从 Dapr.Actors.Actor 类派生。以下示例还展示了如何使用 actor reminder。对于使用 reminder 的 actor,它必须从 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
    {
        // 构造函数必须接受 ActorHost 作为参数,并且还可以接受将从依赖注入容器中检索的其他参数
        //
        /// <summary>
        /// 初始化 MyActor 的新实例
        /// </summary>
        /// <param name="host">将托管此 actor 实例的 Dapr.Actors.Runtime.ActorHost。</param>
        public MyActor(ActorHost host)
            : base(host)
        {
        }

        /// <summary>
        /// 每当 actor 被激活时调用此方法。
        /// actor 在其任何方法首次被调用时被激活。
        /// </summary>
        protected override Task OnActivateAsync()
        {
            // 提供执行一些可选设置的机会。
            Console.WriteLine($"Activating actor id: {this.Id}");
            return Task.CompletedTask;
        }

        /// <summary>
        /// 每当 actor 在一段时间不活动后被停用时调用此方法。
        /// </summary>
        protected override Task OnDeactivateAsync()
        {
            // 提供执行可选清理的机会。
            Console.WriteLine($"Deactivating actor id: {this.Id}");
            return Task.CompletedTask;
        }

        /// <summary>
        /// 将 MyData 设置到 actor 的私有状态存储中
        /// </summary>
        /// <param name="data">用户定义的 MyData,将作为 "my_data" 状态存储到状态存储中</param>
        public async Task<string> SetDataAsync(MyData data)
        {
            // 数据在每次方法执行后由 actor 的运行时隐式保存到配置的状态存储中。
            // 数据也可以通过调用 this.StateManager.SaveStateAsync() 显式保存。
            // 要保存的状态必须是 DataContract 可序列化的。
            await this.StateManager.SetStateAsync<MyData>(
                "my_data",  // 状态名称
                data);      // 为命名状态 "my_data" 保存的数据

            return "Success";
        }

        /// <summary>
        /// 从 actor 的私有状态存储中获取 MyData
        /// </summary>
        /// <return>存储到状态存储中的用户定义的 MyData,作为 "my_data" 状态</return>
        public Task<MyData> GetDataAsync()
        {
            // 从状态存储中获取状态。
            return this.StateManager.GetStateAsync<MyData>("my_data");
        }

        /// <summary>
        /// 向 actor 注册 MyReminder reminder
        /// </summary>
        public async Task RegisterReminder()
        {
            await this.RegisterReminderAsync(
                "MyReminder",              // reminder 的名称
                null,                      // 传递给 IRemindable.ReceiveReminderAsync() 的用户状态
                TimeSpan.FromSeconds(5),   // 在首次调用 reminder 之前的延迟时间
                TimeSpan.FromSeconds(5));  // 在首次调用后 reminder 调用之间的时间间隔
        }

        /// <summary>
        /// 获取 actor 的 MyReminder reminder 详细信息
        /// </summary>
        public async Task<IActorReminder> GetReminder()
        {
            await this.GetReminderAsync("MyReminder");
        }

        /// <summary>
        /// 取消注册 actor 的 MyReminder reminder
        /// </summary>
        public Task UnregisterReminder()
        {
            Console.WriteLine("Unregistering MyReminder...");
            return this.UnregisterReminderAsync("MyReminder");
        }

        // <summary>
        // 实现 IRemindeable.ReceiveReminderAsync(),这是在 actor reminder 触发时调用的回调。
        // </summary>
        public Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period)
        {
            Console.WriteLine("ReceiveReminderAsync is called!");
            return Task.CompletedTask;
        }

        /// <summary>
        /// 向 actor 注册 MyTimer timer
        /// </summary>
        public Task RegisterTimer()
        {
            return this.RegisterTimerAsync(
                "MyTimer",                  // timer 的名称
                nameof(this.OnTimerCallBack),       // timer 回调
                null,                       // 传递给 OnTimerCallback() 的用户状态
                TimeSpan.FromSeconds(5),    // 在首次调用异步回调之前的延迟时间
                TimeSpan.FromSeconds(5));   // 异步回调调用之间的时间间隔
        }

        /// <summary>
        /// 取消注册 actor 的 MyTimer timer
        /// </summary>
        public Task UnregisterTimer()
        {
            Console.WriteLine("Unregistering MyTimer...");
            return this.UnregisterTimerAsync("MyTimer");
        }

        /// <summary>
        /// timer 到期后调用的回调
        /// </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 类型的构造函数中。

actor 是通过与 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 =>
            {
                // 注册 actor 类型并配置 actor 设置
                options.Actors.RegisterActor<MyActor>();
            });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            // 注册与 Dapr 运行时接口的 actor 处理程序。
            app.MapActorsHandlers();
        }
    }
}

步骤 3:添加客户端

创建一个简单的控制台应用程序来调用 actor 服务。Dapr SDK 提供 actor 代理客户端来调用 actor 接口中定义的 actor 方法。

创建 actor 客户端项目并添加依赖项

# 创建 actor 的客户端
dotnet new console -o MyActorClient

cd MyActorClient

# 添加 Dapr.Actors nuget 包。请使用 nuget.org 上的最新包版本
dotnet add package Dapr.Actors

# 添加 actor 接口引用
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...");

            // 在 actor 服务中注册的 actor 类型
            var actorType = "MyActor";

            // ActorId 唯一标识一个 actor 实例
            // 如果与此 id 匹配的 actor 不存在,将会创建它
            var actorId = new ActorId("1");

            // 使用服务实现的相同接口创建本地代理。
            //
            // 您需要提供类型和 id,以便可以定位 actor。
            var proxy = ActorProxy.Create<IMyActor>(actorId, actorType);

            // 现在您可以使用 actor 接口调用 actor 的方法。
            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}");
        }
    }
}

运行代码

您创建的项目现在可以测试示例。

  1. 运行 MyActorService

    由于 MyActorService 托管 actor,因此需要使用 Dapr CLI 运行。

    cd MyActorService
    dapr run --app-id myapp --app-port 5000 --dapr-http-port 3500 -- dotnet run
    

    您将在此终端中看到来自 daprdMyActorService 的命令行输出。您应该看到类似以下内容的内容,这表明应用程序已成功启动。

    ...
    ℹ️  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
    
  2. 运行 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,这被传递给 dapr run 作为 --app-port 5000。Dapr sidecar 的默认 HTTP 端口是 3500。我们告诉 MyActorService 的 sidecar 使用 3500,以便 MyActorClient 可以依赖默认值。

现在您已成功创建了一个 actor 服务和客户端。请参阅相关链接部分以了解更多信息。

相关链接

3.1.3 - Dapr Workflow .NET SDK

快速上手并掌握 Dapr Workflow 和 Dapr .NET SDK 的使用

3.1.3.1 - DaprWorkflowClient 使用

使用 DaprWorkflowClient 的基本提示和建议

生命周期管理

DaprWorkflowClient 可以访问网络资源,这些资源通过 TCP 套接字与 Dapr sidecar 以及其他用于管理和操作工作流的类型进行通信。DaprWorkflowClient 实现了 IAsyncDisposable 接口,以便快速清理资源。

依赖注入

AddDaprWorkflow() 方法用于通过 ASP.NET Core 的依赖注入机制注册 Dapr 工作流服务。此方法需要一个选项委托,用于定义您希望在应用程序中注册和使用的每个工作流和活动。

单例注册

默认情况下,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();

将服务注入到工作流活动中

工作流活动支持现代 C# 应用程序中常用的依赖注入。假设在启动时进行了适当的注册,任何此类类型都可以注入到工作流活动的构造函数中,并在工作流执行期间使用。这使得通过注入的 ILogger 添加日志记录或通过注入 DaprClientDaprJobsClient 访问其他 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);
    }
}

在工作流中使用 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);
        
        //...
    }
}

3.1.3.2 - 如何:在 .NET SDK 中编写和管理 Dapr 工作流

学习如何使用 .NET SDK 编写和管理 Dapr 工作流

我们来创建一个 Dapr 工作流并通过控制台调用它。在提供的订单处理工作流示例中,控制台会提示如何进行购买和补货。在本指南中,您将:

  • 部署一个 .NET 控制台应用程序 (WorkflowConsoleApp)。
  • 使用 .NET 工作流 SDK 和 API 调用来启动和查询工作流实例。

在 .NET 示例项目里:

先决条件

设置环境

克隆 .NET SDK 仓库

git clone https://github.com/dapr/dotnet-sdk.git

从 .NET SDK 根目录,导航到 Dapr 工作流示例。

cd examples/Workflow

本地运行应用程序

要运行 Dapr 应用程序,您需要启动 .NET 程序和一个 Dapr sidecar。导航到 WorkflowConsoleApp 目录。

cd WorkflowConsoleApp

启动程序。

dotnet run

在一个新的终端中,再次导航到 WorkflowConsoleApp 目录,并在程序旁边运行 Dapr sidecar。

dapr run --app-id wfapp --dapr-grpc-port 4001 --dapr-http-port 3500

Dapr 会监听 HTTP 请求在 http://localhost:3500 和内部工作流 gRPC 请求在 http://localhost:4001

启动工作流

要启动工作流,您有两种选择:

  1. 按照控制台提示的指示。
  2. 使用工作流 API 并直接向 Dapr 发送请求。

本指南重点介绍工作流 API 选项。

运行以下命令以启动工作流。

curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 \
  -H "Content-Type: application/json" \
  -d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}'
curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 `
  -H "Content-Type: application/json" `
  -d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}'

如果成功,您应该会看到如下响应:

{"instanceID":"12345678"}

发送 HTTP 请求以获取已启动工作流的状态:

curl -i -X GET http://localhost:3500/v1.0/workflows/dapr/12345678

工作流设计为需要几秒钟才能完成。如果在您发出 HTTP 请求时工作流尚未完成,您将看到以下 JSON 响应(为便于阅读而格式化),工作流状态为 RUNNING

{
  "instanceID": "12345678",
  "workflowName": "OrderProcessingWorkflow",
  "createdAt": "2023-05-10T00:42:03.911444105Z",
  "lastUpdatedAt": "2023-05-10T00:42:06.142214153Z",
  "runtimeStatus": "RUNNING",
  "properties": {
    "dapr.workflow.custom_status": "",
    "dapr.workflow.input": "{\"Name\": \"Paperclips\", \"TotalCost\": 99.95, \"Quantity\": 1}"
  }
}

一旦工作流完成运行,您应该会看到以下输出,表明它已达到 COMPLETED 状态:

{
  "instanceID": "12345678",
  "workflowName": "OrderProcessingWorkflow",
  "createdAt": "2023-05-10T00:42:03.911444105Z",
  "lastUpdatedAt": "2023-05-10T00:42:18.527704176Z",
  "runtimeStatus": "COMPLETED",
  "properties": {
    "dapr.workflow.custom_status": "",
    "dapr.workflow.input": "{\"Name\": \"Paperclips\", \"TotalCost\": 99.95, \"Quantity\": 1}",
    "dapr.workflow.output": "{\"Processed\":true}"
  }
}

当工作流完成时,工作流应用程序的标准输出应如下所示:

info: WorkflowConsoleApp.Activities.NotifyActivity[0]
      Received order 12345678 for Paperclips at $99.95
info: WorkflowConsoleApp.Activities.ReserveInventoryActivity[0]
      Reserving inventory: 12345678, Paperclips, 1
info: WorkflowConsoleApp.Activities.ProcessPaymentActivity[0]
      Processing payment: 12345678, 99.95, USD
info: WorkflowConsoleApp.Activities.NotifyActivity[0]
      Order 12345678 processed successfully!

如果您在本地机器上为 Dapr 配置了 Zipkin,那么您可以在 Zipkin Web UI(通常在 http://localhost:9411/zipkin/)中查看工作流跟踪跨度。

演示

观看此视频演示 .NET 工作流

下一步

3.1.4 - Dapr AI .NET SDK

快速上手使用 Dapr AI .NET SDK

使用 Dapr AI 包,您可以从 .NET 应用程序与 Dapr AI 工作负载进行交互。

目前,Dapr 提供了一个会话 API,用于与大型语言模型进行交互。要开始使用此功能,请参阅 Dapr 会话 AI 指南。

3.1.4.1 - Dapr AI 客户端

学习如何创建 Dapr AI 客户端

Dapr AI 客户端包使您能够与 Dapr sidecar 提供的 AI 功能进行交互。

生命周期的管理

DaprConversationClient 是专门用于与 Dapr conversation API 交互的客户端版本。它可以与 DaprClient 和其他 Dapr 客户端一起注册而不会出现问题。

它通过 TCP 套接字与 Dapr sidecar 通信,以便访问网络资源。

为了获得最佳性能,建议创建一个长期存在的 DaprConversationClient 实例,并在整个应用程序中共享使用。DaprConversationClient 实例是线程安全的,适合共享。

这可以通过依赖注入来实现。注册方法支持以单例、作用域实例或瞬态(每次注入时重新创建)的方式进行注册,但也可以利用 IConfiguration 或其他注入服务中的值进行注册,这在每个类中从头创建客户端时是不切实际的。

避免为每个操作都创建一个新的 DaprConversationClient

通过 DaprConversationClientBuilder 配置 DaprConversationClient

可以通过在 DaprConversationClientBuilder 类上调用方法来配置 DaprConversationClient,然后调用 .Build() 来创建客户端。每个 DaprConversationClient 的设置是独立的,并且在调用 .Build() 后无法更改。

var daprConversationClient = new DaprConversationClientBuilder()
    .UseDaprApiToken("abc123") // 指定用于验证到其他 Dapr sidecar 的 API 令牌
    .Build();

DaprConversationClientBuilder 包含以下设置:

  • Dapr sidecar 的 HTTP 端点
  • Dapr sidecar 的 gRPC 端点
  • 用于配置 JSON 序列化的 JsonSerializerOptions 对象
  • 用于配置 gRPC 的 GrpcChannelOptions 对象
  • 用于验证请求到 sidecar 的 API 令牌
  • 用于创建 SDK 使用的 HttpClient 实例的工厂方法
  • 用于在向 sidecar 发出请求时使用的 HttpClient 实例的超时

SDK 将读取以下环境变量来配置默认值:

  • DAPR_HTTP_ENDPOINT:用于查找 Dapr sidecar 的 HTTP 端点,例如:https://dapr-api.mycompany.com
  • DAPR_GRPC_ENDPOINT:用于查找 Dapr sidecar 的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.com
  • DAPR_HTTP_PORT:如果未设置 DAPR_HTTP_ENDPOINT,则用于查找 Dapr sidecar 的本地 HTTP 端点
  • DAPR_GRPC_PORT:如果未设置 DAPR_GRPC_ENDPOINT,则用于查找 Dapr sidecar 的本地 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();

有时,开发人员需要使用上面详细介绍的各种配置选项来配置创建的客户端。这是通过传入 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);

// 注册一个虚构的服务,从某处检索 secret
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();

3.1.4.2 - 如何在 .NET SDK 中创建和使用 Dapr AI 会话

学习如何使用 .NET SDK 创建和使用 Dapr 会话 AI 客户端

前提条件

安装

要开始使用 Dapr AI .NET SDK 客户端,请从 NuGet 安装 Dapr.AI 包

dotnet add package Dapr.AI

DaprConversationClient 通过 TCP 套接字形式维护对网络资源的访问,用于与 Dapr sidecar 通信。

依赖注入

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 接口,以便从大型语言模型发送和接收消息。

发送消息

3.1.5 - Dapr Jobs .NET SDK

快速上手使用 Dapr Jobs 和 Dapr .NET SDK

使用 Dapr Job 包,您可以在 .NET 应用程序中与 Dapr Job API 进行交互。通过预设的计划,您可以安排未来的操作,并可选择附带数据。

要开始使用,请查看 Dapr Jobs 指南,并参考 最佳实践文档 以获取更多指导。

3.1.5.1 - 如何:在 .NET SDK 中编写和管理 Dapr 任务

学习如何使用 .NET SDK 编写和管理 Dapr 任务

我们来创建一个端点,该端点将在 Dapr 任务触发时被调用,然后在同一个应用中调度该任务。我们将使用此处提供的简单示例,进行以下演示,并通过它来解释如何使用间隔或 Cron 表达式自行调度一次性或重复性任务。在本指南中,您将:

  • 部署一个 .NET Web API 应用程序 (JobsSample)
  • 利用 Dapr .NET 任务 SDK 调度任务调用并设置被触发的端点

在 .NET 示例项目中:

  • 主要的 Program.cs 文件是整个演示的核心。

前提条件

设置环境

克隆 .NET SDK 仓库

git clone https://github.com/dapr/dotnet-sdk.git

从 .NET SDK 根目录,导航到 Dapr 任务示例。

cd examples/Jobs

本地运行应用程序

要运行 Dapr 应用程序,您需要启动 .NET 程序和一个 Dapr sidecar。导航到 JobsSample 目录。

cd JobsSample

我们将运行一个命令,同时启动 Dapr sidecar 和 .NET 程序。

dapr run --app-id jobsapp --dapr-grpc-port 4001 --dapr-http-port 3500 -- dotnet run

Dapr 监听 HTTP 请求在 http://localhost:3500 和内部任务 gRPC 请求在 http://localhost:4001

使用依赖注入注册 Dapr 任务客户端

Dapr 任务 SDK 提供了一个扩展方法来简化 Dapr 任务客户端的注册。在 Program.cs 中完成依赖注入注册之前,添加以下行:

var builder = WebApplication.CreateBuilder(args);

//在这两行之间的任意位置添加
builder.Services.AddDaprJobsClient(); //这样就完成了

var app = builder.Build();

请注意,在当前的任务 API 实现中,调度任务的应用也将是接收触发通知的应用。换句话说,您不能调度一个触发器在另一个应用中运行。因此,虽然您不需要在应用中显式注册 Dapr 任务客户端来调度触发调用端点,但如果没有同一个应用以某种方式调度任务(无论是通过此 Dapr 任务 .NET SDK 还是对 sidecar 的 HTTP 调用),您的端点将永远不会被调用。

您可能希望为 Dapr 任务客户端提供一些配置选项,这些选项应在每次调用 sidecar 时存在,例如 Dapr API 令牌,或者您希望使用非标准的 HTTP 或 gRPC 端点。这可以通过使用允许配置 DaprJobsClientBuilder 实例的注册方法重载来实现:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprJobsClient((_, daprJobsClientBuilder) =>
{
    daprJobsClientBuilder.UseDaprApiToken("abc123");
    daprJobsClientBuilder.UseHttpEndpoint("http://localhost:8512"); //非标准 sidecar HTTP 端点
});

var app = builder.Build();

如果您需要从其他来源检索注入的值,这些来源本身注册为依赖项,您可以使用另一个重载来将 IServiceProvider 注入到配置操作方法中。在以下示例中,我们注册了一个虚构的单例,可以从某处检索 secret,并将其传递到 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 任务客户端

可以使用注册的 IConfiguration 中的值来配置 Dapr 任务客户端,而无需显式指定每个值的重写,如前一节中使用 DaprJobsClientBuilder 所示。相反,通过填充通过依赖注入提供的 IConfigurationAddDaprJobsClient() 注册将自动使用这些值覆盖其各自的默认值。

首先在您的配置中填充值。这可以通过以下示例中的几种不同方式完成。

通过 ConfigurationBuilder 配置

应用程序设置可以在不使用配置源的情况下配置,而是通过填充 ConfigurationBuilder 实例中的值来实现:

var builder = WebApplication.CreateBuilder();

//创建配置
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 任务客户端的 HTTP 端点和 API 令牌。

KeyValue
DAPR_HTTP_ENDPOINThttp://localhost:54321
DAPR_API_TOKENabc123
var builder = WebApplication.CreateBuilder();

builder.Configuration.AddEnvironmentVariables();
builder.Services.AddDaprJobsClient();

Dapr 任务客户端将被配置为使用 HTTP 端点 http://localhost:54321 并用 API 令牌头 abc123 填充所有出站请求。

通过前缀环境变量配置

然而,在共享主机场景中,多个应用程序都在同一台机器上运行而不使用容器或在开发环境中,前缀环境变量并不罕见。以下示例假设 HTTP 端点和 API 令牌都将从前缀为 “myapp_” 的环境变量中提取。在此场景中使用的两个环境变量如下:

KeyValue
myapp_DAPR_HTTP_ENDPOINThttp://localhost:54321
myapp_DAPR_API_TOKENabc123

这些环境变量将在以下示例中加载到注册的配置中,并在没有附加前缀的情况下提供。

var builder = WebApplication.CreateBuilder();

builder.Configuration.AddEnvironmentVariables(prefix: "myapp_");
builder.Services.AddDaprJobsClient();

Dapr 任务客户端将被配置为使用 HTTP 端点 http://localhost:54321 并用 API 令牌头 abc123 填充所有出站请求。

不依赖于依赖注入使用 Dapr 任务客户端

虽然使用依赖注入简化了 .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 请求映射的方式配置应用程序。实现为扩展方法,传递它应该响应的任务名称和一个委托。服务可以根据需要注入到委托的参数中,您可以选择传递 JobDetails 以获取有关已触发任务的信息(例如,访问其调度设置或负载)。

这里有两个委托可以使用。一个提供 IServiceProvider 以防您需要将其他服务注入处理程序:

//我们从上面的示例中得到了这个
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprJobsClient();

var app = builder.Build();

//添加我们的端点注册
app.MapDaprScheduledJob("myJob", (IServiceProvider serviceProvider, string? jobName, JobDetails? jobDetails) => {
    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, JobDetails? jobDetails) => {
    //做一些事情...
});

app.Run();

注册任务

最后,我们必须注册我们想要调度的任务。请注意,从这里开始,所有 SDK 方法都支持取消令牌,并在未设置时使用默认令牌。

有三种不同的方式来设置任务,具体取决于您想要如何配置调度:

一次性任务

一次性任务就是这样;它将在某个时间点运行,并且不会重复。这种方法要求您选择一个任务名称并指定一个触发时间。

参数名称类型描述必需
jobNamestring正在调度的任务的名称。
scheduledTimeDateTime任务应运行的时间点。
payloadReadOnlyMemory触发时提供给调用端点的任务数据。
cancellationTokenCancellationToken用于提前取消操作,例如由于操作超时。

可以从 Dapr 任务客户端调度一次性任务,如以下示例所示:

public class MyOperation(DaprJobsClient daprJobsClient)
{
    public async Task ScheduleOneTimeJobAsync(CancellationToken cancellationToken)
    {
        var today = DateTime.UtcNow;
        var threeDaysFromNow = today.AddDays(3);

        await daprJobsClient.ScheduleOneTimeJobAsync("myJobName", threeDaysFromNow, cancellationToken: cancellationToken);
    }
}

基于间隔的任务

基于间隔的任务是一个在配置为固定时间量的循环中运行的任务,不像今天在 actor 构建块中工作的提醒。这些任务也可以通过许多可选参数进行调度:

参数名称类型描述必需
jobNamestring正在调度的任务的名称。
intervalTimeSpan任务应触发的间隔。
startingFromDateTime任务调度应开始的时间点。
repeatsint任务应触发的最大次数。
ttl任务何时过期且不再触发。
payloadReadOnlyMemory触发时提供给调用端点的任务数据。
cancellationTokenCancellationToken用于提前取消操作,例如由于操作超时。

可以从 Dapr 任务客户端调度基于间隔的任务,如以下示例所示:

public class MyOperation(DaprJobsClient daprJobsClient)
{

    public async Task ScheduleIntervalJobAsync(CancellationToken cancellationToken)
    {
        var hourlyInterval = TimeSpan.FromHours(1);

        //每小时触发任务,但最多触发 5 次
        await daprJobsClient.ScheduleIntervalJobAsync("myJobName", hourlyInterval, repeats: 5), cancellationToken: cancellationToken;
    }
}

基于 Cron 的任务

基于 Cron 的任务是使用 Cron 表达式调度的。这提供了更多基于日历的控制,以便在任务触发时使用日历值在表达式中。与其他选项一样,这些任务也可以通过许多可选参数进行调度:

参数名称类型描述必需
jobNamestring正在调度的任务的名称。
cronExpressionstring指示任务应触发的 systemd 类似 Cron 表达式。
startingFromDateTime任务调度应开始的时间点。
repeatsint任务应触发的最大次数。
ttl任务何时过期且不再触发。
payloadReadOnlyMemory触发时提供给调用端点的任务数据。
cancellationTokenCancellationToken用于提前取消操作,例如由于操作超时。

可以从 Dapr 任务客户端调度基于 Cron 的任务,如下所示:

public class MyOperation(DaprJobsClient daprJobsClient)
{
    public async Task ScheduleCronJobAsync(CancellationToken cancellationToken)
    {
        //在每个月的第五天的每隔一小时的顶部
        const string cronSchedule = "0 */2 5 * *";

        //直到下个月才开始
        var now = DateTime.UtcNow;
        var oneMonthFromNow = now.AddMonths(1);
        var firstOfNextMonth = new DateTime(oneMonthFromNow.Year, oneMonthFromNow.Month, 1, 0, 0, 0);

        //每小时触发任务,但最多触发 5 次
        await daprJobsClient.ScheduleCronJobAsync("myJobName", cronSchedule, dueTime: firstOfNextMonth, cancellationToken: cancellationToken);
    }
}

获取已调度任务的详细信息

如果您知道已调度任务的名称,您可以在不等待其触发的情况下检索其元数据。返回的 JobDetails 提供了一些有用的属性,用于从 Dapr 任务 API 消费信息:

  • 如果 Schedule 属性包含 Cron 表达式,则 IsCronExpression 属性将为 true,并且表达式也将在 CronExpression 属性中可用。
  • 如果 Schedule 属性包含持续时间值,则 IsIntervalExpression 属性将为 true,并且该值将转换为 TimeSpan 值,可从 Interval 属性访问。

这可以通过使用以下方法完成:

public class MyOperation(DaprJobsClient daprJobsClient)
{
    public async Task<JobDetails> GetJobDetailsAsync(string jobName, CancellationToken cancellationToken)
    {
        var jobDetails = await daprJobsClient.GetJobAsync(jobName, canecllationToken);
        return jobDetails;
    }
}

删除已调度的任务

要删除已调度的任务,您需要知道其名称。从那里开始,只需在 Dapr 任务客户端上调用 DeleteJobAsync 方法即可:

public class MyOperation(DaprJobsClient daprJobsClient)
{
    public async Task DeleteJobAsync(string jobName, CancellationToken cancellationToken)
    {
        await daprJobsClient.DeleteJobAsync(jobName, cancellationToken);
    }
}

3.1.5.2 - DaprJobsClient 使用指南

使用 DaprJobsClient 的基本技巧和建议

生命周期管理

DaprJobsClient 是专门用于与 Dapr Jobs API 交互的 Dapr 客户端。它可以与 DaprClient 和其他 Dapr 客户端一起注册而不会出现问题。

它通过 TCP 套接字与 Dapr sidecar 通信,并实现了 IDisposable 接口以便快速清理资源。

为了获得最佳性能,建议创建一个长生命周期的 DaprJobsClient 实例,并在整个应用程序中共享使用。DaprJobsClient 实例是线程安全的,适合在多个线程中共享。

可以通过依赖注入来实现这一点。注册方法支持以单例、作用域实例或瞬态方式注册,但也可以利用 IConfiguration 或其他注入服务中的值进行注册,这样就不需要在每个类中重新创建客户端。

避免为每个操作创建一个新的 DaprJobsClient 并在操作完成后销毁它。

通过 DaprJobsClientBuilder 配置 DaprJobsClient

可以通过 DaprJobsClientBuilder 类上的方法来配置 DaprJobsClient,然后调用 .Build() 来创建客户端。每个 DaprJobsClient 的设置是独立的,调用 .Build() 后无法更改。

var daprJobsClient = new DaprJobsClientBuilder()
    .UseDaprApiToken("abc123") // 指定用于验证到其他 Dapr sidecar 的 API 令牌
    .Build();

DaprJobsClientBuilder 包含以下设置:

  • Dapr sidecar 的 HTTP 端点
  • Dapr sidecar 的 gRPC 端点
  • 用于配置 JSON 序列化的 JsonSerializerOptions 对象
  • 用于配置 gRPC 的 GrpcChannelOptions 对象
  • 用于验证请求到 sidecar 的 API 令牌
  • 用于创建 SDK 使用的 HttpClient 实例的工厂方法
  • 用于在向 sidecar 发出请求时使用的 HttpClient 实例的超时

SDK 将读取以下环境变量来配置默认值:

  • DAPR_HTTP_ENDPOINT:用于查找 Dapr sidecar 的 HTTP 端点,例如:https://dapr-api.mycompany.com
  • DAPR_GRPC_ENDPOINT:用于查找 Dapr sidecar 的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.com
  • DAPR_HTTP_PORT:如果未设置 DAPR_HTTP_ENDPOINT,则用于查找 Dapr sidecar 的本地 HTTP 端点
  • DAPR_GRPC_PORT:如果未设置 DAPR_GRPC_ENDPOINT,则用于查找 Dapr sidecar 的本地 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(如果尚未注册),并在创建 HttpClient 实例时配置 DaprJobsClientBuilder 以尽可能多地重用相同的实例,避免套接字耗尽和其他问题。

在第一种方法中,开发人员没有进行任何配置,DaprJobsClient 使用默认设置进行配置。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprJobsClient(); //根据需要注册 `DaprJobsClient` 以进行注入
var app = builder.Build();

有时,开发人员需要使用上面详细介绍的各种配置选项来配置创建的客户端。这是通过传入 DaprJobsClientBuiler 并公开用于配置必要选项的方法的重载来完成的。

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);

//注册一个虚构的服务,从某处检索 secret
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 编码字节的数组。以下是调度一次性 job 时可能的示例:

var now = DateTime.UtcNow;
var oneWeekFromNow = now.AddDays(7);
await daprJobsClient.ScheduleOneTimeJobWithPayloadAsync("myOtherJob", oneWeekFromNow, "This is a test!");

JobDetails 类型将数据返回为 ReadOnlyMemory<byte>?,因此开发人员可以根据需要进行反序列化,但同样有两个包含的辅助扩展,可以将其反序列化为 JSON 兼容类型或字符串。这两种方法都假设开发人员对最初调度的 job 进行了编码(可能使用辅助序列化方法),因为这些方法不会强制字节表示它们不是的东西。

要将字节反序列化为字符串,可以使用以下辅助方法:

if (jobDetails.Payload is not null)
{
    string payloadAsString = jobDetails.Payload.DeserializeToString(); //如果成功,返回一个具有值的字符串
}

要将 JSON 编码的 UTF-8 字节反序列化为相应的类型,可以使用以下辅助方法。提供了一个重载参数,允许开发人员传入自己的 JsonSerializerOptions 以在反序列化期间应用。

public sealed record Doodad (string Name, int Value);

//...
if (jobDetails.Payload is not null)
{
    var deserializedDoodad = jobDetails.Payload.DeserializeFromJsonBytes<Doodad>();
}

错误处理

DaprJobsClient 上的方法如果在 SDK 和运行在 Dapr sidecar 上的 Jobs API 服务之间遇到问题,将抛出 DaprJobsServiceException。如果由于通过此 SDK 向 Jobs API 服务发出的请求格式不正确而遇到失败,将抛出 DaprMalformedJobException。在非法参数值的情况下,将抛出适当的标准异常(例如 ArgumentOutOfRangeExceptionArgumentNullException),并附上有问题的参数名称。对于其他任何情况,将抛出 DaprException

最常见的失败情况将与以下内容相关:

  • 在与 Jobs API 交互时参数格式不正确
  • 瞬态故障,例如网络问题
  • 无效数据,例如无法将值反序列化为其最初未从中序列化的类型

在任何这些情况下,您都可以通过 .InnerException 属性检查更多异常详细信息。

3.1.6 - Dapr Messaging .NET SDK

快速上手使用 Dapr Messaging .NET SDK

使用 Dapr Messaging 包,您可以在 .NET 应用程序中与 Dapr 消息 API 进行交互。在 v1.15 版本中,该包仅支持流式 pubsub 功能

未来的 Dapr .NET SDK 版本将会把现有的消息功能从 Dapr.Client 迁移到 Dapr.Messaging 包中。这一变更将在发布说明、文档和相关的技术说明中提前告知。

要开始使用,请查看 Dapr Messaging 指南,并参考最佳实践文档以获取更多指导。

3.1.6.1 - 如何:在 .NET SDK 中编写和管理 Dapr 流式订阅

学习如何使用 .NET SDK 编写和管理 Dapr 流式订阅

我们来创建一个使用流式功能的发布/订阅主题或队列的订阅。我们将使用此处提供的简单示例,进行演示,并逐步讲解如何在运行时配置消息处理程序,而无需预先配置端点。在本指南中,您将会学习如何:

前提条件

设置环境

克隆 .NET SDK 仓库

git clone https://github.com/dapr/dotnet-sdk.git

从 .NET SDK 根目录,导航到 Dapr 流式发布/订阅示例。

cd examples/Client/PublishSubscribe

本地运行应用程序

要运行 Dapr 应用程序,您需要启动 .NET 程序和一个 Dapr sidecar。导航到 StreamingSubscriptionExample 目录。

cd StreamingSubscriptionExample

我们将运行一个命令,同时启动 Dapr sidecar 和 .NET 程序。

dapr run --app-id pubsubapp --dapr-grpc-port 4001 --dapr-http-port 3500 -- dotnet run

Dapr 监听 HTTP 请求在 http://localhost:3500,而 gRPC 请求在 http://localhost:4001

使用依赖注入注册 Dapr PubSub 客户端

Dapr Messaging SDK 提供了一个扩展方法来简化 Dapr PubSub 客户端的注册。在 Program.cs 中完成依赖注入注册之前,添加以下行:

var builder = WebApplication.CreateBuilder(args);

//可以在这两行之间的任何位置添加
builder.Services.AddDaprPubSubClient(); //就是这样

var app = builder.Build();

您可能希望为 Dapr PubSub 客户端提供一些配置选项,这些选项应在每次调用 sidecar 时存在,例如 Dapr API 令牌,或者您希望使用非标准的 HTTP 或 gRPC 端点。这可以通过使用允许配置 DaprPublishSubscribeClientBuilder 实例的注册方法重载来实现:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprPubSubClient((_, daprPubSubClientBuilder) => {
    daprPubSubClientBuilder.UseDaprApiToken("abc123");
    daprPubSubClientBuilder.UseHttpEndpoint("http://localhost:8512"); //非标准 sidecar HTTP 端点
});

var app = builder.Build();

尽管如此,您可能希望注入的任何值需要从其他来源检索,该来源本身注册为依赖项。您可以使用另一个重载将 IServiceProvider 注入到配置操作方法中。在以下示例中,我们注册了一个虚构的单例,可以从某处检索 secret 并将其传递到 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 所示。相反,通过填充通过依赖注入提供的 IConfigurationAddDaprPubSubClient() 注册将自动使用这些值覆盖其各自的默认值。

首先在您的配置中填充值。这可以通过多种不同的方式完成,如下所示。

通过 ConfigurationBuilder 配置

应用程序设置可以在不使用配置源的情况下配置,而是通过使用 ConfigurationBuilder 实例在内存中填充值:

var builder = WebApplication.CreateBuilder();

//创建配置
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(); //这将自动从 IConfiguration 填充 HTTP 端点和 API 令牌值

通过环境变量配置

应用程序设置可以从可用于您的应用程序的环境变量中访问。

以下环境变量将用于填充用于注册 Dapr PubSub 客户端的 HTTP 端点和 API 令牌。

DAPR_HTTP_ENDPOINThttp://localhost:54321
DAPR_API_TOKENabc123
var builder = WebApplication.CreateBuilder();

builder.Configuration.AddEnvironmentVariables();
builder.Services.AddDaprPubSubClient();

Dapr PubSub 客户端将被配置为使用 HTTP 端点 http://localhost:54321 并用 API 令牌头 abc123 填充所有出站请求。

通过前缀环境变量配置

然而,在共享主机场景中,多个应用程序都在同一台机器上运行而不使用容器或在开发环境中,前缀环境变量并不罕见。以下示例假设 HTTP 端点和 API 令牌都将从前缀为 “myapp_” 的环境变量中提取。在此场景中使用的两个环境变量如下:

myapp_DAPR_HTTP_ENDPOINThttp://localhost:54321
myapp_DAPR_API_TOKENabc123

这些环境变量将在以下示例中加载到注册的配置中,并在没有附加前缀的情况下提供。

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();

        //使用 `daprPubSubClient` 做一些事情
    }
}

设置消息处理程序

Dapr 中的流式订阅实现使您可以更好地控制事件的背压处理,通过在您的应用程序准备好接受它们之前将消息保留在 Dapr 运行时中。 .NET SDK 支持一个高性能队列,用于在处理挂起时在您的应用程序中维护这些消息的本地缓存。这些消息将保留在队列中,直到每个消息的处理超时或采取响应操作(通常在处理成功或失败后)。在 Dapr 运行时收到此响应操作之前,消息将由 Dapr 保留,并在服务故障时可用。

可用的各种响应操作如下:

响应操作描述
重试事件应在将来再次传递。
丢弃事件应被删除(或转发到死信队列,如果已配置)并且不再尝试。
成功事件应被删除,因为它已成功处理。

处理程序将一次只接收一条消息,如果为订阅提供了取消令牌,则将在处理程序调用期间提供此令牌。

处理程序必须配置为返回一个 Task<TopicResponseAction>,指示这些操作之一,即使是从 try/catch 块中返回。如果您的处理程序未捕获异常,订阅将在订阅注册期间配置的选项中使用响应操作。

以下演示了示例中提供的示例消息处理程序:

Task<TopicResponseAction> HandleMessageAsync(TopicMessage message, CancellationToken cancellationToken = default)
{
    try
    {
        //对消息做一些事情
        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 组成。

其他选项如下:

属性名称描述
Metadata额外的订阅元数据
DeadLetterTopic发送丢弃消息的死信主题的可选名称。
MaximumQueuedMessages默认情况下,内部队列没有强制的最大边界,但设置此属性将施加上限。
MaximumCleanupTimeout当订阅被处理或令牌标记取消请求时,这指定了处理内部队列中剩余消息的最大时间。

然后按以下示例配置订阅:

var messagingClient = app.Services.GetRequiredService<DaprPublishSubscribeClient>();

var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60)); //覆盖默认的30秒
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 秒。

3.1.6.2 - DaprPublishSubscribeClient 使用指南

使用 DaprPublishSubscribeClient 的基本提示和建议

生命周期管理

DaprPublishSubscribeClient 是 Dapr 客户端的一个版本,专门用于与 Dapr 消息 API 交互。它可以与 DaprClient 和其他 Dapr 客户端一起注册而不会出现问题。

它通过 TCP 套接字与 Dapr sidecar 通信,维护对网络资源的访问,并实现了 IAsyncDisposable 接口以支持资源的快速清理。

为了获得最佳性能,建议创建一个长生命周期的 DaprPublishSubscribeClient 实例,并在整个应用程序中共享使用。DaprPublishSubscribeClient 实例是线程安全的,适合共享使用。

可以通过依赖注入来实现这一点。注册方法支持以单例、作用域实例或瞬态(每次注入时重新创建)的方式进行注册,但也可以通过 IConfiguration 或其他注入服务的值来注册,这在每个类中从头创建客户端时是不切实际的。

避免为每个操作创建一个 DaprPublishSubscribeClient 并在操作完成后销毁它。DaprPublishSubscribeClient 应仅在您不再希望接收订阅事件时才被销毁,因为销毁它将取消正在进行的新事件接收。

通过 DaprPublishSubscribeClientBuilder 配置 DaprPublishSubscribeClient

可以通过在 DaprPublishSubscribeClientBuilder 类上调用方法来配置 DaprPublishSubscribeClient,然后调用 .Build() 来创建客户端本身。每个 DaprPublishSubscribeClient 的设置是独立的,并且在调用 .Build() 之后无法更改。

var daprPubsubClient = new DaprPublishSubscribeClientBuilder()
    .UseDaprApiToken("abc123") // 指定用于认证到其他 Dapr sidecar 的 API 令牌
    .Build();

DaprPublishSubscribeClientBuilder 包含以下设置:

  • Dapr sidecar 的 HTTP 端点
  • Dapr sidecar 的 gRPC 端点
  • 用于配置 JSON 序列化的 JsonSerializerOptions 对象
  • 用于配置 gRPC 的 GrpcChannelOptions 对象
  • 用于认证请求到 sidecar 的 API 令牌
  • 用于创建 SDK 使用的 HttpClient 实例的工厂方法
  • 用于在向 sidecar 发出请求时使用的 HttpClient 实例的超时

SDK 将读取以下环境变量来配置默认值:

  • DAPR_HTTP_ENDPOINT:用于查找 Dapr sidecar 的 HTTP 端点,例如:https://dapr-api.mycompany.com
  • DAPR_GRPC_ENDPOINT:用于查找 Dapr sidecar 的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.com
  • DAPR_HTTP_PORT:如果未设置 DAPR_HTTP_ENDPOINT,则用于查找 Dapr sidecar 的本地 HTTP 端点
  • DAPR_GRPC_PORT:如果未设置 DAPR_GRPC_ENDPOINT,则用于查找 Dapr sidecar 的本地 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();

3.1.7 - Dapr .NET SDK 的错误处理

探索如何在 Dapr .NET SDK 中进行错误处理。

3.1.7.1 - Dapr .NET SDK 中更全面的错误模型

了解如何在 .NET SDK 中使用更全面的错误模型。

Dapr .NET SDK 支持由 Dapr 运行时实现的更全面的错误模型。这个模型为应用程序提供了一种丰富错误信息的方式,提供更多上下文信息,使应用程序的用户能够更好地理解问题并更快地解决。您可以在这里阅读更多关于更全面错误模型的信息,并可以在这里找到实现这些错误的 Dapr proto 文件。

Dapr .NET SDK 实现了 Dapr 运行时支持的所有细节,这些细节在 Dapr.Common.Exceptions 命名空间中实现,并可以通过 DaprException 的扩展方法 TryGetExtendedErrorInfo 进行访问。目前,此细节提取仅支持存在细节的 RpcException

// 扩展错误信息的示例用法

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

  1. RetryInfo

  2. DebugInfo

  3. QuotaFailure

  4. PreconditionFailure

  5. RequestInfo

  6. LocalizedMessage

  7. BadRequest

  8. ErrorInfo

  9. Help

  10. ResourceInfo

  11. Unknown

RetryInfo

告知客户端在重试之前应等待多长时间的信息。提供一个 DaprRetryDelay,其属性包括 Second(秒偏移)和 Nano(纳秒偏移)。

DebugInfo

服务器提供的调试信息。包含 StackEntries(包含堆栈跟踪的字符串集合)和 Detail(进一步的调试信息)。

QuotaFailure

与可能已达到的某些配额相关的信息,例如 API 的每日使用限制。它有一个属性 Violations,是 DaprQuotaFailureViolation 的集合,每个都包含 Subject(请求的主题)和 Description(有关失败的更多信息)。

PreconditionFailure

告知客户端某些必需的前置条件未满足的信息。具有一个属性 Violations,是 DaprPreconditionFailureViolation 的集合,每个都有 Subject(前置条件失败发生的主题,例如 “Azure”)、Type(前置条件类型的表示,例如 “TermsOfService”)和 Description(进一步描述,例如 “ToS 必须被接受。")。

RequestInfo

服务器返回的信息,可用于服务器识别客户端请求。包含 RequestIdServingData 属性,RequestId 是服务器可以解释的某个字符串(例如 UID),ServingData 是构成请求一部分的任意数据。

LocalizedMessage

包含本地化消息及其语言环境。包含 Locale(语言环境,例如 “en-US”)和 Message(本地化消息)。

BadRequest

描述错误请求字段。包含 DaprBadRequestDetailFieldViolation 的集合,每个都有 Field(请求中有问题的字段,例如 ‘first_name’)和 Description(详细说明原因,例如 “first_name 不能包含特殊字符”)。

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(与错误相关的资源的进一步信息,例如 “缺少使用此资源的权限”)。

Unknown

当详细类型 URL 无法映射到正确的 DaprExtendedErrorDetail 实现时返回。提供一个属性 TypeUrl(无法解析的类型 URL,例如 “type.googleapis.com/Google.rpc.UnrecognizedType”)。

3.1.8 - 使用 Dapr .NET SDK 开发应用程序

了解 .NET Dapr 应用程序的本地开发集成选项

同时管理多个任务

通常情况下,使用您喜欢的 IDE 或编辑器启动应用程序时,您只需运行一个任务:您正在调试的应用程序。然而,开发微服务要求您在本地开发过程中同时管理多个任务。一个微服务应用程序包含多个服务,您可能需要同时运行这些服务,并管理依赖项(如状态存储)。

将 Dapr 集成到您的开发过程中意味着您需要管理以下事项:

  • 您想要运行的每个服务
  • 每个服务的 Dapr sidecar
  • Dapr 组件和配置清单
  • 额外的依赖项,如状态存储
  • 可选:用于 actor 的 Dapr placement 服务

本文档假设您正在构建一个生产应用程序,并希望创建一套可重复且稳健的开发实践。这里的指导是通用的,适用于任何使用 Dapr 的 .NET 服务器应用程序(包括 actor)。

组件管理

您有两种主要方法来存储 Dapr 本地开发的组件定义:

  • 使用默认位置 (~/.dapr/components)
  • 使用您自定义的位置

在您的源代码库中创建一个文件夹来存储组件和配置,这样可以方便地对这些定义进行版本控制和共享。本文假设您在应用程序源代码旁边创建了一个文件夹来存储这些文件。

开发选项

选择以下链接之一以了解您可以在本地开发场景中使用的工具。这些文章按投入程度从低到高排序。您可能希望阅读所有文章以全面了解可用选项。

3.1.8.1 - 使用 Dapr CLI 进行 Dapr .NET SDK 开发

了解如何使用 Dapr CLI 进行本地开发

Dapr CLI

可以将其视为 .NET 伴侣指南:使用 Docker 的 Dapr 自托管指南的补充

Dapr CLI 通过初始化本地的 Redis 容器、Zipkin 容器、placement 服务和 Redis 的组件清单,为您提供了一个良好的基础环境。这使您能够在全新安装且无需额外设置的情况下使用以下功能模块:

您可以使用 dapr run 命令来运行 .NET 服务,作为本地开发的一种策略。为每个服务运行此命令以启动您的应用程序。

  • 优势: 由于这是 Dapr 默认安装的一部分,因此设置简单
  • 劣势: 这会在您的机器上运行长时间的 Docker 容器,可能不太理想
  • 劣势: 这种方法的可扩展性较差,因为需要为每个服务运行一个单独的命令

使用 Dapr CLI

对于每个服务,您需要选择:

  • 用于寻址的唯一应用 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 命令。

如果您的任何服务不接受 HTTP 流量,请通过删除 --app-port--urls 参数来修改上述命令。

下一步

如果您需要调试,请使用调试器的附加功能附加到其中一个正在运行的进程。

如果您想扩展这种方法,请考虑编写一个脚本来为您的整个应用程序自动化此过程。

3.1.8.2 - 使用 .NET Aspire 进行 Dapr .NET SDK 开发

了解如何使用 .NET Aspire 进行本地开发

.NET Aspire

.NET Aspire 是一款开发工具,旨在通过提供一个框架,简化外部软件与 .NET 应用程序的集成过程。该框架允许第三方服务轻松地与您的软件集成、监控和配置。

Aspire 通过与流行的 IDE(包括 Microsoft Visual StudioVisual Studio CodeJetBrains Rider 等)深度集成,简化了本地开发。在启动调试器的同时,自动启动并配置对其他集成(包括 Dapr)的访问。

虽然 Aspire 也支持将应用程序部署到各种云平台(如 Microsoft Azure 和 Amazon AWS),但本指南不涉及部署相关内容。更多信息请参阅 Aspire 的文档 这里

先决条件

通过 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 应用程序。从同一目录中,使用以下命令创建一个名为 MyApp 的空 ASP.NET Core 项目。它将在 MyApp\MyApp.csproj 中相对于您的当前目录创建。

dotnet new web MyApp

接下来,我们将配置 AppHost 项目以添加支持本地 Dapr 开发所需的包。使用以下命令导航到 AppHost 目录,并从 NuGet 安装 Aspire.Hosting.Dapr 包到项目中。我们还将添加对 MyApp 项目的引用,以便在注册过程中引用它。

cd aspiredemo.AppHost
dotnet add package Aspire.Hosting.Dapr
dotnet add reference ../MyApp/

接下来,我们需要将 Dapr 配置为与您的项目一起加载的资源。在您喜欢的 IDE 中打开该项目中的 Program.cs 文件。它应类似于以下内容:

var builder = DistributedApplication.CreateBuilder(args);

builder.Build().Run();

如果您熟悉 ASP.NET Core 项目中使用的依赖注入方法或其他使用 Microsoft.Extensions.DependencyInjection 功能的项目,您会发现这将是一个熟悉的体验。

因为我们已经添加了对 MyApp 的项目引用,我们需要在此配置中添加一个引用。在 builder.Build().Run() 行之前添加以下内容:

var myApp = builder
    .AddProject<Projects.MyApp>("myapp")
    .WithDaprSidecar();

因为项目引用已添加到此解决方案中,您的项目在此处显示为 Projects. 命名空间中的一个类型。您为项目分配的变量名称在本教程中并不重要,但如果您想在此项目和另一个项目之间创建引用以使用 Aspire 的服务发现功能,则会使用它。

添加 .WithDaprSidecar() 将 Dapr 配置为 .NET Aspire 资源,以便在项目运行时,sidecar 将与您的应用程序一起部署。这接受许多不同的选项,并可以选择性地配置,如以下示例所示:

DaprSidecarOptions sidecarOptions = new()
{
    AppId = "my-other-app",
    AppPort = 8080, //注意,如果您打算配置 pubsub、actor 或 workflow,从 Aspire v9.0 开始,此参数是必需的
    DaprGrpcPort = 50001,
    DaprHttpPort = 3500,
    MetricsPort = 9090
};

builder
    .AddProject<Projects.MyOtherApp>("myotherapp")
    .WithReference(myApp)
    .WithDaprSidecar(sidecarOptions);

当您在 IDE 中打开解决方案时,确保 aspiredemo.AppHost 被配置为您的启动项目,但当您在调试配置中启动它时,您会注意到您的集成控制台应反映您预期的 Dapr 日志,并且它将可用于您的应用程序。

3.1.8.3 - 使用 Project Tye 进行 Dapr .NET SDK 开发

了解如何使用 Project Tye 进行本地开发

Project Tye

.NET Project Tye 是一个专为简化运行多个 .NET 服务而设计的微服务开发工具。Tye 允许您将多个 .NET 服务、进程和容器镜像的配置整合为一个可运行的应用程序。

对于 .NET Dapr 开发者来说,Tye 的优势在于:

  • Tye 可以自动化使用 dapr CLI
  • Tye 遵循 .NET 的约定,对 .NET 服务几乎无需额外配置
  • Tye 能够管理容器中依赖项的生命周期

优缺点:

  • 优点: Tye 可以自动化上述所有步骤。您无需再担心端口或应用程序 ID 等细节。
  • 优点: 由于 Tye 也可以管理容器,您可以将这些容器作为应用程序的一部分定义,并避免机器上长时间运行的容器。

使用 Tye

按照 Tye 入门指南 安装 tye CLI,并为您的应用程序创建 tye.yaml 文件。

接下来,按照 Tye Dapr 配方 中的步骤添加 Dapr。确保在 tye.yaml 中使用 components-path 指定组件文件夹的相对路径。

然后,添加任何额外的容器依赖项,并将组件定义添加到您之前创建的文件夹中。

您应该得到如下内容:

name: store-application
extensions:

  # Dapr 的配置在这里。
- name: dapr
  components-path: <components-path> 

# 要运行的服务在这里。
services:
  
  # 名称将用作应用程序 ID。对于 .NET 项目,Tye 只需要项目文件的路径。
- name: orders
  project: orders/orders.csproj
- name: products
  project: products/products.csproj
- name: store
  project: store/store.csproj

  # 您想要运行的容器需要一个镜像名称和一组要暴露的端口。
- name: redis
  image: redis
  bindings:
    - port: 6973

tye.yaml 和应用程序代码一起提交到源代码管理中。

您现在可以使用 tye run 从一个终端启动整个应用程序。运行时,Tye 在 http://localhost:8000 提供一个仪表板以查看应用程序状态和日志。

下一步

Tye 会将您的服务作为标准 .NET 进程在本地运行。如果您需要调试,可以使用调试器附加到正在运行的进程之一。由于 Tye 了解 .NET,它可以在启动时暂停进程以便进行调试。

如果您希望在容器中进行本地测试,Tye 还提供了一个选项,可以在容器中运行您的服务。

3.1.8.4 - 使用 Docker-Compose 进行 Dapr .NET SDK 开发

了解如何使用 Docker-Compose 进行本地开发

Docker-Compose

这可以看作是 .NET 伴侣指南:使用 Docker 的 Dapr 自托管指南 的补充。

docker-compose 是 Docker Desktop 附带的一个命令行工具,您可以用它同时运行多个容器。它提供了一种自动化管理多个容器生命周期的方法,为面向 Kubernetes 的应用程序提供类似于生产环境的开发体验。

  • 优势在于: docker-compose 帮助您管理容器,您可以将依赖项作为应用程序的一部分进行定义,并停止机器上长时间运行的容器。
  • 劣势在于: 需要较多的前期投入,服务需要先容器化。
  • 劣势在于: 如果您不熟悉 Docker,可能会遇到调试和故障排除的困难。

使用 docker-compose

从 .NET 的角度来看,使用 Dapr 的 docker-compose 并不需要特别的指导。docker-compose 负责运行容器,一旦您的服务在容器中,配置它就和其他编程技术类似。

总结这种方法:

  • 为每个服务创建一个 Dockerfile
  • 创建一个 docker-compose.yaml 并将其提交到源代码库

要了解如何编写 docker-compose.yaml,您可以从 Hello, docker-compose 示例 开始。

类似于使用 dapr run 本地运行,对于每个服务,您需要选择一个唯一的 app-id。选择容器名称作为 app-id 可以帮助您更容易记住。

compose 文件至少应包含以下内容:

  • 容器之间通信所需的网络
  • 每个服务的容器
  • 一个 <service>-daprd sidecar 容器,指定服务的端口和 app-id
  • 在容器中运行的其他依赖项(例如 redis)
  • 可选:Dapr placement 容器(用于 actor)

您还可以查看 eShopOnContainers 示例应用程序中的更大示例。

3.1.9 - Dapr .NET SDK 故障排除与调试

掌握使用 Dapr .NET SDK 进行故障排除与调试的实用方法和指南

3.1.9.1 - 使用 .NET SDK 进行 Pub/Sub 故障排查

使用 .NET SDK 进行 Pub/Sub 故障排查

Pub/Sub 故障排查

Pub/Sub 的常见问题是应用程序中的 Pub/Sub 端点未被调用。

这个问题可以分为几个层次,每个层次有不同的解决方案:

  • 应用程序没有接收到来自 Dapr 的任何流量
  • 应用程序没有向 Dapr 注册 Pub/Sub 端点
  • Pub/Sub 端点已在 Dapr 中注册,但请求没有到达预期的端点

步骤 1:提高日志级别

这一点很重要。后续步骤将依赖于您查看日志输出的能力。ASP.NET Core 默认日志设置几乎不记录任何内容,因此您需要更改它。

调整日志详细程度以包括 ASP.NET Core 的 Information 日志,如此处所述。将 Microsoft 键设置为 Information

步骤 2:验证您可以接收到来自 Dapr 的流量

  1. 像往常一样启动应用程序(dapr run ...)。确保在命令行中包含 --app-port 参数。Dapr 需要知道您的应用程序正在监听流量。默认情况下,ASP.NET Core 应用程序将在本地开发中监听 5000 端口的 HTTP。

  2. 等待 Dapr 启动完成

  3. 检查日志

您应该看到类似这样的日志条目:

info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET http://localhost:5000/.....

在初始化过程中,Dapr 会向您的应用程序发送一些请求以进行配置。如果找不到这些请求,则意味着出现了问题。请通过问题或 Discord 请求帮助(包括日志)。如果您看到对应用程序的请求,请继续执行下一步。

步骤 3:验证端点注册

  1. 像往常一样启动应用程序(dapr run ...)。

  2. 使用命令行中的 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 并包含一些 Pub/Sub 条目

如果您在此测试的 JSON 输出中有条目,则问题出在其他地方,请继续执行下一步。

选项 1:响应不是 200,或不包含 JSON

如果响应不是 200 或不包含 JSON,则 MapSubscribeHandler() 端点未被访问。

确保在 Startup.cs 中有如下代码并重复测试。

app.UseRouting();

app.UseCloudEvents();

app.UseEndpoints(endpoints =>
{
    endpoints.MapSubscribeHandler(); // 这是 Dapr 订阅处理程序
    endpoints.MapControllers();
});

如果添加订阅处理程序没有解决问题,请在此仓库中打开一个问题,并包括您的 Startup.cs 文件的内容。

选项 2:响应包含 JSON 但为空(如 []

如果 JSON 输出是一个空数组(如 []),则订阅处理程序已注册,但没有注册主题端点。


如果您使用控制器进行 Pub/Sub,您应该有一个类似的方法:

[Topic("pubsub", "deposit")]
[HttpPost("deposit")]
public async Task<ActionResult> Deposit(...)

// 使用 Pub/Sub 路由
[Topic("pubsub", "transactions", "event.type == \"withdraw.v2\"", 1)]
[HttpPost("withdraw")]
public async Task<ActionResult> Withdraw(...)

在此示例中,TopicHttpPost 属性是必需的,但其他细节可能不同。


如果您使用路由进行 Pub/Sub,您应该有一个类似的端点:

endpoints.MapPost("deposit", ...).WithTopic("pubsub", "deposit");

在此示例中,调用 WithTopic(...) 是必需的,但其他细节可能不同。


在更正此代码并重新测试后,如果 JSON 输出仍然是空数组(如 []),请在此仓库中打开一个问题,并包括 Startup.cs 和您的 Pub/Sub 端点的内容。

步骤 4:验证端点可达性

在此步骤中,我们将验证注册的 Pub/Sub 条目是否可达。上一步应该让您得到如下的 JSON 输出:

[
  {
    "pubsubName": "pubsub",
    "topic": "deposit",
    "route": "deposit"
  },
  {
    "pubsubName": "pubsub",
    "topic": "deposit",
    "routes": {
      "rules": [
        {
          "match": "event.type == \"withdraw.v2\"",
          "path": "withdraw"
        }
      ]
    }
  }
]

保留此输出,因为我们将使用 route 信息来测试应用程序。

  1. 像往常一样启动应用程序(dapr run ...)。

  2. 使用命令行中的 curl(或其他 HTTP 测试工具)访问注册的 Pub/Sub 端点之一。

假设您的应用程序监听端口为 5000,并且您的一个 Pub/Sub 路由是 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 直接发送 Pub/Sub 消息并比较日志输出。

示例命令:

dapr publish --pubsub pubsub --topic withdraw --data '{}'

如果在这样做之后您仍然不理解问题,请在此仓库中打开一个问题,并包括您的 Startup.cs 的内容。

选项 1:路由未执行

如果您在日志中没有看到 Microsoft.AspNetCore.Routing.EndpointMiddleware 的条目,则意味着请求被其他东西处理了。通常情况下,问题是一个行为不当的中间件。请求的其他日志可能会给您一个线索。

如果您需要帮助理解问题,请在此仓库中打开一个问题,并包括您的 Startup.cs 的内容。

选项 2:路由选择了错误的端点

如果您在日志中看到 Microsoft.AspNetCore.Routing.EndpointMiddleware 的条目,但它包含错误的端点,则意味着您有一个路由冲突。被选择的端点将出现在日志中,这应该能给您一个关于冲突原因的想法。

如果您需要帮助理解问题,请在此仓库中打开一个问题,并包括您的 Startup.cs 的内容。

3.2 - Dapr Go SDK

用于开发 Dapr 应用的 Go SDK 包

这是一个用于在 Go 中构建 Dapr 应用的客户端库。该客户端支持所有公共 Dapr API,专注于提供符合 Go 语言习惯的开发体验,提高开发者的工作效率。

客户端

使用 Go 客户端 SDK 来调用公共 Dapr API。 [**了解更多关于 Go 客户端 SDK 的信息**](https://v1-16.docs.dapr.io/zh-hans/developing-applications/sdks/go/go-client/)

服务

使用 Dapr 服务(回调)SDK 创建可被 Dapr 调用的服务。 [**了解更多关于 Go 服务(回调)SDK 的信息**](https://v1-16.docs.dapr.io/zh-hans/developing-applications/sdks/go/go-service/)

3.2.1 - Dapr 服务(回调)SDK for Go 入门指南

快速掌握如何使用 Dapr 服务(回调)SDK for Go

除了 Dapr API 客户端,Dapr Go SDK 还提供了用于启动 Dapr 回调服务的服务模块。您可以选择通过 gRPC 或 HTTP 来开发这些服务:

3.2.1.1 - 使用 Dapr HTTP 服务 SDK for Go 入门

如何使用 Dapr HTTP 服务 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)
	// 处理事件
	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 {
	// 事件处理程序所需的任何数据或引用。
}

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)
    // 处理事件
    return true, nil
}

然后可以使用 AddTopicEventSubscriber 方法添加 EventHandler

sub := &common.Subscription{
    PubsubName: "messages",
    Topic:      "topic1",
}
eventHandler := &EventHandler{
// 初始化字段
}
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))
	// 处理调用
	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)
	// 处理调用
	return nil, nil
}

相关链接

3.2.1.2 - 使用 Dapr 服务(回调)SDK for Go 入门

如何使用 Dapr 服务(回调)SDK for Go 快速上手

Dapr gRPC 服务 SDK for Go

前置条件

首先,导入 Dapr Go 服务/gRPC 包:

daprd "github.com/dapr/go-sdk/service/grpc"

创建和启动服务

要创建一个 gRPC Dapr 服务,首先需要在特定地址上创建一个 Dapr 回调实例:

s, err := daprd.NewService(":50001")
if err != nil {
    log.Fatalf("无法启动服务器: %v", err)
}

或者,使用地址和现有的 net.Listener,以便与现有的服务器监听器结合:

list, err := net.Listen("tcp", "localhost:0")
if err != nil {
	log.Fatalf("gRPC 监听器创建失败: %s", err)
}
s := daprd.NewServiceWithListener(list)

创建服务实例后,你可以为该服务添加任意数量的事件、绑定和服务调用处理程序,如下所示。定义好逻辑后,你就可以启动服务:

if err := s.Start(); err != nil {
    log.Fatalf("服务器错误: %v", err)
}

事件处理

要处理来自特定主题的事件,你需要在启动服务之前添加至少一个主题事件处理程序:

sub := &common.Subscription{
		PubsubName: "messages",
		Topic:      "topic1",
	}
if err := s.AddTopicEventHandler(sub, eventHandler); err != nil {
    log.Fatalf("添加主题订阅时出错: %v", err)
}

处理程序方法可以是任何符合预期签名的方法:

func eventHandler(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
	log.Printf("事件 - PubsubName:%s, Topic:%s, ID:%s, Data: %v", e.PubsubName, e.Topic, e.ID, e.Data)
	// 在这里处理事件
	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("添加主题订阅时出错: %v", err)
}

你还可以创建一个自定义类型来实现 TopicEventSubscriber 接口,以处理你的事件:

type EventHandler struct {
	// 你的事件处理程序需要的任何数据或引用。
}

func (h *EventHandler) Handle(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
    log.Printf("事件 - PubsubName:%s, Topic:%s, ID:%s, Data: %v", e.PubsubName, e.Topic, e.ID, e.Data)
    // 在这里处理事件
    return true, nil
}

然后可以使用 AddTopicEventSubscriber 方法添加 EventHandler

sub := &common.Subscription{
    PubsubName: "messages",
    Topic:      "topic1",
}
eventHandler := &EventHandler{
// 初始化任何字段
}
if err := s.AddTopicEventSubscriber(sub, eventHandler); err != nil {
    log.Fatalf("添加主题订阅时出错: %v", err)
}

服务调用处理程序

要处理服务调用,你需要在启动服务之前添加至少一个服务调用处理程序:

if err := s.AddServiceInvocationHandler("echo", echoHandler); err != nil {
    log.Fatalf("添加调用处理程序时出错: %v", err)
}

处理程序方法可以是任何符合预期签名的方法:

func echoHandler(ctx context.Context, in *common.InvocationEvent) (out *common.Content, err error) {
	log.Printf("回声 - ContentType:%s, Verb:%s, QueryString:%s, %+v", in.ContentType, in.Verb, in.QueryString, string(in.Data))
	// 在这里处理调用
	out = &common.Content{
		Data:        in.Data,
		ContentType: in.ContentType,
		DataTypeURL: in.DataTypeURL,
	}
	return
}

绑定调用处理程序

要处理绑定调用,你需要在启动服务之前添加至少一个绑定调用处理程序:

if err := s.AddBindingInvocationHandler("run", runHandler); err != nil {
    log.Fatalf("添加绑定处理程序时出错: %v", err)
}

处理程序方法可以是任何符合预期签名的方法:

func runHandler(ctx context.Context, in *common.BindingEvent) (out []byte, err error) {
	log.Printf("绑定 - Data:%v, Meta:%v", in.Data, in.Metadata)
	// 在这里处理调用
	return nil, nil
}

相关链接

3.2.2 - 使用 Dapr 客户端 Go SDK 入门

如何使用 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:
            // 处理 Help 详情
            fmt.Println("HelpInfo:")
            for _, link := range t.GetLinks() {
                fmt.Printf("- Url: %s\n", link.Url)
                fmt.Printf("- Description: %s\n", link.Description)
            }
        
        default:
            // 添加其他类型详情的处理
            fmt.Printf("Unhandled error detail type: %v\n", t)
        }
    }
}

构建块

Go SDK 允许您与所有 Dapr 构建块进行交互。

服务调用

要调用运行在 Dapr sidecar 中的另一个服务上的特定方法,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 客户端提供了易于使用的 SaveGetDelete 方法:

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 方法,用于以事务方式执行多个插入或删除操作。

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 提供了两种方法来调用 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)

有关输出绑定的完整指南,请访问 如何使用绑定

Actor

使用 Dapr Go 客户端 SDK 编写 actor。

// 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()
}

有关 actor 的完整指南,请访问 actor 构建块文档

Secret 管理

Dapr 客户端还提供对运行时 secret 的访问,这些 secret 可以由任意数量的 secret 存储(例如 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")
}

有关 secret 的完整指南,请访问 如何检索 secret

分布式锁

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,您可以使用高级 EncryptDecrypt 加密 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

相关链接

Go SDK 示例

3.3 - Dapr Java SDK

Java SDK包,用于开发Dapr应用

Dapr 提供多种包以帮助开发 Java 应用程序。通过这些包,您可以使用 Dapr 创建 Java 客户端、服务器和虚拟 actor。

前提条件

导入 Dapr Java SDK

接下来,导入 Java SDK 包以开始使用。选择您喜欢的构建工具以了解如何导入。

对于 Maven 项目,将以下内容添加到您的 pom.xml 文件中:

<project>
  ...
  <dependencies>
    ...
    <!-- Dapr 的核心 SDK,包含所有功能,actor 除外。 -->
    <dependency>
      <groupId>io.dapr</groupId>
      <artifactId>dapr-sdk</artifactId>
      <version>1.13.1</version>
    </dependency>
    <!-- Dapr 的 actor SDK(可选)。 -->
    <dependency>
      <groupId>io.dapr</groupId>
      <artifactId>dapr-sdk-actors</artifactId>
      <version>1.13.1</version>
    </dependency>
    <!-- Dapr 与 SpringBoot 的 SDK 集成(可选)。 -->
    <dependency>
      <groupId>io.dapr</groupId>
      <artifactId>dapr-sdk-springboot</artifactId>
      <version>1.13.1</version>
    </dependency>
    ...
  </dependencies>
  ...
</project>

对于 Gradle 项目,将以下内容添加到您的 build.gradle 文件中:

dependencies {
...
    // Dapr 的核心 SDK,包含所有功能,actor 除外。
    compile('io.dapr:dapr-sdk:1.13.1')
    // Dapr 的 actor SDK(可选)。
    compile('io.dapr:dapr-sdk-actors:1.13.1')
    // Dapr 与 SpringBoot 的 SDK 集成(可选)。
    compile('io.dapr:dapr-sdk-springboot:1.13.1')
}

如果您也在使用 Spring Boot,可能会遇到一个常见问题,即 Dapr SDK 使用的 OkHttp 版本与 Spring Boot Bill of Materials 中指定的版本冲突。

您可以通过在项目中指定与 Dapr SDK 使用的版本兼容的 OkHttp 版本来解决此问题:

<dependency>
  <groupId>com.squareup.okhttp3</groupId>
  <artifactId>okhttp</artifactId>
  <version>1.13.1</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()) {
  // 发送带有消息的类;BINDING_OPERATION="create"
  client.invokeBinding(BINDING_NAME, BINDING_OPERATION, myClass).block();

  // 发送纯字符串
  client.invokeBinding(BINDING_NAME, BINDING_OPERATION, message).block();
}

可用包

客户端

创建与 Dapr sidecar 和其他 Dapr 应用程序交互的 Java 客户端。

工作流

创建和管理与其他 Dapr API 一起使用的工作流。

3.3.1 - 工作流

如何使用 Dapr 工作流扩展快速启动和运行

在这篇文档中,我们将介绍如何使用 Dapr 工作流扩展来快速启动和运行工作流。Dapr 工作流扩展为定义和管理工作流提供了一种简便的方法,使开发者能够轻松地在分布式系统中编排复杂的业务流程。

Dapr 工作流扩展的主要功能包括:

  • 工作流定义:可以使用 YAML 或 JSON 格式来定义工作流。
  • 状态管理:通过 Dapr 的状态管理功能,确保工作流在执行过程中状态的正确维护。
  • 服务调用:利用 Dapr 的服务调用功能,在工作流中与其他服务进行交互。
  • 事件驱动:通过发布/订阅机制,工作流可以响应事件并触发相应的操作。
  • 定时器和提醒:使用定时器和提醒功能,在工作流中设置定时任务和提醒。

通过这些功能,Dapr 工作流扩展能够帮助开发者更高效地构建和管理分布式应用程序中的工作流。

3.3.1.1 - 如何:在 Java SDK 中编写和管理 Dapr 工作流

如何使用 Dapr Java SDK 快速启动和运行工作流

我们来创建一个 Dapr 工作流,并通过控制台调用它。通过提供的工作流示例,您将:

自托管模式下,此示例使用 dapr init 的默认配置运行。

准备工作

  • 确保您使用的是最新版本的 proto 绑定

设置环境

克隆 Java SDK 仓库并进入其中。

git clone https://github.com/dapr/java-sdk.git
cd java-sdk

运行以下命令以安装运行此工作流示例所需的 Dapr Java SDK 依赖项。

mvn clean install

从 Java SDK 根目录,导航到 Dapr 工作流示例。

cd examples

运行 DemoWorkflowWorker

DemoWorkflowWorker 类在 Dapr 的工作流运行时引擎中注册了 DemoWorkflow 的实现。在 DemoWorkflowWorker.java 文件中,您可以找到 DemoWorkflowWorker 类和 main 方法:

public class DemoWorkflowWorker {

  public static void main(String[] args) throws Exception {
    // Register the Workflow with the runtime.
    WorkflowRuntime.getInstance().registerWorkflow(DemoWorkflow.class);
    System.out.println("Start workflow runtime");
    WorkflowRuntime.getInstance().startAndBlock();
    System.exit(0);
  }
}

在上面的代码中:

  • WorkflowRuntime.getInstance().registerWorkflow()DemoWorkflow 注册为 Dapr 工作流运行时中的一个工作流。
  • WorkflowRuntime.getInstance().start() 在 Dapr 工作流运行时中构建并启动引擎。

在终端中,执行以下命令以启动 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**");
      WorkflowInstanceStatus workflowMetadata = client.getInstanceState(instanceId, true);
      System.out.printf("Result: %s%n", workflowMetadata);

      System.out.println(separatorStr);
      System.out.println("**WaitForInstanceStart**");
      try {
        WorkflowInstanceStatus waitForInstanceStartResult =
            client.waitForInstanceStart(instanceId, Duration.ofSeconds(60), true);
        System.out.printf("Result: %s%n", waitForInstanceStartResult);
      } catch (TimeoutException ex) {
        System.out.printf("waitForInstanceStart 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("**WaitForInstanceCompletion**");
      try {
        WorkflowInstanceStatus waitForInstanceCompletionResult =
            client.waitForInstanceCompletion(instanceId, Duration.ofSeconds(60), true);
        System.out.printf("Result: %s%n", waitForInstanceCompletionResult);
      } catch (TimeoutException ex) {
        System.out.printf("waitForInstanceCompletion has an exception:%s%n", ex);
      }

      System.out.println(separatorStr);
      System.out.println("**purgeInstance**");
      boolean purgeResult = client.purgeInstance(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: '']
*******
**WaitForInstanceStart**
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
*******
**WaitForInstanceCompletion**
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: '']
*******
**purgeInstance**
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.

发生了什么?

  1. 当您运行 dapr run 时,工作流工作者将工作流(DemoWorkflow)及其活动注册到 Dapr 工作流引擎。
  2. 当您运行 java 时,工作流客户端启动了具有以下活动的工作流实例。您可以在运行 dapr run 的终端中查看输出。
    1. 工作流启动,触发三个并行任务,并等待它们完成。
    2. 工作流客户端调用活动并将 “Hello Activity” 消息发送到控制台。
    3. 工作流超时并被清除。
    4. 工作流客户端启动一个具有随机 ID 的新工作流实例,使用另一个名为 terminateMe 的工作流实例终止它,并使用名为 restarting 的工作流重新启动它。
    5. 然后工作流客户端退出。

下一步

3.3.2 - 使用 Dapr 客户端 Java SDK 入门

如何使用 Dapr Java SDK 快速上手

Dapr 客户端包使您能够从 Java 应用程序与其他 Dapr 应用程序进行交互。

前提条件

完成初始设置并将 Java SDK 导入您的项目

初始化客户端

您可以这样初始化 Dapr 客户端:

DaprClient client = new DaprClientBuilder().build();

这会连接到默认的 Dapr gRPC 端点 localhost:50001

环境变量

Dapr Sidecar 端点

您可以使用标准化的 DAPR_GRPC_ENDPOINTDAPR_HTTP_ENDPOINT 环境变量来指定不同的 gRPC 或 HTTP 端点。当设置了这些变量时,客户端将自动使用它们连接到 Dapr sidecar。

旧的环境变量 DAPR_HTTP_PORTDAPR_GRPC_PORT 仍然受支持,但 DAPR_GRPC_ENDPOINTDAPR_HTTP_ENDPOINT 优先。

Dapr API 令牌

如果您的 Dapr 实例需要 DAPR_API_TOKEN 环境变量,您可以在环境中设置,客户端会自动使用。
您可以在这里阅读更多关于 Dapr API 令牌认证的信息。

错误处理

最初,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.getErrorCode());
        System.out.println("Dapr 异常的消息: " + exception.getMessage());
        // DaprException 现在通过 `getStatusDetails()` 提供来自 Dapr 运行时的更多错误详细信息。
        System.out.println("Dapr 异常的原因: " + 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();
}

保存和获取应用程序状态

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();
}

发布和订阅消息

发布消息
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);
    });
  }
}
批量发布消息

注意: API 处于 Alpha 阶段

import io.dapr.client.DaprClientBuilder;
import io.dapr.client.DaprPreviewClient;
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 (DaprPreviewClient client = (new DaprClientBuilder()).buildPreviewClient()) {
      // 创建要发布的消息列表
      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()
    }
  }
}

与输出绑定交互

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();
}

与输入绑定交互

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)));
        }
}

检索秘密

import com.fasterxml.jackson.databind.ObjectMapper;
importio.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));
}

Actors

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);
}

获取和订阅应用程序配置

注意这是一个预览 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)
}

查询保存的状态

注意这是一个预览 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());
        }
}

分布式锁

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("*******尝试获取一个空闲的分布式锁********");
    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("*******解锁一个分布式锁********");
    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());
    }
  }
}

工作流

package io.dapr.examples.workflows;

import io.dapr.workflows.client.DaprWorkflowClient;
import io.dapr.workflows.client.WorkflowInstanceStatus;

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("**GetInstanceMetadata:Running Workflow**");
      WorkflowInstanceStatus workflowMetadata = client.getInstanceState(instanceId, true);
      System.out.printf("Result: %s%n", workflowMetadata);

      System.out.println(separatorStr);
      System.out.println("**WaitForInstanceStart**");
      try {
        WorkflowInstanceStatus waitForInstanceStartResult =
            client.waitForInstanceStart(instanceId, Duration.ofSeconds(60), true);
        System.out.printf("Result: %s%n", waitForInstanceStartResult);
      } catch (TimeoutException ex) {
        System.out.printf("waitForInstanceStart 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("**WaitForInstanceCompletion**");
      try {
        WorkflowInstanceStatus waitForInstanceCompletionResult =
            client.waitForInstanceCompletion(instanceId, Duration.ofSeconds(60), true);
        System.out.printf("Result: %s%n", waitForInstanceCompletionResult);
      } catch (TimeoutException ex) {
        System.out.printf("waitForInstanceCompletion has an exception:%s%n", ex);
      }

      System.out.println(separatorStr);
      System.out.println("**purgeInstance**");
      boolean purgeResult = client.purgeInstance(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);
  }
}

Sidecar API

等待 sidecar

DaprClient 还提供了一个辅助方法来等待 sidecar 变得健康(仅限组件)。使用此方法时,请确保指定超时时间(以毫秒为单位)并使用 block() 来等待反应操作的结果。

// 在尝试使用 Dapr 组件之前,等待 Dapr sidecar 报告健康。
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 组件操作,例如获取秘密或保存状态。

关闭 sidecar

try (DaprClient client = new DaprClientBuilder().build()) {
  logger.info("Sending shutdown request.");
  client.shutdown().block();
  logger.info("Ensuring dapr has stopped.");
  ...
}

了解更多关于 Dapr Java SDK 可用于添加到您的 Java 应用程序的包

相关链接

3.3.3 - 开始使用 Dapr 和 Spring Boot

如何开始使用 Dapr 和 Spring Boot

通过将 Dapr 和 Spring Boot 结合使用,我们可以创建不依赖于特定基础设施的 Java 应用程序,这些应用程序可以部署在不同的环境中,并支持多种本地和云服务提供商。

首先,我们将从一个简单的集成开始,涵盖 DaprClientTestcontainers 的集成,然后利用 Spring 和 Spring Boot 的机制及编程模型来使用 Dapr API。这有助于团队消除连接到特定环境基础设施(如数据库、键值存储、消息代理、配置/密钥存储等)所需的客户端和驱动程序等依赖项。

将 Dapr 和 Spring Boot 集成添加到您的项目中

如果您已经有一个 Spring Boot 应用程序(Spring Boot 3.x+),可以直接将以下依赖项添加到您的项目中:

	<dependency>
        <groupId>io.dapr.spring</groupId>
		<artifactId>dapr-spring-boot-starter</artifactId>
		<version>0.13.1</version>
	</dependency>
	<dependency>
		<groupId>io.dapr.spring</groupId>
		<artifactId>dapr-spring-boot-starter-test</artifactId>
		<version>0.13.1</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 与 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 组件,该组件连接到由 Testcontainers 引导的 PostgreSQL 实例。

@TestConfiguration(proxyBeanMethods = false)
public class DaprTestContainersConfig {
  @Bean
  @ServiceConnection
  public DaprContainer daprContainer(Network daprNetwork, PostgreSQLContainer<?> postgreSQLContainer){
    
    return new DaprContainer("daprio/daprd:1.14.1")
            .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 集成。在日志中,您应该能够看到 daprdplacement 服务容器已为您的应用程序启动。

除了之前的配置(DaprTestContainersConfig),您的测试不应该测试 Dapr 本身,只需测试您的应用程序暴露的 REST 端点。

利用 Spring 和 Spring Boot 编程模型与 Dapr

Java SDK 允许您与所有 Dapr 构建块 接口。但如果您想利用 Spring 和 Spring Boot 编程模型,可以使用 dapr-spring-boot-starter 集成。这包括 Spring Data 的实现(KeyValueTemplateCrudRepository)以及用于生产和消费消息的 DaprMessagingTemplate(类似于 Spring KafkaSpring PulsarSpring AMQP for RabbitMQ)。

使用 Spring Data CrudRepositoryKeyValueTemplate

您可以使用依赖于 Dapr 实现的知名 Spring Data 构造。使用 Dapr,您无需添加任何与基础设施相关的驱动程序或客户端,使您的 Spring 应用程序更轻量化,并与其运行的环境解耦。

在底层,这些实现使用 Dapr Statestore 和 Binding API。

配置参数

使用 Spring Data 抽象,您可以配置 Dapr 将用于连接到可用基础设施的 statestore 和 bindings。这可以通过设置以下属性来完成:

dapr.statestore.name=kvstore
dapr.statestore.binding=kvbinding

然后您可以像这样 @Autowire 一个 KeyValueTemplateCrudRepository

@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.14.1")
            .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);
}

您可以在此处查看并运行完整示例源代码

下一步

了解更多关于 Dapr Java SDK 可用于添加到您的 Java 应用程序的包的信息。

相关链接

3.4 - JavaScript SDK

用于开发Dapr应用的JavaScript SDK

这是一个用于在JavaScript和TypeScript中构建Dapr应用的开发库。该库对Dapr的常用API进行了抽象,如服务调用、状态管理、发布订阅、密钥管理等,并提供了一个简单直观的API接口来帮助构建应用。

安装

要开始使用JavaScript SDK,请从NPM安装Dapr JavaScript SDK:

npm install --save @dapr/dapr

结构

Dapr JavaScript SDK包含两个主要组件:

  • DaprServer:用于管理Dapr sidecar与应用之间的通信。
  • DaprClient:用于管理应用与Dapr sidecar之间的通信。

这些通信可以配置为使用gRPC或HTTP协议。

Dapr ServerDapr Client

入门

为了帮助您快速上手,请查看以下资源:

客户端

创建一个JavaScript客户端,与Dapr sidecar和其他Dapr应用进行交互(例如,发布事件,支持输出绑定等)。

服务器

创建一个JavaScript服务器,让Dapr sidecar与您的应用进行交互(例如,订阅事件,支持输入绑定等)。

虚拟演员

创建具有状态、提醒/计时器和方法的虚拟演员。


日志

配置和自定义SDK的日志功能。

示例

获取JavaScript SDK的源代码并尝试一些示例以快速入门。

3.4.1 - JavaScript 客户端 SDK

用于开发 Dapr 应用的 JavaScript 客户端 SDK

介绍

Dapr 客户端使您能够与 Dapr sidecar 进行通信,并访问其面向客户端的功能,如发布事件、调用输出绑定、状态管理、密钥管理等。

前置条件

安装和导入 Dapr 的 JS SDK

  1. 使用 npm 安装 SDK:
npm i @dapr/dapr --save
  1. 导入库:
import { DaprClient, DaprServer, HttpMethod, CommunicationProtocolEnum } from "@dapr/dapr";

const daprHost = "127.0.0.1"; // Dapr sidecar 主机
const daprPort = "3500"; // 示例服务器的 Dapr sidecar 端口
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 sidecar 交互: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 脚本
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 脚本
npm run start:dapr-grpc

环境变量

Dapr sidecar 端点

您可以使用 DAPR_HTTP_ENDPOINTDAPR_GRPC_ENDPOINT 环境变量分别设置 Dapr sidecar 的 HTTP 和 gRPC 端点。当这些变量被设置时,daprHostdaprPort 不需要在构造函数的选项参数中设置,客户端将自动从提供的端点中解析它们。

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 });

如果环境变量被设置,但 daprHostdaprPort 值被传递给构造函数,后者将优先于环境变量。

Dapr API 令牌

您可以使用 DAPR_API_TOKEN 环境变量设置 Dapr API 令牌。当此变量被设置时,daprApiToken 不需要在构造函数的选项参数中设置,客户端将自动获取它。

通用

增加主体大小

您可以通过使用 DaprClient 的选项增加应用程序与 sidecar 通信时使用的主体大小。

import { DaprClient, CommunicationProtocol } from "@dapr/dapr";

// 允许使用 10Mb 的主体大小
// 默认是 4Mb
const client = new DaprClient({
  daprHost,
  daprPort,
  communicationProtocol: CommunicationProtocol.HTTP,
  maxBodySizeMb: 10,
});

代理请求

通过代理请求,我们可以利用 Dapr 的 sidecar 架构带来的独特功能,如服务发现、日志记录等,使我们能够立即“升级”我们的 gRPC 服务。在 社区电话 41 中演示了 gRPC 代理的这一特性。

创建代理

要执行 gRPC 代理,只需通过调用 client.proxy.create() 方法创建一个代理:

// 一如既往,创建一个到我们 Dapr sidecar 的客户端
// 这个客户端负责确保 sidecar 已启动,我们可以通信,...
const clientSidecar = new DaprClient({ daprHost, daprPort, communicationProtocol: CommunicationProtocol.GRPC });

// 创建一个允许我们使用 gRPC 代码的代理
const clientProxy = await clientSidecar.proxy.create<GreeterClient>(GreeterClient);

我们现在可以调用在我们的 GreeterClient 接口中定义的方法(在这种情况下是来自 Hello World 示例

技术细节

架构

  1. gRPC 服务在 Dapr 中启动。我们通过 --app-port 告诉 Dapr 这个 gRPC 服务器运行在哪个端口,并通过 --app-id <APP_ID_HERE> 给它一个唯一的 Dapr 应用 ID
  2. 我们现在可以通过一个将连接到 sidecar 的客户端调用 Dapr sidecar
  3. 在调用 Dapr sidecar 时,我们提供一个名为 dapr-app-id 的元数据键,其值为在 Dapr 中启动的 gRPC 服务器(例如,在我们的示例中为 server
  4. Dapr 现在将调用转发到配置的 gRPC 服务器

构建块

JavaScript 客户端 SDK 允许您与所有 Dapr 构建块 进行接口交互,重点是客户端到 sidecar 的功能。

调用 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);
});

PubSub 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!");
  // 如果发布失败,响应包含错误
  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);

  // 将 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);
});

Bindings 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);
});

有关输出绑定的完整指南,请访问 如何:使用绑定

Secret API

检索 secrets

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";

  // 从 secret 存储中检索单个 secret
  const response = await client.secret.get(secretStoreName, secretKey);

  // 从 secret 存储中检索所有 secrets
  const response = await client.secret.getBulk(secretStoreName);
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

有关 secrets 的完整指南,请访问 如何:检索 secrets

Configuration 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: {} } }
}

Cryptography API

JavaScript SDK 中的 gRPC 客户端仅支持 cryptography 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 sidecar 端口

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);
});

有关 cryptography 的完整指南,请访问 如何:Cryptography

分布式锁 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 "成功";
    case LockStatus.LockDoesNotExist:
      return "锁不存在";
    case LockStatus.LockBelongsToOthers:
      return "锁属于他人";
    default:
      return "内部错误";
  }
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

有关分布式锁的完整指南,请访问 如何:使用分布式锁

Workflow API

Workflow 管理

import { DaprClient } from "@dapr/dapr";

async function start() {
  const client = new DaprClient();

  // 启动一个新的 workflow 实例
  const instanceId = await client.workflow.start("OrderProcessingWorkflow", {
    Name: "Paperclips",
    TotalCost: 99.95,
    Quantity: 4,
  });
  console.log(`启动了 workflow 实例 ${instanceId}`);

  // 获取一个 workflow 实例
  const workflow = await client.workflow.get(instanceId);
  console.log(
    `Workflow ${workflow.workflowName}, 创建于 ${workflow.createdAt.toUTCString()}, 状态为 ${
      workflow.runtimeStatus
    }`,
  );
  console.log(`附加属性:${JSON.stringify(workflow.properties)}`);

  // 暂停一个 workflow 实例
  await client.workflow.pause(instanceId);
  console.log(`暂停了 workflow 实例 ${instanceId}`);

  // 恢复一个 workflow 实例
  await client.workflow.resume(instanceId);
  console.log(`恢复了 workflow 实例 ${instanceId}`);

  // 终止一个 workflow 实例
  await client.workflow.terminate(instanceId);
  console.log(`终止了 workflow 实例 ${instanceId}`);

  // 清除一个 workflow 实例
  await client.workflow.purge(instanceId);
  console.log(`清除了 workflow 实例 ${instanceId}`);
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

相关链接

3.4.2 - JavaScript 服务器 SDK

用于开发 Dapr 应用的 JavaScript 服务器 SDK

介绍

Dapr 服务器使您能够接收来自 Dapr sidecar 的通信,并访问其面向服务器的功能,例如:事件订阅、接收输入绑定等。

准备条件

安装和导入 Dapr 的 JS SDK

  1. 使用 npm 安装 SDK:
npm i @dapr/dapr --save
  1. 导入库:
import { DaprServer, CommunicationProtocolEnum } from "@dapr/dapr";

const daprHost = "127.0.0.1"; // Dapr sidecar 主机
const daprPort = "3500"; // Dapr sidecar 端口
const serverHost = "127.0.0.1"; // 应用主机
const serverPort = "50051"; // 应用端口

// HTTP 示例
const server = new DaprServer({
  serverHost,
  serverPort,
  communicationProtocol: CommunicationProtocolEnum.HTTP, // DaprClient 使用与 DaprServer 相同的通信协议,除非另有说明
  clientOptions: {
    daprHost,
    daprPort,
  },
});

// GRPC 示例
const server = new DaprServer({
  serverHost,
  serverPort,
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  clientOptions: {
    daprHost,
    daprPort,
  },
});

运行

要运行示例,您可以使用两种不同的协议与 Dapr sidecar 交互:HTTP(默认)或 gRPC。

使用 HTTP(内置 express 网络服务器)

import { DaprServer } from "@dapr/dapr";

const server = new DaprServer({
  serverHost: appHost,
  serverPort: appPort,
  clientOptions: {
    daprHost,
    daprPort,
  },
});
// 在服务器启动前初始化订阅,Dapr sidecar 依赖于这些
await server.start();
# 使用 dapr run
dapr run --app-id example-sdk --app-port 50051 --app-protocol http -- npm run start

# 或者,使用 npm 脚本
npm run start:dapr-http

ℹ️ 注意: 这里需要 app-port,因为这是我们的服务器需要绑定的地方。Dapr 将检查应用程序是否绑定到此端口,然后完成启动。

使用 HTTP(自带 express 网络服务器)

除了使用内置的网络服务器进行 Dapr sidecar 到应用程序的通信,您还可以自带实例。这在构建 REST API 后端并希望直接集成 Dapr 时非常有用。

注意,这目前仅适用于 express

💡 注意:使用自定义网络服务器时,SDK 将配置服务器属性,如最大主体大小,并向其添加新路由。这些路由是独特的,以避免与您的应用程序发生任何冲突,但不能保证不发生冲突。

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", // 应用主机
      serverPort: "50002", // 应用端口
      serverHttp: myApp,
      clientOptions: {
        daprHost,
        daprPort
      }
    });

// 在服务器启动前初始化订阅,Dapr sidecar 使用它。
// 这也将初始化应用服务器本身(无需调用 `app.listen`)。
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,
  },
});
// 在服务器启动前初始化订阅,Dapr sidecar 依赖于这些
await server.start();
# 使用 dapr run
dapr run --app-id example-sdk --app-port 50051 --app-protocol grpc -- npm run start

# 或者,使用 npm 脚本
npm run start:dapr-grpc

ℹ️ 注意: 这里需要 app-port,因为这是我们的服务器需要绑定的地方。Dapr 将检查应用程序是否绑定到此端口,然后完成启动。

构建块

JavaScript 服务器 SDK 允许您与所有 Dapr 构建块 进行接口交互,重点是 sidecar 到应用程序的功能。

调用 API

监听调用

import { DaprServer, DaprInvokerCallbackContent } from "@dapr/dapr";

const daprHost = "127.0.0.1"; // Dapr sidecar 主机
const daprPort = "3500"; // Dapr sidecar 端口
const serverHost = "127.0.0.1"; // 应用主机
const serverPort = "50051"; // 应用端口

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); // 仅在 HTTP 中可用
  };

  await server.invoker.listen("hello-world", callbackFunction, { method: HttpMethod.GET });

  // 您现在可以使用您的应用 ID 和方法 "hello-world" 调用服务

  await server.start();
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

有关服务调用的完整指南,请访问 如何:调用服务

PubSub API

订阅消息

可以通过多种方式订阅消息,以提供接收主题消息的灵活性:

  • 通过 subscribe 方法直接订阅
  • 通过 subscribeWithOptions 方法直接订阅并带有选项
  • 通过 susbcribeOnEvent 方法之后订阅

每次事件到达时,我们将其主体作为 data 传递,并将头信息作为 headers 传递,其中可以包含事件发布者的属性(例如,来自 IoT Hub 的设备 ID)

Dapr 要求在启动时设置订阅,但在 JS SDK 中,我们允许之后添加事件处理程序,为您提供编程的灵活性。

下面提供了一个示例

import { DaprServer } from "@dapr/dapr";

const daprHost = "127.0.0.1"; // Dapr sidecar 主机
const daprPort = "3500"; // Dapr sidecar 端口
const serverHost = "127.0.0.1"; // 应用主机
const serverPort = "50051"; // 应用端口

async function start() {
  const server = new DaprServer({
    serverHost,
    serverPort,
    clientOptions: {
      daprHost,
      daprPort,
    },
  });

  const pubSubName = "my-pubsub-name";
  const topic = "topic-a";

  // 为主题配置订阅者
  // 方法 1:通过 `subscribe` 方法直接订阅
  await server.pubsub.subscribe(pubSubName, topic, async (data: any, headers: object) =>
    console.log(`Received Data: ${JSON.stringify(data)} with headers: ${JSON.stringify(headers)}`),
  );

  // 方法 2:通过 `subscribeWithOptions` 方法直接订阅并带有选项
  await server.pubsub.subscribeWithOptions(pubSubName, topic, {
    callback: async (data: any, headers: object) =>
      console.log(`Received Data: ${JSON.stringify(data)} with headers: ${JSON.stringify(headers)}`),
  });

  // 方法 3:通过 `susbcribeOnEvent` 方法之后订阅
  // 注意:我们使用默认值,因为如果没有传递路由(空选项),我们将使用 "default" 作为路由名称
  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)}`);
  });

  // 启动服务器
  await server.start();
}

有关状态操作的完整列表,请访问 如何:发布和订阅

使用 SUCCESS/RETRY/DROP 状态订阅

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:
      # 全局重试策略用于入站组件操作
      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 主机
const daprPort = "3500"; // Dapr sidecar 端口
const serverHost = "127.0.0.1"; // 应用主机
const serverPort = "50051"; // 应用端口

async function start() {
  const server = new DaprServer({
    serverHost,
    serverPort,
    clientOptions: {
      daprHost,
      daprPort,
    },
  });

  const pubSubName = "my-pubsub-name";
  const topic = "topic-a";

  // 成功处理消息
  await server.pubsub.subscribe(pubSubName, topic, async (data: any, headers: object) => {
    return DaprPubSubStatusEnum.SUCCESS;
  });

  // 重试消息
  // 注意:此示例将继续重试传递消息
  // 注意 2:每个组件可以有自己的重试配置
  //   例如,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;
  });

  // 丢弃消息
  await server.pubsub.subscribe(pubSubName, topic, async (data: any, headers: object) => {
    return DaprPubSubStatusEnum.DROP;
  });

  // 启动服务器
  await server.start();
}

基于规则订阅消息

Dapr 支持路由消息 到不同的处理程序(路由)基于规则。

例如,您正在编写一个需要根据消息的 “type” 处理消息的应用程序,使用 Dapr,您可以将它们发送到不同的路由 handlerType1handlerType2,默认路由为 handlerDefault

import { DaprServer } from "@dapr/dapr";

const daprHost = "127.0.0.1"; // Dapr sidecar 主机
const daprPort = "3500"; // Dapr sidecar 端口
const serverHost = "127.0.0.1"; // 应用主机
const serverPort = "50051"; // 应用端口

async function start() {
  const server = new DaprServer({
    serverHost,
    serverPort,
    clientOptions: {
      daprHost,
      daprPort,
    },
  });

  const pubSubName = "my-pubsub-name";
  const topic = "topic-a";

  // 为主题配置订阅者并设置规则
  // 注意:默认路由和匹配模式是可选的
  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",
      },
    ],
  });

  // 为每个路由添加处理程序
  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`);
  });

  // 启动服务器
  await server.start();
}

使用通配符订阅

支持流行的通配符 *+(请确保验证 pubsub 组件是否支持)并可以按如下方式订阅:

import { DaprServer } from "@dapr/dapr";

const daprHost = "127.0.0.1"; // Dapr sidecar 主机
const daprPort = "3500"; // Dapr sidecar 端口
const serverHost = "127.0.0.1"; // 应用主机
const serverPort = "50051"; // 应用端口

async function start() {
  const server = new DaprServer({
    serverHost,
    serverPort,
    clientOptions: {
      daprHost,
      daprPort,
    },
  });

  const pubSubName = "my-pubsub-name";

  // * 通配符
  await server.pubsub.subscribe(pubSubName, "/events/*", async (data: any, headers: object) =>
    console.log(`Received Data: ${JSON.stringify(data)}`),
  );

  // + 通配符
  await server.pubsub.subscribe(pubSubName, "/events/+/temperature", async (data: any, headers: object) =>
    console.log(`Received Data: ${JSON.stringify(data)}`),
  );

  // 启动服务器
  await server.start();
}

批量订阅消息

支持批量订阅,并可通过以下 API 获得:

  • 通过 subscribeBulk 方法进行批量订阅:maxMessagesCountmaxAwaitDurationMs 是可选的;如果未提供,将使用相关组件的默认值。

在监听消息时,应用程序以批量方式从 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,
    },
  });

  // 使用默认配置向主题发布多条消息。
  await client.pubsub.subscribeBulk(pubSubName, topic, (data) =>
    console.log("Subscriber received: " + JSON.stringify(data)),
  );

  // 使用特定的 maxMessagesCount 和 maxAwaitDurationMs 向主题发布多条消息。
  await client.pubsub.subscribeBulk(
    pubSubName,
    topic,
    (data) => {
      console.log("Subscriber received: " + JSON.stringify(data));
      return DaprPubSubStatusEnum.SUCCESS; // 如果应用程序没有返回任何内容,默认是 SUCCESS。应用程序还可以根据传入的消息返回 RETRY 或 DROP。
    },
    {
      maxMessagesCount: 100,
      maxAwaitDurationMs: 40,
    },
  );
}

死信主题

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 主机
const daprPort = "3500"; // Dapr sidecar 端口
const serverHost = "127.0.0.1"; // 应用主机
const serverPort = "50051"; // 应用端口

async function start() {
  const server = new DaprServer({
    serverHost,
    serverPort,
    clientOptions: {
      daprHost,
      daprPort,
    },
  });

  const pubSubName = "my-pubsub-name";

  // 方法 1(通过 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");
    },
  });

  // 方法 2(之后订阅)
  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");
  });

  // 启动服务器
  await server.start();
}

Bindings API

接收输入绑定

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);
});

有关输出绑定的完整指南,请访问 如何:使用绑定

Configuration API

💡 配置 API 目前仅通过 gRPC 可用

获取配置值

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);
});

订阅键更改

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"], () => {
    // 收到键更新
  });

  // 当您准备好停止监听时,调用以下命令
  await stream.close();
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

相关链接

3.4.3 - JavaScript SDK for Actors

如何使用 Dapr JavaScript SDK 快速上手 actor

Dapr actors 包允许您通过 JavaScript 应用程序与 Dapr 虚拟 actor 交互。以下示例展示了如何使用 JavaScript SDK 与虚拟 actor 进行交互。

有关 Dapr actor 的详细介绍,请访问 actor 概述页面

前置条件

场景

以下代码示例大致描述了一个停车场车位监控系统的场景,可以在 Mark Russinovich 的这个视频中看到。

一个停车场由数百个停车位组成,每个停车位都配有一个传感器,该传感器向集中监控系统提供更新。停车位传感器(即我们的 actor)用于检测停车位是否被占用或可用。

要运行此示例,请克隆源代码,源代码位于 JavaScript SDK 示例目录中。

Actor 接口

actor 接口定义了 actor 实现和调用 actor 的客户端之间共享的契约。在下面的示例中,我们为停车场传感器创建了一个接口。每个传感器都有两个方法:carEntercarLeave,它们定义了停车位的状态:

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 的代理对象。您可以通过直接调用代理对象上的方法来调用 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();
  }

  // 使用特定类型获取状态
  async getState<T>(key: string): Promise<T | null> {
    return await this.getStateManager<T>().getState(key);
  }

  // 不指定类型获取状态为 `any`
  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

3.4.4 - JavaScript SDK中的日志记录

配置JavaScript SDK中的日志记录

介绍

JavaScript SDK自带一个内置的Console日志记录器。SDK会生成各种内部日志,帮助用户理解事件流程并排查问题。用户可以自定义日志的详细程度,并提供自己的日志记录器实现。

配置日志级别

日志记录有五个级别,按重要性从高到低排列 - errorwarninfoverbosedebug。设置日志级别意味着日志记录器将记录所有该级别及更高重要性的日志。例如,设置为verbose级别意味着SDK不会记录debug级别的日志。默认的日志级别是info

Dapr Client

import { CommunicationProtocolEnum, DaprClient, LogLevel } from "@dapr/dapr";

// 创建一个日志级别设置为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";

// 创建一个日志级别设置为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();

// 创建一个日志级别设置为verbose且日志服务为winston的客户端实例。
const client = new DaprClient({
  daprHost,
  daprPort,
  communicationProtocol: CommunicationProtocolEnum.HTTP,
  logger: { level: LogLevel.Verbose, service: winstonLoggerService },
});

3.4.5 - JavaScript 示例

通过一些示例来学习如何使用 Dapr JavaScript SDK!

快速开始

相关文章

想要分享您的文章?告诉我们! 我们会将其添加到下面的列表中。

3.4.6 - 如何:在 JavaScript SDK 中编写和管理 Dapr 工作流

如何使用 Dapr JavaScript SDK 快速启动和运行工作流

我们将创建一个 Dapr 工作流并通过控制台调用它。在这个示例中,您将:

此示例在自托管模式下运行,使用 dapr init 的默认配置。

先决条件

设置环境

克隆 JavaScript SDK 仓库并进入其中。

git clone https://github.com/dapr/js-sdk
cd js-sdk

从 JavaScript SDK 根目录,导航到 Dapr 工作流示例。

cd examples/workflow/authoring

运行以下命令以安装运行此工作流示例所需的 Dapr JavaScript SDK 依赖。

npm install

运行 activity-sequence.ts

activity-sequence 文件在 Dapr 工作流运行时中注册了一个工作流和一个活动。工作流是按顺序执行的一系列活动。我们使用 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("工作流运行时启动成功");
} catch (error) {
  console.error("启动工作流运行时出错:", error);
}

// 调度一个新的编排
try {
  const id = await workflowClient.scheduleNewWorkflow(sequence);
  console.log(`编排已调度,ID:${id}`);

  // 等待编排完成
  const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30);

  console.log(`编排完成!结果:${state?.serializedOutput}`);
} catch (error) {
  console.error("调度或等待编排时出错:", error);
}

在上面的代码中:

  • workflowRuntime.registerWorkflow(sequence)sequence 注册为 Dapr 工作流运行时中的一个工作流。
  • await workflowRuntime.start(); 构建并启动 Dapr 工作流运行时中的引擎。
  • await workflowClient.scheduleNewWorkflow(sequence) 在 Dapr 工作流运行时中调度一个新的工作流实例。
  • await workflowClient.waitForWorkflowCompletion(id, undefined, 30) 等待工作流实例完成。

在终端中,执行以下命令以启动 activity-sequence.ts

npm run start:dapr:activity-sequence

预期输出

你已启动并运行!Dapr 和您的应用程序日志将出现在这里。

...

== APP == 编排已调度,ID:dc040bea-6436-4051-9166-c9294f9d2201
== APP == 等待 30 秒以完成实例 dc040bea-6436-4051-9166-c9294f9d2201...
== APP == 收到实例 id 为 'dc040bea-6436-4051-9166-c9294f9d2201' 的 "Orchestrator Request" 工作项
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 使用 0 个历史事件重建本地状态...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 处理 2 个新历史事件:[ORCHESTRATORSTARTED=1, EXECUTIONSTARTED=1]
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 等待 1 个任务和 0 个事件完成...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 返回 1 个动作
== APP == 收到 "Activity Request" 工作项
== APP == 活动 hello 完成,输出 "Hello Tokyo!" (14 个字符)
== APP == 收到实例 id 为 'dc040bea-6436-4051-9166-c9294f9d2201' 的 "Orchestrator Request" 工作项
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 使用 3 个历史事件重建本地状态...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 处理 2 个新历史事件:[ORCHESTRATORSTARTED=1, TASKCOMPLETED=1]
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 等待 1 个任务和 0 个事件完成...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 返回 1 个动作
== APP == 收到 "Activity Request" 工作项
== APP == 活动 hello 完成,输出 "Hello Seattle!" (16 个字符)
== APP == 收到实例 id 为 'dc040bea-6436-4051-9166-c9294f9d2201' 的 "Orchestrator Request" 工作项
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 使用 6 个历史事件重建本地状态...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 处理 2 个新历史事件:[ORCHESTRATORSTARTED=1, TASKCOMPLETED=1]
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 等待 1 个任务和 0 个事件完成...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 返回 1 个动作
== APP == 收到 "Activity Request" 工作项
== APP == 活动 hello 完成,输出 "Hello London!" (15 个字符)
== APP == 收到实例 id 为 'dc040bea-6436-4051-9166-c9294f9d2201' 的 "Orchestrator Request" 工作项
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 使用 9 个历史事件重建本地状态...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 处理 2 个新历史事件:[ORCHESTRATORSTARTED=1, TASKCOMPLETED=1]
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 编排完成,状态为 COMPLETED
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 返回 1 个动作
INFO[0006] dc040bea-6436-4051-9166-c9294f9d2201: 'sequence' 完成,状态为 COMPLETED。 app_id=activity-sequence-workflow instance=kaibocai-devbox scope=wfengine.backend type=log ver=1.12.3
== APP == 实例 dc040bea-6436-4051-9166-c9294f9d2201 完成
== APP == 编排完成!结果:["Hello Tokyo!","Hello Seattle!","Hello London!"]

下一步

3.5 - Dapr PHP SDK

用于开发Dapr应用的PHP SDK包

Dapr提供了一个SDK,帮助开发PHP应用程序。通过它,您可以使用Dapr创建PHP客户端、服务器和虚拟actor。

设置

先决条件

可选先决条件

初始化您的项目

在您希望创建服务的目录中,运行composer init并回答提示的问题。 使用composer require dapr/php-sdk安装此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'                  => [],
    
    // 配置Dapr在多长时间后认为actor空闲
    'dapr.actors.idle_timeout'     => null,
    
    // 配置Dapr检查空闲actor的频率
    'dapr.actors.scan_interval'    => null,
    
    // 配置Dapr在关闭期间等待actor完成的时间
    'dapr.actors.drain_timeout'    => null,
    
    // 配置Dapr是否应等待actor完成
    'dapr.actors.drain_enabled'    => null,
    
    // 您可以在此处更改Dapr的端口设置
    '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启动项目。

您现在可以打开一个网页浏览器并访问http://localhost:3000/hello/world,将world替换为您的名字、宠物的名字或您想要的任何内容。

恭喜,您已经创建了您的第一个Dapr服务!期待看到您会用它做些什么!

更多信息

3.5.1 - 虚拟Actor

如何构建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 State

actor状态是一个扩展ActorState的“普通旧PHP对象”(POPO)。ActorState基类提供了一些有用的方法。以下是一个示例实现:

<?php
class CountState extends \Dapr\Actors\ActorState {
    public int $count = 0;
}

注册Actor

Dapr期望在启动时知道服务可能托管的actor。你需要将其添加到配置中:

如果你想利用预编译的依赖注入,你需要使用工厂:

<?php
// 在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
// 在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();

3.5.1.1 - 生产参考:actor

在生产环境中执行PHP角色

代理模式

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";
        }
    }
}

然后在composer.json中为生成的代理添加一个psr-4自动加载器和一个脚本:

{
  "autoload": {
    "psr-4": {
      "Dapr\\Proxies\\": "path/to/proxies"
    }
  },
  "scripts": {
    "compile-proxies": "ProxyCompiler::compile"
  }
}

最后,配置dapr仅使用生成的代理:

<?php
// 在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特性。这允许快速反射和一些快捷方式。使用\Dapr\Actors\Actor抽象基类可以为您做到这一点,但如果您需要覆盖默认行为,可以通过实现接口和使用特性来实现。

激活和停用

当actor激活时,会将一个令牌文件写入临时目录(默认情况下在Linux中为'/tmp/dapr_' + sha256(concat(Dapr type, id)),在Windows中为'%temp%/dapr_' + sha256(concat(Dapr type, id)))。这会一直保留到actor停用或主机关闭。这允许在Dapr在主机上激活actor时仅调用一次on_activation

性能

在使用php-fpmnginx或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中没有这样的东西。例如,在这个示例实现中,保留了以前的值,以防在升级期间可能出现错误;保留以前的值允许再次运行升级,但您可能希望删除以前的值。

3.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');

接收云事件

在您的订阅处理程序中,您可以让 DI 容器将 Dapr\PubSub\CloudEventarray 注入到您的控制器中。使用 Dapr\PubSub\CloudEvent 时,会进行一些验证以确保事件的正确性。如果您需要直接访问数据,或者事件不符合规范,请使用 array

3.5.3 - 应用程序

使用 App 类

PHP 中没有默认的路由器。因此,提供了 \Dapr\App 类。它底层使用了 Nikic 的 FastRoute。然而,你可以选择任何你喜欢的路由器或框架。只需查看 App 类中的 add_dapr_routes() 方法,了解 actor 和订阅是如何实现的。

每个应用程序都应该以 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);

// 你也可以传递一个日志记录器
$clientBuilder = $clientBuilder->withLogger($myLogger);

// 并更改 sidecar 的 URL,例如,使用 https
$clientBuilder = $clientBuilder->useHttpClient('https://localhost:3800') 

在调用之前有几个函数可以使用

3.5.3.1 - 单元测试

单元测试

在 PHP SDK 中,单元测试和集成测试是非常重要的组成部分。通过使用依赖注入容器、模拟、存根以及提供的 \Dapr\Mocks\TestClient,可以实现非常精细的测试。

测试 Actor

在测试 Actor 时,我们主要关注两个方面:

  1. 基于初始状态的返回结果
  2. 基于初始状态的结果状态

以下是一个简单的 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://v1-16.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);
    }
}

3.5.4 - 使用 PHP 进行状态管理

如何使用

Dapr 提供了一种模块化的状态管理方法,适用于您的应用程序。要学习基础知识,请访问 如何操作

元数据

许多状态组件允许您传递元数据给组件,以控制组件行为的特定方面。PHP SDK 允许您通过以下方式传递这些元数据:

<?php
// 使用状态管理器
$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-');
});

3.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 自带的类。对于这些情况,您需要在依赖注入容器中注册一个自定义序列化器:

<?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

3.6 - Dapr Python SDK

用于开发Dapr应用的Python SDK包

Dapr 提供了多种子包以帮助开发 Python 应用程序。通过这些子包,您可以使用 Dapr 创建 Python 客户端、服务器和虚拟 actor。

先决条件

安装

要开始使用 Python SDK,请安装主要的 Dapr Python SDK 包。

pip install dapr

注意: 开发包包含与 Dapr 运行时预发布版本兼容的功能和行为。在安装 dapr-dev 包之前,请确保卸载任何稳定版本的 Python SDK。

pip install dapr-dev

可用子包

SDK 导入

Python SDK 导入是随主 SDK 安装一起包含的子包,但在使用时需要导入。Dapr Python SDK 提供的常用导入包括:

Client

编写 Python 应用以与 Dapr sidecar 和其他 Dapr 应用交互,包括 Python 中的有状态虚拟 actor。

Actors

创建和与 Dapr 的 actor 框架交互。

了解 所有可用的 Dapr Python SDK 导入 的更多信息。

SDK 扩展

SDK 扩展主要用于接收 pub/sub 事件、程序化创建 pub/sub 订阅和处理输入绑定事件。虽然这些任务可以在没有扩展的情况下完成,但使用 Python SDK 扩展会更加方便。

gRPC

使用 gRPC 服务器扩展创建 Dapr 服务。

FastAPI

使用 Dapr FastAPI 扩展与 Dapr Python 虚拟 actor 和 pub/sub 集成。

Flask

使用 Dapr Flask 扩展与 Dapr Python 虚拟 actor 集成。

Workflow

编写与其他 Dapr API 一起工作的 Python 工作流。

了解 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 的指标收集、跟踪、日志记录和健康检查功能。
Pub/sub 教程查看 Dapr Python SDK 如何与其他 Dapr SDK 一起工作以启用 pub/sub 应用。

更多信息

Serialization

了解有关 Dapr SDK 中的序列化的更多信息。

PyPI

Python 包索引

3.6.1 - 使用 Dapr 客户端 Python SDK 入门

如何使用 Dapr Python SDK 快速上手

Dapr 客户端包使您能够从 Python 应用程序与其他 Dapr 应用程序进行交互。

准备工作

在开始之前,安装 Dapr Python 包

导入客户端包

dapr 包包含 DaprClient,用于创建和使用客户端。

from dapr.clients import DaprClient

初始化客户端

您可以通过多种方式初始化 Dapr 客户端:

默认值:

如果不提供参数初始化客户端,它将使用 Dapr sidecar 实例的默认值 (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 Sidecar 端点

您可以使用标准化的 DAPR_GRPC_ENDPOINT 环境变量来指定 gRPC 端点。当设置了此变量时,可以在没有任何参数的情况下初始化客户端:

export DAPR_GRPC_ENDPOINT="mydomain:50051?tls=true"
from dapr.clients import DaprClient

with DaprClient() as d:
    # 客户端将使用环境变量中指定的端点

旧的环境变量 DAPR_RUNTIME_HOSTDAPR_HTTP_PORTDAPR_GRPC_PORT 也被支持,但 DAPR_GRPC_ENDPOINT 优先。

Dapr API 令牌

如果您的 Dapr 实例配置为需要 DAPR_API_TOKEN 环境变量,您可以在环境中设置它,客户端将自动使用它。
您可以在这里阅读更多关于 Dapr API 令牌认证的信息。

健康检查超时

客户端初始化时,会对 Dapr sidecar (/healthz/outbound) 进行健康检查。客户端将在 sidecar 启动并运行后继续。

默认的健康检查超时时间为 60 秒,但可以通过设置 DAPR_HEALTH_TIMEOUT 环境变量来覆盖。

重试和超时

如果从 sidecar 收到特定错误代码,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:
    ...

或对于 actor:

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,一个旨在改善开发者体验的自定义异常类。
需要注意的是,过渡到使用 DaprGrpcError 处理所有 gRPC 状态异常仍在进行中。目前,SDK 中的每个 API 调用尚未更新以利用此自定义异常。我们正在积极进行此增强,并欢迎社区的贡献。

使用 Dapr python-SDK 处理 DaprGrpcError 异常的示例:

try:
    d.save_state(store_name=storeName, key=key, value=value)
except DaprGrpcError as err:
    print(f'状态代码: {err.code()}')
    print(f"消息: {err.message()}")
    print(f"错误代码: {err.error_code()}")
    print(f"错误信息(原因): {err.error_info.reason}")
    print(f"资源信息 (资源类型): {err.resource_info.resource_type}")
    print(f"资源信息 (资源名称): {err.resource_info.resource_name}")
    print(f"错误请求 (字段): {err.bad_request.field_violations[0].field}")
    print(f"错误请求 (描述): {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_HOSTDAPR_HTTP_PORT 变量派生,其默认值分别为 127.0.0.13500

gRPC 调用的基本端点是用于客户端初始化的端点(如上所述)。

保存和获取应用程序状态

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")

查询应用程序状态 (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"},  # 可选
        )

发布和订阅

发布消息

from dapr.clients import DaprClient

with DaprClient() as d:
    resp = d.publish_event(pubsub_name='pubsub', topic_name='TOPIC_A', data='{"message":"Hello World"}')

订阅消息

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'接收到: id={data["id"]}, message="{data ["message"]}"' 
          ' content_type="{event.content_type}"',flush=True)

# 使用 Pub/Sub 路由的特定处理程序
@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'接收到: id={data["id"]}, message="{data ["message"]}"' 
          ' content_type="{event.content_type}"',flush=True)

流式消息订阅

您可以使用 subscribesubscribe_handler 方法创建对 PubSub 主题的流式订阅。

subscribe 方法返回一个 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

counter = 0


def process_message(message):
    global counter
    counter += 1
    # 在此处处理消息
    print(f'处理消息: {message.data()} 来自 {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:
            while counter < 5:
                try:
                    message = subscription.next_message()

                except StreamInactiveError as e:
                    print('流不活跃。重试...')
                    time.sleep(1)
                    continue
                if message is None:
                    print('在超时时间内未收到消息。')
                    continue

                # 处理消息
                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("关闭订阅...")
            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'处理消息: {message.data()} 来自 {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("关闭订阅...")
        close_fn()


if __name__ == '__main__':
    main()

与输出绑定交互

from dapr.clients import DaprClient

with DaprClient() as d:
    resp = d.invoke_binding(binding_name='kafkaBinding', operation='create', data='{"message":"Hello World"}')

检索秘密

from dapr.clients import DaprClient

with DaprClient() as d:
    resp = d.get_secret(store_name='localsecretstore', key='secretKey')

配置

获取配置

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 秒内等待 sidecar 启动。
        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"订阅键={key} 值={item.value} 版本={item.version}", flush=True)
            else:
                print("尚无内容")
        sleep(5)

asyncio.run(executeConfiguration())

分布式锁

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('将尝试从名为 [%s] 的锁存储中获取锁' % store_name)
        print('锁是为名为 [%s] 的资源准备的' % resource_id)
        print('客户端标识符是 [%s]' % client_id)
        print('锁将在 %s 秒后过期。' % expiry_in_seconds)

        with dapr.try_lock(store_name, resource_id, client_id, expiry_in_seconds) as lock_result:
            assert lock_result.success, '获取锁失败。中止。'
            print('锁获取成功!!!')

        # 此时锁已释放 - 通过 `with` 子句的魔力 ;)
        unlock_result = dapr.unlock(store_name, resource_id, client_id)
        print('我们已经释放了锁,因此解锁将不起作用。')
        print('我们仍然尝试解锁它,并得到了 [%s]' % unlock_result.status)

加密

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"

工作流

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"

def main():
    with DaprClient() as d:
        host = settings.DAPR_RUNTIME_HOST
        port = settings.DAPR_GRPC_PORT
        workflowRuntime = WorkflowRuntime(host, port)
        workflowRuntime = WorkflowRuntime()
        workflowRuntime.register_workflow(hello_world_wf)
        workflowRuntime.register_activity(hello_act)
        workflowRuntime.start()

        # 启动工作流
        start_resp = d.start_workflow(instance_id=instanceId, workflow_component=workflowComponent,
                        workflow_name=workflowName, input=inputData, workflow_options=workflowOptions)
        print(f"start_resp {start_resp.instance_id}")

        # ...

        # 暂停测试
        d.pause_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        print(f"从 {workflowName} 获取暂停调用后的响应: {getResponse.runtime_status}")

        # 恢复测试
        d.resume_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        print(f"从 {workflowName} 获取恢复调用后的响应: {getResponse.runtime_status}")
        
        sleep(1)
        # 触发事件
        d.raise_workflow_event(instance_id=instanceId, workflow_component=workflowComponent,
                    event_name=eventName, event_data=eventData)

        sleep(5)
        # 清除测试
        d.purge_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        try:
            getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        except DaprInternalError as err:
            if nonExistentIDError in err._message:
                print("实例成功清除")

        
        # 启动另一个工作流以进行终止
        # 这也将测试在旧实例被清除后在新工作流上使用相同的实例 ID
        start_resp = d.start_workflow(instance_id=instanceId, workflow_component=workflowComponent,
                        workflow_name=workflowName, input=inputData, workflow_options=workflowOptions)
        print(f"start_resp {start_resp.instance_id}")

        # 终止测试
        d.terminate_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        sleep(1)
        getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        print(f"从 {workflowName} 获取终止调用后的响应: {getResponse.runtime_status}")

        # 清除测试
        d.purge_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        try:
            getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        except DaprInternalError as err:
            if nonExistentIDError in err._message:
                print("实例成功清除")

        workflowRuntime.shutdown()

相关链接

Python SDK 示例

3.6.2 - 使用 Dapr actor Python SDK 入门

如何使用 Dapr Python SDK 快速上手

Dapr actor 包使您能够从 Python 应用程序与 Dapr 虚拟 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 服务负责托管虚拟 actor。它是一个从基类 Actor 派生并实现 actor 接口中定义的类。

可以使用以下 Dapr 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 示例。

3.6.3 - Dapr Python SDK 插件

用于开发 Dapr 应用的 Python SDK 工具

3.6.3.1 - 开始使用 Dapr Python gRPC 服务扩展

如何启动并运行 Dapr Python gRPC 扩展

Dapr Python SDK 提供了一个用于创建 Dapr 服务的内置 gRPC 服务器扩展 dapr.ext.grpc

安装

您可以通过以下命令下载并安装 Dapr gRPC 服务器扩展:

pip install dapr-ext-grpc
pip3 install dapr-ext-grpc-dev

示例

您可以使用 App 对象来创建一个服务器。

监听服务调用请求

可以使用 InvokeMethodRequestInvokeMethodResponse 对象来处理传入的请求。

以下是一个简单的服务示例,它会监听并响应请求:

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)

完整示例可以在这里找到。

相关链接

3.6.3.2 - Dapr Python SDK 与 FastAPI 集成指南

如何使用 FastAPI 扩展创建 Dapr Python actor 和发布订阅功能

Dapr Python SDK 通过 dapr-ext-fastapi 扩展实现与 FastAPI 的集成。

安装

您可以通过以下命令下载并安装 Dapr FastAPI 扩展:

pip install dapr-ext-fastapi
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 = 'Jane Doe'

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__}服务')

# 添加 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'}"

3.6.3.3 - Dapr Python SDK 与 Flask 集成

如何使用 Flask 扩展创建 Dapr Python 虚拟 actor

Dapr Python SDK 使用 flask-dapr 扩展来实现与 Flask 的集成。

安装

您可以通过以下命令下载并安装 Dapr Flask 扩展:

pip install flask-dapr
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)

3.6.3.4 - Dapr Python SDK 与 Dapr Workflow 扩展集成

如何使用 Dapr Workflow 扩展快速上手

Dapr Python SDK 内置了一个 Dapr Workflow 扩展,dapr.ext.workflow,用于创建 Dapr 服务。

安装

您可以通过以下命令下载并安装 Dapr Workflow 扩展:

pip install dapr-ext-workflow
pip3 install dapr-ext-workflow-dev

下一步

开始使用 Dapr Workflow Python SDK

3.6.3.4.1 - 使用 Dapr Workflow Python SDK 入门

如何使用 Dapr Python SDK 开始并运行工作流

我们来创建一个 Dapr 工作流,并通过控制台调用它。通过提供的 hello world 工作流示例,您将会:

此示例使用 dapr init 的默认配置在本地模式下运行。

在 Python 示例项目中,app.py 文件包含应用程序的设置,其中包括:

  • 工作流定义
  • 工作流活动定义
  • 工作流和工作流活动的注册

先决条件

设置环境

运行以下命令以安装使用 Dapr Python SDK 运行此工作流示例的必要依赖。

pip3 install -r demo_workflow/requirements.txt

克隆 [Python SDK 仓库]。

git clone https://github.com/dapr/python-sdk.git

从 Python SDK 根目录导航到 Dapr 工作流示例。

cd examples/demo_workflow

本地运行应用程序

要运行 Dapr 应用程序,您需要启动 Python 程序和一个 Dapr 辅助进程。在终端中运行:

dapr run --app-id orderapp --app-protocol grpc --dapr-grpc-port 50001 --resources-path components --placement-host-address localhost:50005 -- python3 app.py

注意: 由于 Windows 中未定义 Python3.exe,您可能需要使用 python app.py 而不是 python3 app.py

预期输出

== APP == ==========根据输入开始计数器增加==========

== APP == start_resp exampleInstanceID

== APP == 你好,计数器!
== APP == 新的计数器值是:1!

== APP == 你好,计数器!
== APP == 新的计数器值是:11!

== APP == 你好,计数器!
== APP == 你好,计数器!
== APP == 在暂停调用后从 hello_world_wf 获取响应:已暂停

== APP == 你好,计数器!
== APP == 在恢复调用后从 hello_world_wf 获取响应:运行中

== APP == 你好,计数器!
== APP == 新的计数器值是:111!

== APP == 你好,计数器!
== APP == 实例成功清除

== APP == start_resp exampleInstanceID

== APP == 你好,计数器!
== APP == 新的计数器值是:1112!

== APP == 你好,计数器!
== APP == 新的计数器值是:1122!

== APP == 在终止调用后从 hello_world_wf 获取响应:已终止
== APP == 在终止调用后从 child_wf 获取响应:已终止
== APP == 实例成功清除

发生了什么?

当您运行 dapr run 时,Dapr 客户端:

  1. 注册了工作流 (hello_world_wf) 及其活动 (hello_act)
  2. 启动了工作流引擎
def main():
    with DaprClient() as d:
        host = settings.DAPR_RUNTIME_HOST
        port = settings.DAPR_GRPC_PORT
        workflowRuntime = WorkflowRuntime(host, port)
        workflowRuntime = WorkflowRuntime()
        workflowRuntime.register_workflow(hello_world_wf)
        workflowRuntime.register_activity(hello_act)
        workflowRuntime.start()

        print("==========根据输入开始计数器增加==========")
        start_resp = d.start_workflow(instance_id=instanceId, workflow_component=workflowComponent,
                        workflow_name=workflowName, input=inputData, workflow_options=workflowOptions)
        print(f"start_resp {start_resp.instance_id}")

然后 Dapr 暂停并恢复了工作流:

       # 暂停
        d.pause_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        print(f"在暂停调用后从 {workflowName} 获取响应:{getResponse.runtime_status}")

        # 恢复
        d.resume_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        print(f"在恢复调用后从 {workflowName} 获取响应:{getResponse.runtime_status}")

一旦工作流恢复,Dapr 触发了一个工作流事件并打印了新的计数器值:

        # 触发事件
        d.raise_workflow_event(instance_id=instanceId, workflow_component=workflowComponent,
                    event_name=eventName, event_data=eventData)

为了从您的状态存储中清除工作流状态,Dapr 清除了工作流:

        # 清除
        d.purge_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        try:
            getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        except DaprInternalError as err:
            if nonExistentIDError in err._message:
                print("实例成功清除")

然后示例演示了通过以下步骤终止工作流:

  • 使用与已清除工作流相同的 instanceId 启动一个新的工作流。
  • 在关闭工作流之前终止并清除工作流。
        # 启动另一个工作流
        start_resp = d.start_workflow(instance_id=instanceId, workflow_component=workflowComponent,
                        workflow_name=workflowName, input=inputData, workflow_options=workflowOptions)
        print(f"start_resp {start_resp.instance_id}")

        # 终止
        d.terminate_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        sleep(1)
        getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        print(f"在终止调用后从 {workflowName} 获取响应:{getResponse.runtime_status}")

        # 清除
        d.purge_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        try:
            getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        except DaprInternalError as err:
            if nonExistentIDError in err._message:
                print("实例成功清除")

下一步

3.7 - Dapr Rust SDK

用于开发Dapr应用的Rust SDK包

这是一个帮助开发者使用Rust构建Dapr应用的客户端库。该客户端旨在支持所有公共的Dapr API,同时注重提供符合Rust习惯的开发体验和提升开发者的工作效率。

客户端

使用Rust客户端SDK调用公共的Dapr API [**了解更多关于Rust客户端SDK的信息**](https://v1-16.docs.dapr.io/zh-hans/developing-applications/sdks/rust/rust-client/)

3.7.1 - 使用 Dapr 客户端 Rust SDK 入门

如何使用 Dapr Rust SDK 快速上手

Dapr 客户端库使您能够从 Rust 应用程序与其他 Dapr 应用程序进行交互。

前提条件

引入客户端库

在您的 cargo.toml 文件中添加 Dapr

[dependencies]
# 其他依赖项
dapr = "0.13.0"

您可以引用 dapr::Client,或者将其完整路径绑定到一个新名称,如下所示:

use dapr::Client as DaprClient

实例化 Dapr 客户端

const addr: String = "https://127.0.0.1";
const port: String = "50001";

let mut client = dapr::Client::<dapr::client::TonicClient>::connect(addr,
    port).await?;

功能模块

Rust SDK 允许您与 Dapr 功能模块 进行交互。

服务调用

要在运行 Dapr sidecar 的另一个服务上调用特定方法,Dapr 客户端 Go SDK 提供了以下选项:

调用服务

let response = client
    .invoke_service("service-to-invoke", "method-to-invoke", Some(data))
    .await
    .unwrap();

有关服务调用的完整指南,请访问 如何:调用服务

状态管理

Dapr 客户端提供对状态管理方法的访问:save_stateget_statedelete_state,可以像这样使用:

let store_name = "store-name";
let state_key = "state-key";

let states = vec![(state_key, ("state-value").as_bytes().to_vec())];

// 使用键 "state-key" 和值 "state-value" 保存状态
client.save_state(store_name, states).await?;

// 获取键 "state-key" 的状态
let response = client.get_state(store_name, state_key, None).await.unwrap();

// 删除键 "state-key" 的状态
client.delete_state(store_name, state_key, None).await?;

注意: save_state 方法目前执行的是批量保存,但未来可能会进行重构

有关状态管理的完整指南,请访问 如何:保存和获取状态

发布消息

要将数据发布到主题上,Dapr Go 客户端提供了一种简单的方法:

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?;

有关发布/订阅的完整指南,请访问 如何:发布和订阅

相关链接

Rust SDK 示例

3.8 - Dapr SDK中的序列化

Dapr如何在SDK中序列化数据

Dapr的SDK应该提供两种用例的序列化功能。首先是通过请求和响应负载发送的API对象。其次是需要持久化的对象。对于这两种用例,SDK提供了默认的序列化。在Java SDK中,使用DefaultObjectSerializer类来进行JSON序列化。

服务调用

    DaprClient client = (new DaprClientBuilder()).build();
    client.invokeService("myappid", "saySomething", "My Message", HttpExtension.POST).block();

在上面的示例中,应用程序会收到一个针对saySomething方法的POST请求,请求负载为"My Message" - 引号是因为序列化器会将输入字符串序列化为JSON格式。

POST /saySomething HTTP/1.1
Host: localhost
Content-Type: text/plain
Content-Length: 12

"My Message"

状态管理

    DaprClient client = (new DaprClientBuilder()).build();
    client.saveState("MyStateStore", "MyKey", "My Message").block();

在此示例中,My Message将被保存。它没有加引号,因为Dapr的API会在内部解析JSON请求对象后再保存它。

[
    {
        "key": "MyKey",
        "value": "My Message"
    }
]

发布订阅

  DaprClient client = (new DaprClientBuilder()).build();
  client.publishEvent("TopicName", "My Message").block();

事件被发布,内容被序列化为byte[]并发送到Dapr sidecar。订阅者将以CloudEvent的形式接收它。CloudEvent定义data为字符串。Dapr SDK还为CloudEvent对象提供了内置的反序列化器。

  @PostMapping(path = "/TopicName")
  public void handleMessage(@RequestBody(required = false) byte[] body) {
      // Dapr的事件符合CloudEvent。
      CloudEvent event = CloudEvent.deserialize(body);
  }

绑定

在这种情况下,对象也被序列化为byte[],输入绑定接收原始的byte[]并将其反序列化为预期的对象类型。

  • 输出绑定:
    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仅支持具有零个或一个参数的方法。

  • 调用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 String actorMethod(String message) {
    // 从键读取状态并将其反序列化为字符串。
    String previousMessage = super.getActorStateManager().get("lastmessage", String.class).block();

    // 在序列化后为键设置新状态。
    super.getActorStateManager().set("lastmessage", message).block();
    return previousMessage;
}

默认序列化器

Dapr的默认序列化器是一个JSON序列化器,具有以下期望:

  1. 使用基本的JSON数据类型以实现跨语言和跨平台的兼容性:字符串、数字、数组、布尔值、null和另一个JSON对象。应用程序可序列化对象中的每个复杂属性类型(例如DateTime)都应表示为JSON的基本类型之一。
  2. 使用默认序列化器持久化的数据也应保存为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."}
  1. 自定义序列化器必须将对象序列化为byte[]
  2. 自定义序列化器必须将byte[]反序列化为对象。
  3. 当用户提供自定义序列化器时,它应作为byte[]传输或持久化。持久化时,也要编码为Base64字符串。这是大多数JSON库本地完成的。
redis-cli MGET "ActorStateIT_StatefulActorService||StatefulActorTest||1581130928192||message
"VGhpcyBpcyBhIG1lc3NhZ2UgdG8gYmUgc2F2ZWQgYW5kIHJldHJpZXZlZC4="
 redis-cli MGET "ActorStateIT_StatefulActorService||StatefulActorTest||1581130928192||mydata
"eyJ2YWx1ZSI6Ik15IGRhdGEgdmFsdWUuIn0="

截至目前,Java SDK是唯一实现此规范的Dapr SDK。在不久的将来,其他SDK也将实现相同的功能。

4 - 组件

了解更多关于 Dapr 的可插拔组件和中间件的开发

4.1 - 可插拔组件

关于如何使用可插拔组件的指南

4.1.1 - 可插拔组件概述

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

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

可插拔组件与内置组件

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

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

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

为什么创建可插拔组件?

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

  • 您需要一个私有组件。
  • 您希望将组件与 Dapr 的发布过程分开。
  • 您对 Go 不太熟悉,或者用 Go 实现组件并不理想。

特性

实现可插拔组件

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

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

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

为组件利用多个构建块

除了从同一组件实现多个 gRPC 服务(例如 StateStoreQueriableStateStoreTransactionalStateStore 等),可插拔组件还可以为其他组件接口提供实现。这意味着单个可插拔组件可以同时作为状态存储、pub/sub 和输入或输出绑定工作。换句话说,您可以将多个组件接口实现到一个可插拔组件中,并将其作为 gRPC 服务公开。

虽然在同一个可插拔组件上公开多个组件接口降低了部署多个组件的操作负担,但这使得实现和调试组件变得更加困难。如果不确定,请坚持“关注点分离”,仅在必要时将多个组件接口合并到同一个可插拔组件中。

如何使可插拔组件投入使用

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

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

下一步

4.1.2 - 如何:实现可插拔组件

学习如何编写和实现可插拔组件

在本指南中,您将学习实现可插拔组件的原因和方法。要了解如何配置和注册可插拔组件,请参阅如何:注册可插拔组件

实现可插拔组件

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

找到 proto 定义文件

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

目前支持以下组件 API:

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

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

// 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)

创建服务脚手架

使用 protocol buffers 和 gRPC 工具生成服务的脚手架。通过 gRPC 概念文档了解更多关于这些工具的信息。

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

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

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

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

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

定义服务

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

  • 状态存储

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

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

  • 发布订阅

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

  • 绑定

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

  • 密钥存储

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

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

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

返回语义错误

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

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

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

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

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

ETag 不匹配

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

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

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

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

ETag 无效

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

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

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

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

批量删除行不匹配

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

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

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

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

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

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

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

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

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

ETag 不匹配

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

ETag 无效

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

批量删除行不匹配

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

ETag 不匹配

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

ETag 无效

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

批量删除行不匹配

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

下一步

4.1.3 - 可插拔组件的SDK

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

Dapr SDK 是帮助您轻松创建可插拔组件的最佳工具。选择您喜欢的编程语言,几分钟内即可开始开发组件。

可插拔组件的SDK

语言进度
Go正在开发
.NET正在开发

4.1.3.1 - 开始使用 Dapr 可插拔组件 .NET SDK

如何使用 Dapr 可插拔组件 .NET SDK 快速上手

Dapr 提供了用于开发 .NET 可插拔组件的 NuGet 包。

前提条件

创建项目

要创建一个可插拔组件,首先从一个空的 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 =>
    {
        // 使用此服务注册一个或多个组件。
    });

app.Run();

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

  • 对应一个 Unix 域套接字
  • 可以托管一个或多个组件类型

实现和注册组件

本地测试组件

可插拔组件可以通过在命令行启动应用程序并配置一个 Dapr sidecar 来进行测试。

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

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 sidecar 将已启动并通过 Unix 域套接字连接到组件。然后您可以通过以下方式与组件交互:

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

创建容器

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

使用 .NET SDK

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

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

dotnet add package Microsoft.NET.Build.Containers

将应用程序发布为容器:

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

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

使用 Dockerfile

虽然有工具可以为 .NET 应用程序生成 Dockerfile,但 .NET SDK 本身并不提供。一个典型的 Dockerfile 可能如下所示:

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

# 创建一个具有显式 UID 的非 root 用户,并添加访问 /app 文件夹的权限
# 更多信息,请参阅 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> .

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

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

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

添加绑定相关的命名空间

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

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

输入绑定:实现 IInputBinding

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

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

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

ReadAsync() 方法的调用是“长时间运行”的,因为在取消之前不期望返回(例如,通过 cancellationToken)。当从组件的底层存储中读取消息时,它们通过 deliveryHandler 回调传递给 Dapr 运行时。这样,组件可以在应用程序(由 Dapr 运行时服务)确认消息处理时接收通知。

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

        // 在取消之前轮询底层存储...
        while (!cancellationToken.IsCancellationRequested)
        {
            var messages = // 从底层存储中轮询消息...

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

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

输出绑定:实现 IOutputBinding

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

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

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

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

输入和输出绑定组件

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

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

    // IOutputBinding 实现...
}

注册绑定组件

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

using Dapr.PluggableComponents;

var app = DaprPluggableComponentsApplication.Create();

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

app.Run();

4.1.3.1.2 - 实现 .NET 发布/订阅组件

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

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

添加发布/订阅命名空间

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

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

实现 IPubSub

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

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

    public Task PublishAsync(PubSubPublishRequest request, CancellationToken cancellationToken = default)
    {
        // 将消息发送到指定的“主题”...
    }

    public Task PullMessagesAsync(PubSubPullMessagesTopic topic, MessageDeliveryHandler<string?, PubSubPullMessagesResponse> deliveryHandler, CancellationToken cancellationToken = default)
    {
        // 持续检查主题中的消息并将其传递给 Dapr 运行时,直到取消为止...
    }
}

PullMessagesAsync() 方法是一个“长时间运行”的调用,因为在取消之前不期望返回(例如,通过 cancellationToken)。需要从中提取消息的“主题”通过 topic 参数传递,而传递到 Dapr 运行时是通过 deliveryHandler 回调执行的。传递机制允许组件在应用程序(由 Dapr 运行时服务)确认消息处理时接收通知。

    public async Task PullMessagesAsync(PubSubPullMessagesTopic topic, MessageDeliveryHandler<string?, PubSubPullMessagesResponse> deliveryHandler, CancellationToken cancellationToken = default)
    {
        TimeSpan pollInterval = // 轮询间隔(可以从初始化元数据中获取)...

        // 持续轮询主题直到取消...
        while (!cancellationToken.IsCancellationRequested)
        {
            var messages = // 从主题中轮询获取消息...

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

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

注册发布/订阅组件

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

using Dapr.PluggableComponents;

var app = DaprPluggableComponentsApplication.Create();

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

app.Run();

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

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

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

添加状态存储命名空间

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

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

实现 IStateStore

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

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

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

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

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

注册状态存储组件

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

using Dapr.PluggableComponents;

var app = DaprPluggableComponentsApplication.Create();

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

app.Run();

支持批量操作的状态存储

如果状态存储打算支持批量操作,应实现可选的 IBulkStateStore 接口。其方法与基础 IStateStore 接口的方法相似,但包含多个请求的值。

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

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

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

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

事务性状态存储

如果状态存储打算支持事务,应实现可选的 ITransactionalStateStore 接口。其 TransactAsync() 方法接收一个请求,其中包含要在事务中执行的删除和/或设置操作序列。状态存储应遍历这些操作,并调用每个操作的 Visit() 方法,传递相应的回调以处理每种操作类型。

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

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

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

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

            throw;
        }

        // 提交事务...
    }
}

可查询状态存储

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

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

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

ETag 和其他语义错误处理

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

异常适用操作描述
ETagInvalidException删除、设置、批量删除、批量设置当 ETag 无效时
ETagMismatchException删除、设置、批量删除、批量设置当 ETag 与预期值不匹配时
BulkDeleteRowMismatchException批量删除当受影响的行数与预期行数不匹配时

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

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

尽管大多数情况下不需要,但这些指南提供了配置 .NET 可插拔组件的高级方法。

4.1.3.1.4.1 - 在 .NET Dapr 可插拔组件中使用多个服务

如何从 .NET 可插拔组件中暴露多个服务

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

  • 以减少集群中运行的sidecar数量
  • 以便将可能共享库和实现的相关组件进行分组,例如:
    • 一个既作为通用状态存储又作为
    • 允许更具体操作的输出绑定。

每个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: []

4.1.3.1.4.2 - .NET Dapr 插件组件的应用环境配置

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

.NET Dapr 插件组件应用可以配置依赖注入、日志记录和配置值,类似于 ASP.NET 应用。DaprPluggableComponentsApplication 提供了一组与 WebApplicationBuilder 类似的配置属性。

依赖注入

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

var app = DaprPluggableComponentsApplication.Create();

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

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

app.Run();

interface IService
{
    // ...
}

class MyService : IService
{
    // ...
}

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

    // ...
}

日志记录

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

var app = DaprPluggableComponentsApplication.Create();

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

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

app.Run();

class MyStateStore : IStateStore
{
    // 在创建 state 存储时注入日志记录器。
    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
{
    // 在创建 state 存储时注入配置。
    public MyStateStore(IConfiguration configuration)
    {
        // ...
    }

    // ...
}

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

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

在 .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;
    }

    // ...
}

4.1.3.2 - 使用 Dapr 可插拔组件 Go SDK 入门

如何使用 Dapr 可插拔组件 Go SDK 快速上手

Dapr 提供了一些工具包,帮助开发者创建 Go 可插拔组件。

前置条件

创建应用程序

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

mkdir example
cd component
go mod init example

导入 Dapr 包

导入 Dapr 可插拔组件 SDK 包。

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

创建主包

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 使用组件,请在资源目录中创建一个组件 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 sidecar 将已启动并通过 Unix 域套接字连接到组件。然后,您可以通过以下方式与组件交互:

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

创建容器

可插拔组件作为容器部署,作为应用程序的 sidecar 运行(如同 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> .

下一步

4.1.3.2.1 - 实现一个 Go pub/sub 组件

如何使用 Dapr 可插拔组件 Go SDK 创建一个 pub/sub 组件

创建一个 pub/sub 组件只需几个基本步骤。

导入 pub/sub 包

创建文件 components/pubsub.go 并添加 import 语句以导入与 pub/sub 相关的包。

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 {
	// 设置一个长时间运行的机制来检索消息,直到取消为止,并将其传递给 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 {
			if ctx.Err() != nil {
				return
			}
	
			messages := // 轮询消息...

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

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

	return nil
}

注册 pub/sub 组件

在主应用程序文件中(例如,main.go),注册 pub/sub 组件。

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()
}

下一步

4.1.3.2.2 - 实现一个Go输入/输出绑定组件

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

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

导入绑定包

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

package components

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

输入绑定:实现 InputBinding 接口

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

type MyInputBindingComponent struct {
}

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

func (component *MyInputBindingComponent) Read(ctx context.Context, handler bindings.Handler) error {
	// 设置一个长期机制来检索消息,直到取消为止...
}

调用 Read() 方法时,应该设置一个长期运行的机制来检索消息,并立即返回 nil(如果无法设置该机制,则返回错误)。当取消时(例如,通过 ctx.Done()ctx.Err() != nil),该机制应停止。当从组件的底层存储读取消息时,它们通过 handler 回调传递给Dapr运行时,直到应用程序(由Dapr运行时服务)确认消息处理后才返回。

func (b *MyInputBindingComponent) Read(ctx context.Context, handler bindings.Handler) error {
	go func() {
		for {
			if ctx.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()
}

下一步

4.1.3.2.3 - 实现一个 Go 状态存储组件

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

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

导入状态存储包

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

package components

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

实现 Store 接口

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

type MyStateStore struct {
}

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

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

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

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

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

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

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

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

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

注册状态存储组件

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

package main

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

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

	dapr.MustRun()
}

批量操作的状态存储

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

事务性状态存储

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

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

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

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

	return nil
}

可查询的状态存储

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

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

ETag 和其他错误处理

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

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

下一步

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

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

尽管大多数情况下不需要使用这些高级配置方法,但本指南将展示如何在 Go 中配置 Dapr 的可插拔组件。

组件生命周期

可插拔组件通过传递一个“工厂方法”进行注册,该方法会在每个与 socket 关联的 Dapr 组件配置中被调用。这个方法返回与该 Dapr 组件相关联的实例(无论是否共享)。这使得多个相同类型的 Dapr 组件可以使用不同的元数据集进行配置,尤其是在需要组件操作相互隔离的情况下。

注册多个服务

每次调用 Register() 都会将一个 socket 绑定到一个注册的可插拔组件。每种组件类型(输入/输出绑定、pub/sub 和状态存储)可以在每个 socket 上注册一个。

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

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

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

	dapr.MustRun()
}

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

配置多个组件

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

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

下一步

4.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) {
      // 入站逻辑
      // ...

      // 调用下一个处理器
      next.ServeHTTP(w, r)

      // 出站逻辑
      // ...
    }
  }, err
}

相关链接

5 - 本地开发

Dapr应用的本地开发功能

5.1 - IDE 支持

支持常见的集成开发环境 (IDEs)

5.1.1 - Dapr 与 Visual Studio Code 的集成

在 Visual Studio Code 中如何高效地开发和运行 Dapr 应用程序

5.1.1.1 - Dapr Visual Studio Code 扩展概述

如何使用 Dapr 扩展开发和运行 Dapr 应用程序

Dapr 提供了一个预览版Dapr Visual Studio Code 扩展,专为本地开发设计。该扩展为用户提供多种功能,以便更好地管理 Dapr 应用程序,并调试支持的 Dapr 语言的应用程序,包括 .NET、Go、PHP、Python 和 Java。

在 VSCode 中打开

功能

脚手架 Dapr 调试任务

Dapr 扩展利用 Visual Studio Code 的内置调试功能帮助您调试应用程序。

通过 Dapr: Scaffold Dapr Tasks 命令面板操作,您可以更新现有的 task.jsonlaunch.json 文件,以便在开始调试时启动和配置 Dapr sidecar。

  1. 确保为您的应用程序设置了启动配置。(了解更多)
  2. 使用 Ctrl+Shift+P 打开命令面板
  3. 选择 Dapr: Scaffold Dapr Tasks
  4. 使用 F5 或通过运行视图运行您的应用程序和 Dapr sidecar。

脚手架 Dapr 组件

在将 Dapr 添加到应用程序时,您可能希望创建一个独立的组件目录,以区别于 dapr init 初始化的默认组件。

要使用默认的 statestorepubsubzipkin 组件创建一个专用的组件文件夹,请使用 Dapr: Scaffold Dapr Components 命令面板操作。

  1. 在 Visual Studio Code 中打开您的应用程序目录
  2. 使用 Ctrl+Shift+P 打开命令面板
  3. 选择 Dapr: Scaffold Dapr Components
  4. 使用 dapr run --resources-path ./components -- ... 运行您的应用程序

查看正在运行的 Dapr 应用程序

应用程序视图显示在您的机器上本地运行的 Dapr 应用程序。


Dapr VSCode 扩展视图运行应用程序选项的截图

调用 Dapr 应用程序

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


Dapr VSCode 扩展调用选项的截图

向 Dapr 应用程序发布事件

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

用户还可以向所有正在运行的 Dapr 应用程序发布消息。


Dapr VSCode 扩展发布选项的截图

其他资源

同时调试多个 Dapr 应用程序

使用 VS Code 扩展,您可以使用多目标调试同时调试多个 Dapr 应用程序。

社区电话演示

观看此视频,了解如何使用 Dapr VS Code 扩展:

5.1.1.2 - 如何:使用 Visual Studio Code 调试 Dapr 应用程序

学习如何配置 VSCode 以调试 Dapr 应用程序

手动调试

在开发 Dapr 应用程序时,通常使用 Dapr CLI 启动服务,命令如下:

dapr run --app-id nodeapp --app-port 3000 --dapr-http-port 3500 app.js

一种将调试器附加到服务的方法是先在命令行中使用正确的参数运行 daprd,然后启动代码并附加调试器。虽然这种方法可行,但需要额外的步骤,并且需要为那些可能克隆您的仓库并希望直接点击“播放”按钮开始调试的开发人员提供一些指导。

如果您的应用程序由多个微服务组成,并且每个微服务都有一个 Dapr 辅助进程,那么在 Visual Studio Code 中同时调试它们会非常有帮助。本页面将使用 hello world 快速入门 来展示如何配置 VSCode 以使用 VSCode 调试 调试多个 Dapr 应用程序。

先决条件

步骤 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_PORTDAPR_GRPC_PORT 环境变量。确保与 tasks.json 中的 httpPortgrpcPort 相匹配。例如,launch.json

{
  // 设置非默认的 HTTP 和 gRPC 端口
  "env": {
      "DAPR_HTTP_PORT": "3502",
      "DAPR_GRPC_PORT": "50002"
  },
}

tasks.json

{
  // 与 launch.json 中设置的端口匹配
  "httpPort": 3502,
  "grpcPort": 50002
}

每个配置都需要一个 requesttypename。这些参数帮助 VSCode 识别 .vscode/tasks.json 文件中的任务配置。

  • type 定义使用的语言。根据语言,可能需要在市场中找到的扩展,例如 Python 扩展
  • name 是配置的唯一名称。这用于在项目中调用多个配置时的复合配置。
  • ${workspaceFolder} 是一个 VS Code 变量引用。这是 VS Code 中打开的工作区的路径。
  • preLaunchTaskpostDebugTask 参数指的是在启动应用程序之前和之后运行的程序配置。请参阅步骤 2 了解如何配置这些。

有关 VSCode 调试参数的更多信息,请参阅 VS Code 启动属性

步骤 2:配置 tasks.json

对于 .vscode/launch.json 中定义的每个 任务,必须在 .vscode/tasks.json 中存在相应的任务定义。

对于快速入门,每个服务都需要一个任务来启动带有 daprd 类型的 Dapr 辅助进程,以及一个带有 daprd-down 的任务来停止辅助进程。参数 appIdhttpPortmetricsPortlabeltype 是必需的。还有其他可选参数可用,请参阅 参考表

{
    "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。用于服务发现、状态封装和 pub/sub 消费者 ID"appId": "divideapp"
appMaxConcurrency限制应用程序的并发性。有效值是大于 0 的任何数字"appMaxConcurrency": -1
appPort此参数告诉 Dapr 您的应用程序正在监听哪个端口"appPort": 4000
appProtocol告诉 Dapr 您的应用程序正在使用的协议。有效选项是 httpgrpchttpsgrpcsh2c。默认是 http"appProtocol": "http"
args设置传递给 Dapr 应用程序的参数列表“args”: []
componentsPath组件目录的路径。如果为空,则不会加载组件。"componentsPath": "./components"
config告诉 Dapr 使用哪个配置资源"config": "./config"
controlPlaneAddressDapr 控制平面的地址"controlPlaneAddress": "http://localhost:1366/"
enableProfiling启用分析"enableProfiling": false
enableMtls为 daprd 到 daprd 通信通道启用自动 mTLS"enableMtls": false
grpcPortDapr API 监听的 gRPC 端口(默认 “50001”)是,如果有多个应用"grpcPort": 50004
httpPortDapr API 的 HTTP 端口"httpPort": 3502
internalGrpcPortDapr 内部 API 监听的 gRPC 端口"internalGrpcPort": 50001
logAsJson将此参数设置为 true 会以 JSON 格式输出日志。默认是 false"logAsJson": false
logLevel设置 Dapr sidecar 的日志级别。允许的值是 debug、info、warn、error。默认是 info"logLevel": "debug"
metricsPort设置 sidecar 指标服务器的端口。默认是 9090是,如果有多个应用"metricsPort": 9093
modeDapr 的运行模式(默认 “standalone”)"mode": "standalone"
placementHostAddressDapr actor 放置服务器的地址"placementHostAddress": "http://localhost:1313/"
profilePort配置文件服务器的端口(默认 “7777”)"profilePort": 7777
sentryAddressSentry CA 服务的地址"sentryAddress": "http://localhost:1345/"
type告诉 VS Code 它将是一个 daprd 任务类型"type": "daprd"

相关链接

5.1.1.3 - 使用开发容器开发Dapr应用

如何使用Dapr设置容器化的开发环境

Visual Studio Code 的 开发容器扩展允许您使用一个自包含的 Docker 容器作为完整的开发环境,而无需在本地文件系统中安装任何额外的软件包、库或工具。

Dapr 提供了预构建的 C# 和 JavaScript/TypeScript 开发容器,您可以选择其中一个来快速搭建开发环境。请注意,这些预构建的容器会自动更新到 Dapr 的最新版本。

我们还发布了一个开发容器功能,可以在任何开发容器中安装 Dapr CLI。

设置开发环境

先决条件

使用开发容器功能添加 Dapr CLI

您可以使用 开发容器功能 在任何开发容器中安装 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 开发容器

以下是一个用于开发 Dapr Java 应用的开发容器示例,基于 官方 Java 17 开发容器镜像

将其放置在项目中的 .devcontainer/devcontainer.json 文件中:

// 有关格式详细信息,请参阅 https://aka.ms/devcontainer.json。有关配置选项,请参阅
// README: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 命令面板(在 Windows 上为 CTRL + SHIFT + P 或在 Mac 上为 CMD + SHIFT + P),选择 Dev Containers: Rebuild and Reopen in Container

使用预构建的开发容器(C# 和 JavaScript/TypeScript)

  1. 在 VS Code 中打开您的应用工作区
  2. 在命令面板中(在 Windows 上为 CTRL + SHIFT + P 或在 Mac 上为 CMD + SHIFT + P)输入并选择 Dev Containers: Add Development Container Configuration Files...
    添加远程容器的截图
  3. 输入 dapr 以过滤可用的 Dapr 远程容器列表,并选择与您的应用匹配的语言容器。请注意,您可能需要选择 Show All Definitions...
    添加 Dapr 容器的截图
  4. 按照提示在容器中重新打开您的工作区。
    在开发容器中重新打开应用的截图

示例

观看此 视频 了解如何在您的应用中使用 Dapr 开发容器。

5.1.2 - IntelliJ

在IntelliJ社区版中配置Dapr调试环境

在开发Dapr应用程序时,通常会使用Dapr CLI来启动您的服务,例如:

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>

可选地,您还可以为可以在多个项目中重用的sidecar工具创建一个新条目:

<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次(按顺序):应用程序id、应用程序端口、Dapr的http端口、Dapr的grpc端口。 -->
      <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. 无应用程序端口的可重用条目。 -->
  <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次(按顺序):应用程序id、Dapr的http端口、Dapr的grpc端口。 -->
      <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中的Java应用程序与Dapr需要两个步骤:

  1. 通过IntelliJ中的工具 -> 外部工具启动dapr

将dapr作为“外部工具”运行

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

以调试模式启动应用程序

总结

调试后,请确保在IntelliJ中停止dapr和您的应用程序。

注意:由于您使用dapr run CLI命令启动了服务,dapr list命令将在当前运行的Dapr应用程序列表中显示来自IntelliJ的运行。

祝调试愉快!

相关链接

  • 更改 IntelliJ配置目录位置

5.2 - 多应用同时运行

支持使用单个命令同时运行多个 Dapr 应用

5.2.1 - 多应用运行概述

使用一个CLI命令运行多个应用程序

如果您想在本地运行多个应用程序进行联合测试,类似于生产环境,多应用运行功能可以帮助您同时启动和停止一组应用程序。这些应用程序可以是:

  • 本地/自托管的进程,或
  • 通过构建容器镜像并部署到Kubernetes集群
    • 您可以使用本地Kubernetes集群(如KiND)或将其部署到云(如AKS、EKS和GKE)。

多应用运行模板文件描述了如何启动多个应用程序,类似于您运行多个单独的CLI run命令。默认情况下,此模板文件名为dapr.yaml

多应用运行模板文件

执行dapr run -f .时,它会启动当前目录中的多应用模板文件(名为dapr.yaml)以运行所有应用程序。

您可以使用自己喜欢的名称命名模板文件,而不是默认名称。例如dapr run -f ./<your-preferred-file-name>.yaml

以下示例展示了一些您可以为应用程序自定义的模板属性。在示例中,您可以同时启动2个应用程序,应用程序ID分别为processoremit-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"]

有关模板属性的更深入示例和解释,请参见多应用模板

资源和配置文件的位置

使用多应用运行时,您可以选择将应用程序的资源和配置文件放置在哪里。

单一文件位置(遵循约定)

您可以将所有应用程序的资源和配置放在~/.dapr根目录下。当所有应用程序共享相同的资源路径时,这种方式很有帮助,比如在本地机器上测试时。

独立文件位置(遵循约定)

使用多应用运行时,每个应用程序目录可以有一个.dapr文件夹,其中包含一个config.yaml文件和一个resources目录。如果应用程序目录中不存在.dapr目录,则使用默认的~/.dapr/resources/~/.dapr/config.yaml位置。

如果您决定在每个应用程序目录中添加一个.dapr目录,其中包含一个/resources目录和config.yaml文件,您可以为每个应用程序指定不同的资源路径。这种方法仍然遵循默认的~/.dapr约定。

自定义位置

您还可以将每个应用程序目录的.dapr目录命名为其他名称,例如webappbackend。如果您希望明确资源或应用程序目录路径,这将有所帮助。

日志

运行模板为每个应用程序及其关联的daprd进程提供了两个日志目标字段:

  1. appLogDestination:此字段配置应用程序的日志目标。可能的值是consolefilefileAndConsole。默认值是fileAndConsole,应用程序日志默认写入控制台和文件。

  2. daprdLogDestination:此字段配置daprd进程的日志目标。可能的值是consolefilefileAndConsole。默认值是filedaprd日志默认写入文件。

日志文件格式

应用程序和daprd的日志分别捕获在不同的文件中。这些日志文件会自动创建在每个应用程序目录(模板中的appDirPath)下的.dapr/logs目录中。这些日志文件名遵循以下模式:

  • <appID>_app_<timestamp>.logapp日志的文件名格式)
  • <appID>_daprd_<timestamp>.logdaprd日志的文件名格式)

即使您决定将资源文件夹重命名为其他名称,日志文件也只会写入应用程序目录中创建的.dapr/logs文件夹。

观看演示

观看此视频以了解多应用运行的概述

多应用运行模板文件

执行dapr run -k -f .dapr run -k -f dapr.yaml时,dapr.yaml多应用运行模板文件中定义的应用程序将在Kubernetes默认命名空间中启动。

注意: 目前,多应用运行模板只能在默认的Kubernetes命名空间中启动应用程序。

Kubernetes所需的默认服务和部署定义会在dapr.yaml模板中为每个应用程序生成在.dapr/deploy文件夹中。

如果dapr.yaml模板中应用程序的createService字段设置为true,则会在应用程序的.dapr/deploy文件夹中生成service.yaml文件。

否则,只会为每个设置了containerImage字段的应用程序生成deployment.yaml文件。

文件service.yamldeployment.yaml用于在Kubernetes的default命名空间中部署应用程序。此功能专门针对在Kubernetes中运行多个应用程序的开发/测试环境。

您可以使用任何首选名称命名模板文件,而不是默认名称。例如:

dapr run -k -f ./<your-preferred-file-name>.yaml

以下示例展示了一些您可以为应用程序自定义的模板属性。在示例中,您可以同时启动2个应用程序,应用程序ID分别为nodeapppythonapp

version: 1
common:
apps:
  - appID: nodeapp
    appDirPath: ./nodeapp/
    appPort: 3000
    containerImage: ghcr.io/dapr/samples/hello-k8s-node:latest
    createService: true
    env:
      APP_PORT: 3000
  - appID: pythonapp
    appDirPath: ./pythonapp/
    containerImage: ghcr.io/dapr/samples/hello-k8s-python:latest

注意:

  • 如果未指定containerImage字段,dapr run -k -f会产生错误。
  • createService字段定义了一个基本的Kubernetes服务(ClusterIP或LoadBalancer),目标是模板中指定的--app-port。如果未指定createService,则应用程序无法从集群外部访问。

有关模板属性的更深入示例和解释,请参见多应用模板

日志

运行模板为每个应用程序及其关联的daprd进程提供了两个日志目标字段:

  1. appLogDestination:此字段配置应用程序的日志目标。可能的值是consolefilefileAndConsole。默认值是fileAndConsole,应用程序日志默认写入控制台和文件。

  2. daprdLogDestination:此字段配置daprd进程的日志目标。可能的值是consolefilefileAndConsole。默认值是filedaprd日志默认写入文件。

日志文件格式

应用程序和daprd的日志分别捕获在不同的文件中。这些日志文件会自动创建在每个应用程序目录(模板中的appDirPath)下的.dapr/logs目录中。这些日志文件名遵循以下模式:

  • <appID>_app_<timestamp>.logapp日志的文件名格式)
  • <appID>_daprd_<timestamp>.logdaprd日志的文件名格式)

即使您决定将资源文件夹重命名为其他名称,日志文件也只会写入应用程序目录中创建的.dapr/logs文件夹。

观看演示

观看此视频以了解Kubernetes中的多应用运行概述

下一步

5.2.2 - 如何使用多应用运行模板文件

解压多应用运行模板文件及其属性

多应用运行模板文件是一个 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。
    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 参考文档中列出

属性必需详情示例
appDirPathY应用代码的路径./webapp/, ./backend/
appIDN应用的 app ID。如果未提供,将从 appDirPath 派生webapp, backend
resourcesPathN已弃用。Dapr 资源的路径。可以按约定为默认值./app/components, ./webapp/components
resourcesPathsN逗号分隔的 Dapr 资源路径。可以按约定为默认值./app/components, ./webapp/components
appChannelAddressN应用监听的网络地址。可以按约定保留为默认值。127.0.0.1
configFilePathN应用配置文件的路径./webapp/config.yaml
appProtocolNDapr 用于与应用通信的协议。http, grpc
appPortN应用监听的端口8080, 3000
daprHTTPPortNDapr HTTP 端口
daprGRPCPortNDapr GRPC 端口
daprInternalGRPCPortNDapr 内部 API 监听的 gRPC 端口;用于从本地 DNS 组件解析值时
metricsPortNDapr 发送其指标信息的端口
unixDomainSocketNUnix 域套接字目录挂载的路径。如果指定,与 Dapr 边车的通信使用 Unix 域套接字,与使用 TCP 端口相比,具有更低的延迟和更高的吞吐量。在 Windows 上不可用。/tmp/test-socket
profilePortN配置文件服务器监听的端口
enableProfilingN通过 HTTP 端点启用分析
apiListenAddressesNDapr API 监听地址
logLevelN日志详细程度。
appMaxConcurrencyN应用的并发级别;默认是无限制
placementHostAddressN
appSSLN启用 https,当 Dapr 调用应用时
daprHTTPMaxRequestSizeN请求体的最大大小(MB)。
daprHTTPReadBufferSizeNHTTP 读取缓冲区的最大大小(KB)。这也限制了 HTTP 头的最大大小。默认是 4 KB
enableAppHealthCheckN启用应用的健康检查true, false
appHealthCheckPathN健康检查文件的路径/healthz
appHealthProbeIntervalN应用健康探测的间隔(秒)
appHealthProbeTimeoutN应用健康探测的超时时间(毫秒)
appHealthThresholdN应用被认为不健康的连续失败次数
enableApiLoggingN启用从应用到 Dapr 的所有 API 调用的日志记录
runtimePathNDapr 运行时安装路径
envN环境变量的映射;每个应用应用的环境变量将覆盖跨应用共享的环境变量DEBUG, DAPR_HOST_ADD
appLogDestinationN输出应用日志的日志目标;其值可以是 file, console 或 fileAndConsole。默认是 fileAndConsolefile, console, fileAndConsole
daprdLogDestinationN输出 daprd 日志的日志目标;其值可以是 file, console 或 fileAndConsole。默认是 filefile, console, fileAndConsole

下一步

观看此视频以了解多应用运行的概述

多应用运行模板的属性与 dapr run -k CLI 标志对齐,在 CLI 参考文档中列出

属性必需详情示例
appDirPathY应用代码的路径./webapp/, ./backend/
appIDN应用的 app ID。如果未提供,将从 appDirPath 派生webapp, backend
appChannelAddressN应用监听的网络地址。可以按约定保留为默认值。127.0.0.1
appProtocolNDapr 用于与应用通信的协议。http, grpc
appPortN应用监听的端口8080, 3000
daprHTTPPortNDapr HTTP 端口
daprGRPCPortNDapr GRPC 端口
daprInternalGRPCPortNDapr 内部 API 监听的 gRPC 端口;用于从本地 DNS 组件解析值时
metricsPortNDapr 发送其指标信息的端口
unixDomainSocketNUnix 域套接字目录挂载的路径。如果指定,与 Dapr 边车的通信使用 Unix 域套接字,与使用 TCP 端口相比,具有更低的延迟和更高的吞吐量。在 Windows 上不可用。/tmp/test-socket
profilePortN配置文件服务器监听的端口
enableProfilingN通过 HTTP 端点启用分析
apiListenAddressesNDapr API 监听地址
logLevelN日志详细程度。
appMaxConcurrencyN应用的并发级别;默认是无限制
placementHostAddressN
appSSLN启用 https,当 Dapr 调用应用时
daprHTTPMaxRequestSizeN请求体的最大大小(MB)。
daprHTTPReadBufferSizeNHTTP 读取缓冲区的最大大小(KB)。这也限制了 HTTP 头的最大大小。默认是 4 KB
enableAppHealthCheckN启用应用的健康检查true, false
appHealthCheckPathN健康检查文件的路径/healthz
appHealthProbeIntervalN应用健康探测的间隔(秒)
appHealthProbeTimeoutN应用健康探测的超时时间(毫秒)
appHealthThresholdN应用被认为不健康的连续失败次数
enableApiLoggingN启用从应用到 Dapr 的所有 API 调用的日志记录
envN环境变量的映射;每个应用应用的环境变量将覆盖跨应用共享的环境变量DEBUG, DAPR_HOST_ADD
appLogDestinationN输出应用日志的日志目标;其值可以是 file, console 或 fileAndConsole。默认是 fileAndConsolefile, console, fileAndConsole
daprdLogDestinationN输出 daprd 日志的日志目标;其值可以是 file, console 或 fileAndConsole。默认是 filefile, console, fileAndConsole
containerImageN在 Kubernetes 开发/测试环境中部署时使用的容器镜像 URI。ghcr.io/dapr/samples/hello-k8s-python:latest
createServiceN在开发/测试环境中部署应用时创建 Kubernetes 服务。true, false

下一步

观看此视频以了解 Kubernetes 中多应用运行的概述

6 - 调试Dapr应用和控制面板

指导如何调试Dapr应用和控制面板的指南

6.1 - 在 Kubernetes 环境中调试 Dapr

如何在 Kubernetes 集群中调试 Dapr

在 Kubernetes 集群中调试 Dapr 是确保应用程序正常运行的关键。通过调试,开发者可以识别并解决 Dapr 组件之间的通信问题、actor 的状态管理问题,以及其他与 Dapr 集成相关的挑战。

在开始调试之前,请确保您的 Kubernetes 集群已正确配置,并且 Dapr 已成功部署。您可以使用以下命令检查 Dapr 的状态:

```bash
kubectl get pods -n dapr-system

这将列出所有正在运行的 Dapr 组件的 pod。确保所有 pod 都处于 Running 状态。

常见调试步骤

  1. 检查 Dapr sidecar 日志:Dapr sidecar 是每个应用程序 pod 中的重要组件。通过查看 sidecar 的日志,您可以获取有关服务调用、发布订阅、绑定等的详细信息。

    kubectl logs <pod-name> daprd
    
  2. 验证配置和密钥:确保您的 Dapr 配置和 Kubernetes 密钥正确无误。错误的配置可能导致服务无法正常工作。

  3. 测试服务调用:使用 Dapr CLI 工具测试服务调用,以确保服务之间的通信正常。

    dapr invoke --app-id <app-id> --method <method-name>
    
  4. 监控状态存储:检查 actor 的状态存储,确保数据持久化和检索正常。

通过这些步骤,您可以有效地调试和优化 Dapr 在 Kubernetes 集群中的运行

6.1.1 - 在 Kubernetes 上调试 Dapr 控制平面

如何在 Kubernetes 集群上调试 Dapr 控制平面

概述

有时我们需要了解 Dapr 控制平面(即 Kubernetes 服务)的运行情况,包括 dapr-sidecar-injectordapr-operatordapr-placementdapr-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 调试二进制文件

如果 Dapr 已经安装在您的 Kubernetes 集群中,首先卸载它:

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

然后进入本指南开头从 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 开始远程调试会话。

相关链接

6.1.2 - 在 Kubernetes 上调试 daprd

如何在 Kubernetes 集群上调试 Dapr sidecar (daprd)

概述

有时我们需要了解 Dapr sidecar (daprd) 的运行情况,特别是在诊断 Dapr 应用程序时,怀疑 Dapr 本身是否存在问题。此外,您可能正在为 Kubernetes 上的 Dapr 开发新功能,并需要调试您的代码。

本指南介绍如何使用 Dapr 的内置调试功能来调试 Kubernetes pod 中的 Dapr sidecar。要了解如何查看日志和排查 Kubernetes 中的 Dapr 问题,请参阅配置和查看 Dapr 日志指南

前提条件

  • 请参阅本指南了解如何将 Dapr 部署到您的 Kubernetes 集群。
  • 按照本指南构建您将在下一步中部署的 Dapr 调试二进制文件。

初始化 Dapr 调试模式

如果 Dapr 已经安装在您的 Kubernetes 集群中,请先卸载它:

dapr uninstall -k

我们将使用 ‘helm’ 来安装 Dapr 调试二进制文件。有关更多信息,请参阅使用 Helm 安装

首先配置一个名为 values.yml 的值文件,选项如下:

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 sidecar 置于调试模式。您还可以通过注解 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 命令

在调试 daprd 和在 Kubernetes 上运行的应用程序时,使用以下常用的 kubectl 命令。

获取所有 pod、事件和服务:

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 社区电话 #36 中观看关于在 Kubernetes 上排查 Dapr 问题的演示。

相关链接

6.2 - Bridge to Kubernetes 对 Dapr 服务的支持

在本地调试 Dapr 应用程序,同时保持与 Kubernetes 集群的连接

Bridge to Kubernetes 允许您在开发计算机上运行和调试代码,同时保持与 Kubernetes 集群中其他应用程序或服务的连接。这种调试方式通常被称为本地隧道调试

了解更多 Bridge to Kubernetes 信息

调试 Dapr 应用程序

Bridge to Kubernetes 支持在您的计算机上调试 Dapr 应用程序,同时与 Kubernetes 集群中的服务和应用程序进行交互。以下示例展示了 Bridge to Kubernetes 如何帮助开发人员调试分布式计算器快速入门

进一步阅读

6.3 - 在 Docker Compose 中调试 Dapr 应用

本地调试作为 Docker Compose 部署一部分的 Dapr 应用

本文旨在介绍一种方法,如何通过你的 IDE 在本地调试一个或多个使用 Dapr 的应用,同时保持与其他通过 Docker Compose 部署的应用的集成。

我们以一个包含两个服务的 Docker Compose 文件的简单示例为例:

  • nodeapp - 你的应用
  • nodeapp-dapr - 你的 nodeapp 服务的 Dapr sidecar 进程

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 sidecar 进程以及其他通过 Docker Compose 文件部署的服务集成的情况下调试 nodeapp 呢?

我们可以通过引入一个名为 compose.debug.yml第二个 Docker Compose 文件来实现。当运行 up 命令时,这个第二个 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"] # 让 sidecar 在主机上查找应用通道
    network_mode: !reset "" # 重置 network_mode...
    networks: # ... 以便 sidecar 可以进入正常网络
      - hello-dapr
    ports:
      - "3500:3500" # 将 HTTP 端口暴露给主机
      - "50001:50001" # 将 GRPC 端口暴露给主机(Dapr 工作流依赖于 GRPC 通道)

接下来,确保你的 nodeapp 在你选择的 IDE 中运行/调试,并在你在 compose.debug.yml 中上面指定的相同端口上暴露 - 在上面的示例中,这设置为端口 8080

接下来,停止你可能已启动的任何现有 Compose 会话,并运行以下命令以组合运行两个 Docker Compose 文件:

docker compose -f compose.yml -f compose.debug.yml up

现在,你应该会发现 Dapr sidecar 和你的调试应用可以相互通信,就像它们在 Docker Compose 环境中正常一起运行一样。

注意:需要强调的是,Docker Compose 环境中的 nodeapp 服务实际上仍在运行,但它已从 Docker 网络中移除,因此实际上被孤立,因为没有任何东西可以与之通信。

演示:观看此视频,了解如何使用 Docker Compose 调试本地 Dapr 应用

7 - 技术集成

Dapr与其他技术的无缝集成

7.1 - AWS 集成

Dapr 集成 AWS 服务

7.1.1 - AWS 认证

关于 AWS 的认证和配置选项的信息

Dapr 组件通过 AWS SDK 使用 AWS 服务(例如,DynamoDB、SQS、S3),并支持标准化的配置属性。了解更多关于 AWS SDK 如何处理凭证的信息

您可以使用 AWS SDK 的默认提供者链,或者选择以下列出的预定义 AWS 认证配置文件之一来进行认证配置。通过测试和检查 Dapr 运行时日志来验证组件配置,确保正确初始化。

术语

  • ARN (Amazon Resource Name): 用于唯一标识 AWS 资源的标识符。格式为:arn:partition:service:region:account-id:resource。示例:arn:aws:iam::123456789012:role/example-role
  • IAM (Identity and Access Management): AWS 提供的用于安全管理对 AWS 资源访问的服务。

认证配置文件

访问密钥 ID 和秘密访问密钥

使用静态访问密钥和秘密密钥凭证,可以通过组件元数据字段或通过默认 AWS 配置进行配置。

属性必需描述示例
regionY要连接的 AWS 区域。“us-east-1”
accessKeyNAWS 访问密钥 ID。在 Dapr v1.17 中将是必需的。“AKIAIOSFODNN7EXAMPLE”
secretKeyNAWS 秘密访问密钥,与 accessKey 一起使用。在 Dapr v1.17 中将是必需的。“wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY”
sessionTokenNAWS 会话令牌,与 accessKeysecretKey 一起使用。对于 IAM 用户密钥通常不需要。

假设 IAM 角色

此配置文件允许 Dapr 假设特定的 IAM 角色。通常在 Dapr sidecar 在 EKS 或链接到 IAM 策略的节点/pod 上运行时使用。目前由 Kafka 和 PostgreSQL 组件支持。

属性必需描述示例
regionY要连接的 AWS 区域。“us-east-1”
assumeRoleArnN具有 AWS 资源访问权限的 IAM 角色的 ARN。在 Dapr v1.17 中将是必需的。“arn:aws:iam::123456789:role/mskRole”
sessionNameN角色假设的会话名称。默认是 "DaprDefaultSession"“MyAppSession”

从环境变量获取凭证

使用环境变量进行认证。这对于在自托管模式下运行的 Dapr 特别有用,因为 sidecar 注入器不会配置环境变量。

此认证配置文件不需要任何元数据字段。

IAM Roles Anywhere

IAM Roles Anywhere 将基于 IAM 角色的认证扩展到外部工作负载。通过使用加密签名的证书,消除了长期凭证的需求,这些证书基于 Dapr PKI 的信任关系。Dapr SPIFFE 身份 X.509 证书用于认证到 AWS 服务,Dapr 在会话生命周期的一半时处理凭证轮换。

要配置此认证配置文件:

  1. 使用 Dapr 证书包作为 外部证书包 在信任的 AWS 账户中创建一个信任锚。
  2. 创建一个具有必要资源权限策略的 IAM 角色,以及一个为 Roles Anywhere AWS 服务指定的信任实体。在此处,您指定允许的 SPIFFE 身份。
  3. 在 Roles Anywhere 服务下创建一个 IAM 配置文件,链接 IAM 角色。
属性必需描述示例
trustAnchorArnY在 AWS 账户中授予 Dapr 证书颁发机构信任的信任锚的 ARN。arn:aws:rolesanywhere:us-west-1:012345678910:trust-anchor/01234568-0123-0123-0123-012345678901
trustProfileArnY在信任的 AWS 账户中的 AWS IAM 配置文件的 ARN。arn:aws:rolesanywhere:us-west-1:012345678910:profile/01234568-0123-0123-0123-012345678901
assumeRoleArnY在信任的 AWS 账户中要假设的 AWS IAM 角色的 ARN。arn:aws:iam:012345678910:role/exampleIAMRoleName

其他字段

一些 AWS 组件包括额外的可选字段:

属性必需描述示例
endpointN端点通常由 AWS SDK 内部处理。然而,在某些情况下,可能需要在本地设置它 - 例如,如果针对 DynamoDB Local 进行开发。

此外,支持 AWS 认证配置文件的非原生 AWS 组件(如 Kafka 和 PostgreSQL)具有触发 AWS 认证逻辑的元数据字段。请务必查看特定组件文档。

在组件清单文件中显式指定凭证的替代方案

在生产场景中,建议使用以下解决方案:

如果在 AWS EKS 上运行,您可以将 IAM 角色链接到 Kubernetes 服务账户,您的 pod 可以使用该账户。

所有这些解决方案都解决了同一个问题:它们允许 Dapr 运行时进程(或 sidecar)动态检索凭证,因此不需要显式凭证。这提供了几个好处,例如自动密钥轮换,以及避免管理 secret。

Kiam 和 Kube2IAM 都通过拦截对实例元数据服务的调用来工作。

在 AWS EC2 上以独立模式运行时使用实例配置文件

如果直接在 AWS EC2 实例上以独立模式运行 Dapr,您可以使用实例配置文件。

  1. 配置一个 IAM 角色。
  2. 将其附加到实例配置文件以用于 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 上,需要在启动 daprdaprd 命令之前设置环境变量,像在 Linux/MacOS 中那样内联设置是不支持的。

如果使用基于 AWS SSO 的配置文件认证到 AWS

如果您使用 AWS SSO 认证到 AWS,某些 AWS SDK(包括 Go SDK)尚不支持此功能。您可以使用几个实用程序来“弥合” AWS SSO 凭证和“传统”凭证之间的差距,例如:

如果使用 AwsHelper,像这样启动 Dapr:

AWS_PROFILE=myprofile awshelper dapr run...

AWS_PROFILE=myprofile awshelper daprd...

在 Windows 上,需要在启动 awshelper 命令之前设置环境变量,像在 Linux/MacOS 中那样内联设置是不支持的。

下一步

参考 AWS 组件规范 >>

相关链接

有关更多信息,请参阅如何 AWS SDK(Dapr 使用的)处理凭证

7.2 - Azure 集成

Dapr 集成 Azure 服务

7.2.1 - Azure 身份验证

了解如何使用 Microsoft Entra ID 或托管身份来进行 Azure 组件的身份验证

7.2.1.1 - Azure 身份验证

如何使用 Microsoft Entra ID 和/或托管身份验证 Azure 组件

大多数 Dapr 的 Azure 组件支持使用 Microsoft Entra ID 进行身份验证。通过这种方式:

  • 管理员可以充分利用 Azure 基于角色的访问控制 (RBAC) 的精细权限。
  • 在 Azure 服务(如 Azure 容器应用、Azure Kubernetes 服务、Azure 虚拟机或其他 Azure 平台服务)上运行的应用程序可以使用 托管身份 (MI)工作负载身份。这些功能使您的应用程序能够在不需要管理敏感凭据的情况下进行身份验证。

关于 Microsoft Entra ID 的身份验证

Microsoft Entra ID 是 Azure 的身份和访问管理 (IAM) 解决方案,用于对用户和服务进行身份验证和授权。

Microsoft Entra ID 基于 OAuth 2.0 等开放标准,允许服务(应用程序)获取访问令牌以请求 Azure 服务,包括 Azure 存储、Azure 服务总线、Azure 密钥保管库、Azure Cosmos DB、Azure PostgreSQL 数据库、Azure SQL 等。

在 Azure 术语中,应用程序也被称为“服务主体”。

一些 Azure 组件提供其他身份验证方法,例如基于“共享密钥”或“访问令牌”的系统。尽管这些方法在 Dapr 中是有效且受支持的,但建议尽可能使用 Microsoft Entra ID 对 Dapr 组件进行身份验证,以利用其众多优势,包括:

托管身份和工作负载身份

使用托管身份 (MI),您的应用程序可以通过 Microsoft Entra ID 进行身份验证并获取访问令牌以请求 Azure 服务。当您的应用程序在支持的 Azure 服务(如 Azure 虚拟机、Azure 容器应用、Azure Web 应用等)上运行时,可以在基础设施级别为您的应用程序分配一个身份。

使用 MI 后,您的代码无需处理凭据,这样可以:

  • 消除安全管理凭据的挑战
  • 允许开发和运营团队之间更好的职责分离
  • 减少有权访问凭据的人员数量
  • 简化操作,尤其是在使用多个环境时

在 Azure Kubernetes 服务上运行的应用程序可以类似地利用 工作负载身份 自动为单个 pod 提供身份。

基于角色的访问控制

使用支持服务的 Azure 基于角色的访问控制 (RBAC) 时,可以对应用程序授予的权限进行精细调整。例如,您可以限制对数据子集的访问或将访问权限设为只读。

审计

使用 Microsoft Entra ID 提供了改进的访问审计体验。租户的管理员可以查阅审计日志以跟踪身份验证请求。

(可选)使用证书进行身份验证

虽然 Microsoft Entra ID 允许您使用 MI,但您仍然可以选择使用证书进行身份验证。

对其他 Azure 环境的支持

默认情况下,Dapr 组件配置为与“公共云”中的 Azure 资源交互。如果您的应用程序部署到其他云(如 Azure 中国或 Azure 政府“主权云”),您可以通过将 azureEnvironment 元数据属性设置为以下支持的值之一来启用该功能:

  • Azure 公共云(默认):"AzurePublicCloud"
  • Azure 中国:"AzureChinaCloud"
  • Azure 政府:"AzureUSGovernmentCloud"

对主权云的支持是实验性的。

凭据元数据字段

要使用 Microsoft Entra ID 进行身份验证,您需要将以下凭据作为值添加到您的 Dapr 组件 的元数据中。

元数据选项

根据您向 Dapr 服务传递凭据的方式,您有多种元数据选项。

使用客户端凭据进行身份验证

字段必需详情示例
azureTenantIdYMicrosoft Entra ID 租户的 ID"cd4b2887-304c-47e1-b4d5-65447fdd542b"
azureClientIdY客户端 ID(应用程序 ID)"c7dd251f-811f-4ba2-a905-acd4d3f8f08b"
azureClientSecretY客户端密钥(应用程序密码)"Ecy3XG7zVZK3/vl/a2NSB+a1zXLa8RnMum/IgD0E"

在 Kubernetes 上运行时,您还可以使用对 Kubernetes secret 的引用来获取上述任何或所有值。

使用证书进行身份验证

字段必需详情示例
azureTenantIdYMicrosoft Entra ID 租户的 ID"cd4b2887-304c-47e1-b4d5-65447fdd542b"
azureClientIdY客户端 ID(应用程序 ID)"c7dd251f-811f-4ba2-a905-acd4d3f8f08b"
azureCertificateazureCertificateazureCertificateFile 之一证书和私钥(PFX/PKCS#12 格式)"-----BEGIN PRIVATE KEY-----\n MIIEvgI... \n -----END PRIVATE KEY----- \n -----BEGIN CERTIFICATE----- \n MIICoTC... \n -----END CERTIFICATE-----
azureCertificateFileazureCertificateazureCertificateFile 之一包含证书和私钥的 PFX/PKCS#12 文件的路径"/path/to/file.pem"
azureCertificatePasswordN如果加密,证书的密码"password"

在 Kubernetes 上运行时,您还可以使用对 Kubernetes secret 的引用来获取上述任何或所有值。

使用托管身份 (MI) 进行身份验证

字段必需详情示例
azureClientIdN客户端 ID(应用程序 ID)"c7dd251f-811f-4ba2-a905-acd4d3f8f08b"

使用托管身份,通常推荐使用 azureClientId 字段。使用系统分配的身份时该字段是可选的,但使用用户分配的身份时可能是必需的。

在 AKS 上使用工作负载身份进行身份验证

在 Azure Kubernetes 服务 (AKS) 上运行时,您可以使用工作负载身份对组件进行身份验证。请参阅 Azure AKS 文档以了解如何为您的 Kubernetes 资源 启用工作负载身份

使用 Azure CLI 凭据进行身份验证(仅限开发)

重要提示: 此身份验证方法仅推荐用于 开发

此身份验证方法在本地机器上开发时可能很有用。您将需要:

  • 安装 Azure CLI
  • 使用 az login 命令成功进行身份验证

当 Dapr 在主机上运行时,如果 Azure CLI 有可用的凭据,组件可以自动使用这些凭据进行身份验证,而无需配置其他身份验证方法。

使用此身份验证方法不需要设置任何元数据选项。

在 Dapr 组件中的示例用法

在此示例中,您将设置一个使用 Microsoft Entra ID 进行身份验证的 Azure 密钥保管库 secret 存储组件。

要使用 客户端密钥,请在组件目录中创建一个名为 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 Secret Store 中,然后在 YAML 文件中引用它们。

要使用 客户端密钥

  1. 使用以下命令创建一个 Kubernetes secret:

    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 secret store 中的 secret 名称
    • [your_k8s_secret_key] 是 Kubernetes secret store 中的 secret 键
  2. 创建一个 azurekeyvault.yaml 组件文件。

    组件 yaml 使用 auth 属性引用 Kubernetes secretstore,并且 secretKeyRef 引用存储在 Kubernetes secret store 中的客户端密钥。

    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
    
  3. 应用 azurekeyvault.yaml 组件:

    kubectl apply -f azurekeyvault.yaml
    

要使用 证书

  1. 使用以下命令创建一个 Kubernetes secret:

    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 secret store 中的 secret 名称
    • [your_k8s_secret_key] 是 Kubernetes secret store 中的 secret 键
  2. 创建一个 azurekeyvault.yaml 组件文件。

    组件 yaml 使用 auth 属性引用 Kubernetes secretstore,并且 secretKeyRef 引用存储在 Kubernetes secret store 中的证书。

    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
    
  3. 应用 azurekeyvault.yaml 组件:

    kubectl apply -f azurekeyvault.yaml
    

下一步

生成新的 Microsoft Entra ID 应用程序和服务主体 >>

参考资料

7.2.1.2 - 如何创建新的 Microsoft Entra ID 应用程序和服务主体

了解如何创建 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)

选择传递凭据的方式。

要创建一个客户端密钥,运行以下命令。

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 组件的元数据时:

  • appIdazureClientId 的值
  • passwordazureClientSecret 的值(这是随机生成的)
  • tenantazureTenantId 的值

对于 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 组件的元数据时:

  • appIdazureClientId 的值
  • tenantazureTenantId 的值
  • 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 "服务主体 ID: ${SERVICE_PRINCIPAL_ID}"

预期输出:

服务主体 ID: 1d0ccf05-5427-4b5e-8eb4-005ac5f9f163

上面返回的值是服务主体 ID,它与 Microsoft Entra ID 应用程序 ID(客户端 ID)不同。服务主体 ID 在 Azure 租户内定义,用于授予应用程序访问 Azure 资源的权限。
您将使用服务主体 ID 授予应用程序访问 Azure 资源的权限。

同时,客户端 ID 由您的应用程序用于身份验证。您将在 Dapr 清单中使用客户端 ID 来配置与 Azure 服务的身份验证。

请记住,刚刚创建的服务主体默认没有访问任何 Azure 资源的权限。需要根据需要为每个资源授予访问权限,如组件文档中所述。

下一步

使用托管身份

7.2.1.3 - 如何使用托管身份

学习如何使用托管身份

托管身份可以自动进行身份验证,因为您的应用程序运行在具有系统分配或用户分配身份的 Azure 服务上。

要开始使用,您需要在各种 Azure 服务中启用托管身份作为服务选项/功能,这与 Dapr 无关。启用后,会在后台为 Microsoft Entra ID(以前称为 Azure Active Directory ID)创建一个身份(或应用程序)。

然后,您的 Dapr 服务可以利用该身份与 Microsoft Entra ID 进行认证,过程是透明的,您无需指定任何凭据。

在本指南中,您将学习如何:

  • 通过官方 Azure 文档将您的身份授予您正在使用的 Azure 服务
  • 在您的组件中设置系统管理或用户分配身份

以上是全部内容。

授予服务访问权限

为特定 Azure 资源(由资源范围标识)设置必要的 Microsoft Entra ID 角色分配或自定义权限给您的系统管理或用户分配身份。

您可以为新的或现有的 Azure 资源设置托管身份。说明取决于所使用的服务。请查看以下官方文档以获取最合适的说明:

在为您的 Azure 资源分配系统管理身份后,您将获得如下信息:

{
    "principalId": "<object-id>",
    "tenantId": "<tenant-id>",
    "type": "SystemAssigned",
    "userAssignedIdentities": null
}

请注意 principalId 值,这是为您的身份创建的 服务主体 ID。使用它来授予您的 Azure 资源组件访问权限。

在您的组件中设置身份

默认情况下,Dapr Azure 组件会查找其运行环境的系统管理身份并以此进行认证。通常,对于给定组件,除了服务名称、存储帐户名称和 Azure 服务所需的任何其他属性(在文档中列出)外,没有使用系统管理身份的必需属性。

对于用户分配身份,除了您正在使用的服务所需的基本属性外,您还需要在组件中指定 azureClientId(用户分配身份 ID)。确保用户分配身份已附加到 Dapr 运行的 Azure 服务上,否则您将无法使用该身份。

以下示例演示了在 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

一旦您在组件 YAML 中设置了 azureClientId 属性,您就可以授予您的用户分配身份访问您的服务。

有关 Kubernetes 或 AKS 中的组件配置,请参阅 工作负载身份指南。

故障排除

如果您收到错误或托管身份未按预期工作,请检查以下项目是否为真:

  • 系统管理身份或用户分配身份没有目标资源的所需权限。

  • 用户分配身份未附加到您加载组件的 Azure 服务(容器应用或 pod)。这尤其可能发生在:

    • 您有一个未限定范围的组件(由环境中的所有容器应用或 AKS 集群中的所有部署加载的组件)。
    • 您仅将用户分配身份附加到 AKS 中的一个容器应用或一个部署(使用 Azure 工作负载身份)。

    在这种情况下,由于身份未附加到 AKS 中的每个其他容器应用或部署,引用用户分配身份的组件通过 azureClientId 失败。

最佳实践: 使用用户分配身份时,请确保将您的组件范围限定在特定应用上!

下一步

参考 Azure 组件规范 >>

7.2.2 - Azure API Management 与 Dapr 的集成策略

通过 Azure API Management 策略发布 Dapr 服务和组件的 API

Azure API Management 是一种用于为后端服务创建一致且现代的 API 网关的方法,其中也包括使用 Dapr 构建的服务。您可以在自托管的 API Management 网关中启用 Dapr 支持,从而实现以下功能:

  • 将请求转发至 Dapr 服务
  • 向 Dapr 发布/订阅主题发送消息
  • 激活 Dapr 输出绑定

试用 Dapr & Azure API Management 集成示例

了解更多关于 Dapr 集成策略的信息

7.2.3 - Azure Functions 运行时的 Dapr 扩展

在 Azure Functions 运行时应用程序中访问 Dapr 功能

Dapr 通过一个扩展与 Azure Functions 运行时 集成,使函数能够轻松地与 Dapr 交互。

  • Azure Functions 提供了一种事件驱动的编程模型。
  • Dapr 提供了云原生的构建模块。

该扩展结合了两者的优势,适用于无服务器和事件驱动的应用程序。

体验 Dapr 扩展 for Azure Functions

7.2.4 - 适用于 Azure Kubernetes Service (AKS) 的 Dapr 扩展

通过 Dapr 扩展在您的 Azure Kubernetes Service (AKS) 集群上部署 Dapr

在 AKS 上安装 Dapr 的推荐方法是使用 AKS Dapr 扩展。该扩展提供以下功能:

  • 通过 Azure CLI 命令行参数支持所有原生 Dapr 配置功能
  • 可选择自动升级 Dapr 运行时的小版本

使用 AKS 的 Dapr 扩展的先决条件:

了解有关 AKS 的 Dapr 扩展的更多信息

7.3 - Diagrid 集成

Dapr 和 Diagrid 的集成

7.3.1 - Conductor: 企业级 Dapr 的 Kubernetes 解决方案

自动化操作,执行安全最佳实践,提高系统稳定性,并增强 Dapr 集群的可视化能力


Diagrid Conductor 图示

Diagrid Conductor 能快速且安全地连接到所有运行 Dapr 和 Dapr 化应用程序的 Kubernetes 集群,提供卓越的操作管理、安全性、可靠性以及洞察力和协作能力。

Dapr 管理自动化

一键完成 Dapr 的安装、升级和修补,选择性应用更新并自动回滚,确保您始终使用最新版本。

智能顾问:发现并自动化最佳实践

提供信息并自动应用生产环境的最佳实践,持续监测以防止配置错误,提高安全性、可靠性和性能。

资源使用报告与优化

通过分析历史资源使用情况,推荐应用程序的资源优化方案,显著降低 CPU 和内存成本。

应用程序可视化工具

应用程序图表提供服务和基础设施组件的动态概览,促进开发与运维团队之间的协作。

了解更多关于 Diagrid Conductor

7.3.2 - 如何使用 Testcontainers Dapr 模块进行集成

在 Java 应用中集成 Dapr Testcontainer 模块

您可以通过 Diagrid 提供的 Testcontainers Dapr 模块,在本地为您的 Java 应用集成 Dapr。只需在您的 Maven 项目中添加以下依赖项:

<dependency>
    <groupId>io.diagrid.dapr</groupId>
    <artifactId>testcontainers-dapr</artifactId>
    <version>0.10.x</version>
</dependency>

如果您使用 Spring Boot,也可以使用 Spring Boot Starter。

了解更多关于 Testcontainers Dapr 模块

7.4 - 如何:使用 KEDA 自动扩展 Dapr 应用

如何配置您的 Dapr 应用程序以使用 KEDA 进行自动扩展

Dapr 通过其构建块 API 方法和众多 pubsub 组件,简化了消息处理应用程序的编写。由于 Dapr 可以在虚拟机、裸机、云或边缘 Kubernetes 等多种环境中运行,因此 Dapr 应用程序的自动扩展由其运行环境的管理层负责。

在 Kubernetes 环境中,Dapr 与 KEDA 集成,KEDA 是一个用于 Kubernetes 的事件驱动自动扩展器。Dapr 的许多 pubsub 组件与 KEDA 提供的扩展器功能相似,因此可以轻松配置您的 Dapr 部署在 Kubernetes 上使用 KEDA 根据负载进行自动扩展。

在本指南中,您将配置一个可扩展的 Dapr 应用程序,并在 Kafka 主题上进行负载管理。不过,您可以将此方法应用于 Dapr 提供的 任何 pubsub 组件

安装 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 主题

创建本示例中使用的主题(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

主题 partitions 的数量与 KEDA 为您的部署创建的最大副本数相关。

部署 Dapr pubsub 组件

为 Kubernetes 部署 Dapr Kafka pubsub 组件。将以下 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 定义了您的应用程序订阅的 pubsub 组件,以及 您之前创建的 (demo-topic)

如果您使用了 Kafka Helm 安装说明,可以保持 brokers 值不变。否则,请将此值更改为您的 Kafka brokers 的连接字符串。

注意为 consumerID 设置的 autoscaling-subscriber 值。此值用于确保 KEDA 和您的部署使用相同的 Kafka 分区偏移量,以便正确进行扩展。

现在,将组件部署到集群:

kubectl apply -f kafka-pubsub.yaml

为 Kafka 部署 KEDA 自动扩展器

部署 KEDA 扩展对象,该对象:

  • 监控指定 Kafka 主题上的滞后
  • 配置 Kubernetes 水平 Pod 自动扩展器 (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在部署中定义的应用程序的 Dapr ID(dapr.io/id 注释的值)。
pollingIntervalKEDA 检查 Kafka 当前主题分区偏移量的频率(以秒为单位)。
minReplicaCountKEDA 为您的部署创建的最小副本数。如果您的应用程序启动时间较长,最好将其设置为 1 以确保您的部署始终至少有一个副本在运行。否则,设置为 0,KEDA 会为您创建第一个副本。
maxReplicaCount您的部署的最大副本数。鉴于 Kafka 分区偏移量 的工作原理,您不应将该值设置得高于主题分区的总数。
triggers/metadata/topic应设置为您的 Dapr 部署订阅的相同主题(在本示例中为 demo-topic)。
triggers/metadata/bootstrapServers应设置为 kafka-pubsub.yaml 文件中使用的相同 broker 连接字符串。
triggers/metadata/consumerGroup应设置为 kafka-pubsub.yaml 文件中 consumerID 的相同值。

将 KEDA 扩展器部署到 Kubernetes:

kubectl apply -f kafka_scaler.yaml

全部完成!

查看 KEDA 扩展器工作

现在 ScaledObject KEDA 对象已配置,您的部署将根据 Kafka 主题的滞后进行扩展。了解有关为 Kafka 主题配置 KEDA 的更多信息

如 KEDA 扩展器清单中定义的,您现在可以开始向您的 Kafka 主题 demo-topic 发布消息,并在滞后阈值高于 5 个主题时观察 pod 自动扩展。使用 Dapr 发布 CLI 命令向 Kafka Dapr 组件发布消息。

下一步

了解有关在 Azure 容器应用中使用 KEDA 扩展 Dapr pubsub 或绑定应用程序的信息

7.5 - 如何:在 GitHub Actions 工作流中使用 Dapr CLI

将 Dapr CLI 集成到您的 GitHub Actions 中,以便在您的环境中部署和管理 Dapr。

Dapr 可以通过 GitHub Marketplace 上的 Dapr 工具安装器与 GitHub Actions 进行集成。这个安装器会将 Dapr CLI 添加到您的工作流中,使您能够在不同环境中部署、管理和升级 Dapr。

使用 Dapr 工具安装器安装 Dapr CLI

请将以下代码片段复制并粘贴到您的应用程序的 YAML 文件中:

- name: Dapr 工具安装器
  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: 安装 Dapr
  uses: dapr/setup-dapr@v1
  with:
    version: '1.15.5'

- name: 初始化 Dapr
  shell: bash
  run: |
    # 获取用于 dapr init 的 K8s 凭据
    az aks get-credentials --resource-group ${{ env.RG_NAME }} --name "${{ steps.azure-deployment.outputs.aksName }}"

    # 初始化 Dapr    
    # 将 Dapr init 日志分组,以便可以折叠这些行。
    echo "::group::初始化 Dapr"
    dapr init --kubernetes --wait --runtime-version ${{ env.DAPR_VERSION }}
    echo "::endgroup::"

    dapr status --kubernetes
  working-directory: ./demos/demo3

下一步

7.6 - 如何:在你的 Dapr 应用中使用 gRPC 接口

在你的应用中使用 Dapr gRPC API

Dapr 提供了用于本地调用的 HTTP 和 gRPC API。gRPC 适用于低延迟、高性能的场景,并通过 proto 客户端进行语言集成。

在 Dapr SDK 文档中查找自动生成的客户端列表

Dapr 运行时提供了一个 proto 服务,应用可以通过 gRPC 与其通信。

除了通过 gRPC 调用 Dapr,Dapr 还支持通过代理方式进行服务到服务的调用。在 gRPC 服务调用指南中了解更多

本指南演示了如何使用 Go SDK 配置和调用 Dapr 的 gRPC。

配置 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 操作。

  1. 导入包:

    package main
    
    import (
    	"context"
    	"log"
    	"os"
    
    	dapr "github.com/dapr/go-sdk/client"
    )
    
  2. 创建客户端:

    // 仅用于此演示
    ctx := context.Background()
    data := []byte("ping")
    
    // 创建客户端
    client, err := dapr.NewClient()
    if err != nil {
      log.Panic(err)
    }
    defer client.Close()
    
    1. 调用 SaveState 方法:
    // 使用键 key1 保存状态
    err = client.SaveState(ctx, "statestore", "key1", data)
    if err != nil {
      log.Panic(err)
    }
    log.Println("数据已保存")
    

现在你可以探索 Dapr 客户端上的所有不同方法。

使用 Dapr 创建 gRPC 应用

以下步骤将展示如何创建一个应用,该应用暴露一个服务器,Dapr 可以与之通信。

  1. 导入包:

    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"
    )
    
  2. 实现接口:

    // server 是我们的用户应用
    type server struct {
         pb.UnimplementedAppCallbackServer
    }
    
    // EchoMethod 是一个简单的演示方法
    func (s *server) EchoMethod() string {
    	return "pong"
    }
    
    // 当远程服务通过 Dapr 调用应用时,此方法被调用
    // 负载携带一个方法以识别方法、一组元数据属性和一个可选负载
    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("从绑定调用")
    	return &pb.BindingEventResponse{}, nil
    }
    
    // 每当消息发布到已订阅的主题时,此方法被触发。Dapr 在 CloudEvents 0.3 信封中发送已发布的消息。
    func (s *server) OnTopicEvent(ctx context.Context, in *pb.TopicEventRequest) (*pb.TopicEventResponse, error) {
    	fmt.Println("主题消息到达")
            return &pb.TopicEventResponse{}, nil
    }
    
  3. 创建服务器:

    func main() {
    	// 创建监听器
    	lis, err := net.Listen("tcp", ":50001")
    	if err != nil {
    		log.Fatalf("监听失败: %v", err)
    	}
    
    	// 创建 grpc 服务器
    	s := grpc.NewServer()
    	pb.RegisterAppCallbackServer(s, &server{})
    
    	fmt.Println("客户端启动中...")
    
    	// 并开始...
    	if err := s.Serve(lis); err != nil {
    		log.Fatalf("服务失败: %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 注解。

其他语言

你可以使用任何 Protobuf 支持的语言与 Dapr 一起使用,而不仅限于当前可用的生成 SDK。

使用 protoc 工具,你可以为其他语言(如 Ruby、C++、Rust 等)生成 Dapr 客户端。

相关主题

7.7 - 如何使用 Dapr Kubernetes Operator

通过 Dapr Kubernetes Operator 管理 Dapr 控制平面

您可以通过 Dapr Kubernetes Operator 来管理 Dapr 的控制平面。这个工具可以帮助您自动化管理在 Kubernetes 环境中 Dapr 控制平面的生命周期任务。

安装和使用 Dapr Kubernetes Operator

7.8 - 如何:与 Kratix 集成

使用 Dapr promise 与 Kratix 集成

Kratix 市场 中,Dapr 可以用于构建满足您需求的定制平台。

只需安装 Dapr Promise,即可在所有匹配的集群上安装 Dapr。

安装 Dapr Promise