模块化软件架构:使用单体、微服务和模块化单体的优缺点

【squids.cn】 全网zui低价RDS,免费的迁移工具DBMotion、数据库备份工具DBTwin、SQL开发工具等

近年来,微服务架构在大多数软件解决方案中已经处于领先地位,而且在很多情况下,它经常被选择为我们开始开发的架构。然而,值得问自己的是,这是否始终是最佳选择。而且,如果你选择微服务作为你想坚守的一套规则,你确定你知道这个选择的后果吗?

微服务的优势 

在我看来,微服务提供了两个主要的优点:

  • 无停机时间的独立部署。

  • 系统(包括数据库)的逻辑(有时是技术的)划分为业务模块和子模块。

“分布式单体”问题 

不幸的是,在大多数情况下,当选择微服务架构时,团队最终会创建一个所谓的“分布式单体”。如果在工作开始时,你依赖于服务或数据库之间的依赖关系,并且最后同时部署了90%的服务,你应该承认,将其作为一个单一的部署单元会更容易。这将减少与微服务的实施、自动化和维护相关的工作量,并允许你集中精力解决业务问题。

长话短说,你必须记住微服务软件架构风格并不简单,并带有很多技术复杂性。不要用大锤砸核桃!为一个应用程序或服务运行一个Kubernetes集群是没有意义的,因为基础设施的成本和所有的配置将超过开发的成本。还有其他“更简单”的云解决方案,例如 AWS ECS、AWS Fargate、AWS Beanstalk,或者甚至是 EC2 + 简单的负载平衡(其他云提供商有类似的解决方案)。

以下是一些可以帮助你决定选择哪种架构的启示法。

你需要什么? 微服务 单体架构
独立实施单位 是的,但只有良好的逻辑分离 是的
简单快速地构建基础设施 是的
动态缩放 是的
业务逻辑自治 是的 – 如果我们正确划分域 是的 – 如果我们正确划分域
特定系统组件的动态水平缩放 是的
技术自主权 是的
独立开发团队 是的
快速项目启动(开发启动) 是的

单体架构并不一定是坏事——尤其是在模块化软件架构中 

“单体”这一术语经常被用作旧应用的同义词。通过适当地设计一个单体应用(适当选择内部架构),你可以确实地缩短开发启动时间,而不排除未来可能的变化。你仍然可以过渡到微服务。这就是模块化单体方法——或者特别是“首先考虑单体”的方法——派上用场的地方。如果你不知道项目在未来几年内的范围,也不知道它会增长多快,那么从一个结构良好的单体应用开始可能是个好主意。

模块化单体提供了什么?

  • 一个单独的部署单元

  • 更简单的维护

  • 后续可能迁移到分布式架构的开放之路

  • 简单的基础设施

  • 整理代码

当然,还有许多其他因素可能会影响系统架构的选择。但是,考虑到我上面提到的因素,最好不要开始一个基于微服务的项目。如果你不确定整个系统在2-3年内会是什么样,通常最好选择模块化架构。你从一个计划良好的单体开始,然后——如果需要的话——逐渐将其转化为模块化单体。

你还应该记住一些可用的、简单的解决方案,它们根本不需要系统架构——例如无服务器解决方案、AWS Lambda等。它们在相对简单,不过于复杂的问题中表现良好。

在Pretius,我们专注于长期项目和长生命周期(维护和开发),这就是为什么可持续性和可扩展性对我们来说通常非常重要(但,当然,你的特定情况可能会有所不同)。系统架构的选择通常归结为部署方法和随之而来的系统启动的基础设施。

然而,从软件开发者的角度看,基础设施和应用部署正在逐渐成为次要话题——有专门的岗位的专家(例如DevOps工程师)做出重要决策并处理这些问题。我们只希望系统得到良好的维护,无故障地运行,不产生技术/商业债务。为了实现这一目标,我们需要专注于应用代码。

内部应用架构 

一旦你决定了系统架构,就该关注单个应用的架构了。不幸的是,大多数项目都基于层(n层架构)。在复杂的项目中,最终结果通常都是一样的:→ 大泥球(下面的截图中有例子)。

模块化软件架构:使用单体、微服务和模块化单体的优缺点_第1张图片

对于简单的CRUD应用或几个/几十个服务,这并不是一个糟糕的选择,但当你试图将复杂的逻辑放入这样的框架时,很快就会发现,依赖关系的网络开始引起严重的问题。了解不同风格的存在并更详细地了解它们是一个好主意。

应用架构风格:

  • 分层架构

  • 六边形/洋葱架构

  • 管道和过滤器

  • 微内核

值得添加的是,你可以混合和匹配这些风格。你不需要固定在一个特定的解决方案上,而只需要选择能使你的生活变得更轻松的工具。这也适用于系统架构。例如,如果你发现需要在微服务系统中创建一个更大的(例如,单体)应用,那么你不应该试图强迫它适应微服务架构。相反,选择内部架构,将其划分为独立的模块/业务域,从而降低DevOps/配置工作的成本。

黄金中间地带 

在选择架构时,你也可以试图找到一个中间地带。说实话,不存在所谓的“黄金中间”——但你可以接近。在选择特定的架构风格之前,值得收集一些关于设计系统的指标。你拥有的信息越多,你的决策就越可靠。

  • 系统复杂度(浅 vs 深): 

  • 浅 - 所有CRUD类型系统,没有或只有微不足道的业务逻辑。

  • 深 - 高复杂度(业务/技术)。复杂性可以由以下特点来确定:

  • 可通讯性

  • 业务规则

  • 算法

  • 协调

  • 时间视角

  • 什么将要改变

  • 可能/不可能的改变

  • 责任分离 - 是否需要独立团队来研究解决方案?

浅层系统易于识别,因为其用户界面完全反映了数据库和应用代码中的结构(→ CRUD)。没有复杂的集成或复杂的算法。另一方面,深层系统的主要特点是从用户交互到最终效果,会有许多对用户不可见的、或多或少复杂的操作。Google搜索引擎或处理租赁申请的系统是深度系统的好例子。

很难谈论选择架构的原则——似乎“启发式”这个词更能引导我们做出特定的选择。

对于由小团队开发的浅层、不复杂的系统,你可以选择更简单的架构,例如分层和单体应用。然而,当你从一开始就知道复杂度会很高,它将是一个多年的项目,并且需要大型或多个团队的工作,应用的适当划分将是关键,我们将追求责任分离。这可以在微服务架构和前面提到的模块化单体中实现。在选择特定风格时,还必须考虑技术方面。

好的旧式头脑风暴——或其更新形式,事件风暴——是你可以使用的工具之一,用于创建模块/服务的初始概述。这样的练习的目的是形成一个给定业务中的域的初始概述——或者没有它们。然后,你应该能估计系统的大小。以下是分析对象图(用颜色标记)后域/模块的第一个概述的例子。

数据库 

模块化软件架构:使用单体、微服务和模块化单体的优缺点_第2张图片

在团队选择项目架构时,数据库模型经常被忽视。他们将服务分为较大或较小的部分,在云中运行它们,并建立与微服务生产相关的整个外包装,但是他们仍然设计数据库,好像它是一个大的、纠缠不清的单体应用程序的一部分。这不是一个好方法 - 如果你决定使用微服务,数据库需要反映你在应用层使用的逻辑分区。在现代微服务架构中,数据库级别的服务绑定是主要问题之一,导致了大泥球(如上面的其中一张图片所示)和随后的维护困难。

模式 

如果您无法为每个服务提供单独的数据库,那么开始时划分为模式就足够了。在模块化单体中,模式也会起作用。当涉及到与微服务的数据层时,您应该充分利用它们提供的机会 - 即,您应该根据业务模型匹配数据库类型,而不是反过来。凭借一点努力,一切都可以被扁平化为关系模型,而微服务架构的自由度允许我们不这么做。

设计数据库的常见方法是根据我们需要收集的数据创建结构(例如,根据从客户端接收的信息)。尝试基于应用程序必须满足的功能设计数据模型是一个很好的练习。结果模型通常比原始假设“更薄”,并且事实证明它是完全足够的。

ORM vs. SQL 

在许多Pretius项目中,我们使用MyBatis工具与DB进行通信。这允许您保持对数据库中发生的事情的100%控制,但它也有一个经常被忽视的副作用 - 贫血领域模型。编写复杂的SQL语句经常使开发人员不愿在应用程序代码的一侧创建复杂的关系。另一个结果是脱离面向对象的编程范式(服务中缺乏封装逻辑)。

几乎任何开发者都可以学习和处理像MyBatis(以及其他替代方案,例如Hibernate)这样的解决方案,所以它们是值得考虑的。但是,它们只是工具,它们不会使您免于思考您决策的副作用和后果。

通信 

在微服务架构中的通信,首选的方法是异步,默认。但是你肯定不能完全避免使用REST或其他某种同步通信形式(gRPC,SOAP等)。技术是一方面,但是您应该问自己更重要的问题。你为什么要通信?这些REST调用是否必要?逻辑是否已正确分离到另一个模块/服务?

经常地,微服务变成了相互连接的网络 - 形象地说,一切都与一切交谈。这通常是域的不良划分/分解的后果,已经是一个严重的迹象,表明您已经构建了一个分布式单体,而不是微服务。

这个问题不仅限于应用程序/服务级别的通信。您必须记住,相同的规则也适用于代码级别 - 服务、门面、类或包。在两种情况下,都值得熟悉低耦合、高内聚的概念。从技术上讲,通信通常不是问题,但分布式事务、传奇、补偿和回退(有时是分布式架构的后果)是。这是一个关注的领域。

分布式系统中的一些通信模式,您应该知道:

  • Outbox模式

  • 传奇模式

  • 消息传递

  • CQRS

创建基本项目结构 

项目的结构应与选定的系统和应用架构紧密相关。除了应该是公共的组件,例如,作为DevOps要求的一部分(适用于系统架构),应用程序本身的内部结构将由选定的应用架构确定。

除了架构外,为了在业务上下文中保持代码的更好可读性,您可以使用“按特性打包”的方法。这个想法是将代码分组放在特定的域/功能/区域的包内,而不是 - 通常的情况 - 分为技术包。例如,控制器、服务、映射器等。

另外,说实话,我不建议在公司内部创建可以重复使用的应用程序框架。有免费的工具,如Spring Initializr,您可以用它在几秒钟内创建这样的项目大纲,所以最好独立考虑每种情况。从其他项目复制内容可能会导致从一开始就产生技术债务 - 因为您甚至没有检查是否有新的/更好的选项。

但是,一旦您选择了一个结构,最好在整个项目中坚持使用它,以保持一致性和透明度。

测试策略

模块化软件架构:使用单体、微服务和模块化单体的优缺点_第3张图片

测试金字塔(上面的截图)在分布式架构的情况下并不完全适用。除了在一个实施单位内部的测试外,您还必须确保在这些单位之间的界面进行测试,因为您必须假设这种通信将存在。所以,除了模块/服务之间的标准集成测试外,您还可以使用合同测试作为模块之间的API测试的一部分(有关该信息的更多信息在此处可用)。

此外,根据模块的不同,测试金字塔的外观也会有所不同。在浅层模块的情况下(例如 CRUD),进行所有种类的测试是没有意义的,因为最后,您只是多次检查相同的事情 - 主要的不同之处在于如何调用测试。对于深层、复杂的模块,其中业务逻辑可能复杂且广泛,单元测试是方法。而集成模块则必然需要比单元测试更多的集成测试。

集中式日志存储 

分布式(微服务)架构需要一个合适的方法来收集应用程序日志。服务可以在生产环境中存在多个实例,并运行在完全不同的物理机器上。因此,登录到服务器并手动搜索文本文件中的日志变得既耗时又有时甚至可能是不可能的。解决这个问题的方法是实现一个集中的日志存储,例如一个文档数据库,其中使用辅助工具收集系统中包含的所有应用程序的日志。

当前实现上述需求的市场标准是以下三个工具的集合:

  • Logstash – 一个负责收集应用程序日志及其处理和聚合的应用程序,然后将它们发送到数据库。

  • Opensearch/Elasticsearch(文档数据库)– 一个数据仓库。

  • Dashboard/Kibana – 一个负责收集日志的可视化的应用程序。

具体的数据库和日志可视化工具将取决于项目。上述点中提到了两个最可能的选项。

下面的图表显示了如何从网站/应用程序将日志发送到数据库引擎的简化流程。

您可以通过REST API与Logstash集成,这使您可以独立于应用程序中使用的编程语言使用它。如果无法直接将应用程序与Logstash集成,可以使用负责从文本文件中下载日志,解析它们,然后将它们发送到Logstash工具的Beats工具。


监控 

使用微服务架构的后果之一是相当高的技术复杂性。应用程序在许多物理机器上分布;系统可能包括数十个应用程序和各种支持组件。因此,分布式系统维护的一个非常重要的方面是它们的持续监控,即检查系统的状态。由于此,当我们注意到故障或系统错误时,我们能够尽可能缩短反应时间。

矛盾的是,为了控制日益增长的架构复杂性,我们需要实施更多的工具来帮助我们。

你应该监控什么? 

1、业务应用程序:软件开发团队提供的应用程序/服务将构成应用程序的核心,并满足主要的业务要求。因此,你需要监控的关键元素。有几个值得关注的领域 

  • 技术指标(可能适用于特定的应用程序/服务):应用程序通过现成的库提供,每种编程语言都有,取决于使用的技术。应用程序的技术参数与基础架构部分中的参数类似(如CPU使用率,RAM,空闲磁盘空间),但它们只适用于特定的应用程序或其实例。 

  • 业务指标:这是开发团队收集的数据。它有时可能看起来多余,但它可以作为其他团队建立统计数据的替代方案。 

  • 应用日志:您将能够从应用程序日志中提取技术和业务信息。

2、系统基础设施:除了业务应用程序,重要的方面是运行平台的基础设施和物理机器的状态。这种监控使您能够例如检查磁盘上还有多少可用空间,并提前通知您。基础设施监控参数的示例包括:

  • 硬盘上的空闲空间 

  • 免费/已使用的内存 

  • CPU使用率 

  • 网络流量 

3、警报和通知:他们是监控的不可分割的部分,消除了需要一个人观察收集到的指标的需要。因此,例如,当基于收集的指标,系统发现给定的应用程序已经使用了10分钟的100%可用RAM时,将自动发送电子邮件给维护团队,该团队将能够采取适当的行动。 

工具 

同样的工具用于所有类型的指标和警报,并且可以从任何编程语言集成。

  • InfluxDB:用于存储时间序列数据(时间序列数据库)的开源数据库。例如,从传感器读取的CPU使用率值每1秒。 

  • Grafana:开源的,跨平台的Web应用程序,用于分析和交互式可视化。连接到支持的数据源(SQL,NoSQL,Logi,InfluxDB)后,我们可以创建各种类型的图表并设置适当的通知。


领域方法对业务逻辑

应用程序通常采用分层架构进行构建。我指的是技术上将其分为以下类型的包:控制器、服务、模型等。在代码中,模型通常只是数据库列到POJO字段的简单映射(没有关系,仅通过ID引用)。另一方面,业务逻辑完全在“服务”包中实现。这样的划分并不是错误的,但在大型项目中可能效果稍逊。

多层架构问题:

  • 没有逻辑的责任划分(您根据技术方面而不是业务方面来创建代码)。

  • 更容易创建一个“一切与一切都在交谈”的情况。贫血领域模型。

  • 通常没有任何封装。

  • 难以分离抽象层。

  • POJO通常在应用程序中共享。

  • 测试单个组件更容易,但由于逻辑分散在不同区域,很难测试特定的、全面的业务案例。带有很多依赖的类 – 更难进行测试。

  • 随着时间的推移,完全缺乏可读性和导航困难。例如,整个应用的所有100个服务可能都在一个包中。

  • 业务逻辑与框架之间的100%连接,例如Spring。这不是一个巨大的问题,因为你在项目中很少更改一个框架,但它可以使业务逻辑变得模糊。

您可以使用以下几种不同的方法来代替层:

  • 六边形端口和适配器。

  • 按特性打包。

  • 与业务逻辑结合的丰富领域模型 - 结合了策略性DDD的元素的面向对象编程。聚合,值对象等。

系统架构级别的逻辑划分不是最终目标 - 我们经常忘记在应用程序本身中的分离。一个网站很少等于一个功能。您还应该分离构成特定服务或应用的领域和子域(可能有很多)。它们之间的通信原则应该与系统架构级别使用的通信原则相似:固定的API、接口通信、封装等。

销售提议与现实 – 过渡到实施 

通常,提供的是由一个与负责实施的人(或团队)不同的人制作的。因此,值得在投标阶段吸引第三方进行架构提议的快速验证。交叉检查无疑是值得的,无论你的经验如何。

首先,值得检查的是,考虑到客户的需求,建筑设计是否过度。如果你首先想到的是微服务,你很容易选择一些过于复杂的东西。例如,你需要设计一个相对小的应用程序(一个领域,几个功能),但你用在Kubernetes上启动的微服务架构来匹配它,使用ELK堆栈、监控等。如果你不知道未来的项目开发计划,你真的想用一个应用程序这样做吗?

您也可能遇到相反的情况 - 您的建议不足以满足客户的需求。这种情况可能发生得较少,因为在报价中,一切通常都是计划有足够的余地 - 但如果发生了,您将需要找到支持您的方法的有力论据。此外,最好在提议阶段限制自己到系统架构,然后在实施阶段留下应用架构。

如果我们放弃了微服务,一年后发现它们毕竟是需要的怎么办?如果您构建了一个具有适当结构并划分为领域和子领域的单片应用程序,更改架构不应该需要一场革命 - 它只是一个您可以不费太大努力进行的自然过渡。

技术和业务限制 

这些领域的决策将严格依赖于客户的要求 - 很难定义行为规则。需要注意的一些元素有:

机器的物理布局。

数据保留的法律方面。

具有给定云提供商的技术的可用性。

重算法计算(服务器可以专门为此进行适应)。

系统上的用户负载。

重流量 

无论您选择的是哪种系统架构,应用程序都应能够处理客户指定的流量,并为可能的“意外”高峰做好准备。您必须小心,不要陷入“过早优化”的陷阱,但如果您已经有了特定的要求,那么您必须从一开始就调整解决方案来满足这些要求。

需要考虑的一些事情包括:

  • 水平和垂直扩展 

  • 缓存 

  • 自动扩展 

  • 负载均衡器 

  • 性能/负载测试 

  • SQL优化 

  • 数据库复制 

最佳的情况是客户能够定义关于系统性能的具体指标,例如每小时的登录次数,X次对特定网站的访问在X时间内,处理十个租赁申请,每分钟搜索100次优惠等。基于这样的指标,您可以准备适当的测试并适当地调整系统。

处理大量或多个文件 

这里有两个方面需要考虑:

  1. 架构方面:将“处理”功能分离为独立的技术服务,这些服务可以根据当前需要进行扩展。

  2. 技术方面:在大多数情况下,处理大文件归结为内存问题。其中一个可以帮助的方法是流处理(您不会将整个文件加载到内存中)。另一件事是与外部文件处理服务集成,例如AWS S3。仔细查看制造商的文档并使用提供的解决方案总是一个好主意。在S3的情况下,它是一个用于部分处理文件的API - 多部分上传和下载。即使您没有关于处理文件大小的具体要求,也应该从一开始就考虑这一点。这不需要太多的工作,您将在未来保持安全。

总结 

如您所见,模块化系统可以作为微服务或传统单体架构的良好替代品。每种方法都有其优点和局限性。您应该为您的项目考虑哪一种呢?这完全取决于您将要工作的系统的性质。为了使这个决策更加简单,请查看以下总结主要优点和缺点的表格。

微服务

优点 缺点
  • 云原生

  • 模块的物理分离

  • 水平缩放

  • 部署自主权

  • 技术自主权

  • 多个团队可以在独立或松散耦合的组件上进行协作。

  • CI 顺利——合作无冲突。

  • 松耦合

  • 困难的通信分析(API 设计和版本控制)。

  • 复杂的基础设施(日志记录、消息传递、监控等)

  • 网络通信开销较高。

  • 多个部署单元

  • 项目开始时付出了很大的努力。

  • 很难测试所有业务功能。

  • CI/CD 过程肯定更复杂。

  • 交易通常是分布式的。

单体架构

优点 缺点
  • 基础设施简单

  • 通讯速度和可靠性

  • 单个部署单元

  • 交易性

  • 通讯安全

  • 快速开球

  • 所有业务功能的可测试性

  • 难以维持结构(MUD 风险大球)。

  • 难以维护——可能会出现问题的地方。

  • 水平扩展更加困难(需要更多资源,至少在理论上)。

  • 向分布式架构的过渡很困难。

  • 困难的 CI – 有很多冲突并且在通用组件上工作。

  • 稳定性——一个软件模块中的错误可能会影响整个应用程序。

  • 永久依恋一种技术。

模块化单体

优点

缺点
  • 基础设施简单

  • 通讯速度和可靠性

  • 单个部署单元

  • 通讯安全

  • 快速开球

  • 轻松迁移到分布式架构。

  • 业务自主模块

  • 模块化结构

  • 所有业务功能的可测试性

  • 从单体架构过渡到微服务的开放方式。

  • 松耦合

  • 数据重复

  • 维护数据完整性更加困难。

  • 难以维护——可能会出现问题的地方。

  • 水平扩展更加困难(需要更多资源,至少在理论上)。

  • 永久依恋一种技术。

作者:Arkadiusz Rosloniec

更多内容请关注公号【云原生数据库

squids.cn,云数据库RDS,迁移工具DBMotion,云备份DBTwin等数据库生态工具。

你可能感兴趣的:(技术专栏,微服务,架构,云原生,sql)