开发可移植的 亚马逊云科技 Lambda 函数

作者: 帕斯卡尔·沃格尔 | 202

这篇博客文章由首席无服务器专家解决方案架构师 Uri Segev 撰写

在开发新应用程序或对现有应用程序进行现代化改造时,您可能会面临一个难题:使用哪种计算技术?像 亚马逊云科技 Lambda 这样的无服务器计算服务 或者容器?通常,无服务器可能是更好的方法,这要归功于自动扩展、内置的高可用性和按使用量付费的计费模式。但是,出于以下原因,您可能会犹豫选择无服务器:

  • 认为成本较高或难以估算成本
  • 这是一种范式转变,需要学习弥合知识鸿沟
  • 对 Lambda 功能和用例的误解
  • 担心使用 Lambda 会导致锁定
  • 对非无服务器平台和工具的现有投资

这篇博文建议了开发便携式 Lambda 函数的最佳实践,如果您以后选择这样做,可以轻松地将代码移植到容器中。通过这样做,你可以避免锁定,并以无风险的方式尝试无服务器方法。

这篇博文的每个部分都描述了在编写可移植代码时需要考虑的事项,以及将此代码从 Lambda 迁移到容器所需的步骤(如果您以后选择这样做)。

便携式 Lambda 函数的最佳实践

将业务逻辑和 Lambda 处理程序分开

Lambda 函数本质上是事件驱动的。 当特定事件发生时,它通过调用其处理方法来调用 Lambda 函数。 处理器方法接收一个 事件 对象,该对象包含有关函数调用原因的信息。函数执行完成后,它将从处理程序方法返回。无论从处理程序返回什么,都是函数的返回值。

要编写可移植代码,我们建议仅使用处理程序方法作为 Lambda 运行时(事件对象)和业务逻辑之间的接口。使用Hexagonal架构术语,处理程序应该是调用端口(即业务逻辑暴露的接口)的驱动适配器。处理程序应从事件对象中提取所有必需的信息,然后调用实现业务逻辑的单独方法。

当该方法返回时,处理程序会以函数调用者期望的格式构造结果并将其返回。我们还建议将处理程序代码和业务逻辑代码拆分成单独的文件。如果您选择稍后迁移到容器,则只需迁移业务逻辑代码文件而无需进行其他更改。

以下伪代码显示了一个 Lambda 处理程序,该处理程序从事件对象中提取信息并调用业务逻辑。业务逻辑完成后,处理程序将响应放在函数的返回值中:

import business_logic

# The Lambda handler extracts needed information from the event
# object and invokes the business logic
handler(event, context) {
  # Extract needed information from event object payload = event[‘payload’]

  # Invoke business logic
  result = do_some_logic(payload)
  
  # Construct result for API Gateway
  return {
    statusCode: 200,
	body: result
  }
}

以下伪代码显示了业务逻辑。它位于单独的文件中,不知道它是从 Lambda 函数中调用的。这是纯粹的逻辑。

# This is the business logic. It knows nothing about who invokes it.
do_some_logic(data) {
result = "This is my result."
  return result
}

这种方法还可以更轻松地在业务逻辑上运行单元测试,而无需构造事件对象和调用 Lambda 处理程序。

如果您稍后迁移到容器,则可以使用下一节所述的新接口代码将业务逻辑文件包含在容器中。

事件源集成

Lambda 函数的一个好处是事件源集成。例如,如果您将 Lambda 与 亚马逊简单队列服务 (Amazon SQS) 集成 ,Lambda 服务将负责轮询队列、调用 Lambda 函数并在完成后从队列中删除消息。通过使用这种集成,你需要编写更少的样板代码。您只能专注于实现业务逻辑,而不能专注于与事件源的集成。

以下伪代码显示了 SQS 事件源的 Lambda 处理程序是什么样子:

import business_logic

handler(event, context) {
  entries = []
  # Iterate over all the messages in the event object
  for message in event[‘Records’] {
    # Call the business logic to process a single message
    success = handle_message(message)

    # Start building the response
    if Not success {
      entries.append({
      'itemIdentifier': message['messageId']
      })
    }
  }

  # Notify Lambda about failed items.
  if (let(entries) > 0) {
    return {
      'batchItemFailures': entries
    }
  }
}

正如您在前面的代码中看到的那样,Lambda 函数几乎不知道它是从 SQS 调用的。没有 SQS API 调用。它只知道事件对象的结构,该结构特定于 SQS。

迁移到容器时,集成责任从 Lambda 服务转移到开发者身上。亚马逊云科技 中有不同的事件源,每个事件源都需要不同的方法来使用事件和调用业务逻辑。例如,如果事件源是 Amazon API Gateway ,则您的应用程序将需要创建一个 HTTP 服务器,该服务器监听 HTTP 端口并等待传入请求以调用业务逻辑。

如果事件源是 Amazon Kinesis Dat a Streams,则您的应用程序将需要运行轮询器来读取分片中的记录、跟踪已处理的记录、处理流中分片数量发生变化的情况、重试错误等。无论事件源如何,如果您遵循之前的建议,则无需更改业务逻辑代码中的任何内容。

以下伪代码显示了容器中与 SQS 的集成将是什么样子。请注意,您将丢失一些功能,例如批处理、筛选,当然还有自动缩放。

import aws_sdk
import business_logic

QUEUE_URL = os.environ['QUEUE_URL']
BATCH_SIZE = os.environ.get('BATCH_SIZE', 1)
sqs_client = aws_sdk.client('sqs')

main() {
  # Infinite loop to poll for messages from SQS
  while True {

    # Receive a batch of messages from the queue
    response = sqs_client.receive_message(
      QueueUrl = QUEUE_URL,
      MaxNumberOfMessages = BATCH_SIZE,
      WaitTimeSeconds = 20 )

    # Loop over the messages in the batch
    entries = []
    i = 1
    for message in response.get('Messages',[]) {
      # Process a single message
      success = handle_message(message)

      # Append the message handle to an array that is later
      # used to delete processed messages
      if success {
        entries.append(
          {
            'Id': f'index{i}',
            'ReceiptHandle': message['receiptHandle']
          }
        )
        i += 1
      }
    }

    # Delete all the processed messages
    if (len(entries) > 0) {
      sqs_client.delete_message_batch(
        QueueUrl = QUEUE_URL,
        Entries = entries
      )
    }
  }
}

这里需要考虑的另一点是 Lambda目的地 。 如果您的函数是异步调用的,并且您为函数配置了目的地,则需要将其包含在接口代码中。它需要发现任何业务逻辑错误,并在此基础上调用正确的目的地。

将函数作为容器包装

Lambda 支持将函数打包为.zip 文件和容器镜像。要开发可移植代码,我们建议使用容器镜像作为默认打包方法。尽管您将函数打包为容器镜像,但您无法在其他容器平台上运行它,例如 亚马逊弹性容器服务 (Amazon ECS) 或 亚马逊 弹性 Kubernetes 服务 (EK S)。 但是,通过这种方式将其打包,以后迁移到容器会更容易,因为您已经在使用相同的工具,并且已经创建了一个需要最少更改的 Dockerfile。

Lambda 的 Dockerfile 示例如下所示:

FROM public.ecr.aws/lambda/python:3.9
COPY *.py requirements.txt ./
RUN python3.9 -m pip install -r requirements.txt -t .
CMD ["app.lambda_handler"]

如果您稍后转到容器,则需要更改 Dockerfile 以使用不同的基础映像,并调整定义如何启动应用程序的 CMD 行。这是对上一节中描述的代码更改的补充。

容器的相应的 Dockerfile 将如下所示:

FROM python:3.9
COPY *.py requirements.txt ./
RUN python3.9 -m pip install -r requirements.txt -t .
CMD ["python", "./app.py"]

当我们部署到不同的目标时,部署管道也需要更改。但是,建造文物保持不变。

每个实例单次调用

Lambda 函数在其自己的隔离运行时环境中运行。每个环境一次只能处理一个请求,这对于 Lambda 非常有用。但是,如果您将应用程序迁移到容器,则很可能会在单个进程中同时从多个线程调用业务逻辑。

本节讨论在同一进程中从单个调用转移到多个并发调用的各个方面。

静态变量

静态变量是指实例化一次然后在多次调用中重复使用的变量。此类变量的示例包括数据库连接或配置信息。

为了进行函数优化,特别是为了减少冷启动和热函数调用的持续时间,我们建议在函数处理程序之外初始化所有静态变量并将其存储在全局变量中,以便进一步的调用能够重用它们。

我们建议使用作为业务逻辑模块的一部分编写并从处理程序外部调用的初始化函数。此函数将信息保存在全局变量中,业务逻辑代码在调用中重复使用这些信息。

以下伪代码显示了 Lambda 函数:

import business_logic

# Call the initialization code
initialize()

handler(event, context) {
  ...
  # Call the business logic
  ...
}

业务逻辑代码将如下所示:

# Global variables used to store static data
var config

initialize() {
  config = read_Config()
}

do_some_logic(data) {
  # Do something with config object
  ...
}

这同样适用于容器。你通常会在进程启动时初始化静态变量,而不是为每个请求初始化。迁移到容器时,你所需要做的就是在启动主应用程序循环之前调用初始化函数。

import business_logic

# Call the initialization code
initialize()

main() {
  while True {
    ...
    # Call the business logic
    ...
  }
}

如您所见,业务逻辑代码没有变化。

数据库连接

由于 Lambda 函数在运行时环境之间没有任何共享,因此与容器不同,它们在连接到关系数据库时不能依赖连接池。出于这个原因,我们创建了 Amazon RDS 代理 ,它充当许多功能使用的集中连接池。

要编写便携式 Lambda 函数,我们建议使用具有单个连接的连接池对象。在发出数据库请求时,您的业务逻辑代码将始终要求池中的连接。您仍然需要使用 RDS 代理。

如果您以后迁移到容器,则可以在不做进一步更改的情况下将池中的连接数增加到更大的数量,并且应用程序将在不占用数据库负担的情况下进行扩展。

文件系统

Lambda 函数附带一个大小在 512 MB 到 10 GB 之间的可写的 /tmp 文件夹。由于每个函数实例都在隔离的运行时环境中运行,因此开发人员通常为存储在该文件夹中的文件使用固定文件名。如果您在容器中使用多个线程运行相同的业务逻辑代码,则不同的线程将覆盖其他线程创建的文件。

我们建议在每次调用中使用唯一的文件名。在文件名后附加 UUID 或其他随机数。处理完文件后将其删除,以免空间不足。

如果您稍后将代码移至容器,则无事可做。

便携式 Web 应用程序

如果你开发一个 Web 应用程序,还有另一种方法可以实现可移植性。您可以使用 亚马逊云科技 Lambda Web Adapter 项目在 Lambda 函数中托管网络应用程序。通过这种方式,你可以使用熟悉的框架(例如 Express.js、Next.js、Flask、Spring Boot、Laravel 或任何使用 HTTP 1.1/1.0 的框架)开发一个 Web 应用程序,然后在 Lambda 上运行它。如果您将 Web 应用程序打包为容器,则相同的 Docker 映像可以在 Lambda(使用 Web 适配器)和容器上运行。

从容器移植到 Lambda

这篇博文演示了如何开发可轻松移植到容器的便携式 Lambda 函数。总体而言,考虑这些建议还可以帮助开发可移植代码,从而允许您将容器移植到 Lambda 函数。

需要考虑的一些事项:

  • 将业务逻辑与容器中的接口代码分开。接口代码应与事件源交互并调用业务逻辑。
  • 由于 Lambda 函数只有 /tmp 可写文件夹,因此请将其复制到您的容器中(即使您可以写入不同的位置)。

结论

这篇博文建议了开发 Lambda 函数的最佳实践,让您在不冒锁定风险的情况下获得无服务器方法的好处。

通过遵循这些最佳实践,将业务逻辑与 Lambda 处理程序分开、将函数打包为容器、处理 Lambda 对每个实例的单次调用等,您可以开发可移植的 Lambda 函数。因此,如果您选择稍后迁移到容器,则可以毫不费力地将代码从 Lambda 移植到容器。

在开发下一个应用程序时,请参阅这些最佳实践和代码示例,以简化无服务器方法的采用。

如需更多无服务器学习资源,请访问 无服务器世界


*前述特定亚马逊云科技生成式人工智能相关的服务仅在亚马逊云科技海外区域可用,亚马逊云科技中国仅为帮助您发展海外业务和/或了解行业前沿技术选择推荐该服务。