2023年4月21日

我們如何使用 Prisma 將 Serverless 冷啟動速度提升 9 倍

冷啟動是阻礙 Serverless 應用提供快速使用者體驗的一大障礙——但它也是不可避免的。讓我們探討一下導致冷啟動的原因,以及我們如何讓每個使用 Prisma ORM 構建的 Serverless 應用執行得更快。

How We Sped Up Serverless Cold Starts with Prisma by 9x

目錄

賦能開發者,盡享 Serverless & Edge 的優勢

在 Prisma,我們堅信 Serverless 和 Edge 應用的理念!這些部署正規化具有巨大優勢,使開發者能夠以更具可伸縮性且成本更低的方式部署其應用。Vercel(支援 Next.js API 路由)或 AWS Lambda 等 Serverless 提供商就是很好的例子。

然而,這些正規化也帶來了新的挑戰——尤其是在處理資料時!

這就是為什麼在過去的幾個月裡,我們加大了對這些部署正規化的關注,以幫助開發者在利用並享受 Serverless 和 Edge 技術優勢的同時,構建資料驅動型應用。

我們從兩個方面著手解決:

  • 構建產品來解決這些生態系統帶來的新挑戰(例如 Accelerate,一個全球分散式資料庫快取)
  • 改善 Prisma ORM 在 Serverless 和 Edge 環境中的體驗

本文將介紹我們如何改進開發者在 Serverless 環境中構建資料驅動型應用時面臨的一個主要問題:使用 Prisma ORM 時的冷啟動

令人頭疼的冷啟動 🥶

在 Serverless 環境中工作時,最常見的效能問題之一就是漫長的冷啟動。但什麼是冷啟動呢?

不幸的是,這個術語存在很多歧義,並且經常被誤解。通常,它描述的是當 Serverless 函式處理其第一個請求時,例項化其環境並執行其程式碼所需的時間。儘管這是基本的技??解釋,但關於冷啟動還有一些具體事項需要注意。

它們是無法避免的

冷啟動是 Serverless 環境中不可避免的現實。Serverless 的主要“優勢”在於,當流量增加時,你的應用可以無限擴充套件;當不使用時,它可以縮減到零。如果沒有這種能力,Serverless 就不會是……Serverless!

如果一段時間沒有請求,所有執行中的環境都會被關閉——這很好,因為這也意味著你不會產生費用。但這也意味著沒有函式可以立即響應傳入的請求。它們必須首先重新啟動,這需要一點時間。

它們具有實際影響

冷啟動不僅具有技術上的影響,還會給部署 Serverless 函式的企業帶來實際問題。

為使用者提供最佳體驗至關重要,緩慢的啟動效能可能會讓使用者望而卻步。

來自 Cal.comPeer Richelsen 最近在意識到他們的應用正遭受漫長冷啟動的困擾後,在 Twitter 上尋求幫助。

最終,在 Serverless 環境中工作的開發者目標應該是儘可能縮短冷啟動時間,因為漫長的冷啟動會導致使用者體驗不佳。

它們比你想象的更復雜

儘管上述冷啟動的解釋相當直接,但重要的是要理解不同的因素都會導致冷啟動。在接下來的幾節中,我們將解釋 Serverless 函式首次生成和執行時實際發生了什麼。

注意:請記住,這是關於 Serverless 函式如何例項化和呼叫的通用概述。該過程的具體細節可能因你的雲提供商和配置而異(我們主要以 AWS Lambda 為參考)。

我們將使用這個簡單的 Serverless 函式作為示例來解釋這些步驟:

步驟 1:啟動環境

當函式接收到請求但當前沒有可用例項時,你的雲提供商會初始化執行環境,在該環境中執行你的 Serverless 函式。此階段會發生多個步驟:

  1. 虛擬環境會根據你為 Serverless 函式分配的 CPU 和記憶體資源建立。
  2. 你的程式碼會以壓縮包的形式下載,並解壓到新環境的檔案系統中。(如果你使用的是 AWS Lambda,任何關聯的 Lambda 層也會被下載。)
  3. 執行時(即函式執行的特定語言環境)被初始化。如果你的函式是用 JavaScript 編寫的,這將是 Node.js 執行時

此後,函式仍未準備好處理請求。虛擬環境已就緒,所有程式碼都已到位,但執行時尚未處理任何程式碼。在呼叫處理程式之前,必須按照下一步所述初始化應用程式。

注意:函式的啟動細節不可配置,由你的雲提供商處理。你對此的工作方式沒有太多發言權。

步驟 2:啟動應用程式

通常,應用程式程式碼存在於兩個不同的作用域:

  • 處理程式函式外部的程式碼
  • 處理程式函式內部的程式碼

在此步驟中,你的雲提供商會執行處理程式外部的程式碼。處理程式內部的程式碼將在下一步中執行。

AWS Lambda 在執行上述函式時會記錄以下內容:

你可以看到外部的 console.log("Executed when the application starts up!") 甚至在 AWS Lambda 記錄實際的 START RequestId 之前就已經執行了。如果存在任何匯入、建構函式呼叫或其他程式碼——它們也將在此時執行。

(當對函式進行熱啟動請求時,此行將不再被記錄。處理程式外部的程式碼僅在冷啟動期間執行一次。)

步驟 3:執行應用程式程式碼

在啟動過程的最後部分,處理程式函式會被執行。它接收傳入的 HTTP 請求(即請求頭、請求體等...)並執行你已實現的邏輯。

上一步的 AWS Lambda 日誌繼續:

至此,函式的冷啟動已結束,執行環境已準備好處理後續請求。

旁註:AWS Lambda 將執行處理程式內部程式碼所花費的時間記錄為 Duration,這發生在步驟 3。Init Duration 包括環境啟動和應用程式啟動,即步驟 1 和 2。

Prisma 如何影響冷啟動

瞭解了什麼是冷啟動以及初始化 Serverless 函式的步驟後,我們現在將探討 Prisma 在啟動時間中扮演的角色。

  1. Prisma Client 是一個獨立於你的函式程式碼的 Node.js 模組,因此需要時間和資源載入到執行環境的記憶體中:整個函式歸檔檔案需要從某個儲存中下載,然後解壓到檔案系統中。所有 Node.js 模組都是如此,但這確實會增加冷啟動時間,並且專案中使用的依賴越多,時間就越長——Prisma 可能是其中之一。

  2. 程式碼載入到記憶體後,還必須匯入到處理程式檔案中,並由 Node.js 直譯器進行解釋。對於 Prisma Client 來說,這通常意味著呼叫 const { PrismaClient } = require('@prisma/client')

  3. 當 Prisma Client 使用 const prisma = new PrismaClient() 例項化時,Prisma 查詢引擎必須被載入並生成輸入型別和函式等,以使客戶端能夠正常執行。它使用內部的 Schema Builder 來完成此操作。

  4. 最後,一旦虛擬環境準備好執行函式的首次呼叫,處理程式將開始執行你的程式碼。程式碼中的任何 Prisma 查詢,例如 await prisma.user.findMany(),如果尚未透過顯式呼叫 await prisma.$connect() 開啟連線,將首先初始化與資料庫的連線,然後執行查詢並將資料返回給你的應用程式。

有了這些理解,我們可以繼續解釋我們如何改進 Prisma 對冷啟動的影響。

啟動效能提升 9 倍

在過去的幾個月裡,我們加大了工程投入,致力於解決這些冷啟動問題,並自豪地說我們取得了顯著的進展 🎉

總的來說,我們在構建 Prisma ORM 時一直遵循“讓它能用,讓它正確,讓它快速”的哲學。自 2020 年 Prisma ORM 投入生產,並增加了對多種資料庫的支援以及實現了廣泛的功能集之後,我們終於開始專注於提高其效能。

為了說明我們的進展,請看下面的圖表。第一張圖表示我們在開始改進之前,一個具有相對較大 Prisma 模式(包含 500 個模型)的應用的冷啟動持續時間:

Before

之前

這下一張圖展示了我們最近進行效能增強後,資料目前的樣子:

After

之後

我們不會在這裡粉飾太平,Prisma 的啟動時間過去確實有很多不足之處,人們也理所當然地為此批評我們。

然而,正如你所看到的,我們現在的冷啟動時間已經大大縮短了。這些進步源於我們對程式碼庫的改進、對 Serverless 函式行為的發現以及最佳實踐的應用。接下來的章節將更詳細地描述這些內容。

新的基於 JSON 的線協議

下圖與上面所示的之前的圖相同:

Before

之前

在此圖中,Prisma Client 條形圖中的藍色部分表示函式首次呼叫期間執行 findMany 查詢所花費的時間。該時間在內部條形圖中分為兩部分:紫色紅色

我們很快意識到這張圖並沒有多大意義。執行查詢所花費的大部分時間都用在了……沒有執行查詢

這個紫色部分佔據了 findMany 查詢段的大部分時間,表示用於解析我們稱之為 DMMF(資料模型元格式)的時間,DMMF 是一種內部結構,用於驗證傳送到 Prisma 查詢引擎的查詢。

紅色部分表示實際執行查詢所花費的時間。

這裡根本問題在於 Prisma Client 使用了一種類似 GraphQL 的語言作為線協議與查詢引擎進行通訊。GraphQL 帶來了一系列限制,迫使 Prisma Client 必須使用 DMMF(其大小可能達到兆位元組級別的 JSON)來序列化查詢。

如果你使用 Prisma 已久,可能會記得 Prisma 1 是一個更側重於 GraphQL 的工具。當我們將 Prisma 重構為 Prisma 2 時,完全專注於成為一個純粹的資料庫 ORM,我們保留了這一部分架構,沒有質疑它——也沒有測量其效能影響。

Serhii's revelation

Serhii 的發現

我們想到的解決方案是從頭開始用純 JSON 重構線協議,這使得 Prisma Client 和查詢引擎之間的通訊效率高得多,因為它不再需要 DMMF 來序列化訊息。

重新設計線協議後,我們有效地從圖中移除了整個紫色部分,留下了以下結果:

With JSON protocol

使用 JSON 協議

注意:如果你感興趣,可以檢視實際更改的拉取請求:prisma-engines#3624prisma#17911

檢視 GitHub 上那些試用了新 JSON 線協議的使用者給出的驚人反饋:

Blog image

注意:基於 JSON 的線協議目前處於 預覽階段。一旦準備好投入生產,它將成為 Prisma Client 與查詢引擎通訊的預設方式。請嘗試使用並 提交任何反饋,以幫助加快該功能普遍可用的程序。

將你的函式與資料庫部署在同一區域

在我們切換到 JSON 協議後,圖中那個令人分心的大塊紫色部分消失了,我們可以專注於剩餘的部分:

With JSON protocol

使用 JSON 協議

我們清楚地注意到淺紅色紅色部分是接下來的主要最佳化物件。這些代表了 Prisma 觸發的與實際資料庫的通訊。

無論何時託管需要訪問傳統關係型資料庫的應用程式或函式,你都需要初始化與該資料庫的連線。這需要時間並會帶來延遲。你執行的任何查詢也是如此。

目標是將時間和延遲降到最低。目前最好的方法是確保你的應用程式或函式與資料庫伺服器部署在同一地理區域。

Blog image

你的請求到達資料庫伺服器的距離越短,連線建立的速度就越快。在部署 Serverless 應用程式時,這一點非常重要,因為這樣做可能造成的負面影響是巨大的。

不這樣做可能會影響以下時間:

  • 完成 TLS 握手
  • 建立與資料庫的安全連線
  • 執行你的查詢

所有這些因素都在冷啟動期間被啟用,因此會影響 Prisma 資料庫對應用程式冷啟動時間的影響。

令人尷尬的是,我們發現最初的幾次測試中,AWS Lambda 的 Serverless 函式部署在 eu-central-1,而 RDS PostgreSQL 例項託管在 us-east-1。我們迅速糾正了這個問題,“之後”的測量結果清楚地顯示了這可能對你的資料庫延遲產生巨大影響,無論是連線的建立還是任何執行的查詢。

With database in same region as function

資料庫與函式位於同一區域

使用與你的函式不在儘可能近的資料庫,將直接增加你的冷啟動持續時間,並且在處理熱請求期間稍後執行任何查詢時也會產生相同的開銷。

最佳化的內部模式構建

在前面顯示的圖中,你可能已經注意到內部條形圖中的三個部分中,只有兩個與資料庫直接相關。另一個名為“Schema builder”的部分,以青色顯示,則不是。這向我們表明,這一部分是潛在的改進領域。

With database in same region as function

資料庫與函式位於同一區域

Prisma Client 條形圖的綠色部分表示 Prisma Client 執行其 $connect 函式以建立與資料庫連線所花費的時間。該部分在內部條形圖中分為兩塊:青色淺紅色

淺紅色部分表示實際建立資料庫連線所花費的時間,而青色部分則顯示 Prisma 的查詢引擎讀取你的 Prisma 模式,然後用它生成用於驗證傳入的 Prisma Client 查詢的模式所花費的時間。

之前生成這些專案的方式不夠最佳化。為了縮短該部分的時間,我們解決了在那裡發現的效能問題。

更具體地說,我們找到了方法來移除一段開銷很大的程式碼,這段程式碼在查詢引擎啟動時,會在構建查詢模式之前轉換內部的 Prisma Schema。

我們現在還惰性地生成查詢模式中許多型別名稱的字串。這產生了可衡量的差異。

伴隨這一改變,我們還找到了最佳化 Schema Builder 內部程式碼以改善記憶體佈局的方法,這帶來了顯著的效能(執行時)提升。

注意:如果你對我們進行的記憶體分配相關修復的具體細節感興趣,請檢視以下示例拉取請求:#3828, #3823

應用這些更改後,之前的請求看起來像這樣:

With Schema Builder enhancements

經過 Schema Builder 增強後

注意青色部分顯著縮短了。這是一個巨大的勝利,然而仍然存在一個青色部分,這意味著時間花費在了與資料庫無關的事情上。我們已經確定了潛在的增強措施,可以將這部分時間縮短到接近(如果不是完全縮短到)零。

各項小改進

在此過程中,我們還發現了許多可以改進的較小效率問題。這樣的問題有很多,所以我們不會一一贅述,但一個很好的例子是我們對平臺檢測例程進行的最佳化,該例程用於在 Linux 環境中搜索 OpenSSL 庫(該改進的拉取請求可以在這裡找到)。

這項增強平均可以縮短冷啟動時間約 10-20 毫秒。雖然看起來不多,但這項增強以及我們所做的其他小改進累積起來,又節省了相當一部分時間。

旁註:關於 TLS 的發現

在此次行動中,我們還有一個值得注意的發現:透過 TLS 為資料庫連線增加安全性,在你的資料庫與 Serverless 函式託管在不同區域時,會對冷啟動時間產生巨大影響。

TLS 握手需要與資料庫進行一次往返。當你的資料庫與函式託管在同一區域時,這會非常快,但如果它們相距遙遠,則會非常慢。

Prisma Client 預設啟用 TLS,因為這是連線資料庫更安全的方式。因此,一些資料庫與函式不在同一區域的開發者可能會發現,由於 TLS 握手導致冷啟動時間增加。

下圖顯示了啟用 TLS(第一部分)和停用 TLS(透過在連線字串中設定 sslmode=disable)時的不同冷啟動時間。

Blog image

如果你的資料庫與函式託管在同一區域,上述所示的 TLS 開銷可以忽略不計。

Node 生態系統中的其他一些資料庫客戶端和 ORM 預設停用 PostgreSQL 資料庫的 TLS。在將 Prisma ORM 的效能與它們進行比較時,這可能會不幸地導致由於這種開箱即用安全性的差異而產生的效能印象。

我們建議將你的資料庫和函式移動到同一區域,而不是為了提升效能而可能損害安全性。這樣做既能保證資料庫安全,又能帶來更快的冷啟動。

這僅僅是個開始

儘管我們在過去幾個月取得了令人難以置信的進展,但這僅僅是個開始。

我們希望:

  • 最佳化 Schema Builder(圖中青色部分),透過可能在查詢驗證期間或採用惰性方式來完成部分工作,使其接近甚至達到 0。
  • 最佳化 Prisma 的載入(圖中黃色部分),這代表了載入 Prisma 所需的時間,並使其儘可能小。
  • 將以上所有經驗教訓應用於 PostgreSQL 之外的其他資料庫。
  • 最重要的一點:研究 Prisma Client 查詢的效能,並最佳化其執行時間,無論資料量大小。

隨著我們不斷改進 Prisma ORM 的效能,你可以在未來幾周或幾個月內期待本文的更新(當我們進一步提升冷啟動效能時),甚至可能會有另一篇博文釋出。

你可以提供幫助!

使 Prisma 在 Serverless 環境中的體驗儘可能流暢是一個非常宏大的目標。儘管我們擁有一支優秀的團隊致力於改善使用 Prisma Client 的 Serverless 函式的啟動效能,但我們也意識到我們擁有龐大的開發者社群,他們渴望為此專案貢獻自己的力量。

我們邀請你幫助改進 Prisma Client 的效能,特別是在 Serverless 啟動時間方面。

你可以透過多種方式為實現提供世界一流 ORM 的目標做出貢獻,使其可在 Serverless 環境和邊緣計算中訪問:

  • 提交你在 Serverless 環境中使用 Prisma 時發現的問題
  • 有改進想法?發起討論
  • 試用 jsonProtocol 預覽功能並提供反饋

Prisma ORM 是一個開源專案,因此我們完全理解社群反饋和參與的重要性。我們歡迎反饋、批評、問題以及任何有助於推動 Prisma 造福每位開發者的建議。

不要錯過下一篇!

訂閱 Prisma 郵件列表

© . This site is unofficial and not affiliated with Prisma Data, Inc.