

# 使用 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 函数执行中的总等待时间。