2023 年 4 月 21 日
我们如何通过 Prisma 将 Serverless 冷启动加速 9 倍
冷启动是 Serverless 应用程序获得快速用户体验的一大障碍,但它本质上也是不可避免的。让我们来探讨一下是什么导致了冷启动,以及我们如何让使用 Prisma ORM 构建的每个 Serverless 应用程序都更快。
目录
使开发人员能够获得 Serverless 和 Edge 的好处
在 Prisma,我们非常相信 Serverless 和 Edge 应用程序的前提!这些部署范例具有巨大的好处,使开发人员能够以更可扩展和更低成本的方式部署他们的应用程序。像 Vercel(带有 Next.js API 路由)或 AWS Lambda 这样的 Serverless 提供商就是很好的例子。
然而,这些范例也带来了新的挑战——尤其是在处理数据时!
这就是为什么在过去的几个月中,我们更加关注这些部署范例,以帮助开发人员构建数据驱动的应用程序,同时利用 Serverless 和 Edge 技术的优势。
我们从两个角度解决这个问题
- 构建解决这些生态系统带来的新挑战的产品(例如 Accelerate,一种全球分布式数据库缓存)
- 改善 Serverless 和 Edge 环境中 Prisma ORM 的体验
本文介绍了我们如何改进开发人员在使用 Prisma ORM 在 Serverless 环境中构建数据驱动应用程序时面临的主要问题之一:冷启动。
可怕的冷启动 🥶
在 Serverless 环境中工作时,最常见的性能问题之一是冷启动时间过长。但是什么是冷启动?
不幸的是,这个术语有很多歧义,并且经常被误解。但一般来说,它描述的是当函数处理其第一个请求时,Serverless 函数的环境被实例化并执行其代码所需的时间。虽然这是基本的技术解释,但关于冷启动还有一些需要注意的具体事项。
它们本质上是不可避免的
在 Serverless 环境中工作时,冷启动是不可避免的现实。Serverless 的主要“优势”在于,当流量增加时,您的应用程序可以无限扩展,而当不使用时,则可以缩小到零。没有这种能力,Serverless 就不会是...Serverless!
如果一段时间内没有请求,所有正在运行的环境都会被关闭——这很好,因为这也意味着您不会产生任何成本。但这也意味着没有函数可以立即响应传入的请求。它们必须首先重新启动,这需要一点时间。
它们具有现实世界的影响
冷启动不仅具有技术上的影响,还会为部署 Serverless 函数的企业带来实际问题。
为您的用户提供尽可能好的体验至关重要,而缓慢的启动性能可能会导致用户流失。
来自 Cal.com 的 Peer Richelsen 最近在意识到他们的应用程序遭受了较长的冷启动后,在 Twitter 上发帖。
最终,在 Serverless 环境中工作的开发人员的目标应该是尽可能缩短冷启动时间,因为较长的冷启动可能会给用户带来不好的体验。
它们比您想象的要复杂
尽管上面对冷启动的解释非常简单,但重要的是要了解不同的因素会导致冷启动。我们将在接下来的几节中解释当首次生成和执行 Serverless 函数时实际会发生什么。
注意:请记住,这是对 Serverless 函数如何被实例化和调用的总体概述。该过程的具体细节可能因您的云提供商和配置而异(我们主要使用 AWS Lambda 作为参考)。
我们将使用这个简单的 Serverless 函数作为示例来解释这些步骤
步骤 1:启动环境
当函数收到请求但当前没有可用的实例时,您的云提供商会初始化执行环境,在该环境中运行您的 Serverless 函数。在此阶段会发生多个步骤
- 使用您为 Serverless 函数分配的 CPU 和内存资源创建虚拟环境。
- 您的代码将以存档形式下载并解压缩到新环境的文件系统中。(如果您使用的是 AWS Lambda,则还会下载任何关联的 Lambda 层。)
- 初始化运行时(即运行函数的特定于语言的环境)。如果您的函数是用 JavaScript 编写的,这将是 Node.js 运行时。
之后,该函数仍然无法处理请求。虚拟环境已准备就绪,所有代码都已到位,但运行时尚未处理任何代码。在调用处理程序之前,必须按照下一步所述初始化应用程序。
注意:函数启动的这些细节是不可配置的,并且由您的云提供商处理。您在如何工作方面没有太多发言权。
步骤 2:启动应用程序
通常,应用程序代码存在于两个不同的作用域中
- 处理程序函数外部的代码
- 处理程序函数内部的代码
在这一步中,您的云提供商执行外部于处理程序的代码。内部处理程序的代码将在下一步执行。
当从上面运行函数时,AWS Lambda 会记录以下内容
您可以看到,即使在 AWS Lambda 记录实际的 START RequestId
之前,外部的 console.log("Executed when the application starts up!")
也会被执行。如果存在任何导入、构造函数调用或其他代码,也会在此期间执行。
(在对您的函数进行热启动请求时,此行将不再被记录。处理程序之外的代码仅在冷启动期间执行一次。)
步骤 3:执行应用程序代码
在启动过程的最后一部分,将执行处理程序函数。它接收传入的 HTTP 请求(即请求头、请求体等),并运行您已实现的逻辑。
上一步中的 AWS Lambda 日志继续如下
至此,您函数的冷启动已结束,执行环境已准备好处理进一步的请求。
旁注:AWS Lambda 将执行处理程序内部的代码所花费的时间记录为
Duration
,这发生在步骤 3 中。Init Duration
是环境的启动和应用程序的启动,因此是步骤 1 和 2。
Prisma 如何影响冷启动
在了解了冷启动是什么以及初始化无服务器函数所采取的步骤之后,我们现在将看看 Prisma 在启动时间中扮演的角色。
-
Prisma Client 是一个 Node.js 模块,它位于函数代码的外部,因此需要时间和资源才能加载到执行环境的内存中:整个函数存档需要从某个存储位置下载,然后解压缩到文件系统中。对于所有 Node.js 模块都是如此,但它确实会增加冷启动时间,并且项目中使用更多依赖项会增加冷启动时间 - 而 Prisma 可能是其中之一。
-
将代码加载到内存后,还必须将其导入到处理程序的文件中,并且必须由 Node.js 解释器进行解释。对于 Prisma Client,通常意味着调用
const { PrismaClient } = require('@prisma/client')
。 -
当使用
const prisma = new PrismaClient()
实例化 Prisma Client 时,必须加载 Prisma 查询引擎并生成诸如输入类型和允许客户端正确操作的函数。它使用内部 Schema Builder 来执行此操作。 -
最后,一旦虚拟环境准备好运行函数的初始调用,处理程序将开始执行您的代码。该代码中的任何 Prisma 查询(如
await prisma.user.findMany()
)将首先启动与数据库的连接(如果尚未通过显式调用await prisma.$connect()
打开连接),然后执行查询并将数据返回到您的应用程序。
了解了这些之后,我们可以继续解释我们如何改进 Prisma 对冷启动的影响。
启动性能提高 9 倍
在过去的几个月中,我们增加了在解决这些冷启动问题方面的工程工作量,并且很自豪地说我们已经取得了巨大的进步 🎉
总的来说,在构建 Prisma ORM 时,我们一直遵循“使其工作、使其正确、使其快速”的理念。在 2020 年为生产环境推出 Prisma ORM 之后,添加了对多个数据库的支持并实现了一系列广泛的功能,我们终于将重点放在提高其性能上。
为了说明我们的进展,请考虑下面的图表。第一个图表表示在我们开始努力改进之前,具有相对较大的 Prisma 模式(包含 500 个模型)的应用程序的冷启动持续时间
之前
下一个图表显示了我们最近在性能增强方面所做的努力之后,目前的数字是什么样的
之后
我们不会在这里粉饰太平,Prisma 的启动时间过去确实有很多不足之处,人们也因此正确地批评过我们。
如您所见,我们现在的冷启动时间短得多。这里的进步来自于对我们代码库的增强、对无服务器函数行为的发现以及最佳实践的应用。以下各节将更详细地描述这些内容。
新的基于 JSON 的有线协议
下图是上面显示的相同的之前图表
之前
在此图中,蓝色 部分的 Prisma Client 条表示在函数的初始调用期间运行 findMany
查询所花费的时间。该时间在 Internals 条中分为两部分:紫色 和 红色。
我们很快意识到这个图表没什么意义。运行查询所花费的大部分时间都花费在...不运行查询!
这个 紫色 部分占 findMany
查询部分的大部分,它表示解析我们称之为 DMMF(数据模型元格式)所花费的时间,DMMF 是用于验证发送到 Prisma 查询引擎的查询的内部结构。
红色 部分表示实际运行查询所花费的时间。
这里的根本问题是,Prisma Client 使用类似 GraphQL 的语言作为有线协议与查询引擎通信。GraphQL 有一系列限制,迫使 Prisma Client 使用 DMMF(可以达到数兆字节的 JSON)来序列化查询。
如果您使用 Prisma 很长时间了,您可能会记得 Prisma 1 是一个更加专注于 GraphQL 的工具。在将 Prisma 重建为 Prisma 2,完全专注于成为一个普通的数据库 ORM 时,我们保留了我们架构的这一部分,没有质疑它 - 也没有衡量其性能影响。
Serhii 的启示
我们提出的解决方案是从头开始使用纯 JSON 重新设计有线协议,这使得 Prisma Client 和查询引擎之间的通信效率更高,因为它不再需要 DMMF 来序列化消息。
在重新设计有线协议后,我们有效地从图中删除了整个 紫色 部分,得到了以下结果
使用 JSON 协议
注意:如果您有兴趣,可以查看 pull request prisma-engines#3624 和 prisma#17911 中的实际更改。
查看 GitHub 上使用过新的基于 JSON 的有线协议的用户的出色反馈
注意:基于 JSON 的有线协议目前处于 预览版。一旦它为生产环境做好准备,它将成为 Prisma Client 与查询引擎通信的默认方式。请尝试一下并提交任何反馈,以帮助加快此功能普遍可用的过程。
将您的函数托管在与数据库相同的区域中
在我们切换到 JSON 协议后,大的分散注意力的 紫色 部分从图中消失了,我们可以专注于其余部分
使用 JSON 协议
我们清楚地注意到 浅红色 和 红色 部分是下一个主要候选对象。这些代表 Prisma 触发的与实际数据库的通信。
每当您托管需要访问传统关系数据库的应用程序或函数时,您都需要启动与该数据库的连接。这需要时间并且会产生延迟。对于您执行的任何查询也是如此。
目标是将时间和延迟保持在绝对最小值。目前最好的方法是确保您的应用程序或函数部署在与数据库服务器相同的地理区域中。
您的请求到达数据库服务器的距离越短,建立连接的速度就越快。在部署无服务器应用程序时,务必牢记这一点,因为不这样做所产生的负面影响可能非常严重。
不这样做会影响所需时间
- 完成 TLS 握手
- 使用数据库安全连接
- 执行您的查询
所有这些因素都会在冷启动期间激活,因此会导致使用带有 Prisma 的数据库对应用程序的冷启动产生影响。
我们尴尬地注意到,我们在 eu-central-1
的 AWS Lambda 中运行了前几次测试,并且 RDS PostgreSQL 实例托管在 us-east-1
中。我们很快修复了这个问题,“之后”的测量清楚地显示了这对数据库延迟的巨大影响,无论是对于连接的创建,还是对于执行的任何查询
数据库与函数位于同一区域
使用与您的函数不尽可能接近的数据库会直接增加冷启动的持续时间,并且在稍后处理热请求期间执行查询时也会产生相同的成本。
优化的内部模式构建
在前面显示的图表中,您可能已经注意到 Internals 条上的三个部分中只有两个与数据库直接相关。另一个部分“Schema builder”,以 青色 显示,不是。这向我们表明,此部分是潜在的改进领域
数据库与函数位于同一区域
Prisma Client 条形图中绿色部分表示 Prisma Client 运行其 $connect
函数以建立与数据库的连接所花费的时间。在 内部 条形图中,此部分分为两个部分:青色和浅红色。
浅红色部分表示实际创建数据库连接所花费的时间,而青色部分显示 Prisma 的查询引擎读取您的 Prisma 架构,然后使用它生成用于验证传入的 Prisma Client 查询的架构所花费的时间。
先前生成这些项的方式不如它们可能的那样优化。为了缩短该部分,我们解决了在那里可以找到的性能问题。
更具体地说,我们找到了方法来移除在查询引擎启动之前转换内部 Prisma 架构的昂贵代码,以便构建查询架构。
现在,我们还惰性地生成查询架构中许多类型的名称字符串。这带来了显著的差异。
除了此更改之外,我们还找到了优化 Schema Builder 中代码的方法,以改善内存布局,从而显著提高性能(运行时)。
应用这些更改后,之前的请求看起来如下所示
使用 Schema Builder 增强功能
请注意,青色部分明显缩短。这是一个巨大的胜利,但是仍然存在青色部分,这意味着花费时间执行与数据库无关的操作。我们已经确定了潜在的增强功能,可以将此部分缩短到接近(如果不是完全降至)零。
各种小的改进
在此过程中,我们还发现了很多可以改进的较小效率低下之处。有很多这样的改进,因此我们不会逐一介绍,但一个很好的例子是我们对平台检测例程的优化,该例程用于在 Linux 环境中搜索 OpenSSL 库(有关该增强功能的拉取请求可以在此处找到)。
此增强功能平均可将冷启动时间缩短约 10-20 毫秒。虽然这看起来不多,但此增强功能和其他小的增强功能的累积加起来可以节省相当多的时间。
附注:关于 TLS 的发现
在此过程中,我们另一个值得注意的发现是,通过 TLS 为您的数据库连接添加安全性,当您的数据库托管在与您的无服务器函数不同的区域时,会对冷启动时间产生很大的影响。
TLS 握手需要往返于您的数据库。当您的数据库托管在与您的函数相同的区域时,这非常快,但是如果它们相距很远,则可能非常慢。
Prisma Client 默认启用 TLS,因为它是连接到数据库的更安全的方式。因此,一些数据库与函数不在同一区域的开发人员可能会发现由 TLS 握手引起的冷启动时间增加。
下图显示了启用 TLS(第一个)和禁用 TLS(通过在连接字符串中设置sslmode=disable
)的不同冷启动时间
如果您的数据库托管在与您的函数相同的区域中,则上面显示的 TLS 开销可以忽略不计。
Node 生态系统中的其他一些数据库客户端和 ORM 默认禁用 PostgreSQL 数据库的 TLS。在将 Prisma ORM 的性能与它们进行比较时,这不幸会导致由于这种开箱即用的安全性的差异而导致的性能印象。
我们建议将您的数据库和函数移动到同一区域,而不是为了提高性能而可能损害安全性。这将使您的数据库保持安全,并导致更快的冷启动。
这仅仅是个开始
虽然我们在过去的几个月中取得了令人难以置信的进步,但这仅仅是个开始。
我们想:
- 优化 Schema Builder(图中的青色部分),通过潜在地惰性执行某些工作或在查询验证期间执行某些工作,使其接近或实际为 0。
- 优化 Prisma 的加载(图中的黄色部分),这表示加载 Prisma 所花费的时间,并使其尽可能小。
- 将以上所有学习应用于除 PostgreSQL 之外的其他数据库。
- 最重要的是:查看 Prisma Client *查询* 的性能,并优化这些查询所花费的时间,无论数据量大小。
您可以期望此博客文章的更新(当我们进一步提高冷启动性能时),或者甚至在未来几周或几个月内发布另一篇博客文章,因为我们正在不断努力提高 Prisma ORM 的性能。
您可以提供帮助!
使 Prisma 在无服务器上的体验尽可能顺畅的目标非常雄心勃勃。尽管我们有一个出色的团队致力于使用 Prisma Client 提高无服务器函数的启动性能,但我们也意识到我们拥有庞大的开发人员社区,他们渴望尽可能地为此做出贡献。
我们邀请您帮助提高 Prisma Client 的性能,尤其是在无服务器启动时间方面。
您可以通过多种方式为在无服务器环境和边缘提供世界一流的 ORM 的目标做出贡献
Prisma ORM 是一个开源项目,因此我们完全理解社区反馈和参与的重要性。我们喜欢反馈、批评、问题以及任何可能有助于推动 Prisma 为每位开发人员的利益而前进的事物。
不要错过下一篇文章!
注册 Prisma 新闻通讯