当我们提到单体应用架构,我们在说什么?

1,248 阅读9分钟

定义

在建筑结构中,单体建筑描述了从一块材料(历史上是从岩石)雕刻、铸造或挖掘而成的建筑。在计算机科学中,建筑是系统,材料是我们的可执行代码。因此,在单体应用架构中,我们的系统仅由一段可执行代码组成。

一个软件系统被称为“单体”,如果它有一个单体架构,其中功能上可区分的方面(例如数据输入和输出,数据处理,错误处理和用户界面)都是交织在一起的,而不是包含架构上独立的组件。

单体应用程序或单体架构可能是您以前听说过的一个术语,因为它可能是开发企业应用程序最流行的模式。如果系统的所有不同组件都封装到一个单元中,例如,如果用户界面、多个域和基础设施服务组合到一个可部署单元中,我们将其称为单体应用程序。

单体架构是软件程序的一种传统模式,它是作为一个统一的单元构建的,自成一体,独立于其他应用程序。“单体”一词通常被归结为庞大而冰冷的东西,这与软件设计中的单体架构的真实情况相差无几。单体架构是一个单一的大型计算网络,它只有一个代码库,将所有业务关注点结合在一起。 要对这种应用程序进行更改,需要通过访问代码库、构建和部署服务端接口的更新版本来更新整个堆栈。这就使得更新既受限制又耗时。

单体架构是设计软件程序的传统统一模式。在这里,"单体 "指的是由一个整体组成。单体软件的设计是自成一体的;程序的各个组件相互连接、相互依存,而不是像模块化软件程序那样松散地耦合在一起。

模块单体化

我已经定义了单体的含义,让我们进入第二个方面:模块化。由分开的部分组成,当它们结合在一起时就形成一个完整的整体/由一组分开的部分组成,这些部分可以结合在一起形成一个更大的物体。

image.png

图片来源:www.kamilgrzybek.com/images/blog…

模块单体化这个想法很简单。保持代码库在一起,但仔细打包成单独的组件。为了使边界清晰,我们对在另一个组件中使用来自一个组件的代码设置了一些额外的限制,并且非常有意识地对 Pythonic 模块化单体组件之间的通信进行建模。

每个组件都有一个公共 API 和私有的内部细节。前者只能从外面使用,而后者则不能触达,也就是封装。

有趣的是,每个组件可以有不同的内部架构。例如,具有业务关键性内容或最复杂内容的核心组件可以实现整洁架构。它促进了可测试性,并将业务规则置于基础设施和较低级别的关注点之前。您可以在这里看到一个示例:

├── application
│   ├── queries
│   ├── repositories
│   └── use_cases
├── domain
│   ├── entities
│   ├── events
│   ├── exceptions
│   └── value_objects
└── tests

或者,我们可以使用轻量级方法,将数据库模型作为我们的域对象。还有一种情况是我们的组件仅仅是第三方 API 的包装器。 查看示例:

├── api
│   ├── __init__.py
│   ├── consumer.py
│   ├── exceptions.py
│   ├── requests.py
│   └── responses.py
├── config.py
├── dao.py
├── events.py
├── facade.py
├── models.py
└── tests

一个组件从外部看起来是什么样子?如前所述,每个组件都有自己的 API。实现整洁架构的组件将有一系列用例:

└── application
    ├── queries
    │   ├── get_single_auction.py
    │   └── get_all_auctions.py
    └── use_cases
        ├── beginning_auction.py
        ├── ending_auction.py
        ├── placing_bid.py
        └── withdrawing_bids.py

而内部架构不太复杂的组件可以使用外观模式:

class PaymentsFacade:

    def get_pending_payments(self, customer_id: int) -> List[PaymentDto]:
        ...

    def start_new_payment(self, payment_uuid: UUID, customer_id: int, amount: Money, description: str) -> None:
        ...

    def charge(self, payment_uuid: UUID, customer_id: int, token: str) -> None:
        ...

    def capture(self, payment_uuid: UUID, customer_id: int) -> None:
        ...

在 API 上公开数据库模型或实体/域对象是一个坏主意。使用数据传输对象,可以使用 attr.s 或懒汉模式从标准库实现-类!

@dataclass(frozen=True)
class PaymentDto:
    id: UUID
    amount: Money
    description: str
    status: str

优点

  • 便于开发:当使用一个代码库构建应用程序时,所有代码和关注点都存在于一个地方,您无需过多担心分布式系统中远程过程调用可能带来的故障
  • 容易部署:一个可执行文件或目录使部署更容易。它们部署起来很简单。只有一个可部署的,并且它的要求应该很好理解。
  • 性能:在中心化代码库中
  • 便于测试:
  • 便于调试:当所有代码都位于一个位置,可以更轻松地跟踪请求并发现问题

不足

随着应用程序复杂性或规模的增长,这些缺点往往会出现:

  • 应用程序的启动时间可能会长达数分钟。这很快就会增加工程师在开发过程中浪费的大量时间。
  • 扩展应用程序开始变得困难。除了启动时间慢(可能会影响客户)之外,单体应用通常只能在一维进行扩展。由于应用程序涵盖了许多不同的用例,因此可以花费大量时间来优化配置以涵盖所有这些用例。这可能导致单体在资源方面(例如 CPU)变得非常昂贵。
  • 持续部署变得缓慢。即使您对特定部分进行了小的代码更改,也必须部署整个应用程序。随着应用程序复杂性的增加,这会变得越来越慢。部署时间超过一个小时的情况并非闻所未闻。
  • 技术栈单一。如果您的整体应用程序是用 PHP 编写的,您必须继续使用 PHP,即使新的应用程序需求更适合新技术,或者如果您聘请了不同语言的专家,您也必须坚持使用 PHP。迁移到新语言将需要重写整个系统(或者如果您决定迁移到微服务,则需要重写部分系统。我们将在下一章中更多地讨论微服务)。
  • 改变变得困难。随着交付压力和开始开发的障碍越来越高,系统的模块化常常会变得模糊

选择微服务架构需要考虑什么

随着几年的微服务热潮,大量的文章和技术分享、框架和书籍都在讨论着微服务方案。微服务也不是唯一的方向,的确,微服务是一项很好的技术,但不可置否的是它们成本很高,而且分布式架构本身就很复杂。分布式应用程序的决定也不能轻易就能开发完成,因为它需要付出巨大的代价。

微服务是一种有用的架构,但即使是它们的拥护者也表示,使用它们会产生显著的微服务溢价,这意味着它们只对更复杂的系统有用。这种溢价,本质上是管理一套服务的成本,会减慢团队的速度,有利于更简单的应用程序的整体。这就引出了一个强有力的论点,即首先应该将一个新的应用程序构建为一个整体,即使你认为它很可能会在以后从微服务架构中受益。

第一个原因是经典的 Yagni 原则(You Aren't Gonna Need It)。当你开始一个新的应用程序时,你有多确定它对你的用户有用?可能很难扩展一个设计糟糕但成功的软件系统。通常发现一个软件想法是否有用的最好方法是构建一个简单的版本,看看它的效果如何。在第一阶段,你需要优先考虑速度(以及反馈的周期时间),所以微服务的代价是你应该考虑的。

使用微服务的第二个问题是,只有在服务之间建立了良好、稳定的边界时,微服务才能很好地运行,这主要是指建立一组正确的 BoundedContexts。服务之间的任何功能重构都比单体服务困难得多。但是,即使是经验丰富的架构师,在熟悉的领域工作时,也很难在一开始就正确地确定边界。如果先构建一个单体,就可以在微服务设计给边界涂上一层糖衣之前,找出正确的边界。这还能让你有时间开发细粒度服务所需的微服务先决条件(MicroservicePrerequisites)。

那么什么时候选微服务架构呢,你可以考虑如下因素:

  • 系统各部分的不同安全问题
  • 应用间需要独立扩展
  • 需要使用另一种编程语言或并发范例
  • 需要单独部署

总结

单体应用架构比微服务更简单,更容易部署,但需要要求做到很好的代码分离。微服务架构需要更多的工具、库、组件、团队经验、基础设施管理等。单体架构比微服务具有更好的性能,直到出现扩展需求,然后它取决于扩展的可能性,在单体架构中,故障影响更大,因为一切都在同一过程中工作。可以通过复制来降低风险,但其成本将高于微服务架构。

单体架构并不意味着系统设计不好,模块化单体架构是以模块化方式设计的单体应用系统的明确名称。为了实现高水平的模块化,每个模块必须是独立的,具有提供所需功能所需的一切(按业务领域分离),封装并具有定义良好的接口/合同。

参考文章: