排查 Lambda 中的配置问题 - Amazon Lambda
Amazon Web Services 文档中描述的 Amazon Web Services 服务或功能可能因区域而异。要查看适用于中国区域的差异,请参阅 中国的 Amazon Web Services 服务入门 (PDF)

排查 Lambda 中的配置问题

您的函数配置设置可能会影响 Lambda 函数的整体性能和行为。这些可能不会导致实际的函数错误,但可能会导致意外的超时和结果。

以下主题为您可能遇到的与 Lambda 函数配置设置相关的常见问题提供问题排查建议。

内存配置

您可以将 Lambda 函数配置为使用 128 MB 与 10240 MB 之间的内存。默认情况下,为在控制台中创建的任何函数都分配了最小量的内存。在此最低设置下,许多 Lambda 函数的性能都很高。但是,如果您要导入大型代码库或完成内存密集型任务,128 MB 是不够的。

如果函数的运行速度比预期慢得多,则第一步是增加内存设置。对于内存受限函数,这将解决瓶颈问题,且可以提高函数性能。

CPU 受限配置

对于计算密集型操作,如果您的函数性能低于预期,则这可能是由于您的函数受 CPU 限制。在这种情况下,函数的计算能力无法跟上工作进度。

虽然 Lambda 不允许您直接修改 CPU 配置,但 CPU 是通过内存设置间接控制。当您分配更多内存时,Lambda 服务会按比例分配更多虚拟 CPU。内存为 1.8 GB 时,Lambda 函数会分配整个 vCPU,而在此级别以上,它可以访问多个 vCPU 核心。在 10240 MB 时,有 6 个可用的 vCPU。也就是说,即使函数没有使用所有内存,您也可以通过增加内存分配来提高性能。

超时

Lambda 函数的超时可以设置为 1 到 900 秒(15 分钟)。默认情况下,Lambda 控制台会将该值设置为 3 秒。超时值是一个安全阀,可确保函数不会无限期运行。达到超时值后,Lambda 会停止函数调用。

如果将超时值设置得接近函数的平均持续时间,则会增加函数意外超时的风险。函数的持续时间可能因数据传输和处理量以及与该函数交互的任何服务的延迟而不同。导致超时的常见原因包括:

  • 从 S3 存储桶或其他数据存储下载数据时,下载量会比平均值更大或花费更长时间。

  • 一个函数向另一项服务发出请求,这需要更长的时间才能响应。

  • 提供给函数的参数要求函数具有更高的计算复杂度,这会导致调用花费更长的时间。

在测试应用程序时,请确保您的测试准确反映数据的大小和数量以及真实的参数值。重要的是,可在您的工作负载合理预期值的上限使用数据集。

此外,在可行的情况下,可对工作负载实施上限限制。在本示例中,应用程序可以对每种文件类型使用最大大小限制。然后,您可以针对一系列的预期文件大小(达到并包括最大限制)来测试应用程序的性能。

两次调用之间的内存泄漏

Lambda 调用的 INIT 阶段中存储的全局变量和对象在暖调用之间保留其状态。只有在执行环境首次运行时(也称为“冷启动”),它们才会完全重置。处理程序退出时,存储在处理程序中的任何变量都会销毁。最佳实践是使用 INIT 阶段来设置数据库连接、加载库、创建缓存和加载不可变资产。

在同一执行环境中跨多个调用使用第三方库时,请查看其文档以了解在无服务器计算环境中的使用情况。某些数据库连接和日志记录库可能会保存中间调用结果和其他数据。这会导致这些库的内存使用量随着随后的暖调用而增长。在此情况下,即使您的自定义代码正确处理了变量,您也可能会发现 Lambda 函数的内存不足。

此问题会影响在暖执行环境中发生的调用。例如,以下代码会在两次调用之间产生内存泄漏。Lambda 函数通过增加全局数组的大小,每次调用都会占用额外的内存:

let a = []

exports.handler = async (event) => {
    a.push(Array(100000).fill(1))
}

配置了 128 MB 内存,在调用此函数 1000 次后,Lambda 函数的监控选项卡会显示在发生内存泄漏时调用、持续时间和错误计数的典型变化:

调试操作图 4
  1. 调用 – 由于调用需要更长时间才能完成,因此会定期中断稳定的事务速率。在稳定状态期间,内存泄漏不会使用函数分配的所有内存。随着性能下降,操作系统会对本地存储进行分页以适应函数所需的不断增长的内存,从而减少要完成的事务量。

  2. 持续时间 – 在函数的内存不足之前,它会以稳定的两位数毫秒速率完成调用。随着分页的发生,持续时间会延长一个数量级。

  3. 错误计数 – 由于内存泄漏超出分配的内存,最终函数因计算超出超时值而出错,或者执行环境停止该函数。

错误发生后,Lambda 会重新启动执行环境,这解释了为什么所有三个图表都显示恢复到原始状态。扩展 CloudWatch 持续时间指标可提供最短、最大和平均持续时间统计数据的更多详细信息:

调试操作图 5

要查找在 1000 次调用中所生成的错误,您可以使用 CloudWatch Insights 查询语言。以下查询排除信息日志,以仅报告错误:

fields @timestamp, @message
| sort @timestamp desc
| filter @message not like 'EXTENSION'
| filter @message not like 'Lambda Insights'
| filter @message not like 'INFO' 
| filter @message not like 'REPORT'
| filter @message not like 'END'
| filter @message not like 'START'

针对此函数的日志组运行时,这表明超时是造成周期性错误的原因:

调试操作图 6

返回给以后调用的异步结果

对于使用异步模式的函数代码,一次调用的回调结果可能会在未来调用中返回。此示例使用 Node.js,但相同逻辑可以应用于使用异步模式的其他运行时。该函数使用 JavaScript 中传统的回调语法。它调用一个带有增量计数器的异步函数,用于跟踪调用次数:

let seqId = 0 exports.handler = async (event, context) => { console.log(`Starting: sequence Id=${++seqId}`) doWork(seqId, function(id) { console.log(`Work done: sequence Id=${id}`) }) } function doWork(id, callback) { setTimeout(() => callback(id), 3000) }

连续多次调用时,回调的结果会出现在后续调用中:

调试操作图 7
  1. 代码调用 doWork 函数,提供回调函数作为最后一个参数。

  2. 在调用回调之前,doWork 函数需要一段时间来完成。

  3. 该函数的日志记录表明调用将在 doWork 函数完成执行之前结束。此外,在开始迭代之后,将处理先前迭代的回调,如日志所示。

在 JavaScript 中,异步回调通过事件循环处理。其他运行时使用不同的机制来处理并发。函数的执行环境结束时,Lambda 会冻结环境,直到下次调用。恢复后,JavaScript 会继续处理事件循环,在这种情况下包括来自先前调用的异步回调。如果没有此上下文,则该函数可能会无缘无故地运行代码,并返回任意数据。事实上,它实际上是运行时并发和执行环境如何交互的构件。

这使得前一次调用的私有数据有可能出现在后续调用中。有两种方法可防止或检测此行为。首先,JavaScript 提供了 async 和 await 关键字来简化异步开发,还强制代码执行等待异步调用完成。可以使用此方法重写上面的函数,如下所示:

let seqId = 0 exports.handler = async (event) => { console.log(`Starting: sequence Id=${++seqId}`) const result = await doWork(seqId) console.log(`Work done: sequence Id=${result}`) } function doWork(id) { return new Promise(resolve => { setTimeout(() => resolve(id), 4000) }) }

使用此语法可防止处理程序在异步函数完成之前退出。在这种情况下,如果回调的时间超过 Lambda 函数的超时时间,则该函数将引发错误,而不是在以后的调用中返回回调结果:

调试操作图 8
  1. 该代码在处理程序中使用 await 关键字调用异步 doWork 函数。

  2. 在解析 promise 之前,doWork 函数需要一段时间才能完成。

  3. 该函数超时,因为 doWork 花费的时间超过了超时限制允许的时间,并且在以后的调用中不会返回回调结果。

通常,您应确保代码中的任何后台进程或回调在代码退出前已完成。如果在您的使用案例中无法做到这一点,您可以使用标识符来确保回调属于当前调用。为此,您可以使用上下文对象所提供的 awsRequestId。通过将此值传递给异步回调,您可以将传递的值与当前值进行比较,以检测回调是否来自另一个调用:

let currentContext exports.handler = async (event, context) => { console.log(`Starting: request id=$\{context.awsRequestId}`) currentContext = context doWork(context.awsRequestId, function(id) { if (id != currentContext.awsRequestId) { console.info(`This callback is from another invocation.`) } }) } function doWork(id, callback) { setTimeout(() => callback(id), 3000) }
调试操作图 9
  1. Lambda 函数处理程序取用上下文参数,该参数提供对唯一调用请求 ID 的访问权限。

  2. awsRequestId 传递给 doWork 函数。在回调中,将 ID 与当前调用的 awsRequestId 进行比较。如果这些值不同,则代码可以相应地采取行动。