你开始构建一个漂亮的单体系统。也许是一个模块化的单体系统。随着时间的推移,系统不断增长,需求也在不断变化。渐渐地,系统开始出现裂痕。
这可能是出于组织原因,需要在团队之间分配工作。也可能是由于扩展性问题和性能瓶颈。你开始评估可能的解决方案,以及每种解决方案的优势和权衡。最后,你做出了一个决定。是时候将系统的部分部分迁移到独立的(微)服务中了。
那么,我们如何从单体架构迁移到微服务呢?
使用有界上下文进行解耦
从单体架构转移到微服务的第一步是识别有界上下文。因为它们代表了可用于提取的领域的内聚部分。
一个解决方案是使用领域驱动设计战略建模来识别有界上下文。
有界上下文定义了模块之间的显式边界,并分离了各自的责任。这是迁移到微服务时面临的最大挑战之一。确定良好的边界确保微服务专注于一个问题领域。
在单体中定义边界也更容易,因为你不是在处理分布式系统。重构不良边界风险较低,你有更多自由度去“搞定”。
接下来你需要解决的问题是耦合。耦合表现为两种方式:
- 数据库依赖
- 模块间的通信
你可以通过构建模块化单体来从一开始解决这些问题。但我也会解释你可以使用的指导原则来解决耦合。
模块化单体如何解决耦合
模块化单体是一个响亮的名字,指的是由几个有界上下文(模块)构建的单体系统,并遵循一系列控制耦合的原则。每个模块包含一组内聚的功能,并且在系统中与其他模块隔离。这种隔离涉及数据库依赖和模块间的通信。
你可以把一个模块看作系统中的一个独立应用程序。一个模块拥有自己的领域、实体、用例和数据库表。模块作为一个单一可执行应用程序一起部署。但在其他方面它们是独立的。
你可以对每个模块应用不同的架构方法,比如清晰架构。
我提到你需要减少模块间的耦合。
以下是解决数据库耦合的两个原则:
- 模块不能在数据库中共享表
- 模块不能直接查询其他模块的数据库表
共享数据库表会导致高度耦合,而这恰恰是你要避免的。你可以使用模式在逻辑层面或物理上使用不同的数据库为每个模块隔离数据。
一个模块应该暴露一个其他模块可以调用的公共 API。这个公共 API 是模块的入口点。这是模块间通信的唯一方式。
模块间通信可以是同步的,使用方法调用,或者异步的,使用消息总线。
我更倾向于使用消息传递的异步通信。它耦合度低,使得向微服务的转变更加容易。
为系统添加消息代理
为了在模块间实现异步通信,你可以引入一个消息代理。但你无需从一开始引入一个完整的消息代理。
你可以使用诸如MassTransit这样的抽象来在模块之间实现消息传递,同时将传输机制抽象化。
MassTransit 有一个内存传输机制,可以很好地在单个进程中工作。它非常快速。但它不是持久化的,如果总线停止,你可能会丢失消息。
在引入真正的消息代理时,你只需要配置不同的传输机制。但你不需要改变你的消息传递代码。
在模块化单体中引入消息传递的目的是什么?
这样设计系统可以使模块之间松耦合和独立。在项目成熟后,你在开始时增加的复杂性是合理的。
将模块提取到微服务中
我们决定从单体系统迁移到微服务。因为我们以模块化的方式构建了系统,所以迁移的关键在于将一个模块提取到一个新的进程中。
你应该在服务前面引入一个反向代理,来路由进入的流量。这将隐藏微服务系统的实现细节,不让客户端应用程序知道。
新的微服务需要连接到消息总线,但我们不需要在代码中做任何改变。使用消息传递在模块之间进行通信简化了迁移过程。这可能让你想起事件驱动架构。
如果你使用方法调用来实现模块间通信,你必须将这种实现替换为通过网络的 HTTP 调用。因为你现在正在构建一个分布式系统,之前的方法调用实现将无法工作。你还需要考虑认证、容错等问题……
从单体系统中提取模块会用新微服务的功能替换旧系统的所有功能。这个迁移到微服务的过程遵循了榕树模式。
总结思考
从单体架构迁移到微服务的最大障碍是耦合。耦合是变更的阻止者。因此,这是你需要解决的第一件事。
你需要在数据库层面和代码中的组件间解决耦合。以模块化的方式构建系统可以从一开始就避免这些问题。
这就是为什么模块化单体是一个很好的方法。
你可以在系统中识别有界上下文,并将它们用作单体中的边界。在单体中正确划分边界要容易得多。
迁移到微服务就是将模块提取到独立服务的过程。
当然,你仍然需要考虑安全性和容错性,因为现在你有了一个分布式系统。
当谈论抽象的架构时,可能难以理解,但在讨论概念性解决方案时却是很重要的。