事件来源/事件记录——一种基本的微服务模式


正如我在之前的帖子中提到的how to fail with microservices调试分布式系统是一项具有挑战性的任务。许多事情都可能出错,其中一些是我们无法控制的,例如网络不稳定、暂时不可用,甚至是外部错误。

监控网络可以通过大量工具快速解决,例如Service Mesh例如,您还可以使用工具,如OpenTracing用于分布式日志记录。然而,当我们谈论理解我们实体的状态时,没有快速的即插即用框架。

您的数据可能会比您的代码更长寿,但是我们忽略了我们的数据是如何随着时间而演变的。在大多数系统中,即使是简单的问题,比如“这个实体是如何达到这种状态的?”或者“一个月前我的状态如何?”无法回答,因为没有保存任何变化历史。跟踪这些变化对于一个健康的系统是至关重要的,不仅仅是出于安全或调试的目的,还因为它的巨大商业价值(你的产品所有者会很高兴)。

解决方案

通过事件来源|事件日志记录,可以更好地了解您的服务发生了什么。这个有10年历史的模式背后的基本概念是,应用程序状态的每个变化都应该封装在一个事件对象中,并按顺序存储。如果这听起来很熟悉,那可能是因为任何版本的控制系统或数据库事务日志都是这种模式的大量用户。

但是让我们更深入地了解它是如何工作的。假设我们正在为电子商务构建一个订单服务,让我们看看我们的应用程序状态和事件是什么样子的:

许多作者为事件来源/记录系统定义了三个主要规则:

  • 事件总是不可改变的;
  • 事件总是发生在过去。一些开发人员将命令(例如:放置顺序)与事件(例如:放置顺序)相混淆
  • 理论上,在任何时间点,你都可以丢弃你当前的状态,通过重新处理收到的所有消息来重建整个系统。

这种模式的另一个优点是,它促使你在考虑实际结构是什么样子之前,先考虑系统的事件。起初,这可能是反直觉的,因为我们已经学会了如何通过绘制实体和属性来设计系统,但这与另一个常见的DDD建议非常一致:首先考虑您的服务将如何相互通信,以便轻松识别

事件来源|事件日志记录流程

事件来源最常见的流程如下:


  • 消息接收器负责将传入的请求转换为事件并对其进行验证。
  • 事件存储负责按顺序存储事件并通知侦听器。
  • 事件侦听器:正如您可能猜到的,这是负责根据每个事件类型执行相应业务逻辑的代码。

这种模式有许多可能的实现,其中之一是使用Eventing Service,在Couchbase 5.5中引入。总之,它允许您编写在插入/更新/删除文档时触发的函数。事件机制还允许您发出请求,因此无论何时给定的文档存储在数据库中,您都可以在应用程序中触发一个端点来处理它。让我们看看使用event会是什么样子:

如果你想了解更多,请查看Couchbase eventing official documentation

Couchbase Eventing是异步的,因此只有当您的应用程序只接收异步调用时,上面的实现才适合您。它还可以作为触发通知的额外安全层,例如,如果有人试图手动更新事件。

在某些系统中,事件的字段和结构可能彼此差异很大,而RDBM的固定结构使得事件存储很难建模。因此,开发人员通常将其事件存储为JSON字符串变成一个可变长字符串字段。这种方法有一个主要问题:它使你的事件很难找到,因为你的大多数查询将是缓慢的,复杂的和充满“喜欢”的。其中一个可能的解决方案是使用文档数据库,因为它们中的大多数都将文档存储为JSON,并使用适当的类似于SQL的语言进行查询,例如N1QL

快照—根据您的状态进行版本控制

在事件源世界中,将版本/历史添加到您的状态有时被称为“快照”。当你需要知道你N天前的状态时,避免重新处理所有事件是很重要的。它还有助于调试,因为您可以快速识别应用程序状态与处理事件后预期状态不同的时间点。

快照是有用的、廉价的、易于实现的,并且非常适合时态报告。如果您已经决定实现事件源,那就多花点力气来实现快照。

修复不一致

这里通常是你所有努力都有回报的地方。一旦您有了事件源/日志和快照,您就可以使用稍微修改过的版本Retroactive Event修复不一致的模式。

总结一下,如果你已经修复了一个bug,现在还需要调整受影响实体的状态,而不是手动更新它,你可以将你的实体的状态设置为bug之前的状态,并重放自那以后所有相关的事件。这将自动纠正您的状态指南干预。


  • 回滚状态:将一个实体的状态回滚到bug之前的状态。你可以避免步骤12通过重播所有事件。然而,在这种情况下,我们正在恢复以前的状态,因为我们希望避免重新处理整个事情。
  • 忽略快照:恢复后的所有快照都应标记为,以避免将来恢复不一致的快照。
  • 重建事件:从目标开始重建所有事件。

但是,如果事件中有错误的数据或者根本就不应该被触发呢?我们可以更新或删除事件并重新处理整个事件吗?

如果你还记得,第一个规则的事件来源是”事件总是不可改变的“这是一个很好的理由;你需要相信你看到的日志。但是它没有回答我们的问题;稍微修改一下:我们如何在不改变事件的情况下改变事件日志?

解决这个问题的一个简单方法是将事件标记为可忽略,这样我们就可以在重建过程中忽略它们:

如果事件是由错误的数据或错误的顺序触发的,该怎么办?使用这种方法,我们所要做的就是将事件标记为可忽略的,并在正确的位置添加一个具有正确值的新事件,如下所示:

很酷,不是吗?但是这里有一个额外的棘手任务:我们如何建立一个事件序列,允许你在中间添加事件?

一个简单的解决方案是为每个实体添加一个浮点计数器。它会让你根据理论无限地在中间添加物品supertasks(实际上,您受到float/double max大小的限制),这通常足以添加所有必要的事件来修复您的状态:

当然,上面的方法有很多缺陷,但是它实现起来简单得可笑,容易查询,并且在大多数情况下工作得很好。如果您需要构建更健壮的结构,请考虑将您的事件存储在链表结构中:

外部系统如何|其他微服务?

微服务不是一个孤岛,所以有理由认为重放事件的副作用之一是您的服务可能会向外部服务发送消息。这些消息可能会触发不一致或在其他系统中传播错误,这可能会使情况比以前更糟。

不幸的是,由于各种各样的可能性,没有解决这个问题的灵丹妙药,每个案件都必须单独处理。一些常规解决方案是:

  • 暂时更改配置以不发送任何外部消息,或者添加一个拦截器以允许您配置需要发送哪些消息;
  • 将特定请求重新路由到假服务(如果使用服务网格模式,这是一个典型的场景)
  • 使其他服务能够识别给定的操作在过去已经用相同的参数执行过,然后,不抛出错误,只返回与以前相同的成功消息。

当然,有相当多的情况下,您将无法自动修复外部不一致,在这种情况下,预计其他系统会打印出人类可读的错误和/或触发通知,要求有人干预。

活动采购的优势

尽管这是一个简单的模式,但是使用它有很多好处:

  • 事件日志具有很高的商业价值;
  • 它非常适合DDD和事件驱动架构。
  • 试听你的申请状态的所有变化的来源;
  • 它允许您重放失败的事件;
  • 易于调试,因为您可以将目标实体的所有事件复制到您的机器上,并调试每个事件以了解应用程序是如何达到特定状态的(忽略从生产中复制数据的安全影响);
  • 允许您使用Retroactive Event模式来重建/修复您的状态。

许多作者还将进行时态查询的能力作为一个优势,但是我认为查询多个后续事件不是一个简单的任务。因此,我通常认为时态查询是快照模式的一个优势。

事件来源的缺点

  • 在同步调用中工作有点不直观,因为您需要首先将请求转换为事件。
  • 每当您部署一个突破性的变更时,如果您想要向后兼容,您将被迫迁移您的事件历史(也称为事件升级)。
  • 一些实现可能需要额外的工作来检查最新事件的状态,以确保所有事件都已被处理。
  • 事件可能包含私有数据,所以不要忘记确保您的事件日志是适当安全的。

结论

我展示了事件源/事件日志模式的一个稍加修改的版本,它在过去几年里一直对我很有效。我第一次听说这件事是在将近10年前famous Martin Fowler blog post(必须阅读)。从那以后,它帮助我使我的微服务状态几乎牢不可破,更不用说所有的报告功能了。

然而,在你的所有服务中,这不是一个可以任意使用的东西。我个人认为只有核心部分才是真正值得的。例如,您可能不需要保存用户在系统中更改自己名字的所有时间的历史。

如果你有任何问题,请随时发推给我@deniswsrosa