Lambda 并发 1,000 的幕后 - Firecracker 预热池与 Worker Manager 的机制
本文讲解 Lambda 在默认并发数 1,000 的限制内管理微型虚拟机的机制、基于预热池的复用策略、Worker Manager 的放置决策,以及降低冷启动率的内部优化。
Lambda 执行环境的生命周期
当 Lambda 函数调用到达时,Lambda 服务会分配执行环境(Execution Environment)。执行环境是运行在 Firecracker 微型虚拟机上的沙箱,包含函数的代码、运行时和配置的环境变量。执行环境的生命周期由 3 个阶段组成。在 INIT 阶段,会启动运行时并执行处理程序之外的全局作用域代码。在 INVOKE 阶段,会调用处理程序函数。在 SHUTDOWN 阶段,会执行运行时的终止处理。重要的是,INVOKE 阶段完成后,执行环境并不会立即被销毁,而是以“冻结”状态保留一段时间。下一个请求到达时,被冻结的执行环境会被“解冻”并重复使用。这就是热启动。处于冻结状态的执行环境不消耗 CPU 时间,只占用内存。Lambda 管理着这些被冻结的执行环境的池,这就是“预热池”。
预热池的管理策略
Lambda 的预热池管理是一个在冷启动率最小化与资源效率最大化之间取得平衡的优化问题。如果在预热池中长时间保留执行环境,冷启动会减少,但由于持续占用内存,物理主机的容量会受到挤压。反之,如果立即销毁,资源会被释放,但冷启动会增加。用哪种算法来取得这种平衡属于 Lambda 的内部实现,并未公开。官方文档化的只有两点:完成调用的执行环境会以冻结状态保留并被后续调用重复使用,以及 Lambda 会在任意时间终止该执行环境。预热池的保留时间也没有公布,会根据函数的调用模式和物理主机的容量状况动态变化。因此在设计时,需要以“被重复使用时速度快,但无论何时被销毁都能正确运行”为前提编写代码。即使是重复使用在处理程序之外初始化的连接和客户端的实现,把相当于首次的初始化当作随时都可能发生的事情来对待也是安全的。另外,Firecracker 本身作为在 Lambda 和 Fargate 两项服务中运行的轻量级虚拟化基础,以开源形式公开。其设计可以在 NSDI 2020 上发表的论文中读到。
Worker Manager - 放置到哪台物理主机
Lambda 的 Worker Manager 是决定把新的执行环境放置到哪台物理主机(Worker)上的组件。这一放置决策需要同时满足多个约束。第一,物理主机的容量(CPU、内存)有空余。第二,同一客户的执行环境分散在多台物理主机上(故障隔离)。第三,对于需要 VPC 连接的函数,能够在目标 VPC 的子网中分配 ENI。在 2019 年 9 月的改进之前,连接 VPC 的 Lambda 函数每次冷启动都需要等待 ENI 的创建和挂载完成。AWS 将当时的问题说明为“创建和挂载网络接口所需的时间会延长冷启动”,增加的延迟秒数本身并未公布。此外,由于 ENI 的数量随并发数增加,这些函数还同时面临 3 个上限:子网 IP 地址的消耗、每个账户的 ENI 上限,以及 ENI 创建 API 的速率上限。在现行方式中,ENI 会在创建函数时或更新 VPC 设置时预先创建。执行环境通过隧道连接到由 Hyperplane(Network Load Balancer 和 NAT Gateway 也在使用的 AWS 网络功能虚拟化基础)管理的 ENI。由于 ENI 在执行环境之间共享,每个安全组与子网的组合有几个就足够了,并发数的增加不再直接导致 ENI 的增加。通过这一变更,连接 VPC 的 Lambda 函数的冷启动变得与非 VPC 函数几乎相同。
并发数的管理与突发限制
Lambda 每个账户默认的并发数 1,000 由区域内的所有函数共享。如果函数 A 使用了 800 的并发,函数 B 就只能使用 200。另一方面,并发数增加的速度(扩展速率)不是共享的,而是以函数为单位。官方文档将其定义为“每个区域 / 每个函数每 10 秒 1,000 个执行环境实例(或每 10 秒每秒 10,000 个请求)”。每个区域设有初始突发额度、之后每分钟只按固定数量增加的旧模型,已在 2023 年 11 月的修订中被取代。由于上限变成了以函数为单位,即使某个函数用尽了扩展速率,同一区域中其他函数的启动速度也不会受到影响。超过这一速度到达的调用会被限流,同步调用会返回错误,异步调用则会在内部队列中重试。作为速度的参考,默认的 1,000 可以在 10 秒内达到;即使在提高上限后从 3,000 扩展到 10,000,也大约是 70 秒的水平。这一上限的设计目的,是为 Firecracker 微型虚拟机的启动、ENI 的分配、物理主机容量的分配等内部基础设施留出跟上负载急剧增加的时间。
冷启动对策的 2 代 - Provisioned Concurrency 与 SnapStart
在启动阶段想要避免冷启动时,有 2 代手段。Provisioned Concurrency 是一项预先初始化指定数量的执行环境并将其保持在热状态的功能,无需等待扩展速率的限制,就能以两位数毫秒的速度响应。另一种是 Lambda SnapStart。在 SnapStart 中,发布函数版本时,Lambda 会执行到初始化完成,并将已初始化执行环境的内存和磁盘状态作为 Firecracker 微型虚拟机的快照获取。之后,Lambda 会将快照加密并缓存。调用时以及横向扩展时,Lambda 会从已缓存的快照恢复执行环境,而不是从零开始执行 INIT 阶段。因此,本文主题即执行环境的初始化本身的定位会发生变化。支持的运行时为 Java 11 起、Python 3.12 起、.NET 8 起,并且只在已发布的版本和指向它的别名上有效。它不支持与 Provisioned Concurrency 同时使用,也不支持 Amazon EFS、Amazon S3 Files 以及超过 512 MB 的临时存储。此外,1 个快照会作为多个执行环境的初始状态被重复使用。因此,对于初始化时生成的 ID 和随机数的唯一性、初始化时建立的网络连接的状态,需要以在处理程序一侧重新建立为前提进行设计。
重复使用执行环境时需要注意的事项
执行环境的重复使用(热启动)从性能的角度来看是有利的,但存在开发者应当注意的副作用。第一,全局变量的状态会被保留。上一次调用中设置的全局变量的值,在下一次调用中仍然存在。利用这一点缓存数据库连接(连接池)是推荐的模式,但如果把请求特有的数据保存在全局变量中,就会发生前一个请求的数据泄漏到下一个请求的缺陷。第二,/tmp 目录中的文件会被保留。由于上一次调用写入 /tmp 的文件仍然存在,有可能用尽磁盘容量(最大 10 GB)。第三,后台进程会残留。如果在处理程序函数中启动异步处理,并在不等待其完成的情况下返回响应,那么该异步处理在下一次调用时有可能仍在运行。
参考资料(AWS 官方)
本页的第一手信息来源是 AWS 官方网站及官方文档。最新的规格与价格请以下列官方页面为准。
如本页内容与官方文档不一致,请以官方文档为准。