上个月,我写了一篇关于模块化单体和现代单体架构的价值的文章。该文章(和视频)中出现的更有趣的讨论之一是逆向讨论:什么时候仍然选择微服务是正确的?
与任何设计选择一样,答案是主观的并且取决于很多因素。但是我们仍然可以使用一般的经验法则和全球指标。在我们进入这些问题之前,我们需要了解拥有微服务架构意味着什么。然后我们可以衡量拥有这样一个架构的好处和代价。
一个常见的误解是微服务只是简单地分解为单体。事实并非如此。我和很多仍然持有这种观点的人谈过,公平地说,他们可能有道理。AWS 是这样定义微服务的:
微服务是一种软件开发的架构和组织方法,其中软件由通过定义明确的 API 进行通信的小型独立服务组成。这些服务由独立的小型团队拥有。
微服务架构使应用程序更易于扩展和更快地开发,从而实现创新并加快新功能的上市时间。
较小的单体可能符合该定义,但如果您从字里行间中读到,则它们不符合。“独立”和“更易于扩展”这两个词暗示了这个问题。单体的问题(和优势)是单点故障。通过一项服务,我们通常可以更容易地发现问题。架构要简单得多。
如果我们将此服务分解成更小的部分,我们实际上会创建分布式故障点。如果链条上的一个部分出现故障,整个架构就会崩溃。这不是独立的,也不容易扩展。微服务不是小单体,分解单体不仅仅是处理较小的项目。这是关于改变我们的工作方式。
是什么造就了微服务?
一个好的微服务需要遵循以下健壮性和规模原则:
- 按业务功能划分:这是一个逻辑划分。微服务是提供完整包的独立“产品”。这意味着负责微服务的团队可以在没有依赖关系的情况下进行业务所需的所有更改。
- 通过 CI/CD 实现自动化:如果没有持续交付,更新成本将消除微服务的所有优势。
- 独立部署:这是隐含的,因为对一个微服务的提交只会触发该特定服务的 CD。我们可以通过 Kubernetes 和基础架构即代码 (IaC) 解决方案来实现这一点。
- 封装:它应该隐藏底层的实现细节。服务充当独立产品,为其他产品发布 API。我们通常通过 REST 接口以及消息传递中间件来实现这一点。API 网关进一步增强了这一点。
- 去中心化,没有单点故障:否则,我们会分散故障。
- 故障应该被隔离:否则,单个服务宕机可能会产生多米诺骨牌效应。断路器可能是隔离故障的最重要工具。为了满足这种依赖性,每个微服务都处理自己的数据。这意味着很多数据库,有时可能具有挑战性。
- 可观察的:这是处理大规模故障所必需的。没有适当的可观察性,我们实际上是盲目的,因为各个团队可以自动部署。
这一切都很好,但实际上这意味着什么?
它的大部分意思是我们需要对我们处理一些重要想法的方式做出几项重大改变。我们需要将更多的复杂性转移给 DevOps 团队。我们需要以不同的方式处理跨微服务的事务状态。这是处理微服务时最难掌握的概念之一。
在理想的世界中,我们所有的操作都将很简单,并包含在一个小型微服务中。围绕我们的微服务的服务网格框架将处理所有全球复杂性并为我们管理我们的个人服务。但这不是真实的世界。实际上,我们的微服务可能具有在服务之间传输的事务状态。外部服务可能会失败,为此,我们需要采取一些独特的方法。
依赖 DevOps 团队
如果您的公司没有优秀的 DevOps 和平台工程团队,微服务就不是一个选择。由于迁移,我们可能会部署数百个应用程序,而不是部署一个应用程序。虽然单个部署简单且自动化,但您仍然会在操作上投入大量工作。
当某些东西不起作用或无法连接时。当需要集成新服务或需要采用服务配置时。在使用微服务时,运营会承担更大的负担。这需要良好的沟通和协作。这也意味着管理特定服务的团队需要重新承担一些 OPS 负担。这不是一项简单的任务。
作为开发人员,我们需要了解许多用于将我们的单独服务绑定回单个统一服务的工具:
- 服务网格:让我们组合独立的服务,并有效地充当它们之间的负载均衡器。它还提供安全、授权、流量控制等功能。
- API 网关:应该使用而不是直接调用 API。这有时会很尴尬,但通常对于避免成本、防止速率限制等来说是必不可少的。
- 特征标志和秘密:在单体中也很有用。但如果没有专用工具,它们就不可能在微服务规模上进行管理。
- 熔断:让我们终止断开的 Web 服务连接并优雅地恢复。否则,单个损坏的服务可能会导致整个系统崩溃。
- 身份管理必须是独立的:在处理微服务环境时,您无法摆脱数据库中的身份验证表。
我将跳过编排、CI/CD 等,但它们也需要针对出现的每项服务进行调整。其中一些工具对开发人员来说是不透明的,但我们在所有阶段都需要 DevOps 的帮助。
传奇模式
无状态服务将是理想的,承载状态会使一切变得更加复杂。如果我们将状态存储在客户端中,我们需要一直来回发送它。如果它在服务器上,我们将需要不断获取它、缓存它或将其保存在本地,然后所有交互都将针对当前系统执行。这消除了系统的可扩展性。
典型的微服务将存储在自己的数据库中并使用本地数据。需要远程信息的服务通常会缓存一些数据以避免往返于其他服务。这是微服务可以扩展的最大原因之一。在单体中,数据库应该成为应用程序的瓶颈,这意味着单体是高效的并且受限于我们存储和检索数据的速度。这有两个主要缺点:
- 大小:我们拥有的数据越多;数据库越大,性能会同时影响所有用户。想象一下,查询亚马逊上每次购买的 SQL 表只是为了找到您的特定购买。
- 域:数据库有不同的用例。一些数据库针对一致性、写入速度、读取速度、时间数据、空间数据等进行了优化。跟踪用户信息的微服务可能会使用时间序列数据库,该数据库针对与时间相关的信息进行了优化,而购买服务将专注于传统的保守 ACID 数据库。
- 注意:一个整体可以使用多个数据库。这可以很好地工作并且非常有用。但这是例外。不是规则。
saga 模式通过使用补偿事务来撤销 saga 失败时的影响。当 saga 失败时,将执行补偿事务以撤消前一个事务所做的更改。这允许系统从故障中恢复并保持一致的状态。我们可以使用 Apache Camel 等工具来完成此操作,但这并非易事,并且需要比现代系统中的典型事务更多的参与。这意味着对于每个主要的跨服务操作,您都需要执行等效的撤消操作来恢复状态。那是不平凡的。有多种用于 saga 编排的工具,但这是一个超出本文范围的大主题,我仍然会从广义上对其进行解释。
了解 saga 的重要之处在于它避免了经典的 ACID 数据库原则,而侧重于“最终一致性”。这意味着操作会在某个时候使数据库处于一致状态。那是一个非常艰难的过程。想象调试一个只有在系统处于不一致状态时才会出现的问题。
下图从广义上展示了这个想法。假设我们有一个汇款流程:
对于汇款,我们需要先分配资金。
然后我们验证收件人是否有效且存在。
接下来,我们需要从我们的账户中扣除资金。
最后,我们需要将钱添加到收款人的帐户中。
那就是一笔成功的交易。对于常规数据库,这将是一个事务,我们可以在下图中左侧的蓝色列中看到它。但如果出现问题,我们需要运行相反的过程:
如果分配资金失败,我们需要移除分配。我们需要创建一个单独的代码块来执行分配的逆操作。
如果验证收件人失败,我们需要删除该收件人。然后我们还需要删除分配。
如果扣除资金失败,我们需要恢复资金,移除接收者,移除分配。
最后,如果向收款人添加资金失败,我们需要运行所有的撤销操作!
saga 中的另一个问题在 CAP 定理中得到了说明。CAP 代表一致性、可用性和分区容错性。问题是我们需要选择任意两个……别误会我的意思,你可能三个都选。但是,在失败的情况下,你只能保证两个。
可用性意味着请求收到响应。但不能保证它们包含最新的写入。
一致性意味着每次读取都会收到最近的错误写入。
容忍意味着即使许多消息在途中被丢弃,一切都会继续工作。
这与我们处理交易失败的历史方法大不相同。
我们应该选择微服务吗?
希望您了解正确部署微服务有多么困难。我们需要做出一些重大妥协。这种新方式不一定更好,在某些方面,它更糟。但是微服务的支持者还是有道理的,我们可以通过微服务获得很多,也应该关注这些好处。
我们预先提到了第一个要求:DevOps。拥有一支优秀的 DevOps 团队是考虑微服务的先决条件。我看到团队试图在没有 OPS 团队的情况下解决这个问题,他们最终花在操作复杂性上的时间比编写代码还多。这是不值得的努力。
微服务最大的好处是给团队的。这就是为什么拥有稳定的团队和范围至关重要的原因。将团队拆分成独立工作的垂直团队是一个巨大的好处。世界上模块化程度最高的单体无法与之抗衡。当我们有数百名开发人员单独跟踪 git 提交时,跟踪代码的规模变化就变得站不住脚了。微服务的价值只有在大团队中才能体现出来。这听起来很合理,但在创业环境中,事情突然发生了变化。我的一位同事在一家雇佣了数十名开发人员的初创公司工作。他们决定遵循微服务架构并构建了很多。然后是缩减和维护多种语言的数十种服务成为一个问题。
拆分单体很难但可行。将微服务统一到一个整体可能更难,我不知道有谁认真尝试过这样做但很想听听故事。
不是一个尺寸
要迁移到微服务架构,我们需要进行一些思维转变。一个很好的例子是数据库和用户跟踪微服务。在整体中,我们会将数据写入表并继续我们的工作。但这是有问题的。
随着数据规模的扩大,这个用户跟踪表最终可能包含大量数据,这些数据很难在不影响操作系统其余部分的情况下进行实时分析。通过微服务,我们可以提供几个优势:
微服务的接口可以使用消息传递,这意味着发送跟踪信息的成本将降至最低。
跟踪数据可以使用时间序列数据库,这对于这个用例来说会更有效。
我们可以流式传输数据并异步处理它以从该数据中获取额外的价值。
存在复杂性,数据将不再本地化。因此,如果我们异步发送跟踪数据,我们需要发送所有必要的信息,因为跟踪服务将无法返回到原始服务以获取额外的元数据。但它具有位置优势,如果有关跟踪存储的法规发生变化,则只有一个地方可以存储它。
动态控制和推出
您是否曾经按下过发布按钮导致生产中断?
我做了不止一次(太多次了)。那是一种可怕的感觉。微服务在生产中仍然会失败,并且仍然会发生灾难性的失败,但是,它们的失败通常是局部的。将它们推广到系统的特定子集 (Canary) 并进行验证也更容易。这些都是可以由实际掌握用户脉搏的人深入控制的策略:OPS。
微服务的可观察性是必不可少的、昂贵的,但也更强大。由于一切都发生在网络层,因此都暴露给可观察性工具。SRE 或 DevOps 可以更详细地了解故障。这是以开发人员为代价的,他们可能需要面对增加的复杂性和有限的工具。
应用程序可能会变得太大而不能失败。即使采用模块化,一些最大的单体应用也有如此多的代码,运行一个完整的 CI/CD 周期需要数小时。然后,如果部署失败,恢复到上一个好的版本也可能需要一段时间。
分割
过去,我们曾经根据层级划分团队。客户端、服务器、数据库等。这是有道理的,因为每一个都需要一套独特的技能。今天,垂直团队更有意义,但我们仍然有专长。
通常,移动开发人员不会在后端工作。但是假设我们有一个移动团队想要使用 GraphQL 而不是 REST。对于单体,我们要么告诉他们“接受它”,要么我们必须完成这项工作。有了微服务,我们可以用很少的代码为他们创建一个简单的服务。核心服务的简单外观。我们不需要担心移动团队编写服务器代码,因为这相对孤立。我们可以对每个客户层做同样的事情,这样更容易垂直整合一个团队。
太大
很难将手指放在使整体式应用不切实际的尺寸上,但这是您应该问自己的问题:
我们拥有或想要多少支球队?
如果你有几个团队,那么单体应用可能会很棒。如果您有十几个团队,那么您可能会在那里遇到问题。
衡量拉取请求和问题解决时间
随着项目的增长,您的拉取请求将花费更多时间等待合并,并且问题将需要更长的时间来解决。这是不可避免的,因为项目的复杂性趋于增加。请注意,新项目将具有更大的功能,一旦您在项目统计中考虑到生产力的下降应该是可衡量的,这可能会影响结果。
请注意,这是一个指标。在许多情况下,它可以指示其他事情,例如需要优化测试管道、审查流程、模块化等。
我们有知道代码的专家吗?
在某个时候,一个庞大的项目变得如此之大,以至于专家们开始忘记细节。当错误变得难以为继并且没有权威人物可以不经咨询就做出决定时,这就成为一个问题。
你愿意花钱吗?
微服务将花费更多。没有办法解决这个问题。在某些特殊情况下,我们可以调整规模,但最终,可观察性和管理成本将消除任何潜在的成本节约。由于人员成本通常超过云托管成本,因此总成本可能仍对您有利,因为如果规模足够大,这些成本可能会降低。
取舍
下面的雷达图很好地说明了单体与微服务的权衡。请注意,此图表是为大型项目设计的。项目越小,单体应用的前景就越好。
请注意,微服务在容错和团队独立性方面为大型项目带来了好处。但他们付出了代价。他们可以减少研发支出,但他们主要将其转移到 DevOps,因此这并不是一个主要的好处。
最后一句话
微服务的复杂性是巨大的,有时会被实施团队忽略。开发人员使用微服务作为一个大棒来抛弃他们不想维护的系统部分,而不是构建一个可持续的、可扩展的架构来取代单体。
我坚信项目应该从单体开始。微服务是扩展团队的优化,过早优化是万恶之源。问题是,什么时候做这样的优化合适?
我们可以使用一些指标来简化决策。最终,这种变化不仅仅是拆分单体。这意味着重新思考交易和核心概念。从单体开始,我们就有了一个蓝图,我们可以使用它来调整我们的新实现,因为它会加强。