1 - 发布订阅概述

发布订阅 API 构建块概述

发布订阅(pub/sub)使微服务能够使用消息进行通信,实现事件驱动架构。

  • 生产者(发布者)将消息写入输入通道并发送到主题,但不知道哪个应用程序会接收它们。
  • 消费者(订阅者)订阅主题并从输出通道接收消息,但不知道这些消息是由哪个服务产生的。

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



发布订阅 API

Dapr 中的发布订阅 API:

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

服务使用的特定消息代理是可插拔的,在运行时配置为 Dapr 发布订阅组件。这消除了服务对消息代理的依赖,使服务更具可移植性和灵活性。

使用 Dapr 中的发布订阅时:

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

以下概述视频和演示 演示了 Dapr 发布订阅的工作原理。

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

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

查看 Dapr 支持的完整发布订阅组件列表

功能

发布订阅 API 构建块为你的应用程序带来多项功能。

使用 CloudEvents 发送消息

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

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

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

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

更多信息,请阅读 如何在不使用 CloudEvents 的情况下使用发布订阅

设置消息内容类型

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

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

消息传递

原则上,Dapr 认为订阅者处理消息并返回非错误响应后,消息即成功传递。为了更精细的控制,Dapr 的发布订阅 API 还提供明确的状态(在响应负载中定义),订阅者使用这些状态向 Dapr 指示特定的处理指令(例如,RETRYDROP)。

使用主题订阅接收消息

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

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

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

重新加载主题订阅

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

消息路由

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

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

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

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

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

启用发件箱模式

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

命名空间消费者组

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

至少一次保证

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

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

所有 Dapr 发布订阅组件都支持至少一次保证。

订阅启动可靠性

Dapr 自动重试失败的订阅启动,以提高部署场景下的可靠性。这确保了你的发布订阅应用程序即使在面临临时连接或权限问题时也能保持弹性。

当 Dapr 遇到启动订阅的错误时,它会在日志中显示错误消息并继续尝试启动订阅。

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

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

当同一应用程序的多个实例(具有相同的 app-id)订阅主题时,Dapr 将每条消息传递到该应用程序的只有一个实例。此概念如下图所示。



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

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

限制主题以增强安全性

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

消息生存时间(TTL)

Dapr 可以按消息设置超时消息,这意味着如果消息未从发布订阅组件中读取,则消息将被丢弃。此超时消息可防止未读消息堆积。如果消息在队列中停留的时间超过配置的 TTL,它将被标记为死信。更多信息,请阅读 发布订阅消息 TTL

发布和订阅批量消息

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

使用 StatefulSets 扩展订阅者

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

尝试发布订阅

快速入门和教程

想测试 Dapr 发布订阅 API?请完成以下快速入门和教程来了解发布订阅的实际应用:

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

直接在应用程序中使用发布订阅

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

下一步

2 - 操作指南:发布消息并订阅主题

了解如何通过一个服务向主题发送消息,并在另一个服务中订阅该主题

既然您已经了解了 Dapr 发布订阅构建块提供的能力,接下来了解如何在您的服务中使用它。下面的代码示例简单描述了一个包含两个服务的订单处理应用程序,每个服务都配置了 Dapr 边车:

  • 一个结账服务,使用 Dapr 订阅消息队列中的主题。
  • 一个订单处理服务,使用 Dapr 向 RabbitMQ 发布消息。
Diagram showing state management of example service

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

下面的示例演示了您的应用程序如何发布和订阅名为 orders 的主题。

设置发布/订阅组件

第一步是设置发布/订阅组件:

当您运行 dapr init 时,Dapr 会创建一个默认的 Redis pubsub.yaml 并在您的本地机器上运行一个 Redis 容器,位于:

  • 在 Windows 上,位于 %UserProfile%\.dapr\components\pubsub.yaml
  • 在 Linux/MacOS 上,位于 ~/.dapr/components/pubsub.yaml

通过 pubsub.yaml 组件,您可以轻松更换底层组件而无需更改应用程序代码。在本示例中,使用的是 RabbitMQ。

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: order-pub-sub
spec:
  type: pubsub.rabbitmq
  version: v1
  metadata:
  - name: host
    value: "amqp://localhost:5672"
  - name: durable
    value: "false"
  - name: deletedWhenUnused
    value: "false"
  - name: autoAck
    value: "false"
  - name: reconnectWait
    value: "0"
  - name: concurrency
    value: parallel
scopes:
  - orderprocessing
  - checkout

您可以通过创建一个包含该文件的组件目录(在本例中为 myComponents)并在 dapr run CLI 命令中使用 --resources-path 标志,用另一个发布订阅组件覆盖此文件。

要将其部署到 Kubernetes 集群中,请填写下面 YAML 中发布/订阅组件metadata 连接详情,将其保存为 pubsub.yaml,然后运行 kubectl apply -f pubsub.yaml

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: order-pub-sub
spec:
  type: pubsub.rabbitmq
  version: v1
  metadata:
  - name: connectionString
    value: "amqp://localhost:5672"
  - name: protocol
    value: amqp  
  - name: hostname
    value: localhost 
  - name: username
    value: username
  - name: password
    value: password 
  - name: durable
    value: "false"
  - name: deletedWhenUnused
    value: "false"
  - name: autoAck
    value: "false"
  - name: reconnectWait
    value: "0"
  - name: concurrency
    value: parallel
scopes:
  - orderprocessing
  - checkout
dapr run --app-id myapp --resources-path ./myComponents -- dotnet run
dapr run --app-id myapp --resources-path ./myComponents -- mvn spring-boot:run
dapr run --app-id myapp --resources-path ./myComponents -- python3 app.py
dapr run --app-id myapp --resources-path ./myComponents -- go run app.go
dapr run --app-id myapp --resources-path ./myComponents -- npm start

订阅主题

Dapr 提供了三种订阅主题的方法:

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

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

创建一个名为 subscription.yaml 的文件并粘贴以下内容:

apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
  name: order-pub-sub
spec:
  topic: orders
  routes: 
    default: /checkout
  pubsubname: order-pub-sub
scopes:
- orderprocessing
- checkout

上面的示例展示了对 orders 主题的事件订阅,使用的发布订阅组件是 order-pub-sub

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

subscription.yaml 放在与 pubsub.yaml 组件相同的目录中。当 Dapr 启动时,它会与组件一起加载订阅。

以下是利用 Dapr SDK 订阅您在 subscription.yaml 中定义的主题的代码示例。

using System.Collections.Generic;
using System.Threading.Tasks;
using System;
using Microsoft.AspNetCore.Mvc;
using Dapr;
using Dapr.Client;

namespace CheckoutService.Controllers;

[ApiController]
public sealed class CheckoutServiceController : ControllerBase
{
    //从 "order-pub-sub" 组件订阅名为 "orders" 的主题 
    [Topic("order-pub-sub", "orders")]
    [HttpPost("checkout")]
    public void GetCheckout([FromBody] int orderId)
    {
        Console.WriteLine("Subscriber received : " + orderId);
    }
}

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

dapr run --app-id checkout --app-port 6002 --dapr-http-port 3602 --dapr-grpc-port 60002 --app-protocol https dotnet run
//dependencies
import io.dapr.Topic;
import io.dapr.client.domain.CloudEvent;
import org.springframework.web.bind.annotation.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;

//code
@RestController
public class CheckoutServiceController {

    private static final Logger log = LoggerFactory.getLogger(CheckoutServiceController.class);
     //订阅一个主题
    @Topic(name = "orders", pubsubName = "order-pub-sub")
    @PostMapping(path = "/checkout")
    public Mono<Void> getCheckout(@RequestBody(required = false) CloudEvent<String> cloudEvent) {
        return Mono.fromRunnable(() -> {
            try {
                log.info("Subscriber received: " + cloudEvent.getData());
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
    }
}

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

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

#code
app = App()
logging.basicConfig(level = logging.INFO)
#订阅一个主题 
@app.subscribe(pubsub_name='order-pub-sub', topic='orders')
def mytopic(event: v1.Event) -> None:
    data = json.loads(event.Data())
    logging.info('Subscriber received: ' + str(data))

app.run(6002)

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

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

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

//code
var sub = &common.Subscription{
	PubsubName: "order-pub-sub",
	Topic:      "orders",
	Route:      "/checkout",
}

func main() {
	s := daprd.NewService(":6002")
   //订阅一个主题
	if err := s.AddTopicEventHandler(sub, eventHandler); err != nil {
		log.Fatalf("error adding topic subscription: %v", err)
	}
	if err := s.Start(); err != nil && err != http.ErrServerClosed {
		log.Fatalf("error listenning: %v", err)
	}
}

func eventHandler(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
	log.Printf("Subscriber received: %s", e.Data)
	return false, nil
}

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

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

//code
const daprHost = "127.0.0.1"; 
const serverHost = "127.0.0.1";
const serverPort = "6002"; 

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

async function start(orderId) {
    const server = new DaprServer({
        serverHost,
        serverPort,
        communicationProtocol: CommunicationProtocolEnum.HTTP,
        clientOptions: {
          daprHost,
          daprPort: process.env.DAPR_HTTP_PORT,
        },
    });
    //订阅一个主题
    await server.pubsub.subscribe("order-pub-sub", "orders", async (orderId) => {
        console.log(`Subscriber received: ${JSON.stringify(orderId)}`)
    });
    await server.start();
}

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

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

发布消息

启动一个 app-id 为 orderprocessing 的 Dapr 实例:

dapr run --app-id orderprocessing --dapr-http-port 3601

然后向 orders 主题发布一条消息:

dapr publish --publish-app-id orderprocessing --pubsub order-pub-sub --topic orders --data '{"orderId": "100"}'
curl -X POST http://localhost:3601/v1.0/publish/order-pub-sub/orders -H "Content-Type: application/json" -d '{"orderId": "100"}'
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '{"orderId": "100"}' -Uri 'http://localhost:3601/v1.0/publish/order-pub-sub/orders'

以下是利用 Dapr SDK 发布主题的代码示例。

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Dapr.Client;
using System.Threading;

const string PUBSUB_NAME = "order-pub-sub";
const string TOPIC_NAME = "orders";

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();

var app = builder.Build();
var random = new Random();

var client = app.Services.GetRequiredService<DaprClient>();

while(true) {
    await Task.Delay(TimeSpan.FromSeconds(5));
    var orderId = random.Next(1,1000);
    var source = new CancellationTokenSource();
    var cancellationToken = source.Token;
    
    //使用 Dapr SDK 发布主题
    await client.PublishEventAsync(PUBSUB_NAME, TOPIC_NAME, orderId, cancellationToken);
    Console.WriteLine("Published data: " + orderId);
}

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

dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 --app-protocol https dotnet run
//dependencies
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.Metadata;
import static java.util.Collections.singletonMap;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Random;
import java.util.concurrent.TimeUnit;

//code
@SpringBootApplication
public class OrderProcessingServiceApplication {

	private static final Logger log = LoggerFactory.getLogger(OrderProcessingServiceApplication.class);

	public static void main(String[] args) throws InterruptedException{
		String MESSAGE_TTL_IN_SECONDS = "1000";
		String TOPIC_NAME = "orders";
		String PUBSUB_NAME = "order-pub-sub";

		while(true) {
			TimeUnit.MILLISECONDS.sleep(5000);
			Random random = new Random();
			int orderId = random.nextInt(1000-1) + 1;
			DaprClient client = new DaprClientBuilder().build();
      //使用 Dapr SDK 发布主题
			client.publishEvent(
					PUBSUB_NAME,
					TOPIC_NAME,
					orderId,
					singletonMap(Metadata.TTL_IN_SECONDS, MESSAGE_TTL_IN_SECONDS)).block();
			log.info("Published data:" + orderId);
		}
	}
}

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

dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 mvn spring-boot:run
#dependencies  
import random
from time import sleep    
import requests
import logging
import json
from dapr.clients import DaprClient

#code
logging.basicConfig(level = logging.INFO)
while True:
    sleep(random.randrange(50, 5000) / 1000)
    orderId = random.randint(1, 1000)
    PUBSUB_NAME = 'order-pub-sub'
    TOPIC_NAME = 'orders'
    with DaprClient() as client:
        #使用 Dapr SDK 发布主题
        result = client.publish_event(
            pubsub_name=PUBSUB_NAME,
            topic_name=TOPIC_NAME,
            data=json.dumps(orderId),
            data_content_type='application/json',
        )
    logging.info('Published data: ' + str(orderId))

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

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

//code
var (
	PUBSUB_NAME = "order-pub-sub"
	TOPIC_NAME  = "orders"
)

func main() {
	for i := 0; i < 10; i++ {
		time.Sleep(5000)
		orderId := rand.Intn(1000-1) + 1
		client, err := dapr.NewClient()
		if err != nil {
			panic(err)
		}
		defer client.Close()
		ctx := context.Background()
    //使用 Dapr SDK 发布主题
		if err := client.PublishEvent(ctx, PUBSUB_NAME, TOPIC_NAME, []byte(strconv.Itoa(orderId))); 
		err != nil {
			panic(err)
		}

		log.Println("Published data: " + strconv.Itoa(orderId))
	}
}

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

dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 go run OrderProcessingService.go
//dependencies
import { DaprServer, DaprClient, CommunicationProtocolEnum } from '@dapr/dapr'; 

const daprHost = "127.0.0.1"; 

var main = function() {
    for(var i=0;i<10;i++) {
        sleep(5000);
        var orderId = Math.floor(Math.random() * (1000 - 1) + 1);
        start(orderId).catch((e) => {
            console.error(e);
            process.exit(1);
        });
    }
}

async function start(orderId) {
    const PUBSUB_NAME = "order-pub-sub"
    const TOPIC_NAME  = "orders"
    const client = new DaprClient({
        daprHost,
        daprPort: process.env.DAPR_HTTP_PORT, 
        communicationProtocol: CommunicationProtocolEnum.HTTP
    });
    console.log("Published data:" + orderId)
    //使用 Dapr SDK 发布主题
    await client.pubsub.publish(PUBSUB_NAME, TOPIC_NAME, orderId);
}

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

main();

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

dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 npm start

消息确认和重试

为了告诉 Dapr 消息已成功处理,需要返回 200 OK 响应。如果 Dapr 收到除 200 以外的任何返回状态码,或者您的应用程序崩溃,Dapr 将按照至少一次语义尝试重新传递消息。

演示视频

观看此演示视频以了解更多关于使用 Dapr 进行发布订阅消息传递的信息。

后续步骤

3 - 使用 CloudEvents 发布和订阅消息

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

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

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

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

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

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

Dapr 生成的 CloudEvents 示例

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

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

以下示例演示了 Dapr 为发布到 orders 主题的操作生成的 CloudEvent,其中包括:

  • W3C traceid,对每条消息唯一
  • data 和 CloudEvent 的字段,其中数据内容被序列化为 JSON
{
  "topic": "orders",
  "pubsubname": "order_pub_sub",
  "traceid": "00-113ad9c4e42b27583ae98ba698d54255-e3743e35ff56f219-01",
  "tracestate": "",
  "data": {
    "orderId": 1
  },
  "id": "5929aaac-a5e2-4ca1-859c-edfe73f11565",
  "specversion": "1.0",
  "datacontenttype": "application/json; charset=utf-8",
  "source": "checkout",
  "type": "com.dapr.event.sent",
  "time": "2020-09-23T06:23:21Z",
  "traceparent": "00-113ad9c4e42b27583ae98ba698d54255-e3743e35ff56f219-01"
}

作为 v1.0 CloudEvent 的另一个示例,以下显示数据作为 XML 内容在序列化为 JSON 的 CloudEvent 消息中:

{
  "topic": "orders",
  "pubsubname": "order_pub_sub",
  "traceid": "00-113ad9c4e42b27583ae98ba698d54255-e3743e35ff56f219-01",
  "tracestate": "",
  "data" : "<note><to></to><from>user2</from><message>Order</message></note>",
  "id" : "id-1234-5678-9101",
  "specversion" : "1.0",
  "datacontenttype" : "text/xml",
  "subject" : "Test XML Message",
  "source" : "https://example.com/message",
  "type" : "xml.message",
   "time" : "2020-09-23T06:23:21Z"
}

替换 Dapr 生成的 CloudEvents 值

Dapr 自动生成多个 CloudEvent 属性。你可以通过提供以下可选元数据键/值来替换这些生成的 CloudEvent 属性:

  • cloudevent.id:覆盖 id
  • cloudevent.source:覆盖 source
  • cloudevent.type:覆盖 type
  • cloudevent.traceid:覆盖 traceid
  • cloudevent.tracestate:覆盖 tracestate
  • cloudevent.traceparent:覆盖 traceparent

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

示例

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

with DaprClient() as client:
    order = {'orderId': i}
    # Publish an event/message using Dapr PubSub
    result = client.publish_event(
        pubsub_name='order_pub_sub',
        topic_name='orders',
        publish_metadata={'cloudevent.id': 'd99b228f-6c73-4e78-8c4d-3f80a043d317', 'cloudevent.source': 'payment'}
    )

    # or

    cloud_event = {
        'specversion': '1.0',
        'type': 'com.example.event',
        'source': 'payment',
        'id': 'd99b228f-6c73-4e78-8c4d-3f80a043d317',
        'data': {'orderId': i},
        'datacontenttype': 'application/json',
        ...
    }

    # Set the data content type to 'application/cloudevents+json'
    result = client.publish_event(
        pubsub_name='order_pub_sub',
        topic_name='orders',
        data=json.dumps(cloud_event),
        data_content_type='application/cloudevents+json',
    )
var order = new Order(i);
using var client = new DaprClientBuilder().Build();

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

// Publish an event/message using Dapr PubSub
await client.PublishEventAsync("order_pub_sub", "orders", order, metadata);
Console.WriteLine("Published data: " + order);

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

JSON 负载随后会反映新的 sourceid 值:

{
  "topic": "orders",
  "pubsubname": "order_pub_sub",
  "traceid": "00-113ad9c4e42b27583ae98ba698d54255-e3743e35ff56f219-01",
  "tracestate": "",
  "data": {
    "orderId": 1
  },
  "id": "d99b228f-6c73-4e78-8c4d-3f80a043d317",
  "specversion": "1.0",
  "datacontenttype": "application/json; charset=utf-8",
  "source": "payment",
  "type": "com.dapr.event.sent",
  "time": "2020-09-23T06:23:21Z",
  "traceparent": "00-113ad9c4e42b27583ae98ba698d54255-e3743e35ff56f219-01"
}

发布你自己的 CloudEvent

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

如果应用程序编写的 CloudEvent 不包含 CloudEvent 规范中的最小必填字段,则消息会被拒绝。如果以下字段缺失,Dapr 会将它们添加到 CloudEvent 中:

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

你可以在自定义 CloudEvent 中添加不属于官方 CloudEvent 规范的额外字段。Dapr 会原样传递这些字段。

示例

orders 主题发布一个 CloudEvent:

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

orders 主题发布一个 CloudEvent:

curl -X POST http://localhost:3601/v1.0/publish/order-pub-sub/orders -H "Content-Type: application/cloudevents+json" -d '{"specversion" : "1.0", "type" : "com.dapr.cloudevent.sent", "source" : "testcloudeventspubsub", "subject" : "Cloud Events Test", "id" : "someCloudEventId", "time" : "2021-08-02T09:00:00Z", "datacontenttype" : "application/cloudevents+json", "data" : {"orderId": "100"}}'

orders 主题发布一个 CloudEvent:

Invoke-RestMethod -Method Post -ContentType 'application/cloudevents+json' -Body '{"specversion" : "1.0", "type" : "com.dapr.cloudevent.sent", "source" : "testcloudeventspubsub", "subject" : "Cloud Events Test", "id" : "someCloudEventId", "time" : "2021-08-02T09:00:00Z", "datacontenttype" : "application/cloudevents+json", "data" : {"orderId": "100"}}' -Uri 'http://localhost:3601/v1.0/publish/order-pub-sub/orders'

发布二进制 CloudEvents

在二进制模式下,传输负载仅包含事件主体,而 CloudEvent 属性通过以 ce_ 前缀开头的传输元数据提供(HTTP 头、Kafka 头、NATS 头等)。这在你已经生成二进制模式事件或想要发送任意二进制数据而不将其包装在额外的 JSON 信封中时非常有用。

要将二进制 CloudEvent 发布到 Dapr(通过 HTTP/gRPC 发布 API 或直接发布到 Dapr 读取的代理):

  1. 将传输的原生 content-type 元数据(例如 HTTP Content-Type 头或 Kafka content-type 消息头)设置为表示二进制数据的 MIME 类型,即 application/octet-stream

  2. 将必填的 CloudEvent 属性(ce_specversionce_typece_sourcece_id)添加为传输元数据。可选属性如 ce_subjectce_timece_traceparent 也会被识别。

  3. 在消息主体中发送负载字节。

向 orders 主题发布二进制 CloudEvent:

curl -X POST http://localhost:3500/v1.0/publish/order-pub-sub/orders \
  -H "Content-Type: application/octet-stream" \
  -H "ce_specversion: 1.0" \
  -H "ce_type: com.example.order.created" \
  -H "ce_source: urn:example:/checkout" \
  -H "ce_id: 2a8bbf52-1222-4c2c-85f0-8a8875c7bc10" \
  -H "ce_subject: orders/100" \
  --data-binary $'\x01\x02\x03\x04'

事件去重

使用 Dapr 创建的 cloud events 时,信封包含一个 id 字段,应用程序可以将其用于消息去重。Dapr 不会自动处理去重。Dapr 支持使用原生支持消息去重的消息代理。

下一步

4 - 不使用 CloudEvents 发布和订阅消息

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

在向应用程序添加 Dapr 时,某些服务可能仍需要通过未封装在 CloudEvents 中的发布/订阅消息进行通信,原因可能是兼容性要求,或某些应用程序未使用 Dapr。这些被称为"原始"发布/订阅消息。Dapr 使应用程序能够发布和订阅原始事件,这些事件未包装在 CloudEvent 中,以实现兼容性并发送不可 JSON 序列化的数据。

发布原始消息

Dapr 应用程序能够向发布/订阅主题发布不包含 CloudEvent 封装的原始事件,以实现与非 Dapr 应用程序的兼容。

图表展示当订阅者不使用 Dapr 或 CloudEvent 时如何使用 Dapr 发布消息

要禁用 CloudEvent 包装,请在发布请求中设置 rawPayload 元数据为 true。这允许订阅者接收这些消息而无需解析 CloudEvent schema。

curl -X "POST" http://localhost:3500/v1.0/publish/pubsub/TOPIC_A?metadata.rawPayload=true -H "Content-Type: application/json" -d '{"order-number": "345"}'
using Dapr.Client;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers().AddDapr();

var app = builder.Build();

app.MapPost("/publish", async (DaprClient daprClient) =>
{
    var message = new Message(
        Guid.NewGuid().ToString(),
        $"Hello at {DateTime.UtcNow}",
        DateTime.UtcNow
    );

    await daprClient.PublishEventAsync(
        "pubsub",           // pubsub name
        "messages",         // topic name
        message,           // message data
        new Dictionary<string, string> 
        { 
            { "rawPayload", "true" },
            { "content-type", "application/json" }
        }
    );
    
    return Results.Ok(message);
});

app.Run();
from dapr.clients import DaprClient

with DaprClient() as d:
    req_data = {
        'order-number': '345'
    }
    # Create a typed message with content type and body
    resp = d.publish_event(
        pubsub_name='pubsub',
        topic_name='TOPIC_A',
        data=json.dumps(req_data),
        publish_metadata={'rawPayload': 'true'}
    )
    # Print the request
    print(req_data, flush=True)
<?php

require_once __DIR__.'/vendor/autoload.php';

$app = \Dapr\App::create();
$app->run(function(\DI\FactoryInterface $factory) {
    $publisher = $factory->make(\Dapr\PubSub\Publish::class, ['pubsub' => 'pubsub']);
    $publisher->topic('TOPIC_A')->publish('data', ['rawPayload' => 'true']);
});
@RestController
@PathMapping("/publish")
public class PublishController {

    @Inject
    DaprClient client;

    @PostMapping
    public void sendRawMessage() {

        Map<String, String> metadata = new HashMap<>();
        metatada.put("content-type", "application/json");
        metadata.put("rawPayload", "true");

        Message message = new Message(UUID.random().toString(), "Hello from Dapr");

        client.publishEvent(
            "pubsub", // pubsub name
            "messages", // topic name
            message, // message data
            metadata) // metadata
                .block(); // wait for completion
    }
}

订阅原始消息

Dapr 应用程序可以订阅来自发布/订阅主题的原始消息,即使这些消息不是作为 CloudEvents 发布的。然而,订阅的 Dapr 进程仍会在将这些原始消息传递给订阅应用程序之前将其包装在 CloudEvent 中。

图表展示当发布者不使用 Dapr 或 CloudEvent 时如何使用 Dapr 订阅消息

以编程方式订阅原始事件

以编程方式订阅时,添加 rawPayload 的额外元数据条目以允许订阅者接收未由 CloudEvent 包装的消息。对于 .NET,此元数据条目称为 isRawPayload

使用原始负载时,消息始终使用 base64 编码,内容类型为 application/octet-stream

using System.Text.Json;
using System.Text.Json.Serialization;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/dapr/subscribe", () =>
{
    var subscriptions = new[]
    {
        new
        {
            pubsubname = "pubsub",
            topic = "messages",
            route = "/messages",
            metadata = new Dictionary<string, string>
            {
                { "rawPayload", "true" },
                { "content-type", "application/json" }
            }
        }
    };
    return Results.Ok(subscriptions);
});

app.MapPost("/messages", async (HttpContext context) =>
{
    using var reader = new StreamReader(context.Request.Body);
    var json = await reader.ReadToEndAsync();

    Console.WriteLine($"Raw message received: {json}");

    return Results.Ok();
});

app.Run();
import flask
from flask import request, jsonify
from flask_cors import CORS
import json
import sys

app = flask.Flask(__name__)
CORS(app)

@app.route('/dapr/subscribe', methods=['GET'])
def subscribe():
    subscriptions = [{'pubsubname': 'pubsub',
                      'topic': 'deathStarStatus',
                      'route': 'dsstatus',
                      'metadata': {
                          'rawPayload': 'true',
                      } }]
    return jsonify(subscriptions)

@app.route('/dsstatus', methods=['POST'])
def ds_subscriber():
    print(request.json, flush=True)
    return json.dumps({'success':True}), 200, {'ContentType':'application/json'}

app.run()
<?php

require_once __DIR__.'/vendor/autoload.php';

$app = \Dapr\App::create(configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions(['dapr.subscriptions' => [
    new \Dapr\PubSub\Subscription(pubsubname: 'pubsub', topic: 'deathStarStatus', route: '/dsstatus', metadata: [ 'rawPayload' => 'true'] ),
]]));

$app->post('/dsstatus', function(
    #[\Dapr\Attributes\FromBody]
    \Dapr\PubSub\CloudEvent $cloudEvent,
    \Psr\Log\LoggerInterface $logger
    ) {
        $logger->alert('Received event: {event}', ['event' => $cloudEvent]);
        return ['status' => 'SUCCESS'];
    }
);

$app->start();
@RequestMapping("/consumer")
@RestController
public class MessageConsumerController {

    @PostMapping
    @ResponseStatus(HttpStatus.OK)
    @Topic(pubsubName = "pubsub", name = "messages", metadata = "{\"rawPayload\":\"true\", \"content-type\": \"application/json\"}")
    public void consume(@RequestBody Message message) {
        System.out.println("Message received: " + message);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.OK)
    @Topic(pubsubName = "pubsub", name = "another-topic", metadata = """
            {"rawPayload": "true", "content-type": "application/json"}
            """) // Using Java 15 text block
    public void consumeAnother(@RequestBody Message message) {
        System.out.println("Message received: " + message);
    }
}

以声明方式订阅原始事件

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

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

后续步骤

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

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

发布订阅路由是基于内容的路由的一种实现,这是一种使用 DSL 而非命令式应用程序代码的消息传递模式。通过发布订阅路由,你可以使用表达式根据内容将 CloudEvents 路由到应用程序中的不同 URI/路径和事件处理程序。如果没有路由匹配,则使用可选的默认路由。当应用程序扩展以支持多个事件版本或特殊情况时,这非常有用。

虽然路由可以通过代码实现,但将路由规则保持在应用程序外部可以提高可移植性。

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

声明式订阅

对于声明式订阅,使用 dapr.io/v2alpha1 作为 apiVersion。以下是使用路由的 subscriptions.yaml 示例:

apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
  name: myevent-subscription
spec:
  pubsubname: pubsub
  topic: inventory
  routes:
    rules:
      - match: event.type == "widget"
        path: /widgets
      - match: event.type == "gadget"
        path: /gadgets
    default: /products
scopes:
  - app1
  - app2

编程式订阅

在编程式方法中,返回 routes 结构而不是 route。JSON 结构与声明式 YAML 匹配:

import flask
from flask import request, jsonify
from flask_cors import CORS
import json
import sys

app = flask.Flask(__name__)
CORS(app)

@app.route('/dapr/subscribe', methods=['GET'])
def subscribe():
    subscriptions = [
      {
        'pubsubname': 'pubsub',
        'topic': 'inventory',
        'routes': {
          'rules': [
            {
              'match': 'event.type == "widget"',
              'path': '/widgets'
            },
            {
              'match': 'event.type == "gadget"',
              'path': '/gadgets'
            },
          ],
          'default': '/products'
        }
      }]
    return jsonify(subscriptions)

@app.route('/products', methods=['POST'])
def ds_subscriber():
    print(request.json, flush=True)
    return json.dumps({'success':True}), 200, {'ContentType':'application/json'}
app.run()
const express = require('express')
const bodyParser = require('body-parser')
const app = express()
app.use(bodyParser.json({ type: 'application/*+json' }));

const port = 3000

app.get('/dapr/subscribe', (req, res) => {
  res.json([
    {
      pubsubname: "pubsub",
      topic: "inventory",
      routes: {
        rules: [
          {
            match: 'event.type == "widget"',
            path: '/widgets'
          },
          {
            match: 'event.type == "gadget"',
            path: '/gadgets'
          },
        ],
        default: '/products'
      }
    }
  ]);
})

app.post('/products', (req, res) => {
  console.log(req.body);
  res.sendStatus(200);
});

app.listen(port, () => console.log(`consumer app listening on port ${port}!`))
        [Topic("pubsub", "inventory", "event.type ==\"widget\"", 1)]
        [HttpPost("widgets")]
        public async Task<ActionResult<Stock>> HandleWidget(Widget widget, [FromServices] DaprClient daprClient)
        {
            // Logic
            return stock;
        }

        [Topic("pubsub", "inventory", "event.type ==\"gadget\"", 2)]
        [HttpPost("gadgets")]
        public async Task<ActionResult<Stock>> HandleGadget(Gadget gadget, [FromServices] DaprClient daprClient)
        {
            // Logic
            return stock;
        }

        [Topic("pubsub", "inventory")]
        [HttpPost("products")]
        public async Task<ActionResult<Stock>> HandleProduct(Product product, [FromServices] DaprClient daprClient)
        {
            // Logic
            return stock;
        }
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"

	"github.com/gorilla/mux"
)

const appPort = 3000

type subscription struct {
	PubsubName string            `json:"pubsubname"`
	Topic      string            `json:"topic"`
	Metadata   map[string]string `json:"metadata,omitempty"`
	Routes     routes            `json:"routes"`
}

type routes struct {
	Rules   []rule `json:"rules,omitempty"`
	Default string `json:"default,omitempty"`
}

type rule struct {
	Match string `json:"match"`
	Path  string `json:"path"`
}

// This handles /dapr/subscribe
func configureSubscribeHandler(w http.ResponseWriter, _ *http.Request) {
	t := []subscription{
		{
			PubsubName: "pubsub",
			Topic:      "inventory",
			Routes: routes{
				Rules: []rule{
					{
						Match: `event.type == "widget"`,
						Path:  "/widgets",
					},
					{
						Match: `event.type == "gadget"`,
						Path:  "/gadgets",
					},
				},
				Default: "/products",
			},
		},
	}

	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(t)
}

func main() {
	router := mux.NewRouter().StrictSlash(true)
	router.HandleFunc("/dapr/subscribe", configureSubscribeHandler).Methods("GET")
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", appPort), router))
}
<?php

require_once __DIR__.'/vendor/autoload.php';

$app = \Dapr\App::create(configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions(['dapr.subscriptions' => [
    new \Dapr\PubSub\Subscription(pubsubname: 'pubsub', topic: 'inventory', routes: (
      rules: => [
        ('match': 'event.type == "widget"', path: '/widgets'),
        ('match': 'event.type == "gadget"', path: '/gadgets'),
      ]
      default: '/products')),
]]));
$app->post('/products', function(
    #[\Dapr\Attributes\FromBody]
    \Dapr\PubSub\CloudEvent $cloudEvent,
    \Psr\Log\LoggerInterface $logger
    ) {
        $logger->alert('Received event: {event}', ['event' => $cloudEvent]);
        return ['status' => 'SUCCESS'];
    }
);
$app->start();

通用表达式语言 (CEL)

在这些示例中,根据 event.type 的不同,应用程序将在以下路径被调用:

  • /widgets
  • /gadgets
  • /products

表达式以通用表达式语言 (CEL)编写,其中 event 表示云事件。可以引用 CloudEvents 核心规范中的任何属性。

表达式示例

匹配"重要"消息:

has(event.data.important) && event.data.important == true

匹配大于 $10,000 的存款:

event.type == "deposit" && int(event.data.amount) > 10000

匹配消息的多个版本:

event.type == "mymessage.v1"
event.type == "mymessage.v2"

CloudEvent 属性

作为参考,以下属性来自 CloudEvents 规范。

事件数据

data

根据术语 data 的定义,CloudEvents _可能_包含有关发生的领域特定信息。如果存在,此信息将封装在 data 中。

  • 描述: 事件负载。此规范不对信息类型施加限制。它被编码为由 datacontenttype 属性指定的媒体格式(例如 application/json),并在存在相应属性时遵循 dataschema 格式。
  • 约束:
    • 可选 (OPTIONAL)

必需属性

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

id

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

source

  • 类型: URI-reference

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

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

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

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

    应用程序可以:

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

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

  • 约束:

    • 必需 (REQUIRED)
    • 必须是非空 URI 引用
    • 建议使用绝对 URI
  • 示例:

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

specversion

  • 类型: String

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

    目前,此属性仅包含"主"和"次"版本号。这允许对规范进行补丁更改而不更改序列化中此属性的值。

    注意:对于"候选版本"版本,可能会使用后缀进行测试目的。

  • 约束:

    • 必需 (REQUIRED)
    • 必须是非空字符串

type

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

可选属性

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

datacontenttype

  • 类型: String,根据 RFC 2046

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

    例如,使用 JSON 信封格式渲染的事件可能在 data 中承载 XML 负载。通过将此属性设置为 "application/xml" 来通知消费者。

    对于不同 datacontenttype 值如何渲染 data 内容的规则在事件格式规范中定义。例如,JSON 事件格式在第 3.1 节中定义了关系。

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

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

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

  • 约束:

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

dataschema

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

subject

  • 类型: String

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

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

  • 约束:

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

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

    新创建的 blob 的名称承载在 subject 中:

time

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

社区电话演示

观看此视频,了解如何使用发布订阅进行消息路由:

后续步骤

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

了解允许您订阅消息主题的订阅类型。

发布订阅 API 订阅类型

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

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

下面的示例演示了 checkout 应用程序和 orderprocessing 应用程序之间通过 orders 主题进行的发布订阅消息传递。这些示例演示了同一个 Dapr 发布订阅组件首先以声明方式使用,然后以编程方式使用。

声明式订阅

您可以使用外部组件文件以声明方式订阅主题。此示例使用名为 subscription.yaml 的 YAML 组件文件:

apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
  name: order
spec:
  topic: orders
  routes:
    default: /orders
  pubsubname: pubsub
scopes:
- orderprocessing

这里名为 order 的订阅:

  • 使用名为 pubsub 的发布订阅组件订阅名为 orders 的主题。
  • 设置 route 字段以将所有主题消息发送到应用程序中的 /orders 端点。
  • 设置 scopes 字段以将此订阅的范围限制为仅由 ID 为 orderprocessing 的应用程序访问。

运行 Dapr 时,设置 YAML 组件文件路径以将 Dapr 指向该组件。

dapr run --app-id myapp --resources-path ./myComponents -- dotnet run
dapr run --app-id myapp --resources-path ./myComponents -- mvn spring-boot:run
dapr run --app-id myapp --resources-path ./myComponents -- python3 app.py
dapr run --app-id myapp --resources-path ./myComponents -- npm start
dapr run --app-id myapp --resources-path ./myComponents -- go run app.go

在 Kubernetes 中,将组件应用到集群:

kubectl apply -f subscription.yaml

在应用程序代码中,订阅 Dapr 发布订阅组件中指定的主题。

 //Subscribe to a topic 
[HttpPost("orders")]
public void getCheckout([FromBody] int orderId)
{
    Console.WriteLine("Subscriber received : " + orderId);
}
import io.dapr.client.domain.CloudEvent;

 //Subscribe to a topic
@PostMapping(path = "/orders")
public Mono<Void> getCheckout(@RequestBody(required = false) CloudEvent<String> cloudEvent) {
    return Mono.fromRunnable(() -> {
        try {
            log.info("Subscriber received: " + cloudEvent.getData());
        } 
    });
}
from cloudevents.sdk.event import v1

#Subscribe to a topic 
@app.route('/orders', methods=['POST'])
def checkout(event: v1.Event) -> None:
    data = json.loads(event.Data())
    logging.info('Subscriber received: ' + str(data))
const express = require('express')
const bodyParser = require('body-parser')
const app = express()
app.use(bodyParser.json({ type: 'application/*+json' }));

// listen to the declarative route
app.post('/orders', (req, res) => {
  console.log(req.body);
  res.sendStatus(200);
});
//Subscribe to a topic
var sub = &common.Subscription{
	PubsubName: "pubsub",
	Topic:      "orders",
	Route:      "/orders",
}

func eventHandler(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
	log.Printf("Subscriber received: %s", e.Data)
	return false, nil
}

/orders 端点与订阅中定义的 route 匹配,这是 Dapr 将所有主题消息发送到的地方。

流式订阅

流式订阅是在应用程序代码中定义的订阅,可以在运行时动态停止和启动。 消息由应用程序从 Dapr 拉取。这意味着不需要端点来订阅主题,并且可以在边车上根本没有配置任何应用程序的情况下进行订阅。 可以同时订阅任意数量的发布订阅和主题。 当消息发送到给定的消息处理程序代码时,没有路由或批量订阅的概念。

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

您可以使用 DaprPublishSubscribeClient 上的 SubscribeAsync 方法来配置用于从流中拉取消息的消息处理程序。

using System.Text;
using Dapr.Messaging.PublishSubscribe;
using Dapr.Messaging.PublishSubscribe.Extensions;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprPubSubClient();
var app = builder.Build();

var messagingClient = app.Services.GetRequiredService<DaprPublishSubscribeClient>();

//Create a dynamic streaming subscription and subscribe with a timeout of 30 seconds and 10 seconds for message handling
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var subscription = await messagingClient.SubscribeAsync("pubsub", "myTopic",
    new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(10), TopicResponseAction.Retry)),
    HandleMessageAsync, cancellationTokenSource.Token);

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

//When you're done with the subscription, simply dispose of it
await subscription.DisposeAsync();
return;

//Process each message returned from the subscription
Task<TopicResponseAction> HandleMessageAsync(TopicMessage message, CancellationToken cancellationToken = default)
{
    try
    {
        //Do something with the message
        Console.WriteLine(Encoding.UTF8.GetString(message.Data.Span));
        return Task.FromResult(TopicResponseAction.Success);
    }
    catch
    {
        return Task.FromResult(TopicResponseAction.Retry);
    }
}

了解有关使用 .NET SDK 客户端进行流式订阅的更多信息。

您可以使用 subscribe 方法,该方法返回一个 Subscription 对象,并允许您通过调用 next_message 方法从流中拉取消息。这在等待消息时运行并可能阻塞主线程。

import time

from dapr.clients import DaprClient
from dapr.clients.grpc.subscription import StreamInactiveError

counter = 0


def process_message(message):
    global counter
    counter += 1
    # Process the message here
    print(f'Processing message: {message.data()} from {message.topic()}...')
    return 'success'


def main():
    with DaprClient() as client:
        global counter

        subscription = client.subscribe(
            pubsub_name='pubsub', topic='orders', dead_letter_topic='orders_dead'
        )

        try:
            while counter < 5:
                try:
                    message = subscription.next_message()

                except StreamInactiveError as e:
                    print('Stream is inactive. Retrying...')
                    time.sleep(1)
                    continue
                if message is None:
                    print('No message received within timeout period.')
                    continue

                # Process the message
                response_status = process_message(message)

                if response_status == 'success':
                    subscription.respond_success(message)
                elif response_status == 'retry':
                    subscription.respond_retry(message)
                elif response_status == 'drop':
                    subscription.respond_drop(message)

        finally:
            print("Closing subscription...")
            subscription.close()


if __name__ == '__main__':
    main()

您也可以使用 subscribe_with_handler 方法,该方法接受一个回调函数,该函数对从流接收到的每条消息执行。这在单独的线程中运行,因此不会阻塞主线程。

import time

from dapr.clients import DaprClient
from dapr.clients.grpc._response import TopicEventResponse

counter = 0


def process_message(message):
    # Process the message here
    global counter
    counter += 1
    print(f'Processing message: {message.data()} from {message.topic()}...')
    return TopicEventResponse('success')


def main():
    with (DaprClient() as client):
        # This will start a new thread that will listen for messages
        # and process them in the `process_message` function
        close_fn = client.subscribe_with_handler(
            pubsub_name='pubsub', topic='orders', handler_fn=process_message,
            dead_letter_topic='orders_dead'
        )

        while counter < 5:
            time.sleep(1)

        print("Closing subscription...")
        close_fn()


if __name__ == '__main__':
    main()

了解有关使用 Python SDK 客户端进行流式订阅的更多信息。

package main

import (
	"context"
	"log"

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

func main() {
	cl, err := client.NewClient()
	if err != nil {
		log.Fatal(err)
	}

	sub, err := cl.Subscribe(context.Background(), client.SubscriptionOptions{
		PubsubName: "pubsub",
		Topic:      "orders",
	})
	if err != nil {
		panic(err)
	}
	// Close must always be called.
	defer sub.Close()

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

		// Process the event

		// We _MUST_ always signal the result of processing the message, else the
		// message will not be considered as processed and will be redelivered or
		// dead lettered.
		// msg.Retry()
		// msg.Drop()
		if err := msg.Success(); err != nil {
			panic(err)
		}
	}
}

package main

import (
	"context"
	"log"

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

func main() {
	cl, err := client.NewClient()
	if err != nil {
		log.Fatal(err)
	}

	stop, err := cl.SubscribeWithHandler(context.Background(),
		client.SubscriptionOptions{
			PubsubName: "pubsub",
			Topic:      "orders",
		},
		eventHandler,
	)
	if err != nil {
		panic(err)
	}

	// Stop must always be called.
	defer stop()

	<-make(chan struct{})
}

func eventHandler(e *common.TopicEvent) common.SubscriptionResponseStatus {
	// Process message here
    // common.SubscriptionResponseStatusRetry
    // common.SubscriptionResponseStatusDrop
			common.SubscriptionResponseStatusDrop, status);
	}

	return common.SubscriptionResponseStatusSuccess
}

演示

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

编程式订阅

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

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

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

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

// Dapr subscription in [Topic] routes orders topic to this route
app.MapPost("/orders", [Topic("pubsub", "orders")] (Order order) => {
    Console.WriteLine("Subscriber received : " + order);
    return Results.Ok(order);
});

上面定义的处理程序还需要映射到 dapr/subscribe 端点。这是在定义端点时的应用程序启动代码中完成的。

app.UseEndpoints(endpoints =>
{
    endpoints.MapSubscribeHandler();
});
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

@Topic(name = "orders", pubsubName = "pubsub")
@PostMapping(path = "/orders")
public Mono<Void> handleMessage(@RequestBody(required = false) CloudEvent<String> cloudEvent) {
  return Mono.fromRunnable(() -> {
    try {
      System.out.println("Subscriber received: " + cloudEvent.getData());
      System.out.println("Subscriber received: " + OBJECT_MAPPER.writeValueAsString(cloudEvent));
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  });
}
@app.route('/dapr/subscribe', methods=['GET'])
def subscribe():
    subscriptions = [
      {
        'pubsubname': 'pubsub',
        'topic': 'orders',
        'routes': {
          'rules': [
            {
              'match': 'event.type == "order"',
              'path': '/orders'
            },
          ],
          'default': '/orders'
        }
      }]
    return jsonify(subscriptions)

@app.route('/orders', methods=['POST'])
def ds_subscriber():
    print(request.json, flush=True)
    return json.dumps({'success':True}), 200, {'ContentType':'application/json'}
app.run()
const express = require('express')
const bodyParser = require('body-parser')
const app = express()
app.use(bodyParser.json({ type: 'application/*+json' }));

const port = 3000

app.get('/dapr/subscribe', (req, res) => {
  res.json([
    {
      pubsubname: "pubsub",
      topic: "orders",
      routes: {
        rules: [
          {
            match: 'event.type == "order"',
            path: '/orders'
          },
        ],
        default: '/products'
      }
    }
  ]);
})

app.post('/orders', (req, res) => {
  console.log(req.body);
  res.sendStatus(200);
});

app.listen(port, () => console.log(`consumer app listening on port ${port}!`))
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"

	"github.com/gorilla/mux"
)

const appPort = 3000

type subscription struct {
	PubsubName string            `json:"pubsubname"`
	Topic      string            `json:"topic"`
	Metadata   map[string]string `json:"metadata,omitempty"`
	Routes     routes            `json:"routes"`
}

type routes struct {
	Rules   []rule `json:"rules,omitempty"`
	Default string `json:"default,omitempty"`
}

type rule struct {
	Match string `json:"match"`
	Path  string `json:"path"`
}

// This handles /dapr/subscribe
func configureSubscribeHandler(w http.ResponseWriter, _ *http.Request) {
	t := []subscription{
		{
			PubsubName: "pubsub",
			Topic:      "orders",
			Routes: routes{
				Rules: []rule{
					{
						Match: `event.type == "order"`,
						Path:  "/orders",
					},
				},
				Default: "/orders",
			},
		},
	}

	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(t)
}

func main() {
	router := mux.NewRouter().StrictSlash(true)
	router.HandleFunc("/dapr/subscribe", configureSubscribeHandler).Methods("GET")
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", appPort), router))
}

下一步

7 - 死信主题

使用订阅死信主题转发无法投递的消息

简介

应用程序有时可能因各种原因无法处理消息。例如,在检索处理消息所需的数据时可能存在瞬时问题,或者应用程序业务逻辑失败并返回错误。死信主题用于转发无法投递到订阅应用程序的消息。这减轻了应用程序的压力,使其无需处理这些失败的消息,允许开发者编写代码从死信主题读取消息,然后修复消息并重新发送,或完全放弃该消息。

死信主题通常与重试弹性策略以及死信订阅一起使用,死信订阅负责处理从死信主题转发来的消息所需的逻辑。

当设置了死信主题时,任何未能投递到已配置主题的应用程序的消息都会被放到死信主题上,以便转发给处理这些消息的订阅。这可能是同一个应用程序,也可能是完全不同的应用程序。

Dapr 为其所有发布/订阅组件启用死信主题,即使底层系统本身不支持此功能。例如,AWS SNS 组件 具有死信队列,RabbitMQ 具有死信主题。你需要确保正确配置此类组件。

下图是死信主题如何工作的示例。首先,从 orders 主题上的发布者发送一条消息。Dapr 代表订阅应用程序接收该消息,但是 orders 主题消息未能投递到应用程序上的 /checkout 端点,即使经过重试也是如此。由于投递失败,该消息被转发到 poisonMessages 主题,该主题将其传递到 /failedMessages 端点进行处理,在本例中是在同一应用程序上。failedMessages 处理代码可以丢弃消息或重新发送新消息。

使用声明式订阅配置死信主题

以下 YAML 显示如何为从 orders 主题消费的消息配置名为 poisonMessages 的死信主题的订阅。此订阅的作用域限定为具有 checkout ID 的应用程序。

apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
  name: order
spec:
  topic: orders
  routes: 
    default: /checkout
  pubsubname: pubsub
  deadLetterTopic: poisonMessages
scopes:
- checkout

使用流式订阅配置死信主题

	var deadLetterTopic = "poisonMessages"
	sub, err := cl.Subscribe(context.Background(), client.SubscriptionOptions{
		PubsubName:      "pubsub",
		Topic:           "orders",
		DeadLetterTopic: &deadLetterTopic,
	})

使用编程式订阅配置死信主题

/subscribe 端点返回的 JSON 显示如何为从 orders 主题消费的消息配置名为 poisonMessages 的死信主题。

app.get('/dapr/subscribe', (_req, res) => {
    res.json([
        {
            pubsubname: "pubsub",
            topic: "orders",
            route: "/checkout",
            deadLetterTopic: "poisonMessages"
        }
    ]);
});

重试和死信主题

默认情况下,当设置了死信主题时,任何失败的消息都会立即进入死信主题。因此,建议在订阅中使用死信主题时始终设置重试策略。要在将消息发送到死信主题之前启用重试,请将重试弹性策略 应用于发布/订阅组件。

此示例显示如何为 pubsub 发布/订阅组件设置名为 pubsubRetry 的恒定重试策略,最大投递尝试次数为 10 次,每 5 秒应用一次。

apiVersion: dapr.io/v1alpha1
kind: Resiliency
metadata:
  name: myresiliency
spec:
  policies:
    retries:
      pubsubRetry:
        policy: constant
        duration: 5s
        maxRetries: 10
  targets:
    components:
      pubsub:
        inbound:
          retry: pubsubRetry

配置用于处理死信主题的订阅

请记住,现在要配置一个订阅来处理死信主题。例如,你可以创建另一个声明式订阅,以在同一或不同的应用程序上接收这些消息。下面的示例显示 checkout 应用程序订阅 poisonMessages 主题,并通过另一个订阅将这些消息发送到 /failedmessages 端点进行处理。

apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
  name: deadlettertopics
spec:
  topic: poisonMessages
  routes:
    rules:
      - match:
        path: /failedMessages
  pubsubname: pubsub
scopes:
- checkout

演示

观看此视频以了解死信主题的概述

后续步骤

8 - 如何操作:设置发布订阅命名空间消费者组

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

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

Dapr 通过其发布订阅命名空间消费者组构造解决了大规模的多租户问题。

没有命名空间消费者组

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

Diagram showing basic pubsub process.

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

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

Diagram showing complications around multi-tenancy without Dapr namespace consumer groups.

为了避免这种情况,你需要根据运行的命名空间,在代码中添加一些内容来更改 app-id。这种变通方法很繁琐,是一个显著的痛点。

使用命名空间消费者组

Dapr 不仅允许你通过 UUID 和 Pod 名称的 consumerID 更改消费者组的行为,还提供了一个存在于发布/订阅组件元数据中的命名空间构造。例如,使用 Redis 作为消息代理:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: pubsub
spec:
  type: pubsub.redis
  version: v1
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    value: ""
  - name: consumerID
    value: "{namespace}"

通过将 consumerID 配置为 {namespace} 值,你将能够从不同的命名空间使用相同的 app-id 和相同的主题。

Diagram showing how namespace consumer groups help with multi-tenancy.

在上图中,你有两个命名空间,每个命名空间都有具有相同 app-id 的应用程序,发布和订阅到同一个中心化消息代理 orders。然而这一次,Dapr 创建了以其运行的命名空间为前缀的消费者组名称。

无需更改代码/app-id,命名空间消费者组允许你:

  • 添加更多命名空间
  • 保持相同的主题
  • 在命名空间之间保持相同的 app-id
  • 保持整个部署管道完整

只需在组件元数据中包含 "{namespace}" 消费者组构造即可。你不需要在元数据中编码命名空间。Dapr 理解它运行的命名空间,并为你完成命名空间值,就像运行时注入的动态元数据值一样。

演示

观看此视频了解发布订阅多租户的概述

后续步骤

9 - 如何:通过 StatefulSet 水平扩展订阅者

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

与 Pod 为临时性的 Deployments 不同,StatefulSets 允许在 Kubernetes 上部署有状态应用程序,同时为每个 Pod 保持一个稳定的标识。

以下是使用 Dapr 的 StatefulSet 示例:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: python-subscriber
spec:
  selector:
    matchLabels:
      app: python-subscriber  # has to match .spec.template.metadata.labels
  serviceName: "python-subscriber"
  replicas: 3
  template:
    metadata:
      labels:
        app: python-subscriber # has to match .spec.selector.matchLabels
      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "python-subscriber"
        dapr.io/app-port: "5001"
    spec:
      containers:
      - name: python-subscriber
        image: ghcr.io/dapr/samples/pubsub-python-subscriber:latest
        ports:
        - containerPort: 5001
        imagePullPolicy: Always

通过 Dapr 订阅发布订阅主题时,应用程序可以定义 consumerID,该 ID 决定了订阅者在队列或主题中的位置。借助 StatefulSets 的 Pod 稳定标识特性,每个 Pod 可以拥有唯一的 consumerID,从而实现订阅者应用程序的每个水平扩展。Dapr 会跟踪每个 Pod 的名称,该名称可在声明组件时使用 {podName} 标记。

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

  • 广播:发布到主题的每条消息都会被所有订阅者消费。
  • 共享:一条消息由任意订阅者消费(但不是全部)。

Kafka 通过 consumerID 隔离每个订阅者,并在主题中维护各自的位置。当实例重启时,它会重用相同的 consumerID 并从其最后已知位置继续,不会跳过消息。下面的组件演示了 Kafka 组件如何被多个 Pod 使用:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: pubsub
spec:
  type: pubsub.kafka
  version: v1
  metadata:
  - name: brokers
    value: my-cluster-kafka-bootstrap.kafka.svc.cluster.local:9092
  - name: consumerID
    value: "{podName}"
  - name: authRequired
    value: "false"

MQTT3 协议具有共享主题功能,允许多个订阅者"竞争"主题中的消息,即一条消息仅由其中一个处理。例如:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: mqtt-pubsub
spec:
  type: pubsub.mqtt3
  version: v1
  metadata:
    - name: consumerID
      value: "{podName}"
    - name: cleanSession
      value: "true"
    - name: url
      value: "tcp://admin:public@localhost:1883"
    - name: qos
      value: 1
    - name: retain
      value: "false"

下一步

10 - 限制发布订阅主题访问

使用范围将发布订阅主题限定到特定应用程序

简介

命名空间或组件范围 可用于将组件访问限制到特定应用程序。添加到组件的这些应用程序范围仅允许具有特定 ID 的应用程序使用该组件。

除了此常规组件范围外,发布订阅组件还可以限制以下内容:

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

这称为发布订阅主题范围

发布订阅范围是为每个发布订阅组件定义的。您可能有一个名为 pubsub 的发布订阅组件,它具有一组范围,而另一个 pubsub2 具有不同的范围。

要使用此主题范围,可以为发布订阅组件设置三个元数据属性:

  • spec.metadata.publishingScopes
    • 以分号分隔的应用程序列表和以逗号分隔的主题列表,允许该应用程序向该主题列表发布
    • 如果在 publishingScopes 中未指定任何内容(默认行为),则所有应用程序都可以向所有主题发布
    • 要拒绝应用程序向任何主题发布的能力,请将主题列表留空(app1=;app2=topic2
    • 例如,app1=topic1;app2=topic2,topic3;app3= 将允许 app1 向 topic1 发布,而不允许向其他主题发布;app2 仅向 topic2 和 topic3 发布;app3 不向任何主题发布
  • spec.metadata.subscriptionScopes
    • 以分号分隔的应用程序列表和以逗号分隔的主题列表,允许该应用程序订阅该主题列表
    • 如果在 subscriptionScopes 中未指定任何内容(默认行为),则所有应用程序都可以订阅所有主题
    • 例如,app1=topic1;app2=topic2,topic3 将允许 app1 仅订阅 topic1,app2 订阅 topic2 和 topic3
  • spec.metadata.allowedTopics
    • 所有应用程序的允许主题的逗号分隔列表。
    • 如果未设置 allowedTopics(默认行为),则所有主题均有效。如果存在 subscriptionScopespublishingScopes,它们仍然生效。
    • publishingScopessubscriptionScopes 可与 allowedTopics 结合使用以添加精细限制
  • spec.metadata.protectedTopics
    • 所有应用程序的受保护主题的逗号分隔列表。
    • 如果某个主题被标记为受保护,则应用程序必须通过 publishingScopessubscriptionScopes 显式授予发布或订阅权限才能发布/订阅该主题。

这些元数据属性可用于所有发布订阅组件。以下示例使用 Redis 作为发布订阅组件。

示例 1:限制主题访问

如果您有包含敏感信息的主题,并且只允许您的应用程序的一个子集发布或订阅这些主题,那么限制哪些应用程序可以发布/订阅主题会很有用。

它还可以用于所有主题,以始终拥有哪些应用程序作为发布者/订阅者使用哪些主题的"真实来源"。

以下是三个应用程序和三个主题的示例:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: pubsub
spec:
  type: pubsub.redis
  version: v1
  metadata:
  - name: redisHost
    value: "localhost:6379"
  - name: redisPassword
    value: ""
  - name: publishingScopes
    value: "app1=topic1;app2=topic2,topic3;app3="
  - name: subscriptionScopes
    value: "app2=;app3=topic1"

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

topic1topic2topic3
app1
app2
app3

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

topic1topic2topic3
app1
app2
app3

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

示例 2:限制允许的主题

如果 Dapr 应用程序向主题发送消息,则会创建该主题。在某些情况下,应该控制此主题创建。例如:

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

在这些情况下,可以使用 allowedTopics

以下是三个允许主题的示例:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: pubsub
spec:
  type: pubsub.redis
  version: v1
  metadata:
  - name: redisHost
    value: "localhost:6379"
  - name: redisPassword
    value: ""
  - name: allowedTopics
    value: "topic1,topic2,topic3"

所有应用程序都可以使用这些主题,但只能使用这些主题,不允许使用其他主题。

示例 3:结合 allowedTopics 和范围

有时您希望结合这两种范围,从而只有一组固定的允许主题并指定对特定应用程序的范围。

以下是三个应用程序和两个主题的示例:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: pubsub
spec:
  type: pubsub.redis
  version: v1
  metadata:
  - name: redisHost
    value: "localhost:6379"
  - name: redisPassword
    value: ""
  - name: allowedTopics
    value: "A,B"
  - name: publishingScopes
    value: "app1=A"
  - name: subscriptionScopes
    value: "app1=;app2=A"

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

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

ABC
app1
app2
app3

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

ABC
app1
app2
app3

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

如果您的主题涉及敏感数据,则必须在 publishingScopessubscriptionScopes 中显式列出每个新应用程序,以确保它无法从该主题读取或向其写入。或者,您可以将主题指定为"受保护"(使用 protectedTopics)并仅授予真正需要它的特定应用程序的访问权限。

以下是三个应用程序和三个主题的示例,其中两个是受保护的:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: pubsub
spec:
  type: pubsub.redis
  version: v1
  metadata:
  - name: redisHost
    value: "localhost:6379"
  - name: redisPassword
    value: ""
  - name: protectedTopics
    value: "A,B"
  - name: publishingScopes
    value: "app1=A,B;app2=B"
  - name: subscriptionScopes
    value: "app1=A,B;app2=B"

在上面的示例中,主题 A 和 B 被标记为受保护。因此,即使 app3 未在 publishingScopessubscriptionScopes 下列出,它也无法与这些主题交互。

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

ABC
app1
app2
app3

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

ABC
app1
app2
app3

演示

后续步骤

11 - 消息生存时间 (TTL)

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

简介

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

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

对于某些组件(如 Kafka),可以通过 retention.ms 在主题级别配置生存时间,详见 文档。通过 Dapr 的消息 TTL,使用 Kafka 的应用程序现在可以除了按主题设置外,还可以为每条消息设置生存时间。

原生消息 TTL 支持

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

支持的组件

Azure Service Bus

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

非 Dapr 订阅者

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

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

示例

消息 TTL 可以作为发布请求的一部分在元数据中设置:

[%] (tab "Python SDK" %%) ```python from dapr.clients import DaprClient with DaprClient() as d: req_data = { 'order-number': '345' } # Create a typed message with content type and body resp = d.publish_event( pubsub_name='pubsub', topic='TOPIC_A', data=json.dumps(req_data), publish_metadata={'ttlInSeconds': '120'} ) # Print the request print(req_data, flush=True) ```

curl -X "POST" http://localhost:3500/v1.0/publish/pubsub/TOPIC_A?metadata.ttlInSeconds=120 -H "Content-Type: application/json" -d '{"order-number": "345"}'
[%] (tab “PHP SDK” %%)

<?php

require_once __DIR__.'/vendor/autoload.php';

$app = \Dapr\App::create();
$app->run(function(\DI\FactoryInterface $factory) {
    $publisher = $factory->make(\Dapr\PubSub\Publish::class, ['pubsub' => 'pubsub']);
    $publisher->topic('TOPIC_A')->publish('data', ['ttlInSeconds' => '120']);
});

[%] (/tab “PHP SDK” %%) [%] (/tabpane >)

有关发布订阅 API 的参考,请参阅本指南

下一步

12 - 批量发布和订阅消息

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

借助批量发布和订阅 API,您可以在单个请求中发布和订阅多条消息。在编写需要发送或接收大量消息的应用程序时,使用批量操作可以通过减少 Dapr 边车、应用程序与底层发布/订阅代理之间的总体请求数量,从而实现高吞吐量。

批量发布消息

批量发布消息的限制

批量发布 API 允许您在单个请求中向主题发布多条消息。它是非事务性的,也就是说,在单个批量请求中,部分消息可能成功,部分消息可能失败。如果有任何消息发布失败,批量发布操作会返回失败消息列表。

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

示例

import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.BulkPublishResponse;
import io.dapr.client.domain.BulkPublishResponseFailedEntry;
import java.util.ArrayList;
import java.util.List;

class BulkPublisher {
  private static final String PUBSUB_NAME = "my-pubsub-name";
  private static final String TOPIC_NAME = "topic-a";

  public void publishMessages() {
    try (DaprClient client = (new DaprClientBuilder()).build()) {
      // 创建要发布的消息列表
      List<String> messages = new ArrayList<>();
      for (int i = 0; i < 10; i++) {
        String message = String.format("This is message #%d", i);
        messages.add(message);
      }

      // 使用批量发布 API 发布消息列表
      BulkPublishResponse<String> res = client.publishEvents(PUBSUB_NAME, TOPIC_NAME, "text/plain", messages).block();
    }
  }
}

import { DaprClient } from "@dapr/dapr";

const pubSubName = "my-pubsub-name";
const topic = "topic-a";

async function start() {
    const client = new DaprClient();

    // 向主题发布多条消息。
    await client.pubsub.publishBulk(pubSubName, topic, ["message 1", "message 2", "message 3"]);

    // 使用显式的批量发布消息向主题发布多条消息。
    const bulkPublishMessages = [
    {
      entryID: "entry-1",
      contentType: "application/json",
      event: { hello: "foo message 1" },
    },
    {
      entryID: "entry-2",
      contentType: "application/cloudevents+json",
      event: {
        specversion: "1.0",
        source: "/some/source",
        type: "example",
        id: "1234",
        data: "foo message 2",
        datacontenttype: "text/plain"
      },
    },
    {
      entryID: "entry-3",
      contentType: "text/plain",
      event: "foo message 3",
    },
  ];
  await client.pubsub.publishBulk(pubSubName, topic, bulkPublishMessages);
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});
using System;
using System.Collections.Generic;
using Dapr.Client;

const string PubsubName = "my-pubsub-name";
const string TopicName = "topic-a";
IReadOnlyList<object> BulkPublishData = new List<object>() {
    new { Id = "17", Amount = 10m },
    new { Id = "18", Amount = 20m },
    new { Id = "19", Amount = 30m }
};

using var client = new DaprClientBuilder().Build();

var res = await client.BulkPublishEventAsync(PubsubName, TopicName, BulkPublishData);
if (res == null) {
    throw new Exception("null response from dapr");
}
if (res.FailedEntries.Count > 0)
{
    Console.WriteLine("Some events failed to be published!");
    foreach (var failedEntry in res.FailedEntries)
    {
        Console.WriteLine("EntryId: " + failedEntry.Entry.EntryId + " Error message: " +
                          failedEntry.ErrorMessage);
    }
}
else
{
    Console.WriteLine("Published all events!");
}
import requests
import json

base_url = "http://localhost:3500/v1.0/publish/bulk/{}/{}"
pubsub_name = "my-pubsub-name"
topic_name = "topic-a"
payload = [
  {
    "entryId": "ae6bf7c6-4af2-11ed-b878-0242ac120002",
    "event": "first text message",
    "contentType": "text/plain"
  },
  {
    "entryId": "b1f40bd6-4af2-11ed-b878-0242ac120002",
    "event": {
      "message": "second JSON message"
    },
    "contentType": "application/json"
  }
]

response = requests.post(base_url.format(pubsub_name, topic_name), json=payload)
print(response.status_code)
package main

import (
  "fmt"
  "strings"
  "net/http"
  "io/ioutil"
)

const (
  pubsubName = "my-pubsub-name"
  topicName = "topic-a"
  baseUrl = "http://localhost:3500/v1.0/publish/bulk/%s/%s"
)

func main() {
  url := fmt.Sprintf(baseUrl, pubsubName, topicName)
  method := "POST"
  payload := strings.NewReader(`[
        {
            "entryId": "ae6bf7c6-4af2-11ed-b878-0242ac120002",
            "event":  "first text message",
            "contentType": "text/plain"
        },
        {
            "entryId": "b1f40bd6-4af2-11ed-b878-0242ac120002",
            "event":  {
                "message": "second JSON message"
            },
            "contentType": "application/json"
        }
]`)

  client := &http.Client {}
  req, _ := http.NewRequest(method, url, payload)

  req.Header.Add("Content-Type", "application/json")
  res, err := client.Do(req)
  // ...
}
curl -X POST http://localhost:3500/v1.0/publish/bulk/my-pubsub-name/topic-a \
  -H 'Content-Type: application/json' \
  -d '[
        {
            "entryId": "ae6bf7c6-4af2-11ed-b878-0242ac120002",
            "event":  "first text message",
            "contentType": "text/plain"
        },
        {
            "entryId": "b1f40bd6-4af2-11ed-b878-0242ac120002",
            "event":  {
                "message": "second JSON message"
            },
            "contentType": "application/json"
        },
      ]'
Invoke-RestMethod -Method Post -ContentType 'application/json' -Uri 'http://localhost:3500/v1.0/publish/bulk/my-pubsub-name/topic-a' `
-Body '[
        {
            "entryId": "ae6bf7c6-4af2-11ed-b878-0242ac120002",
            "event":  "first text message",
            "contentType": "text/plain"
        },
        {
            "entryId": "b1f40bd6-4af2-11ed-b878-0242ac120002",
            "event":  {
                "message": "second JSON message"
            },
            "contentType": "application/json"
        },
      ]'

批量订阅消息

批量订阅 API 允许您在单个请求中从主题订阅多条消息。 正如我们从如何操作:发布和订阅主题中了解到的,有三种方式订阅主题:

  • 声明式 - 订阅在外部文件中定义。
  • 编程式 - 订阅在代码中定义。
  • 流式 - 不支持批量订阅,因为消息会发送到处理程序代码。

要批量订阅主题,我们只需要使用 bulkSubscribe 规范属性,如下所示:

apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
  name: order-pub-sub
spec:
  topic: orders
  routes:
    default: /checkout
  pubsubname: order-pub-sub
  bulkSubscribe:
    enabled: true
    maxMessagesCount: 100
    maxAwaitDurationMs: 40
scopes:
- orderprocessing
- checkout

在上面的示例中,bulkSubscribe 是_可选的_。如果您使用 bulkSubscribe,则:

  • enabled 是必需的,用于在此主题上启用或禁用批量订阅
  • 您可以选择配置在批量消息中传递的最大消息数量(maxMessagesCount)。 对于不支持批量订阅的组件,maxMessagesCount 的默认值为 100,即应用程序与 Dapr 之间的默认批量事件。请参阅组件如何处理批量消息的发布和订阅。 如果组件支持批量订阅,则可以在该组件的文档中找到此参数的默认值。
  • 您可以选择在批量消息发送到应用程序之前提供等待的最大持续时间(maxAwaitDurationMs)。 对于不支持批量订阅的组件,maxAwaitDurationMs 的默认值为 1000,即应用程序与 Dapr 之间的默认批量事件。请参阅组件如何处理批量消息的发布和订阅。 如果组件支持批量订阅,则可以在该组件的文档中找到此参数的默认值。

应用程序会收到与批量消息中每个条目(单独的消息)关联的 EntryId。应用程序必须使用此 EntryId 来传达该特定条目的状态。如果应用程序未能通知 EntryId 状态,则视为 RETRY

需要发送一个 JSON 编码的负载正文,其中包含每个条目的处理状态:

{
  "statuses":
  [
    {
    "entryId": "<entryId1>",
    "status": "<status>"
    },
    {
    "entryId": "<entryId2>",
    "status": "<status>"
    }
  ]
}

可能的状态值:

状态描述
SUCCESS消息处理成功
RETRY消息将由 Dapr 重试
DROP记录警告并丢弃消息

有关响应的更多见解,请参阅批量订阅的预期 HTTP 响应

示例

以下代码示例演示如何使用批量订阅。

import io.dapr.Topic;
import io.dapr.client.domain.BulkSubscribeAppResponse;
import io.dapr.client.domain.BulkSubscribeAppResponseEntry;
import io.dapr.client.domain.BulkSubscribeAppResponseStatus;
import io.dapr.client.domain.BulkSubscribeMessage;
import io.dapr.client.domain.BulkSubscribeMessageEntry;
import io.dapr.client.domain.CloudEvent;
import io.dapr.springboot.annotations.BulkSubscribe;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import reactor.core.publisher.Mono;

class BulkSubscriber {
  @BulkSubscribe()
  // @BulkSubscribe(maxMessagesCount = 100, maxAwaitDurationMs = 40)
  @Topic(name = "topicbulk", pubsubName = "orderPubSub")
  @PostMapping(path = "/topicbulk")
  public Mono<BulkSubscribeAppResponse> handleBulkMessage(
          @RequestBody(required = false) BulkSubscribeMessage<CloudEvent<String>> bulkMessage) {
    return Mono.fromCallable(() -> {
      List<BulkSubscribeAppResponseEntry> entries = new ArrayList<BulkSubscribeAppResponseEntry>();
      for (BulkSubscribeMessageEntry<?> entry : bulkMessage.getEntries()) {
        try {
          CloudEvent<?> cloudEvent = (CloudEvent<?>) entry.getEvent();
          System.out.printf("Bulk Subscriber got: %s\n", cloudEvent.getData());
          entries.add(new BulkSubscribeAppResponseEntry(entry.getEntryId(), BulkSubscribeAppResponseStatus.SUCCESS));
        } catch (Exception e) {
          e.printStackTrace();
          entries.add(new BulkSubscribeAppResponseEntry(entry.getEntryId(), BulkSubscribeAppResponseStatus.RETRY));
        }
      }
      return new BulkSubscribeAppResponse(entries);
    });
  }
}

import { DaprServer } from "@dapr/dapr";

const pubSubName = "orderPubSub";
const topic = "topicbulk";

const daprHost = process.env.DAPR_HOST || "127.0.0.1";
const daprPort = process.env.DAPR_HTTP_PORT || "3502";
const serverHost = process.env.SERVER_HOST || "127.0.0.1";
const serverPort = process.env.APP_PORT || "5001";

async function start() {
    const server = new DaprServer({
        serverHost,
        serverPort,
        clientOptions: {
            daprHost,
            daprPort,
        },
    });

    // 使用默认配置向主题发布多条消息。
    await client.pubsub.bulkSubscribeWithDefaultConfig(pubSubName, topic, (data) => console.log("Subscriber received: " + JSON.stringify(data)));

    // 使用特定的 maxMessagesCount 和 maxAwaitDurationMs 向主题发布多条消息。
    await client.pubsub.bulkSubscribeWithConfig(pubSubName, topic, (data) => console.log("Subscriber received: " + JSON.stringify(data)), 100, 40);
}
using Microsoft.AspNetCore.Mvc;
using Dapr.AspNetCore;
using Dapr;

namespace DemoApp.Controllers;

[ApiController]
[Route("[controller]")]
public class BulkMessageController : ControllerBase
{
    private readonly ILogger<BulkMessageController> logger;

    public BulkMessageController(ILogger<BulkMessageController> logger)
    {
        this.logger = logger;
    }

    [BulkSubscribe("messages", 10, 10)]
    [Topic("pubsub", "messages")]
    public ActionResult<BulkSubscribeAppResponse> HandleBulkMessages([FromBody] BulkSubscribeMessage<BulkMessageModel<BulkMessageModel>> bulkMessages)
    {
        List<BulkSubscribeAppResponseEntry> responseEntries = new List<BulkSubscribeAppResponseEntry>();
        logger.LogInformation($"Received {bulkMessages.Entries.Count()} messages");
        foreach (var message in bulkMessages.Entries)
        {
            try
            {
                logger.LogInformation($"Received a message with data '{message.Event.Data.MessageData}'");
                responseEntries.Add(new BulkSubscribeAppResponseEntry(message.EntryId, BulkSubscribeAppResponseStatus.SUCCESS));
            }
            catch (Exception e)
            {
                logger.LogError(e.Message);
                responseEntries.Add(new BulkSubscribeAppResponseEntry(message.EntryId, BulkSubscribeAppResponseStatus.RETRY));
            }
        }
        return new BulkSubscribeAppResponse(responseEntries);
    }
    public class BulkMessageModel
    {
        public string MessageData { get; set; }
    }
}

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

import json
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/dapr/subscribe', methods=['GET'])
def subscribe():
    # 定义批量订阅配置
    subscriptions = [{
        "pubsubname": "pubsub",
        "topic": "TOPIC_A",
        "route": "/checkout",
        "bulkSubscribe": {
            "enabled": True,
            "maxMessagesCount": 3,
            "maxAwaitDurationMs": 40
        }
    }]
    print('Dapr pub/sub is subscribed to: ' + json.dumps(subscriptions))
    return jsonify(subscriptions)


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

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

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

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

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

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

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

从 Dapr 边车到发布/订阅代理的优化取决于许多因素,例如:

  • 代理必须本身支持批量发布/订阅
  • Dapr 组件必须更新以支持代理提供的批量 API 的使用

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

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

演示

观看以下关于批量发布/订阅的演示和演示文稿。

KubeCon Europe 2023 演讲

Dapr 社区会议 #77 演讲

相关链接