如何从单体架构迁移为事件驱动的架构

单体架构

在现代软件架构兴起之前,应用程序通常作为单一的部署单元构建,这类部署单元被称作单体应用。单体应用一般从一个简单的核心起步,随后逐步发展得愈发复杂。这一过程中,技术债务不断累积,最终导致其扩展与维护成本越来越高。或许你正面临这样的状况,并且可能在考虑从单体架构向分布式、基于微服务和事件驱动的架构转变。

单体架构

尽管单体架构具备一定优势,但单体应用内各功能间的紧密耦合,最终会对吞吐量性能以及(或)功能的创建与维护形成阻碍。倘若你的应用程序需要在吞吐量或业务范围上进行扩展,或许就需要一种不同的架构,这种架构能够打破那些妨碍这两方面扩展的功能间的紧密依赖关系。你可能需要一种基于事件的架构。

基于事件的架构

在基于事件的架构里,支撑整个系统的关键信息是“事件”。

事件是一个时间点,标志着某件事情已经发生。尽管可能存在与该事件相关联的数据,用以描述所发生的情况(即事件元数据),但事件本身才是核心要点。

从历史事实层面看,事件具有不可变性。它们是对发生了何事以及何时发生的记录,无法更改。

那么,事件是如何解决我们在扩展和维护方面所面临的问题的呢?由于事件不可变,在生成后的任何时刻都能被处理。这意味着生成事件的系统(“生产者”)在创建事件时,无需知晓其他哪个系统(“消费者”)会处理该事件,也不必知道何时会处理。它同样不关心有多少个消费者,以及这些消费者将对该事件进行何种处理。这就表明,事件生产者和消费者之间是完全解耦的。

基于事件的架构

从上述图表中可以看到,事件生产者创建事件并将其添加到队列中。随后,它的任务便完成了,且不会再对该事件予以更多关注。

消费者解耦

另外,从上面的图表能够看出,有三个消费者在对该事件进行消费。要注意的是,当一个消费者消费某个事件时,该事件不会被删除,而是会留在队列中,以供其他消费者继续消费。每个消费者会依次处理这些事件(关于这一点,后续会有更详细的介绍)。

需留意,虽然从概念上讲事件不会被删除,但考虑到资源成本,旧事件会被清理。确定事件可访问的时长,这属于解决方案设计方面的决策范畴。

由于事件保留在队列中,任何一个准备好的消费者都能够去消费它们。这意味着,不仅生产者与消费者之间实现了解耦,消费者彼此之间也相互解耦了。

需注意,基于事件的架构并不禁止在需要即时响应的情形下,各个功能通过同步应用程序编程接口(API)相互访问。不过,这样做会形成很强的依赖关系,所以必须仔细斟酌对架构造成的影响。

消费者扩展

除了让不同消费者以不同方式处理事件,我们还能够对每个消费者自身进行扩展。

消费者扩展

假设消费者是无状态的,那么可以添加更多消费者副本用于消费这些事件。它们轮流从队列中取出事件进行处理,并且其表现就如同它们是一个单一消费者一样。如此一来,这些事件便会被并行处理,这可能导致事件被乱序处理。与单体架构中必须对所有功能都进行扩展不同,现在我们能够决定仅对那些有扩展需求的功能进行扩展。

功能扩展

在单体应用中,我们面临的另一个难题是,添加或更改功能变得极为困难。即便我们设置了自动化回归测试,始终存在一个问题,即我们是否会破坏某些功能。

功能扩展

基于事件的架构再度为我们解决了这个问题。例如,在上面的图表中,我们可以对功能3进行更改,因为我们清楚这不会对其他任何功能造成影响。此外,我们添加了功能5,同样可以放心,这不会影响任何现有的功能。所以现在我们已经打破了依赖关系,并且能够依据需求对应用程序的某些部分进行扩展。接下来,让我们看看它在实际中是如何运作的。

事件代理

在探讨是否需要基于事件的架构之前,我们首先应该谈谈事件代理及其在发布/订阅模型中的作用。我之前提到的事件队列,实际上是一个名为事件代理(或简称为代理)的组件。生产者将事件发送给这个代理,代理会对事件进行持久化存储,而消费者则在必要时从代理处读取这些事件。

事件代理

在实际情况中,可能会有众多生产者创建(或“发布”)许多不同类型的事件。如果一个消费者仅对一两种特定类型的事件感兴趣,却要读取所有事件,这效率会非常低。为了仅获取它需要处理的事件,每个消费者会告知代理它感兴趣的事件类型,这被称作订阅事件类型。之后,代理只会将那些类型的事件发送给该消费者。这就是所谓的发布 - 订阅架构,有时也简称为“发布/订阅(pub/sub)”。有一点需要明白,代理会维护一个“已到此处”的检查点,这个检查点会告知代理消费者已经处理到了什么位置。这样,当消费者请求获取更多事件时,代理就可以只发送新的事件。这(几乎)就是它保证事件按顺序处理的方式。不过,根据代理的配置情况,故障可能会扰乱事件处理的流程。

那么,你需要基于事件的架构吗?

如今我们已经明晰基于发布/订阅的事件架构究竟是什么,接下来就可以探讨一下,你是否真的需要这样一种架构。

为什么我不想要基于事件的架构呢?

乍看之下,基于事件的架构似乎就如同人们常说的“万灵药”,能够解决我们面临的所有问题。所有组件都实现了解耦,一切都具备可扩展性,而且各个部分都便于维护。然而在实际应用中,它可能并非我们所想象的那般万能。

  • 复杂性:在架构中引入代理,就额外增添了一个需要进行配置与维护的组件。
  • 可靠性:代理在系统中也引入了一个关键的故障点。一旦它发生故障,整个系统都可能陷入离线状态。
  • 隐藏的依赖关系:尽管看似所有组件都已解耦,不存在依赖关系,但实际并非如此。倘若生产者停止发布某个事件,转而发布另一个事件,或者与该事件相关联的元数据发生改变,那么很可能不得不对所有其他功能组件进行更改。
  • 数据一致性:由于系统的不同部分以不同速率处理事件,系统的某些部分可能会出现不一致的情况。只有当事件停止时,系统才会“最终一致”,但在实际场景中,这种情况可能永远不会出现。
  • 异步响应:如果系统的某些部分需要同步响应(例如,对当下必须执行的操作做出回应),那么它可能需要被阻塞,进而在相当长的一段时间内占用资源。
  • 查询数据:数据不一致不仅会在跨不同功能查询数据时引发问题,而且跨多个数据库进行查询也会带来极大的挑战。
  • 调试:当出现问题时,想要查明究竟发生了什么以及原因,也会变得更加困难。虽然可以重放事件序列,但竞争条件可能会使得重现问题异常艰难。
  • 测试:如今测试系统,可能需要同时启动所有功能,包括事件代理。这可能是一项极为艰巨的任务。
  • 反模式:人们利用事件驱动架构构建的一些解决方案,实际上会削弱使用该架构所带来的优势。这些被称为“反模式”。
  • 事件排序:从表面上看,消费者似乎能够直接消费事件,无需担忧事件顺序错乱、事件丢失以及性能问题。但实际上,在软件设计中需要全面考虑所有这些因素。
  • 数据重复:每个功能都有其独特的数据视角,一旦这个视角出现偏差,可能需要耗费大量精力来修复所有功能中的数据问题。
  • 安全漏洞:由于所有业务数据如今都在一个单一系统中传输,这就使得它容易遭受攻击。

上述这些挑战,大多数通过单体架构都能够解决。单体架构之所以得以发展,是因为相对而言,与基于事件架构的一组分布式微服务相比,它的开发过程要简单得多。这意味着开发单体架构更快,成本也更低。对于一些情况(比如初创公司),若不需要大规模处理业务,且问题领域相对简单,那么单体架构或许是更好的选择。

为什么我会考虑采用基于事件的架构呢?

鉴于基于事件的架构存在着诸多挑战,你可能会疑惑,为什么还有人会选择它呢?既然它被广泛应用,那必然存在一些极具说服力的理由,促使人们采用它,不是吗?简而言之,在处理量和功能集方面,它在可扩展性上的优势极为显著。实际上,这些优势如此突出,以至于超越了应对相关挑战所付出的成本。而且,大多数挑战都是可以克服的:

  • 复杂性:可采用经过实践检验的技术解决方案,例如 Apache Kafka 。
  • 可靠性:选用高可用性且经过实践检验的技术解决方案,比如 Apache Kafka 。
  • 隐藏的依赖关系:在事件设计中引入版本控制理念,并实现向后兼容一到两个版本(n-1n-2)。
  • 数据一致性:先判断这是否构成问题,若构成问题,可采用同步解决方案,如应用程序编程接口(API)。
  • 异步响应:与上述情况类似,若这是个问题,采用同步解决方案。
  • 查询数据:出于特定目的,考虑使用数据仓库,它能够接收所有事件,并为系统构建一个一致的当前状态。若在功能内部需要数据,可考虑在其自身数据库中构建用于查询所需的数据,但要尽量避免数据的杂乱分布。
  • 调试:采用专门为监控和管理基于事件的分布式系统而设计的工具,例如 ZipkinJaeger
  • 测试:创建一个框架,使你能够在系统的一个部分内开展测试工作。
  • 反模式:深入了解反模式,并直接予以规避。
  • 事件排序:在微服务中构建一个框架,用于管理顺序错乱和重复的消息。依据业务需求做出合理的设计选择。
  • 数据重复:考虑采用用于重新同步分布式数据存储的机制,并开发用于修复数据的工具。
  • 安全漏洞:确保代理能够对所有事件实施所需级别的访问控制,涵盖正在产生的事件、正在被消费的事件以及存储在持久化存储中的事件。

别误解我的意思,解决上述每一个问题,都需要组织投入巨大的努力。但是,如果你的解决方案需要借助基于事件架构的优势,那么在时间和资金上的投入是完全值得的。

单体架构迁移的挑战

假设你拥有一个单体应用,且正面临扩展性与维护方面的难题。你或许会琢磨,将其拆分为微服务,并添加一个事件代理,借此获取基于事件架构的优势,这或许值得一试。倘若你处于这样的阶段,务必留意,你需要通过周全的规划与设计,考量诸多因素。

1. 拆分单体应用

里克·佩奇(Rick Page)曾写道:“不能把期待当作一种策略”。同样,只是简单地拆分单体应用,再用事件代理将其重新组合起来,就指望它能正常运作,这并非可行的策略。如此操作只会让你承受前文提及的所有挑战,却几乎无法收获基于事件架构的优势。你所打造的会是一个“分布式单体应用”,这种架构很难实现扩展,维护起来更是难如登天。

2. 异步世界中的同步性

拆分同步的单体应用时,人们很自然地会寻求一些方案,试图在基于异步事件的架构中重现那种同步性。但这是典型的反模式。当某些流程发出事件,随后在继续执行前等待响应,你就会意识到这属于反模式。这实际上会使你的微服务相互耦合,几乎瞬间就抵消了基于事件架构的优势。

3. 无处不在的数据重复

在单体应用体系里,每个功能都能访问所有数据。尽管内部设计可能借助服务层将不同业务实体分隔开,但内部应用程序编程接口(API)让你能够广泛访问所有数据。当把单体应用拆分为微服务时,你可能会掉进这样的陷阱:似乎系统中的所有数据,每个微服务都得有自己的一份副本。如此一来,你就把事件代理当成了一个简陋的数据库同步工具。同样,所有东西又变得紧密耦合了。

4. 事件设计

人们很容易误以为事件队列本身就像一个拥有无限容量的容器,能够处理任意数量的事件。但实际上,你需要仔细斟酌事件的设计,以确保事件代理自身具备可扩展性。你还得考虑设计规范,避免各类事件中出现概念泛滥的情况,因为这会导致难以理解正在发生的事情,也不利于诊断问题。在思考事件设计时,要尽量牢记这个概念:生产者不应知晓消费者的任何信息。这有助于构建一个松散且灵活的系统,从而最大程度发挥这种架构的优势。

5. 优化你的设计

我刚提到生产者不应知晓消费者的任何信息。但倘若将此奉为不可更改的原则,最终你会得到一个难以管理与开发的系统,因为特定业务领域的业务逻辑可能会被分散,甚至出现重复。在这种情形下,你或许需要考虑在生产者端添加一些逻辑,以避免重复并简化所有消费者的操作。不过要记住,只能在生产者所属的领域内进行这样的操作。

6. 做最坏的打算

当一切都启动并正常运行时,你会发现事件代理为消费者提供了一组不重复、顺序正确且连续的事件。人们很容易认为情况一直如此,并且消费者可以基于这个假设来实现自身的业务逻辑。遗憾的是,这是个错误的假设。在出现故障时,依据特定配置,你的事件代理可能会丢失事件、重复事件,或者乱序发送事件。如果这些情况中的任何一种对系统至关重要,你就需要了解生产者、事件代理和消费者的故障模式,并对它们进行配置,以确保事件集满足解决方案的要求。你需要为最坏的情况进行设计,而非最好的情况。

规划你的迁移工作

希望你已阅读至此,并且现在明白,不能心血来潮地某天突然就着手拆分单体应用。你需要对目标状态进行设计,涵盖以下方面:

  • 基于事件的架构在哪些方面能够发挥作用(以及在哪些方面无法发挥作用)
  • 针对本文提及的每一项挑战,你将如何应对
  • 为保持系统设计的一致性,你需要遵循哪些规范
  • 你的开发人员和质量保证工程师将如何开发和测试该解决方案

一旦确定了目标状态,接下来就需思考如何从当前的单体应用过渡至目标状态。

完全重写的方式往往难以顺利推进。重写耗时较长,在此期间,需求可能发生变动,利益相关者可能失去兴趣,并且无法及时获取业务收益。

你应当规划一个渐进式的实施流程。实现这一目标有多种方法:

扼杀者模式(Strangler Pattern)

在这种模式下,你逐步替换系统的各个部分,但借助外观模式,系统外部的使用者不会察觉到任何变化。其目的在于通过每次仅迁移一小部分,降低迁移风险,直至整个解决方案迁移完成。不过,这可能会带来大量额外工作,而且由于需要提供同步响应,可能会限制基于事件的解决方案。此外,它还可能延迟业务收益的实现。

渐进式演进(Incremental Evolution)

此技术仅针对新功能引入基于事件的架构。当旧功能被弃用时,将它们从系统中移除,最终便可达成目标状态。但这取决于新功能引入的速度以及旧功能被弃用的速度。这种方式可能会引发系统数量增多的问题,最终你将面临更多需要管理和维护的系统,从而削弱了最初进行迁移的业务初衷。

先处理问题区域(Problem area first)

如果单体应用中有某个特别棘手的部分,你或许需要考虑重写这一部分,仅在必要时再将其重新集成到单体应用中。依据该功能在单体应用中的集成程度,这可能会成为一个技术上极为复杂的构建过程。

先选定受众(Select audience first)

在某些情形下,你可能认为部分用户群体(例如:新注册用户、最低等级套餐用户等)可能不需要完整的功能集,你可以为他们提供独立的系统。这个系统将包含一组基本功能,能够满足这部分用户群体的需求,从而缩小新构建的范围。现有用户可继续使用单体应用。根据这些用户的反馈,你能够确定在新解决方案上开发功能的先后顺序。理想状况下,单体应用应停止功能更新,仅允许在新解决方案上开发新功能。但实际上,由于单体应用服务的可能是你最老、最忠实,或许也是付费最多的客户,完全做到这一点可能不太现实,这就会导致系统数量增加。

其他方法

还有其他迁移方案,可能会更有效地适配你的特定使用场景。关键在于,你要思考如何以一种能够实现以下几点的方式达成目标状态:

  • 获得并持续赢得利益相关者的支持
  • 尽可能降低风险
  • 避免给自己“挖坑”(即不要做出实际上让事情变得更棘手的决策)
  • 在预期时间范围内实现预期收益

总结

在本文中,我尝试阐释基于事件的架构究竟是什么,你为何会考虑采用这种架构(以及为何可能不想采用),还有当你从单体架构向基于事件的架构迁移时需要考量哪些因素。

倘若你确实决定变更解决方案架构,务必要确保目标架构在可扩展性、可维护性、可靠性、关注点分离,以及成本控制 等方面能够契合你的需求。

这种迁移并非那种可以“顺带完成”或者“利用业余时间完成”的项目。它是一项重大任务,需要精心规划、设计与执行。

迁移是能够成功实现的。众多人已经踏上了这条道路,并成功收获了基于事件架构带来的优势。

如果你觉得这篇文章有意思,还请点赞,因为这能帮助我了解大家认为哪些内容有价值,以及明确未来该撰写哪些主题的文章。要是你有任何建议,欢迎在评论区留言。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件举报,一经查实,本站将立刻删除。

文章由技术书栈整理,本文链接:https://study.disign.me/article/202509/16.monolith-to-an-event-based-system.md

发布时间: 2025-02-26

原文阅读: https://medium.com/@martin.hodges/how-to-decide-to-move-from-a-monolith-to-an-event-based-system-fc4f625de645