2023 年 4 月 21 日
我們如何透過 Prisma 將 Serverless 冷啟動速度提升 9 倍
冷啟動是 serverless 應用程式快速使用者體驗的巨大阻礙 — 但同時也是天生不可避免的。讓我們一起探索造成冷啟動的原因,以及我們如何讓每個使用 Prisma ORM 建構的 serverless 應用程式變得更快。
目錄
讓開發人員能夠充分利用 Serverless 與 Edge 的優勢
在 Prisma,我們堅信 serverless 和 edge 應用程式的前提!這些部署範例具有巨大的優勢,並允許開發人員以更具擴展性和更低成本的方式部署其應用程式。Serverless 供應商,例如 Vercel(使用 Next.js API 路由)或 AWS Lambda 就是很好的例子。
然而,這些範例也帶來了新的挑戰 — 尤其是在處理資料時!
這就是為什麼在過去幾個月中,我們更加關注這些部署範例,以幫助開發人員建構資料驅動的應用程式,同時利用 serverless 和 edge 技術的優勢並從中獲益。
我們正從兩個角度解決這個問題
- 建構解決這些生態系統帶來的新挑戰的產品(例如 Accelerate,一個全球分散式資料庫快取)
- 改善在 serverless 和 edge 環境中使用 Prisma ORM 的體驗
本文介紹了我們如何改進開發人員在 serverless 環境中建構資料驅動應用程式時面臨的主要問題之一:使用 Prisma ORM 時的冷啟動。
可怕的冷啟動 🥶
在 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 在執行上述函數時記錄以下內容
您可以看到外部 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 在啟動時間中所扮演的角色。
-
Prisma Client 是一個 Node.js 模組,它在函數程式碼的外部,因此需要時間和資源才能載入到執行環境的記憶體中:整個函數封存檔需要從某些儲存空間下載,然後解壓縮到檔案系統中。這對於所有 Node.js 模組都是如此,但是它確實會增加冷啟動時間,並且專案中使用的相依性越多,冷啟動時間就越長 - 而 Prisma 可能是其中之一。
-
一旦程式碼載入到記憶體中,也必須將其匯入到處理常式檔案中,並且必須由 Node.js 解譯器解譯。對於 Prisma Client,這通常意味著呼叫
const { PrismaClient } = require('@prisma/client')
。 -
當使用
const prisma = new PrismaClient()
實例化 Prisma Client 時,必須載入 Prisma Query Engine,並產生諸如輸入類型和函數之類的東西,以使用戶端能夠正確運作。它使用內部 Schema Builder 來完成此操作。 -
最後,一旦虛擬環境準備好執行函數的初始調用,處理常式將開始執行您的程式碼。該程式碼中的任何 Prisma 查詢(例如
await prisma.user.findMany()
)都將首先啟動與資料庫的連線(如果尚未透過顯式呼叫await prisma.$connect()
開啟連線),然後執行查詢並將資料傳回給您的應用程式。
有了這種理解,我們可以繼續解釋我們如何改善 Prisma 對冷啟動的影響。
啟動效能提升 9 倍
在過去的幾個月中,我們加大了工程力度來解決這些冷啟動問題,並且很自豪地說,我們已經取得了巨大的進展 🎉
總的來說,在建構 Prisma ORM 時,我們一直遵循「先求有,再求好,後求快」的哲學。在 2020 年推出用於生產環境的 Prisma ORM、新增對多個資料庫的支援並實作大量功能後,我們終於專注於改善其效能。
為了說明我們的進展,請考慮下面的圖表。第一個圖表代表在我們開始努力改善之前,具有相對較大的 Prisma schema(具有 500 個模型)的應用程式的冷啟動持續時間

之前
下一個圖表是我們最近在效能增強方面努力後,目前數字的概況

之後
我們不會在此粉飾太平,Prisma 的啟動時間過去確實令人不滿意,人們也因此而正確地批評我們。
正如您所看到的,我們現在的冷啟動時間短得多。這裡的進展來自於對我們程式碼庫的增強、關於 serverless 函數行為的發現以及應用最佳實務。接下來的章節將更詳細地描述這些內容。
新的基於 JSON 的線路協定
下面的圖表是上面顯示的同一個之前圖表

之前
在此圖表中,藍色 Prisma Client 長條圖部分代表在函數的初始調用期間執行 findMany
查詢所花費的時間。該時間在 Internals 長條圖中分為兩個部分:紫色 和 紅色。
我們很快意識到這個圖表沒有太多意義。執行查詢所花費的大部分時間... 不是在執行查詢!
這個 紫色 部分佔 findMany
查詢部分的大部分,代表解析我們稱之為 DMMF(資料模型元格式)所花費的時間,DMMF 是一種內部結構,用於驗證傳送到 Prisma query engine 的查詢。
紅色 部分代表實際執行查詢所花費的時間。
這裡的根本問題是,Prisma Client 使用類似 GraphQL 的語言作為線路協定,與 query engine 進行通訊。GraphQL 有一組限制,迫使 Prisma Client 使用 DMMF(可以達到兆位元組的 JSON)來序列化查詢。
如果您使用 Prisma 很長時間了,您可能會記得 Prisma 1 是一個更以 GraphQL 為中心的工具。當將 Prisma 重建為 Prisma 2 時,完全專注於成為一個純粹的資料庫 ORM,我們保留了我們架構的這一部分,而沒有質疑它 - 也沒有衡量其效能影響。

Serhii 的啟示
我們提出的解決方案是從頭開始以純 JSON 重新設計線路協定,這使得 Prisma Client 和 query engine 之間的通訊更加有效率,因為它不再需要 DMMF 來序列化訊息。
在重新設計線路協定後,我們有效地從圖表中移除了整個 紫色 部分,讓我們得到以下結果

使用 JSON 協定
注意:如果您有興趣,可以查看 pull request prisma-engines#3624 和 prisma#17911,其中包含所做的實際變更。
查看 GitHub 上使用過新的基於 JSON 的線路協定的使用者的驚人回饋

注意:基於 JSON 的線路協定目前處於 預覽 狀態。一旦準備好用於生產環境,它將成為 Prisma Client 與 query engine 通訊的預設方式。請試用一下並提交任何回饋,以協助加快此功能普遍可用的過程。
將您的函數託管在與資料庫相同的區域中
在我們切換到 JSON 協定後,巨大的分散注意力的 紫色 部分從圖表中消失了,我們可以專注於剩餘的部分

使用 JSON 協定
我們清楚地注意到 淺紅色 和 紅色 部分是下一個大的候選部分。這些代表 Prisma 觸發的與實際資料庫的通訊。
每當您託管需要存取傳統關聯式資料庫的應用程式或函數時,您都需要啟動與該資料庫的連線。這需要時間並帶來延遲。對於您執行的任何查詢也是如此。
目標是將時間和延遲保持在絕對最小值。目前實現此目的的最佳方法是確保您的應用程式或函數部署在與您的資料庫伺服器相同的地理區域中。

您的請求到達資料庫伺服器的距離越短,建立連線的速度就越快。在部署 serverless 應用程式時,牢記這一點非常重要,因為不這樣做所產生的負面影響可能是顯著的。
不這樣做可能會影響以下操作所需的時間
- 完成 TLS 握手
- 使用資料庫保護連線
- 執行您的查詢
所有這些因素都在冷啟動期間被啟動,因此會影響使用 Prisma 的資料庫對應用程式冷啟動的影響。
令人尷尬的是,我們注意到我們在 AWS Lambda 中以 eu-central-1
中的 serverless 函數進行了最初幾次測試,以及在 us-east-1
中託管的 RDS PostgreSQL 實例。我們很快修正了這個問題,「之後」的測量清楚地顯示了這對您的資料庫延遲可能產生的巨大影響,無論是對於連線的建立,還是對於之後執行的任何查詢

資料庫與函數位於同一區域
使用離您的函數不夠近的資料庫將直接增加您的冷啟動持續時間,但也會在稍後處理熱請求期間執行查詢時產生相同的成本。
最佳化的內部 schema 建構
在先前顯示的圖表中,您可能已經注意到,在 Internals 長條圖上,只有三個部分中的兩個與資料庫直接相關。另一個部分「Schema builder」(以 青色 顯示)則不是。這向我們表明,這個部分是潛在的改進領域

資料庫與函數位於同一區域
Prisma Client 長條圖的 綠色 部分代表 Prisma Client 執行其 $connect
函數以建立與資料庫的連線所花費的時間。此部分在 Internals 長條圖中分為兩個區塊:青色 和 淺紅色。
淺紅色 部分代表實際建立資料庫連線所花費的時間,而 青色 部分顯示 Prisma 的 query engine 花費在讀取您的 Prisma schema,然後使用它來產生它用於驗證傳入的 Prisma Client 查詢的 schema 的時間。
先前產生這些項目的方式並不像它們本來可以的那樣最佳化。為了縮短該部分,我們解決了我們可以在那裡找到的效能問題。
更具體地說,我們找到了在啟動 query engine 時,移除轉換內部 Prisma Schema 的昂貴程式碼片段的方法,然後再建構查詢 schema。
我們現在也 延遲產生查詢 schema 中許多類型的名稱字串。這產生了可衡量的差異。
除了該變更之外,我們還找到了最佳化 Schema Builder 內程式碼的方法,以改善記憶體佈局,從而顯著提高效能(執行時間)。
注意:如果您對我們所做的與記憶體分配相關的修正的具體細節感興趣,請查看以下範例 pull request:#3828、#3823
在應用這些變更後,之前的請求看起來像這樣

使用 Schema Builder 增強功能
請注意,青色 部分明顯縮短了。這是一個巨大的勝利,但是仍然有一個 青色 部分,這意味著時間花費在執行與資料庫無關的事情上。我們已經確定了潛在的增強功能,這些功能將使該部分接近(如果不是完全降至)零。
各種小的勝利
在此過程中,我們還發現了許多較小的效率低下之處,我們可以加以改進。這些有很多,因此我們不會一一介紹,但一個很好的例子是我們對平台偵測例程所做的最佳化,該例程用於在 Linux 環境中搜尋 OpenSSL 程式庫(可以在 此處找到該增強功能的 pull request)。
此增強功能平均可將冷啟動時間縮短約 10-20 毫秒。雖然這看起來不多,但此增強功能和我們所做的其他小增強功能的累積加起來,又節省了相當多的時間。
題外話:關於 TLS 的發現
在此計劃期間,我們的另一個值得注意的發現是,透過 TLS 為資料庫連線新增安全性在您的資料庫託管在與 serverless 函數不同區域時,可能會對冷啟動時間產生很大的影響。
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 效能的旅程中不斷前進。
您可以提供協助!
使 serverless 上的 Prisma 體驗盡可能順暢的目標是一個非常宏大的目標。雖然我們有一個出色的團隊致力於改善使用 Prisma Client 的 serverless 函數的啟動效能,但我們也意識到我們擁有龐大的開發人員社群,他們渴望為這項計劃盡自己的一份力量。
我們邀請您協助改善 Prisma Client 的效能,尤其是在 serverless 啟動時間的背景下。
有很多方法可以為提供世界一流的 ORM 做出貢獻,這種 ORM 在 serverless 環境和 edge 上都可存取
Prisma ORM 是一個開放原始碼專案,因此我們完全理解社群回饋和參與的重要性。我們喜歡回饋、評論、問題以及任何可能有助於為每位開發人員推進 Prisma 的事物。
不要錯過下一篇文章!
訂閱 Prisma 電子報