什么是 CloudEvents,它的目的是什么?

背景

目前几乎所有主流的 公有云 都接入了 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,它的目的是什么?_第1张图片

传输

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

你可能感兴趣的:(什么是 CloudEvents,它的目的是什么?)