背景
目前几乎所有主流的 公有云 都接入了 CloudEvents 规范,从大多数使用者的角度上来看——统一规范对后期接入任何云都有显著的帮助,尤其在 Serverless 层面,大家做的事情都很类似的情况下。
那么 CloudEvents 解决了什么问题:
- 规范化
- 集成化
- 提高可移植性
- 更加友善的进行开发和测试
- 更好的安全策略、更容易的事件追踪、更直接的事件关联
Event(s)
所以什么是事件?
当 组件 或者 类 之间的内聚性很高时,那么它们的耦合性应该很低。
假设两个组件之间存在相互调用的场景:A组件需要调用B组件内部的逻辑,从逻辑上来说A组件需要调用B组件的方法才可以实现这个场景,但前提是A组件必须知道B组件是存在,也就是说——它们存在依赖,也就是耦合关系。
当存在这种关系时,组件数量上增加之后系统便会难以维护,那么我们需要一个架构来解决这种问题,也就是:事件驱动(Event-driven Architecture, EDA)
当有了事件驱动之后,我们可以做类似这样的事情:
A组件执行的逻辑需要触发B组件的逻辑时,不需要直接调用它,而是将事件发送到派发器。B组件只需要监听调度事件,并在事件触发时执行操作即可。
在这种场景中,事件是应用程序的一部分,它在多个组件中之间存在并执行组件之间的通讯。
Cloud Event(s)
结合 Event(s) 中提到了事件的定义,云服务上的事件则是在服务器之间来通讯:一个系统的状态变更,导致另外一个系统的代码触发执行。
让我们再具体一点:某个事件源在收到 信号 时(HTTP or RPC),派发了一条 基于某个规范的数据 事件,随后触发了某个使用 该事件规范 的方法(函数)。
事件源也就是我们所理解的:托管服务,而方法(函数)指的就是 Serverless 函数。
传输
CloudEvents 支持通过各种协议进行传输,已支持的如下:
- 各种行业标准协议(如 HTTP、AMQP、MQTT、SMTP)
- 开源协议(例如 Kafka、NATS)
- 平台/供应商专有协议(AWS Kinesis、Azure Event Grid)
格式
将服务放在云上并在其各个服务之间拓展了事件,这是一件好事。
但我们先看看 市场占有率前三 的云服务商所设计的 Event-schema:
亚马逊 AWS (暂时没有对接 CloudEvents)
{ "version": "0", "id": "6a7e8feb-b491-4cf7-a9f1-bf3703467718", "detail-type": "EC2 Instance State-change Notification", "source": "aws.ec2", "account": "111122223333", "time": "2017-12-22T18:43:48Z", "region": "us-west-1", "resources": [ "arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0" ], "detail": { "instance-id": "i-1234567890abcdef0", "state": "terminated" } }
微软 Azure
详见 Azure Event Grid event schema
{ "topic": "/subscriptions/{subscription-id}", "subject": "/subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/Microsoft.EventGrid/eventSubscriptions/LogicAppdd584bdf-8347-49c9-b9a9-d1f980783501", "eventType": "Microsoft.Resources.ResourceWriteSuccess", "eventTime": "2017-08-16T03:54:38.2696833Z", "id": "25b3b0d0-d79b-44d5-9963-440d4e6a9bba", "data": { "authorization": "{azure_resource_manager_authorizations}", "claims": "{azure_resource_manager_claims}", "correlationId": "54ef1e39-6a82-44b3-abc1-bdeb6ce4d3c6", "httpRequest": "", "resourceProvider": "Microsoft.EventGrid", "resourceUri": "/subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/Microsoft.EventGrid/eventSubscriptions/LogicAppdd584bdf-8347-49c9-b9a9-d1f980783501", "operationName": "Microsoft.EventGrid/eventSubscriptions/write", "status": "Succeeded", "subscriptionId": "{subscription-id}", "tenantId": "72f988bf-86f1-41af-91ab-2d7cd011db47" } }
谷歌 GCP
{ "data": { "@type": "types.googleapis.com/google.pubsub.v1.PubsubMessage", "attributes": { "foo": "bar" }, "messageId": "12345", "publishTime": "2017-06-05T12:00:00.000Z", "data": "somebase64encodedmessage" }, "context": { "eventId": "12345", "timestamp": "2017-06-05T12:00:00.000Z", "eventTypeId": "google.pubsub.topic.publish", "resource": { "name": "projects/myProject/topics/myTopic", "service": "pubsub.googleapis.com" } } }
这三大云服务商所提供的事件数据,均不一致。这迫使了开发者需要 自定义适配器 以实现跨平台的相关操作。
事实上,跨平台传输事件的场景已经非常普遍,但往往会出现些许问题:
- 没有通用的数据格式进行描述
- 事件流的源头和去向,是不明确的
再加上 FaaS 函数计算如今已经非常普遍,格式不一致再加上考虑成本的情况下,可移植性几乎为0
这是一个通用的 CloudEvent 事件的数据结构,此处不做展开描述。
{
"specversion" : "1.0",
"type" : "com.github.pull_request.opened",
"source" : "https://github.com/cloudevents/spec/pull",
"subject" : "123",
"id" : "A234-1234-1234",
"time" : "2018-04-05T17:31:00Z",
"comexampleextension1" : "value",
"comexampleothervalue" : 5,
"datacontenttype" : "text/xml",
"data" : ""
}
目的
按照 CloudEvents 自身的描述:
- 一个用于定义事件格式的供应商中立规范
- 一个以通用格式来描述事件数据的标准。它提供了事件在服务、平台和系统中的互操作性。
在关于 架构 描述中,我们可以理解为:
- CloudEvents 定义了一种基本数据结构,其中包含了必要的字段及类型
- 开发者可以基于这份数据结构,拓展你业务中所需要的上下文,但前提是数据类型要符合要求
- 如果业务中存在 WebHooks 服务,也要按照其 CloueEvents 规范来。
至于 核心规范 中提到的其他部分,是针对其所提供功能的解读和进一步描述,例如:
- 开发者所发送的数据,可能产生的安全问题应该由开发者来解决。
- 事件的大小进行了限制
回到目的本身,这套规范所能提供的是——增加业务的可移植性,当然这并不是真的要求开发者任意在各云服务之间来回摇摆。
而是可移植性的提升,带来的是开发者在跨平台之间通讯便利性的提升,其次是学习成本的降低。
纵使现在看来 数据结构 没有那么复杂,但随着业务的复杂度提升,作为开发者我希望对接的格式是稳定且统一,而不是五花八门要写各式各样适配器的。
以谷歌 Cloud Functions Gen2 为例,在 编写事件驱动型函数 时,已经不区分语言的建议所有运行时都使用 CloudEvent 函数。
functions.cloudEvent('myCloudEventFunction', cloudEvent => {
// Your code here
// Access the CloudEvent data payload via cloudEvent.data
});
package mycloudeventfunction
import (
"context"
"github.com/GoogleCloudPlatform/functions-framework-go/functions"
"github.com/cloudevents/sdk-go/v2/event"
)
func init() {
// Register a CloudEvent function with the Functions Framework
functions.CloudEvent("MyCloudEventFunction", myCloudEventFunction)
}
// Function myCloudEventFunction accepts and handles a CloudEvent object
func myCloudEventFunction(ctx context.Context, e event.Event) error {
// Your code here
// Access the CloudEvent data payload via e.Data() or e.DataAs(...)
// Return nil if no error occurred
return nil
}
这意味着你在开发函数时,参数格式是与 CloudEvents 所提供保持完全一致。
除此之外,微软 Azure 也在操作文档中提到了 如何使用CloudEvents 来与他们的 Event Grid 做配合使用。
SDK
以一个简单的场景做出发点,即可理解SDK做了什么事情。
让我们分几个流程:
- 客户端发起请求,
request-body
中包含业务数据。 - 服务端接受到请求,获取
Header
/Body
,调用 CloudEvents-SDK 生成对应格式数据 - 调用 FaaS 某个函数,将转换之后的数据格式注入到函数中
Client 端
import axios from 'axios'
axios({
method: 'post',
url: 'http://localhost:1234',
data: {
name: 'John Doe',
age: 40,
},
// headers: {},
})
Server 端
import express from 'express'
import { CloudEvent, HTTP } from 'cloudevents'
import bp from 'body-parser'
const app = express()
const port = 1234
app.use(bp.json())
app.use(bp.urlencoded({ extended: true }))
app.post('/', (req, res) => {
console.log('HEADERS', req.headers)
console.log('BODY', req.body)
try {
const event = HTTP.toEvent({
headers: {
...req.header,
'content-type': 'application/cloudevents+json',
},
body: req.body,
})
// events -> HTTP.toEvent 加工之后变成:
// console.log(event) ===> {
// id: '66711760-1da0-4db3-b6b0-fbdc6de98244', *新增
// time: '2022-09-06T13:37:50.641Z', *新增
// specversion: '1.0', *新增
// name: 'John Doe', #业务数据
// age: 40, #业务数据
// }
// 如 CloudEvents 规范所指出:
// 由于一次事件的发生可能导致生成多个cloud event
// 在所有这些事件都来自同一事件源的情况下 生成的每个 CloudEvent 将具有唯一的 id
const ce = {
// 这一步是为了整合字段
// 其中 source、type 均为必填字段
// 字段详见:https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/translated/zh-cn/spec_CN.md#%E5%BF%85%E8%A6%81%E5%B1%9E%E6%80%A7
...event,
source: '/',
type: 'test-type',
}
const responseEventMessage = new CloudEvent(ce)
responseEventMessage.data = {
hello: 'world',
}
// const message = HTTP.binary(responseEventMessage)
// console.log(message) ===> {
// headers: {
// 'content-type': 'application/json; charset=utf-8',
// 'ce-id': '66711760-1da0-4db3-b6b0-fbdc6de98244',
// 'ce-time': '2022-09-06T13:37:50.641Z',
// 'ce-type': 'test-type',
// 'ce-source': '/',
// 'ce-specversion': '1.0',
// 'ce-name': 'John Doe',
// 'ce-age': 40,
// },
// body: '{"hello":"world"}',
// }
const message = HTTP.structured(responseEventMessage)
// console.log(message) ===> {
// headers: {
// 'content-type': 'application/cloudevents+json; charset=utf-8',
// },
// body: '{"id":"66711760-1da0-4db3-b6b0-fbdc6de98244","time":"2022-09-06T13:37:50.641Z","type":"test-type","source":"/","specversion":"1.0","name":"John Doe","age":40,"data":{"hello":"world"}}',
// }
// 你可以选择以两种形式输出你的数据格式
// ...后续逻辑
} catch (err) {
console.error(err)
}
})
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})