

# Lambda 的工作原理
<a name="concepts-basics"></a>

Lambda 函数是用于构建 Lambda 应用程序的基本构建块。要编写函数，了解构成 Lambda 编程模型的核心概念和组件至关重要。本节将指导您了解使用 Lambda 构建无服务器应用程序所需了解的基本要素。
+ **[Lambda 函数和函数处理程序](#gettingstarted-concepts-function)**：Lambda 函数是为响应事件而运行的一段小型的代码块。函数可以是标准型（最长 15 分钟）或[持久性](durable-functions.md)（最长一年）。函数是用于构建应用程序的基本构建块。函数处理程序是 Lambda 函数代码处理的事件对象的入口点。
+ **[Lambda 执行环境和运行时](#gettingstarted-concepts-runtime)** - Lambda 执行环境管理运行函数所需的资源。对于[持久性函数](durable-functions.md)，执行环境包括自动状态管理和检查点功能。运行时是您的函数在其中运行的特定于语言的环境。
+ **[事件和触发器](#gettingstarted-concepts-event)**：其他 AWS 服务可以响应特定事件来调用函数。对于持久性函数，事件还可以触发暂停工作流的恢复。
+ **[Lambda 权限和角色](#gettingstarted-concepts-permissions)** - 控制谁可以访问您的函数以及您的函数可以与哪些其他 AWS 服务 服务交互。持久性函数需要额外的权限进行状态管理和扩展执行。

**提示**  
如果您想从更全面地了解无服务器开发开始，请参阅《AWS Serverless Developer Guide》**中的 [Understanding the difference between traditional and serverless development](https://docs.aws.amazon.com/serverless/latest/devguide/serverless-shift-mindset.html)。

## Lambda 函数和函数处理程序
<a name="gettingstarted-concepts-function"></a>

在 Lambda 中，**函数**是用来创建应用程序的基本构建块。Lambda 函数是一段代码，为响应事件而运行，例如用户点击网站上的按钮或将文件上传到 Amazon Simple Storage Service（Amazon S3）存储桶。借助持久性函数，您的代码能够在步骤之间暂停执行，并自动保存状态，使其非常适合用于诸如订单处理或内容审核等长时间运行的工作流。您可以将函数想象成一种具有以下属性的独立程序。

Lambda **函数处理程序**是函数代码中用于处理事件的方法。当函数为响应事件运行时，Lambda 会运行函数处理程序。有关导致函数运行的事件的数据将直接传递给处理程序。虽然 Lambda 函数中的代码可包含多个方法或函数，但 Lambda 函数只能有一个处理程序。

要创建 Lambda 函数，您需要将函数代码及其依赖项捆绑到一个部署包中。Lambda 支持两种类型的部署包：[.zip 文件归档](configuration-function-zip.md)和[容器映像](images-create.md)。
+ 函数具有一项特定任务或目的
+ 它们仅在需要时运行，以响应特定事件
+ 完成后，它们会自动停止运行

## Lambda 执行环境和运行时
<a name="gettingstarted-concepts-runtime"></a>

Lambda 函数在 Lambda 为您管理的安全、隔离的*[执行环境](lambda-runtime-environment.md)*中运行。对于[持久性函数](durable-functions.md)，执行环境包括用于状态管理和工作流协调的其他组件。该执行环境负责管理运行函数所需的流程和资源。首次调用函数时，Lambda 会为该函数创建一个新的执行环境以供其运行。函数运行完毕后，Lambda 不会立即停止执行环境；如果再次调用该函数，Lambda 可以重复使用现有执行环境。

Lambda 执行环境还包含*运行时*，即特定于语言的环境，可用于在 Lambda 与您的函数之间中继事件信息和响应。Lambda 为最流行的编程语言提供了多种[托管运行时](lambda-runtimes.md#runtimes-supported)，您也可以创建自己的运行时。

对于托管运行时，Lambda 会使用运行时自动应用安全更新和函数补丁。

## 事件和触发器
<a name="gettingstarted-concepts-event"></a>

您也可以使用 Lambda 控制台 [AWS CLI](https://aws.amazon.com/cli/) 或其中一个 [AWS 软件开发工具包（SDK）](https://aws.amazon.com/developer/tools/)直接调用 Lambda 函数。在生产应用程序中，更常见的情况是，其他 AWS 服务服务会调用您的函数来响应特定事件。例如，您可能希望在向 Amazon DynamoDB 表中添加项目时运行一个函数。

为了使函数响应事件，您需要设置一个**触发器**。触发器将函数连接到事件源，并且函数可以有多个触发器。当事件发生时，Lambda 会以 JSON 文档形式接收事件数据，并将其转换为代码可处理的对象。您可以为事件定义下面的 JSON 格式，Lambda 运行时会将此 JSON 转换为对象，然后再将其传递给函数的处理程序。

**Example 自定义 Lambda 事件**  

```
{
  "Location": "SEA",
  "WeatherData":{
    "TemperaturesF":{
      "MinTempF": 22,
      "MaxTempF": 78
    },
    "PressuresHPa":{
      "MinPressureHPa": 1015,
      "MaxPressureHPa": 1027
    }
  }
}
```

Amazon Kinesis 或 Amazon SQS 等流和队列服务使用[事件源映射](invocation-eventsourcemapping.md)而非标准触发器。事件源映射会轮询源以获取新数据，批处理记录，然后使用批量事件调用函数。有关更多信息，请参阅 [事件源映射与直接触发器的区别](invocation-eventsourcemapping.md#eventsourcemapping-trigger-difference)。

要了解触发器的工作原理，请先完成[使用 Amazon S3 触发器](with-s3-example.md)教程，或者要了解有关使用触发器的一般概述以及有关使用 Lambda 控制台创建触发器的说明，请参阅[与其他服务集成](lambda-services.md)。

## Lambda 权限和角色
<a name="gettingstarted-concepts-permissions"></a>

对于 Lambda，您需要配置两种主要的[权限](permissions-granting-access.md)类型：
+ 您的函数访问其他 AWS 服务所需的权限
+ 其他用户和 AWS 服务访问您的函数所需的权限

以下部分介绍了这两种权限类型，并讨论了应用最低权限许可的最佳实践。

### 函数访问其他 AWS 资源的权限
<a name="gettingstarted-concepts-permissions-role"></a>

Lambda 函数通常需要访问其他 AWS 资源并对这些资源执行操作。例如，函数可能从 DynamoDB 表中读取项目，将对象存储在 S3 存储桶中，或者写入 Amazon SQS 队列。要向函数提供执行这些操作所需的权限，可以使用*[执行角色](lambda-intro-execution-role.md)*。

Lambda 执行角色是一种特殊的 AWS Identity and Access Management（IAM）[角色](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html)，即您在账户中创建的身份，其具有*策略*中定义的与之相关的特定权限。

每个 Lambda 函数都必须有一个执行角色，并且一个角色可以供多个函数使用。调用函数时，Lambda 将担任该函数的执行角色，并获得权限可以执行该角色策略中定义的操作。

在 Lambda 控制台中创建函数时，Lambda 会自动为函数创建一个执行角色。该角色的策略授予函数向 Amazon CloudWatch Logs 写入日志输出的基本权限。要向您的函数授予对其他 AWS 资源执行操作的权限，您需要编辑角色以添加额外的权限。添加权限的最简单方法是使用 AWS [托管策略](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html#aws-managed-policies)。托管策略由 AWS 创建和管理，并为许多常见应用场景提供权限。例如，如果您的函数对 DynamoDB 表执行 CRUD 操作，则可以将 [AmazonDynamoDBFullAccess](https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonDynamoDBFullAccess.html) 策略添加到您的角色中。

### 其他用户和资源访问您的函数的权限
<a name="gettingstarted-concepts-permissions-resource-based"></a>

要授予其他 AWS 服务访问 Lambda 函数的权限，您需使用*[基于资源的策略](access-control-resource-based.md)*。在 IAM 中，基于资源的策略附加到资源（在本例中为您的 Lambda 函数），并且指定了有权访问资源的用户以及此类用户可以执行的操作。

要让其他 AWS 服务通过触发器调用您的函数，则函数基于资源的策略必须向该服务授予使用 `lambda:InvokeFunction` 操作的权限。如果您使用控制台创建触发器，则 Lambda 会自动为您添加此权限。

要向其他 AWS 用户授予访问您的函数的权限，您可以用与针对其他 AWS 服务或资源完全相同的方式，在函数基于资源的策略中定义此项操作。您也可以使用与用户关联的*[基于身份的策略](access-control-identity-based.md)*。

### Lambda 权限的最佳实践
<a name="gettingstarted-concepts-permissions-best-practice"></a>

在使用 IAM 策略设置权限时，[安全最佳实践](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html)是仅授予执行任务所需的许可。这称为*最低权限*原则。要开始为您的函数授予权限，您可以选择使用 AWS 托管策略。托管策略可能是授予执行任务所需权限的最快、最简单的方法，但它们也可能包含您不需要的其他权限。在您从早期开发过渡到测试和生产时，我们建议您通过定义自己的[客户管理型策略](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html#customer-managed-policies)，将权限减少到仅限于所需权限。

使用基于资源的策略授予访问函数的权限时，同样的原则也适用。例如，如果您想授予 Amazon S3 调用函数的权限，最佳实践是限制对单个存储桶或特定 AWS 账户中存储桶的访问权限，而不是向 S3 服务授予一揽子权限。

# 通过 Lambda 运行代码
<a name="concepts-how-lambda-runs-code"></a>

编写 Lambda 函数时，您创建的代码将在独特的无服务器环境中运行。了解 Lambda 实际如何运行您的代码涉及两个关键方面：定义您的代码如何与 Lambda 交互的编程模型，以及确定 Lambda 如何管理您的代码的运行时环境的执行环境生命周期。

## Lambda 编程模型
<a name="concepts-progmodel-overview"></a>

编程模型充当 Lambda 如何与您的代码协同工作的一组通用规则，无论您使用 Python、Java 还是任何其他受支持的语言编写。编程模型包括运行时和处理程序。

**对于标准函数：**

1. Lambda 接收事件。

1. Lambda 使用运行时以您的代码可使用的格式准备事件。

1. 运行时将格式化的事件发送到处理程序。

1. 处理程序使用您编写的代码来处理事件。

**对于持久性函数：**

1. Lambda 接收事件

1. 运行时会同时准备事件和 DurableContext

1. 您的处理程序可以：
   + 使用自动检查点机制处理步骤
   + 不消耗资源的情况下暂停执行
   + 从上次成功的检查点恢复
   + 在各步骤间保持状态

此模型的核心是*处理程序*，Lambda 将事件发送到处理程序，供您的代码处理。可以将其视为代码的入口点。当 Lambda 收到事件时，它会将此事件及其一些上下文信息传递给处理程序。然后，处理程序会运行代码来处理这些事件，例如，它可能会读取上传到 Amazon S3 的文件、分析图像或更新数据库。当代码完成对一个事件的处理后，处理程序便可以处理下一个事件。

## Lambda 执行模型
<a name="concepts-exec-env-overview"></a>

虽然编程模型定义了 Lambda 如何与您的代码交互，但执行环境才是 Lambda 实际运行函数的地方，它是一个专为您的函数创建的安全、隔离的计算空间。

**每个环境都有其自身的生命周期，其在标准函数和持久性函数之间有所不同：**

**标准函数（最长 15 分钟）：**

1. **初始化：**环境设置和代码加载

1. **调用：**单次执行函数代码

1. **关闭：**环境清理

**持久性函数（最长 1 年）：**

1. **初始化：**环境和持久状态设置

1. **调用：**具有自动检查点机制的多个步骤

1. **等待状态：**在不消耗资源的情况下暂停执行

1. **恢复：**从上一个检查点重新启动

1. **关闭：**清理持久状态

此环境负责处理运行函数的重要方面。它为您的函数提供内存和 `/tmp` 目录用于临时存储。**对于持久性函数，它还管理：**
+ 步骤之间的自动状态持久性
+ 检查点存储和恢复
+ 等待状态协调
+ 长时间运行的执行的进度跟踪

# 了解 Lambda 编程模型
<a name="foundation-progmodel"></a>

Lambda 提供两种编程模型：最长运行 15 分钟的标准函数和最长可运行一年的持久性函数。虽然两者都具有相同的核心概念，但持久性函数还为长时间运行的有状态工作流增加了功能。

Lambda 提供了对所有运行时都通用的编程模型。该编程模型定义了代码和 Lambda 系统之间的接口。您可以通过在函数配置中定义*处理程序*来告知 Lambda 您函数的入口点。运行时将对象（例如函数名和请求 ID）传递给包含调用*事件*以及*上下文*的处理程序。

**对于持久性函数，处理程序还会收到一个 DurableContext 对象，该对象提供：**
+ 通过 step() 实现的检查点功能
+ 通过 wait() 和 waitForCallback() 进行的等待状态管理
+ 调用之间的自动状态持久性

在处理程序完成第一个事件的处理后，运行时会向处理程序发送另一个事件。对于持久性函数，处理程序可以在各步骤之间暂停执行，而 Lambda 会自动保存并在函数恢复时恢复状态。函数的类保留在内存中，因此，可以重用*初始化代码*中在处理程序方法外部声明的客户端和变量。要节省后续事件的处理时间，请在初始化期间创建可重用的资源，如 AWS 开发工具包客户端。在初始化后，函数的每个实例都可以处理数千个请求。

此外，您的函数有权访问 `/tmp` 目录中的本地存储，这是可用于多次调用的暂时性缓存。有关更多信息，请参阅 [执行环境](lambda-runtime-environment.md)。

在启用[AWS X-Ray跟踪](services-xray.md)时，运行时会单独记录初始化和执行的子分段。

运行时捕获来自函数的日志记录输出，并将它发送到 Amazon CloudWatch Logs。除了记录函数的输出外，运行时还会在函数调用开始和结束时记录条目。这包括具有请求 ID、计费持续时间、初始化持续时间和其他详细信息的报告日志。如果函数引发一个错误，则运行时会将该错误返回到调用程序。

**注意**  
日志记录受 [CloudWatch Logs 配额](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/cloudwatch_limits_cwl.html)的限制。由于节流或在某些情况下函数实例停止，日志数据可能会丢失。

**持久性函数的主要区别：**
+ 状态在步骤之间自动保存
+ 函数可以在不消耗资源的情况下暂停执行
+ 步骤会在失败时自动重试
+ 通过检查点跟踪进度

Lambda 通过在需求增加时运行其他实例并在需求减少时停止实例来扩展您的函数。此模型会导致应用程序架构发生变化，例如：
+ 除非另有说明，否则传入请求可能会不按次序处理或同时处理。
+ 不依赖长期存在的函数实例，而是将应用程序的状态存储在其他位置。
+ 虽然可使用本地存储和类级别对象来提高性能，但应将部署包的大小和传输到执行环境中的数据量保持在最小值。

有关您常用的编程语言中的编程模式的实际操作介绍，请参阅以下章节。
+ [使用 Node.js 构建 Lambda 函数](lambda-nodejs.md)
+ [使用 Python 构建 Lambda 函数](lambda-python.md)
+ [使用 Ruby 构建 Lambda 函数](lambda-ruby.md)
+ [使用 Java 构建 Lambda 函数](lambda-java.md)
+ [使用 Go 构建 Lambda 函数](lambda-golang.md)
+ [使用 C\$1 构建 Lambda 函数](lambda-csharp.md)
+ [使用 PowerShell 构建 Lambda 函数](lambda-powershell.md)

# 了解 Lambda 执行环境生命周期
<a name="lambda-runtime-environment"></a>

Lambda 执行环境既支持标准函数（最长运行 15 分钟），也支持持久性函数（最长运行一年）。尽管两者具有相同的基本生命周期，但持久性函数还为长时间运行的工作流增添了状态管理功能。

 Lambda 在执行环境中调用您的函数，该环境提供一个安全和隔离的运行时环境。执行环境管理运行函数所需的资源。执行环境为函数的运行时以及与函数关联的任何[外部扩展](lambda-extensions.md)提供生命周期支持。

**对于持久性函数，执行环境包括用于以下内容的其他组件：**
+ 步骤之间的状态持久性
+ 检查点管理
+ 等待状态协调
+ 进度跟踪

**Lambda 托管实例执行环境**  
如果您使用的是 [Lambda 托管实例](lambda-managed-instances-execution-environment.md)，则执行环境与 Lambda（默认）函数相比存在重要区别。托管实例支持并发调用，使用不同的生命周期模型，并在客户拥有的基础设施上运行。有关托管实例执行环境的详细信息，请参阅[了解 Lambda 托管实例执行环境](lambda-managed-instances-execution-environment.md)。

函数的运行时使用[运行时 API](runtimes-api.md) 与 Lambda 进行通信。扩展使用[扩展 API](runtimes-extensions-api.md) 与 Lambda 进行通信。扩展还可借助[遥测 API](telemetry-api.md)，从该函数接收日志消息与其他遥测数据。



![\[执行环境的体系结构图。\]](http://docs.aws.amazon.com/zh_cn/lambda/latest/dg/images/telemetry-api-concept-diagram.png)


创建 Lambda 函数时，您需要指定配置信息，例如可用内存量和函数允许的最长执行时间。Lambda 使用此信息设置执行环境。

函数的运行时和每个外部扩展都是在执行环境中运行的进程。权限、资源、凭证和环境变量在函数和扩展之间共享。

**Topics**
+ [Lambda 执行环境生命周期](#runtimes-lifecycle)
+ [冷启动和延迟](#cold-start-latency)
+ [使用预调配并发减少冷启动](#cold-starts-pc)
+ [优化静态初始化](#static-initialization)

## Lambda 执行环境生命周期
<a name="runtimes-lifecycle"></a>

![\[Lambda 生命周期阶段：初始化、调用、关闭\]](http://docs.aws.amazon.com/zh_cn/lambda/latest/dg/images/Overview-Successful-Invokes.png)


每个阶段都以 Lambda 发送到运行时和所有注册的扩展的事件开始。运行时和每个扩展通过发送 `Next` API 请求来指示完成。当运行时和每个扩展完成且没有挂起的事件时，Lambda 会冻结执行环境。

**持久性函数的生命周期阶段包括：**
+ **初始化：**标准初始化加上持久状态设置
+ **调用：**可以包括具有自动检查点机制的多个步骤执行
+ **等待：**函数可以在不消耗资源的情况下暂停执行
+ **恢复：**函数从上一个检查点重新启动
+ **关闭：**清理持久状态和资源

**Topics**
+ [Init 阶段](#runtimes-lifecycle-ib)
+ [在 Init 阶段失败](#runtimes-lifecycle-init-errors)
+ [还原阶段（仅限 Lambda SnapStart）](#runtimes-lifecycle-restore)
+ [调用阶段](#runtimes-lifecycle-invoke)
+ [在调用阶段失败](#runtimes-lifecycle-invoke-with-errors)
+ [关闭阶段](#runtimes-lifecycle-shutdown)

### Init 阶段
<a name="runtimes-lifecycle-ib"></a>

在 `Init` 阶段，Lambda 执行三项任务：
+ 开启所有扩展 (`Extension init`)
+ 引导运行时 (`Runtime init`)
+ 运行函数的静态代码 (`Function init`)
+ 运行任何检查点前的[运行时钩子](snapstart-runtime-hooks.md)（仅限 Lambda SnapStart）

当运行时和所有扩展通过发送 `Init` API 请求表明它们已准备就绪时， `Next` 阶段结束。`Init` 阶段限制为 10 秒。如果所有三个任务都未在 10 秒内完成，Lambda 在第一个函数调用时使用配置的函数超时值重试 `Init` 阶段。

激活 [Lambda SnapStart](snapstart.md) 后，在您发布一个函数版本时会发生 `Init` 阶段。Lambda 保存初始化的执行环境的内存和磁盘状态的快照，永久保存加密快照并对其进行缓存以实现低延迟访问。如果您具有检查点前的[运行时钩子](snapstart-runtime-hooks.md)，则该代码将在 `Init` 阶段结束时运行。

**注意**  
10 秒超时不适用于使用预置并发、SnapStart 或 Lambda 托管实例的函数。对于预置并发、SnapStart 和托管实例函数，初始化代码最长可能会运行 15 分钟。时间限制为 130 秒或配置的函数超时（最大 900 秒），以较高者为准。

使用[预置并发](https://docs.aws.amazon.com/lambda/latest/dg/provisioned-concurrency.html)时，Lambda 会在您为函数配置 PC 设置时初始化执行环境。Lambda 还确保初始化的执行环境在调用之前始终可用。您会发现函数的调用和初始化阶段之间存在时间差。根据函数的运行时系统和内存配置，在初始化的执行环境中首次调用时可能会发生一些延迟变化。

对于使用按需并发的函数，Lambda 可能会在调用请求之前偶尔初始化执行环境。发生这种情况时，您可能会观察到函数的初始化和调用阶段之间存在时间差。我们建议您不要依赖此行为。

### 在 Init 阶段失败
<a name="runtimes-lifecycle-init-errors"></a>

如果函数在 `Init` 阶段崩溃或超时，Lambda 会在日志中发出错误信息。`INIT_REPORT`

**Example — INIT\$1REPORT 超时日志**  

```
INIT_REPORT Init Duration: 1236.04 ms Phase: init Status: timeout
```

**Example — INIT\$1REPORT 扩展失败日志**  

```
INIT_REPORT Init Duration: 1236.04 ms Phase: init Status: error Error Type: Extension.Crash
```

如果 `Init` 阶段成功，除非启用 [SnapStart](snapstart.md) 或[预置并发](provisioned-concurrency.md)，否则 Lambda 不会发出 `INIT_REPORT` 日志。SnapStart 和预置并发函数始终会发出 `INIT_REPORT`。有关更多信息，请参阅 [监控 Lambda SnapStart](snapstart-monitoring.md)。

### 还原阶段（仅限 Lambda SnapStart）
<a name="runtimes-lifecycle-restore"></a>

当您首次调用 [SnapStart](snapstart.md) 函数时，随着该函数的扩展，Lambda 会从永久保存的快照中恢复新的执行环境，而不是从头开始初始化函数。如果您有还原后[运行时挂钩](snapstart-runtime-hooks.md)，则代码将在 `Restore` 阶段结束时运行。还原后运行时挂钩执行期间将产生费用。必须加载运行时，并且还原后运行时挂钩必须在超时限制（10 秒）内完成。否则，您将收到 SnapStartTimeoutException。`Restore` 阶段完成后，Lambda 将调用函数处理程序（[调用阶段](#runtimes-lifecycle-invoke)）。

#### 在 Restore 阶段失败
<a name="runtimes-lifecycle-restore-errors"></a>

如果 `Restore` 阶段失败，Lambda 会在 `RESTORE_REPORT` 日志中发出错误信息。

**Example — RESTORE\$1REPORT 超时日志**  

```
RESTORE_REPORT Restore Duration: 1236.04 ms Status: timeout
```

**Example — RESTORE\$1REPORT 运行时系统钩子失败日志**  

```
RESTORE_REPORT Restore Duration: 1236.04 ms Status: error Error Type: Runtime.ExitError
```

有关 `RESTORE_REPORT` 日志的更多信息，请参阅 [监控 Lambda SnapStart](snapstart-monitoring.md)。

### 调用阶段
<a name="runtimes-lifecycle-invoke"></a>

当调用 Lambda 函数以响应 `Next` API 请求时，Lambda 向运行时和每个扩展发送一个 `Invoke` 事件。

函数的超时设置限制了整个 `Invoke` 阶段的持续时间。例如，如果将函数超时设置为 360 秒，则该函数和所有扩展都需要在 360 秒内完成。请注意，没有独立的调用后阶段。持续时间是所有调用时间（运行时 \$1 扩展）的总和，直到函数和所有扩展完成执行之后才计算。

调用阶段在运行时之后结束，所有扩展都通过发送 `Next` API 表示它们已完成。

### 在调用阶段失败
<a name="runtimes-lifecycle-invoke-with-errors"></a>

如果 Lambda 函数在 `Invoke` 阶段崩溃或超时，Lambda 会重置执行环境。下图演示了调用失败时的 Lambda 执行环境行为：

![\[执行环境示例：初始化、调用、调用时出错、调用、关闭\]](http://docs.aws.amazon.com/zh_cn/lambda/latest/dg/images/Overview-Invoke-with-Error.png)


在上图中：
+ 第一个阶段是 **INIT** 阶段，运行没有错误。
+ 第二个阶段是 **INVOKE** 阶段，运行没有错误。
+ 在某些情况下，假设您的函数遇到调用失败（常见原因包括函数超时、运行时错误、内存耗尽、VPC 连接问题、权限错误、并发限制和各种配置问题）。有关可能的调用失败的完整列表，请参阅 [Lambda 中的调用问题疑难解答](troubleshooting-invocation.md)。标签为 **INVOKE WITH ERROR** 的第三个阶段演示了这种情况。出现这种情况时，Lambda 服务会执行重置。重置的行为类似于 `Shutdown` 事件。首先，Lambda 会关闭运行时，然后向每个注册的外部扩展发送一个 `Shutdown` 事件。该事件包括关闭的原因。如果此环境用于新调用，则 Lambda 会将扩展和运行时与下一次调用一起重新初始化。

  请注意，Lambda 重置不会在下一个初始化阶段之前清除 `/tmp` 目录内容。这种行为与常规关闭阶段一致。
**注意**  
AWS 目前正在实施对 Lambda 服务的更改。由于这些更改，您可能会看到 AWS 账户 中不同 Lambda 函数发出的系统日志消息和跟踪分段的结构和内容之间存在细微差异。  
如果您的函数的系统日志配置设置为纯文本，则当您的函数遇到调用失败时，此更改会影响在 CloudWatch Logs 中捕获的日志消息。以下示例显示了新旧格式的日志输出。  
这些更改将在未来几周内实施，除中国和 GovCloud 区域外，所有 AWS 区域 的函数都将过渡到使用新格式的日志消息和跟踪分段。

    
**Example CloudWatch Logs 日志输出（运行时或扩展崩溃）（旧样式）**  

  ```
  START RequestId: c3252230-c73d-49f6-8844-968c01d1e2e1 Version: $LATEST
  RequestId: c3252230-c73d-49f6-8844-968c01d1e2e1 Error: Runtime exited without providing a reason
  Runtime.ExitError
  END RequestId: c3252230-c73d-49f6-8844-968c01d1e2e1
  REPORT RequestId: c3252230-c73d-49f6-8844-968c01d1e2e1 Duration: 933.59 ms Billed Duration: 934 ms Memory Size: 128 MB Max Memory Used: 9 MB
  ```  
**Example CloudWatch Logs 日志输出（函数超时）（旧样式）**  

  ```
  START RequestId: b70435cc-261c-4438-b9b6-efe4c8f04b21 Version: $LATEST
  2024-03-04T17:22:38.033Z b70435cc-261c-4438-b9b6-efe4c8f04b21 Task timed out after 3.00 seconds
  END RequestId: b70435cc-261c-4438-b9b6-efe4c8f04b21
  REPORT RequestId: b70435cc-261c-4438-b9b6-efe4c8f04b21 Duration: 3004.92 ms Billed Duration: 3117 ms Memory Size: 128 MB Max Memory Used: 33 MB Init Duration: 111.23 ms
  ```

  CloudWatch 日志的新格式在 `REPORT` 行中包含一个附加 `status` 字段。在运行时或扩展崩溃的情况下，`REPORT` 行还包含一个字段 `ErrorType`。

    
**Example CloudWatch Logs 日志输出（运行时或扩展崩溃）（新样式）**  

  ```
  START RequestId: 5b866fb1-7154-4af6-8078-6ef6ca4c2ddd Version: $LATEST
  END RequestId: 5b866fb1-7154-4af6-8078-6ef6ca4c2ddd
  REPORT RequestId: 5b866fb1-7154-4af6-8078-6ef6ca4c2ddd Duration: 133.61 ms Billed Duration: 214 ms Memory Size: 128 MB Max Memory Used: 31 MB Init Duration: 80.00 ms Status: error Error Type: Runtime.ExitError
  ```  
**Example CloudWatch Logs 日志输出（函数超时）（新样式）**  

  ```
  START RequestId: 527cb862-4f5e-49a9-9ae4-a7edc90f0fda Version: $LATEST
  END RequestId: 527cb862-4f5e-49a9-9ae4-a7edc90f0fda
  REPORT RequestId: 527cb862-4f5e-49a9-9ae4-a7edc90f0fda Duration: 3016.78 ms Billed Duration: 3101 ms Memory Size: 128 MB Max Memory Used: 31 MB Init Duration: 84.00 ms Status: timeout
  ```
+ 第四个阶段是调用失败后立即进入的 **INVOKE** 阶段。在这里，Lambda 通过重新运行 **INIT** 阶段重新初始化环境。此情况称为*隐藏初始化*。出现隐藏初始化时，Lambda 不会在 CloudWatch Logs 中显式报告额外的 **INIT** 阶段。相反，您可能会注意到 REPORT 行中的持续时间包括一个额外的 **INIT** 持续时间 \$1 **INVOKE** 持续时间。例如，假设您在 CloudWatch 中看到以下日志：

  ```
  2022-12-20T01:00:00.000-08:00 START RequestId: XXX Version: $LATEST 
  2022-12-20T01:00:02.500-08:00 END RequestId: XXX 
  2022-12-20T01:00:02.500-08:00 REPORT RequestId: XXX Duration: 3022.91 ms 
  Billed Duration: 3000 ms Memory Size: 512 MB Max Memory Used: 157 MB
  ```

  在此例中，REPORT 和 START 时间戳的间隔为 2.5 秒。这与报告的持续时间（3022.91 毫秒）不一致，因为它没有考虑 Lambda 执行的额外 **INIT**（隐藏初始化）。在此例中，您可以推断出实际的 **INVOKE** 阶段用时 2.5 秒。

  要更深入地了解这种行为，您可以使用 [使用遥测 API 访问扩展的实时遥测数据](telemetry-api.md)。每当在调用阶段之间出现隐藏初始化时，Telemetry API 都会发出 `INIT_START`、`INIT_RUNTIME_DONE`、`INIT_REPORT` 事件以及 `phase=invoke`。
+ 第五个阶段是 **SHUTDOWN** 阶段，该阶段运行没有错误。

### 关闭阶段
<a name="runtimes-lifecycle-shutdown"></a>

若 Lambda 即将关闭运行时，它会向每个已注册的外部扩展发送一个 `Shutdown` 事件。扩展可以使用此时间执行最终清理任务。`Shutdown` 事件是对 `Next` API 请求的响应。

**持续时间限制**：`Shutdown` 阶段的最长持续时间取决于已注册扩展的配置：
+ 0 毫秒 – 无已注册扩展的函数
+ 500 毫秒 – 带有注册的内部扩展的函数
+ 2000 毫秒 – 具有一个或多个注册的外部扩展的函数

如果运行时或扩展没有在限制范围内响应 `Shutdown` 事件，则 Lambda 会使用 `SIGKILL` 信号结束该进程。

在函数和所有扩展完成后，Lambda 维护执行环境一段时间，以预期另一个函数调用。但是，Lambda 每隔几个小时就会终止执行环境，以便进行运行时更新和维护，即使是连续调用的函数亦不例外。您不应假设执行环境将无限期持续。有关更多信息，请参阅 [在函数中实施无状态性](concepts-application-design.md#statelessness-functions)。

当再次调用该函数时，Lambda 会解冻环境以便重复使用。重复使用执行环境会产生以下影响：
+ 在该函数的处理程序方法的外部声明的对象保持已初始化的状态，再次调用函数时提供额外的优化功能。例如，如果您的 Lambda 函数建立数据库连接，而不是重新建立连接，则在后续调用中使用原始连接。建议您在代码中添加逻辑，以便在创建新连接之前检查是否存在连接。
+ 每个执行环境都在 `/tmp` 目录中提供 512MB 到 10240MB 之间的磁盘空间（以 1MB 递增）。冻结执行环境时，目录内容会保留，同时提供可用于多次调用的暂时性缓存。您可以添加额外的代码来检查缓存中是否有您存储的数据。有关部署大小限制的更多信息，请参阅[Lambda 配额Lambda 限额](gettingstarted-limits.md)：
+ 如果 Lambda 重复使用执行环境，则由 Lambda 函数启动但在函数结束时未完成的后台进程或回调将继续执行。确保代码中的任何后台进程或回调在代码退出前已完成。

## 冷启动和延迟
<a name="cold-start-latency"></a>

当 Lambda 收到通过 Lambda API 运行函数的请求时，该服务会首先准备执行环境。在此初始化阶段，该服务会下载您的代码，启动环境，并在主处理程序之外运行任何初始化代码。最后，Lambda 会运行处理程序代码。

![\[性能优化图 1\]](http://docs.aws.amazon.com/zh_cn/lambda/latest/dg/images/perf-optimize-figure-1.png)


在此图中，下载代码和设置环境的前两个步骤通常称为“冷启动”。您[需为此时间付费](https://aws.amazon.com/blogs/compute/aws-lambda-standardizes-billing-for-init-phase/)，它会增加总调用持续时间的延迟。

调用完成后，执行环境将被冻结。为了改善资源管理和性能，Lambda 会在一段时间内保留执行环境。在此期间，如果收到另一个针对相同函数的请求，则 Lambda 可重复使用该环境。由于执行环境已经完全设置，第二个请求通常会更快地完成。这称为“暖启动”。

冷启动通常发生在低于 1% 的调用中。冷启动的持续时间从低于 100 毫秒到超过 1 秒不等。通常，在开发和测试函数中，冷启动通常比生产工作负载更常见。这是因为开发和测试函数的调用频率通常较低。

## 使用预调配并发减少冷启动
<a name="cold-starts-pc"></a>

如果您的工作负载需要可预测的函数启动时间，则推荐使用[预置并发](provisioned-concurrency.md)解决方案，以确保尽可能降低延迟。此功能可预先初始化执行环境，从而减少冷启动。

例如，预置并发数为 6 的函数预热了 6 个执行环境。

![\[性能优化图 4\]](http://docs.aws.amazon.com/zh_cn/lambda/latest/dg/images/perf-optimize-figure-4.png)


## 优化静态初始化
<a name="static-initialization"></a>

静态初始化发生在处理程序代码开始在函数中运行之前。这是您提供的初始化代码，位于主处理程序之外。此代码通常用于导入库和依赖项、设置配置和初始化与其他服务的连接。

以下 Python 示例显示了在调用期间运行 `lambda_handler` 函数之前，在初始化阶段导入和配置模块以及创建 Amazon S3 客户端。

```
import os
import json
import cv2
import logging
import boto3

s3 = boto3.client('s3')
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):

  # Handler logic...
```

函数执行前延迟的最大因素来自初始化代码。此代码在首次创建新的执行环境时运行。如果调用使用暖执行环境，则不会再次运行初始化代码。影响初始化代码延迟的因素包括：
+ 函数包的大小，包括导入的库和依赖项以及 Lambda 层。
+ 代码量和初始化工作。
+ 库和其他服务在设置连接和其他资源方面的表现。

开发人员可以采取多个步骤来优化静态初始化延迟。如果一个函数具有许多对象和连接，您可以将单一函数重新架构为多个专用函数。它们各自较小并且都具有较少的初始化代码。

重要的是，函数只导入其需要的库和依赖项。例如，如果您仅在 AWS SDK 中使用 Amazon DynamoDB，则可能需要单独的服务而不是整个 SDK。比较以下三个示例：

```
// Instead of const AWS = require('aws-sdk'), use:
const DynamoDB = require('aws-sdk/clients/dynamodb')

// Instead of const AWSXRay = require('aws-xray-sdk'), use:
const AWSXRay = require('aws-xray-sdk-core')

// Instead of const AWS = AWSXRay.captureAWS(require('aws-sdk')), use:
const dynamodb = new DynamoDB.DocumentClient()
AWSXRay.captureAWSClient(dynamodb.service)
```

静态初始化通常也是打开数据库连接的最佳位置，以允许函数在对同一执行环境的多次调用中重复使用连接。但是，您可能具有大量仅在函数的某些执行路径中使用的对象。在这种情况下，您可以在全局作用域内延迟加载变量，以缩短静态初始化持续时间。

避免使用全局变量来获取特定于上下文的信息。如果您的函数有一个全局变量，该变量仅在单一调用的生命周期内使用，并且在下一次调用时重置，请使用处理程序局部的变量作用域。这不仅可以防止调用间的全局变量泄漏，还可以提高静态初始化性能。

# 使用 Lambda 创建事件驱动型架构
<a name="concepts-event-driven-architectures"></a>

事件是指触发 Lambda 函数运行的任何内容。事件可以通过两种方式触发 Lambda 函数：直接调用（推送）和事件源映射（拉取）。

许多 AWS服务可以直接调用 Lambda 函数。这些服务会将事件*推送*到 Lambda 函数。触发函数的事件几乎可以是任何事物，包括通过 API Gateway 发出的 HTTP 请求、由 EventBridge 规则管理的计划、AWS IoT 事件或 Amazon S3 事件。借助事件源映射，Lambda 主动从队列或流中提取（或*拉取*）事件。您可以配置 Lambda 来检查来自受支持服务的事件，然后 Lambda 会处理函数的轮询和调用。

传递到函数时，事件会采用 JSON 格式的结构。JSON 结构因生成它的服务和事件类型而异。虽然标准 Lambda 函数调用最多可持续 15 分钟（[持久性函数](durable-functions.md)最长可持续一年），但 Lambda 最适合持续一秒或更短时间的短调用。对于事件驱动型架构来说尤其如此，其中每个 Lambda 函数都被视为负责执行一组特定指令的微服务。

**注意**  
事件驱动型架构使用网络在不同的系统之间进行通信，这会引入可变的延迟。对于需要非常低延迟的工作负载（如实时交易系统），此设计可能不是最佳选择。但是，对于高度可扩展和可用的工作负载，或者流量模式不可预测的工作负载，事件驱动型架构可以提供满足这些需求的有效方法。

**Topics**
+ [事件驱动型架构的优势](#event-driven-benefits)
+ [事件驱动型架构的利弊权衡](#event-driven-tradeoffs)
+ [基于 Lambda 的事件驱动型应用程序中的反模式](#event-driven-anti-patterns)

## 事件驱动型架构的优势
<a name="event-driven-benefits"></a>

在事件驱动型架构中，Lambda 支持两种调用方法：

1. 直接调用（推送方法）：AWS 服务直接触发 Lambda 函数。例如：
   + Amazon S3 在文件上传时触发函数
   + API Gateway 在收到 HTTP 请求时触发函数

1. 事件源映射（拉取方法）：Lambda 检索事件并调用函数。例如：
   + Lambda 从 Amazon SQS 队列中检索消息并调用函数
   + Lambda 从 DynamoDB 流中读取记录并调用函数

这两种方法都有助于发挥事件驱动型架构的优势，如下所述。

### 将轮询和 Webhook 替换为事件
<a name="polling-webhooks-events"></a>

许多传统架构使用轮询和 Webhook 机制来传达不同组件之间的状态。轮询在获取更新方面的效率可能非常低，因为在新数据可用和与下游服务同步之间存在延迟。您想要集成的其他微服务并不总是支持 Webhook。它们可能还需要自定义授权以及身份验证配置。在这两种情况下，如果没有开发团队的额外工作，则这些集成方法都很难按需扩展。

![\[事件驱动型架构图 7\]](http://docs.aws.amazon.com/zh_cn/lambda/latest/dg/images/event-driven-architectures-figure-7.png)


这两种机制都可以被事件替换，事件可以被筛选、路由并推送到下游使用微服务。此方法可以减少带宽消耗、CPU 使用率，且可能降低成本。这些架构还可以降低复杂性，因为每个功能单元都较小，而且通常代码较少。

![\[事件驱动型架构图 8\]](http://docs.aws.amazon.com/zh_cn/lambda/latest/dg/images/event-driven-architectures-figure-8.png)


事件驱动型架构还可以使设计近乎实时的系统变得更加容易，从而帮助组织摆脱基于批处理的处理。事件是在应用程序状态发生变化时生成的，因此微服务的自定义代码应设计为处理单一事件。由于扩展由 Lambda 服务处理，因此该架构无需更改自定义代码即可应对流量的显著增加。随着事件纵向扩展，处理事件的计算层也在扩展。

### 降低复杂性
<a name="complexity"></a>

微服务使开发人员和架构师能够简化复杂的工作流。例如，电子商务单体可以分解为订单接受和付款流程，并具有单独的库存、履行和会计服务。在单体中管理和编排可能很复杂的事件变成了一系列通过事件以异步方式通信的解耦服务。

![\[事件驱动型架构图 9\]](http://docs.aws.amazon.com/zh_cn/lambda/latest/dg/images/event-driven-architectures-figure-9.png)


此方法还可以组合以不同速率处理数据的服务。在这种情况下，订单接受微服务可以通过在 Amazon SQS 队列中缓冲消息来存储大量传入订单。

由于处理付款的复杂性，付款处理服务通常速度较慢，但可以从 Amazon SQS 队列中获取稳定的消息流。它可以使用 AWS Step Functions 编排复杂的重试和错误处理逻辑，并协调数十万个订单的有效付款工作流。

**替代方法：**对于使用标准编程语言的编排，您可以使用 [Lambda 持久性函数](durable-functions.md)。利用持久性函数，您可以通过带有自动检查点机制和重试功能的代码编写订单接受、付款处理和通知逻辑。当工作流主要涉及 Lambda 函数，并且您更喜欢在代码中保留编排逻辑时，这种方法效果很好。

### 提高可扩展性和可延长性
<a name="scalability-extensibility"></a>

微服务生成的事件通常会发布到 Amazon SNS 和 Amazon SQS 等消息收发服务。它们的行为就像微服务之间的弹性缓冲，有助于在流量增加时处理扩展。然后，Amazon EventBridge 等服务可以根据规则中定义的事件内容筛选和路由消息。因此，基于事件的应用程序比单体应用程序更具可扩展性，并提供更大的冗余。

此系统还具有高度可扩展性，允许其他团队扩展功能并添加功能，而不会影响订单处理和付款处理微服务。通过使用 EventBridge 发布事件，此应用程序可与库存微服务等现有系统集成，但也允许任何未来的应用程序作为事件使用器集成。事件的生成者对事件使用器一无所知，这有助于简化微服务逻辑。

## 事件驱动型架构的利弊权衡
<a name="event-driven-tradeoffs"></a>

### 可变延迟
<a name="variable-latency"></a>

与可以在单一设备上的同一内存空间内处理所有内容的单体应用程序不同，事件驱动型应用程序跨网络进行通信。这种设计引入了可变延迟。虽然可以设计应用程序来最大限度地减少延迟，但几乎总是能以牺牲可扩展性和可用性为代价来优化单体应用程序以降低延迟。

需要一致的低延迟性能的工作负载（例如银行中的高频交易应用程序或仓库中的亚毫秒机器人自动化）不适合事件驱动型架构。

### 最终一致性
<a name="eventual-consistency"></a>

事件代表状态的变化，并且由于许多事件在任何给定时间点流经架构中的不同服务，因此此类工作负载通常[最终是一致的](https://en.wikipedia.org/wiki/Eventual_consistency)。这让处理事务、处理重复项或确定系统的确切总体状态变得更加复杂。

一些工作负载包含最终一致（例如，当前小时的订单总数）或高度一致（例如当前库存）的要求的组合。对于需要强大数据一致性的工作负载，有一些架构模式可以支持这一点。例如：
+ DynamoDB 可以提供[强一致性读取](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadConsistency.html)，但有时会产生更高的延迟，并且比默认模式使用更大的吞吐量。DynamoDB 还可以[支持事务](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/transactions.html)以帮助保持数据一致性。
+ 您可以将 Amazon RDS 用于需要 [ACID 属性](https://en.wikipedia.org/wiki/ACID)的功能，但关系数据库的可扩展性通常不如 DynamoDB 等 NoSQL 数据库。[Amazon RDS 代理](https://aws.amazon.com/rds/proxy/)有助于管理来自 Lambda 函数等临时使用器的连接池和扩展。

基于事件的架构通常是围绕单个事件而不是大量数据设计的。通常，工作流旨在管理单个事件或执行流的步骤，而不是同时对多个事件进行操作。在无服务器中，实时事件处理优于批处理：应使用许多较小的增量更新取代批处理。虽然这可以提高工作负载的可用性和可扩展性，但也使得事件对其他事件的感知变得更具挑战性。

### 向调用方返回值
<a name="values-callers"></a>

在许多情况下，基于事件的应用程序是异步的。这意味着调用方服务不会等待其他服务的请求后再继续其他工作。这是事件驱动型架构的基本特征，可实现可扩展性和灵活性。这意味着传递返回值或工作流结果通常比在同步执行流中传递更为复杂。

生产系统中的大多数 Lambda 调用都是[异步](invocation-async.md)的，用于响应来自 Amazon S3 或 Amazon SQS 等服务的事件。在这些情况下，处理事件的成败通常比返回值更重要。Lambda 中提供了[死信队列](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async.html)（DLQ）等功能，可确保您无需通知调用方，即可识别并重试失败事件。

### 跨服务和函数调试
<a name="services-functions"></a>

调试事件驱动型系统也不同于单体应用程序。不同的系统和服务传递事件时，不可能在发生错误时记录和重现多个服务的确切状态。由于每个服务和函数调用都有单独的日志文件，因此确定导致错误的特定事件发生的情况可能会更加复杂。

在事件驱动型系统中构建成功的调试方法有三个重要要求。首先，强大的日志记录系统至关重要，Amazon CloudWatch 跨 AWS 服务提供并嵌入在 Lambda 函数中。其次，在这些系统中，务必确保每个事件都有一个事务标识符，该标识符在整个事务的每个步骤中都记录下来，以帮助搜索日志。

最后，强烈建议使用调试和监控服务（如 AWS X-Ray）来自动解析和分析日志。这可以使用跨多个 Lambda 调用和服务的日志，从而更容易查明问题的根本原因。有关使用 X-Ray 进行问题排查的深入介绍，请参阅[问题排查演练](lambda-troubleshooting.md)。

## 基于 Lambda 的事件驱动型应用程序中的反模式
<a name="event-driven-anti-patterns"></a>

使用 Lambda 构建事件驱动架构时，请避免以下常见反模式。这些模式有效，但会增加成本和复杂性。

### Lambda 单体
<a name="monolith"></a>

在许多从传统服务器，例如 Amazon EC2 实例或 Elastic Beanstalk 应用程序迁移的应用程序中，开发人员“直接迁移”现有代码。通常，这会生成单一 Lambda 函数，其中包含针对所有事件触发的所有应用程序逻辑。对于基本的 Web 应用程序，单体 Lambda 函数将处理所有 API Gateway 路由，并与所有必要的下游资源集成。

![\[事件驱动型架构图 13\]](http://docs.aws.amazon.com/zh_cn/lambda/latest/dg/images/event-driven-architectures-figure-13.png)


此方法有几个缺点：
+  **程序包大小**：Lambda 函数可能要大得多，因为其中包含所有路径的所有可能代码，这样会使 Lambda 服务的运行速度变慢。
+  **难以执行最低权限** – 该函数的[执行角色](lambda-intro-execution-role.md)必须允许所有路径所需的所有资源的权限，从而使权限非常广泛。这是一个安全问题。功能单体中的许多路径不需要已授予的所有权限。
+  **更难升级** – 在生产系统中，对单一函数的任何升级都更具风险，并可能中断整个应用程序。升级 Lambda 函数中的单一路径就是对整个函数的升级。
+  **更难维护** – 由于该服务是一个单体代码存储库，因此让多个开发人员开发该服务更加困难。它还增加了开发人员的认知负担，使得为代码创建适当的测试覆盖率变得更加困难。
+  **更难重复使用代码** – 将可重复使用的库与单体分开会更难，这使得重复使用代码变得更加困难。随着您开发和支持更多项目，这会使支持代码和扩展团队速度变得更难。
+  **更难测试** – 随着代码行的增加，在代码库中对所有可能的输入和入口点组合进行单元测试变得越来越困难。通常，使用较少的代码对较小的服务实施单元测试会更容易。

首选替代方案是将单体 Lambda 函数分解为各个微服务，将单一 Lambda 函数映射到单一定义明确的任务。在这个具有几个 API 端点的简单 Web 应用程序中，生成的基于微服务的架构可以基于 API Gateway 路由。

![\[事件驱动型架构图 14\]](http://docs.aws.amazon.com/zh_cn/lambda/latest/dg/images/event-driven-architectures-figure-14.png)


### 导致 Lambda 函数失控的递归模式
<a name="recursive-runaway"></a>

AWS 服务生成调用 Lambda 函数的事件，而 Lambda 函数可以向 AWS 服务发送消息。通常，调用 Lambda 函数的服务或资源应该与该函数输出到的服务或资源不同。未能对此进行管理可能会导致无限循环。

例如，Lambda 函数向 Amazon S3 对象写入一个对象，该对象又通过放置事件调用同一 Lambda 函数。该调用会导致将第二个对象写入存储桶，从而调用同一 Lambda 函数：

![\[事件驱动型架构图 15\]](http://docs.aws.amazon.com/zh_cn/lambda/latest/dg/images/event-driven-architectures-figure-15.png)


虽然大多数编程语言都存在无限循环的可能性，但此反模式有可能在无服务器应用程序中占用更多资源。Lambda 和 Amazon S3 都会根据流量自动扩展，因此循环可能会导致 Lambda 扩展来占用所有可用的并发，而 Amazon S3 将继续为 Lambda 写入对象并生成更多事件。

此示例使用 S3，但是 Amazon SNS、Amazon SQS、DynamoDB 及其他服务中也存在递归循环的风险。您可以使用[递归循环检测](invocation-recursion.md)来查找和避免这种反模式。

### 调用 Lambda 函数的 Lambda 函数
<a name="functions-calling-functions"></a>

函数支持封装和代码重复使用。大多数编程语言都支持代码在代码库内同步调用函数的概念。在这种情况下，调用方会等待，直到函数返回响应。

**注意**  
尽管出于成本和复杂性考虑，直接调用其他 Lambda 函数的 Lambda 函数通常是一种反模式，但这不适用于[持久性函数](durable-functions.md)，其专门用于通过调用其他函数来编排多步骤工作流。

当这种情况发生在传统服务器或虚拟实例上时，操作系统调度器会切换到其他可用工作。无论 CPU 以 0% 还是 100% 的速度运行，都不会影响应用程序的总体成本，因为您需要支付拥有和运营服务器的固定成本。

这种模型通常不能很好地适应无服务器开发。例如，考虑一个由三个处理订单的 Lambda 函数组成的简单电子商务应用程序：

![\[事件驱动型架构图 16\]](http://docs.aws.amazon.com/zh_cn/lambda/latest/dg/images/event-driven-architectures-figure-16.png)


在这种情况下，“创建订单”**函数会调用“处理付款”**函数，后者又调用“创建发票”**函数。虽然此同步流可以在服务器上的单一应用程序内运行，但它在分布式无服务器架构中引入了几个可以避免的问题：
+  **成本** – 使用 Lambda，您需要为调用的持续时间付费。在此示例中，当*创建发票*函数运行时，另外两个函数也在等待状态下运行，如图中的红色所示。
+  **错误处理** – 在嵌套调用中，错误处理可能会变得复杂得多。例如，*创建发票*中的错误可能需要*处理付款*函数来撤销费用，或者可能会重试*创建发票*流程。
+  **紧密耦合** – 处理付款通常比创建发票需要更长时间。在此模型中，整个工作流的可用性受最慢函数限制。
+  **扩展** – 所有三个函数的[并发](lambda-concurrency.md)必须相等。在繁忙的系统中，这会使用比原本需要的更多的并发。

在无服务器应用程序中，有两种常见的方法可以避免此模式。首先，在 Lambda 函数之间使用 Amazon SQS 队列。如果下游进程比上游进程更慢，则队列会持久保留消息并将这两个函数解耦。在此示例中，*创建订单*函数会将消息发布到 Amazon SQS 队列，*处理付款*函数则使用队列中的消息。

第二种方法是使用 AWS Step Functions。对于具有多种失败和重试逻辑的复杂流程，Step Functions 可以有助于减少编排工作流所需的自定义代码量。因此，Step Functions 会编排工作并稳健地处理错误和重试，而 Lambda 函数仅包含业务逻辑。

### 在单一 Lambda 函数内同步等待
<a name="synchronous-waiting"></a>

在单一 Lambda 函数内，确保任何可能的并发活动都不是同步计划的。例如，Lambda 函数可能会写入 S3 存储桶，之后写入 DynamoDB 表：

![\[事件驱动型架构图 17\]](http://docs.aws.amazon.com/zh_cn/lambda/latest/dg/images/event-driven-architectures-figure-17.png)


在这种设计中，由于活动是连续的，等待时间会变得复杂。如果第二个任务取决于第一个任务的完成情况，则您可以通过使用两个单独的 Lambda 函数来减少总等待时间和执行成本：

![\[事件驱动型架构图 19\]](http://docs.aws.amazon.com/zh_cn/lambda/latest/dg/images/event-driven-architectures-figure-19.png)


在这种设计中，第一个 Lambda 函数在将对象放入 Amazon S3 存储桶后立即响应。S3 服务调用第二个 Lambda 函数，之后该函数将数据写入 DynamoDB 表。此方法最大限度地减少了 Lambda 函数执行中的总等待时间。

# 设计 Lambda 应用程序
<a name="concepts-application-design"></a>

架构完善的事件驱动型应用程序使用 AWS 服务和自定义代码的组合来处理和管理请求与数据。本章重点介绍应用程序设计中特定于 Lambda 的主题。在为繁忙的生产系统设计应用程序时，无服务器架构师有许多需要考虑的重要注意事项。

许多适用于软件开发和分布式系统的最佳实践也适用于无服务器应用程序开发。总体目标是开发满足以下条件的工作负载：
+  **可靠** – 为您的最终用户提供高水平的可用性。AWS 无服务器服务之所以可靠，是因为它们也是为应对故障而设计的。
+  **耐用** – 提供可满足工作负载耐用性需求的存储选项。
+  **安全** – 遵循最佳实践并使用提供的工具来保护对工作负载的访问并限制影响范围。
+  **性能** – 高效使用计算资源，并满足最终用户的性能需求。
+  **成本效益** – 设计的架构可避免不必要的成本，既可以在不超支的情况下进行扩展，也可以在必要时停用，而不会产生大量开销。

以下设计原则有助于构建满足这些目标的工作负载。并非所有原则都适用于每种架构，但它们应能够指导您做出一般的架构决策。

**Topics**
+ [使用服务而不是自定义代码](#services-custom-code)
+ [了解 Lambda 抽象级别](#level-abstraction)
+ [在函数中实施无状态性](#statelessness-functions)
+ [最小化耦合](#minimize-coupling)
+ [为按需数据而非批量数据构建](#on-demand-batches)
+ [为复杂的工作流选择编排选项](#orchestration)
+ [实施幂等性](#retries-failures)
+ [使用多个 AWS 账户管理配额](#multiple-accounts)

## 使用服务而不是自定义代码
<a name="services-custom-code"></a>

无服务器应用程序通常包含多项 AWS 服务，这些服务与在 Lambda 函数中运行的自定义代码集成。虽然 Lambda 可以与大多数 AWS 服务集成，但无服务器应用程序中最常用的服务有：


| 类别 | AWS 服务 | 
| --- | --- | 
|  计算  |  AWS Lambda  | 
|  数据存储  |  Amazon S3 Amazon DynamoDB Amazon RDS  | 
|  API  |  Amazon API Gateway  | 
|  应用程序集成  |  Amazon EventBridge Amazon SNS Amazon SQS  | 
|  编排  |  Lambda 持久性函数 AWS Step Functions  | 
|  流式传输数据和分析  |  Amazon Data Firehose  | 

**注意**  
许多无服务器服务都为多个区域提供复制和支持，包括 DynamoDB 和 Amazon S3。作为部署管线的一部分，Lambda 函数可以部署在多个区域，并且 API Gateway 可以配置为支持此配置。请参阅此[示例架构](https://d1.awsstatic.com/architecture-diagrams/ArchitectureDiagrams/serverless-architecture-for-global-applications-ra.pdf?did=wp_card&trk=wp_card)，其展示了如何实现这一点。

分布式架构中有许多成熟的常见模式，您可以自行构建或使用 AWS 服务来实施。对于大多数客户来说，投入时间从零开始开发这些模式几乎没有商业价值。当您的应用程序需要以下其中一个模式时，请使用相应的 AWS 服务：


| 模式 | AWS 服务 | 
| --- | --- | 
|  队列  |  Amazon SQS  | 
|  事件总线  |  Amazon EventBridge  | 
|  发布/订阅（扇出）  |  Amazon SNS  | 
|  编排  |  Lambda 持久性函数 AWS Step Functions  | 
|  API  |  Amazon API Gateway  | 
|  事件流  |  Amazon Kinesis  | 

这些服务旨在与 Lambda 集成，您可以使用基础设施即代码（IaC）来创建和丢弃服务中的资源。您可以通过 [AWS SDK](https://aws.amazon.com/tools/) 使用其中任何服务，而无需安装应用程序或配置服务器。熟练通过 Lambda 函数中的代码使用这些服务，是生成精心设计的无服务器应用程序的重要步骤。

## 了解 Lambda 抽象级别
<a name="level-abstraction"></a>

Lambda 服务限制您对运行 Lambda 函数的底层操作系统、虚拟机监控程序和硬件的访问。该服务不断改进和更改基础设施，以增加功能、降低成本并提高服务的性能。您的代码应假设不了解 Lambda 的架构方式，且假设没有硬件关联性。

同样，Lambda 与其他服务的集成由 AWS 管理，且仅向您公开少量配置选项。例如，API Gateway 和 Lambda 交互时，没有负载均衡概念，因为负载均衡完全由服务管理。您也无法直接控制服务在任何时间点调用函数时使用哪些[可用区](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/)，也无法直接控制 Lambda 如何确定纵向扩展或缩减执行环境数量的时间。

此抽象可帮助您专注于应用程序的集成方面、数据流，以及工作负载为最终用户提供价值的业务逻辑。允许服务管理底层机制可以帮助您更快地开发应用程序，并减少需要维护的自定义代码。

## 在函数中实施无状态性
<a name="statelessness-functions"></a>

对于标准 Lambda 函数时，您应假设环境仅存在于单次调用中。函数应在首次启动时初始化所有必需的状态。例如，您的函数可能需要从 DynamoDB 表提取数据。在退出之前，它应将任何永久性数据更改提交到持久存储（例如 Amazon S3、DynamoDB 或 Amazon SQS）。它不应依赖任何现有的数据结构或临时文件，也不应依赖将由多次调用管理的任何内部状态。

使用持久性函数时，会在两次调用之间自动保留状态，从而无需手动将状态保存到外部存储。但是，对于任何未通过 DurableContext 明确管理的数据，您仍应遵循无状态原则。

要初始化数据库连接和库或加载状态，可以利用[静态初始化](lambda-runtime-environment.md#static-initialization)。由于尽可能重复使用执行环境以提高性能，因此您可以通过多次调用来分摊初始化这些资源所花的时间。但是，不应该在此全局范围内存储函数中使用的任何变量或数据。

## 最小化耦合
<a name="minimize-coupling"></a>

大多数架构应首选大量且较短的函数，而不是少量且较大的函数。每个函数的目的应该是处理传递到函数中的事件，而无需了解或预期整体工作流或事务量。这使得该函数与事件源无关，与其他服务的耦合最少。

任何不经常更改的全局范围常量都应作为环境变量来实施，以便无需部署即可进行更新。任何机密或敏感信息都应存储在 [AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html) 或 [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) 中，并由函数加载。由于这些资源是特定于账户的，因此您可以跨多个账户创建构建管道。管线会为每个环境加载适当的密钥，而无需向开发人员公开这些密钥，也不需要对代码进行任何更改。

## 为按需数据而非批量数据构建
<a name="on-demand-batches"></a>

许多传统系统旨在定期运行并处理随着时间而积累的批量事务。例如，银行应用程序可能每小时运行一次，将 ATM 交易处理到中央分类账。在基于 Lambda 的应用程序中，自定义处理应由每个事件触发，从而允许服务根据需要纵向扩展并发，以提供近乎实时的事务处理。

虽然标准 Lambda 函数的执行时间限制在 15 分钟以内，但持久性函数的运行时间最长可达一年，因此非常适合长时间运行的处理需求。然而，在可能的情况下，您仍应优先选择事件驱动型处理而非批量处理。

虽然您可以在 Amazon EventBridge 中[使用规则的计划表达式](https://docs.aws.amazon.com/eventbridge/latest/userguide/scheduled-events.html)在无服务器应用程序中运行 [cron](https://en.wikipedia.org/wiki/Cron) 任务，但应谨慎使用这些任务或作为最后手段。在处理批次的任何计划任务中，事务量都有可能超出在 15 分钟的 Lambda 持续时间限制内可以处理的范围。如果外部系统的限制迫使您使用调度器，则通常应计划最短的合理重复时间段。

例如，使用触发 Lambda 函数的批量处理来获取新 Amazon S3 对象列表并不是最佳实践。这是因为该服务在批处理之间接收的新对象可能比在 15 分钟的 Lambda 函数内处理的要多。

![\[事件驱动型架构图 10\]](http://docs.aws.amazon.com/zh_cn/lambda/latest/dg/images/event-driven-architectures-figure-10.png)


相反，每次将新对象放入存储桶时，Amazon S3 都应该调用 Lambda 函数。此方法的可扩展性要高得多，且可以近乎实时地工作。

![\[事件驱动型架构图 11\]](http://docs.aws.amazon.com/zh_cn/lambda/latest/dg/images/event-driven-architectures-figure-11.png)


## 为复杂的工作流选择编排选项
<a name="orchestration"></a>

涉及分支逻辑、不同类型的故障模型和重试逻辑的工作流通常使用编排工具来跟踪整体执行的状态。请勿在标准 Lambda 函数中构建临时编排。这会导致紧密耦合、复杂的路由代码以及无自动状态恢复。

请改用下面其中一个专门构建的编排选项：
+ **[Lambda 持久性函数](durable-functions.md)：**使用标准编程语言进行以应用程序为中心的编排，具有自动检查点机制、内置重试和执行恢复功能。非常适合那些喜欢将工作流逻辑与 Lambda 中的业务逻辑一起保留在代码中的开发人员。
+ **[AWS Step Functions](with-step-functions.md)：**可视化工作流编排，与 220 多种 AWS 服务本机集成。非常适合多服务协调、零维护基础设施和可视化工作流设计。

有关在这些选项之间进行选择的指导，请参阅[持久性函数或 Step Functions](durable-step-functions.md)。

使用 [ Step Functions](https://aws.amazon.com/step-functions/)，您可以使用状态机来管理编排。这将从代码中提取错误处理、路由和分支逻辑，将其替换为使用 JSON 声明的状态机。除了使工作流更强大、更易于观测外，您还可以向工作流添加版本控制，并使状态机成为可以添加到代码存储库中的编码资源。

随着时间的推移，Lambda 函数中较简单的工作流程通常会变得越来越复杂。在操作生产无服务器应用程序时，识别何时发生这种情况非常重要，这样您就可以将此逻辑迁移到状态机或持久性函数。

## 实施幂等性
<a name="retries-failures"></a>

包括 Lambda 在内的 AWS 无服务器服务具有容错能力，并且专为处理故障而设计。例如，如果某项服务调用 Lambda 函数并且出现服务中断，则 Lambda 会在不同的可用区中调用您的函数。如果您的函数引发错误，则 Lambda 将重试调用。

由于同一事件可能会被多次接收，因此函数应该设计为[幂等性](https://en.wikipedia.org/wiki/Idempotence)。这意味着，多次接收同一事件不会改变第一次接收事件之后的结果。

您可以使用 DynamoDB 表跟踪最近处理的标识符，从而在 Lambda 函数中实施幂等性，以确定之前是否处理过事务。DynamoDB 表通常会实施[生存时间（TTL）](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html)值来使项目过期，以限制使用的存储空间。

## 使用多个 AWS 账户管理配额
<a name="multiple-accounts"></a>

AWS 中的许多[服务配额](gettingstarted-limits.md)都是在账户级别设置。这意味着，添加更多的工作负载时，您的限额可迅速耗尽。

解决此问题的有效方法是使用多个 AWS 账户，将每个工作负载专用于自身账户。这样可以防止与其他工作负载或非生产资源一起共享配额。

 此外，使用 [AWS Organizations](https://aws.amazon.com/organizations/)，您可以集中管理这些账户的账单、合规性和安全性。您可以为账户组附加策略，以避免使用自定义脚本和手动流程。

一种常见的方法是为每位开发人员提供一个 AWS 账户，然后在测试版部署阶段和生产阶段使用不同的账户：

![\[应用程序设计图 3\]](http://docs.aws.amazon.com/zh_cn/lambda/latest/dg/images/application-design-figure-3.png)


在此模型中，每个开发人员都有自己的一组账户限制，因此他们的使用不会影响您的生产环境。这种方法还允许开发人员在开发机器上根据个人账户中的实时云资源在本地测试 Lambda 函数。