第 12 章:可扩展性的途径

本章内容概览:

  • 将微服务扩展到更大的开发团队
  • 根据需求增长扩展微服务
  • 减轻变更引起的问题
  • 理解基本的安全顾虑
  • 将单体应用转换为微服务
  • 可能性的范围与混合方法
  • 在预算内使用微服务构建

本章内容涵盖了如何将微服务扩展至更庞大的开发团队,应对需求的增长进行微服务的扩展,缓解变更可能带来的影响,深入探讨基本的安全问题,以及如何将传统的单体应用转换为微服务架构。我们还将讨论在预算约束下构建微服务的可能性与混合策略。

全书的目标是打造一个生产级的微服务应用。我们已经介绍了多种技术和策略,以快速且成本效益高的方式部署微服务。虽然以 FlixTube 为例的应用结构相对简单,但已涵盖了许多使应用可扩展的微服务架构特性。

本章将探讨如何管理日益增长的微服务应用,包括扩大开发团队和应对客户需求不断增长的挑战。同时,我们还将探讨微服务与安全问题的关系,并略述如何将现有的单体应用迁移至微服务架构。

本书将以强调那些简化微服务部署的策略结束,这些建议尤其适用于小团队、初创公司或独立开发者,帮助他们在拥有广阔未来可能性的同时,以最小的成本启动微服务应用。

12.1 我们的未来是可扩展的

微服务为我们打开了众多扩展应用的大门。本章我们将讨论未来需要采取的措施,以扩展应用和开发流程,适应日益增长的应用需求。我们将探讨应用性能的扩展方法,以提高处理能力和吞吐量。

你可能目前还不需要采用这些技术,但随着应用规模的扩大、开发团队的增长或客户基数的扩大,这些技术将变得必不可少。

我们正进入一个高级技术领域,主要目的是为你揭示未来可能需要采取的扩展措施。虽然只是初探门径,但足以使你对未来的道路有所预见。

本章所讨论的扩展问题,若你不得不面对,实际上是一个好迹象。这意味着你的业务正在取得成功,你的客户群正在扩大。此时,你会真切感受到选择微服务架构的优势,因为它使得扩展过程更为直接和简单。

本章并非实操性强的内容,而是提供一些洞察,帮助你预见微服务旅程的可能走向。虽然本章介绍的许多技术都易于尝试,但在实验过程中仍有可能出错,甚至无意中损害你的应用集群。

因此,请勿在关键的生产基础设施上尝试这些技术,以免影响现有的员工或客户依赖的服务。但你可以参考第 10 章的指引,启动一个新的 FlixTube 生产实例进行实验,这种方式无风险,允许你自由尝试本章介绍的任何技术。

12.2 扩展开发过程

首先,我们需要解决的是如何扩展我们的开发过程。在本书的前几章,我们体验了从单个开发者在较小微服务应用中的开发过程和生产工作流。而在第 10 章,我们尝试将整个应用放置在单一的代码仓库中。现在,我们需要将关注点从单一开发者扩展至整个团队。

至此,我们采用的简化流程非常适合小团队和规模较小的产品:

  • 开发者在单一代码库上工作,负责编写和测试代码。
  • 代码变更推送至托管的代码仓库,触发持续部署(CD)流程,自动将更新部署至生产环境。

这种流程为快速开发和部署新应用提供了极大便利。但随着时间推移,我们的初步开发流程开始显现出局限:

  • 我们不希望代码直接从开发者提交至客户。我们期望开发者能够在类生产环境中测试他们的代码,但同时需要确保这些正在开发中的功能能在对客户造成影响之前得到充分的验证和测试。
  • 我们不希望开发者互相干扰。随着开发团队的扩大,同一代码库中的开发者更易发生代码冲突和构建失败。
  • 单一代码仓库的扩展性受限。为了应对不断增长的应用复杂性,我们可能需要拆分单一代码仓库,确保即使应用极为复杂,每个微服务的代码库仍保持精简、清晰和易于管理。

为了构建一个可扩展的开发流程,适应多团队工作,并充分利用微服务架构的优势,我们必须对现有流程进行重组。

12.2.1 多个团队的扩展

随着我们的应用逐步扩大,我们需要增加更多的微服务以支持新功能并提升应用的整体性能。这也意味着,为了应对日益增长的工作量,我们必须相应地扩展我们的开发团队。当一个团队的规模变得过大时,我们需要将其拆分为多个更小的团队,这样不仅可以保持团队的灵活性,还能利用小团队之间更高效的沟通和组织优势。

微服务架构为我们提供了天然的划分界限,我们可以依此将不同的服务分配给不同团队来开发。如图 12.1 所示,我们在项目初期采用了简单的开发流程,当时只有一个团队负责所有微服务的开发。

图 12.1 在启动新应用时,它应该足够小,单个团队可以自行管理所有微服务。
图 12.1 在启动新应用时,它应该足够小,单个团队可以自行管理所有微服务。

图 12.2 展示了我们的团队结构在扩展后的样子。我们将应用拆分,让每个团队负责一组特定的微服务,并确保团队之间的职责没有重叠。这有助于减少团队间的相互干扰。现在,我们可以根据微服务的边界来拓展团队规模。

图 12.2 随着我们应用的增长,开发可以分开,以便不同的团队管理独立的微服务或微服务组。
图 12.2 随着我们应用的增长,开发可以分开,以便不同的团队管理独立的微服务或微服务组。

图 12.2 随着我们应用的增长,开发可以分开,以便不同的团队管理独立的微服务或微服务组。

每个团队将拥有一个或多个微服务,并通常负责从编码到测试,再到生产环境的所有阶段。团队还通常负责其微服务的运维需求,确保其功能性、健康状态和性能。

当然,不同公司的团队结构和开发流程在细节上可能会有所不同,但这种组织自足团队的方式具有很好的可扩展性,这意味着我们可以围绕一个庞大的应用构建一个庞大的公司,同时保持高效的开发流程。

12.2.2 独立的代码仓库

至今为止,我们的 FlixTube 应用一直使用单一的代码仓库(我们的单一仓库)进行开发。图 12.3 展示了这种方式的基本结构。

图 12.3 在启动新的微服务应用时,将所有微服务包含在一个代码仓库中会更简单。
图 12.3 在启动新的微服务应用时,将所有微服务包含在一个代码仓库中会更简单。

在项目初期,使用单一仓库可以简化事务——减少复杂性,同时也简化了开发基础设施的建立和管理。考虑到我们可以为每个微服务设立独立的持续部署(CD)管道,单一仓库在微服务的旅程中可以走得很远(参考第 10 章 10.10.4 节的讨论)。

重要的是,有效利用微服务意味着它们需要拥有独立的部署计划。如果只有一个 CD 管道,对所有微服务进行一次部署将变得非常繁琐,因为即使只有一个微服务发生变化,也需要重建和重新部署所有微服务。幸运的是,我们使用的 GitHub Actions 支持在单一代码仓库中设置多个 CD 管道,这允许我们为每个微服务独立运行 CD 管道,确保代码变更只会影响相关的微服务。

在可能的情况下,应该尽量将单一代码仓库的使用推向极致,保持尽可能的简单。然而,随着团队和应用的扩展,会有一个阶段,单一代码仓库似乎开始限制了你的灵活性和成长。当你感受到这种压力时,就是开始考虑将微服务迁移到独立代码仓库的时候了(如图 12.4 所示)。

图 12.4 随着我们应用的增长,我们需要将微服务分离到独立的代码仓库中,以便每个微服务的代码保持简单和独立。
图 12.4 随着我们应用的增长,我们需要将微服务分离到独立的代码仓库中,以便每个微服务的代码保持简单和独立。

不过,应该等到真正感受到必要性时再进行拆分——我们只在单一代码库变得难以管理时才进行分离。通过将一个庞大的代码库拆分为多个更小的部分,我们可以简化复杂度。虽然整个微服务应用可能依然复杂,但每个独立的代码库和微服务都将保持简单,它们之间的交互也更加明确。这回归到了第 1 章 1.4 节的讨论:微服务架构旨在将复杂的整体分解成简单的部分。

将代码库分离可能看起来是一项复杂的工作,我完全理解这种感觉。实际上,这种复杂性通常是人们认为微服务架构复杂的原因之一。

尽管我们的应用可能最终变得非常复杂,但如果我们能够关注于单个微服务,整个视角会截然不同。事情突然变得简单了许多。这是因为复杂性是逐步累积的,更易于管理。当我们专注于单个微服务时,这些服务本身是简单的,而不是专注于整体应用,这样我们对应用的整体复杂性的感受也会减少。

这实际上是解决微服务应用复杂性的方法。每个独立的微服务都是一个小而易于理解的单元,拥有简洁的代码和简单的部署过程。每个微服务都易于管理,而它们合在一起却能构建出功能强大的复杂应用。这种从复杂应用到简单微服务的视角转变是管理复杂性的关键。

我们通过将开发过程细分成微服务大小的块,虽然引入了一些额外的复杂性,但这与应用最终可能达到的复杂程度相比简直微不足道。通过将注意力从整体应用的复杂性转移到单个微服务上,我们实际上使应用能够扩展到真正的大规模,同时保持每个微服务的简洁和易用性。

不过,不要过早地冲动将微服务迁移到独立的代码仓库。如果你过早进行这一步,可能会发现自己为过渡支付了成本,而这个阶段你还无法从中得到足够的好处。你不希望在享受其优势之前就开始支付成本。

良好的软件开发需要做出明智的权衡。只要单一代码仓库对你来说还有意义,就继续使用它。但要意识到这不是长久之计。随着应用和团队的不断扩大,这种简单的方式最终会失效。到了某个点,分离代码仓库可能是管理大规模复杂性、保持高效开发流程的必要步骤。

12.2.3 分离代码仓库

我们的首要任务是将单一的代码仓库分割成多个独立的仓库,每个微服务都拥有自己专属的代码仓库,其中包括微服务的代码以及部署至生产环境的相关脚本。

此外,我们开发的用于构建基础设施的 Terraform 代码(见第 7 章),负责创建容器仓库和 Kubernetes 集群,也应单独配置一个代码仓库。由于这部分代码不属于任何具体的微服务,它需要独立管理。

图 12.5 展示了如何将第 10 章中的 FlixTube 项目拆分成多个代码仓库。我们首先使用git init命令为每个微服务创建一个新的空仓库,然后将相应的代码迁移过去并进行提交。此外,为了保留现有的版本历史,我们可能需要采取额外的步骤(详见下方侧栏)。

图 12.5 当我们分离代码仓库时,每个微服务的子目录变成其自己的独立 Git 仓库。
图 12.5 当我们分离代码仓库时,每个微服务的子目录变成其自己的独立 Git 仓库。

保留版本历史记录

创建新仓库时,要从旧仓库保留版本历史,我们可以使用git filter-branch命令并附带--subdirectory-filter参数。更多信息请参阅 Git 文档: https://git-scm.com/docs/git-filter-branch

你也可以在线查找使用“filter-branch”的示例——有众多资源可供参考!

12.2.4 元仓库

是否觉得使用多个独立的代码仓库有些繁琐?还怀念过去只用一个代码仓库的简单日子吗?现在有了一个好消息。

我们可以创建一个元仓库,这样可以把所有独立的仓库整合成一个统一的聚合代码仓库。可以将元仓库看作是一种虚拟的代码仓库,这意味着我们能够在不牺牲各独立仓库的灵活性和独立性的前提下,恢复单一仓库的一些简单性和便利性。创建元仓库需要使用 meta 工具,详见: https://github.com/mateodelnorte/meta

通过配置一个名为.meta 的配置文件,元仓库链接了一组独立的仓库。图 12.6 展示了 FlixTube 项目中.meta 文件的位置示例,而清单 12.1 则展示了这个文件的结构。

图 12.6 .meta 配置文件将独立的仓库连接成一个元仓库。
图 12.6 .meta 配置文件将独立的仓库连接成一个元仓库。

清单 12.1 配置 FlixTube 的元代码仓库(.meta)

{
  "projects": {
    "gateway": "[email protected]:bmdk/gateway.git",
    "azure-storage": "[email protected]:bmdk/azure-storage.git",
    "video-streaming": "[email protected]:bmdk/video-streaming.git",
    "video-upload": "[email protected]:bmdk/video-upload.git",
    "history": "[email protected]:bmdk/history.git",
    "metadata": "[email protected]:bmdk/metadata.git"
  }
}
  • 列出了构成这个元仓库的独立代码仓库。
  • 引用了位于 GitHub 上的微服务代码(示例,并非实际存在)。

使用 meta 工具,我们可以执行影响整个仓库集合的单一 Git 命令。例如,如果我们想一次性拉取 FlixTube 项目下所有微服务的代码更新,我们可以使用 meta 来实现:

meta git pull

我们仍在处理独立的代码仓库,但通过 meta,我们可以同时对多个仓库执行命令,给人一种在处理单一代码仓库的感觉。

Meta 为你提供额外的灵活性。你可以用它来管理自己的微服务集。作为大团队的一员,你可以为自己经常处理的微服务集创建一个元仓库。其他开发者可能有他们自己的独立元仓库。你甚至可以创建多个元仓库,根据当前的工作需要轻松切换不同的微服务集。

作为团队领导,你可以为应用的不同部署配置创建独立的元仓库,每个元仓库都有自己的 Docker Compose 文件。这使得团队成员可以轻松克隆整套微服务代码,并使用 Docker Compose 启动应用配置。这是为团队提供即时且可管理的开发环境的绝佳方式。

12.2.5 创建多个环境

随着我们应用用户的增长,变得越来越重要的是要确保他们不会受到开发中的功能或部分完成的新功能带来的负面影响。开发团队需要在类似生产的环境中测试他们的代码,以确保其稳定性后再向客户展示。

虽然每个开发者都会在自己的计算机上进行代码测试,但这还远远不够。他们还需要在代码与其他开发者的更改整合后进行测试。这种测试应该在一个模拟生产的环境中进行——显然,这个环境不能是客户正在使用的那一个!

我们需要一个能够让开发者将他们的更改从本地开发环境推送到集成环境,再到测试环境,最终在所有测试通过后,部署到面向客户的生产环境的工作流程。尽管没有两个公司的工作流会完全相同,但你可以在图 12.7 中看到一个典型的工作流是怎样的。

图 12.7 将代码更改推进到开发和测试环境,然后再进入生产环境
图 12.7 将代码更改推进到开发和测试环境,然后再进入生产环境

建立多个环境实际上相对简单,我们已经在第 7 章中使用的 Terraform 代码涵盖了大部分所需功能。我们已经通过app_name变量参数化了代码,利用它根据分配的名称创建独立的应用资源(参见第 7 章第 7.10.2 节,首次引入这个变量)。

现在,我们可以在执行 Terraform 命令时使用app_name来创建不同用途的环境实例。我们只需为每个环境提供一个不同的名称,比如设置app_nameflixtube-developmentflixtube-testflixtube-production,来创建我们的独立环境。

我们可以通过引入一个新的变量environment来进一步改进(如清单 12.2 所示,更新第 9 章/example-1/scripts/variables.tf),然后将app_name转化为一个依赖于environment变量值的本地计算变量。

清单 12.2 在 Terraform 中根据环境派生app_name

variable "environment" {}

locals {
  app_name = "flixtube-${var.environment}"
}
  • 添加一个新的 Terraform 变量,指定当前环境。需要在执行 Terraform 命令时提供该变量,将其设置为 development、test 或 production 等。
  • 创建一个本地变量app_name,为每个环境构建独立版本的应用(例如,flixtube-developmentflixtube-testflixtube-production)。

引入新变量(environment)允许我们从命令行设置当前环境。清单 12.3 展示了如何从命令行向 Terraform 提供变量值,包括新的environment变量。

我们可以重用同一个 Terraform 项目来创建任意数量的独立环境,所有环境都托管在同一个云账户中,通过名称进行区分(如flixtube-developmentflixtube-testflixtube-production)。这种方法使我们能够创建如图 12.7 所示的工作流,或者根据需要设计更复杂的工作流。

清单 12.3 从命令行设置 Terraform 变量

terraform init
terraform apply -auto-approve \
 -var "app_version=$VERSION" \
 -var "client_id=$ARM_CLIENT_ID" \
 -var "client_secret=$ARM_CLIENT_SECRET" \
 -var "environment=$ENVIRONMENT" \
 -var "storage_account_name=$STORAGE_ACCOUNT_NAME" \
 -var "storage_access_key=$STORAGE_ACCESS_KEY"
  • 通过环境名称参数化我们的 Terraform 代码。我们通过操作系统环境变量传入正在部署的环境名称。

12.2.6 生产工作流

随着我们的应用发展,创建多个环境成为了确保客户不受不稳定代码影响的关键策略。剩下的问题是如何触发特定环境的部署?这实际上比你想象的简单。

我们可以在代码仓库中为不同的环境设立独立的分支来进行部署。图 12.8 展示了一个基本的分支策略,尽管实际上可能会更加复杂。

开发团队在开发(或主)分支上进行日常工作。当代码推送至该分支时,会触发 CI 流程,运行测试并通过 CD 流程将代码部署到开发环境。这允许我们的团队在一个类生产环境中频繁集成和测试他们的更改。

开发人员应该多频繁推送代码变更呢?尽可能频繁地推送,理想情况下每天至少一次。代码合并的时间越短,由于冲突更改和错误集成导致的问题就越少。这正是持续集成(CI)的核心思想,它是支持持续部署(CD)的重要实践。

较不频繁地(如每周一次),我们会将代码从开发分支合并到测试分支,这会触发到测试环境的部署。从开发到测试的合并较少,这为我们提供了时间进行测试、修复问题并在向客户提供之前确保代码稳定。

最终,当测试分支的代码经过充分验证并准备好上线时(通常是每一到两周一次),我们会将其合并到生产分支。这会将更新后的微服务部署到生产环境,使客户可以使用新增的功能和错误修复。

图 12.8 开发、测试和生产分支的代码自动部署到相应的环境。
图 12.8 开发、测试和生产分支的代码自动部署到相应的环境。

这种通过分支管理代码的工作流可以选择是否搭配自动化测试。它为手动和探索性测试留出空间,同时允许管理者做出有意识的决策以部署到生产环境。自动化测试的加入会使这个过程更加健全和可扩展。如果在工作流的任何阶段自动化测试失败,部署将自动被阻止。快速而可靠的自动化测试的引入意味着我们可以安全地增加部署频率,很多现代企业甚至每天都会部署代码到生产环境。

使用 GitHub Actions,我们可以为每个分支轻松配置独立的 CD 管道,如清单 12.4 所示。这个特定工作流适用于主分支(或开发分支)的网关微服务,但我们同样可以轻松为其他分支(测试和生产)配置独立的工作流,将此微服务部署到不同的环境。

清单 12.4 配置 CI/CD 管道的分支

name: Deploy gateway

on:
  push:
    branches:
      - main
    paths:
      - gateway/**  # 针对网关微服务(假设是单一仓库)触发此工作流

workflow_dispatch:  # 允许通过 GitHub Actions UI 手动触发部署

jobs:
  # --省略--

实施这一多分支/多环境策略的关键是每个环境都拥有独立的 Terraform 状态,这样我们在更新基础设施时能够追踪每个独立环境的状态。这是一个复杂的任务,但通过一些研究,我们可以将 Terraform 状态存储在云存储中(如 Azure Storage),并根据环境名称(开发、测试或生产)进行配置。这样我们就能持久化 Terraform 状态,并在 CI/CD 管道中运行 Terraform 来更新我们的基础设施,当我们对 Terraform 代码库进行更改时。

12.2.7 将应用配置与微服务配置分离

本书中,我们一直推荐将微服务的配置与应用配置放置在同一代码仓库中。这种方法简单方便,便于部署,因为当我们推送微服务代码时,CD 管道不仅构建和发布微服务,还将其部署到我们的集群中。虽然这种方式简化了部署流程,但在尝试扩展时,可能会发现它具有一定局限性。

另一种常见的做法是创建一个与微服务代码库分离的配置仓库,专门用来存放应用的配置。这个应用配置代码仓库用于设定当前部署的微服务版本号及其环境变量。你可以在图 12.9 中看到这种配置的实例。

实行这种更改后,部署将分为两个阶段:

  1. 第一阶段,微服务的 CD 管道仅负责构建和发布特定微服务。
  2. 第二阶段,在应用配置仓库中更新微服务的版本号,并推送更改,触发 CD 管道将更新后的微服务部署到我们的集群中。

显然,将应用配置分离到另一个代码仓库会使部署过程更加复杂,并可能增加出错的机会——想象一下,如果更新微服务版本号时不小心输入了旧版本号而非新版本号的情形。因此,不要匆忙采取这种变更,除非真正有必要,因为它可能使事情变得更加困难。

图 12.9 使用独立的代码仓库将微服务配置与应用配置分离
图 12.9 使用独立的代码仓库将微服务配置与应用配置分离

应用配置的独立管理有多方面的好处:

  • 将所有应用配置集中在一处,便于查找和管理。
  • 在一个地方查看所有应用更新的历史。
  • 管理多个配置不同的应用实例。
  • 从一个环境向另一个环境推广特定版本的微服务(无需重新构建和测试)。
  • 对应用配置仓库的访问可以受到限制,同时允许所有开发人员访问代码仓库(这有助于安全和审计)。

12.3 性能扩展

微服务架构不仅可以扩展以支持更大的开发团队,还可以扩展应用的性能,增加处理能力,应对更大的工作负载。

使用微服务架构,我们可以精确控制应用的性能。我们可以容易地测量单个微服务的性能(如图 12.10 所示),并识别出在高峰时期性能不佳、过载或超负荷的微服务。

图 12.10 在 Kubernetes 仪表板中查看微服务的 CPU 和内存使用情况
图 12.10 在 Kubernetes 仪表板中查看微服务的 CPU 和内存使用情况

相较之下,如果使用单体应用,性能控制会受到限制。虽然我们可以垂直扩展单体应用,但水平扩展单体应用要困难得多,无法独立扩展其任何部分。在这种情况下,可能只是应用的一小部分造成了性能问题,但我们不得不扩展整个应用来解决问题,这是一项昂贵的操作。

使用微服务,我们有多种扩展选项。我们可以独立微调系统的部分性能,消除瓶颈,获得最佳的性能组合。本节将概述一些扩展微服务应用的方法:

  • 垂直扩展整个集群。
  • 水平扩展整个集群。
  • 水平扩展单个微服务。
  • 弹性扩展整个集群。
  • 弹性扩展单个微服务。
  • 扩展数据库。

注意 所谓的“简单”,是相对于我们经过前面 11 章学习后的背景而言。

扩展通常涉及对集群进行高风险的配置更改。不建议直接在依赖生产集群上进行这些更改。稍后,我们将介绍蓝绿部署,这是一种在管理大型基础设施变更时降低风险的方法。

12.3.1 垂直扩展集群

随着应用的发展,我们可能会发现集群在应对增加的工作负载时,整体上缺乏足够的计算资源、内存或存储能力。随着新微服务的加入或现有微服务的复制以实现冗余,集群中的节点资源可能最终会被耗尽。这可以通过 Azure 门户或 Kubernetes 仪表板来监控。在这种情况下,我们需要增加集群的总体资源。在 Kubernetes 集群中,我们可以通过垂直或水平扩展来增强微服务的规模。

图 12.11 展示了在 Kubernetes 上进行垂直扩展的过程。我们通过增加节点池中虚拟机(VM)的规模来实现集群的扩展。比如,原本配置的是三台小型 VM,后来我们增加了它们的规模,改为了三台大型 VM。这里的变化并不是增加 VM 的数量,而是提升了现有 VM 的规格。

图 12.11 通过增加 VM 的大小垂直扩展集群
图 12.11 通过增加 VM 的大小垂直扩展集群

使用 Terraform,我们可以调整 vm_size 参数,从 Standard_B2s 改为 Standard_B4ms(参见第 7 章第 7.11.1 节的初始设置)。这个更改增加了 Kubernetes 节点池中每个 VM 的规模。现在,每个节点拥有四个虚拟 CPU 而非两个,同时内存和硬盘容量也得到了增加。你可以在以下链接查看 Azure VM 的各种规格对比: http://mng.bz/0lxv

尽管集群的节点数没有变化,节点的规模却有了显著增加。扩展集群就像修改代码那样简单。这正体现了基础设施即代码的强大功能,允许我们将基础设施配置保存为代码,并通过修改这些代码来实施更改。但要注意,使用更强大的计算资源意味着更高的费用,因此更大的 VM 会带来更高的成本。

清单 12.5 使用 Terraform 垂直扩展集群

default_node_pool {
 name = "default"
 node_count = 1
 vm_size = "Standard_B4ms" // 为集群中的每个节点设置更大的 VM
}

12.3.2 水平扩展集群

除了垂直扩展,我们还可以通过水平扩展来增强集群的能力。在这种方法中,我们保持虚拟机(VM)的规格不变,但增加更多的 VM 数量。这样,应用的负载可以分散到更多的计算资源上。

图 12.12 展示了如何将集群从三台 VM 扩展到六台。每台 VM 的规格保持不变,但通过增加 VM 的数量,我们获得了更多的计算能力。

图 12.12 通过增加 VM 的数量水平扩展集群
图 12.12 通过增加 VM 的数量水平扩展集群

清单 12.6 展示了如何通过更改配置来在节点池中添加更多的 VM。在此例中,我们将 node_count 从 1 增加到 6,并且保持 vm_size 为较小的 Standard_B2ms。

清单 12.6 使用 Terraform 水平扩展集群

default_node_pool {
  name = "default"
  node_count = 6
  vm_size = "Standard_B2ms" // 将节点池的大小增加到 6,集群现在由六台 VM 提供支持
}

12.3.3 水平扩展单个微服务

假设我们的集群已经足够大,可以承载所有微服务并提供良好的性能,但如果某个单独的微服务出现过载怎么办?解决方法是水平扩展该微服务,即增加其实例数量,分散负载。

图 12.13 展示了通过复制单个微服务来进行水平扩展的过程。这实际上为特定的微服务增加了计算、内存和存储资源,使其能够处理更大的工作负载。

图 12.13 通过复制微服务来水平扩展微服务
图 12.13 通过复制微服务来水平扩展微服务

这种扩展可以通过简单的配置更改来实现。例如,在我们的 Kubernetes 部署中,将 replicas 字段设置为 3,如清单 12.7 所示,可以实现这一目标。

清单 12.7 水平扩展微服务

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gateway
spec:
  replicas: 3 # 将微服务的副本数量设置为 3,使负载能够均匀分配到三个实例之间。
  selector:
    matchLabels:
      app: gateway
  template:
    metadata:
      labels:
        app: gateway
    spec:
      --省略--

12.3.4 集群的弹性扩展

在更高级的策略中,我们可以考虑弹性扩展,这涉及到自动和动态地调整集群规模以适应变化的需求。在需求低时,Kubernetes 可以自动减少不必要的资源;在需求高峰,又能迅速扩展资源以应对增加的负载。这种方法可以显著节省成本,因为你只需为实际使用的资源支付费用。

在集群级别实现弹性扩展可以自动调整资源以适应需求变化。如此只需简单的代码更改即可实现。清单 12.8 展示了如何更新第 7 章的 Terraform 配置,以启用 Kubernetes 自动扩展器,并设置节点池的最小和最大规模。这种自动水平扩展是默认行为,但可根据需要进行定制。更多关于如何配置自动扩展器的详细信息可以在 Terraform 文档中找到: http://mng.bz/K9YO

清单 12.8 使用 Terraform 为集群启用弹性扩展

default_node_pool {
  name = "default"
  vm_size = "Standard_B2ms"
  enable_auto_scaling = true // 启用 Kubernetes 集群的自动扩展功能
  min_count = 3 // 设置最小节点数为 3,即集群起始时有三台 VM
  max_count = 20 // 设置最大节点数为 20,即集群可自动扩展至 20 台 VM 以应对需求
}

12.3.5 单个微服务的弹性扩展

在单个微服务层面,我们也可以启用弹性扩展。清单 12.9 展示了一个部署到 Kubernetes 集群的资源配置示例,这个配置使网关微服务能够根据负载变化动态地调整副本数量。这种能力使微服务可以应对突发事件中的负载波动。要了解更多关于在 Kubernetes 中配置 Pod 自动扩展的信息,请参见 Kubernetes 文档: http://mng.bz/9Q2r

清单 12.9 为微服务启用弹性扩展

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: gateway
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: gateway
  minReplicas: 3 # 设置此微服务的最小实例数为 3,以确保基本可用性
  maxReplicas: 20 # 设置最大实例数为 20,以应对可能的高峰需求
    --省略--

12.3.6 扩展数据库

在第 4 章中,我们探讨了每个微服务应拥有自己的数据库的原则(见第 4.5.4 节)。共享数据库在微服务之间会引发许多问题,其中一个主要问题是它严重限制了扩展性。请参考图 12.14,我们展示了多个微服务共享一个数据库的情景。这种做法将成为未来扩展性的噩梦。

图 12.14 为什么我们不在微服务之间共享数据库(除了可能为同一个微服务的副本共享数据库)
图 12.14 为什么我们不在微服务之间共享数据库(除了可能为同一个微服务的副本共享数据库)

这些微服务并不是真正独立的。共享的数据库成为了它们之间的一个固定连接点,并可能成为一个严重的性能瓶颈。如果微服务需要共享数据,它们会变得紧密耦合,这严重限制了将来的重构和调整的灵活性。通过共享数据库,我们限制了未来解决性能问题的能力。

这种情况可能完全破坏我们努力实现的简单扩展性。如果我们想要这样结构化我们的应用程序,不如完全不采用微服务!

相反,如图 12.15 所示,每个微服务都应有其自己独立的数据库。这样的设计使微服务完全独立,这意味着如果需要,我们可以很容易地对它们进行水平扩展。

图 12.15 每个独立的微服务应有自己的数据库。
图 12.15 每个独立的微服务应有自己的数据库。

我们确实需要每个微服务拥有独立的数据库,但这并不意味着每个数据库需要独立的物理服务器。虽然独立管理数据库服务器的成本很高,我们通常希望降低这些成本。可以在一个共享的数据库服务器上运行多个独立的数据库,如图 12.16 所示。这种做法简化了架构并降低了成本,仍然满足了每个微服务一个数据库的规则。

图 12.16 运行在共享数据库服务器上的独立数据库是完全可以接受的(这通常是开始使用微服务的最简单和最便宜的方法)。
图 12.16 运行在共享数据库服务器上的独立数据库是完全可以接受的(这通常是开始使用微服务的最简单和最便宜的方法)。

如果在未来某个数据库的工作负载变得过大,我们可以很容易地创建一个新的数据库服务器,并将该数据库迁移到新服务器上,如图 12.17 所示。在需要时,我们可以为那些需要更多计算、内存或存储的数据库提供专用服务器。

图 12.17 随着应用程序的增长,可以通过将大型数据库拆分到各自独立的数据库服务器上来扩展它。
图 12.17 随着应用程序的增长,可以通过将大型数据库拆分到各自独立的数据库服务器上来扩展它。

如果需要更具可扩展性的数据库方案,我们在本书中使用的 MongoDB 提供了数据库分片功能,可以将一个大型数据库分布在多个 VM 上。这在非常大的数据库场景中可能是必需的,虽然大多数应用可能永远不需要这种级别的扩展性。

图 12.18 对于非常大的数据库,我们可能需要 MongoDB 的分片功能将一个大型数据库分布在多个 VM 上。
图 12.18 对于非常大的数据库,我们可能需要 MongoDB 的分片功能将一个大型数据库分布在多个 VM 上。

12.3.7 避免过早扩展

扩展计划的制定是一项挑战。预测未来的扩展需求很难做到准确无误。通常,试图为应用程序提前做好充分准备,最终只会导致过度设计。这意味着,为了应对尚未出现的未来可能性,我们使应用的设计和架构变得复杂。这通常会导致应用不必要地复杂化,更难维护。

过度复杂的设计会使添加新功能变得更加困难(降低开发效率),甚至在将来真正需要扩展时,也可能难以按预期的方式进行扩展。

因此,不要急于过早地扩展,因为这实际上可能会阻碍未来的可扩展性,而不是促进它。未来的发展往往难以预料:需要什么功能,应用将如何被使用,以及它需要如何扩展。在这一过程中,应尽量保持简单。保持代码和架构的简单性是应对未来变化的最佳策略,当未来真正到来时,我们会做好准备。

12.4 缓解变更引起的问题

我们采用微服务的部分原因是为了实现故障隔离。在第 11 章的第 11.4 节中,我们讨论了各种技术来发现和修复可能在应用中出现的问题。有些问题可能在系统中潜伏很久,直到特定条件触发它们才显现出来。

但这些问题最初是如何出现的呢?我们能在开发过程的早期阶段(成本较低且尚未对客户造成影响时)缓解这些问题吗?

有些问题是无法预防的,比如我们无法预测硬件、网络或云服务提供商何时可能出现问题。然而,许多问题源自开发人员将有缺陷的代码推送到生产环境。我们可以采取措施,在代码推送时尽早发现这些问题。下面是一些可帮助你实现这一目标的策略。

12.4.1 自动化测试和部署

将频繁更新安全地推送给客户是自动化部署的关键动力。对于微服务而言,拥有一个可靠的持续部署(CD)管道并使其成为常态是非常重要的。如果运气好的话,它会在不知不觉中完成工作,你甚至可能忘记它是如何运作的。

自动化部署是采用微服务进行扩展的一个基本要求。同样重要的是自动化测试。随着微服务数量的增加,如果没有自动化测试支持,几乎不可能跟上测试的步伐。

在 CD 管道中集成自动化测试可以创建一个通道,当代码无法编译或自动化测试未通过时,这个通道就会自动关闭,防止损坏的代码被部署到生产环境中。这是一个重要的安全措施,可以阻止有缺陷的代码流入生产环境。

12.4.2 分支保护

为了防止开发人员直接将有问题的代码更改推送到生产环境,可以启用代码仓库中生产分支的分支保护功能。

例如,在 GitHub 上,可以配置设置来阻止直接推送到特定分支。开发人员需要通过提交拉取请求来推送更改,这个请求必须通过自动化测试,并得到一个或多个其他开发人员的审查批准后,更改才能合并到该分支。这一步骤在代码最终影响客户之前提供了一个重要的检验点。

你可以通过这个链接了解更多关于 GitHub 分支保护的信息: http://mng.bz/j1Ge

12.4.3 部署到测试环境

如果开发人员不能直接推送代码到生产环境,他们应该如何测试他们的代码呢?答案是:通过一个专门的测试环境。如我们在第 12.2.5 节讨论的,确保新代码在接触生产环境前在测试环境中经过彻底检验是我们开发工作流程的一部分。没有测试环境,我们就无法捕捉到那些只在类似生产的环境中才会出现的错误。

12.4.4 滚动更新

在向 Kubernetes 部署更新时,可以利用其滚动更新功能来逐步部署新版本的微服务。假设我们管理着三个网关微服务的副本。推出这些服务的新版本时,我们可以先替换第一个副本。如果一切顺利,再继续替换第二个和第三个副本。这种缓慢的更新策略确保了在继续部署前每个新副本都能正常工作,从而减少风险。

如果在任何阶段遇到问题——例如,第一个替换的副本开始出现故障——我们可以撤销这些更改,最大限度地减少对客户的影响。

Kubernetes 可以自动管理这一过程。清单 12.10 展示了 FlixTube 网关微服务的一个简单配置示例。使用这种配置,新部署的网关微服务将逐个副本推出,每个新实例都必须通过其就绪探针测试后才能继续部署。如果新实例未通过就绪测试,部署将停止,我们需要对其进行调试,并可能需要回滚更改。

清单 12.10 启用网关微服务的滚动更新

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gateway
spec:
  replicas: 3  # 设置网关微服务的副本数量
  selector:
    matchLabels:
      app: gateway
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1  # 部署期间一次可以额外创建的最大副本数
      maxUnavailable: 0  # 部署期间允许的最大不可用副本数为 0
  template:
    --省略--

12.4.5 蓝绿部署

对基础设施进行更改总是伴随着风险,这些风险需要谨慎管理。对本章提到的任何扩展技术操作不当,都有可能导致整个集群的故障。为了避免这些风险影响到面向客户的基础设施,我们介绍一种称为蓝绿部署的技术。

蓝绿部署涉及创建两个生产环境,标记为绿(标签仅为方便识别)。如我们在第 12.2.5 节讨论的,通过参数化 Terraform 代码以创建并通过名称区分不同环境,可以轻松实现这一点。

首先创建的环境标记为环境。客户通过我们的域名(如 www.company.com)访问应用,通过 DNS 记录路由到蓝环境。为了保护我们的客户,我们不在蓝环境进行任何高风险的更改(常规和频繁的微服务更新除外,因为它们不涉及基础设施变动)。

为了进行高风险或实验性质的更改(如扩展实验),我们会创建一个全新的生产基础设施,并将其标记为绿环境。现在,开发人员在绿环境中工作,这样他们的任何操作都与客户当前使用的蓝环境隔离,如图 12.19 所示。

图 12.19 客户使用蓝环境,而开发人员和测试人员在绿环境中工作。
图 12.19 客户使用蓝环境,而开发人员和测试人员在绿环境中工作。

一旦绿环境的调整完成并通过测试验证无误,我们可以简单地将 DNS 记录从蓝环境切换到绿环境。

此时,客户将开始使用绿环境,而我们的开发人员和测试人员则转到蓝环境继续他们的工作,如图 12.20 所示。

图 12.20 当绿环境准备就绪并通过测试后,客户切换到它。然后开发人员和测试人员切换到蓝环境并继续工作。当蓝环境准备好并通过测试后,客户再次切换,循环继续。
图 12.20 当绿环境准备就绪并通过测试后,客户切换到它。然后开发人员和测试人员切换到蓝环境并继续工作。当蓝环境准备好并通过测试后,客户再次切换,循环继续。

如果在任何时候发现绿环境存在问题,我们可以轻松地将 DNS 切换回蓝环境,恢复客户的正常服务。这种蓝绿部署策略在保持集群无状态时尤为有效,这一点在第 4 章(4.4.5 节)中提到过。当数据存放在集群外部时,可以无缝地在不同集群之间切换客户,无需迁移数据。将数据存储在托管的外部数据库和云存储中,不仅更安全,还便于在蓝绿环境之间进行无风险切换。

12.5 基本安全概念

虽然本书各章中已简要提及安全问题,但这并未充分突出其重要性。安全在开发的早期阶段至关重要,其重要性甚至可以撰写一整本专书。值得一提的是,Prabath Siriwardena 和 Nuwan Dias 的《Microservices Security in Action》(Manning 出版社,2020 年)就是一本专注于微服务安全的优秀著作。现在,让我们先了解一些基本的安全知识。

每个应用程序都需要一定级别的安全措施。即使数据看似不敏感,我们也不希望有人能够未经授权地修改它。同样,即便系统不承担关键任务,我们也不愿看到它被攻击者破坏。

我们需要有效地利用安全技术,如身份验证、授权和加密,来防止应用程序或数据被恶意使用。我们还可能需要根据所在地区的法律要求来安排数据存储,以保护客户的隐私和匿名性。尽管 FlixTube 目前尚未实施以下措施,但已经采取了一些基本的安全措施:

  • 唯一对外暴露的微服务是网关微服务,这是一种设计选择。内部微服务不能从集群外部直接访问。
  • 初期阶段为了实验将 RabbitMQ 服务器和 MongoDB 数据库对外暴露,但很快就终止了这些访问权限。这样做是为了防止这些关键资源被直接外部访问。这是非常重要的!除非确定它们完全受到保护,否则不应对外暴露这些关键资源。

我们希望未来能为 FlixTube 引入以下安全功能:

  • 实现网关处的身份验证系统。
  • 使用 HTTPS 保护与客户的连接。这将加密它们的通信,且可以利用如 Cloudflare 这类外部服务来快速实现。

当然,任何应用程序所需的安全级别都与它需要保护的系统和数据的重要性相匹配。FlixTube 增强的安全措施将远不及银行应用或政府网站所需的安全级别。

安全应该是从组织的两端出发。企业应有符合行业和客户需求的安全政策和策略。然后,开发人员负责按照公司标准考虑并实施安全措施。应编写简单且安全的代码。如同防御性编程(见第 11 章第 11.4.1 节),在安全性方面也应采用防御性思维。

在编写代码和构建微服务时,应问自己:这个系统可能会被如何攻击?这有助于在最有效的时刻——即在遭受攻击之前——主动解决安全问题。

12.5.1 信任模型

FlixTube 的需求相对简单,我们采用了所谓的内部信任模型,也称为信任网络。在这个模型中,所有身份验证都在系统的入口点(即网关微服务)进行。集群内的微服务彼此隐式信任,依赖于底层网络的安全性保护它们不受外部攻击。

图 12.21 内部信任模型。网关处进行所有外部请求的身份验证。集群内的微服务彼此信任,无需身份验证即可通信。
图 12.21 内部信任模型。网关处进行所有外部请求的身份验证。集群内的微服务彼此信任,无需身份验证即可通信。

内部信任模型提供了一种简化的微服务入门策略。在安全性方面,简单性往往优于复杂性,因为简单结构暴露的风险点更少,安全隐患更不易被掩盖。引入更复杂的安全措施时需谨慎,因为额外的复杂性实际上可能引入新的安全漏洞。有时,增强的安全措施反而可能降低整体的安全性,还可能对用户体验产生负面影响。

如果你的安全需求超出了 FlixTube 的场景,或者你的微服务需要跨多个集群通信,内部信任模型可能不足够。

你可能需要考虑的一个更安全的选项是零信任模型(见图 12.22)。在零信任模型中,无论是内部还是外部的所有连接都必须进行身份验证。微服务间不会自动互相信任。假设任何特定的微服务都可能受到威胁或遭到破坏,尤其是那些托管在其他集群中的微服务。

图 12.22 零信任模型。所有连接,无论是内部的还是外部的,都需要经过身份验证。此模型支持与外部微服务的连接。
图 12.22 零信任模型。所有连接,无论是内部的还是外部的,都需要经过身份验证。此模型支持与外部微服务的连接。

12.5.2 敏感配置管理

每个应用程序都有一些敏感配置信息需要严格保护。你可能还记得在第 8 章,我们讨论过如何将私密信息(如容器仓库密码和 Kubernetes 认证信息)安全地存储在 GitHub Secrets 中,与我们的代码一同托管在 GitHub 上(参见第 8 章第 8.9.9 节)。

在开发过程中,我们还需要处理其他敏感信息,如密码、令牌和 API 密钥。这些信息虽然可以方便地存储在代码库中,但这也意味着任何可以访问到代码的人都可能轻易获取并操控这些敏感数据,进而威胁到应用程序的安全。

使用 GitHub Secrets 或依据你的持续部署(CD)服务提供商的推荐方案,都是管理这类敏感信息的有效方法。然而,如果你倾向于一个与代码源控制或 CD 服务提供商无关的解决方案,Kubernetes 提供了独立的机密配置存储解决方案,你可以在此了解更多相关信息: http://mng.bz/84xD

若这些选项仍不满足你的需求,市面上也有许多其他的产品可供选择。例如,你可以了解更多关于 Vault 的信息,这是由 HashiCorp(即 Terraform 的开发者)提供的另一款开源产品。详情请访问: www.vaultproject.io

12.6 微服务重构

早在第 1 章(第 1.1 节)我们就提到,通过学习从零开始构建微服务应用,我们终将探讨如何将现有的单体应用重构为微服务架构。对于不同的单体应用,重构的具体操作和细节可能有所区别。转换过程可以采用多种策略,但在本节中,我将介绍一些基本的转换策略和战术,这些策略和战术适用于任何情况。

正如第 2 章第 2.4 节所述,基本理念与任何软件开发过程一致,关键在于迭代实施、进行小而简单的更改,并确保在整个过程中软件的持续正常运行(如图 12.23 所示)。

图 12.23 将单体应用重构为微服务只能通过一系列小而经过充分测试的迭代步骤完成。
图 12.23 将单体应用重构为微服务只能通过一系列小而经过充分测试的迭代步骤完成。

因应单体应用的大小和复杂度,转换为微服务可能是一项庞大的工作。尝试一次性大规模转换通常不太可能成功。唯一安全的方法是通过小而可控的步骤逐步实施,并在整个过程中进行详尽的测试。

同时,我们也不能忽视产品的常规维护工作。继续增加新功能、修复错误,以及确保产品正常运行同样至关重要;我们不能让问题在重构过程中积累。

12.6.1 转向微服务的必要性

在开始将单体应用转化为微服务架构之前,必须先自问一个关键问题:你真的需要微服务吗?转变成微服务架构是一个既漫长又复杂的过程,它会给项目带来额外的复杂性,并且考验团队的耐心与决心。在此过程中,你需要思考以下几个问题:

  • 进行这种转变的成本和收益是否相称?
  • 你的项目真的需要扩展性吗?
  • 你是否真的需要微服务提供的灵活性?

这些问题的答案至关重要。确保你对它们有深思熟虑的回答。

12.6.2 规划转换并全员参与

在未经深思熟虑的情况下盲目向微服务迈进是不可取的!为了尽可能地增加成功的可能性,你需要一个关于产品未来发展方向的明确书面规划。

采用领域驱动设计(DDD)将业务逻辑拆解为多个微服务(详见本章末尾的参考文献)。目标是构建一个简洁的架构,为近期的发展制定计划,而不是为遥远且不确定的未来做准备。从架构的最终愿景逆向工程,确定需要按照何种顺序进行改变,以实现向微服务的过渡。这个规划不必过于具体,但需要有一个清晰的方向。

你需要一个详细的建设计划和如何实现这一目标的策略。有人曾说,战斗计划从未能在与敌人首次接触后存活下来(这是对赫尔穆特·冯·毛奇的话的改编)。虽然计划总是变化,但这并不意味着不需要计划。相反,应该预见到过程中的自然变化,因为我们对应用程序结构的了解会不断深入。随着项目的推进,必须不断地回顾和更新计划,确保其在实施过程中的相关性。

转换计划应与团队成员(或其代表)共同制定,因为实现这一转换将是一场团队的共同努力。确保让所有人参与进来。

不仅仅是制定计划,现在还必须将其传达给整个公司。确保开发人员清楚他们的角色和期望。与其他业务部门进行沟通,用他们能理解的语言解释变革的必要性及其带来的价值。确保每个人,毫无例外,都理解这一变革的重要性。

12.6.3 深入了解你的遗留代码

在转换开始之前和过程中,你需要花大量时间来深入理解你的单体应用。制定测试计划,进行实验,探索其潜在的故障模式。对于转换过程中可能遇到问题的各个部分,形成一个初步的认识。

12.6.4 提升自动化水平

在任何微服务项目中,良好的自动化都是至关重要的。在转换前和转换过程中,应不断地投资于自动化的改进和完善。如果你还没有掌握基础设施和自动化流程,那么需要立即开始行动(甚至在开始转换之前!)。你可能会发现,改变公司对自动化的看法实际上是转换过程中最为困难的部分之一。需要建立可靠而高效的自动化部署流程(参见第 8 章)。你转换的任何功能都应该已经通过自动化测试,或者在将功能转换为微服务的同时,实现具有良好覆盖率的自动化测试(参见第 9 章)。

在微服务架构中,自动化是不可或缺的。如果你无法进行必要的自动化投资,那么你可能也无法承担将单体应用转换为微服务的成本。

12.6.5 构建你的微服务平台

在转换开始之前,你需要一个能够托管新创建微服务的平台。为从单体应用中逐步提取出来的微服务准备一个生产环境(见图 12.24)。

在本书中,我们提供了构建这样一个平台的步骤。创建一个私有容器仓库,并根据第 6 章或第 7 章的指导创建一个 Kubernetes 集群。在创建第一个微服务后,为团队创建一个共享模板:一个空白的微服务,它可以作为开发其他每个微服务的起点。如果存在不同类型的微服务,应为每种类型创建多个模板。

构建一个自动化测试流程,并使其易于开发人员使用。创建详尽的文档、示例和教程,帮助开发人员快速掌握如何在平台上创建和部署新的微服务。

图 12.24 可以逐步从单体应用中提取出小块,并将其移动到你的 Kubernetes 集群中。
图 12.24 可以逐步从单体应用中提取出小块,并将其移动到你的 Kubernetes 集群中。

12.6.6 沿自然边界切割

现在,寻找你的单体应用中与你的架构愿景中的微服务相对应的现有组件。这些组件为你提供了从单体应用向微服务逐块提取组件的绝佳机会,如图 12.25 所示。

如果你难以找到自然的边界,你的工作将会更加困难。如果你的单体应用是一个巨大的泥球或充满了意大利面代码,你可能需要先重构或在提取过程中进行重构。无论哪种方式,都将会有挑战。为了安全,你的重构应该通过自动化测试来支持(如果必要,边做边构建自动化测试)。这将会变得混乱——做好准备。

图 12.25 单体应用通常会有自然的边界。利用这些边界来识别可以逐步提取到微服务的独立组件。
图 12.25 单体应用通常会有自然的边界。利用这些边界来识别可以逐步提取到微服务的独立组件。

12.6.7 优先级提取

在确定将哪些组件首先转化为微服务时,应优先考虑那些能从微服务架构中获得最大益处的组件。这些组件可能是经常需要修改的代码,或者是那些在性能或可扩展性方面将极大受益的部分。

优先提取这些关键组件至微服务可以带来立即的实际好处,这将显著提高你的开发效率。这种做法不仅能减少部署风险,还能帮助你向团队证明转型项目的进展顺利。

12.6.8 迭代实施

通过持续地将小块功能迁移到微服务,并在过程中进行全面测试,你将能够安全地将单体应用转化为基于微服务的系统(如图 12.26 所示)。这个过程不会是一帆风顺的。根据应用的规模和复杂度,这可能需要几年时间。但这是一条可行的道路。你只需一步一个脚印,持续工作,直到全部完成。

图 12.26 迭代地将单体应用的小块提取到微服务中,始终进行测试并保持其正常工作。最终,你的应用程序将被分解为微服务。
图 12.26 迭代地将单体应用的小块提取到微服务中,始终进行测试并保持其正常工作。最终,你的应用程序将被分解为微服务。

12.7 可能性区间

当我们设定微服务的架构目标时,常常追求一种理想状态,我称之为“开发者的微服务乌托邦”。这是每个开发者梦寐以求的环境——如果可能的话。问题在于,这种理想状态在现实中并不总是可行的,业务需求和遗留代码的限制经常阻碍我们实现那种美好而优雅的架构。因此,关于单体应用和微服务哪个更好的争论从未停止。

事实上,单体应用与微服务之间并没有绝对的对立关系。实际上,它们之间存在一个广泛的可能性区间。没有任何理由认为区间上的某一个点比其他点更优。

如图 12.27 所示,从单体应用到微服务,甚至到函数即服务,存在多种可能的架构选择。

图 12.27 单体应用与微服务之间并不存在真正的对立。实际上,存在一个可能性的区间。
图 12.27 单体应用与微服务之间并不存在真正的对立。实际上,存在一个可能性的区间。

12.7.1 追求实用而非完美

不要试图实现一个不存在的“完美”状态。完美是一个主观的概念,不同人对此有不同的解释,这使得在团队内达成一致几乎是不可能的。我的建议是:放弃追求完美,专注于满足业务和客户的实际需求。

当然,我们应该努力改善我们的应用程序(无论它处于区间的哪个位置),但我们不一定要达到那个理想状态。这只是一个远大的目标。我们应该确保每一步都对团队、业务或客户有积极的影响。如果在某个点上,我们发现继续前进不再带来预期的价值(或者情况变得更糟),那么我们应该停下来重新评估我们的行动和目标。

12.7.2 投资回报递减

你可能会惊讶地发现,在追求“完美的微服务”时,投资回报率逐渐递减。在转换初期,你可能会看到显著的效益,但随着过程的深入,每一步带来的附加值将逐渐减少。当服务变得过于细化时,成本可能开始超过收益。

在某个阶段,你可能会觉得已经取得了足够的成果,认为可以停止进一步的转换了。你的系统可能最终由一个较小的单体应用和若干微服务组成,或者是区间上的其他某种组合——如果这种状态对你有用,那就很好。也许你会决定现在就保持系统的现状,继续在新的微服务或小型单体应用中添加新代码,同时为你的应用程序引入更多功能。

图 12.28 向微服务转换的时间线。在早期,你会获得高投资回报率。然而,随着过程的推进,你会得到递减的投资回报率,可能不值得追求开发者的微服务乌托邦。
图 12.28 向微服务转换的时间线。在早期,你会获得高投资回报率。然而,随着过程的推进,你会得到递减的投资回报率,可能不值得追求开发者的微服务乌托邦。

12.7.3 混合架构策略

我们的目标是为业务和客户带来最佳结果。如果一种方法能有效完成任务,那么它就是一种好方法,不必过分纠结是否完美。

如果完美的微服务架构只能带来递减的投资回报,那么追求极致的微服务似乎并非明智之举。实际上,维护一个微服务架构并非易事。尽管你可能设计了一个理想的系统,但随着系统规模的扩大和更多开发者的参与,维持最初的设计愿景可能会变得异常困难。

基于我构建过的多种应用程序的经验(无论是单体应用还是微服务),我现在建议采用一种中庸之道,即“混合方法”。这种方法结合了单体应用和围绕其构建的微服务“星座”,比起全面微服务架构,这种策略更易于实现和维护(见图 12.29)。

图 12.29 混合方法:一个单体应用被一组辅助微服务包围
图 12.29 混合方法:一个单体应用被一组辅助微服务包围

对于不确定是否能从微服务架构中获益的新项目,混合方法成了我的默认选择。这种布局允许我们在适当的时候将特定功能迁移到微服务,同时保留单体应用的便利性和易管理性作为主要的代码基础。

这种架构模型提供了最佳的两全其美的方案:为了简化,大部分代码依旧在单体应用中维护。但是,当我们需要微服务带来的性能、可扩展性、灵活性或容错能力时,我们仍然可以轻松实现这些需求。更重要的是,当现实情况无法完全符合开发者理想的微服务乌托邦时,我们无需自责。我们可以专注于满足客户的实际需求,而不是被理想化的技术结构所束缚。

例如,如果我在 FlixTube 使用这种模型,我会将大部分代码维护在单体应用中,并将一些特定任务,如“为视频生成缩略图”或“利用机器学习判定视频主题”这样的任务,委托给专门的微服务处理。这些任务因其对性能和可扩展性的高要求,非常适合由微服务来执行,可以有效地提高整体系统的效率和响应速度。

12.8 成本效益型微服务策略

分布式架构一直是构建复杂应用程序的强有力手段。随着云技术、现代工具和自动化的发展,微服务架构已变得非常流行。这些技术进步使得构建微服务不仅更加简单,也更具成本效益。

然而,构建微服务仍是一项挑战性任务。虽然单个微服务的复杂性较低,但管理整个应用程序的复杂性可能仍然是一个挑战,尤其是对于小团队、独立开发者或初创公司而言。

在本书中,我们介绍了许多技巧和技术,这些技巧和技术不仅便于学习微服务,还能在未来提供持续的支持。以下是一些简洁的见解,帮助你高效启动微服务项目:

  • 掌握现代工具并充分利用它们! 自主开发工具既耗时又困难,容易分散你专注于为客户提供功能的精力。

  • 从一个含有多个持续部署(CD)管道的单一代码仓库(monorepo)开始。 当需要时,你可以通过创建一个或多个元仓库来管理多个分离的代码仓库。

  • 在单一数据库服务器上,为每个微服务维护一个独立数据库。 这种方式便于管理同时降低运营成本。

  • 搭建一个仅包含单一虚拟机的 Kubernetes 集群。 初始阶段,每个微服务只需配置一个实例,无需配置副本,这有助于控制成本。

  • 为每个微服务配置一个简单的持续部署管道,直接部署至你的集群。 如果需要,在客户面前部署之前,可以通过不同的生产和测试分支来进行预部署测试。

  • 除非必要,否则避免将应用配置的代码仓库与微服务的代码仓库分开。 这样做可以简化部署流程,避免不必要的复杂性。

  • 采用外部文件存储和外部数据库服务器,使集群运行在无状态模式。 这减少了实验性更改对集群的影响风险,即使集群出现问题,数据也能安全无恙。

  • 使用 Docker Compose 在本地开发环境中模拟你的应用,进行开发和测试。 利用实时重载功能实现快速迭代开发。

  • 初期可能不需要自动化测试,但在建立大型、可持续的微服务应用时,自动化测试变得非常关键。 对于初创公司来说,在产品生命周期的早期,投入大量资源于复杂的基础设施可能为时尚早。应首先验证产品的市场适应性。

  • 虽然可能缺乏自动化测试,但仍需进行充分的测试。 配置高效且可靠的手动测试流程。应有能力快速地在本地启动应用程序,从无到有进行全面测试。

  • 利用 Docker 容易部署第三方镜像的优势,简化在集群中运行服务的过程。 这正是我们在第 5 章中部署 RabbitMQ 的策略。你可以在 DockerHub 上找到许多其他有用的镜像: https://hub.docker.com/

  • 尽早投资于自动化,特别是自动化部署。 你的日常工作将依赖于它,因此确保它的稳定性和可靠性至关重要。

12.9 开始简单地行动

回顾我们共同走过的旅程,图 12.30 精彩呈现了我们的发展历程。我们从创建一个简单的微服务开始,逐步学习如何使用 Docker 进行打包和部署。你了解了如何利用 Docker Compose 在开发环境中开发和测试多个微服务。最终,你将你的微服务应用部署到了云端的 Kubernetes 集群中。管理复杂性是现代软件开发的核心能力,这就是为什么我们要花时间去掌握像微服务这样的高级架构技术。

图 12.30 我们从一个微服务到在生产环境中运行多个微服务的旅程
图 12.30 我们从一个微服务到在生产环境中运行多个微服务的旅程

这是一段激动人心的旅程!遗憾的是,我们的共同课程即将结束。当然,你的探索之旅还将继续,我祝你在构建复杂应用的道路上一切顺利。

请关注邮件列表: www.bootstrapping-microservices.com ,以获取本书及相关内容的更新。

12.10 持续学习

最后,让我们通过一些建议书籍来结束本章,这些书籍将帮助你深化理解并扩展你的知识库。要深入了解领域驱动设计,请参考以下经典著作:

  • Eric Evans 著,《领域驱动设计》(Domain-Driven Design)(Addison-Wesley,2003 年)

如果你希望快速了解,可以查看以下免费电子书的精简总结:Abel Avram 和 Floyd Marinescu 著,《领域驱动设计简介》(Domain Driven Design Quickly)(InfoQ,2018 年),可在此下载: http://mng.bz/E9pR

要更全面了解微服务的安全性,请阅读:

  • Prabath Siriwardena 和 Nuwan Dias 著,《微服务安全实战》(Microservices Security in Action)(Manning,2020 年)

以下书籍深入讨论了微服务的理论、设计和实现:

  • S. Ramesh 著,《设计微服务》(Designing Microservices)(Manning,预计 2024 年春季发布)
  • Richard Rodger 著,《微服务之道》第 2 版(The Tao of Microservices)(Manning,预计 2024 年春季发布)
  • Chris Richardson 著,《微服务模式》(Microservices Patterns)(Manning,2018 年)
  • Morgan Bruce 和 Paulo A. Pereira 著,《微服务实战》(Microservices in Action)(Manning,2018 年)
  • Christian Horsdal Gammelgaard 著,《.NET Core 中的微服务》第 2 版(Microservices in .NET Core)(Manning,2021 年)
  • John Carnell 和 Illary Huaylupo Sánchez 著,《Spring 微服务实战》第 2 版(Spring Microservices in Action)(Manning,2021 年)
  • José Haro Peralta 著,《Python 微服务 API》(Microservice APIs with Python)(Manning,2022 年)

总结

  • 可以通过以下方式将微服务扩展到更大和更多的开发团队:

    • 将每个微服务独立存放在自己的代码仓库中。

    • 为每个微服务配置单独的持续部署(CD)管道,确保它们能够独立部署。

  • 我们可以创建一个元仓库,这是一个包含多个独立代码仓库的复合仓库,便于管理和整合分散的代码库。

  • 通过对 Terraform 基础设施代码进行轻微重构,我们能够轻松创建独立的环境或应用实例,这允许我们为客户提供一个独立的生产环境,与用于测试新代码变更的环境分开。

  • 我们可以通过设立一个或多个合并点的工作流来控制代码更改合并到生产环境,这些合并点为代码审查和测试提供了机会,以确保对客户无影响。

  • CD 管道可以配置为自动向特定环境部署,例如,当代码推送到测试分支时部署到测试环境,而推送到生产分支时则部署到生产环境。

  • 以下是几种扩展微服务以满足增长需求的策略:

    • 垂直扩展集群:增加集群中每个虚拟机(VM)的资源。

    • 水平扩展集群:增加集群中虚拟机的数量。

    • 水平扩展微服务:增加需要更多计算能力或吞吐量的微服务实例数。

    • 弹性扩展集群:配置集群自动根据需求增减虚拟机数量。

    • 弹性扩展微服务:配置每个微服务自动根据需求增减实例数量。

    • 扩展数据库:遵循每个微服务一个数据库的原则,关键在于数据库的垂直或水平扩展以增加性能。

  • 我们可以通过多种策略来防止问题影响客户:

    • 实施可靠的自动化测试和部署。

    • 启用分支保护,设置在代码更改推送到生产分支前的审查和测试检查点。

    • 在测试环境中进行彻底测试后再将更改合并到生产环境。

    • 利用 Kubernetes 的滚动更新功能安全发布微服务更新。

    • 采用蓝绿部署来更安全地进行高风险更改以及对应用及其基础设施进行重组。

  • 微服务的安全性至关重要,因为微服务应用可能有多个公开访问的入口点。

    • 我们应采取身份验证和授权等安全措施来保护系统访问。
  • 将单体应用重构为微服务应通过一系列小的、经过充分测试的步骤来完成。

  • 并不存在单体应用与微服务的对立;实际上,我们可以在一个广阔的选择区间中做出选择。

  • 一个理想的默认选择是位于区间中间的混合方法:核心为单体应用,周围环绕一组微服务。这种方法结合了单体应用的便利性和简洁与微服务的灵活性和扩展性。

  • 有多种方法可以在开始时使微服务成本更低,复杂度更小,这使得微服务成为初创公司、小团队和独立开发者的一个有效且高效的起点。

文章导航

独立页面

这是书籍中的独立页面。

书籍首页

评论区