このコンテンツは自動機械翻訳サービスによる翻訳版であり、皆さまの便宜のために提供しています。原本の英語版と異なる誤り、省略、解釈の微妙な違いが含まれる場合があります。ご不明な点がある場合は、英語版原本をご確認ください。
注:この投稿は、AWS Lambdaに関する追加の詳細で更新されました。
昨年、当社はPython Workersの基本的なサポートを発表しました。これにより、Python開発者は単一のコマンドでPythonを地球全体に配信し、Workersプラットフォームを活用できるようになります。
それ以来、私たちはWorkersでのPython体験を素晴らしいものにすることに懸命に取り組んできました。当社は、パッケージサポートをプラットフォームにもたらすことに注力し、超高速コールドスタートとPythonネイティブの開発者体験を実現しました。
これは、パッケージがPython Workerに組み込まれる方法の変更を意味します。限られた組み込みパッケージのセットを提供する代わりに、Python Workersを支えるWebAssemblyランタイムであるPyodideがサポートする任意のパッケージをサポートするようになりました。これには、すべての純粋なPythonパッケージと、動的ライブラリに依存する多くのパッケージが含まれます。また、パッケージのインストールを簡単にするために、uv 関連のツールも構築しました。
また、コールドスタート時間を短縮するために、専用のメモリスナップショットを実装しました。これらのスナップショットは、他のサーバーレスPythonベンダーよりも大幅な速度向上をもたらします。一般的なパッケージを使用したコールドスタートテストでは、Cloudflare Workersは、SnapStartを使用しないAWS Lambdaよりも2.4倍以上、Googleクラウド Runよりも3倍速く起動します。
このブログ記事では、Python Workersの独自性とは何かを説明し、上述の成功をどのように実現したかについての技術的な詳細をご紹介します。しかし、まず、Workersやサーバーレスプラットフォームをよく知らない人、特にPythonのバックグラウンドを持つ人のために、Workersを使いたいと思う理由を共有しましょう。
Workersの魔法の一部として、シンプルなコードと簡単なグローバルデプロイメントがあります。まず、2分以内の高速コールドスタートで、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
ほんの少しのコードとPythonでのデプロイで、125か国、330か所に広がるCloudflareのエッジネットワーク全体にアプリケーションをデプロイできました。インフラストラクチャやスケーリングを心配する必要はありません。
また、多くの場合、Python Workersは完全に無料です。当社の無料枠では、1日あたりリクエスト100,000件、呼び出し1回あたりのCPU時間10ミリ秒を保証しています。詳しくは、当社のドキュメントで価格設定をご覧ください。
その他の例については、GitHubのリポジトリをご覧ください。Python Workersの詳細について、さらにお読みください。
では、Python Workersでは何ができるのでしょうか。
Workerを手に入れたら、どんなことも可能になります。コードを書くから、決定を決められます。Python WorkerはHTTPリクエストを受信し、パブリックインターネット上の任意のサーバーにリクエストを行うことができます。
Cronトリガーを設定できるため、Workerが定期的に実行されます。さらに、より複雑な要件がある場合は、Python Workers用Workflowsを使用することも、Durable Objectsを使用して長期間実行されているWebSocketサーバーやクライアントでも使用することができます。
以下では、Python Workersを使用してできることの追加の例を紹介します。
Workersのようなサーバーレスプラットフォームは、必要な時だけコードを実行することで、コストを節約します。つまり、Workerがリクエストを受信しない場合、Workerはシャットダウンされる可能性があり、新しいリクエストが入ってくると、再起動する必要があります。これは通常、「コールドスタート」と呼ばれるリソースのオーバーヘッドが発生します。エンドユーザーの遅延を最小限に抑えるためには、これらをできるだけ短くすることが重要です。
標準的なPythonでは、ランタイムの起動にコストがかかるため、Python Workersの初期実装は、ランタイムの起動を高速化することに重点を置きました。しかし、これだけでは不十分であることにすぐに気付きました。Pythonランタイムがすばやく起動したとしても、現実世界のシナリオでは、通常、初期起動はパッケージからのモジュールの読み込みを含むのが通常で、残念ながら、Pythonでは多くの人気のあるパッケージは読み込みに数秒かかることがあります。
私たちは、パッケージが読み込まれているかどうかに関係なく、コールドスタートを高速化することにしました。
現実的なコールドスタートパフォーマンスを測定するために、一般的なパッケージをインポートするベンチマークと、ベアPythonランタイムを使用して「hello world」を実行するベンチマークを設定しました。Standard Lambdaはランタイムだけを素早く起動することができますが、パッケージをインポートする必要があると、コールドスタート時間が長くなります。パッケージでのより速いコールドスタートを最適化するには、LambdaでSnapStartを使用することができます(間もなくリンクされたベンチマークに追加されます)。これには、スナップショットの保存コストと、復元のたびに追加のコストが発生します。Python Workersは、すべてのPython Workerに無料でメモリスナップショットを自動的に適用します。
3つの一般的なパッケージ(httpx、fastapi、pydantic)を読み込む際の平均コールドスタート時間は以下の通りです。
プラットフォーム | 平均コールドスタート(秒) |
Cloudflare Python Workers | 1.027 |
AWS Lambda(SnapStartなし) | 2.502 |
Google クラウド Run | 3.069 |
この場合、Cloudflare Python Workersは、SnapStartを使用しないAWS Lambdaよりも2.4倍、Google Cloud Runよりも3倍速いコールドスタートを実現しています。この低いコールドスタート数は、メモリスナップショットを使用することで実現しました。後のセクションではその方法について説明します。
これらのベンチマークは定期的に実行しています。当社のテスト方法に関する最新のデータや詳細情報は、こちらからご覧いただけます。
当社はアーキテクチャ的に他のプラットフォームと異なります。つまり、Workersは分離ベースです。そのため、私たちの目標は高いものであり、ゼロコールドスタートの未来に向けて計画を立てています。
多様なパッケージエコシステムは、Pythonを素晴らしいものにしている理由の大部分です。そのため、Workersでパッケージをできるだけ簡単に使用できるように懸命に取り組んできました。
私たちは、既存のPythonツールと連携することが、優れた開発体験を実現するための最善の方法であると考えました。そこで私たちは、高速で成熟し、Pythonエコシステムで勢いを増しているuvパッケージとプロジェクトマネージャーを選びました。
Cloudflareは、pywranglerと呼ばれるuvを中心とした独自のツールを構築しました。このツールは、基本的に次のアクションを実行します。
Pywranglerはuvに呼び出して、Python Workersと互換性のある方法で依存関係をインストールし、ローカル開発やWorkersをデプロイする際にはwranglerに呼び出します。
実際は、pywrangler devとpywrangler deployを実行して、Workerをローカルでテストし、デプロイするだけでいいのです。
pywrangler型を使って、Wrangler設定で定義されたすべてのバインディングに対し、型ヒントを生成することができます。これらの型ヒントは、Pylanceまたはmypyの最新バージョンで動作します。
型を生成するには、wrangler typesを使用してTypescriptの型ヒントを作成し、次にTypescriptコンパイラを使用して、型の抽象構文ツリーを生成します。最後に、JSオブジェクトに反復子フィールドがあるかどうかなど、TypeScriptのヒントを使用して、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インタプリタはこの関数を呼び出さず、ブートストラップができません。また、多くのパッケージは起動時にエントロピーを必要とします。このエントロピーには、大きく分けて2つの目的があります。
ハッシュランダム化用のハッシュシード
擬似乱数発生器用シード
ハッシュのランダム化は起動時に行い、各Workerが固定のハッシュシードを持つコストを受け入れます。Pythonには、起動後にハッシュシードを交換できる仕組みがありません。
擬似乱数発生器(PRNG)に対して、当社は次のアプローチをとっています。
デプロイ時:
PRNGに固定の「ポイズニングシード」をシードし、PRNGの状態を記録します。
PRNGを呼び出すすべてのAPIを、ユーザーエラーでデプロイに失敗するオーバーレイに置き換える。
ユーザーコードのトップレベルスコープを実行する。
スナップショットをキャプチャする。
実行時:
PRNGの状態が変更されていないことを保証します。変更していただければ、何らかのメソッドのためのオーバーレイを忘れていました。内部エラーにより、デプロイに失敗する。
スナップショットを復元したら、ハンドラーを実行する前に乱数ジェネレーターを再シードします。
これにより、Workerの実行中にPRNGを使用できるようにしますが、初期化とプリスナップショットの実行中はWorkersがPRNGを使用しないようにできます。
メモリスナップショットとWebAssemblyの状態
WebAssembly上でメモリスナップショットを作成するときに、さらなる困難が発生します。保存しているメモリスナップショットは、WebAssemblyリニアメモリのみで構成されていますが、Pyodide WebAssemblyインスタンスの完全な状態はリニアメモリに含まれていません。
このメモリの外側には2つのテーブルがあります。
1つのテーブルが関数ポインタの値を保持します。従来のコンピュータは「Von Neumann」アーキテクチャを使用しています。つまり、コードはデータと同じメモリ空間に存在するため、関数ポインタを呼び出すことは、何らかのメモリアドレスにジャンプすることになります。WebAssemblyには、コードが別のアドレス空間に存在する「ハーバードアーキテクチャ」があります。これは、WebAssemblyのセキュリティ保証のほとんどにおいて重要なことであり、特にWebAssemblyがアドレス空間レイアウトのランダム化を必要としない理由です。WebAssemblyの関数ポインタは、関数ポインタテーブルへのインデックスです。
2番目のテーブルは、Pythonから参照されるすべてのJavaScriptオブジェクトを保持します。JavaScript仮想マシンはJavaScriptオブジェクトへのポインタを直接取得することを禁止しているため、JavaScriptオブジェクトをメモリに直接保存することはできません。その代わり、それらはテーブルに格納され、WebAssemblyでテーブルのインデックスとして表現されます。
スナップショットを復元した後のこれらのテーブルの両方が、スナップショットを取得したときとまったく同じ状態になっていることを確認する必要があります。
WebAssemblyインスタンスが初期化されるとき、関数ポインタテーブルは常に同じ状態にあり、動的ライブラリ(numpyのようなネイティブPythonパッケージ)を読み込むときに動的ローダーによって更新されます。
動的読み込みを処理するには:
スナップショットを取得するときに、ローダーにパッチを適用して、動的ライブラリの読み込み順、各ライブラリのメタデータが割り当てられるメモリ内のアドレス、および再配置用の関数ポインタテーブルのベースアドレスを記録します。
スナップショットを復元するとき、同じ順序で動的ライブラリを再ロードし、パッチを適用したメモリアロケーターを使用して、メタデータを同じ場所に配置します。関数ポインタテーブルの現在のサイズが、動的ライブラリに対して記録した関数ポインタテーブルベースと一致することを保証します。
これにより、スナップショットを復元した後も、各関数のポインターが、スナップショット取得時と同じ意味を持つことが保証されます。
JavaScriptの参照を処理するために、かなり限定的なシステムを実装しました。JavaScriptオブジェクトがグローバルからアクセス可能な場合は、一連のプロパティアクセスによって、それらのプロパティアクセスを記録し、スナップショットを復元する時に再生します。この方法でアクセスできないJavaScriptオブジェクトへの参照が存在する場合、Workerのデプロイに失敗します。これは、Pyodideサポートを持つすべての既存のPythonパッケージを扱うのに十分です。Pyodideは、次のようなトップレベルのインポートを行います。
from js import fetch
Python Workersのパフォーマンス戦略におけるもう1つの重要な特徴は、シャーディングです。実装の経緯については、こちらに詳しく説明されています。簡単に言うと、以前は新しいインスタンスを開始することを選択していたかもしれませんが、既存のWorkerインスタンスにリクエストをルーティングするようになりました。
シャーディングは実際にPython Workersで最初に有効になり、そのための素晴らしいテスト層であることが証明されました。PythonではJavaScriptよりもコールドスタートがはるかにコスト高になるため、リクエストが既に実行中の分離にルーティングされるようにすることが特に重要です。
これは始まりにすぎません。Python Workersをより良くするための計画はたくさんあります:
Python Workersの詳細については、こちらからドキュメントをご覧ください。サポートが必要な場合は、ぜひDiscordに参加してください。