我发现人们可能会混淆什么是事件代理,什么是消息代理。事件代理可以用来代替消息代理,但消息代理不能完全实现事件代理的功能。下面深入比较一下它们。
消息代理拥有悠久的历史并已被许多组织用于大型的面向消息中间件架构。消息代理使得系统可以通过发布/订阅消息队列进行网络通信。生产者将消息写入队列,而消费者会消费这些消息并进行相应的处理。然后,消息在消费时得到应答,并立即或在之后不久被删除。消息代理被设计为处理与事件代理不同类型的问题。
事件代理是围绕提供有序的事实日志而设计的。事件代理满足消息代理无法满足的两个非常具体的需求。首先,消息代理只提供消息的队列,消息的消费是基于每个队列进行处理的。共同消费一个队列的应用程序只能接收到记录的一个子集。这就无法通过事件来正确地传递状态,因为每个消费者都无法获得所有事件的完整副本。与消息代理不同,事件代理维护着一个单独的记录总账,并通过索引管理每个单独的访问,因此每个单独的消费者能够访问所有必需的事件。此外,消息代理会在应答之后删除事件,而事件代理会根据组织的需要保留它们。消费后删除事件使得消息代理不足以向所有应用程序提供无限期存储、全局可访问、可重放和单一的事实来源。
事件代理支持一个不可变的追加日志,以保留事件顺序的状态。消费者可以在任何时候从日志中的任何位置提取数据并进行重新处理。此模式对于启用事件驱动型微服务是必不可少的,但消息代理无法提供。
请记住,消息代理中使用的队列在事件驱动型微服务中仍然扮演着一定的角色。队列提供了有用的访问模式,但在使用严格分区的事件流时可能难以实现。对于事件驱动型微服务架构来说,消息代理系统引入的这些模式当然有效,但它们不足以实现此架构所需的全部职责。本书剩余部分不会聚焦于消息代理架构或应用程序设计,而会聚焦于事件驱动型微服务架构中事件代理的使用。
从不可变日志中消费
虽然不是一个明确的标准,但通常可用的事件代理使用一种只追加的不可变日志。事件被追加到日志尾部并分配一个自增的索引 ID。数据消费者使用索引 ID 的引用来访问数据。然后,可以依据业务需要和事件代理能够提供的功能,以事件流或队列的形式消费事件。
01. 以事件流形式消费
每个消费者负责更新自己的指针,该指针指向事件流中之前读取数据的索引。这个索引称为偏移量,是从事件流开始处到当前事件的度量。偏移量允许多个消费者相互独立地消费并跟踪它们的进度,如图 2-6 所示。
消费者组允许将多个消费者视为同一个逻辑实体,并可用于消息消费的横向扩展。新的消费者加入消费者组中会导致事件流分区指派的重新分配。新的消费者仅从分配给它的分区中消费事件,就像组中之前的旧消费者实例仅从分配给它们的分区中消费事件一样。通过这种方式,在同一个消费者组内可以平衡事件的消费,同时确保给定分区的所有事件是由单一的消费者实例独占消费的。在消费者组内激活的消费者实例数量受限于事件流中的分区数量。
02. 以队列形式消费
在基于队列的消费中,每个事件仅被一个微服务实例所消费。一旦被消费,该事件就被事件代理标记为“已消费”并不再提供给任何其他消费者。当以队列形式消费时,消费者数量与分区数量就变得没有耦合性,因为任意数量的消费者实例都能用于消费。
当以队列形式进行处理时,事件顺序是无法保证的。并行的消费者无序地消费和处理事件,同时单个消费者可能在处理一个事件时失败,进而将其放回队列以待后面再处理,然后就继续消费接下来的事件了。
不是所有的事件代理都支持队列。例如 Apache Pulsar 目前支持队列,Apache Kafka 则不支持。图 2-7 展示了使用单独的偏移量应答的队列的实现。
提供单一事实来源
持久和不可变的日志为单一事实来源提供了存储机制,事件代理成了服务消费和生产数据的唯一位置。这样,每个消费者都能获得一份完全相同的数据副本
采用事件代理作为单一事实来源需要组织进行一次文化转变。之前团队可能只需编写直接的 SQL 查询语句来访问单体数据库中的数据,而现在团队还必须把单体数据发布到事件代理上。管理单体数据库的开发者必须确保生成的数据是完全正确的,因为事件流和单体数据库之间的任何不一致都会被认为是生产团队的事故。数据消费者不再直接耦合于单体数据库,而是从事件流中进行消费。
采用事件驱动型微服务可以创建只使用事件代理进行存储和访问数据的微服务。虽然微服务的业务逻辑肯定会使用事件的本地副本,但事件代理仍然是所有数据的唯一事实来源。