以下内容参考 https://mp.weixin.qq.com/s/fAw2wWvHM1JHtuG0OzDUDw
什么是事件?我们给事件下的一个定义是: 过去已经发生的事,尤其是比较重要的事。
比如说,昨天下午我做了一次核酸检测;今天上午又吃了一个冰激淋。这些都是过去已经发生的事件。但是,如果我再问:事件跟消息有什么区别?这个时候,大家是不是觉得事件这个定义,好像又不那么清晰?
刚才说的那些事件,是不是也可以理解为消息啊?如果,老张给我发送了一条短信,这个算是事件,还是消息啊?平常开发过程中,“什么时候使用消息,什么时候使用事件?”
不过,在回答这个问题之前,我们一起来看一个典型的微服务。
一个微服务系统和外部系统的交互,可以简单分为两部分:一是接收外部请求(就是图中上面黄色的部分);二是是调用外部服务(就是图中下面绿色的部分)。
接收外部请求,我们有两种方式:一种是提供 API,接收外部发过来的 Query 请求和 Commond 请求;另外一种是主动订阅外部 Command 消息。这两类操作,进入系统内部之后呢,我们常常还会,调用其他为微服务系统,一起协同处理,来完成一个具体的操作。当这些操作,使得系统状态发生改变时,就会产生事件。
这里呢,我们把从外部接收到的 Command 消息,和系统内部产生的事件,都称之为消息。
我们总结一下,消息和事件的关系是这样的:消息包含两部分,Command 消息和 Event 消息
所以,事件和消息是不同的,事件可以理解为是一种特殊的消息。
事件,一定是“已发的”。“已发生”的代表什么呢?不可变的。我们不可能改变过去。这个特性非常重要,在我们处理事件、分析事件的时候,这就意味着,我们绝对可以相信这些事件,只要是收到的事件,一定是系统真实发生过的行为。而且是不可修改。
事件是客观的描述一个事物的状态或属性值的变化,但对于如何处理事件本身并没有做任何期望。
相比之下,Commond 和 Query 则都是有期望的,他们希望系统做出改变或则返回结果,但是 Event 呢,它只是客观描述系统的一个变化。
事件的第三个特性是:“天然有序”。含义:同一个实体,不能同时发生 A 又发生 B,必有先后关系;如果是,则这两个事件必属于不同的事件类型。
因为天然有序,跟时间轴上的某一时刻强绑定,且不能同时发生,所以它一定是唯一的。
如果我们看到了两个内容一样的事件,那么一定是发生了两次,而且一次在前,一次在后。(这对于我们处理数据最终一致性、以及系统行为分析都很有价值:我们看到的,不光光是系统的一个最终结果,而是看到变成这个结果之前的,一系列中间过程)
事件的第四个特性是:“具象化”的。
事件会尽可能的把“案发现场”完整的记录下来,因为它也不知道消费者会如何使用它,所以它会做到尽量的详尽,比如:
对比我们常见的消息,因为上下游一般是确定的,常常为了性能和传输效率,则会做到尽可能的精简,只要满足“计划经济”指定安排的消费者需求即可。
总结一下,事件上面的 4 个特性,是对事件巨大的一个属性加成,让事件拥有了跟普通消息不一样的“超能力”。使事件,常常被用到 4 个典型场景:事件通知、事件溯源、系统间集成和 CQRS。
在 Auhting 中比如管理员删除用户池时,会产生很多事件包括用户池删除、该用户池下的 N 个应用的删除等。需要通知给其他微服务如权限服务删除相应的策略。
事件通知有多种方式:
对于第三种方式,生产者和消费者,他们都只需要关注自己系统本身就可以了。生产者,生产什么样的事件,消费者,消费什么样的数据格式,都各自以自己的业务为中心,不需要为对方做适配。真正做到我们说的高内聚低耦合,实现彻底的完全解耦。
回到我们一开始提到的典型微服务模型,对于有些场景,我们就可以变为下面这种方式:对微服务的变更操作,统一收敛到 API 操作入口,去掉 Command 消息入口。收敛入口,对于我们维护微服务,保障系统稳定性,常常非常有好处的。
事件溯源是什么?事件溯源简单理解就是让系统回到过去任意时刻。那怎么样,才能让系统可以回到过去呢?很简单,首先系统所有发生的变化,都得以事件的方式记录下来;然后,我们就可以通过回放事件的方式,回到过去任何一个时刻。
那为什么只有事件才能做这个事,其他普通消息不行呢?这个还是要回到我们刚才说的几个事件特性:已发生不可变的、天然有序且唯一的、而且是非常详细具体的,完整的记录了事件的案发现场。所以,对于事件溯源这个场景,事件可以说是系统的一等一的公民。举个例子:比如说,如果我们能够完整地收集路上的各种事件信息,包括信号灯、车量、天气、拥堵路况等等,那么,我们就可以“穿越时间”,回到交通现场,重新做一次决策。比如,在智慧交通场景,当我们想去验证一个调度算法的时候,我们就可以回放当时发生的所有事件,来重现现场。
大家可能觉得这个很神奇,但是,其实我们平常一直有接触,就是我们常用的代码版本-管理系统,比如:github。
刚才讲的第1个场景:事件通知,一般涉及到两个上下游团队的协作开发;讲的第 2 个场景:事件溯源,则一般是 1 个团队内的开发;但系统间集成,则往往面对的是三个业务团队的协作开发。这个怎么理解呢?
其实这个也很常见:比如公司里购买了 ERP 系统,同时也购买了外部考勤系统、外部营销系统服务等等。这些系统都有一个共同点,是什么?都不是我们自己开发的,是而买来的。
如果我们想把 ERP 系统的人员信息,实时且自动同步到考勤系统中去怎么办?其实这个是有点麻烦的,因为这些都不是我们自己开发的。
1、我们不能修改 ERP 系统的代码,主动去调用考勤系统,把人员变更信息发送过去;
2、也不能修改考情系统的代码,主动去调用外部 ERP 系统的 API;
但是我们可以通过事件总线,借助 webhook 或则标准 API 等等方式,收集上游的 ERP 系统产生的人员变更事件,然后进行过滤和转换,推送到下游考勤系统中去,当然,这里也可以是内部自研服务。
所以,现在的研发模式变成了:事件中心管理了所有 SaaS 服务,包括内部自研系统产生的所有事件。然后呢,我们只需要在事件中心,寻找我们需要的事件,进行订阅,对 SaaS 服务和内部自研系统,进行简单服务编排,即可完成开发。
CQRS 中的 C 代表 Command,Command 什么意思?就是明令,一般包含:Create/Update/Delete,Q 代表 Query,是指查询。所以 CQRS 本质是读写分离:所有的写操作,在图中左边的系统中完成,然后将系统因为 Command 产生变化的事件,同步到右边的查询系统。
以下内容参考 https://mp.weixin.qq.com/s/MCdVuaV7_dwJt0Ibe4sdHg
在业务系统中,事件指的是领域事件,而消息可以是任意数据或数据片段。
领域事件的特点包括:
事件驱动架构是一种用于设计应用的软件架构和模型。对于事件驱动系统而言,事件的捕获、通信、处理和持久保留是解决方案的核心结构。这和传统的请求驱动模型有很大不同。
许多现代应用设计都是由事件驱动的,例如必须实时利用客户数据的客户互动框架。事件驱动应用可以用任何一种编程语言来创建,因为事件驱动本身是一种编程方法,而不是一种编程语言。事件驱动架构可以最大程度减少耦合度,因此是现代化分布式应用架构的理想之选。
事件驱动架构是一种松耦合、分布式的驱动架构,收集到某应用产生的事件后实时对事件采取必要的处理后路由至下游系统,无需等待系统响应。
事件驱动架构由事件发起者和事件使用者组成。事件的发起者会检测或感知事件,并以消息的形式来表示事件。它并不知道事件的使用者或事件引起的结果。
检测到事件后,系统会通过事件通道从事件发起者传输给事件使用者,而事件处理平台则会在该通道中以异步方式处理事件。事件发生时,需要通知事件使用者。他们可能会处理事件,也可能只是受事件的影响。
事件处理平台将对事件做出正确响应,并将活动下发给相应的事件使用者。通过这种下发活动,我们就可以看到事件的结果。
Apache Kafka 是一种分布式数据流平台,也是事件处理的常见之选。它可以实时进行事件流的发布、订阅、存储和处理。Apache Kafka 支持需要高吞吐量和可扩展性的用例,同时,通过最大程度减少某些应用中对数据共享的点对点集成需求,它可以将延迟降至毫秒级。
除此之外,还有其他一些中间件事件管理器也可用作事件处理平台。
发布/订阅模型
这是一种基于事件流订阅的消息传递基础架构。对于该模型而言,在事件发生或公布之后,系统会将相应的消息发送给需要通知的订阅用户。
借助事件流模型,事件将被写入日志。事件使用者无需订阅事件流。相反,它们可以从流的任何部分读取并随时加入流。
事件流有几种不同的类型:
事件驱动架构具有以下优势:
下面我们举几个例子来描述事件驱动架构的解耦和广播能力如何帮助解决现实工作中的问题:
在基于请求/响应方式的服务化架构中,上游服务按照约定的RPC接口调用下游服务,这样有一个比较严重的问题:上游服务作为数据(例如业务单据)的生产者,强依赖了作为数据消费方的下游服务所定义的接口,导致上游服务自身无法沉淀接口和数据标准。
一种更合理的方案是依赖倒置:由上游服务定义SPI,下游服务实现SPI,这样,上游服务终于有机会沉淀出自身的接口和数据标准,不再需要适配各个下游服务的接口,而是由下游服务的开发者按照接口文档来做实现。但这种设计仍然无法解决运行时上游服务仍然依赖下游服务的问题,下游服务的可用性、一致性、幂等性能力会直接影响上游服务的相关指标及实现方式,需要上下游服务开发者一起对齐方案,在出问题时一起解决。
使用事件驱动设计可以实现契约定义和运行时的全面解耦:上游服务可以沉淀自己的事件契约,在运行时无论是上游服务还是下游服务都只依赖事件Broker,下游服务的可用性和一致性等问题由事件Broker来保障。
在供应链中台这样复杂的微服务架构中,关键的上游服务往往有多个下游服务,上游服务一般需要顺序或并发调用所有的下游服务来完成一次完整的调用。
上游服务的开发者会面临多个难题:
而下游服务的开发者也有自己的问题:
以下内容参考 https://github.com/cloudevents/spec
事件无处不在,然而,事件生产者倾向于以不同的方式来描述事件。若在没有统一的规范标准下会存在以下等问题:
CloudEvents 是一个用通用格式描述事件数据的规范,以提供跨服务、跨平台和跨系统的互操作性。
CloudEvents 得到了大量的行业关注,从主要的云提供商到流行的 SaaS 公司都有。CloudEvents 由云原生计算基金会(CNCF)主办,于2018年5月15日获批为云原生沙盒级项目。
CloudEvents 提供了 Go、JavaScript、Java、C#、Ruby 和 Python 的 SDK,可用于构建事件路由器、跟踪系统和其他工具。
CloudEvents 通常用于分布式系统中,允许服务在开发过程中松散耦合,独立部署,稍后可以连接起来创建新的应用。CloudEvents 规范的目标是定义事件系统的互操作性,允许服务产生或消费事件,其中生产者和消费者可以独立开发和部署。生产者可以在消费者监听之前产生事件,而消费者可以对尚未产生的事件或事件类表达兴趣。请注意,这项工作所产生的规范侧重于事件格式的互操作性,以及事件在HTTP 等各种协议上发送时如何呈现。该规范将不关注事件生产者或事件消费者的处理模型。
CloudEvents 的核心是定义了一组关于系统间传输事件的元数据(被称为属性),以及这些元数据应该如何出现在该消息中。这些元数据是将请求路由到适当的组件并促进该组件对事件进行适当处理所需的最低限度的信息集。因此,虽然这可能意味着事件本身的一些应用数据可能会和作为 CloudEvent 属性集的一部分重复,但这只是为了正确传递和处理消息的目的。不用于此目的的数据应放在事件(数据)本身中。此外,假定协议层向目标系统传递消息所需的元数据完全由协议处理,因此不包含在 CloudEvents 属性中。在定义这些属性的同时,还将对如何以不同格式(如 JSON)和协议(如 HTTP、AMQP、Kafka)序列化事件进行规范。一些协议原生支持将多个事件批量化为一个 API 调用。为了帮助实现互操作性,是否需要批处理和如何实现批处理由协议决定。详情可在协议绑定或协议规范中找到。
CloudEvents 的批处理没有语义,也没有顺序。中间人可以添加或删除批处理,也可以将事件分配给不同的批次。
事件(Event)包含和发生(Occurrence)相关的上下文和数据。每一个发生(Occurrence)都由事件的数据唯一标识。
事件代表事实,因此不包括目的地,而消息则传达意图,将数据从源头传送到特定的目的地。
在服务器端代码中,事件通常用于连接不同的系统,其中一个系统的状态变化会导致另一个系统的代码执行。例如,当源接收到外部信号(如 HTTP 或 RPC)或观察到一个变化的值(如 IoT 传感器或非活动期)时,可能会产生一个事件。
为了说明系统如何使用 CloudEvents,下面的简化图显示了来自源的事件如何触发一个动作。
源(Source)生成消息(Message),其中事件(Event)被封装在协议中。事件到达目的地,触发一个由事件数据提供的动作(Action)。源是源类型的一个特定实例,它允许 staging 和测试实例。一个特定源类型的开放源软件可以由多个公司或供应商部署。
以下内容被认为超出了规范的范围。
CloudEvents 规范集定义了四种不同类型的协议元素,它们构成了一个分层架构模型。
大多数情况下,CloudEvents 规范并没有规定与创建或处理 CloudEvents 相关的处理模型。因此,如果在处理 CloudEvents 过程中出现错误,鼓励遇到错误的软件使用常规的协议级错误报告来报告错误。
对于某些 CloudEvents 属性,其值所引用的实体或数据模型可能会随着时间的推移而变化。例如,dataschema 可能会引用模式文档的一个特定版本。通常情况下,这些属性值将通过在其值中包含某些特定于版本的字符串作为其值的一部分来区分每个变体。例如,可能会使用版本号(v1、v2)或日期(2018-01-01-01)。CloudEvents 规范并没有规定要使用任何特定的模式,甚至根本不要求使用版本字符串。此决定权在每个事件制生产者手中。但是,当包含特定版本字符串时,应注意其值的变化,因为事件的消费者可能会依赖现有的值,因此变化可能会被解释为 “破坏性变化”。应该在生产者和消费者之间建立某种形式的沟通,以确保事件消费者知道可能使用的值。一般来说,所有 CloudEvents 属性也是如此。
id 属性是指在与一个事件源相关的所有事件中都是唯一的值(每个事件源都是由其 CloudEvents 的 source 属性值唯一标识的)。虽然所使用的确切值由生产者定义,但可以保证来自单一事件源的 CloudEvents 接收者不会有两个事件共享相同的 id 值。唯一的例外情况是,如果支持事件的某些重播,在这种情况下,可以使用 id 来检测。由于一个事件的发生可能会产生一个以上的事件,所以在所有这些事件都来自同一个事件源的情况下,每个 CloudEvent 构造的事件都有一个唯一的 id。以创建 DB 条目为例,可能会产生一个创建类型为 create 的 CloudEvent 和一个写类型为 write 的 CloudEvent。这些CloudEvents 中的每一个都有一个唯一的 id。如果希望这两个 CloudEvents 之间有一定的相关性,以表明它们都与同一事件相关,那么 CloudEvent 中的一些附加数据将用于此目的。在这方面,虽然事件生成者选择的确切值可能是一些随机字符串,或在其他语境中具有某种语义意义的字符串,但对于本CloudEvent 属性的目的来说,这些意义并不相关,因此,将id用于唯一性检查之外的其他目的不在本规范的范围内,也不建议使用。
为了实现既定目标,规范作者将试图限制他们在 CloudEvents 中定义的元数据属性的数量。为此,本项目所定义的属性将分为三类。
CloudEvents 规范特意避免过强的规定如何创建 CloudEvents。例如,它不假定原始事件源是为该事件的发生构造相关的 CloudEvents 的同一个实体。这样就可以有多种实现选择。但是,对于规范的实现者来说,了解规范作者的期望值是很有用的,因为这可能有助于确保互操作性和一致性。
如上所述,生成初始事件的实体是否是创建相应的 CloudEvent 的实体是一个实现选择。然而,当构造/填充 CloudEvents 属性的实体代表事件源行事时,这些属性的值是为了描述事件或事件源,而不是计算 CloudEvent 属性值的实体。换句话说,当事件源和 CloudEvents 生产者之间的分工对事件消费者没有实质性的意义时,规范定义的属性通常不会包含任何值来表示这种责任的分工。
这并不是说 CloudEvents 生产者不能为 CloudEvents 添加一些额外的属性,但这些属性不属于规范中互操作性定义的属性范围。这就类似于 HTTP 代理通常会尽量减少对传入消息中定义好的 HTTP 标头的更改,但它可能会添加一些包含代理特定元数据的附加标头。还值得注意的是,原始事件源和 CloudEvents 生产者之间的这种分离可以是小的或大的。这意味着,即使CloudEvents 生产者不属于原始事件源的生态系统的一部分,但如果它是代表事件源行事,并且它在事件流中的存在对事件消费者来说没有意义,那么上述指导仍然适用。当一个实体同时充当CloudEvents的接收方和发送方,以转发或转换入站事件为目的时,出站CloudEvent与入站CloudEvent的匹配程度将根据该实体的处理语义而有所不同。如果它作为代理,只是将 CloudEvents 转发到另一个事件消费者,那么出站 CloudEvent 通常与入站 CloudEvent 在规范定义的属性方面看起来与入站 CloudEvent 完全相同。但是,如果这个实体正在对CloudEvent进行某种类型的语义处理,通常会导致数据属性的值发生变化,那么它可能需要被视为一个与原始事件源不同的 “事件源”。因此,预计与事件生产者相关的 CloudEvents 属性(如源和id)将从传入的CloudEvent中更改。
CloudEvents 属性名称必须由 ASCII 字符集的小写字母(“a”到“z”)或数字(“0”到“9”)组成,并且必须以小写字母开头。属性名称应具有描述性和简洁性,长度不应超过 20 个字符。
术语定义
本规范定义如下术语:
符合本规范的每个 CloudEvent 必须包括根据需要指定的上下文属性,并且可以包括一个或多个可选的上下文属性。 参考示例:
specversion: 0.2
type: dev.knative.k8s.event
source: /apis/serving.knative.dev/v1alpha1/namespaces/default/routes/sls-cloudevent
id: 269345ff-7d0a-11e9-b1f1-00163f005e02
time: 2022-10-20T03:23:36Z
contenttype: application/json
CloudEvents 生产者可以在事件中包含其他上下文属性,这些属性可能用于与事件处理相关的辅助操作。
正如术语Data所定义的,CloudEvents 产生具体事件的内容信息封装在数据属性中。例如,KubernetesEventSource 所产生的 CloudEvent 的Data信息如下:
data:
{
"metadata": {
"name": "event-display.15a0a2b54007189b",
"namespace": "default",
"selfLink": "/api/v1/namespaces/default/events/event-display.15a0a2b54007189b",
"uid": "9195ff11-7b9b-11e9-b1f1-00163f005e02",
"resourceVersion": "18070551",
"creationTimestamp": "2022-10-20T07:39:30Z"
},
"involvedObject": {
"kind": "Route",
"namespace": "default",
"name": "event-display",
"uid": "31c68419-675b-11e9-a087-00163e08f3bc",
"apiVersion": "serving.knative.dev/v1alpha1",
"resourceVersion": "9242540"
},
"reason": "InternalError",
"message": "Operation cannot be fulfilled on clusteringresses.networking.internal.knative.dev \"route-31c68419-675b-11e9-a087-00163e08f3bc\": the object has been modified; please apply your changes to the latest version and try again",
"source": {
"component": "route-controller"
},
"firstTimestamp": "2022-10-20T07:39:30Z",
"lastTimestamp": "2022-10-20T07:10:51Z",
"count": 5636,
"type": "Warning",
"eventTime": null,
"reportingComponent": "",
"reportingInstance": ""
}
微众银行开源云原生事件驱动架构
https://github.com/apache/incubator-eventmesh/blob/master/README.zh-CN.md
阿里云开源事件中心
https://github.com/apache/rocketmq-eventbridge
第一,我们得有一个事件标准。刚才,我们也讲到事件是无期望的,它没有明确的消费者,所有都是潜在的消费者,所以,我们得规范化事件的定义,让所有人都能看得懂,一目了然。
第二,我们得有一个事件中心,事件中心里面有所有系统,注册上来的各种事件,(这个跟消息不一样,我们没有消息中心,因为消息一般是定向的,是生产者和消费者约定的,有点像计划经济,消息生产出来的时候,带着很强的目的性,是给谁谁消费的。而事件有点像市场经济)这个有点类似市场经济大卖场,玲琅满目,里面分类摆放了各种各样的事件,所有人即使不买,也都可以进来瞧一瞧,看一看,有哪些事件,可能是我需要的,那就可以买回去。
第三,我们得有一个事件格式,用来描述事件的具体内容。这相当于市场经济的一个买卖契约。生产者发送的事件格式是什么,得确定下来,不能总是变;消费者以什么格式接收事件也得确定下来,不然整个市场就乱套了。
第四,我们得给消费者一个,把投递事件到目标端的能力。并且投递前,可以对事件进行过滤和转换,让它可以适配目标端 API 接收参数的格式,我们把这个过程呢,统一叫做订阅规则。
第五,我们还得有一个存储事件的地方,就是最中间的事件总线。
它的协议也很简单,主要规范了 4 个必选字段:id,source、type、specversion;以及多个可选字段:subject、time、dataschema、datacontenttype和data。上图右边,我们有一个简单的例子,大家可以看下,这里就不具体展开了。 另外,事件的传输也需要定义一种协议,方便不同系统之间的沟通,默认支持三种 HTTP 的传输方式:Binary Content Mode、Structured Content Mode 和 Batched Content Mode。通过 HTTP 的 Content-Type,就可以区分这三种不同的模式。其中前两种,都是传递单个事件;第三种则是传递批量事件。
事件的 Schema,用来描述事件中有哪些属性、对应的含义、约束等等信息。目前我们选取了 Json Schema. 和 OpenAPI 3.0,根据事件的 Schema 描述,我们可以对事件进行合法性校验。,当然 Schema 本身的修改,也需要符合兼容性原则,这里不作具体展开。
关于事件的过滤和转换,参考阿里云开源的实现,它提供了 7 种事件过滤方式和 4 种事件转换方式,详细可以下图描述:
我们 RocketMQ 围绕事件驱动推出的产品,叫做 EventBridge,也是我们这次要开源的新产品。 他的整个架构可以分为两部分:上面是我们的控制面、下面是我们的数据面。
控制面中最上面的 EventSource 是各个系统注册上来的事件源,这些事件可以通过 APIGateway 发送事件到事件总线,也可以通过配置的 EventSource,生成 SouceRuner,主动从我们的系统中,去拉取事件。事件到达事件总线 EventBus 之后,我们就可以配置订阅规则了 EventRule,在规则 EventRule 里我们设置事件怎么过滤,以及投递到目标端前,做哪些转换。系统基于创建的规则会生成 TargetRunner,就可以将事件推送到指定的目标端。
那这里 SouceRuner 和 TargetRunner 是什么呢?我们具体能对接哪些上下游 Source 和 Target? 这些我们都可以在下面的 SourceRegister 和 TargetRegister 提前进行注册。 所以 EventBridge 的数据面是一个开放的架构,他定义了事件处理的SPI,底下可以有多种实现。
比如,我们把 RocketMQ 的 HTTPConnector 注册到 EventBridge 中,那我们就可以把事件推送到 HTTP 服务端。 如果我们把 Kafka 的 JDBC Connector 注册到 EventBridge 中,我们就可以把事件推送到数据库。 当然,如果你的系统不是通用的像 HTTP/JDPC 等协议,也可以开发自己的 Connector,这样就能将事件实时同步到 EventBridge,或则接收来自 EventBridge 的事件。 除此之外,我们还会有一些附加的运维能力,包括:
事件中心的核心是分布式消息中间件,目前业界主流的有 Kafka、RocketMQ、Pulsar
参考资料
https://mp.weixin.qq.com/s/fAw2wWvHM1JHtuG0OzDUDw
https://mp.weixin.qq.com/s/MCdVuaV7_dwJt0Ibe4sdHg
https://mp.weixin.qq.com/s/Ue_-0lym-G_PavSs6xe_gQ
https://zhuanlan.zhihu.com/p/158194645
https://www.npmjs.com/package/cloudevents-kafka
https://cloudevents.github.io/sdk-java/kafka.html