亚马逊AWS官方博客

重构为无服务器架构:从应用程序到自动化

无服务器技术不仅能够最大限度地减少构建者用于管理基础设施的时间,还可以协助构建者减少他们需要编写的应用程序代码的数量。得益于业务逻辑和应用程序拓扑之间的更清晰的分离,将应用程序代码替换为完全托管式云服务可以提高应用程序的运营性和可维护性。本博客文章将向您介绍其中的工作原理。

无服务器不是运行时,而是一种架构

自 2014 年推出 AWS Lambda 以来,无服务器发展到今天,已不再只是云运行时。轻松部署和扩展单个函数能力与按毫秒计费相结合,推动了现代应用程序架构从整体式向松耦合应用程序的演进。函数通常通过事件进行通信,事件是一个由无服务器集成服务(例如 Amazon EventBridgeAmazon SNS)和 Lambda 的异步调用模型共同支持的交互模型。

带独立运行时元素(如 Lambda 函数或容器)的现代分布式架构具有一个独特的拓扑图,展示哪些元素与其它元素进行通信。在下图中,Amazon API Gateway、Lambda、EventBridge 和 Amazon SQS 进行交互,在典型的订单处理系统中处理订单。拓扑会对应用程序的运行时特征(例如延迟、吞吐量或弹性)产生重大影响。

使用 AWS 服务进行订单处理的无服务器拓扑

云自动化的角色在不断演变

云自动化语言通常被称为 IaC(基础设施即代码),可追溯到 2011 年推出的 CloudFormation,它可让用户在配置文件中声明一组云资源,而不是发出一系列 API 调用或 CLI 命令。最初面向文档的自动化语言(例如 AWS CloudFormationTerraform)很快得到了 AWS Cloud Development Kit(CDK)CDK for TerraformPulumi 等框架的补充,这些框架使得能够使用 TypeScript、Python 或 Java 等流行的通用语言编写云自动化代码。

云自动化的角色已随无服务器应用程序架构一起发生演变。借助无服务器技术,构建者无需管理基础设施,这样一来,无服务器 IaC 中实际上已没有“基础设施”。相反,无服务器云自动化主要通过将 Lambda 函数与事件源或目标(可以是其它 Lambda 函数)联系起来以定义应用程序的拓扑。这种方法更类似于“AaC”(架构即代码),因为自动化现在会定义应用程序架构,而不是预置基础设施元素。

使用自动化代码改进无服务器应用程序

通过利用 AWS 无服务器运行时功能,自动化代码通常能够实现与应用程序代码相同的功能。

例如,以下用 TypeScript 编写的 Lambda 函数会向 EventBridge 发送一条消息:

export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => { 
    const result = // some logic
    const eventParam = new PutEventsCommand({
        Entries: [
            {
              Detail: JSON.stringify(result),
              DetailType: 'OrderCreated',
              EventBusName: process.env.EVENTBUS_NAME,
            }
          ]
    });
 await eventBridgeClient.send(eventParam);     return {
       statusCode: 200,
       body: JSON.stringify({ message: 'Order created', result }),
    };
};

您可以使用 AWS Lambda Destinations 实施同一行为,它可指示 Lambda 运行时在函数完成后发布事件。您可以通过用 TypeScript 编写的以下 AWS CDK 代码配置 Lambda 目标:

import {EventBridgeDestination} from "aws-cdk-lib/aws-lambda-destinations"

const createOrderLambda = new Function(this,'createOrderLambda', {
    functionName: `OrderService`,
    runtime: Runtime.NODEJS_20_X,
    code: Code.fromAsset('lambda-fns/send-message-using-destination'),
    handler: 'OrderService.handler',
 onSuccess: new EventBridgeDestination(eventBus)
});

借助 AWS CDK,您可以对应用程序和自动化代码使用相同的编程语言,从而能够轻松地在两者之间切换。

Lambda 函数现在可侧重于业务逻辑,并且不包含对消息发送功能或 EventBridge 的任何引用。这种将关注点分离的做法是一项最佳实践,因为更改业务逻辑不会产生架构中断风险,反之亦然。

export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
    const result = //some logic
    return {
        statusCode: 200,
        body: JSON.stringify({ message: 'Order created', result }),
     };
};

相较于在应用程序代码中对事件进行手动编码,指示无服务器 Lambda 运行时发送事件有多种优势

  • 可将应用程序逻辑与拓扑分离开来。由服务类型(例如,EventBridge 与其它 Lambda 函数)和目标的 ARN 构成的消息目标定义了应用程序的架构(或拓扑)。在应用程序代码中嵌入消息发送功能会将架构与业务逻辑结合在一起。在运行时处理消息的发送可以分离关注点,从而无需因拓扑更改而接触应用程序代码。
  • 使构成更加清晰明确。如果应用程序代码发送消息,则可能会从传递给 Lambda 函数的环境变量中读取目标。用于此目的的变量的名称会隐藏在应用程序代码中,迫使您依赖命名约定。通过在自动化代码中定义服务实例之间的所有依赖项,可将它们置于中心位置,并可让您使用代码分析和重构工具来推理或更改架构。
  • 避免出现简单的错误。冗余的代码可能会导致出现错误。例如,调试一个意外地将消息日期字段中的日和月交换的 Lambda 函数需要花费数小时。让运行时发送消息可以避免出现此类错误。
  • 更高级别的构造简化了权限授予。利用像 CDK 这样的云自动化库,可以创建更高级别的构造,这些构造可以组合多种资源并包含必要的 IAM 权限。这将减少您需要编写的代码并避免调试周期。
  • 运行时更加稳健。将消息发送委托给无服务器运行时可处理所有必需的重试,从而确保发送消息,使构建者无需为这种无差别的繁重工作编写额外的代码。

总之,让托管服务处理消息传递可让您的无服务器应用程序更简洁、更稳健。我们还喜欢说这是“无服务器-原生”的,因为充分利用了对应用程序可用的原生服务。

重构到无服务器-原生

我们所说的“重构到无服务器”是指将代码从应用程序转移到自动化。重构是 Martin Fowler 在上个世纪 90 年代后期提出的一个术语,用于描述在不改变源代码的外部行为的情况下重新构建源代码以改变其结构的过程。代码重构既可以简单到将代码提取到单独的方法中,也可以复杂到用多态性替换条件表达式

开发人员重构代码以提高代码的可读性和可维护性。测试驱动开发(TDD)中的一种常见方法是所谓的红色-绿色-重构循环:编写一个测试,该测试会因功能未实现而变为红色,然后编写代码以使测试变为绿色,最后进行重构以抵消代码库中不断增长的熵。

无服务器重构从这一概念中汲取灵感,并将它延伸到无服务器自动化的环境中:

无服务器重构:一种控制技术,通过将应用程序代码替换为同等的自动化代码来改进无服务器应用程序的设计

让我们探究无服务器重构如何增强无服务器应用程序的设计和运行时特性。下图显示了一个通过图像识别来执行质量检查的 AWS Step Functions 工作流。如左图所示,早期实现将使用中间 AWS Lambda 函数来调用 Amazon Rekognition 服务。借助 2021 年推出的 Step Functions 的 AWS SDK 服务集成,您可以重构工作流以直接调用 Rekognition API。如右图所示,这种重构设计消除了 Lambda 函数(假设它没有执行任何额外任务),从而降低了成本和运行时复杂性。

在 Step Function 工作流中将 Lambda 替换为服务集成

GitHub 上了解使用 TypeScript 进行的有关此重构的 AWS CDK 实现。

重构限制

替换应用程序代码以通过 Lambda Destinations 向 SQS 发送消息的初始示例表明,从应用程序自动化代码的重构并不能完全保留行为。

首先,仅在异步调用函数时才会触发 Lambda Destinations。对于同步调用,该函数将结果传递回调用方,而不会调用目标。其次,无服务器运行时会将从函数返回的数据包装在消息信封中,这会影响消息接收者解析 JSON 对象的方式。消息数据将置于 responsePayload 字段(如果发送到另一个 Lambda 函数)或 detail 字段(如果发送到 EventBridge 目标)中。最后,Lambda Destinations 会在函数完成后发送消息,而应用程序代码会在执行期间的任何时间点发送消息。

Lambda Destination 执行

行为的最后一个变化对于架构良好的异步应用程序是透明的,因为它们不依赖于消息传送的时间。如果 Lambda 函数在发送消息后继续处理(例如,发送到 EventBridge),则代码不能假定消息已被处理,因为传送是异步进行的。罕见的例外可能是一个等待下游消息处理结果的循环,但此循环不仅违反了异步集成的原则,还浪费了计算资源(Amazon Step Functions 是异步回调的绝佳选择)。如果需要此类行为,可以通过将 Lambda 函数分成两部分来实现。

无服务器重构可以自动实施吗?

像“提取方法”这样的旧式代码重构是自动实施的,这要归功于许多代码编辑器的内置支持。无服务器重构还不是一种完全自动的 100% 等效代码转换,因为它需要将应用程序代码转换为自动化代码(反之亦然)。虽然像 Amazon Q Developer 这样的人工智能驱动型工具正在让我们更接近这一愿景,但我们认为无服务器重构主要是一种设计技术,可供开发人员用来更好地利用 AWS 运行时。改进的代码设计和运行时特性超出了行为差异,尤其是在您的应用程序包含自动化测试的情况下。

将重构整合到您的团队结构中

如果一个团队同时拥有应用程序和自动化代码,则重构会在团队内部进行。但是,当独立的团队开发业务逻辑而不是管理底层基础设施、配置和部署时,无服务器重构可以跨越团队边界。

在此类模型中,AWS 建议开发团队负责应用程序代码和特定于应用程序的自动化,例如用于配置 Lambda Destinations 的 CDK 代码、Step Functions 工作流或 EventBridge 路由。如果跨团队拆分应用程序和特定于应用程序的自动化,则会使开发团队需要依赖平台团队进行每次重构,并引发不必要的摩擦。

如果两个团队都使用相同的基础设施即代码(IaC)工具,例如 AWS CDK,则平台团队可以构建可重复使用的模板和构造来封装组织要求和防护机制,例如已启用加密的 S3 存储桶的 CDK 构造。开发团队可以轻松地跨 CDK 堆栈使用这些资源。

不过,团队可以使用不同的 IaC 工具,例如,基础设施团队更喜欢使用 CloudFormation,但开发团队更喜欢使用 AWS CDK。在这种情况下,开发团队可以在基础设施团队提供的 CFN 模块上构建自动化。但是,他们无法像使用 CDK 那样从高级编程抽象中受益。

分组模式下的协作

持续重构

就像旧式代码重构一样,重构到无服务器不是一次性活动,而是软件交付的一个重要方面。由于添加功能会增加应用程序的复杂性,因此定期重构有助于保持复杂性和维持开发速度。与持续交付一样,您可以使用持续重构来改进软件交付。

难以实施无服务器重构的团队可能缺少自动化测试覆盖或云自动化。因此,重构可以成为一项有用的强制功能,让团队实施软件交付代码异味检测,例如通过实施自动化测试。

入门

本博文讨论的重构示例是一个包含大量开源代码示例的目录的子集,您可以在 refactoringserverless.com 上找到这些示例以及 AWS CDK 实施示例。您还可以参阅另一篇博文,深入了解无服务器重构如何使您的应用程序架构更加松耦合。

借助这些示例,加快您自己的重构工作速度。立即实施重构!