Dapr云原生应用开发系列6:绑定构建块
题记:这篇介绍绑定构建块,这是一个极度简化应用程序本身代码的特性。本文在GitHub Copilot的帮助下书写。
原理
由于Dapr由微软Azure团队孵化,所以绑定这一概念也是来源于微软的开源Serverless项目Azure Functions。所以我们理解Dapr的绑定构建块,可以参考Azure Functions绑定的概念。
其中,Dapr的输入绑定即Azure Function的触发器;Dapr的输出绑定即Azure Function的输出绑定;但是Azure Function的输入绑定在Dapr中并没有对应概念。
通过输入绑定,我们可以让Dapr应用程序被外部事件触发,而不是像传统应用程序那样,需要自己去轮询外部事件源。通过输出绑定,我们可以让Dapr应用程序触发外部事件,而不是像传统应用程序那样,需要自己去调用外部事件源。另外通过输出绑定,也可以让Dapr应用程序把相关数据写入到外部服务。
如下图所示,我们可以通过输入绑定来接收来自于Kafka的消息:
或者,通过输出绑定来发送消息到Kafka:
当然输出绑定不是说只能发送消息到消息队列,也是可以写入数据到外部数据源,比如调用HTTP API,访问数据库,甚至通过SMTP协议来发送邮件。比如下图的例子中,通过定时器触发Dapr应用,Dapr应用写入数据到PostgreSQL里面:
边车的作用
众所周知,Dapr构建块都是通过Dapr边车来提供能力的,绑定构建块也不例外。Dapr边车会在应用程序启动的时候,自动去注册绑定构建块所需要的输入和输出绑定。这样,应用程序就可以通过Dapr边车来使用绑定构建块的能力了。
Dapr边车会处理输入绑定的消息队列的连接,消息队列的消息实际上是发给Dapr边车,Dapr边车再请求Dapr应用程序匹配的路由接口(默认是和绑定名称一致的路由接口,也可以通过绑定定义yaml文件中设定不同的名称)。要特别注意的是,Dapr边车访问应用程序的接口,是需要获得 200 OK
才认为成功,不然会一直重试。所以应用程序需要很小心的处理异常(一般需要Catch后总是返回200),以避免Dapr边车进入死循环。还有一种特殊的情况是,如果使用诸如ASP.NET Core的模型绑定能力,那么要保证模型定义具备很强的兼容性,不然ASP.NET Core框架本身会返回400,导致Dapr边车进入死循环。
同样,Dapr边车会处理输出绑定的消息队列的连接,Dapr应用程序发送消息或写入数据实际上是发给Dapr边车,Dapr边车再转发给消息队列。Dapr边车会根据绑定定义yaml文件中的配置,来决定消息队列(或数据源)的类型和连接方式。
和发布订阅构建块PubSub的关系
大家应该看到,输入和输出绑定都可以和消息队列中间件结合,那么在这种情况下和PubSub的关系是什么呢?
我想主要区别在于:PubSub主要处理的是系统边界之内的消息(这些消息往往是广播的事件消息),而输入和输出绑定主要处理的是系统边界之外的消息(这些消息往往是点对点的数据消息)。
正因为这样的区别,所以两者在使用同一种消息队列中间件的时候,也会有不同的使用方式。比如发布订阅和绑定构建块都可以使用Azure Service Bus作为消息队列中间件,但是在使用的时候,发布订阅会使用Service Bus的Topic和Subscription(支持消息广播),而绑定构建块会使用Service Bus的Queue(支持点对点)。
能力
绑定目前支持的组件在这里可以查询到:https://docs.dapr.io/reference/components-reference/supported-bindings/
这个列表应该会不断的扩充。我在这里仅列出一些比较重要的组件。
输入绑定
- Cron (Scheduler)
- Kafka
- RabbitMQ
- Azure Service Bus Queues
- Azure Storage Queues
输出绑定
- HTTP
- Kafka
- PostgreSQL
- RabbitMQ
- Redis
- SMTP
- Azure Blob Storage
- Azure Service Bus Queues
- Azure Storage Queues
规范
绑定定义yaml文件
以Kafka为例,绑定定义yaml文件如下:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: checkout
spec:
type: bindings.kafka
version: v1
metadata:
- name: brokers
value: "http://localhost:5050"
- name: topics
value: "someTopic"
- name: publishTopic
value: "someTopic2"
- name: consumerGroup
value: "group1"
- name: "direction"
value: "input, output"
这个文件中,我们可以看到:
- 通过
spec.type
字段来指定绑定的类型,这里是bindings.kafka
,也就是Kafka的输入输出绑定。 - 通过
spec.metadata
字段来指定绑定的配置,这里是Kafka的特定配置,包括brokers
、topics
、publishTopic
和consumerGroup
。不同绑定组件,可能具备不同的配置。 - 通过
spec.metadata.direction
(可选)字段来指定绑定的方向,这里是input, output
,也就是输入输出都支持。 - 在实践当中,我们最好通过
scopes
限定一下组件的作用域,比如scopes: ["myapp"]
,这样就只有在dapr run --app-id myapp
的时候才会加载这个组件。避免无关的应用使用到不应该使用的组件。
注册输入绑定的触发接口
在定义了输入方向的绑定组件之后,就需要在应用中提供一个和组件名称一致(约定优先)的路由地址处理接口,来让外部可以触发应用程序的接口(也即接收消息队列的消息)。比如上面的Kafka绑定,就需要提供一个 /checkout
的路由地址。
以ASP.NET Core为例,我们可以这样实现这个接口:
//dependencies
using System.Collections.Generic;
using System.Threading.Tasks;
using System;
using Microsoft.AspNetCore.Mvc;
//code
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;
}
}
}
调用输出绑定发送数据或消息
在定义了输出方向的绑定组件之后,就可以在应用中调用通过Dapr HTTP API调用如下地址:
POST/PUT http://localhost:<daprPort>/v1.0/bindings/<name>
并传递如下的JSON数据:
{
"data": "some data",
"operation": "create",//目前默认都是create这个值
"metadata": {
"key": "value"
}
}
DOTNET SDK
.NET SDK提供了对输出绑定调用的封装,具体用法为:
string BINDING_NAME = "checkout";
string BINDING_OPERATION = "create";
using var client = new DaprClientBuilder().Build();
//Using Dapr SDK to invoke output binding
await client.InvokeBindingAsync(BINDING_NAME, BINDING_OPERATION, orderId);
用法与例子
用法和例子,可以参考官方文档:https://docs.dapr.io/developing-applications/building-blocks/bindings/
包括绑定的API规范文档:https://docs.dapr.io/reference/api/bindings_api/
也可以参考我的Quickstarts项目:https://github.com/heavenwing/dapr-dotnet-quickstarts/tree/main/BindingsWithSdk
实际场景
我在某个项目中把输入和输出绑定运用到了一个用户导入的实际场景中。
具体做法是:
- 用户在前端上传一个Excel文件,后端接收到这个文件之后,会把这个文件的内容解析成一个个用户对象,然后把每个用户对象通过输出绑定发送到消息队列(Request队列)中;
- 在一个外部的Azure Function中订阅Request队列中的数据,并创建出SSO用户,然后把创建结果发送到消息队列(Response队列)中;
- 后端通过输入绑定来订阅Response队列中的数据,并把创建结果保存到数据库。
整个过程如下图所示: