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

This is the multi-page printable view of this section. Click here to print.
在概念部分中,您可以查看 Dapr 构建模块的高级概述。
通过服务调用,您的应用程序可以使用标准的gRPC或HTTP协议可靠且安全地与其他应用程序进行通信。
在许多基于微服务的应用程序中,多个服务需要能够相互通信。这种服务间通信要求应用程序开发人员处理以下问题:
Dapr通过提供一个类似反向代理的服务调用API来解决这些挑战,该API内置了服务发现,并利用了分布式追踪、指标、错误处理、加密等功能。
Dapr采用sidecar架构。要使用Dapr调用应用程序:
invoke
API。以下概述视频和演示展示了Dapr服务调用的工作原理。
下图概述了Dapr的服务调用在两个集成Dapr的应用程序之间的工作原理。
您还可以使用服务调用API调用非Dapr HTTP端点。例如,您可能只在整个应用程序的一部分中使用Dapr,可能无法访问代码以迁移现有应用程序以使用Dapr,或者只是需要调用外部HTTP服务。阅读“如何:使用HTTP调用非Dapr端点”以获取更多信息。
服务调用提供了多种功能,使您可以轻松地在应用程序之间调用方法或调用外部HTTP端点。
dapr-app-id
头即可开始。有关更多信息,请参阅使用HTTP调用服务。通过Dapr Sentry服务,所有Dapr应用程序之间的调用都可以通过托管平台上的相互(mTLS)认证来实现安全,包括自动证书轮换。
有关更多信息,请阅读服务到服务的安全性文章。
在调用失败和瞬态错误的情况下,服务调用提供了一种弹性功能,可以在回退时间段内自动重试。要了解更多信息,请参阅弹性文章。
默认情况下,所有应用程序之间的调用都会被追踪,并收集指标以提供应用程序的洞察和诊断。这在生产场景中特别重要,提供了服务之间调用的调用图和指标。有关更多信息,请阅读可观测性。
通过访问策略,应用程序可以控制:
例如,您可以限制包含人员信息的敏感应用程序不被未授权的应用程序访问。结合服务到服务的安全通信,您可以提供软多租户部署。
有关更多信息,请阅读服务调用的访问控制允许列表文章。
您可以将应用程序限定到命名空间以进行部署和安全,并在部署到不同命名空间的服务之间进行调用。有关更多信息,请阅读跨命名空间的服务调用文章。
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服务调用中将数据作为流处理。这可以在使用Dapr通过HTTP调用另一个服务时提供性能和内存利用率的改进,尤其是在请求或响应体较大的情况下。
下图演示了数据流的六个步骤。
按照上述调用顺序,假设您有如Hello World教程中描述的应用程序,其中一个Python应用程序调用一个Node.js应用程序。在这种情况下,Python应用程序将是"服务A",Node.js应用程序将是"服务B"。
下图再次显示了本地机器上的1-7序列,显示了API调用:
nodeapp
。Python应用程序通过POST http://localhost:3500/v1.0/invoke/nodeapp/method/neworder
调用Node.js应用程序的neworder
方法,该请求首先发送到Python应用程序的本地Dapr sidecar。Dapr文档包含多个利用服务调用构建模块的快速入门,适用于不同的示例架构。为了直观地理解服务调用API及其功能,我们建议从我们的快速入门开始:
快速入门/教程 | 描述 |
---|---|
服务调用快速入门 | 这个快速入门让您直接与服务调用构建模块进行交互。 |
Hello World教程 | 本教程展示了如何在本地机器上运行服务调用和状态管理构建模块。 |
Hello World Kubernetes教程 | 本教程演示了如何在Kubernetes中使用Dapr,并涵盖了服务调用和状态管理构建模块。 |
想跳过快速入门?没问题。您可以直接在应用程序中试用服务调用构建模块,以安全地与其他服务通信。在Dapr安装完成后,您可以通过以下方式开始使用服务调用API。
使用以下方式调用服务:
dapr-app-id
头即可开始。有关更多信息,请阅读使用HTTP调用服务。localhost:<dapr-http-port>
,您就可以直接调用API。您还可以在上面链接的HTTP代理文档中阅读更多关于此的信息。为了快速测试,尝试使用Dapr CLI进行服务调用:
dapr invoke --method <method-name>
命令以及方法标志和感兴趣的方法。有关更多信息,请阅读Dapr CLI。本文演示了如何部署服务,每个服务都有一个唯一的应用程序ID,以便其他服务可以通过HTTP进行服务调用来发现并调用它们的端点。
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中,在您的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))
}
}
要调用’GET’端点:
curl http://localhost:3602/v1.0/invoke/checkout/method/checkout/100
为了尽量减少URL路径的更改,Dapr提供了以下方式来调用服务API:
localhost:<dapr-http-port>
。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末尾附加查询字符串或片段,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:
有关跟踪和日志的更多信息,请参阅可观察性文章。
本文介绍如何通过 Dapr 使用 gRPC 进行服务间通信。
通过 Dapr 的 gRPC 代理功能,您可以使用现有的基于 proto 的 gRPC 服务,并让流量通过 Dapr sidecar。这为开发人员带来了以下 Dapr 服务调用 的优势:
Dapr 支持代理所有类型的 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 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 run --app-id client --dapr-grpc-port 50007 -- go run main.go
如果您在本地运行 Dapr 并安装了 Zipkin,请在浏览器中打开 http://localhost:9411
并查看客户端和服务器之间的跟踪。
在您的部署上设置以下 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 输出指标、跟踪和日志信息,允许您可视化服务之间的调用图、记录错误并可选地记录负载体。
有关跟踪和日志的更多信息,请参阅可观测性文章。
使用 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 时,由于其长时间存在的特性,弹性策略仅应用于“初始握手”。因此:
观看此视频了解如何使用 Dapr 的 gRPC 代理功能:
本文介绍如何通过Dapr使用HTTP调用非Dapr端点。
通过Dapr的服务调用API,您可以与使用或不使用Dapr的端点进行通信。使用Dapr调用非Dapr端点不仅提供了一致的API,还带来了以下Dapr服务调用的优势:
有时您可能需要调用非Dapr的HTTP端点,例如:
通过定义HTTPEndpoint
资源,您可以声明性地配置与非Dapr端点的交互方式。然后,您可以使用服务调用URL来访问非Dapr端点。或者,您可以直接在服务调用URL中使用非Dapr的完全限定域名(FQDN)端点URL。
在进行服务调用时,Dapr运行时遵循以下优先级顺序:
HTTPEndpoint
资源?http://
或https://
前缀的FQDN URL?appID
?下图概述了Dapr在调用非Dapr端点时的工作流程。
HTTPEndpoint
或FQDN URL定位服务B的位置,然后将消息转发给服务B。在与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用于通过appID
和my-method
调用Dapr应用程序。阅读如何:使用HTTP调用服务指南以获取更多信息。例如:
localhost:3500/v1.0/invoke/<appID>/method/<my-method>
curl http://localhost:3602/v1.0/invoke/orderprocessor/method/checkout
使用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端点。
在本文中,您将学习如何在不同命名空间之间进行服务调用。默认情况下,service-invocation支持通过简单引用应用程序ID(如nodeapp
)来调用同一命名空间内的服务:
localhost:3500/v1.0/invoke/nodeapp/method/neworder
service-invocation也支持跨命名空间的调用。在所有支持的平台上,Dapr应用程序ID遵循包含目标命名空间的有效FQDN格式。您可以同时指定:
nodeapp
),以及production
)。示例 1
调用位于production
命名空间中nodeapp
的neworder
方法:
localhost:3500/v1.0/invoke/nodeapp.production/method/neworder
在使用service-invocation调用不同命名空间中的应用程序时,您需要使用命名空间来限定它。这在Kubernetes集群中的跨命名空间调用中非常有用。
示例 2
调用位于production
命名空间中myapp
的ping
方法:
https://localhost:3500/v1.0/invoke/myapp.production/method/ping
示例 3
使用curl命令从外部DNS地址(例如api.demo.dapr.team
)调用与示例2相同的ping
方法,并提供Dapr API令牌进行身份验证:
MacOS/Linux:
curl -i -d '{ "message": "hello" }' \
-H "Content-type: application/json" \
-H "dapr-api-token: ${API_TOKEN}" \
https://api.demo.dapr.team/v1.0/invoke/myapp.production/method/ping
了解如何使用 Dapr 进行发布/订阅:
发布-订阅模式(pubsub)使微服务能够通过消息进行事件驱动的架构通信。
消息代理会将每条消息从发布者的输入通道复制到所有对该消息感兴趣的订阅者的输出通道。这种模式在需要将微服务解耦时特别有用。
在 Dapr 中,pubsub API:
您可以在运行时配置 Dapr pubsub 组件来使用特定的消息代理,这种可插拔性使您的服务更具可移植性和灵活性。
在 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”端点。
pubsub API 构建块为您的应用程序带来了多个特性。
为了启用消息路由并为服务之间的每条消息提供额外的上下文,Dapr 使用 CloudEvents 1.0 规范 作为其消息格式。任何应用程序通过 Dapr 发送到主题的消息都会自动包装在 Cloud Events 信封中,使用 Content-Type
头值 作为 datacontenttype
属性。
有关更多信息,请阅读 使用 CloudEvents 进行消息传递,或 发送不带 CloudEvents 的原始消息。
如果您的一个应用程序使用 Dapr 而另一个不使用,您可以为发布者或订阅者禁用 CloudEvent 包装。这允许在无法一次性采用 Dapr 的应用程序中部分采用 Dapr pubsub。
有关更多信息,请阅读 如何在没有 CloudEvents 的情况下使用 pubsub。
发布消息时,指定发送数据的内容类型很重要。除非指定,否则 Dapr 将假定为 text/plain
。
Content-Type
头中设置内容类型原则上,Dapr 认为消息一旦被订阅者处理并以非错误响应进行响应,就已成功传递。为了更细粒度的控制,Dapr 的 pubsub API 还提供了明确的状态,定义在响应负载中,订阅者可以用这些状态向 Dapr 指示特定的处理指令(例如,RETRY
或 DROP
)。
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 主题范围。
Dapr 可以在每条消息的基础上设置超时消息,这意味着如果消息未从 pubsub 组件中读取,则消息将被丢弃。此超时消息可防止未读消息的积累。如果消息在队列中的时间超过配置的 TTL,则标记为死信。有关更多信息,请阅读 pubsub 消息 TTL。
Dapr 支持在单个请求中发送和接收多条消息。当编写需要发送或接收大量消息的应用程序时,使用批量操作可以通过减少请求总数来实现高吞吐量。有关更多信息,请阅读 pubsub 批量消息。
在 Kubernetes 上运行时,使用 StatefulSets 结合 {podName}
标记,订阅者可以为每个实例拥有一个粘性 consumerID
。请参阅 如何使用 StatefulSets 水平扩展订阅者。
想要测试 Dapr pubsub API 吗?通过以下快速入门和教程来查看 pubsub 的实际应用:
快速入门/教程 | 描述 |
---|---|
Pubsub 快速入门 | 使用发布和订阅 API 发送和接收消息。 |
Pubsub 教程 | 演示如何使用 Dapr 启用 pubsub 应用程序。使用 Redis 作为 pubsub 组件。 |
想要跳过快速入门?没问题。您可以直接在应用程序中试用 pubsub 构建块来发布消息并订阅主题。在 安装 Dapr 后,您可以从 pubsub 如何指南 开始使用 pubsub API。
既然您已经了解了Dapr pubsub构建块的功能,接下来我们来看看如何在您的服务中应用它。下面的代码示例描述了一个使用两个服务处理订单的应用程序,每个服务都有Dapr sidecar:
Dapr会自动将用户的负载封装在符合CloudEvents v1.0的格式中,并使用Content-Type
头的值作为datacontenttype
属性。了解更多关于CloudEvents的消息。
以下示例展示了如何在您的应用程序中发布和订阅名为orders
的主题。
第一步是设置pubsub组件:
当您运行dapr init
时,Dapr会创建一个默认的Redis pubsub.yaml
并在您的本地机器上运行一个Redis容器,位置如下:
%UserProfile%\.dapr\components\pubsub.yaml
~/.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为orderprocessing
和checkout
的应用程序。将subscription.yaml
放在与您的pubsub.yaml
组件相同的目录中。当Dapr启动时,它会加载订阅和组件。
HotReload
功能门启用。
为了防止重新处理或丢失未处理的消息,在Dapr和您的应用程序之间的飞行消息在热重载事件期间不受影响。以下是利用Dapr SDK订阅您在subscription.yaml
中定义的主题的代码示例。
//依赖项
using System.Collections.Generic;
using System.Threading.Tasks;
using System;
using Microsoft.AspNetCore.Mvc;
using Dapr;
using Dapr.Client;
//代码
namespace CheckoutService.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消息传递。
为了实现消息路由并为每条消息提供额外的上下文,Dapr 采用 CloudEvents 1.0 规范 作为其消息格式。通过 Dapr 发送到主题的任何消息都会自动被包装在 CloudEvents 信封中,使用 Content-Type
头部值 作为 datacontenttype
属性。
Dapr 使用 CloudEvents 为事件负载提供额外的上下文,从而实现以下功能:
您可以选择以下三种方法之一通过发布订阅发布 CloudEvent:
向 Dapr 发送发布操作会自动将其包装在一个包含以下字段的 CloudEvent 信封中:
id
source
specversion
type
traceparent
traceid
tracestate
topic
pubsubname
time
datacontenttype
(可选)以下示例演示了 Dapr 为发布到 orders
主题的操作生成的 CloudEvent,其中包括:
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 自动生成多个 CloudEvent 属性。您可以通过提供以下可选元数据键/值来替换这些生成的 CloudEvent 属性:
cloudevent.id
: 覆盖 id
cloudevent.source
: 覆盖 source
cloudevent.type
: 覆盖 type
cloudevent.traceid
: 覆盖 traceid
cloudevent.tracestate
: 覆盖 tracestate
cloudevent.traceparent
: 覆盖 traceparent
使用这些元数据属性替换 CloudEvents 属性的能力适用于所有发布订阅组件。
例如,要替换代码中上述 CloudEvent 示例中的 source
和 id
值:
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 负载反映新的 source
和 id
值:
{
"topic": "orders",
"pubsubname": "order_pub_sub",
"traceid": "00-113ad9c4e42b27583ae98ba698d54255-e3743e35ff56f219-01",
"tracestate": "",
"data": {
"orderId": 1
},
"id": "d99b228f-6c73-4e78-8c4d-3f80a043d317",
"specversion": "1.0",
"datacontenttype": "application/json; charset=utf-8",
"source": "payment",
"type": "com.dapr.event.sent",
"time": "2020-09-23T06:23:21Z",
"traceparent": "00-113ad9c4e42b27583ae98ba698d54255-e3743e35ff56f219-01"
}
traceid
/traceparent
和 tracestate
,但这样做可能会干扰事件跟踪并在跟踪工具中报告不一致的结果。建议使用 Open Telemetry 进行分布式跟踪。了解更多关于分布式跟踪的信息。如果您想使用自己的 CloudEvent,请确保将 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 支持使用本身具备消息去重功能的消息代理。
在将Dapr集成到您的应用程序时,由于兼容性原因或某些应用程序不使用Dapr,某些服务可能仍需要通过不封装在CloudEvents中的pub/sub消息进行通信。这些消息被称为“原始”pub/sub消息。Dapr允许应用程序发布和订阅原始事件,这些事件未封装在CloudEvent中以实现兼容性。
Dapr应用程序可以将原始事件发布到pub/sub主题中,而不需要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主题的原始事件。
在以编程方式订阅时,添加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
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();
在这些示例中,根据 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"
作为参考,以下属性来自 CloudEvents 规范。
根据术语 data 的定义,CloudEvents 可能 包含有关事件发生的领域特定信息。当存在时,此信息将被封装在 data
中。
datacontenttype
属性指定(例如 application/json),并在这些相应属性存在时遵循 dataschema
格式。以下属性在所有 CloudEvents 中是必需的:
String
source
+ id
对于每个不同的事件都是唯一的。如果由于网络错误而重新发送重复事件,则它可能具有相同的 id
。消费者可以假设具有相同 source
和 id
的事件是重复的。类型: URI-reference
描述: 标识事件发生的上下文。通常包括以下信息:
URI 中编码的数据的确切语法和语义由事件生产者定义。
生产者 必须 确保 source
+ id
对于每个不同的事件都是唯一的。
应用程序可以:
source
,以便更容易生成唯一的 ID,并防止其他生产者具有相同的 source
。source
标识符。一个 source 可能包含多个生产者。在这种情况下,生产者 必须 合作以确保 source
+ id
对于每个不同的事件都是唯一的。
约束:
示例:
类型: String
描述: 事件使用的 CloudEvents 规范版本。这使得上下文的解释成为可能。合规的事件生产者 必须 在引用此版本的规范时使用 1.0
值。
目前,此属性仅包含“主要”和“次要”版本号。这允许在不更改此属性值的情况下对规范进行补丁更改。
注意:对于“候选发布”版本,可能会使用后缀进行测试。
约束:
String
type
的版本信息。有关更多信息,请参阅CloudEvents 的版本控制。以下属性在 CloudEvents 中是可选的。有关可选定义的更多信息,请参阅符号约定部分。
类型: 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
。
约束:
有关媒体类型示例,请参阅 IANA 媒体类型
URI
data
遵循的模式。与模式不兼容的更改应通过不同的 URI 反映。有关更多信息,请参阅CloudEvents 的版本控制。类型: String
描述: 这描述了事件生产者(由 source
标识)上下文中的事件主题。在发布-订阅场景中,订阅者通常会订阅由 source
发出的事件。如果 source
上下文具有内部子结构,则仅 source
标识符可能不足以作为任何特定事件的限定符。
在上下文元数据中(而不是仅在 data
负载中)识别事件的主题在通用订阅过滤场景中很有帮助,其中中间件无法解释 data
内容。在上述示例中,订阅者可能只对名称以 ‘.jpg’ 或 ‘.jpeg’ 结尾的 blob 感兴趣。使用 subject
属性,您可以为该事件子集构建简单而高效的字符串后缀过滤器。
约束:
示例:
订阅者可能会注册对在 blob 存储容器中创建新 blob 时的兴趣。在这种情况下:
source
标识订阅范围(存储容器)type
标识“blob 创建”事件id
唯一标识事件实例,以区分同名 blob 的单独创建事件。新创建的 blob 的名称在 subject
中传递:
source
: https://example.com/storage/tenant/containersubject
: mynewfile.jpgTimestamp
source
的生产者 必须 在这方面保持一致。换句话说,要么他们都使用事件发生的实际时间,要么他们都使用相同的算法来确定使用的值。观看此视频以了解如何使用 pubsub 进行消息路由:
Dapr 应用程序可以通过三种订阅类型来订阅已发布的主题,这三种类型支持相同的功能:声明式、流式和编程式。
订阅类型 | 描述 |
---|---|
声明式 | 订阅在外部文件中定义。声明式方法将 Dapr 的依赖从代码中移除,允许现有应用程序无需更改代码即可订阅主题。 |
流式 | 订阅在应用程序代码中定义。流式订阅是动态的,允许在运行时添加或删除订阅。它们不需要在应用程序中设置订阅端点(这是编程式和声明式订阅所需的),使其在代码中易于配置。流式订阅也不需要应用程序配置 sidecar 来接收消息。 |
编程式 | 订阅在应用程序代码中定义。编程式方法实现了静态订阅,并需要在代码中设置一个端点。 |
下面的示例演示了通过 orders
主题在 checkout
应用程序和 orderprocessing
应用程序之间的发布/订阅消息。示例首先以声明式,然后以编程式演示了相同的 Dapr 发布/订阅组件。
HotReload
功能门控启用。
为了防止重新处理或丢失未处理的消息,在 Dapr 和您的应用程序之间的飞行消息在热重载事件期间不受影响。您可以使用外部组件文件声明性地订阅一个主题。此示例使用名为 subscription.yaml
的 YAML 组件文件:
apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
name: order
spec:
topic: orders
routes:
default: /orders
pubsubname: pubsub
scopes:
- orderprocessing
这里的订阅名为 order
:
pubsub
的发布/订阅组件订阅名为 orders
的主题。route
字段以将所有主题消息发送到应用程序中的 /orders
端点。scopes
字段以将此订阅的访问范围仅限于 ID 为 orderprocessing
的应用程序。运行 Dapr 时,设置 YAML 组件文件路径以指向 Dapr 的组件。
dapr run --app-id myapp --resources-path ./myComponents -- dotnet run
dapr run --app-id myapp --resources-path ./myComponents -- mvn spring-boot:run
dapr run --app-id myapp --resources-path ./myComponents -- python3 app.py
dapr run --app-id myapp --resources-path ./myComponents -- npm start
dapr run --app-id myapp --resources-path ./myComponents -- go run app.go
在 Kubernetes 中,将组件应用到集群:
kubectl apply -f subscription.yaml
在您的应用程序代码中,订阅 Dapr 发布/订阅组件中指定的主题。
//订阅一个主题
[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()
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))
}
在某些情况下,应用程序可能由于各种原因无法处理消息。例如,可能会出现获取处理消息所需数据的临时问题,或者应用程序的业务逻辑失败并返回错误。死信主题用于处理这些无法投递的消息,并将其转发到订阅应用程序。这可以减轻应用程序处理失败消息的负担,使开发人员可以编写代码从死信主题中读取消息,修复后重新发送,或者选择放弃这些消息。
死信主题通常与重试策略和处理死信主题消息的订阅一起使用。
当配置了死信主题时,任何无法投递到应用程序的消息都会被放置在死信主题中,以便转发到处理这些消息的订阅。这可以是同一个应用程序或完全不同的应用程序。
即使底层系统不支持,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
您已经配置了 Dapr 的 pub/sub API 构建块,并且您的应用程序正在使用集中式消息代理顺利地发布和订阅主题。如果您想为应用程序执行简单的 A/B 测试、蓝/绿部署,甚至金丝雀部署,该怎么办?即使使用 Dapr,这也可能很困难。
Dapr 通过其 pub/sub 命名空间消费者组机制解决了大规模的多租户问题。
假设您有一个 Kubernetes 集群,其中两个应用程序(App1 和 App2)部署在同一个命名空间(namespace-a)中。App2 发布到一个名为 order
的主题,而 App1 订阅名为 order
的主题。这将创建两个以您的应用程序命名的消费者组(App1 和 App2)。
为了在使用集中式消息代理时进行简单的测试和部署,您创建了另一个命名空间,其中包含两个具有相同 app-id
的应用程序,App1 和 App2。
Dapr 使用单个应用程序的 app-id
创建消费者组,因此消费者组名称将保持为 App1 和 App2。
为了避免这种情况,您需要在代码中“潜入”一些东西来更改 app-id
,具体取决于您运行的命名空间。这种方法既麻烦又容易出错。
Dapr 不仅允许您使用 UUID 和 pod 名称的 consumerID 更改消费者组的行为,还提供了一个存在于 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 会自动识别其运行的命名空间并为您填充命名空间值,就像由运行时注入的动态元数据值一样。
与在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"
命名空间或组件范围可以用来限制组件的访问权限,使其仅对特定应用程序可用。这些应用程序范围的设置确保只有具有特定 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 和 topic3spec.metadata.allowedTopics
allowedTopics
(默认行为),则所有主题都是有效的。如果存在,subscriptionScopes
和 publishingScopes
仍然生效。publishingScopes
或 subscriptionScopes
可以与 allowedTopics
结合使用以添加细粒度限制spec.metadata.protectedTopics
publishingScopes
或 subscriptionScopes
明确授予应用程序发布或订阅权限才能发布/订阅该主题。这些元数据属性可用于所有 pub/sub 组件。以下示例使用 Redis 作为 pub/sub 组件。
在某些情况下,限制哪些应用程序可以发布/订阅主题是有用的,例如当您有包含敏感信息的主题时,只有一部分应用程序被允许发布或订阅这些主题。
它也可以用于所有主题,以始终拥有一个“真实来源”,以了解哪些应用程序作为发布者/订阅者使用哪些主题。
以下是三个应用程序和三个主题的示例:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: "localhost:6379"
- name: redisPassword
value: ""
- name: publishingScopes
value: "app1=topic1;app2=topic2,topic3;app3="
- name: subscriptionScopes
value: "app2=;app3=topic1"
下表显示了哪些应用程序被允许发布到主题:
topic1 | topic2 | topic3 | |
---|---|---|---|
app1 | ✅ | ||
app2 | ✅ | ✅ | |
app3 |
下表显示了哪些应用程序被允许订阅主题:
topic1 | topic2 | topic3 | |
---|---|---|---|
app1 | ✅ | ✅ | ✅ |
app2 | |||
app3 | ✅ |
注意:如果应用程序未列出(例如 subscriptionScopes 中的 app1),则允许其订阅所有主题。因为未使用
allowedTopics
,且 app1 没有任何订阅范围,它也可以使用上面未列出的其他主题。
如果 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"
所有应用程序都可以使用这些主题,但仅限于这些主题,不允许其他主题。
allowedTopics
和范围有时您希望结合两者范围,从而仅拥有一组固定的允许主题,并指定对某些应用程序的范围。
以下是三个应用程序和两个主题的示例:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: "localhost:6379"
- name: redisPassword
value: ""
- name: allowedTopics
value: "A,B"
- name: publishingScopes
value: "app1=A"
- name: subscriptionScopes
value: "app1=;app2=A"
注意:第三个应用程序未列出,因为如果应用程序未在范围内指定,则允许其使用所有主题。
下表显示了哪个应用程序被允许发布到主题:
A | B | C | |
---|---|---|---|
app1 | ✅ | ||
app2 | ✅ | ✅ | |
app3 | ✅ | ✅ |
下表显示了哪个应用程序被允许订阅主题:
A | B | C | |
---|---|---|---|
app1 | |||
app2 | ✅ | ||
app3 | ✅ | ✅ |
如果您的主题涉及敏感数据,则每个新应用程序必须在 publishingScopes
和 subscriptionScopes
中明确列出,以确保其无法读取或写入该主题。或者,您可以将主题指定为“保护”(使用 protectedTopics
),并仅授予真正需要的特定应用程序访问权限。
以下是三个应用程序和三个主题的示例,其中两个主题是保护的:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: "localhost:6379"
- name: redisPassword
value: ""
- name: protectedTopics
value: "A,B"
- name: publishingScopes
value: "app1=A,B;app2=B"
- name: subscriptionScopes
value: "app1=A,B;app2=B"
在上面的示例中,主题 A 和 B 被标记为保护。因此,即使 app3
未列在 publishingScopes
或 subscriptionScopes
中,它也无法与这些主题交互。
下表显示了哪个应用程序被允许发布到主题:
A | B | C | |
---|---|---|---|
app1 | ✅ | ✅ | |
app2 | ✅ | ||
app3 | ✅ |
下表显示了哪个应用程序被允许订阅主题:
A | B | C | |
---|---|---|---|
app1 | ✅ | ✅ | |
app2 | ✅ | ||
app3 | ✅ |
Dapr 支持为每条消息设置生存时间 (TTL)。这意味着应用程序可以为每条消息指定生存时间,过期后订阅者将不会收到这些消息。
所有 Dapr 发布/订阅组件 都兼容消息 TTL,因为 Dapr 在运行时内处理 TTL 逻辑。只需在发布消息时设置 ttlInSeconds
元数据即可。
在某些组件中,例如 Kafka,可以通过 retention.ms
在主题中配置生存时间,详见文档。使用 Dapr 的消息 TTL,使用 Kafka 的应用程序现在可以为每条消息设置生存时间,而不仅限于每个主题。
当发布/订阅组件原生支持消息生存时间时,Dapr 仅转发生存时间配置而不添加额外逻辑,保持行为的可预测性。这在组件以不同方式处理过期消息时非常有用。例如,在 Azure Service Bus 中,过期消息会被存储在死信队列中,而不是简单地删除。
Azure Service Bus 支持实体级别的生存时间。这意味着消息有默认的生存时间,但也可以在发布时设置为更短的时间跨度。Dapr 传播消息的生存时间元数据,并让 Azure Service Bus 直接处理过期。
如果消息由不使用 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 的参考。
通过批量发布和订阅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)
对于事件发布/订阅,涉及两种网络传输。
这些是可以进行优化的机会。当优化时,进行批量请求,从而减少总体调用次数,从而提高吞吐量并提供更好的延迟。
启用批量发布和/或批量订阅时,应用程序和Dapr sidecar之间的通信(上面第1点)针对所有组件进行了优化。
从Dapr sidecar到pubsub代理的优化取决于许多因素,例如:
目前,以下组件已更新以支持此级别的优化:
组件 | 批量发布 | 批量订阅 |
---|---|---|
Kafka | 是 | 是 |
Azure Servicebus | 是 | 是 |
Azure Eventhubs | 是 | 是 |
观看以下关于批量pubsub的演示和演讲。
Dapr 工作流让开发人员能够可靠地编写业务逻辑和集成。由于 Dapr 工作流是有状态的,它们支持长时间运行和容错应用程序,非常适合编排微服务。Dapr 工作流与其他 Dapr 构建块(如服务调用、发布订阅、状态管理和绑定)无缝协作。
Dapr 工作流的耐用性和弹性功能包括:
Dapr 工作流可以应用于以下场景:
使用 Dapr 工作流,您可以编写活动,然后在工作流中编排这些活动。工作流活动是:
除了活动之外,您还可以编写工作流以调度其他工作流作为子工作流。子工作流具有独立于启动它的父工作流的实例 ID、历史记录和状态,除了终止父工作流会终止由其创建的所有子工作流这一事实。子工作流还支持自动重试策略。
与 Dapr actor 相同,您可以为任何时间范围安排类似提醒的持久延迟。
当您使用工作流代码创建应用程序并使用 Dapr 运行它时,您可以调用驻留在应用程序中的特定工作流。每个单独的工作流可以:
Dapr 工作流简化了微服务架构中复杂的、有状态的协调需求。以下部分描述了可以从 Dapr 工作流中受益的几种应用程序模式。
了解更多关于不同类型的工作流模式
Dapr 工作流 编写 SDK 是特定语言的 SDK,包含用于实现工作流逻辑的类型和函数。工作流逻辑存在于您的应用程序中,并由运行在 Dapr sidecar 中的 Dapr 工作流引擎通过 gRPC 流进行编排。
您可以使用以下 SDK 编写工作流。
语言栈 | 包 |
---|---|
Python | dapr-ext-workflow |
JavaScript | DaprWorkflowClient |
.NET | Dapr.Workflow |
Java | io.dapr.workflows |
Go | workflow |
想要测试工作流?通过以下快速入门和教程来查看工作流的实际应用:
快速入门/教程 | 描述 |
---|---|
工作流快速入门 | 运行一个包含四个工作流活动的工作流应用程序,查看 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 安装完成后,您可以开始使用工作流,从如何编写工作流开始。
在您已经从高层次了解了工作流构建块之后,让我们深入探讨 Dapr 工作流引擎和 SDK 所包含的功能和概念。Dapr 工作流在所有支持的语言中都提供了几个核心功能和概念。
Dapr 工作流是您编写的函数,用于定义一系列按特定顺序执行的任务。Dapr 工作流引擎负责调度和执行这些任务,包括管理故障和重试。如果托管工作流的应用程序在多台机器上扩展,工作流引擎还可以在多台机器上负载均衡工作流及其任务的执行。
工作流可以调度多种类型的任务,包括:
每个您定义的工作流都有一个类型名称,工作流的每次执行都需要一个唯一的_实例 ID_。工作流实例 ID 可以由您的应用程序代码生成,这在工作流对应于业务实体(如文档或作业)时很有用,或者可以是自动生成的 UUID。工作流的实例 ID 对于调试以及使用工作流 API管理工作流非常有用。
在任何给定时间,只能存在一个具有给定 ID 的工作流实例。然而,如果一个工作流实例完成或失败,其 ID 可以被新的工作流实例重用。但请注意,新工作流实例实际上会在配置的状态存储中替换旧的实例。
Dapr 工作流通过使用一种称为事件溯源的技术来维护其执行状态。工作流引擎不是将工作流的当前状态存储为快照,而是管理一个仅追加的历史事件日志,描述工作流所采取的各种步骤。当使用工作流 SDK 时,这些历史事件会在工作流“等待”计划任务的结果时自动存储。
当工作流“等待”计划任务时,它会从内存中卸载自己,直到任务完成。一旦任务完成,工作流引擎会再次调度工作流函数运行。此时的工作流函数执行被称为_重放_。
当工作流函数被重放时,它会从头开始再次运行。然而,当它遇到已经完成的任务时,工作流引擎不会再次调度该任务,而是:
这种“重放”行为会持续到工作流函数完成或因错误而失败。
通过这种重放技术,工作流能够从任何“等待”点恢复执行,就像它从未从内存中卸载过一样。即使是先前运行的局部变量的值也可以恢复,而无需工作流引擎了解它们存储了什么数据。这种恢复状态的能力使 Dapr 工作流具有_持久性_和_容错性_。
如工作流重放部分所述,工作流维护其所有操作的仅写事件溯源历史日志。为了避免资源使用失控,工作流必须限制其调度的操作数量。例如,确保您的工作流不会:
您可以使用以下两种技术来编写可能需要调度极大量任务的工作流:
使用 continue-as-new API:
每个工作流 SDK 都公开了一个 continue-as-new API,工作流可以调用该 API 以使用新的输入和历史记录重新启动自己。continue-as-new API 特别适合实现“永恒工作流”,如监控代理,否则将使用 while (true)
类构造实现。使用 continue-as-new 是保持工作流历史记录小的好方法。
continue-as-new API 会截断现有历史记录,并用新的历史记录替换它。
使用子工作流:
每个工作流 SDK 都公开了一个用于创建子工作流的 API。子工作流的行为与任何其他工作流相同,只是它由父工作流调度。子工作流具有:
如果一个工作流需要调度数千个或更多任务,建议将这些任务分布在子工作流中,以免单个工作流的历史记录大小过大。
由于工作流是长时间运行且持久的,因此更新工作流代码必须非常小心。如工作流确定性限制部分所述,工作流代码必须是确定性的。如果系统中有任何未完成的工作流实例,更新工作流代码必须保留这种确定性。否则,更新工作流代码可能会导致下次这些工作流执行时出现运行时故障。
工作流活动是工作流中的基本工作单元,是在业务流程中被编排的任务。例如,您可能会创建一个工作流来处理订单。任务可能涉及检查库存、向客户收费和创建发货。每个任务将是一个单独的活动。这些活动可以串行执行、并行执行或两者的某种组合。
与工作流不同,活动在您可以在其中执行的工作类型上没有限制。活动经常用于进行网络调用或运行 CPU 密集型操作。活动还可以将数据返回给工作流。
Dapr 工作流引擎保证每个被调用的活动在工作流的执行过程中至少执行一次。由于活动仅保证至少一次执行,建议尽可能将活动逻辑实现为幂等。
除了活动之外,工作流还可以调度其他工作流作为_子工作流_。子工作流具有独立于启动它的父工作流的实例 ID、历史记录和状态。
子工作流有许多好处:
子工作流的返回值是其输出。如果子工作流因异常而失败,则该异常会像活动任务失败时一样显示给父工作流。子工作流还支持自动重试策略。
终止父工作流会终止由工作流实例创建的所有子工作流。有关更多信息,请参阅终止工作流 API。
Dapr 工作流允许您为任何时间范围安排类似提醒的持久延迟,包括分钟、天甚至年。这些_持久计时器_可以由工作流安排以实现简单的延迟或为其他异步任务设置临时超时。更具体地说,持久计时器可以设置为在特定日期触发或在指定持续时间后触发。持久计时器的最大持续时间没有限制,它们在内部由内部 actor 提醒支持。例如,跟踪服务 30 天免费订阅的工作流可以使用在工作流创建后 30 天触发的持久计时器实现。工作流在等待持久计时器触发时可以安全地从内存中卸载。
工作流支持活动和子工作流的持久重试策略。工作流重试策略与Dapr 弹性策略在以下方面是分开的和不同的。
重试在内部使用持久计时器实现。这意味着工作流在等待重试触发时可以安全地从内存中卸载,从而节省系统资源。这也意味着重试之间的延迟可以任意长,包括分钟、小时甚至天。
可以同时使用工作流重试策略和 Dapr 弹性策略。例如,如果工作流活动使用 Dapr 客户端调用服务,则 Dapr 客户端使用配置的弹性策略。有关示例的更多信息,请参阅快速入门:服务到服务的弹性。但是,如果活动本身因任何原因失败,包括耗尽弹性策略的重试次数,则工作流的弹性策略会启动。
由于工作流重试策略是在代码中配置的,因此具体的开发者体验可能会因工作流 SDK 的版本而异。通常,工作流重试策略可以通过以下参数进行配置。
参数 | 描述 |
---|---|
最大尝试次数 | 执行活动或子工作流的最大次数。 |
首次重试间隔 | 第一次重试前的等待时间。 |
退避系数 | 用于确定退避增长率的系数。例如,系数为 2 会使每次后续重试的等待时间加倍。 |
最大重试间隔 | 每次后续重试前的最大等待时间。 |
重试超时 | 重试的总体超时,无论配置的最大尝试次数如何。 |
有时工作流需要等待由外部系统引发的事件。例如,如果订单处理工作流中的总成本超过某个阈值,审批工作流可能需要人类明确批准订单请求。另一个例子是一个问答游戏编排工作流,它在等待所有参与者提交他们对问答问题的答案时暂停。这些中间执行输入被称为_外部事件_。
外部事件具有_名称_和_负载_,并传递给单个工作流实例。工作流可以创建“等待外部事件”任务,订阅外部事件并_等待_这些任务以阻止执行,直到接收到事件。然后,工作流可以读取这些事件的负载,并决定采取哪些下一步。外部事件可以串行或并行处理。外部事件可以由其他工作流或工作流代码引发。
工作流还可以等待同名的多个外部事件信号,在这种情况下,它们会以先进先出 (FIFO) 的方式分派给相应的工作流任务。如果工作流接收到外部事件信号,但尚未创建“等待外部事件”任务,则事件将保存到工作流的历史记录中,并在工作流请求事件后立即消费。
了解有关外部系统交互的更多信息。
Dapr 工作流依赖于 Go 的持久任务框架(即 durabletask-go)作为执行工作流的核心引擎。该引擎设计为支持多种后端实现。例如,durabletask-go 仓库包括一个 SQLite 实现,Dapr 仓库包括一个 actor 实现。
默认情况下,Dapr 工作流支持 actor 后端,该后端稳定且可扩展。然而,您可以选择 Dapr 工作流中支持的其他后端。例如,SQLite(待定未来版本)可以是本地开发和测试的后端选项。
后端实现在很大程度上与您看到的工作流核心引擎或编程模型解耦。后端主要影响:
从这个意义上说,它类似于 Dapr 的状态存储抽象,但专为工作流设计。无论使用哪个后端,所有 API 和编程模型功能都是相同的。
工作流状态可以从状态存储中清除,清除其所有历史记录并删除与特定工作流实例相关的所有元数据。清除功能用于已运行到 COMPLETED
、FAILED
或 TERMINATED
状态的工作流。
在工作流 API 参考指南中了解更多信息。
为了利用工作流重放技术,您的工作流代码需要是确定性的。为了使您的工作流代码确定性,您可能需要绕过一些限制。
生成随机数、随机 UUID 或当前日期的 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 等)上运行。工作流函数绝不能:
不遵循此规则可能导致未定义的行为。任何后台处理都应委托给活动任务,这些任务可以串行或并行调度运行。
例如,不要这样做:
// 不要这样做!
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)
确保您对工作流代码所做的更新保持其确定性。以下是可能破坏工作流确定性的代码更新示例:
更改工作流函数签名:
更改工作流或活动函数的名称、输入或输出被视为重大更改,必须避免。
更改工作流任务的数量或顺序:
更改工作流任务的数量或顺序会导致工作流实例的历史记录不再与代码匹配,可能导致运行时错误或其他意外行为。
要解决这些限制:
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 通常使用异步请求-回复模式实现。传统上实现此模式涉及以下步骤:
以下图示说明了端到端流程。
实现异步请求-回复模式的挑战在于它涉及使用多个 API 和状态存储。它还涉及正确实现协议,以便客户端知道如何自动轮询状态并知道操作何时完成。
Dapr 工作流 HTTP API 开箱即支持异步请求-回复模式,无需编写任何代码或进行任何状态管理。
以下 curl
命令说明了工作流 API 如何支持此模式。
curl -X POST http://localhost:3500/v1.0/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 -d '{"Name":"Paperclips","Quantity":1,"TotalCost":9.95}'
上一个命令将导致以下响应 JSON:
{"instanceID":"12345678"}
HTTP 客户端然后可以使用工作流实例 ID 构建状态查询 URL,并反复轮询,直到在负载中看到“COMPLETE”、“FAILURE”或“TERMINATED”状态。
curl http://localhost:3500/v1.0/workflows/dapr/12345678
以下是进行中的工作流状态可能的样子。
{
"instanceID": "12345678",
"workflowName": "OrderProcessingWorkflow",
"createdAt": "2023-05-03T23:22:11.143069826Z",
"lastUpdatedAt": "2023-05-03T23:22:22.460025267Z",
"runtimeStatus": "RUNNING",
"properties": {
"dapr.workflow.custom_status": "",
"dapr.workflow.input": "{\"Name\":\"Paperclips\",\"Quantity\":1,\"TotalCost\":9.95}"
}
}
如上例所示,工作流的运行时状态为 RUNNING
,这让客户端知道它应该继续轮询。
如果工作流已完成,状态可能如下所示。
{
"instanceID": "12345678",
"workflowName": "OrderProcessingWorkflow",
"createdAt": "2023-05-03T23:30:11.381146313Z",
"lastUpdatedAt": "2023-05-03T23:30:52.923870615Z",
"runtimeStatus": "COMPLETED",
"properties": {
"dapr.workflow.custom_status": "",
"dapr.workflow.input": "{\"Name\":\"Paperclips\",\"Quantity\":1,\"TotalCost\":9.95}",
"dapr.workflow.output": "{\"Processed\":true}"
}
}
如上例所示,工作流的运行时状态现在为 COMPLETED
,这意味着客户端可以停止轮询更新。
监控模式是一个通常包括以下步骤的重复过程:
下图提供了此模式的粗略说明。
根据业务需求,可能只有一个监控器,也可能有多个监控器,每个业务实体(例如股票)一个。此外,休眠时间可能需要根据情况进行更改。这些要求使得使用基于 cron 的调度系统不切实际。
Dapr 工作流通过允许您实现_永恒工作流_本地支持此模式。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 工作流通过外部事件功能支持此事件模式。
以下是涉及人类的采购订单工作流示例:
下图说明了此流程。
以下示例代码显示了如何使用 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 向工作流触发事件。
Dapr 工作流 允许开发者使用多种编程语言的普通代码定义工作流。工作流引擎运行在 Dapr sidecar 内部,并协调作为应用程序一部分部署的工作流代码。本文描述了:
有关如何在应用程序中编写 Dapr 工作流的更多信息,请参见 如何:编写工作流。
Dapr 工作流引擎的内部支持来自于 Dapr 的 actor 运行时。下图展示了 Kubernetes 模式下的 Dapr 工作流架构:
要使用 Dapr 工作流构建块,您需要在应用程序中使用 Dapr 工作流 SDK 编写工作流代码,该 SDK 内部通过 gRPC 流连接到 sidecar。这会注册工作流和任何工作流活动,或工作流可以调度的任务。
引擎直接嵌入在 sidecar 中,并通过 durabletask-go
框架库实现。此框架允许您更换不同的存储提供者,包括为 Dapr 创建的存储提供者,该提供者在幕后利用内部 actor。由于 Dapr 工作流使用 actor,您可以将工作流状态存储在状态存储中。
当工作流应用程序启动时,它使用工作流编写 SDK 向 Dapr sidecar 发送 gRPC 请求,并根据 服务器流式 RPC 模式 获取工作流工作项流。这些工作项可以是从“启动一个新的 X 工作流”(其中 X 是工作流的类型)到“调度活动 Y,输入 Z 以代表工作流 X 运行”的任何内容。
工作流应用程序执行相应的工作流代码,然后将执行结果通过 gRPC 请求发送回 sidecar。
所有交互都通过单个 gRPC 通道进行,并由应用程序发起,这意味着应用程序不需要打开任何入站端口。这些交互的细节由特定语言的 Dapr 工作流编写 SDK 内部处理。
如果您熟悉 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。
引擎管理的每个工作流实例都表示为一个或多个跨度。有一个单一的父跨度表示完整的工作流执行,以及各种任务的子跨度,包括活动任务执行和持久计时器的跨度。
工作流活动代码目前无法访问追踪上下文。
在 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 嵌入在 Dapr sidecar 中。应用程序代码完全不知道这些 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 的典型生命周期。
总结:
活动 actor 负责管理所有工作流活动调用的状态和放置。每当工作流调度一个活动任务时,就会激活一个新的活动 actor 实例。活动 actor 的 ID 是工作流的 ID 加上一个序列号(序列号从 0 开始)。例如,如果一个工作流的 ID 是 876bf371
,并且是工作流调度的第三个活动,它的 ID 将是 876bf371::2
,其中 2
是序列号。
每个活动 actor 将单个键存储到状态存储中:
键 | 描述 |
---|---|
activityState | 键包含活动调用负载,其中包括序列化的活动输入数据。此键在活动调用完成后自动删除。 |
下图展示了活动 actor 的典型生命周期。
活动 actor 是短暂的:
Dapr 工作流通过使用 actor 提醒 来确保工作流的容错性,以从瞬态系统故障中恢复。在调用应用程序工作流代码之前,工作流或活动 actor 将创建一个新的提醒。如果应用程序代码执行没有中断,提醒将被删除。然而,如果托管相关工作流或活动的节点或 sidecar 崩溃,提醒将重新激活相应的 actor 并重试执行。
Dapr 工作流在内部使用 actor 来驱动工作流的执行。像任何 actor 一样,这些内部工作流 actor 将其状态存储在配置的状态存储中。任何支持 actor 的状态存储都隐式支持 Dapr 工作流。
如 工作流 actor 部分所述,工作流通过追加到历史日志中增量保存其状态。工作流的历史日志分布在多个状态存储键中,以便每个“检查点”只需追加最新的条目。
每个检查点的大小由工作流在进入空闲状态之前调度的并发操作数决定。顺序工作流 因此将对状态存储进行较小的批量更新,而 扇出/扇入工作流 将需要更大的批量。批量的大小还受到工作流 调用活动 或 子工作流 时输入和输出大小的影响。
不同的状态存储实现可能隐式对您可以编写的工作流类型施加限制。例如,Azure Cosmos DB 状态存储将项目大小限制为 2 MB 的 UTF-8 编码 JSON(来源)。活动或子工作流的输入或输出负载作为状态存储中的单个记录存储,因此 2 MB 的项目限制意味着工作流和活动的输入和输出不能超过 2 MB 的 JSON 序列化数据。
同样,如果状态存储对批量事务的大小施加限制,这可能会限制工作流可以调度的并行操作数。
工作流状态可以从状态存储中清除,包括其所有历史记录。每个 Dapr SDK 都公开用于清除特定工作流实例的所有元数据的 API。
由于 Dapr 工作流在内部使用 actor 实现,Dapr 工作流具有与 actor 相同的可扩展性特征。放置服务:
工作流的预期可扩展性由以下因素决定:
目标应用程序中工作流代码的实现细节也在个别工作流实例的可扩展性中起作用。每个工作流实例一次在单个节点上执行,但工作流可以调度在其他节点上运行的活动和子工作流。
工作流还可以调度这些活动和子工作流以并行运行,允许单个工作流可能将计算任务分布在集群中的所有可用节点上。
目前,没有对工作流和活动并发性施加全局限制。因此,一个失控的工作流可能会在尝试并行调度过多任务时消耗集群中的所有资源。在编写 Dapr 工作流时,请小心调度大量并行工作的批次。
此外,Dapr 工作流引擎要求每个工作流应用程序的所有实例注册完全相同的工作流和活动。换句话说,无法独立扩展某些工作流或活动。应用程序中的所有工作流和活动必须一起扩展。
工作流不控制负载在集群中的具体分布方式。例如,如果一个工作流调度 10 个活动任务并行运行,所有 10 个任务可能在多达 10 个不同的计算节点上运行,也可能在少至一个计算节点上运行。实际的扩展行为由 actor 放置服务决定,该服务管理表示工作流每个任务的 actor 的分布。
工作流后端负责协调和保存工作流的状态。在任何给定时间,只能支持一个后端。您可以将工作流后端配置为一个组件,类似于 Dapr 中的任何其他组件。配置要求:
例如,以下示例演示了如何定义一个 actor 后端组件。Dapr 工作流目前默认仅支持 actor 后端,用户不需要定义 actor 后端组件即可使用它。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: actorbackend
spec:
type: workflowbackend.actor
version: v1
为了提供关于持久性和弹性的保证,Dapr 工作流频繁地写入状态存储并依赖提醒来驱动执行。因此,Dapr 工作流可能不适合对延迟敏感的工作负载。预期的高延迟来源包括:
有关工作流 actor 设计如何影响执行延迟的更多详细信息,请参见 提醒使用和执行保证部分。
本文提供了如何编写由Dapr工作流引擎执行的工作流的高级概述。
Dapr工作流逻辑是通过通用编程语言实现的,这使您可以:
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)
定义您希望工作流执行的工作流任务。任务被封装在实现工作流任务的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
以发送成功订单的通知。public class NotifyActivity : WorkflowActivity<Notification, object>
{
//...
public NotifyActivity(ILoggerFactory loggerFactory)
{
this.logger = loggerFactory.CreateLogger<NotifyActivity>();
}
//...
}
查看完整的NotifyActivity.cs
工作流任务示例。
public class ReserveInventoryActivity : WorkflowActivity<InventoryRequest, InventoryResult>
{
//...
public ReserveInventoryActivity(ILoggerFactory loggerFactory, DaprClient client)
{
this.logger = loggerFactory.CreateLogger<ReserveInventoryActivity>();
this.client = client;
}
//...
}
查看完整的ReserveInventoryActivity.cs
工作流任务示例。
public class ProcessPaymentActivity : WorkflowActivity<PaymentRequest, object>
{
//...
public ProcessPaymentActivity(ILoggerFactory loggerFactory)
{
this.logger = loggerFactory.CreateLogger<ProcessPaymentActivity>();
}
//...
}
定义您希望工作流执行的工作流任务。任务被封装在实现工作流任务的公共DemoWorkflowActivity
类中。
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class DemoWorkflowActivity implements WorkflowActivity {
@Override
public DemoActivityOutput run(WorkflowActivityContext ctx) {
Logger logger = LoggerFactory.getLogger(DemoWorkflowActivity.class);
logger.info("Starting Activity: " + ctx.getName());
var message = ctx.getInput(DemoActivityInput.class).getMessage();
var newMessage = message + " World!, from Activity";
logger.info("Message Received from input: " + message);
logger.info("Sending message to output: " + newMessage);
logger.info("Sleeping for 5 seconds to simulate long running operation...");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
logger.info("Activity finished");
var output = new DemoActivityOutput(message, newMessage);
logger.info("Activity returned: " + output);
return output;
}
}
定义您希望工作流执行的每个工作流任务。任务输入可以通过ctx.GetInput
从上下文中解组。任务应定义为接受ctx workflow.ActivityContext
参数并返回接口和错误。
func TestActivity(ctx workflow.ActivityContext) (any, error) {
var input int
if err := ctx.GetInput(&input); err != nil {
return "", err
}
// Do something here
return "result", nil
}
接下来,在工作流中注册并调用任务。
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)
接下来,使用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();
}
}
OrderProcessingWorkflow
类是从一个名为Workflow
的基类派生的,具有输入和输出参数类型。它还包括一个RunAsync
方法,该方法完成工作流的繁重工作并调用工作流任务。
class OrderProcessingWorkflow : Workflow<OrderPayload, OrderResult>
{
public override async Task<OrderResult> RunAsync(WorkflowContext context, OrderPayload order)
{
//...
await context.CallActivityAsync(
nameof(NotifyActivity),
new Notification($"Received order {orderId} for {order.Name} at {order.TotalCost:c}"));
//...
InventoryResult result = await context.CallActivityAsync<InventoryResult>(
nameof(ReserveInventoryActivity),
new InventoryRequest(RequestId: orderId, order.Name, order.Quantity));
//...
await context.CallActivityAsync(
nameof(ProcessPaymentActivity),
new PaymentRequest(RequestId: orderId, order.TotalCost, "USD"));
await context.CallActivityAsync(
nameof(NotifyActivity),
new Notification($"Order {orderId} processed successfully!"));
// End the workflow with a success result
return new OrderResult(Processed: true);
}
}
接下来,使用WorkflowRuntimeBuilder
注册工作流并启动工作流运行时。
public class DemoWorkflowWorker {
public static void main(String[] args) throws Exception {
// Register the Workflow with the builder.
WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder().registerWorkflow(DemoWorkflow.class);
builder.registerActivity(DemoWorkflowActivity.class);
// Build and then start the workflow runtime pulling and executing tasks
try (WorkflowRuntime runtime = builder.build()) {
System.out.println("Start workflow runtime");
runtime.start();
}
System.exit(0);
}
}
定义您的工作流函数,参数为ctx *workflow.WorkflowContext
,返回任何和错误。从您的工作流中调用您定义的任务。
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
}
最后,使用工作流编写应用程序。
在以下示例中,对于使用Python SDK的基本Python hello world应用程序,您的项目代码将包括:
DaprClient
的Python包,用于接收Python SDK功能。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应用程序。在此示例中,您的项目代码将包括:
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
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
Workflow
的DemoWorkflow
类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();
// ...
};
}
}
如以下示例所示,使用Go SDK和Dapr工作流的hello-world应用程序将包括:
client
的Go包,用于接收Go SDK客户端功能。TestWorkflow
方法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
}
现在您已经编写了一个工作流,学习如何管理它。
管理工作流 >>现在您已经在应用程序中编写了工作流及其活动,您可以使用HTTP API调用来启动、终止和获取工作流的信息。有关更多信息,请阅读工作流API参考。
在代码中管理您的工作流。在编写工作流指南中的工作流示例中,工作流通过以下API在代码中注册:
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在代码中注册:
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在代码中注册:
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在代码中注册:
// 启动工作流
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调用的信息。
了解如何使用 Dapr 进行状态管理:
您的应用程序可以利用Dapr的状态管理API在支持的状态存储中保存、读取和查询键/值对。通过状态存储组件,您可以构建有状态且长时间运行的应用程序,例如购物车或游戏的会话状态。如下图所示:
以下视频和演示概述了Dapr状态管理的工作原理。
通过状态管理API模块,您的应用程序可以利用一些通常复杂且容易出错的功能,包括:
以下是状态管理API的一些功能:
Dapr的数据存储被设计为组件,可以在不更改服务代码的情况下进行替换。查看支持的状态存储以获取更多信息。
使用Dapr,您可以在状态操作请求中附加元数据,描述您期望请求如何被处理。您可以附加:
默认情况下,您的应用程序应假设数据存储是最终一致的,并使用最后写入胜出并发模式。
并非所有存储都是平等的。为了确保您的应用程序的可移植性,您可以查询存储的元数据能力,并使您的代码适应不同的存储能力。
Dapr支持使用ETags的乐观并发控制(OCC)。当请求状态值时,Dapr总是将ETag属性附加到返回的状态中。当用户代码:
If-Match
头附加ETag。当提供的ETag与状态存储中的ETag匹配时,写入
操作成功。
在许多应用程序中,数据更新冲突很少见,因为客户端通常根据业务上下文分区以操作不同的数据。然而,如果您的应用程序选择使用ETags,不匹配的ETags可能导致请求被拒绝。建议您在代码中使用重试策略,以在使用ETags时补偿冲突。
如果您的应用程序在写入请求中省略ETags,Dapr在处理请求时会跳过ETag检查。这使得最后写入胜出模式成为可能,与使用ETags的首次写入胜出模式相比。
阅读API参考以了解如何设置并发选项。
Dapr支持强一致性和最终一致性,最终一致性是默认行为。
阅读API参考以了解如何设置一致性选项。
状态存储组件可能会根据内容类型不同地维护和操作数据。Dapr支持在状态管理API中作为请求元数据的一部分传递内容类型。
设置内容类型是_可选的_,组件决定是否使用它。Dapr仅提供将此信息传递给组件的手段。
metadata.contentType
设置内容类型。例如,http://localhost:3500/v1.0/state/store?metadata.contentType=application/json
。"contentType" : <content type>
来设置内容类型。Dapr支持两种类型的多读或多写操作:批量或事务性。阅读API参考以了解如何使用批量和多选项。
您可以将多个读取请求分组为批量(或批次)操作。在批量操作中,Dapr将读取请求作为单独的请求提交到底层数据存储,并将它们作为单个结果返回。
您可以将写入、更新和删除操作分组为一个请求,然后作为一个原子事务处理。请求将作为一组事务性操作成功或失败。
事务性状态存储可用于存储actor状态。要指定用于actor的状态存储,请在状态存储组件的元数据部分中将属性actorStateStore
的值指定为true
。actor状态以特定方案存储在事务性状态存储中,允许进行一致的查询。所有actor只能使用一个状态存储组件作为状态存储。阅读state API参考和actors API参考以了解有关actor状态存储的更多信息。
在保存actor状态时,您应始终设置TTL元数据字段(ttlInSeconds
)或在您选择的SDK中使用等效的API调用,以确保状态最终被移除。阅读actors概述以获取更多信息。
Dapr支持应用程序状态的自动客户端加密,并支持密钥轮换。这在所有Dapr状态存储上都支持。有关更多信息,请阅读如何:加密应用程序状态主题。
不同应用程序在共享状态时的需求各不相同。在一种情况下,您可能希望将所有状态封装在给定应用程序中,并让Dapr为您管理访问。在另一种情况下,您可能希望两个应用程序在同一状态上工作,以获取和保存相同的键。
Dapr使状态能够:
有关更多详细信息,请阅读如何:在应用程序之间共享状态。
Dapr使开发人员能够使用外发模式在事务性状态存储和任何消息代理之间实现单一事务。有关更多信息,请阅读如何启用事务性外发消息。
有两种方法可以查询状态:
使用_可选的_状态管理查询API,您可以查询状态存储中保存的键/值数据,无论底层数据库或存储技术如何。使用状态管理查询API,您可以过滤、排序和分页键/值数据。有关更多详细信息,请阅读如何:查询状态。
Dapr在不进行任何转换的情况下保存和检索状态值。您可以直接从底层状态存储查询和聚合状态。例如,要获取与应用程序ID “myApp” 相关的所有状态键,请在Redis中使用:
KEYS "myApp*"
如果数据存储支持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'
Dapr支持每个状态设置请求的生存时间(TTL)。这意味着应用程序可以为每个存储的状态设置生存时间,这些状态在过期后无法检索。
状态管理API可以在状态管理API参考中找到,该参考描述了如何通过提供键来检索、保存、删除和查询状态值。
想要测试Dapr状态管理API吗?通过以下快速入门和教程,看看状态管理的实际应用:
快速入门/教程 | 描述 |
---|---|
状态管理快速入门 | 使用状态管理API创建有状态的应用程序。 |
Hello World | 推荐 演示如何在本地运行Dapr。突出显示服务调用和状态管理。 |
Hello World Kubernetes | 推荐 演示如何在Kubernetes中运行Dapr。突出显示服务调用和_状态管理_。 |
想要跳过快速入门?没问题。您可以直接在您的应用程序中试用状态管理模块。在Dapr安装后,您可以从状态管理如何指南开始使用状态管理API。
状态管理是新应用程序、遗留应用程序、单体应用程序或微服务应用程序的常见需求之一。处理和测试不同的数据库库,以及处理重试和故障,可能既困难又耗时。
在本指南中,您将学习如何使用键/值状态API来保存、获取和删除应用程序的状态。
下面的代码示例描述了一个处理订单的应用程序,该应用程序使用Dapr sidecar。订单处理服务通过Dapr将状态存储在Redis状态存储中。
状态存储组件是Dapr用于与数据库通信的资源。
在本指南中,我们将使用Redis状态存储,但您也可以选择支持列表中的其他状态存储。
当您在selfhost模式下运行dapr init
时,Dapr会在您的本地机器上创建一个默认的Redis statestore.yaml
并运行一个Redis状态存储,位置如下:
%UserProfile%\.dapr\components\statestore.yaml
~/.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上设置不同的状态存储以获取更多信息。
app-id
,因为状态键会以此值为前缀。如果您不设置app-id
,系统会在运行时为您生成一个。下次运行命令时,会生成一个新的app-id
,您将无法再访问之前保存的状态。以下示例展示了如何使用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'
通过状态查询API,您可以从状态存储组件中检索、过滤和排序键/值数据。查询API并不是完整查询语言的替代品。
尽管状态存储是键/值存储,value
可能是一个包含自身层次结构、键和值的JSON文档。查询API允许您使用这些键/值来检索相应的文档。
您可以通过HTTP POST/PUT或gRPC提交查询请求。请求的主体是一个包含以下三个部分的JSON对象:
filter
sort
page
filter
filter
用于指定查询条件,结构类似于树形,每个节点表示一个操作,可能是单一或多个操作数。
支持以下操作:
操作符 | 操作数 | 描述 |
---|---|---|
EQ | key:value | key 等于 value |
NEQ | key:value | key 不等于 value |
GT | key:value | key 大于 value |
GTE | key:value | key 大于等于 value |
LT | key:value | key 小于 value |
LTE | key:value | key 小于等于 value |
IN | key:[]value | key 等于 value[0] 或 value[1] 或 … 或 value[n] |
AND | []operation | operation[0] 且 operation[1] 且 … 且 operation[n] |
OR | []operation | operation[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
包含limit
和token
参数。
limit
设置每页返回的记录数。token
是组件返回的分页令牌,用于获取后续查询的结果。在后台,此查询请求被转换为本地查询语言并由状态存储组件执行。
让我们来看一些从简单到复杂的真实示例。
作为数据集,考虑一个包含员工ID、组织、州和城市的员工记录集合。注意,这个数据集是一个键/值对数组,其中:
key
是唯一IDvalue
是包含员工记录的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结构中选择记录。
现在您可以运行示例查询。
首先,查找加利福尼亚州的所有员工,并按其员工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"
}
]
}
现在,查找来自"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'
与前一个示例类似,结果是一个匹配键/值对的数组。
在此示例中,查找:
此外,首先按州按字母降序排序,然后按员工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有以下限制:
您可以在相关链接部分找到更多信息。
在本文中,您将学习如何创建一个可以水平扩展的有状态服务,选择性使用并发和一致性模型。状态管理API可以帮助开发者简化状态协调、冲突解决和故障处理的复杂性。
状态存储组件是Dapr用来与数据库通信的资源。在本指南中,我们将使用默认的Redis状态存储。
当您在本地模式下运行dapr init
时,Dapr会创建一个默认的Redis statestore.yaml
并在您的本地机器上运行一个Redis状态存储,位置如下:
%UserProfile%\.dapr\components\statestore.yaml
~/.dapr/components/statestore.yaml
通过statestore.yaml
组件,您可以轻松替换底层组件而无需更改应用程序代码。
查看支持的状态存储列表。
在强一致性模式下,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利用ETags来确定状态的版本号。ETags从状态请求中以ETag
头返回。使用ETags,您的应用程序知道自上次检查以来资源已更新,因为在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)
事务性 Outbox 模式是一种广为人知的设计模式,用于发送应用程序状态变化的通知。它通过一个跨越数据库和消息代理的单一事务来传递通知。
开发人员在尝试自行实现此模式时会遇到许多技术难题,通常需要编写复杂且容易出错的中央协调管理器,这些管理器最多支持一种或两种数据库和消息代理的组合。
例如,您可以使用 Outbox 模式来:
通过 Dapr 的 Outbox 支持,您可以在调用 Dapr 的事务 API时通知订阅者应用程序的状态何时被创建或更新。
下图概述了 Outbox 功能的工作原理:
Outbox 功能可以与 Dapr 支持的任何事务性状态存储一起使用。所有发布/订阅代理都支持 Outbox 功能。
要启用 Outbox 功能,请在状态存储组件上添加以下必需和可选字段:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: mysql-outbox
spec:
type: state.mysql
version: v1
metadata:
- name: connectionString
value: "<CONNECTION STRING>"
- name: outboxPublishPubsub # 必需
value: "mypubsub"
- name: outboxPublishTopic # 必需
value: "newOrder"
- name: outboxPubsub # 可选
value: "myOutboxPubsub"
- name: outboxDiscardWhenMissingState # 可选,默认为 false
value: false
名称 | 必需 | 默认值 | 描述 |
---|---|---|---|
outboxPublishPubsub | 是 | N/A | 设置发布状态更改时传递通知的发布/订阅组件的名称 |
outboxPublishTopic | 是 | N/A | 设置接收在配置了 outboxPublishPubsub 的发布/订阅上的状态更改的主题。消息体将是 insert 或 update 操作的状态事务项 |
outboxPubsub | 否 | outboxPublishPubsub | 设置 Dapr 用于协调状态和发布/订阅事务的发布/订阅组件。如果未设置,则使用配置了 outboxPublishPubsub 的发布/订阅组件。如果您希望将用于发送通知状态更改的发布/订阅组件与用于协调事务的组件分开,这将很有用 |
outboxDiscardWhenMissingState | 否 | false | 通过将 outboxDiscardWhenMissingState 设置为 true ,如果 Dapr 无法在数据库中找到状态且不重试,则 Dapr 将丢弃事务。如果在 Dapr 能够传递消息之前,状态存储数据因任何原因被删除,并且您希望 Dapr 从发布/订阅中删除项目并停止重试获取状态,此设置可能会很有用 |
如果您希望使用相同的状态存储来发送 Outbox 和非 Outbox 消息,只需定义两个连接到相同状态存储的状态存储组件,其中一个具有 Outbox 功能,另一个没有。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: mysql
spec:
type: state.mysql
version: v1
metadata:
- name: connectionString
value: "<CONNECTION STRING>"
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: mysql-outbox
spec:
type: state.mysql
version: v1
metadata:
- name: connectionString
value: "<CONNECTION STRING>"
- name: outboxPublishPubsub # 必需
value: "mypubsub"
- name: outboxPublishTopic # 必需
value: "newOrder"
您可以通过设置另一个不保存到数据库并明确提及为投影的事务来覆盖发布到发布/订阅代理的 Outbox 模式消息。此事务添加了一个名为 outbox.projection
的元数据键,值设置为 true
。当添加到事务中保存的状态数组时,此负载在写入状态时被忽略,数据用作发送到上游订阅者的负载。
要正确使用,key
值必须在状态存储上的操作和消息投影之间匹配。如果键不匹配,则整个事务失败。
如果您为同一键启用了两个或多个 outbox.projection
状态项,则使用第一个定义的项,其他项将被忽略。
在以下 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
):
您可以使用自定义 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",
}
}'
data
CloudEvent 字段仅供 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
。
对静态应用程序状态进行加密,以在企业工作负载或受监管环境中提供更强的安全性。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
字段附加到实际状态密钥的末尾。
要轮换密钥,
primaryEncryptionKey
以指向包含新密钥的 secret。secondaryEncryptionKey
。新数据将使用新密钥加密,任何检索到的旧数据将使用次密钥解密。
使用旧密钥加密的数据项的任何更新都将使用新密钥重新加密。
Dapr 在保存和检索状态时不对状态值进行转换。Dapr 要求所有状态存储实现遵循特定的键格式规范(参见状态管理规范)。您可以直接与底层存储交互以操作状态数据,例如:
要连接到您的 Cosmos DB 实例,您可以:
要获取与应用程序 “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'
要获取与实例 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'
Dapr 在保存和检索状态时不对状态值进行转换。Dapr 要求所有状态存储实现遵循特定的键格式规范(参见状态管理规范)。您可以直接与底层存储交互以操作状态数据,例如:
您可以使用官方的 redis-cli 或任何其他兼容 Redis 的工具连接到 Redis 状态存储以直接查询 Dapr 状态。如果您在容器中运行 Redis,最简单的使用 redis-cli 的方法是通过容器:
docker run --rm -it --link <Redis 容器的名称> redis redis-cli -h <Redis 容器的名称>
要获取与应用程序 “myapp” 关联的所有状态键,请使用命令:
KEYS myapp*
上述命令返回现有键的列表,例如:
1) "myapp||balance"
2) "myapp||amount"
Dapr 将状态值保存为哈希值。每个哈希值包含一个 “data” 字段,其中存储状态数据,以及一个 “version” 字段,作为 ETag,表示不断递增的版本。
例如,要通过键 “balance” 获取应用程序 “myapp” 的状态数据,请使用命令:
HGET myapp||balance data
要获取状态版本/ETag,请使用命令:
HGET myapp||balance version
要获取与应用程序 ID 为 “mypets” 的 actor 类型 “cat” 的实例 ID 为 “leroy” 关联的所有状态键,请使用命令:
KEYS mypets||cat||leroy*
要获取特定的 actor 状态,例如 “food”,请使用命令:
HGET mypets||cat||leroy||food value
Dapr 在保存和检索状态时不对状态值进行转换。Dapr 要求所有状态存储实现遵循特定的键格式(参见状态管理规范)。您可以直接与底层存储交互来操作状态数据,例如:
连接到 SQL Server 实例的最简单方法是使用:
要获取与应用程序 “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 类型 “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'
Dapr 允许为每个状态设置生存时间 (TTL)。这意味着应用程序可以为存储的每个状态指定一个生存时间,过期后将无法检索这些状态。
对于支持的状态存储,只需在发布消息时设置 ttlInSeconds
元数据。其他状态存储将忽略此值。对于某些状态存储,您可以为每个表或容器指定默认的过期时间。
当状态存储组件原生支持状态 TTL 时,Dapr 会直接传递 TTL 配置,而不添加额外的逻辑,从而保持行为的一致性。这在组件以不同方式处理过期状态时尤为有用。
如果未指定 TTL,将保留状态存储的默认行为。
对于允许为所有数据指定默认 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'
了解如何使用 Dapr bindings 的更多信息:
通过 Dapr 的 bindings API,您可以利用外部系统的事件来触发应用程序,并与外部系统交互。使用 bindings API,您可以:
例如,通过 bindings,您的应用程序可以响应传入的 Twilio/SMS 消息,而无需:
在上图中:
"create"
。bindings 的开发独立于 Dapr 运行时。您可以查看并贡献 bindings。
通过输入 bindings,您可以在外部资源发生事件时触发您的应用程序。请求中可以发送可选的负载和元数据。
以下概述视频和演示展示了 Dapr 输入 binding 的工作原理。
要接收来自输入 binding 的事件:
阅读使用输入 bindings 创建事件驱动应用程序指南以开始使用输入 bindings。
通过输出 bindings,您可以调用外部资源。调用请求中可以发送可选的负载和元数据。
以下概述视频和演示展示了 Dapr 输出 binding 的工作原理。
要调用输出 binding:
"create"
"update"
"delete"
"exec"
阅读使用输出 bindings 与外部资源交互指南以开始使用输出 bindings。
您可以提供 direction
元数据字段以指示 binding 组件支持的方向。这可以使 Dapr sidecar 避免“等待应用程序准备就绪”状态,减少 Dapr sidecar 与应用程序之间的生命周期依赖:
"input"
"output"
"input, output"
direction
属性。查看 bindings direction
元数据的完整示例。
想要测试 Dapr bindings API?通过以下快速入门和教程来查看 bindings 的实际应用:
快速入门/教程 | 描述 |
---|---|
bindings 快速入门 | 使用输入 bindings 处理外部系统的事件,并使用输出 bindings 调用操作。 |
bindings 教程 | 演示如何使用 Dapr 创建到其他组件的输入和输出 bindings。使用 bindings 连接到 Kafka。 |
想要跳过快速入门?没问题。您可以直接在应用程序中试用 bindings 模块,以调用输出 bindings 和触发输入 bindings。在Dapr 安装完成后,您可以从输入 bindings 如何指南开始使用 bindings API。
当外部资源发生事件时,您可以通过输入绑定来触发您的应用程序。外部资源可以是队列、消息管道、云服务、文件系统等。请求中可以发送可选的负载和元数据。
输入绑定非常适合用于事件驱动的处理、数据管道或一般的事件响应和后续处理。Dapr输入绑定允许您:
本指南使用Kafka绑定作为示例。您可以从绑定组件列表中找到您偏好的绑定规范。在本指南中:
/binding
端点,使用checkout
作为要调用的绑定名称。data
字段中,可以是任何可序列化为JSON的值。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
指定的绑定名称相同。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
事件投递保证由绑定实现控制。根据绑定实现,事件投递可以是精确一次或至少一次。
使用输出绑定,您可以与外部资源进行交互。在调用请求中,您可以发送可选的负载和元数据。
本指南以Kafka绑定为例。您可以从绑定组件列表中选择您偏好的绑定规范。在本指南中:
/binding
端点,使用checkout
作为要调用的绑定名称。data
字段中,可以是任何JSON可序列化的值。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" }'
观看此视频以了解如何使用双向输出绑定。
Actor 模型将 actor 描述为“计算的基本单位”。换句话说,您可以将代码编写在一个自包含的单元中(称为 actor),该单元接收消息并一次处理一条消息,而无需任何形式的并发或线程。
当您的代码处理一条消息时,它可以向其他 actor 发送一条或多条消息,或创建新的 actor。底层运行时管理每个 actor 的运行方式、时间和位置,并在 actor 之间路由消息。
大量的 actor 可以同时执行,并且 actor 彼此独立执行。
Dapr 包含一个专门实现虚拟 Actor 模型的运行时。通过 Dapr 的实现,您可以根据 Actor 模型编写 Dapr actor,Dapr 利用底层平台提供的可扩展性和可靠性保证。
每个 actor 都被定义为 actor 类型的一个实例,与对象是类的一个实例相同。例如,可能有一个实现计算器功能的 actor 类型,并且可能有许多此类型的 actor 分布在集群的各个节点上。每个这样的 actor 都由一个 actor ID 唯一标识。
以下概述视频和演示展示了 Dapr 中的 actor 如何工作。
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 管理其激活和生命周期。
与任何其他技术决策一样,您应该根据要解决的问题来决定是否使用 actor。例如,如果您正在构建一个聊天应用程序,您可能会使用 Dapr actors 来实现聊天室和用户之间的单个聊天会话,因为每个聊天会话都需要维护自己的状态并且具有可扩展性和容错性。
一般来说,如果您的问题空间涉及大量(数千个或更多)小型、独立和隔离的状态和逻辑单元,可以考虑使用 actor 模式来建模您的问题或场景。
当您需要定义和编排涉及多个服务和组件的复杂 workflow 时,您可以使用 Dapr Workflow。例如,使用前面提到的聊天应用程序示例,您可能会使用 Dapr Workflow 来定义应用程序的整体 workflow,例如如何注册新用户、如何发送和接收消息以及应用程序如何处理错误和异常。
了解有关 Dapr Workflow 的更多信息以及如何在应用程序中使用 workflow。
actor 被唯一定义为 actor 类型的一个实例,类似于对象是类的一个实例。例如,您可能有一个实现计算器功能的 actor 类型。可能有许多此类型的 actor 分布在集群的各个节点上。
每个 actor 都由一个 actor ID 唯一标识。actor ID 可以是您选择的任何字符串值。如果您不提供 actor ID,Dapr 会为您生成一个随机字符串作为 ID。
Dapr 支持命名空间化的 actor。actor 类型可以部署到不同的命名空间中。您可以在同一命名空间中调用这些 actor 的实例。
由于 Dapr actors 是虚拟的,因此不需要显式创建或销毁。Dapr actor 运行时:
actor 的状态超出了对象的生命周期,因为状态存储在为 Dapr 运行时配置的状态提供者中。
为了提供可扩展性和可靠性,actor 实例在整个集群中分布,Dapr 在整个集群中分布 actor 实例,并自动将它们迁移到健康的节点。
您可以通过 HTTP 调用 actor 方法,如下面的通用示例所示。
Dapr actor 运行时为访问 actor 方法提供了一个简单的轮流访问模型。轮流访问极大地简化了并发系统,因为不需要同步机制来进行数据访问。
事务性状态存储可以用于存储 actor 状态。无论您是否打算在 actor 中存储任何状态,您都必须在状态存储组件的元数据部分中将属性 actorStateStore
的值指定为 true
。actor 状态以特定方案存储在事务性状态存储中,允许进行一致的查询。所有 actor 只能使用单个状态存储组件作为状态存储。阅读状态 API 参考和actors API 参考以了解有关 actor 状态存储的更多信息。
actor 可以通过注册定时器或提醒来安排定期工作。
定时器和提醒的功能非常相似。主要区别在于 Dapr actor 运行时在停用后不保留有关定时器的任何信息,而是使用 Dapr actor 状态提供者持久化有关提醒的信息。
这种区别允许用户在轻量级但无状态的定时器与更耗资源但有状态的提醒之间进行权衡。
以下概述视频和演示展示了 actor 定时器和提醒如何工作。
在您已经从高层次上了解了 Actor 构建块之后,让我们深入探讨 Dapr 中 Actor 的特性和概念。
Dapr 中的 Actor 是虚拟的,这意味着它们的生命周期与内存中的表示无关。因此,不需要显式地创建或销毁它们。Dapr 的 Actor 运行时会在首次收到某个 Actor ID 的请求时自动激活该 Actor。如果某个 Actor 在一段时间内未被使用,Dapr 的 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。
Dapr 的 Actor 运行时通过 Actor Placement
服务为您管理分布方案和键范围设置。当创建服务的新实例时:
Placement
服务计算给定 Actor 类型的所有实例的分区。每个 Actor 类型的分区数据表在环境中运行的每个 Dapr 实例中更新和存储,并且可以随着 Actor 服务的新实例的创建和销毁而动态变化。
当客户端调用具有特定 ID 的 Actor(例如,Actor ID 123)时,客户端的 Dapr 实例对 Actor 类型和 ID 进行哈希,并使用信息调用可以为该特定 Actor ID 提供请求的相应 Dapr 实例。因此,对于任何给定的 Actor ID,总是调用相同的分区(或服务实例)。这在下图中显示。
这简化了一些选择,但也带来了一些考虑:
您可以通过调用 HTTP 端点与 Dapr 交互以调用 Actor 方法。
POST/GET/PUT/DELETE http://localhost:3500/v1.0/actors/<actorType>/<actorId>/<method/state/timers/reminders>
您可以在请求体中为 Actor 方法提供任何数据,请求的响应将在响应体中,这是来自 Actor 调用的数据。
另一种可能更方便的与 Actor 交互的方式是通过 SDK。Dapr 目前支持 .NET、Java 和 Python 的 Actor SDK。
有关更多详细信息,请参阅 Dapr Actor 特性。
Dapr 的 Actor 运行时为访问 Actor 方法提供了简单的轮转访问模型。这意味着在任何时候,Actor 对象的代码中最多只能有一个线程处于活动状态。轮转访问极大地简化了并发系统,因为不需要同步机制来进行数据访问。这也意味着系统必须针对每个 Actor 实例的单线程访问特性进行特殊设计。
单个 Actor 实例不能同时处理多个请求。如果期望 Actor 实例处理并发请求,它可能会导致吞吐量瓶颈。
如果在两个 Actor 之间存在循环请求,同时对其中一个 Actor 发出外部请求,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)的方法和回调执行时间线的示例。
您可以使用以下配置参数来调整 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>();
}
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);
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
)
)
// 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);
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)
}
在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命名空间使用不同的物理数据库,但某些状态存储组件提供了一种通过表、前缀、集合等逻辑分隔数据的方法。这允许您在多个命名空间中使用相同的物理数据库,只要您在Dapr组件定义中提供逻辑分隔即可。
以下是一些示例。
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"
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"
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
- name: actorStateStore
value: "true"
- name: redisDB
value: "1"
- name: redisPassword
secretKeyRef:
name: redis-secret
key: redis-password
- name: actorStateStore
value: "true"
- name: redisDB
value: "1"
auth:
secretStore: <SECRET_STORE_NAME>
查看您的状态存储组件规格以了解其提供的功能。
actor 可以通过注册定时器或提醒来安排周期性工作。
定时器和提醒的功能非常相似。主要区别在于 Dapr actor 运行时在停用后不会保留任何关于定时器的信息,而是使用 Dapr actor 状态提供程序持久化提醒的信息。
这种区别允许用户在轻量但无状态的定时器与更资源密集但有状态的提醒之间进行选择。
定时器和提醒的调度配置是相同的,概述如下:
dueTime
是一个可选参数,用于设置第一次调用回调的时间或时间间隔。如果省略 dueTime
,则在定时器/提醒注册后立即调用回调。
支持的格式:
2020-10-02T15:00:00Z
2h30m
PT2H30M
period
是一个可选参数,用于设置两次连续回调调用之间的时间间隔。当以 ISO 8601-1 持续时间
格式指定时,您还可以配置重复次数以限制回调调用的总次数。
如果省略 period
,则回调只会被调用一次。
支持的格式:
2h30m
PT2H30M
, R5/PT1M30S
ttl
是一个可选参数,用于设置定时器/提醒将过期和删除的时间或时间间隔。如果省略 ttl
,则不应用任何限制。
支持的格式:
2020-10-02T15:00:00Z
2h30m
PT2H30M
actor 运行时验证调度配置的正确性,并在输入无效时返回错误。
当您同时指定 period
中的重复次数和 ttl
时,定时器/提醒将在任一条件满足时停止。
您可以在 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 停用和故障转移期间被触发,因为 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 提醒
GET http://localhost:3500/v1.0/actors/<actorType>/<actorId>/reminders/<name>
您可以通过调用以下命令删除 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 的状态。
如果方法的调用失败,定时器不会被移除。定时器仅在以下情况下被移除:
actor 提醒数据默认序列化为 JSON。从 Dapr v1.13 开始,支持通过 Placement 和 Scheduler 服务为工作流的内部提醒数据使用 protobuf 序列化格式。根据吞吐量和负载大小,这可以显著提高性能,为开发人员提供更高的吞吐量和更低的延迟。
另一个好处是将较小的数据存储在 actor 底层数据库中,这在使用某些云数据库时可以实现成本优化。使用 protobuf 序列化的限制是提醒数据不再可查询。
以 protobuf 格式保存的提醒数据无法在 Dapr 1.12.x 及更早版本中读取。建议在 Dapr v1.13 中测试此功能,并验证它在您的数据库中按预期工作,然后再投入生产。
要在 Kubernetes 上为 actor 提醒使用 protobuf 序列化,请使用以下 Helm 值:
--set dapr_placement.maxActorApiLevel=20
要在自托管环境中为 actor 提醒使用 protobuf 序列化,请使用以下 daprd
标志:
--max-api-level=20
actor提醒在sidecar重启后仍然持久化并继续触发。注册了多个提醒的应用程序可能会遇到以下问题:
为了解决这些问题,应用程序可以通过在state存储中将数据分布在多个键中来启用actor提醒分区。
actors\|\|<actor type>\|\|metadata
中使用一个元数据记录来存储给定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的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>();
}
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");
from datetime import timedelta
ActorRuntime.set_actor_config(
ActorRuntimeConfig(
actor_idle_timeout=timedelta(hours=1),
actor_scan_interval=timedelta(seconds=30),
remindersStoragePartitions=7
)
)
// 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);
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)
}
以下是一个有效的提醒分区配置示例:
{
"entities": [ "MyActorType", "AnotherActorType" ],
"remindersStoragePartitions": 7
}
为了配置actor提醒分区,Dapr将actor类型元数据持久化在actor的state存储中。这允许配置更改在全局范围内应用,而不仅仅是在单个sidecar实例中。
此外,您只能增加分区数量,不能减少。这允许Dapr在滚动重启时自动重新分配数据,其中一个或多个分区配置可能处于活动状态。
了解如何通过HTTP/gRPC端点来使用虚拟actor。
您可以通过调用HTTP/gRPC端点与Dapr交互,以调用actor方法。
POST/GET/PUT/DELETE http://localhost:3500/v1.0/actors/<actorType>/<actorId>/method/<method>
在请求体中提供actor方法所需的数据。请求的响应,即actor方法调用返回的数据,将在响应体中。
有关更多详细信息,请参阅Actors API规范。
您可以通过HTTP/gRPC端点与Dapr交互,利用Dapr的actor状态管理功能来可靠地保存状态。
要使用actors,您的状态存储必须支持多项事务。这意味着您的状态存储组件需要实现TransactionalStore
接口。
查看支持事务/actors的组件列表。所有actors只能使用一个状态存储组件来保存状态。
虚拟actor模式的一个核心原则是actor的单线程执行特性。没有重入时,Dapr运行时会锁定所有actor请求。第二个请求必须等到第一个请求完成后才能启动。这意味着actor不能调用自身,也不能被另一个actor调用,即使它们属于同一调用链。
重入通过允许同一链或上下文的请求重新进入已锁定的actor来解决这个问题。这在以下场景中非常有用:
重入允许的调用链示例如下:
Actor A -> Actor A
Actor A -> Actor B -> Actor A
通过重入,您可以执行更复杂的actor调用,而不影响虚拟actor的单线程特性。
maxStackDepth
参数用于设置一个值,以控制对同一actor可以进行多少次重入调用。默认情况下,这个值为32,通常已经足够。
要启用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重入。
了解如何使用DaprSecret的更多信息:
应用程序通常使用专用的 secret 存储来保存敏感信息。例如,您可以使用存储在 secret 存储中的连接字符串、密钥、令牌和其他应用程序级别的 secret 来对数据库、服务和外部系统进行身份验证,例如 AWS Secrets Manager, Azure Key Vault, Hashicorp Vault 等。
为了访问这些 secret 存储,应用程序需要导入 secret 存储的 SDK。在多云场景中,这种情况更具挑战性,因为可能会使用不同供应商特定的 secret 存储。
Dapr 的专用 secrets 构建块 API 使开发人员更容易从 secret 存储中使用应用程序 secret。要使用 Dapr 的 secret 存储构建块,您需要:
以下概述视频和演示展示了 Dapr secrets 管理的工作原理。
Secrets 管理 API 构建块为您的应用程序带来了多种功能。
您可以在应用程序代码中调用 secrets API,从 Dapr 支持的 secret 存储中检索和使用 secret。观看此视频以了解如何在应用程序中使用 secrets 管理 API 的示例。
例如,下图显示了一个应用程序从配置的云 secret 存储中请求名为 “mysecret” 的 secret,该 secret 存储名为 “vault”。
应用程序还可以使用 secrets API 从 Kubernetes secret 存储中访问 secret。默认情况下,Dapr 在 Kubernetes 模式下启用了内置的 Kubernetes secret 存储,可以通过以下方式部署:
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。在下面的示例中:
在上述示例中,应用程序代码无需更改即可获取相同的 secret。Dapr 通过 secrets 管理构建块 API 使用 secret 管理组件。
尝试使用 secrets API 通过我们的快速入门或教程之一。
在配置 Dapr 组件(如 state 存储)时,通常需要在组件文件中包含凭据。或者,您可以将凭据放在 Dapr 支持的 secret 存储中,并在 Dapr 组件中引用该 secret。这是首选方法和推荐的最佳实践,尤其是在生产环境中。
有关更多信息,请阅读在组件中引用 secret 存储。
为了对 secret 的访问提供更细粒度的控制,Dapr 提供了定义范围和限制访问权限的能力。了解更多关于使用 secret 范围的信息。
想要测试 Dapr secrets 管理 API 吗?通过以下快速入门和教程来查看 Dapr secrets 的实际应用:
快速入门/教程 | 描述 |
---|---|
Secrets 管理快速入门 | 使用 secrets 管理 API 从配置的 secret 存储中在应用程序代码中检索 secret。 |
Secret Store 教程 | 演示如何使用 Dapr Secrets API 访问 secret 存储。 |
想要跳过快速入门?没问题。您可以直接在应用程序中尝试使用 secret 管理构建块来检索和管理 secret。在安装 Dapr后,您可以从secrets 使用指南开始使用 secrets 管理 API。
在了解了Dapr Secret 构建块的功能后,接下来学习如何在服务中使用它。本指南将演示如何调用 Secret API,并从配置的 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: ":"
dapr run
命令的位置。更多信息:
通过调用 Dapr sidecar 的 Secret API 来获取 Secret:
curl http://localhost:3601/v1.0/secrets/localsecretstore/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();
当您为应用程序配置了 secret 存储后,Dapr 应用程序默认可以访问该存储中定义的所有 secret。
您可以通过在应用程序配置中定义 secret 访问范围策略,来限制 Dapr 应用程序对特定 secret 的访问权限。
secret 访问范围策略适用于任何secret 存储,包括:
有关如何设置secret 存储的详细信息,请阅读如何:检索 secret。
观看此视频以了解如何在应用程序中使用 secret 访问范围的演示。
在此示例中,所有 secret 访问都被拒绝给运行在 Kubernetes 集群上的应用程序,该集群配置了名为 mycustomsecretstore
的Kubernetes 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。
此示例使用名为 vault
的 secret 存储。这可以是设置在应用程序上的 Hashicorp secret 存储组件。要允许 Dapr 应用程序仅访问 vault
secret 存储中的 secret1
和 secret2
,请定义以下 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。
定义以下 config.yaml
:
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: appconfig
spec:
secrets:
scopes:
- storeName: vault
defaultAccess: allow # 这是默认值,可以省略
deniedSecrets: ["secret1", "secret2"]
此示例配置明确拒绝访问名为 vault
的 secret 存储中的 secret1
和 secret2
,同时允许访问所有其他 secret。了解如何将配置应用于 sidecar。
allowedSecrets
和 deniedSecrets
列表的设置优先于 defaultAccess
策略。
场景 | defaultAccess | allowedSecrets | deniedSecrets | 权限 |
---|---|---|---|---|
1 - 仅默认访问 | deny/allow | 空 | 空 | deny/allow |
2 - 默认拒绝并允许列表 | deny | [“s1”] | 空 | 仅 “s1” 可访问 |
3 - 默认允许并拒绝列表 | allow | 空 | [“s1”] | 仅 “s1” 不可访问 |
4 - 默认允许并允许列表 | allow | [“s1”] | 空 | 仅 “s1” 可访问 |
5 - 默认拒绝并拒绝列表 | deny | 空 | [“s1”] | deny |
6 - 默认拒绝/允许并同时有列表 | deny/allow | [“s1”] | [“s2”] | 仅 “s1” 可访问 |
在开发应用程序时,配置是一个常见的任务。通常,我们会使用配置存储来管理这些配置数据。配置项通常具有动态特性,并且与应用程序的需求紧密相关。
例如,应用程序的配置可能包括:
通常,配置项以键/值对的形式存储在状态存储或数据库中。开发人员或运维人员可以在运行时更改配置存储中的应用程序配置。一旦进行了更改,服务会被通知以加载新的配置。
从应用程序API的角度来看,配置数据是只读的,配置存储的更新通过运维工具进行。使用Dapr的配置API,您可以:
想要测试Dapr配置API?通过以下快速入门来了解配置API的实际应用:
快速入门 | 描述 |
---|---|
配置快速入门 | 使用配置API获取配置项或订阅配置更改。 |
想要跳过快速入门?没问题。您可以直接在应用程序中尝试配置构建模块以读取和管理配置数据。在Dapr安装完成后,您可以从配置操作指南开始使用配置API。
请参阅以下指南:
本示例使用Redis配置存储组件来演示如何检索配置项。
在支持的配置存储中创建一个配置项。这可以是一个简单的键值项,使用您选择的任何键。本示例使用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组件YAML:
kubectl
。statestore.yaml
组件具有相同的元数据,如果您已经有Redis statestore.yaml
,可以直接复制或修改Redis状态存储组件类型。apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: configstore
spec:
type: configuration.redis
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: <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'
锁用于确保资源的互斥访问。例如,您可以使用锁来:
任何需要更新的共享资源都可以被锁定。锁通常用于改变状态的操作,而不是读取操作。
每个锁都有一个名称。应用程序决定锁定哪些资源。通常,同一应用程序的多个实例使用这个命名锁来独占访问资源并进行更新。
例如,在竞争消费者模式中,应用程序的多个实例访问一个队列。您可以选择在应用程序执行其业务逻辑时锁定队列。
在下图中,同一应用程序的两个实例,App1
,使用Redis锁组件来锁定共享资源。
*此API目前处于Alpha
状态。
在任何给定时刻,只有一个应用程序实例可以持有命名锁。锁的范围限定在Dapr应用程序ID内。
Dapr分布式锁使用基于租约的锁定机制。如果应用程序获取锁后遇到异常,无法释放锁,则锁将在一段时间后通过租约自动释放。这防止了在应用程序故障时发生资源死锁。
了解了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概述以了解更多信息。
许多应用程序需要作业调度,或者需要在未来执行某些操作。作业API是一个用于管理和安排这些未来作业的工具,可以在特定时间或间隔执行。
作业API不仅帮助您安排作业,Dapr内部还利用调度服务来安排actor提醒。
在Dapr中,作业包括:
作业API是一个作业调度器,而不是作业的执行者。设计上保证作业至少执行一次,注重可靠性和可扩展性,而非精确性。这意味着:
所有计划作业的详细信息和用户相关数据都存储在调度器服务的Etcd数据库中。 您可以使用作业来:
作业调度在以下场景中可能会有所帮助:
自动化数据库备份: 确保数据库每天备份以防止数据丢失。安排一个备份脚本在每晚2点运行,创建数据库备份并将其存储在安全位置。
定期数据处理和ETL(提取、转换、加载): 处理和转换来自各种来源的原始数据并将其加载到数据仓库中。安排ETL作业在特定时间运行(例如:每小时、每天)以获取新数据、处理并更新数据仓库中的信息。
电子邮件通知和报告: 通过电子邮件接收每日销售报告和每周性能摘要。安排一个作业生成所需的报告并在每天早上6点通过电子邮件发送每日报告,每周一早上8点发送每周摘要。
维护任务和系统更新: 执行定期维护任务,如清理临时文件、更新软件和检查系统健康状况。安排各种维护脚本在非高峰时段运行,如周末或深夜,以尽量减少对用户的干扰。
金融交易的批处理: 处理需要在每个工作日结束时批处理和结算的大量交易。安排批处理作业在每个工作日下午5点运行,汇总当天的交易并执行必要的结算和对账。
Dapr的作业API确保这些场景中表示的任务在没有人工干预的情况下始终如一地执行,提高效率并减少错误风险。
作业API提供了多种特性,使您可以轻松调度作业。
调度器服务支持在多个副本之间扩展作业调度,同时保证作业仅由一个调度器服务实例触发。
您可以在应用程序中试用作业API。在Dapr安装完成后,您可以开始使用作业API,从如何:调度作业指南开始。
现在您已经了解了作业构建块提供的功能,让我们来看一个如何使用API的示例。下面的代码示例描述了一个为数据库备份应用程序调度作业并在触发时处理它们的应用程序,也就是作业因到达其到期时间而被返回到应用程序的时间。
当您在本地托管模式或Kubernetes上运行dapr init
时,Dapr调度器服务会启动。
在您的代码中,配置并调度应用程序内的作业。
以下.NET SDK代码示例调度名为prod-db-backup
的作业。作业数据包含有关您将定期备份的数据库的信息。在本示例中,您将:
在以下示例中,您将创建记录,序列化并与作业一起注册,以便在将来作业被触发时可以使用这些信息:
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
,此作业被触发并最多发送回应用程序Repeats
(10
)次。
// ...
// 设置作业
job := daprc.Job{
Name: "prod-db-backup",
Schedule: "@every 1s",
Repeats: 10,
Data: &anypb.Any{
Value: jobData,
},
}
在触发时间,调用prodDBBackupHandler
函数,在触发时间执行此作业的所需业务逻辑。例如:
当您使用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) {
// 处理触发的作业
}
当作业到达其计划的触发时间时,触发的作业通过以下回调函数发送回应用程序:
注意:以下示例是用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方法路由。
对于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
}
一旦您在应用程序中设置了作业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
Dapr的会话API简化了与大型语言模型(LLM)进行大规模、安全、可靠交互的复杂性。无论您是缺乏必要本地SDK的开发者,还是只想专注于LLM交互提示的多语言开发团队,会话API都提供了一个统一的API接口来与底层LLM提供商进行对话。
除了启用关键的性能和安全功能(如提示缓存和个人信息清理),您还可以将会话API与Dapr的其他功能结合使用,例如:
Dapr通过为您的LLM交互提供指标,增强了系统的可观测性。
以下功能适用于所有支持的会话组件。
提示缓存通过存储和重用在多个API调用中经常重复的提示来优化性能。Dapr将这些频繁使用的提示存储在本地缓存中,从而显著减少延迟和成本,使您的集群、pod或其他组件可以重用,而无需为每个新请求重新处理信息。
个人信息清理功能能够识别并删除会话响应中的任何形式的敏感用户信息。只需在输入和输出数据上启用此功能,即可保护您的隐私,清除可能用于识别个人的敏感细节。
观看在Diagrid的Dapr v1.15庆祝活动中展示的演示,了解会话API如何使用.NET SDK工作。
想要测试Dapr会话API?通过以下快速入门和教程来查看其实际应用:
快速入门/教程 | 描述 |
---|---|
会话快速入门 | TODO |
想跳过快速入门?没问题。您可以直接在您的应用中试用会话模块。在Dapr安装完成后,您可以从操作指南开始使用会话API。
让我们开始使用 conversation API。在本指南中,您将学习如何:
dapr run
启动连接。创建一个名为 conversation.yaml
的新配置文件,并将其保存到应用程序目录中的组件或配置子文件夹中。
为您的 conversation.yaml
文件选择 合适的 conversation 组件规范。
在这个场景中,我们使用一个简单的 echo 组件。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: echo
spec:
type: conversation.echo
version: v1
以下示例使用 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(())
}
使用 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。
type: docs title: “加密技术” linkTitle: “加密技术” weight: 100 description: “在不暴露密钥的情况下执行加密操作,确保应用程序的安全性”
使用加密构建块,您可以安全且一致地利用加密技术。Dapr 提供的 API 允许您在密钥库或 Dapr sidecar 中执行加密和解密操作,而无需将加密密钥暴露给您的应用程序。
加密技术在应用程序中被广泛使用,正确实施可以在数据泄露时提高安全性。在某些情况下,您可能需要使用加密技术以符合行业法规(如金融领域)或法律要求(如 GDPR 等隐私法规)。
然而,正确使用加密技术可能很复杂。您需要:
安全的一个重要要求是限制对加密密钥的访问,这通常被称为“原始密钥材料”。Dapr 可以与密钥库集成,如 Azure Key Vault(未来将支持更多组件),这些密钥库将密钥存储在安全的环境中,并在库中执行加密操作,而不将密钥暴露给您的应用程序或 Dapr。
或者,您可以配置 Dapr 为您管理加密密钥,在 sidecar 中执行操作,同样不将原始密钥材料暴露给您的应用程序。
使用 Dapr,您可以在不将加密密钥暴露给应用程序的情况下执行加密操作。
通过使用加密构建块,您可以:
Dapr 加密构建块包括两种组件:
允许与管理服务或库(“密钥库”)交互的组件。
类似于 Dapr 在各种 secret 存储或 state 存储之上的“抽象层”,这些组件允许与各种密钥库(如 Azure Key Vault)交互(未来 Dapr 版本中会有更多)。通过这些组件,对私钥的加密操作在库中执行,Dapr 从未看到您的私钥。
基于 Dapr 自身加密引擎的组件。
当密钥库不可用时,您可以利用基于 Dapr 自身加密引擎的组件。这些组件名称中带有 .dapr.
,在 Dapr sidecar 中执行加密操作,密钥存储在文件、Kubernetes secret 或其他来源中。虽然 Dapr 知道私钥,但它们仍然对您的应用程序不可用。
这两种组件,无论是利用密钥库还是使用 Dapr 中的加密引擎,都提供相同的抽象层。这允许您的解决方案根据需要在各种库和/或加密组件之间切换。例如,您可以在开发期间使用本地存储的密钥,而在生产中使用云库。
加密 API 允许使用 Dapr Crypto Scheme v1 加密和解密数据。这是一种有见地的加密方案,旨在使用现代、安全的加密标准,并以流的方式高效处理数据(甚至是大文件)。
想要测试 Dapr 加密 API 吗?通过以下快速入门和教程,看看加密如何实际运作:
快速入门/教程 | 描述 |
---|---|
加密快速入门 | 使用加密 API 使用 RSA 和 AES 密钥加密和解密消息和大文件。 |
想要跳过快速入门?没问题。您可以直接在应用程序中试用加密构建块来加密和解密您的应用程序。在 安装 Dapr 后,您可以从 加密操作指南 开始使用加密 API。
观看此 Dapr 社区电话 #83 中的加密 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",
})