事件溯源|日志记录-一个基础的微服务模式

导语: 微服务架构目前是各互联网系统架构的首选,在使用微服务的过程中,调试一个分布式系统是一项具有挑战的任务, 事件溯源是一种非常好的方式来解决微服务可见性的一种手段。且看大名鼎鼎的couchbase如何使用事件溯源解决微服务的可见性问题。

正如我在之前的文章中提到那样,微服务是怎样失败的,调试一个分布式系统是一项具有挑战的任务。 许多东西可能是错的并且是不可控的, 例如网络的不稳定性,临时不可用或者是一些外部的BUG。

用一些工具监控网络能被快速解决(像Service Mesh ),你也可以使用一些额外的工具像OpenTracing 来做分布式的日记记录. 但是当我们谈到理解我们的实体状态时,并没有快速的即插即用的框架。

你的数据可能比你代码存活的时间还要长, 然而我们却忽略了我们的数据随着时间推移而产生的变化。 在大多数系统中,即使是简单的问题, 例如“该实体如何达到这种状态?” 或 “一个月前我的状态是怎样的?” 都无法回答,因为没有保存任何变更的历史记录。 持续追踪这些变更状态对一个系统的健康是极其重要的, 不仅是为了安全或者是调试目的, 而是出于巨大的商业价值(你的产品负责人会很高兴)

解决方案

通过事件溯源|事件记录为服务行为增加可见性是一种很好的方式。

一个比较好的办法是。这个有10年之久的基本概念是让一个应用的每一次变更都应该被记录在一个事件对象并且被有序存储。 

如果这听起来很熟悉,那可能是因为任何版本的控制系统或数据库事务日志都是这种模式的重度用户。

让我们来深入理解它是怎么工作的。 假设我们正在为一个电商网站构建一个订单服务(Order Service) , 让我们看一下我们的应用状态和事件看起来是怎样的:

许多作者对于事件的溯源和记录都定义了三个主要的规则: 

  • 事件总是不可变的; 

  • 事件总是那些过去已经发生过的事。 一些开发者错误指令(例如: PlaceOrder) 事件(ex: OrderPlaced)

  • 理论上, 在任意时间点,你可以删除你当前的状态并且通过重新处理所接收到全部消息来重新构建你的整个系统。

事件溯源| 事件记录流

  • 消息接器: 负责将传入的请求转换为事件并且校验他们; 

  • 事件存储: 负责有序存储这些事件并且通知监听器; 

  • 事件监听器:正如你可能猜到的, 它负责根据每一个事件类型来执行对应的业务逻辑

这种模式有很多种实现方式, 在Couchbase5.5中使用的“事件服务”就是这种模式的实现之一。 总而言之, 它允许你编写函数, 在一个文档被插入/更新/删除时来触发这些函数。 这个事件机制也能让你生成curl 请求,因此无论何时将给定的文档存储在数据库中,都可以在应用程序中触发一个endpoint来处理它。 让我们看一下它是如何使用事件的: 

如果你想了解更多关于它(Event Service) 资料, 阅读 couchbase eventing 官方文档。

Couchbase Eventing 是异步的, 所以上述套件实现仅适用于你的应用只接收异步调用的情况。 它也可以用来充当额外的安全层以触发通知, 例如,如果有人试图手动更新一个事件。

在某些系统中, 事件的字段和结构可能有很大的不同, 将这些事件存储在一个固定结构的RDBM 中是很难建模的, 出于这个原因, 开发人员通常将它他们用一个varchar类型的字段将这些事件存储为一个json字符串。这种办法存在一个主要的问题: 它使得事件查找变得困难,使你的大部分查询变慢, 变复杂并且充拆着大量的类似'likes'操作。 其中一种可有的解决方案是使用文档数据库, 因为它们大多数将文档存储为json并且具有用于查询它的类似SQL的语言, 如N1QL[1]。

快照-对你的状态进行版本控制

事件溯源世界中添加版本控制/历史记录被称为快照。 当你想要想要知道N天以前的状态是什么样的, 可以避免你重新处理所有的事件。 当你需要快速识别在某时间点时应用的状态与处理一个事件之后的所预期的状态之间的差异。 

快照具功能十分好用,成本低,易实现并且非常适合实时的上报。 如果你决定实现一个Event Sourcing, 可以在实现快照中多投入一点努力。 

修复不一致的问题

这部份是你的所有的努力得到回报的地方。 一旦你在这个地方有了事件溯源/记录并且具有快照功能, 你就可以使用Retroactive Event 模式的来修复不一致的情况。 

我总结一下, 如果你修复了一个BUG并且现在需要调整被影响实体的状态, 而不是手动更新他, 你可以将你的实体的状态设置为BUG之前的状态,并且从那个时刻重放所以与之关联的事件。 无须手动就会自动修改你的状态。 

  • 回滚状态: 回滚一个实体的到这个BUG之前的状态。 你能避免第一步和第二步重放所有的事件。  然而在这种情况下,我们正恢复以前的状态,因为我们希望避免重新处理整个过程。

  • 忽略快照: 所有恢复后的快照都应该标记为忽略,以避免将来恢复不一致的快照。

  • 重新构建事件: 从目标之后重建所有事件。

但是,如果事件中有错误数据或者从来没有被触发过,该怎么办?我们可以更新或删除事件并重新处理整个事件吗?

如果你还记得,事件溯源的第一条规则是“事件永远是不变的”,这是一个很好的理由;你需要相信你所看到的日志。但它不能回答我们的问题;只需略微修改一下:我们如何在不更改事件的情况下更改事件日志?

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

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

很酷,不是吗?但是这里有一个额外的棘手任务:我们如何构建一系列事件,而事件本身也可以在其中间插入其他事件?

一个笨办法是为每个实体添加一个浮点计数器。它会让你根据超任务(supertask)的理论在中间无限增加项(实际上,你受到float/double 最大的长度限制),这通常足以容纳所有必要的事件来修复你的状态:

当然,上面的方法有一些瑕疵, 但是它是一种简单的实现,易查询并且能大多数场上运行的很好。 如果你需构建一个更健壮结构,考虑在一个链表结构中存储你的事件:

关于外部系统|其他微服务?

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

不幸的事,由于可能的情况多种多样, 这里没有银弹来解决这个问题, 并且每一个案例不得不单独处理。 下面给出一些普遍的解决方案:

  • 临时修改配置禁止发送任务外部消息或者添加一个拦截器允许你配置哪些消息需要发送; 

  • 重新路由指定的请求到一个假的服务(如果你正在使用的服务网络模式就是一个典型的场景)

  • 使其他服务能够识别出一个给定的操作已经在过去以相同的参数执行了,而不是抛出一个错误,服务需要像以前一样返回相同的成功消息。

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

事件溯源的优点

 尽管它是一个简单的模式,但使用它有很多优点:

  • 事件日志具有很高的商业价值;

  • 它在DDD和事件驱动架构下运行得非常好。

  • 调试用应用程序状态中所有变更的来源;

  • 它允许您重放失败的事件;

  • 易于调试,您可以将目标实体的所有事件复制到您的机器并调试每个事件,以了解应用程序如何达到特定状态(忽略从生产环境复制数据的安全隐患);

  • 允许您使用追溯事件模式重建/修复您的状态。

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

事件溯源的缺点

  • 在同步调用中不太直观,因为需要首先将请求转换为事件。

  • 无论何时部署重大更新,如果您想要向后兼容(也称为“事件升级”),你将被迫迁移事件历史记录。

  • 某些实现可能需要额外的工作来检查最新事件的状态,以确保所有事件都已被处理。

  • 事件可能包含私有数据,所以不要忘记确保事件日志得到适当保护。

结论

我已经展示了稍微修改过的事件溯源/事件记录模式,这在过去几年一直很适合我。我第一次听说这种方法是近10年前在Marting Fowler博客文章(必读)中。从那以后,它为我提供了很多帮助,使我的微服务的状态几乎牢不可破。

然而,这种方法也不该在你的所有服务中不分青红皂白地使用。我个人认为只有核心价值才是真正值得的。例如,您可能不需要保留用户在系统中更改自己的姓名的所有时间的历史记录。

如果您有任何问题,请随时在@deniswsrosa推特给我

参考链接

[1] https://query-tutorial.couchbase.com/tutorial/#1

猜你喜欢

转载自blog.csdn.net/zl1zl2zl3/article/details/84647415