亚马逊AWS官方博客

AWS Lambda 优雅停机的最佳实践

本文主要介绍 AWS Lambda 优雅停机的具体实现,优雅停机功能赋予了每个 AWS Lambda instance 主动释放资源的能力。简单来讲通过这个功能我们可以完成 pre stop 类资源释放工作,例如 MySQL 连接池中长链接的主动关闭、未完成的长事务(超过 15min)超时前的主动回滚等。

AWS Lambda Instance 的生命周期

在理解优雅停机具体实现前,我们需要了解单个 instance 的生命周期。一个 instance 会经历初始化(init)、多次 event 调用(invoke)、instance 资源闲置一段时间后的关闭(shutdown),共 3 个主要的阶段。

如下图:

常见的 Linux 上的普通服务进程或者 Kubernetes 容器服务的优雅停机钩子,一般是通过监听 SIGTERM 信号来实现。在类 UNIX 系统中 SIGTERM 信号用于终止程序,它是一种可捕获的软性的终止信号,运行在类 UNIX 系统中的进程可以选择忽略它或者捕获它,AWS Lambda 底层的 OS 是 Amazon Linux,所以实现的思路类似。我们可以在 init 阶段提前注册好 SIGTERM 信号处理钩子,当 instance 进入 shutdown 阶段时这个钩子捕获 SIGTERM 信号并触发执行优雅停机相关的代码。

实现 AWS Lambda 优雅停机的具体原理

通过前面的介绍,我们可以知道实现这个功能的核心前提是我们能够准确地在 AWS Lambda instance 即将被回收时,提前一小段时间捕获到具体的 SIGTERM 信号,只有 SIGTERM 被捕获后我们才能及时的完成后续的优雅停机操作。

为了捕获这个关键性的 SIGTERM 信号,我们需要参考 Lambda Extensions API

从图中我们知道 SIGTERM 信号实际上是 instance 处于 shutdown 阶段时,runtime shutdown 行为产生时发出的(见下图中红色方框部分),我们可以通过为 Lambda 加上 Lambda extensions 来实现 SIGTERM 信号的捕获。

Shutdown 阶段持续时间限制

Shutdown 阶段的最长持续时间取决于已注册扩展的配置:

  • 0 毫秒 – 无已注册扩展的函数
  • 500 毫秒 – 带有注册的内部扩展的函数
  • 2000 毫秒 – 具有一个或多个注册的外部扩展的函数

对于具有外部扩展的 Lambda 函数,Lambda 最多保留 300 毫秒(对于具有内部扩展的运行时为 500 毫秒),以便运行时进程执行正常关闭,所以优雅停机的代码逻辑必须在这段时间内完成。Lambda 会将 2000 毫秒限制的剩余时间分配给要关闭的外部扩展,如果运行时或扩展未在限制范围内响应 Shutdown 事件,则 Lambda 将使用 SIGKILL 信号强制结束进程。

怎么实现 AWS Lambda 优雅停机

不同编程语言都能进行 SIGTERM 的捕获和处理,我们以一个 AWS SAM 编排的 python3.12 demo 为例进行详细的说明,方便起见它采用 external extension 的方式来捕获 SIGTERM 信号。其他的语言请看 graceful-shutdown-with-aws-lambda

注册 Lambda extension

首先我们需要在 SAM 模板里面注册 Lambda extension,推荐您推荐注册使用最新版本的 extensions,相关的 ARN 可以在 Lambda Insights extension 中找到。

具体的 AWS SAM 模版 yaml 样例为:

      Layers:
        - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension-Arm64:5"
      Policies:
        # Add IAM Permission for Lambda Insight Extension
        - CloudWatchLambdaInsightsExecutionRolePolicy

如下图:

编写优雅停机处理逻辑

我们使用 python3 代码进行优雅停机时的逻辑处理。当 instance init 阶段冷启动后,instance 被首次 invoke 调用时下面的 python 3.12 代码被运行一次,这也是这部分代码仅有的一次运行;后续的 invoke 只会运行 handler 里面的 event 处理逻辑。

优雅停机的处理代码样例为:

def exit_gracefully(signum, frame):
    r"""
    SIGTERM Handler: https://docs.aws.amazon.com/lambda/latest/operatorguide/static-initialization.html
    Listening for os signals that can be handled,reference: https://docs.aws.amazon.com/lambda/latest/dg/runtimes-extensions-api.html
    Termination Signals: https://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html
    """
    print("[runtime] SIGTERM received")

    print("[runtime] cleaning up")
    # perform actual clean up work here.
    time.sleep(0.2)

    print("[runtime] exiting")
    sys.exit(0)


signal.signal(signal.SIGTERM, exit_gracefully)

如下图:

注意: 任何语言都是类似的编程模型,优雅停机逻辑需要写在 handler 外面,作为 initialization code 运行。更细节的部分可以参考 Lambda programming model

测试优雅停机

当我们调用多次 Lambda 后停止调用一段时间,instance 会被自动回收,我们可以在 Amazon CloudWatch 观察到如下日志。

关于优雅停机的总结

对于 Serverless 函数式计算,AWS Lambda instance 的创建和销毁是比较频繁的,这必然产生大量的冷启动和资源初始化动作,单个 instance 的创建时冷启动会消耗一定的时间,资源的初始化也会耗费一些时间并对其他资源产生影响(比如大量的 MySQL 连接的创建会影响数据库的性能)。为了更好的降低冷启动的影响、更合理的利用资源,instance 会被尽可能的高效的复用。instance 复用就是当一个 instance 完成一个 event 请求后并不会立即释放,而是进入“等待”的状态。在一定时间范围内,如果有新的event请求被分配过来,则会直接调用对应的 handler 方法,而不需要再次初始化各类资源等,这在很大程度上减少了上述冷启动的影响,尽量利用复用功能是我们开发者优化 Lambda 函数代码的关注重点。在整个复用的过程中,这些 instance 一直是活着的状态,它们维持这个对外界资源的持有状态,直到 instance 被销毁。

我们来看一些典型的状态持有场景:

  • 数据库等连接,我们推荐在初始化的时候进行数据库连接的建立,后续尽量对连接进行复用,避免每次请求都创建连接,降低数据库频繁创建和销毁连接的压力。此外使用 AWS RDS Proxy 也可以降低数据库频繁创建和销毁大量连接的压力。
  • 长事务或者长耗时任务的处理,由于 Lambda 对单次 event 事件的处理时长不能超过 15 分钟。这些耗时比较长但是不确定15分钟内一定可以处理完的情况下,我们需要在 event 超时前进行主动撤销。
  • 状态的同步或者通知,一些可中断可重新计算的分布式业务可能是由多个 Lambda instance 协调处理的,某个被即将中断的 instance 需要在死亡前主动告知其他 instance 自己的状态并保存断点状态。

这些场景都需要 pre stop,也就是在每次 AWS Lambda 决定停止当前 instance 前调用某种善后逻辑,开发者负责实现相应逻辑以确保完成 instance 销毁前的必要操作,比如关闭数据库链接,回滚事务,上报、更新状态等。

对此我们的最佳实践是主动+被动结合的资源处理逻辑,主动处理逻辑就是本文介绍的优雅停机,主动释放资源;被动处理逻辑一般是资源侧管理的超时处理,比如 MySQL 服务器主动在一段时间后释放无 instance 认领的连接,Redis 对注册在内存里面的分布式锁状态进行超时释放等。

AWS Lambda 上各种语言的有关优雅停机的支持列表

下面的表格是几种主流的语言在 AWS Lambda runtime 中以 zip 方式运行时有关于优雅停机的支持情况:

注:截止 2023/12/22 日,已经 EOL 或者申明为即将 EOL 的编程语言版本没有包括在内。

language version Identifier Operating system Architecture Support status
Python 3.12 python3.12 Amazon Linux 2023

arm64

x86_64

 Support
Python 3.11 or earlier

python3.11

python3.10

python3.9

Amazon Linux 2

arm64

x86_64

 NOT Support
Node.js 20 nodejs20.x Amazon Linux 2023

arm64

x86_64

Support
Node.js 18 nodejs18.x Amazon Linux 2

arm64

x86_64

Support

go 1.x

(1.19,1.20,1.21)

provided.al2023 Amazon Linux 2023

arm64

x86_64

Support

go 1.x

(1.19,1.20,1.21)

provided.al2 Amazon Linux 2

arm64

x86_64

Support
rust provided.al2023 Amazon Linux 2023

arm64

x86_64

Support
Java 21 java21 Amazon Linux 2023

arm64

x86_64

Support
Java 17 java17 Amazon Linux 2

arm64

x86_64

Support
Java 11 java11 Amazon Linux 2

arm64

x86_64

Support
Java 8 java8.al2 Amazon Linux 2

arm64

x86_64

Support
.NET 6 dotnet6 Amazon Linux 2

arm64

x86_64

Support

参考

本篇作者

柯达

TCL 鸿鹄实验室海外 IoT 云团队开发工程师,致力于基于 AWS 平台构建成熟的海外 IoT 云平台,为 TCL 海外家电设备提供安全的接入与稳定的连接。闲暇时,他喜欢唱歌,健身与读书。

罗新宇

亚马逊云科技解决方案架构师,在架构与开发领域有非常丰富的实践经验,目前致力于 Serverless 在云原生架构中的应用。

林益龙

亚马逊云科技解决方案架构师,专注于在企业中推广云计算与人工智能的最佳实践,以促进业务增长。曾担任运维经理、解决方案架构师等岗位,拥有多年的企业 IT 运维和架构设计经验。