注意:本篇貼文已更新,新增了關於 AWS Lambda 的更多詳細資訊。
去年,我們宣布了對 Python Workers 的基本支援,這讓 Python 開發人員能夠透過單一命令在全球範圍運用 Python,發揮 Workers 平台的優勢。
時至今日,我們一直努力讓 Workers 上的 Python 體驗感覺很棒。我們一直致力於將套件支援引入平台,現在這一目標已經實現 — 具有極快的冷啟動速度和 Python 原生開發人員體驗。
這意味著套件整合至 Python Worker 的方式發生了變化。現在,我們並非僅提供有限的內建套件,而是能夠針對 Pyodide(讓 Python Workers 運作的 WebAssembly 執行階段)支援的任何套件來提供支援。這包括所有純 Python 套件,以及許多依賴於動態連結庫的許多套件。我們還圍繞 uv 建置了工具,以便簡化套件安裝。
此外,我們還實作了專用記憶體快照功能,以縮短冷啟動時間。相較於其他無伺服器 Python 供應商,這些快照可大幅提升速度。在使用常見套件的冷啟動測試中,Cloudflare Workers 的啟動速度比未採用 SnapStart 的 AWS Lambda 快 2.4 倍,比 Google Cloud Run 快 3 倍。
在這篇部落格文章中,我們將說明 Python Workers 的獨特之處,並分享我們如何實現上述目標的一些技術詳細資料。首先,對於那些可能不熟悉 Workers 或無伺服器平台的人,特別是具有 Python 背景的人,我們將分享您為什麼可能需要使用 Workers。
Workers 之所以有魔力,部分原因在於其簡潔的程式碼和便利的全球部署。我們先來看看如何透過快速冷啟動,在不到兩分鐘的時間內,將 FastAPI 應用程式部署到全球各地。
只需幾行程式碼,即可使用 FastAPI 實作一個簡單的 Worker。
from fastapi import FastAPI
from workers import WorkerEntrypoint
import asgi
app = FastAPI()
@app.get("/")
async def root():
return {"message": "This is FastAPI on Workers"}
class Default(WorkerEntrypoint):
async def fetch(self, request):
return await asgi.fetch(app, request.js_object, self.env)
若要部署類似的項目,只需確保您已安裝 uv 和 npm,然後執行以下命令。
$ uv tool install workers-py
$ pywrangler init --template \
https://github.com/cloudflare/python-workers-examples/03-fastapi
$ pywrangler deploy
只需少量程式碼和 pywrangler deploy,您現在能夠在 Cloudflare 遍及 125 個國家/地區 330 個地點的邊緣網路部署應用程式。無需擔心基礎架構或擴展問題。
在許多使用案例中,Python Workers 完全免費。我們的免費方案提供每天 100,000 個請求,每次調用 10 毫秒 CPU 時間。如需詳細資訊,請參閱說明文件中的定價頁面。
如需更多範例,請參閱 GitHub 中的存放庫。繼續閱讀以深入瞭解 Python Workers。
您可以使用 Python Workers 做些什麼呢?
現在已經有了 Worker,幾乎一切都變得可能。程式碼由您撰寫,所以決定權在您。您的 Python Worker 接收 HTTP 請求,並且可以向公共網際網路上的任何伺服器傳送請求。
您可以設定 Cron 觸發程序,讓您的 Worker 定期執行。此外,如果您有更複雜的要求,您可以使用 Workflows for Python Workers,甚至是 使用 Durable Objects 的長期執行 WebSocket 伺服器和用戶端。
以下是更多您可以使用 Python Workers 執行的各種操作範例:
無伺服器平台(如 Workers)僅在必要時執行程式碼,從而為您節省資金。這意味著,如果您的 Worker 沒有收到請求,可能會被關閉,並需要在收到新請求時重新啟動。通常,這會產生我們稱為「冷啟動」的資源開銷。務必盡可能縮短這些時間,以盡量減少終端使用者的延遲。
在標準 Python 中,啟動執行階段的成本很高,因此 Python Workers 的初始實作著重於加快執行階段的啟動速度。然而,我們很快就意識到這還不夠。即使 Python 執行階段啟動迅速,在實際情況中,初始啟動通常也包括從套件載入模組。並且遺憾的是,在 Python 中,許多常用的套件可能需要數秒才能載入。
我們的目標是無論是否載入套件,都能讓冷啟動快速執行。
為了測量實際的冷啟動效能,我們建立了一項用於匯入常見套件的基準,以及使用裸 Python 執行階段來執行「hello world」的基準。標準 Lambda 能夠快速僅啟動執行階段,但一旦您需要匯入套件,冷啟動時間就會迅速增加。為了實現最佳化,以便在匯入套件的情況下加速冷啟動,您可在 Lambda 上使用 SnapStart(我們稍後會將其新增至連結的基準測試中)。這會產生儲存快照的費用,並且每次還原都會產生額外費用。Python Workers 會自動針對每個 Python Worker,免費套用記憶體快照。
以下是載入三個常用套件 (httpx、fastapi 和 pydantic) 時的平均冷啟動時間:
平台 | 平均冷啟動(秒) |
Cloudflare Python Workers | 1.027 |
AWS Lambda(不使用 SnapStart) | 2.502 |
Google Cloud Run | 3.069 |
在此案例中,Cloudflare Python Workers 的冷啟動速度比不使用 SnapStart 的 AWS Lambda 快 2.4 倍,也比 Google Cloud Run 快 3 倍。我們藉由使用記憶體快照,實現了這些較低的冷啟動數字。我們將在後面的章節中說明我們是如何做到的。
我們會定期執行這些基準。前往此處,瞭解有關我們測試方法的最新資料和更多資訊。
我們在架構上與這些其他平台不同,即 Workers 以隔離為基礎。正因如此,我們的目標遠大,並且正為實現零冷啟動的未來而努力。
Python 之所以如此出色,主要原因是採用多樣化的套件生態系統。因此,我們一直在努力確保在 Workers 中盡可能簡單地使用套件。
我們瞭解到,使用現有的 Python 工具是實現卓越開發體驗的最佳途徑。因此,我們選擇了 uv 套件和專案管理器,因其速度快、成熟,並且在 Python 生態系統中發展勢頭良好。
我們圍繞 uv 建置自己的工具,稱為 pywrangler。此工具主要執行以下操作:
Pywrangler 呼叫 uv,以與 Python Workers 相容的方式來安裝相依項,並在本地開發或部署 Workers 時呼叫 wrangler。
實際上,這意味著您只需執行 pywrangler dev 和 pywrangler deploy,即可在本地測試您的 Worker 並進行部署。
您可以使用 pywrangler types,針對在您的 Wrangler 中定義的繫結項來生成類型提示。這些類型提示將適用於 Pylance 或最新版本的 mypy。
如需生成類型,使用 wrangler types 來建立 typescript 類型提示,然後使用 typescript 編譯器,針對這些類型生成抽象語法樹。最後,我們使用 TypeScript 提示(例如 JS 物件是否具有迭代器欄位),來生成與 Pyodide 外部函數介面搭配使用的 mypy 類型提示。
Python 啟動通常很慢,並且匯入 Python 模組可能會觸發大量工作。因此,使用記憶體快照,避免在冷啟動期間執行 Python 啟動。
部署 Worker 時,我們會執行 Worker 的頂層作用域,接著擷取記憶體快照,並將其與您的 Worker 一起儲存。每當我們為 Worker 啟動新的隔離程序時,我們會還原記憶體快照,並且 Worker 可隨時處理請求,而無需在準備階段執行任何 Python 程式碼。這能大幅縮短冷啟動時間。例如,在沒有快照的情況下,啟動一個匯入 fastapi、httpx 和 pydantic 的 Worker 大約需要 10 秒鐘。使用快照時,則只需 1 秒鐘。
由於 Pyodide 在 WebAssembly 基礎上建置,讓這一點得以實現。我們可輕鬆擷取執行階段的完整線性記憶體並加以還原。
WebAssembly 執行階段並不需要位址空間配置隨機化之類的功能來確保安全性,因此在現代作業系統上,記憶體快照的大部分困難都不會發生。就像原生記憶體快照一樣,我們仍然必須在啟動時仔細處理熵,以避免使用 XKCD 隨機數生成器(我們非常注重實際隨機性)。
藉由拍攝記憶體快照,我們可能會不小心鎖定隨機性的種子值。在這種情況下,未來對「隨機」數字的呼叫會在多個請求中持續傳回相同的值序列。
避免這種情況特別困難,因為 Python 在啟動時會使用大量的熵。這些包括 libc 函數 getentropy() 和 getrandom(),以及從 /dev/random 和 /dev/urandom 讀取。所有這些函數與 JavaScript crypto.getRandomValues() 函數的實作相同。
在 Cloudflare Workers 中,crypto.getRandomValues()啟動時一直停用,以便將來能夠轉換為使用記憶體快照。遺憾的是,Python 解譯器若不呼叫此函數就無法啟動。而且許多套件在啟動時也需要熵。這種熵主要有兩個用途:
用於雜湊隨機化的雜湊種子
用於偽隨機數字生成器的種子
我們在啟動時執行雜湊隨機化操作,並接受每個特定 Worker 具有固定雜湊種子的成本。Python 沒有在啟動之後允許取代雜湊種子的機制。
對於虛擬亂數生成器 (PRNG),我們採取以下方法:
部署時:
使用固定的「毒種」為 PRNG 植入種子,然後記錄 PRNG 狀態。
將所有呼叫 PRNG 的 API 取代為一個覆蓋層,該覆蓋層會因使用者錯誤而導致部署失敗。
執行使用者程式碼的頂層範圍。
擷取快照。
執行時:
確認 PRNG 狀態未變更。若發生變更,表示我們忘記了某些方法的覆疊。部署失敗,且發生內部錯誤。
還原快照之後,為隨機數生成器重新植入種子,然後再執行任何處理程式。
這樣,我們可確保在 Workers 執行時可以使用 PRNG,但會阻止 Worker 在初始化和預快照期間使用它們。
在 WebAssembly 上建立記憶體快照時,會出現其他難題:我們要儲存的記憶體快照僅包含 WebAssembly 線性記憶體,但 Pyodide WebAssembly 執行個體的完整狀態並未包含在線性記憶體中。
在這個記憶體之外有兩個資料表。
一個資料表儲存函數指標的值。傳統電腦使用「馮·諾伊曼」架構,這意味著程式碼與資料存在於相同的記憶體空間中,因此,呼叫函數指標就是跳轉到某個記憶體位址。WebAssembly 具有「哈佛架構」,其中程式碼位於獨立的位址空間中。這是實現 WebAssembly 大部分安全性保證的關鍵,尤其說明了 WebAssembly 為何不需要位址空間配置隨機化。在 WebAssembly 中,函數指標是指向函數指標表的索引。
第二個資料表包含所有從 Python 參照的 JavaScript 物件。JavaScript 物件不能直接儲存到記憶體中,因為 JavaScript 虛擬機禁止直接取得指向 JavaScript 物件的指標。取而代之的是,這些物件會儲存到一個資料表中,並在 WebAssembly 中表示為資料表的索引。
我們需要確保這兩個資料表在還原快照之後,處於與我們擷取快照時完全相同的狀態。
函數指標資料表在 WebAssembly 執行個體初始化時始終處於相同的狀態。我們載入動態函式庫(例如 numpy 這類的原生 Python 套件)時,動態載入程式會更新此資料表。
若要處理動態載入:
拍攝快照時,我們會套用修補程式至載入器,以記錄動態函式庫的載入順序、記憶體中每個函式庫的中繼資料位址,以及重新配置的函式指標表基底位址。
還原快照時,我們會以相同的順序重新載入動態程式庫,並使用修補過的記憶體配置器,將中繼資料放置在相同的位置。我們確認,函數指標表的目前大小與我們為動態程式庫記錄的函數指標表基底相符。
所有這些都確保在還原快照之後,每個函式指標都具有與拍攝快照時相同的含義。
為了處理 JavaScript 參考,我們實作了一個相當有限的系統。如果 JavaScript 物件可透過一系列屬性存取項從 globalThis 存取,則我們會記錄這些屬性存取項,並在還原快照時重播這些屬性存取項。如果存在任何無法透過此方式存取的 JavaScript 物件參考,則我們將無法部署 Worker。這足以處理所有具有 Pyodide 支援的現有 Python 套件,這些套件會執行頂層匯入,例如:
from js import fetch
Python Workers 效能策略的另一項重要特徵是分片。此處有非常詳細的實作說明。簡而言之,我們現在會將請求路由到現有的 Worker 執行個體,而在此之前,我們可能會選擇啟動新的執行個體。
實際上首先會為 Python Workers 啟用分片,並證明其是很好的測試平台。相較於 JavaScript,Python 的冷啟動成本遠高於 JavaScript,因此確保將請求路由到已在執行的隔離環境至關重要。
這才剛剛開始。我們有很多計畫來改善 Python Workers:
若要瞭解有關 Python Workers 的詳細資訊,請參閱此處提供的文件。如需獲得協助,請務必加入我們的 Discord。