Event-Driven Architecture思考
什么是Event?
An event represents a fact, something happened; and it is immutab.
事件代表着事实,代表着过去发生的某件事情,是不可变的。
既然事件代表着在过去某一时刻发生的某个事情,那么那必然具备一些基本的要素,就像现实生活中发生某件事情也具备时间、地点等几个要素。事件的基本要素包含time、source、key、header、metadata和payload。
Event vs Message
日常开发中我们接触到的和事件最接近的应该是消息,这两者也比较容易混淆,难以说清楚它们的界限:什么是事件,而什么是消息?
A message is an item of data that is sent to a specific destination. An event is a signal emitted by a component upon reaching a given state.
上面是我觉得的解释是比较好的,消息是发送到特定目标的数据项,而事件是标识某个组件达到了某个状态。消息更关注于行为,即“要做一件什么事情”——要把某一条消息发送到服务端;而事件关注于事实,即“发生了什么事情”。
消息没有特定的意图(no special intent),可以承载任何数据,那么也可以用消息来承载事件,所以消息是事件的超集(事件可以认为是一类加上了一些特定限制的消息)。
Event vs Command
还有一类日常中使用的远程调用的方式Command,通常我们执行一个RPC调用都是执行一个Command。Command和Event的区别在于Command着重于要做什么,用于传递一个要执行某个动作的请求。
因为Command具有以下特性:
- 意味着行为即将发生,但是还没发生
- 可能被拒绝:可能被拒绝执行,或者因为某些原因无法执行
- 有明确的源(发起者)和目标(执行者)
结合和Message及Command的差异,总结一下Event具备的特征:
- 代表着已经发生的事情->已经发生的事情不可改变
- 事件有特定的一些属性,表明特定的含义,事件是消息的子集
- 事件有明确的源,但没有明确的目标
CloudEvents
因为事件无处不在,DB中的数据发生了一次变更是一个事件,几台机器宕机了也是一个事件,并且每个发布者对事件的描述是不一致的,导致难以在一个大规模的范围内使用事件。
- 没有统一的标准去描述事件意味着开发者需要为每一个事件源编写逻辑
- 没有统一的标准去描述事件意味着没有通用的类库、工具、基础设置来支持事件的处理、分发
- 没有统一的标准去描述事件意味无法进行移植,可能无法跨平台的去使用
其实在每个特定的系统中我们都会制定事件的标准来解决这个问题,比如CDC(Change Data Capture)的场景中,类似Canal(https://github.com/alibaba/canal)这样的产品都会将DB中的变更事件封装成特定的Java对象,而这个对象实际就是事件的标准。面对这个问题,CNCF(Cloud Native Computing Foundation)站在更高的角度来抽象事件,而不是局限在特定的系统或者领域,制定了CloudEvents标准。
每个符合规范的CloudEvent都需要包含必须(REQUIRED)的属性,并且可以包括多个非必须(OPTIONAL)的属性。这些属性描述了事件,并且独立于事件的数据进行序列化,这样就可以在不必进行事件数据反序列化的情况下对事件进行检查。
REQUIRED Attributes
- id: String类型,标识Event,必须保证Source+ID能唯一确定一个Event
- source:URI-reference类型,事件发生的“源”
- specversion:String类型,标识事件使用的CloudEvents Spec版本
- type:String类型,描述事件的类型
OPTIONAL Attributes
- datacontenttype:String类型,Event内容的类型,例如"application/xml"
- dataschema:URI类型,指明Event内容的Schema信息
- subject:String类型,Event的Subject信息
- time:Timestamp类型,事件发生的时间
Example
{ "specversion" : "1.0-rc1", "type" : "com.github.pull.create", "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" : "<much wow=\"xml\"/>" }
什么是Event-Driven?
在讨论Event-Driven之前需要弄清楚Event-Driven的概念,这里就需要理清楚Event-Driven和Request-Driven的关系。
上面这张图来源于《Build Services on a Backbone of Events》一文,比较清楚的描述了Request-Driven和Event的区别:
- Request-Driven包含Command和Query两种,Command意为执行命令,会导致状态的变更;而Query仅查询数据,不会导致状态变更。Request-Driven是希望执行某段业务逻辑。
- Event-Driven则是事件驱动,事件在状态变更后触发,是业务逻辑执行完后导致的状态变更的通知。
日常我们使用的RPC服务都可以理解为是Request-Driven,都是请求执行某个命令;而日常使用的消息中间件都是Event-Driven。如果由Event来驱动执行逻辑,那么称为Event-Driven的应用。一个应用往往即会处理RPC请求,也会订阅消息,那么它有一部分是Request-Driven的,有一部分是Event-Driven的(除非是Function或者是特定领域的简单的应用——比如做消息的转换,很少会有一个纯粹的Event-Driven的应用)。没有必要过分纠结一个应用是否是纯粹的Event-Driven的,更重要的是理解Event-Driven的思想,将它融入到架构的思想中来把业务系统做的更好。
什么是Event-Driven Architecture?
Event-Driven Architecture是一种用于构建可扩展的分布式异步处理模式,由高度解耦的、单一职责的事件处理器组成。
Event-Driven Architecture模式有两种主要的结构:Mediator Topology和Broker Topology。Mediator Topology多用于需要有多个编排步骤的事件处理,而Broker Topology用于链式的事件处理。
Mediator Topology
Mediator Topology用于Event需要多个步骤进行处理,且需要一些编排能力的场景。比如一个Event进来之后,系统需要决定处理的顺序,以及其中哪些步骤可以并发的执行。Mediator Topology中有四个核心的组件:event -queue、event-mediator、event-channel、event-processor。整个流程通过客户端发送一个Event到event-queue开始,然后由event-queue将Event传输到event-mediator,event-mediator收到初始的Event之后通过异步的发送额外的Event到event-channel来完成编排,event-processor从event-channel接收Event并执行特定的业务逻辑来完成处理。
在Event-Driven Architecture中会有成百上千Queue,Event-Driven Architecture并不绑定Queue的实现,它可以由MQ实现,也可以由其他组件实现。
在Mediator模式下有两类Event,一类是initial event,一类是processing event。initial event是外部输入的初始Event,processing event是由Mediator产生的,由Event Processor处理的Event。
event-mediator的职责是编排initial event,对于initial event的每个处理步骤event-mediator发送一个processing event给event-channel,event-mediator并不执行任何真正的业务逻辑。
event-channel用于event-mediator将Event异步的投递给event-processor处理,event-channel可以使用message queue或者message topic实现。message topic是更常用的,因为event可以被投递给多个event processor处理。
event-processor包含了用于处理processing event的业务逻辑。event-processor是应用中用于执行特定任务的、自包含的、独立的、高度解耦的组件。明确event-processor是单一职责的非常重要,event-processor执行任何一个任务不应该依赖于其他event-processor的处理。
上图是用户通过保险公司投保并修改地址的例子。首先event-mediator收到初始事件,然后执行change address操作(发送Processing Event: change address event给ConsumerProcessor处理),再之后并发的执行RecalcQuote和UpdateClaims事件,接着执行AdjustClaims调整报价,最后执行Insured的通知。
Broker Topology
区别于Mediator Topology,Broker Topology没有中心式的event-mediator,Event在event-processor之间以一种链式的结构流转,在事件流相对简单,不需要集中编排的场景下这种模式是非常适用的。
在Broker Topology下只有两个核心的组件:broker和event-processor。Broker可以是中心式的,也可以是分布式的,包含事件流中所需要的所有event-channel。Broker中的event-channel可以是message queue或者message topic,也可以是它们的组合。
Broker Topology结构如上图所示,在图中没有一个event-mediator来编排event,仅有event-processor来处理event并在处理完成之后产生新的event来表示它刚刚执行的操作。
同样以Mediator中的例子来看的话,在Broker模式下的处理流程如下:
其中ChangeAddress的Processor直接处理Event,处理完毕后产生一个ChangeAddress完成的事件,然后由QuoteProcess和ClaimsProcess订阅这个事件并执行各自的处理逻辑(这一步是并发的),之后AdjustProcess会订阅UpdateClaims的Event,而NotificationProcess会同时订阅RecalcQuote和UpdateClaims的Event。
总结
Event-Driven Architecture在某些方面是具有天然优势的,在另一些方面则对现行的开发模式增加了负担,比如:
- 敏捷度:EDA模式中event-processor都是解耦的且单一职责的,所以再进行变更时会非常容易,能够快速的变更完成业务,所以有较好的敏捷性。
- 易于部署:EDA模式是非常容易部署的,特别是Broker的模式,因为各个组件是相互独立的。
- 可测试性:EDA模式的测试相对于REQUEST驱动的模式会复杂很多,因为需要构建各个Event并且都是异步的。
- 性能:EDA模式各个组件独立解耦,且异步执行,这大大提升了系统的性能。
- 可伸缩性:EDA进行了组件的解耦,所以天然的具备的非常好的伸缩性,可以根据各个组件的性能执行不同的伸缩策略。
- 易于开发:EDA模式下,比如Broker模式每个event-processor只需要处理自己关注的事件,并决定是否产出一个新的事件,逻辑开发是简单的,但是如何将event-processor组合起来完成整个业务是相对复杂的。
Event-Driven Architecture是相对复杂的架构,因为它天然是异步的、分布式的。当选择采用Event-Driven来处理业务逻辑时,如何划分event-processor是非常重要的,因为event-processor是独立的,需要避免将带有事务语义的逻辑拆分到多个event-processor中。统一事件标准也是非常重要的,因为event-processor会随着业务的变化而不断的增长,使用统一的标准将降低event-processor之间的交互及新event-processor的接入成本。