作者 | Chris Price
译者 | 明知山
策划 | Tina
什么是蜂窝架构?
蜂窝架构是一种有助于在多租户应用程序中实现高可用性的设计模式。其目标是在设计应用程序时将所有组件部署到一个完全自给自足的隔离“单元”中,然后创建许多这种“单元”的离散部署,它们之间没有任何依赖关系。
每个单元都是应用程序的一个完全可操作的自治实例,随时准备为流量提供服务,不依赖于任何其他单元,也不与其他单元交互。
用户的流量可以分布到这些单元中,如果一个单元发生故障,它只会影响该单元中的用户,其他单元仍然完全可用。这最大程度地减小了服务可能经历的故障“爆炸半径”,并确保故障不会影响大多数用户的 SLA。
关于如何组织单元以及将哪些流量路由到哪个单元,有许多不同的策略。有关蜂窝架构的好处以及这些策略的一些示例,可以观看 Peter Voshall 在 re:Invent 2018 的演讲:“AWS 如何最小化故障的爆炸半径”。
管理一个具有许多独立单元的应用程序可能令人望而生畏。因此,对于创建和维护单元所需的常见基础设施任务来说,进行尽可能多的自动化是非常有价值的。在本文的其余部分,我们将较少关注蜂窝架构的“为什么”,而更多地关注“如何”进行这种自动化。有关“为什么”的更多信息,请查看 Peter 的演讲和文章末尾的其他资源链接!
自动化你的蜂窝架构
在实现蜂窝基础设施自动化的过程中,有五个关键问题需要解决:
  • 隔离:如何确保单元之间的明确边界?
  • 新单元:如何持续有效地让它上线?
  • 部署:如何将最新的代码变更传送到每个单元?
  • 权限:如何确保单元是安全的,并有效地管理其入站和出站权限?
  • 监控:运维人员如何一目了然地确定所有单元的健康状况,并轻松地识别哪些单元受到故障的影响?
有许多工具和策略可用于解决这些问题。本文将讨论 Momento 公司所使用的工具和解决方案。
在解决这些具体问题之前,让我们先谈一谈标准化。
标准化
对构建 / 测试 / 部署生命周期的某些部分进行标准化,可以更轻松地围绕它们构建通用的自动化。而通用自动化将大大简化在每个单元所有组件之间重用基础设施代码的工作。
需要注意的是,我们讨论的是标准化,而不是同质化。大多数现代云应用程序都不是同质化的。你的应用程序可能由五种不同的微服务组成,运行在 Kubernetes、AWS Lambda 和 EC2 等平台上。要为这些不同类型的组件构建通用的自动化,我们只需要标准化它们生命周期的几个特定部分。
标准化——部署模板
那么,我们需要标准化什么?我们来看一下通常将代码变更部署到生产环境所涉及的步骤。它们可能是这样的:
  • 开发人员提交代码变更到版本控制存储库。
  • 我们使用最新的变更构建二进制构件,可能是一个 Docker 镜像,一个 JAR 文件,一个 ZIP 文件或其他一些构件。
  • 构件被发布:Docker 镜像被推送到 Docker 存储库,JAR 文件被推送到 Maven 存储库,ZIP 文件被推送到云存储的某个位置,等等。
  • 构件被部署到生产环境。这通常涉及逐个部署到每个单元。因此,对于应用程序的任何一个给定组件,这是部署过程的大致模板:
图 1:最小化的部署模板
蜂窝架构的目标之一是最小化故障的爆炸半径,而故障最有可能发生的一个时间点是在部署之后。因此,在实践中,我们希望在部署过程中添加一些保护措施,如果检测到问题,可以停止部署变更,直到解决问题为止。为此,我们添加了一个“暂存(staging)”单元,并在部署到后续单元之间添加一个“烘烤(bake)”周期,这是一个好主意。在烘烤期间,我们可以监视指标和警报,如果有任何异常就停止部署。现在给定的组件的部署模板可能看起来像这样:
图 2:带有“烘烤”阶段的部署模板
现在,我们的目标是通用化我们的自动化,对于任何一个应用程序组件都可以轻松实现这一组部署步骤,无论这些组件是基于什么样的技术构建的。
有许多工具可以自动执行上述的步骤。在本文的其余部分,我们将使用一些基于 Momento 选择的工具,但你也可以使用更适合你特定环境的工具来实现这些步骤。
Momento 的大部分基础设施都部署在 AWS 上,因此我们倾向于使用 AWS 工具。对于在 EC2 上运行并通过 CloudFormation 部署的应用程序组件,我们使用:
  • AWS CodePipeline 用于定义和执行阶段;
  • AWS CodeBuild 用于执行各个构建步骤;
  • AWS Elastic Container Registry 用于发布组件的新 Docker 镜像;
  • AWS CloudFormation 用于将新版本部署到每个单元;
  • AWS Step Functions 用于在“烘烤”阶段监视警报,并决定是否可以安全地将变更部署到下一个单元。
图 3:部署阶段实现——基于 CloudFormation
对于基于 Kubernetes 的组件,我们稍微做一些修改即可实现相同的步骤:我们使用 AWS Lambda 调用 k8s  API 将新镜像部署到单元中。
图 4:部署阶段实现——基于 Kubernetes
尽管应用程序组件的技术栈存在差异,但我们可以定义一个通用的模板来描述变更的部署步骤。然后,我们可以使用相同的工具链实现这些步骤,并对特定步骤进行微小的修改。将构建生命周期中的一些东西标准化,有助于我们以一种通用的方式为这些步骤构建自动化,这意味着我们可以重复使用大量的基础设施代码,我们的部署在所有组件中都将是一致和可识别的。
标准化——构建目标
那么,我们如何在各种组件之间标准化所需的步骤呢?一个有价值的策略是定义一些标准化的构建目标,并在所有组件中重用它们。在 Momento,我们使用了一种经过验证的技术:Makefiles。
Makefiles 相当简单,而且它们已经存在很久了。它们在这方面有非常好的效果。
图 5:使用 Makefiles 标准化构建目标
在左边,你可以看到我们的一个 Kotlin 微服务的 Makefile 片段。右边是一个 Rust 服务的 Makefile 片段。构建命令不同,但关键在于在这两个文件中有着完全相同的构建目标。
例如,我们有一个 pipeline-build 目标,它用于控制服务在 build 步骤所发生的事情。然后,我们有一些用于“单元引导”和“GCP 单元引导”的目标,因为我们可以部署到 AWS 单元或 GCP 单元。Makefile 的目标名称是相同的;在这些单独服务之外运行的基础设施的其他部分现在有了这个共同的生命周期,它们知道它们可以依赖于每个组件内部的存在,在进行部署时,它们需要与这些组件交互。
标准化——单元注册表
帮助我们标准化自动化的另一个构建块是“单元注册表”。它是一种机制,为我们提供所有单元的清单和它们的基本元数据。
图 6:用于单元注册表的 TypeScript 模型
在 Momento,我们使用 TypeScript 构建单元注册表。我们使用了大约 100 行 TypeScript 代码定义了一些简单的接口,我们可以使用它们来表示所有单元的数据。我们有一个 CellConfiguration 接口,它可能是最重要的一个,能捕获给定单元的所有重要信息。比如,它是一个生产单元还是开发单元?它在哪个区域?这个单元中端点的 DNS 名称是什么?这是 AWS 单元还是 GCP 单元?
我们还有一个 MomentoOrg 接口,它包含了一个 CellConfiguration 数组。
使用这些接口提供的模型,我们可以编写更多的 TypeScript 代码来实例化它们,并创建单元的数据。下面是这些代码的一个片段:
图 7:我们“alpha”单元的单元注册表数据
这是我们的“alpha”单元的数据,有单元名称、帐户 ID、区域、DNS 配置等。现在,每当我们想要添加新单元时,只需要输入这个单元注册表代码,并向这个数组添加一个新条目。
现在,我们有了所有单元的数据,我们需要将其发布到某个地方,这样就可以从基础设施的其他部分访问它。根据不同的情况,你可能会做一些复杂一点的事情,比如将数据存储在可以查询的数据库中。我们不需要这些东西,所以只需将数据以 JSON 的形式存储在 S3 中。
单元注册表的最后一个组件是一个小型的 TypeScript 库,它知道如何从 S3 检索这些数据,并将其转换成 TypeScript 对象。我们将该库发布到私有 npm 存储库,可以在我们基础设施的代码中使用它。这使得我们可以在我们的基础设施自动化过程中构建一些通用的模式,我们可以遍历所有单元并为每个单元配置相同的自动化。
标准化——单元引导脚本
我们用来通用自动化的最后一个标准化部分是“单元引导脚本”。将应用程序的所有组件部署到一个新单元可能非常具有挑战性、耗时且容易出错,而单元引导脚本可以简化这个过程,并确保单元之间的一致性。
假设你的应用程序组件的代码都位于一个 git 存储库中,那么,根据上述构建块,引导新单元的逻辑就可以像下面这样简单:
  • 使用单元注册表查找我们在此单元中所需的元数据(例如,AWS 帐户 ID、DNS 配置等)。
  • 对于每个应用程序组件:
    • 克隆该组件的 git 存储库;
    • 运行 Makefile 中标准化的 cell-bootstrap 目标。
图 8:单元引导脚本
这个脚本仅用五行代码为我们提供了一个通用且可扩展的用于部署应用程序新单元的解决方案。如果你向应用程序引入新组件,脚本仍然是适用的,并确保简单且一致的部署流程。
将所有部分组合起来
我们已经定义了一些标准化的构建块来帮助我们组织单元的信息,并对应用程序组件的各种生命周期任务进行了通用化,现在是时候重新审视一下我们需要解决的自动化基础设施的五个问题。
隔离
在 AWS 环境中确保单元隔离性最简单的方法是为每个单元创建一个单独的 AWS 帐户。起初,如果你不习惯于管理多个帐户,那么这看起来有点令人生畏。然而,现在的 AWS 工具已经非常成熟,因此这比你想象的要容易得多。
使用专有的 AWS 帐户部署单元可以确保默认与其他单元隔离,但你必须为一个单元与另一个单元的交互设置复杂的跨帐户 IAM 策略。反过来,如果你使用一个 AWS 帐户部署多个单元,就必须设置复杂的 IAM 策略来防止单元之间的交互。IAM 策略管理是使用 AWS 最具挑战性的部分之一,所以任何时候你都可以选择避免这么做,为你节省时间和减少痛点。
使用多个帐户的另一个好处是你可以使用 AWS Organizations 将这些帐户链接在一起,然后使用 AWS Cost Explorer 可视化和分析每个单元的成本。如果你选择使用单个 AWS 帐户部署多个单元,就必须仔细标记与每个单元相关的资源,以便查看每个单元的成本。使用多个帐户可以免费获得这个功能。
图 9:使用 AWS Cost Explorer 查看每个单元帐户的成本
与蜂窝架构随之而来的一个挑战是路由。如果你有多个隔离的单元,并且在每个单元中运行应用程序的一个副本,你就必须选择一种策略,将用户的流量从用户路由到目标单元。
如果用户通过 SDK 或你提供的其他客户端软件与应用程序交互,那么将流量路由到某个单元的一种简单方法是为每个单元使用唯一的 DNS 名。这是我们在 Momento 使用的方法。在为用户创建身份验证令牌时,我们将目标单元的 DNS 名作为令牌内部的声明包含在内,然后我们的客户端库就可以根据这个信息路由流量。
不过这种方法只适用于某些场景。如果你的用户通过网络浏览器与服务交互,你可能希望为他们提供一个可以在浏览器中访问的 DNS 名,这样他们就不需要知道单元的信息。对于这种情况就有必要创建一个薄路由层来引导流量。
图 10:用于单元隔离的路由层
路由层应该尽可能小。它应该包含用于识别用户的最基本的逻辑(根据请求中的一些信息),确定应该将流量路由到哪个单元,然后相应地代理或重定向请求。
这个路由层提供了更简单的用户体验(用户不需要知道单元的信息),但代价是你必须维护和监控这个新的全局组件。它还变成了一个单点故障点,但你可以通过蜂窝架构在很大程度上避免这种情况。这就是为什么你应该努力使让它变得尽可能小而简单。
拥有这样一个路由层的一个好处是,它可以透明地将用户从一个单元迁移到另一个单元。假设一个用户需要更大的或不那么拥挤的单元,你可以为他们准备好新单元,然后部署一个改变路由逻辑 / 配置的变更,在他们无感的情况下重定向他们的流量。
新单元
如果按照上面的标准化部分进行操作,你会发现,我们已经完成了大部分工作,解决了如何创建新单元的问题。我们所需要做的就是:
  • 在 Organization 中创建一个新的 AWS 账户;
  • 将账户添加到单元注册表中;
  • 运行单元引导脚本来构建和部署所有组件。
就这样,我们有了一个新的单元。由于我们在 Makefiles 中对每个组件的构建生命周期步骤进行了标准化,所以部署逻辑非常通用,几乎不需要花费什么功夫就可以启动一个新的单元。

部署
部署可能是应用程序架构需要解决的最具挑战性的问题,蜂窝架构尤其如此。所幸的是,在最近几年,基础设施即代码工具所取得的重大进展使这些挑战变得更容易解决。
在过去的几年里,大多数 IaC 工具都使用声明性配置语法(例如 YAML 或 JSON)来定义用户希望创建的资源。而最近的一种趋势是为开发人员提供一种使用真正的编程语言来表达基础设施定义的方式。开发人员现在可以使用他们熟悉的编程语言来定义基础设施组件,不必纠结于复杂且冗长的配置文件。下面是这类工具的一些示例:
  • AWS CDK(云开发工具包)——用于部署 CloudFormation 基础设施;
  • AWS cdk8s——用于部署 Kubernetes 基础设施;
  • CDKTF(Terraform 的 CDK)——用于通过 HashiCorp Terraform 部署基础设施。我们可以在这些工具中使用 for 循环之类的构造来消除大量的 YAML/JSON 样板配置代码。
图 11:CloudFormation JSON 与 CDK TypeScript
使用编程语言,比如 TypeScript,来表达基础设施的另一个好处是,我们可以将 npm 库作为依赖项。这意味着我们的 IaC 项目可以在单元注册表库中添加依赖项,可以访问包含所有单元元数据的数组。然后,我们可以循环遍历这个数组,定义每个单元所需的基础设施步骤。在添加新单元和更新单元注册表时,基础设施也将自动更新!
AWS CDK 和 AWS CodePipeline 的组合功能非常强大,我们可以使用通用模式为每个应用程序组件定义管道,并在共享大部分代码的同时为每个组件设置必要的构建和部署步骤。
在 Momento,我们为可能需要添加到 AWS CodePipeline 中的每种类型的阶段编写了一些 TypeScript CDK 代码(例如,构建项目、推送 Docker 镜像、部署 CloudFormation 栈、将新镜像部署到 Kubernetes 集群等)。我们可以将这些阶段放到数组中,然后循环遍历它,将阶段添加到每个管道中:
图 12:将阶段添加到 CodePipeline 的 CDK 代码
我们创建了一个特殊的管道,叫作“管道的管道”。它是一个“元”管道,负责为每个应用程序组件创建单独的管道。
图 13:管道的管道
这个存储库作为我们所有部署逻辑的单一事实来源。每当开发人员需要更改部署基础设施的内容时,都可以在这里完成。我们对部署步骤列表(例如,更改单元的顺序或使用更复杂的“烘焙”步骤)所做的任何更改都将自动反映在所有组件管道中。在添加新单元时,管道的管道会运行并更新所有组件管道,将新单元添加到部署步骤列表中。
为了帮助改进可用性,我们仔细考虑了部署到生产单元的顺序。单元根据大小、重要性和流量级别进行分组。在第一阶段,我们会部署到预生产单元,在这里对变更进行测试,然后才会被推送到生产单元。如果这些部署进展顺利,我们会逐渐部署到越来越大的生产单元中。这种分阶段的部署方法让变更部署变得可控,并增加在问题影响更多客户之前捕获它们的可能性。
权限
为了管理单元的访问权限,我们主要依赖 AWS 的 SSO(现在的 IAM Identity Center)。这个服务为我们提供了一个单点登录页面,所有开发人员都可以使用他们的 Google 身份登录,然后访问他们有权限访问的 AWS 控制台。它还通过命令行和 AWS SDK 的方式提供对目标账户的访问,使自动化操作任务变得容易。
管理接口提供了对每个帐户内的用户访问的细粒度控制。例如,在单元账户内定义了“只读”和“单元操作员”等角色,授予不同级别的权限。
图 14:AWS SSO 账户权限
将 AWS SSO 的角色映射能力与 CDK 和我们的单元注册表结合起来,我们就可以完全自动化每个单元账户的入站和出站权限。
对于入站权限,我们可以循环遍历注册表中所有开发人员和单元账户,并使用 CDK 授予适当的角色。在向单元注册表添加新账户时,自动化机制会自动设置正确的权限。我们对注册表中的每个单元进行循环遍历,根据需要对资源(如 ECR 镜像或私有 VPC)授予访问权限,以获得出站权限。
监控
监控大量的单元可能很困难。关键在于要有一种监控手段,确保运维人员可以在单个视图中评估所有单元内服务的运行状况。指望运维人员查看每个单元账户中的指标不是一个可扩展的解决方案。
为了解决这个问题,你只需要选择一种集中式的指标解决方案,你可以导出所有单元账户的指标。这种解决方案还必须支持通过维度对指标进行分组,比如单元名称。
许多指标解决方案提供了这种功能,可以将多个账户的指标聚合到中央监控账户的 CloudWatch 指标中。还有许多第三方选择,例如 Datadog、New Relic、LightStep 和 Chronosphere。
下面是 LightStep 仪表盘的截图,其中 Momento 的指标按单元名称分组:
图 15:指标仪表盘,按单元名称分组的指标
额外的好处
我们已经介绍了蜂窝架构如何帮助实现高可用性,以及现代基础设施和基础设施工具如何帮助我们自动化蜂窝基础设施,现在来看一下我们可以从这种自动化中获得的额外好处。
一个关键的好处是能够非常快速地启动新单元。我们可以使用本文描述的单元引导脚本从零开始在几小时内部署一个新单元。如果没有基础的标准化和自动化,这个过程的大多数步骤都必须手动完成,甚至需要花费数周的时间。对于初创公司和小型公司来说,能够快速添加新单元来满足用户的需求可能是一个巨大的价值主张。它可能成为是否能够达成重要交易的关键因素。
另一个巨大的价值在于开发人员可以在自己的开发账户中创建个人单元。有时候,如果没有真实的环境,根本无法测试和调试依赖多个服务或组件之间交互的复杂功能。
一些工程组织会尝试使用共享的开发环境来解决这个问题,但这需要开发人员之间的密切协作,并且容易发生冲突和停机。相反,使用我们的单元引导脚本,开发人员可以在一天内创建和销毁完整的应用程序开发部署环境。这种敏捷的方法最大程度地减少了中断,提升了生产效率,使开发人员能够专注于他们的任务,而不会在无意中影响到其他人。
没有一刀切的解决方案
在本文中,我们分享了我们选择的几种工具和技术来实现蜂窝基础设施的自动化。需要注意的是,对于本文中提到的技术,都有大量的替代选择。例如,虽然 Momento 使用了一些 AWS 工具,但其他主要的云提供商,如 GCP 和 Azure,也为每个相关的任务提供了类似的产品。
此外,你可能会选择自动化其中的部分内容,或者可能选择自动化更多超出本文分享的内容!关键在于你要选择适合你业务和环境的工具和自动化级别。
总     结
蜂窝架构可以提升可用性,并确保达成你的 SLA。对业务的敏捷性和工程速度也很有价值。自动化这些过程只需要解决本文中提出的一些关键问题,并在应用程序组件之间标准化一些东西。
基础设施即代码领域的一些进展让自动化变得更加简单,只要你利用这些机会来标准化一些关于如何定义组件的东西。
其他资源
  • Peter Vosshall 在 re:Invent 2018 上的演讲: AWS 如何最小化故障的影响范围
  • AWS Well-Architected:蜂窝架构
  • Slack 迁移到蜂窝架构的经验
查看英文原文
https://www.infoq.com/articles/high-availability-in-the-cloud-with-cellular-architecture/
声明:本文由 InfoQ 翻译,未经许可禁止转载。
今日好文推荐
继续阅读
阅读原文