微服务应用配置实践指南

NGINX | 2023年3月02日

被称为“十二要素应用”的指南最早发布于十多年前。此后,其中几乎所有强制性实践都已成为编写和部署Web应用的事实标准。尽管应用的组织和部署方式不断变化,这些实践仍然适用,但在某些情况下,需要进一步了解其中的细微差别,才能理解这些实践如何应用于开发和部署应用的微服务模式。

本博客重点介绍第3项原则:在环境中存储配置。该原则指出:

  • 配置是指在不同部署环境(十二要素应用将其称为“deploys”)之间存在差异的所有内容。
  • 配置必须与应用代码严格分离,否则如何能因部署环境而异?
  • 配置数据存储在环境变量中。

在转向微服务时,仍可遵循这些准则,但具体做法不一定与十二要素应用方法的字面解释完全对应。有些准则可以直接沿用,例如通过环境变量提供配置数据。其他常见的微服务实践虽然遵循十二要素应用方法的核心原则,但更像是对这些原则的扩展。本文将从要素3的视角,探讨微服务配置管理的3个核心概念:

微服务关键术语和概念

在讨论如何针对微服务调整因素 3 之前,先了解一些关键术语和概念会有所帮助。

  • 单体应用架构:一种传统架构模型,将应用功能划分为多个组件模块,但所有模块均包含在同一个代码库中。
  • 微服务应用架构:一种使用多个小型组件构建大型复杂应用的架构模型。每个组件执行一组范围明确的操作(例如身份验证、通知或支付处理)。“微服务”也指这些小型组件本身。在实践中,有些“微服务”实际上可能相当庞大。
  • 服务:系统中单个应用或微服务的统称。
  • 系统:在本博客中,指共同构成组织所提供完整功能的全部微服务及配套基础设施。
  • 制品:由测试和构建流水线创建的对象。它可以采用多种形式,例如包含应用代码的Docker镜像。
  • 部署:制品的一个运行中“实例”,在预发布、集成或生产等环境中运行。

微服务与单体架构对比

采用单体应用时,组织内的所有团队都在同一个应用及其配套基础设施上开展工作。尽管从理论上看,单体应用通常比微服务更简单,但组织决定迁移到微服务通常有以下几个原因:

  • 团队自主权:在单体应用中,功能和子系统的归属可能难以界定。随着组织不断发展和成熟,应用功能的职责通常会分散到越来越多的团队。这会导致团队之间产生依赖关系,因为负责某项功能的团队并不负责单体应用中与该功能相关的所有子系统。
  • 缩小“影响范围”:当大型应用作为一个整体进行开发和部署时,某个子系统中的错误可能会导致整个应用的功能受损。
  • 独立扩展功能,即使单体应用中只有一个模块处于高负载状态,组织也必须部署整个应用的多个实例,以避免系统故障或性能下降。

当然,微服务也会带来一些挑战,包括复杂性增加、可观测性降低,以及需要采用新的安全模型。不过,许多组织,尤其是大型或快速发展的组织,认为这些挑战值得应对,因为这能让团队在构建可靠、稳定的基础以支持客户体验时,拥有更大的自主性和灵活性。

微服务架构所需的变革

将单体应用重构为微服务时,各项服务必须:

  • 以可预测的方式接受配置更改
  • 以可预测的方式向更广泛的系统表明自身存在
  • 具备完善的文档

对于单体应用,流程中的细微不一致以及对共同假设的依赖并不严重。然而,当存在大量独立的微服务时,这些不一致和假设可能会带来诸多问题和混乱。采用微服务时需要做出的许多改变源于技术上的必要性,但其中相当一部分涉及团队内部的工作方式以及与其他团队的协作方式。

采用微服务架构后,值得关注的组织变革包括:

  • 各团队不再基于同一代码库协同工作,而是完全独立,每个团队对一项或多项服务负全责。在最常见的微服务实施模式中,团队还会重组为“跨职能”团队,即团队成员具备实现团队目标所需的全部能力,从而尽量减少对其他团队的依赖。
  • 平台团队(负责系统的整体健康状况)现在必须协调由不同团队负责的多项服务,而不再只是处理单个应用。
  • 工具团队必须持续为各服务负责人团队提供工具和指导,帮助他们快速实现目标,同时保持系统稳定。

单体应用与微服务应用的开发团队组织结构对比图

明确定义服务配置

在微服务架构中,我们需要对因素 3 加以扩展,其中一个方面是明确界定服务的某些关键信息(包括其配置),并尽量减少与其他服务共享的上下文。因素 3 并未直接涉及这一点,但当大量独立的微服务共同实现应用功能时,这一点尤为重要。

作为微服务架构中的服务所有者,您的团队负责在整个系统中承担特定角色的服务。其他团队的服务需要与这些服务交互,因此这些团队需要访问相应的服务代码库,以阅读代码和文档并参与贡献。

此外,软件开发领域还面临一个令人遗憾的现实:团队成员经常变动,这不仅是因为开发人员入职或离职,也可能源于内部重组。同时,特定服务的负责团队也经常发生变更。

鉴于这些现实情况,代码库和文档必须高度清晰且保持一致。为此,需要做到以下几点:

  • 明确定义每个配置选项的用途
  • 明确定义配置值的预期格式
  • 明确定义应用程序所要求的配置值提供方式
  • 将这些信息记录在少量文件中

许多应用程序框架都提供了定义必需配置的方法。例如,适用于Node.js应用程序的convict NPM包使用存储在单个文件中的完整配置“架构”。该文件是Node.js应用程序运行所需全部配置的唯一可信来源。

一个健全且易于查找的架构,让团队内外的人员都能轻松、放心地与您的服务交互。

如何向服务提供配置

明确应用所需的配置值后,还需要注意,已部署的微服务应用从以下两个主要来源获取配置,而这两类来源之间存在重要区别:

  • 明确指定配置设置并随应用程序源代码一同提供的部署脚本
  • 部署时查询的外部源

部署脚本是微服务架构中常见的代码组织模式。由于它们是在十二要素应用最初发布后才出现的,因此必然是对该方法的扩展。

模式:将部署和基础设施配置与应用程序放在一起

近年来,通常会在应用程序代码所在的同一代码库中创建一个名为“infrastructure”(或类似名称)的文件夹。该文件夹通常包含:

  • 基础设施即代码(Terraform是一个常见示例),用于描述服务所依赖的基础设施,例如数据库
  • 容器编排系统的配置,例如Helm charts和Kubernetes manifests
  • 与应用部署相关的任何其他文件

乍看之下,这似乎违反了因素 3 中关于配置必须与代码严格分离的规定。

事实上,将基础设施文件夹放在应用程序旁边,既遵循了这项规则,又能推动有价值的流程改进,而这些改进对于在微服务环境中工作的团队至关重要。

此模式的优势包括:

  • 负责该服务的团队也负责部署该服务及其专用基础设施(如数据库)。
  • 负责该服务的团队可以确保对其中任何元素的更改都经过其开发流程(代码审查、CI)。
  • 团队可以轻松更改其服务及配套基础设施的部署方式,无需依赖外部团队代为完成相关工作。

请注意,此模式带来的优势既能增强各团队的自主性,也能确保以更严谨的方式执行部署和配置流程。

各类配置应放在哪里?

在实际操作中,可以使用存储在基础设施文件夹中的部署脚本,既管理脚本本身明确定义的配置,也管理部署时从外部来源检索配置的过程。具体做法是让服务的部署脚本执行以下操作:

  1. 直接定义某些配置值
  2. 定义执行部署脚本的进程可从哪些外部源中查找所需的配置值

特定于服务某次部署且完全由团队控制的配置值,可以直接在 infrastructure 文件夹中的文件内指定。例如,可以限制由应用发起的数据库查询允许运行的时长。可以通过修改部署文件并重新部署应用来更改此值。

这种方案的一个好处是,此类配置变更必须经过代码审查和自动化测试,从而降低因值配置错误而导致服务中断的可能性。可以在源代码控制工具的历史记录中查找经过代码审查的值变更,以及任意时间点的配置键值。

应用运行所必需但不受团队控制的值,必须由应用的部署环境提供。例如,服务连接其所依赖的另一个微服务时使用的主机名和端口。

由于该服务不归您的团队所有,因此不能对端口号等值作出假设。这些值随时可能发生变化;无论是手动更改还是通过某种自动化流程更改,都需要在更改后将其注册到某个中央配置存储中。依赖这些值的应用随后可以查询它们。

这些准则可归纳为两项微服务配置最佳实践。

微服务配置禁忌:依赖硬编码值或共同约定值

在部署脚本中硬编码某些值看似最简单,例如与您的服务交互的另一项服务的位置。但在实践中,硬编码此类配置存在风险,尤其是在现代环境中,服务位置通常会频繁变化。如果第二项服务并非由您负责管理,风险会更高。

您可能认为,只要自己足够认真,就能及时更新脚本中的服务位置;更糟的是,您可能认为可以依靠负责该服务的团队在位置发生变化时通知您。但在压力之下,人们往往难以始终保持严谨。依赖人为的严谨性,可能导致系统在毫无预警的情况下发生故障。

微服务配置的正确做法:让服务询问“我的数据库在哪里?”

无论位置信息是否硬编码,应用都不得依赖位于特定位置的关键基础设施。新部署的服务应改为向系统内的某个公共信息源询问“我的数据库在哪里?”之类的问题,并获得有关该外部资源当前位置的准确答复。让每项服务在部署时向系统注册,可大幅简化这一过程。

将服务作为配置提供

系统需要回答“我的数据库在哪里?”以及“我所依赖的‘服务 X’在哪里?”等问题。同样,必须以适当方式向系统公开服务,使其他服务无需了解其部署方式即可轻松发现并与之通信。

微服务架构中的一项关键配置实践是服务发现,即注册新服务信息,并在其他服务访问这些信息时动态更新。了解微服务为何需要服务发现后,我们将通过一个示例,探讨如何使用NGINX Open Source和Consul实现服务发现。

通常会同时运行一个服务的多个实例(部署)。这样不仅可以处理更多流量,还能通过启动新部署,在不中断服务的情况下完成更新。NGINX等工具充当反向代理和负载均衡器,处理传入流量并将其路由到最合适的实例。这种模式很实用,因为依赖该服务的其他服务只需向NGINX发送请求,无需了解这些部署的任何信息。

例如,假设有一个名为messenger的服务实例,运行在充当反向代理的NGINX后端。

NGINX对“messenger”微服务的单个实例进行反向代理的示意图

如果应用广受欢迎,会怎么样?这当然是好消息,但随后您发现,随着流量增加,messenger实例占用了大量CPU,处理请求所需的时间也更长,而数据库似乎运行正常。这表明,或许可以通过再部署一个messenger服务实例来解决此问题。

部署第二个messenger服务实例后,NGINX如何知道该实例已上线并开始向其发送流量?一种方法是在NGINX配置中手动添加新实例,但随着更多服务扩容和缩容,这种方法很快就会变得难以管理。

一种常见的解决方案是使用高可用服务注册中心(如Consul)跟踪系统中的服务。新服务实例在部署时向Consul注册。Consul通过定期对这些实例执行健康检查来监控其状态。当某个实例未通过健康检查时,该实例将从可用服务列表中移除。

示意图:NGINX对两个“messenger”微服务实例进行反向代理,并使用Consul进行服务发现

NGINX可以通过多种方式查询Consul等注册中心,并相应调整路由。请注意,当NGINX充当反向代理或负载均衡器时,会将流量路由到“上游”服务器。请看以下简单配置:


# Define an upstream group called "messenger_service"
upstream messenger_service {
    server 172.18.0.7:4000;
    server 172.18.0.8:4000;
}

server {
    listen 80;

    location /api {
        # Proxy HTTP traffic with paths starting with '/api' to the
        # 'upstream' block above. The default load-balancing algorithm, 
        # Round-Robin, alternates requests between the two servers 
        # in the block.
        proxy_pass http://messenger_service;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
}

默认情况下,NGINX需要知道每个messenger实例的确切IP地址和端口,才能将流量路由到该实例。在本例中,172.18.0.7和172.18.0.8使用的端口均为4000。

这时就需要使用Consul和Consul template。Consul template与NGINX在同一容器中运行,并与维护服务注册表的Consul客户端通信。

当注册表信息发生变化时,Consul template会生成包含正确IP地址和端口的新版本NGINX配置文件,将其写入NGINX配置目录,并通知NGINX重新加载配置。NGINX重新加载配置时不会造成停机,重新加载完成后,新实例会立即开始接收流量。

在这种情况下,使用NGINX等反向代理,只需在系统中注册一个统一接入点,供其他服务访问。团队可以灵活管理各个服务实例,无需担心其他服务无法访问整个服务。

动手实践 NGINX 和 Microservices March

不可否认,微服务会增加复杂性,既包括服务层面的技术复杂性,也包括与其他团队协作时的组织复杂性。要充分发挥微服务架构的优势,必须审慎地重新评估为单体架构设计的实践,确认这些实践应用于截然不同的环境时,是否仍能带来同样的益处。本文探讨了十二要素应用的第3项原则如何继续在微服务环境中发挥作用,以及如何通过微调具体实施方式进一步提升其价值。

要进一步了解如何将十二要素应用方法用于微服务架构,请参阅Microservices March 2023的第1单元(即将在博客上发布)。免费注册,即可参加相关主题的网络研讨会和实践实验室。


分享

相关博客文章

NGINX Plus R26 正式发布
NGINX | 2022/02/15

NGINX Plus R26 正式发布

NGINX Plus R26通过缓存JSON Web密钥集加快JWT验证,并强化TLS握手,从而提升安全性。NGINX JavaScript的新功能包括增强对异步函数的支持,以及实现WebCrypto API。

使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击
NGINX | 2022/02/10

使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击

gRPC具备开发人员设计和部署现代应用所需的速度与灵活性,但其开放性也使其极易受到第7层DoS攻击。NGINX App Protect DoS可保护gRPC应用免受此类破坏性攻击。

Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划
NGINX | 2021/09/13

Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划

了解选择错误的Ingress控制器可能带来的风险,以及如何从关键方面着手,让您的选择更好地适应未来需求。

NGINX Plus R19 正式发布
NGINX | 2019/09/03

NGINX Plus R19 正式发布

NGINX Plus R19扩展了监控功能,不仅提供更多指标,还新增了指标分析方式;增加试运行模式,用于测试限速效果;增强键值存储,以支持IP地址范围;并使限速更加动态。

NGINX现已正式加入F5
NGINX | 2019/05/09

NGINX现已正式加入F5

NGINX现已正式成为F5的一部分。Gus Robertson现任F5 NGINX业务部门负责人。他阐述了两家公司在人才、文化和愿景方面融合的价值,以及双方将如何携手增强NGINX的开源产品和商业产品。

使用NGINX和NGINX Plus实现高性能缓存
NGINX | 2016/08/24

使用NGINX和NGINX Plus实现高性能缓存

我将介绍NGINX在内容缓存和性能方面的工作原理,并分享一些控制NGINX内容缓存方式的技巧。