Ab sofort können Sie in der Open Beta-Version Cloudflare-Worker in Python schreiben.
Dieser neue Unterstützung für Python unterscheidet sich von der Art und Weise, wie Workers in der Vergangenheit andere Sprachen neben JavaScript unterstützt hat – in diesem Fall haben wir eine Python-Implementierung direkt in workerd, der Open-Source-Laufzeitumgebung von Workers, integriert. Alle Bindungen, einschließlich der Bindungen an Vectorize, Workers AI, R2 und Durable Objects, werden vom ersten Tag an unterstützt. Python-Worker können einen Teil beliebter Python-Pakete importieren, darunter FastAPI, Langchain und Numpy. Es sind keine zusätzlichen Build-Schritte oder externe Toolchains erforderlich.
Dafür mussten wir die Grenzen aller unserer Systeme verschieben, von der Laufzeitumgebung selbst über unser Bereitstellungssystem bis hin zu den Inhalten des Worker-Pakets, das über unser Netzwerk veröffentlicht wird. Sie können die Begleitdokumentation dazu lesen und noch heute anfangen, die Lösung zu nutzen.
Dieser Beitrag soll einen Einblick in den internen Lebenszyklus eines Python-Worker geben. Außerdem wollen wir von unseren Erkenntnissen bei der Arbeit an diesem Angebot berichten und unsere weiteren Pläne vorstellen.
Mehr als die bloße Kompilierung in WebAssembly
Cloudflare-Worker unterstützen WebAssembly seit 2018: Jeder Worker ist eine V8-Isolierung, die von derselben JavaScript-Engine unterstützt wird wie der Chrome-Webbrowser. Im Prinzip ist es seit Jahren möglich, Worker in jeder Sprache einschließlich Python zu schreiben, solange zunächst eine Kompilierung für WebAssembly oder JavaScript stattfindet.
Nur weil etwas möglich ist, heißt es nicht, dass es sich in der Praxis ohne Weiteres umsetzen lässt. Und nur weil „hello world“ funktioniert, bedeutet das noch lange nicht, dass Sie auch zuverlässig eine Anwendung erstellen können. Zur Programmierung einer vollständigen Applikation ist die Unterstützung eines Ökosystems von Paketen erforderlich, mit denen Entwickler zu arbeiten gewohnt sind. Bei der Unterstützung einer Programmiersprache ist es nicht damit getan, dass auf einer Plattform gezeigt wird, wie sich Quellcode mit externen Toolchains kompilieren lässt.
Python-Worker unterscheiden sich von dem, was wir bisher angeboten haben. Das Produkt befindet sich noch in der Beta-Phase und damit in einem Frühstadium. Wir glauben aber, dass sich damit demonstrieren lässt, wie die Bereitstellung erstklassiger Unterstützung für Programmiersprachen jenseits von JavaScript für Worker aussehen kann.
Der Lebenszyklus eines Python-Worker
Da Pyodide jetzt in workerd integriert ist, kann ein Worker wie folgt geschrieben werden:
… mit einer wrangler.toml-Datei, die auf eine .py-Datei verweist:
from js import Response
async def on_fetch(request, env):
return Response.new("Hello world!")
… und wenn Sie npx wrangler@latest dev ausführen, tut die Workers-Laufzeitumgebung Folgendes:
name = "hello-world-python-worker"
main = "src/entry.py"
compatibility_date = "2024-03-18"
compatibility_flags = ["python_workers"]
Die erforderliche Pyodide-Version anhand Ihres Kompatibilitätsdatums ermitteln
Eine Isolierung für Ihren Worker erstellen und Pyodide automatisch injizieren
Ihren Python-Quellcode mit Pyodide bereitstellen
Dies alles geschieht im Hintergrund: Es sind weder eine weitere Toolchain noch zusätzliche Vorkompilierungsschritte erforderlich. Die Python-Laufzeitumgebung wird Ihnen bereitgestellt und reproduziert die Funktionsweise von Workern, die in JavaScript geschrieben wurden.
Ein in die Workers-Laufzeitumgebung integrierter Python-Interpreter
Ebenso wie JavaScript über viele Engines verfügt, gibt es in Python viele Implementierungen, die Python-Quellcode ausführen können. CPython ist die Referenzimplementierung von Python. Wenn Sie Python schon einmal verwendet haben, dann mit ziemlicher Sicherheit diese Implementierung, die im Allgemeinen einfach als „Python“ bezeichnet wird.
Pyodide ist eine Portierung von CPython für WebAssembly. Damit wird Python-Quellcode interpretiert, ohne dass er vorher in ein anderes Format vorkompiliert werden muss. Sie läuft in einem Webbrowser: Hier eine REPL dazu. Die Lösung entspricht dem CPython, das Python-Entwickler kennen und erwarten, und stellt den größten Teil der Python-Standardbibliothek bereit. Sie bietet ein Foreign Function Interface (FFI) für JavaScript, über das Sie die JavaScript-API direkt von Python aus aufrufen können (mehr dazu erfahren Sie weiter unten). Außerdem stehen beliebte quelloffene Pakete zur Verfügung und reine Python-Pakete lassen sich direkt aus PyPI importieren.
Pyodide erschien uns als perfekte Lösung für Workers. Das Produkt ist so konzipiert, dass der Core-Interpreter und jedes native Python-Modul als separate WebAssembly-Module erstellt und zur Laufzeit dynamisch verlinkt werden. Dadurch kann der Code-Footprint für diese Module von allen Workern, die auf demselben Rechner laufen, gemeinsam genutzt werden – anstatt dass jeder Worker über eine eigene Kopie verfügen muss. Das ist unerlässlich, damit WebAssembly in der Workers-Umgebung gut funktioniert, in der wir oft Tausende von Workern pro Rechner ausführen. Die Worker müssen dieselbe Programmiersprache verwenden, um ihren Laufzeitcode-Footprint weitergeben zu können. Der Einsatz von Tausenden von Workern auf jedem Rechner ermöglicht es uns, jede Anwendung an jedem Standort zu einem angemessenen Preis bereitzustellen.
Genau wie bei JavaScript-Workern stellen wir auch bei Python-Workern die Laufzeitumgebung für Sie bereit:
Pyodide ist derzeit eine Ausnahme: Die meisten auf WebAssembly ausgerichteten Sprachen unterstützen noch keine dynamische Verlinkung, sodass letztlich jede Anwendung eine eigene Kopie ihrer Sprachlaufzeitumgebung mitbringt. Wir hoffen, dass in Zukunft mehr Sprachen die dynamische Verlinkung unterstützen werden, damit wir sie effektiver in Workers einbringen können.
So funktioniert Pyodide
Pyodide führt Python-Quellcode in WebAssembly aus. Dabei handelt es sich um eine von der Host-Laufzeitumgebung getrennte Sandbox-Umgebung. Anders als bei der Ausführung von nativem Quellcode müssen alle Vorgänge, die nicht der reinen Rechenleistung zuzurechnen sind (wie das Lesen von Dateien), von einer Laufzeitumgebung bereitgestellt und dann vom WebAssembly-Modul importiert werden.
LLVM bietet drei Target Triple für WebAssembly:
wasm32-unknown-unknown: Dieses Backend bietet keine C-Standardbibliothek und keine Systemaufrufschnittstelle; um dieses Backend zu unterstützen und Importe zu verwenden, die wir in der Laufzeitumgebung selbst definieren würden, müssten wir jeden System- oder Bibliotheksaufruf manuell umschreiben.
wasm32-wasi: WASI ist eine standardisierte Systemschnittstelle und definiert einen Standardsatz von Importen, die in WASI-Laufzeitumgebungen wie wasmtime implementiert werden.
wasm32-unknown-emscripten: Emscripten definiert wie WASI die Importe, die ein WebAssembly-Programm ausführen muss, gibt aber auch eine begleitende JavaScript-Bibliothek aus, die diese importierten Funktionen implementiert.
Pyodide verwendet Emscripten und bietet drei Dinge:
Eine mit Emscripten kompilierte Distribution des CPython-Interpreters
Ein Foreign Function Interface (FFI) zwischen Python und JavaScript
Eine Reihe von Python-Paketen von Drittanbietern, die mit dem Compiler von Emscripten zu WebAssembly kompiliert wurden
Von diesen Targets unterstützt derzeit nur Emscripten eine dynamische Verlinkung, die – wie bereits erwähnt – für die Bereitstellung einer von allen Isolierungen gemeinsam genutzten Sprachlaufzeitumgebung für Python unerlässlich ist. Emscripten stellt Implementierungen von dlopen und dlsym bereit, die mithilfe der zugehörigen JavaScript-Bibliothek die Tabelle des WebAssembly-Programms so modifizieren, dass zusätzliche WebAssembly-kompilierte Module zur Laufzeit verknüpft werden. WASI bietet noch keine Unterstützung der von CPython verwendeten dynamischen Verlinkungs-Abstraktionen dlpen/dlsym.
Pyodide und die Magie von Foreign Function Interfaces (FFI)
Vielleicht ist Ihnen aufgefallen, dass wir in unserem „Hello World“-Python-Worker Response aus dem js-Modul importieren:
Was ist der Grund dafür?
from js import Response
async def on_fetch(request, env):
return Response.new("Hello world!")
Die meisten Worker sind in JavaScript geschrieben, und der größte Teil unseres technischen Aufwands für die Workers-Laufzeitumgebung fließt in die Verbesserung von JavaScript-Workern. Wird eine zweite Sprache hinzugefügt, besteht das Risiko, dass sie niemals Feature-Parität mit der ersten Sprache erreicht und immer stiefmütterlich behandelt wird. Das Foreign Function Interface (FFI) von Pyodide ist entscheidend, um dies zu vermeiden. Es bietet von Python aus Zugriff auf alle JavaScript-Funktionen. Dies kann vom Autor des Worker direkt genutzt werden und wird auch verwendet, damit Pakete wie FastAPI und Langchain sofort funktionieren, wie wir in diesem Beitrag noch zeigen werden.
Ein FFI ist ein System zum Aufrufen von Funktionen in einer Sprache, die in einer anderen Sprache implementiert sind. In den meisten Fällen wird ein FFI durch eine übergeordnete Sprache definiert, um Funktionen aufzurufen, die in einer Systemsprache, häufig C, implementiert sind. Das ctypes-Modul von Python ist ein solches System. Diese Art von FFI sind aufgrund der Natur von C-API oft schwer zu verwenden.
Bei dem FFI von Pyodide handelt es sich um eine Schnittstelle zwischen Python und JavaScript, zwei objektorientierten Programmiersprachen mit vielen strukturellen Gemeinsamkeiten. Unveränderliche Typen wie Zeichenfolgen und Zahlen werden bei der Weitergabe von einer Sprache in eine andere transparent übersetzt. Alle veränderbaren Objekte sind in einen geeigneten Proxy verpackt.
Wird ein JavaScript-Objekt an Python übergeben, ermittelt Pyodide, welches JavaScript-Protokoll das Objekt unterstützt, und erstellt dynamisch eine entsprechende Python-Klasse, die das passende Python-Protokoll implementiert. Wenn beispielsweise das JavaScript-Objekt das JavaScript-Iterationsprotokoll unterstützt, dann unterstützt der Proxy das Python-Iterationsprotokoll. Ist das JavaScript-Objekt ein Promise-Objekt oder ein anderes thenable, dann ist das Python-Objekt ein awaitable.
Der Lebenszyklus einer Anfrage an einen Python-Worker nutzt das FFI von Pyodide, wobei das eingehende JavaScript-Anfrage-Objekt in ein JsProxy-Objekt verpackt wird, das in Ihrem Python-Quellcode zugänglich ist. Anschließend wird der vom Handler des Python-Worker zurückgegebene Wert in ein JavaScript-Antwort-Objekt konvertiert, das an den Client zurückgesendet werden kann:
from js import JSON
js_array = JSON.parse("[1,2,3]")
for entry in js_array:
print(entry)
Warum dynamische Verlinkungen unverzichtbar sind und statische Verlinkungen nicht ausreichen
Python ist mit einem C-FFI ausgestattet und viele Python-Pakete nutzen dieses FFI, um native Bibliotheken zu importieren. Diese Bibliotheken sind in der Regel in C geschrieben, sodass sie zunächst zu WebAssembly kompiliert werden müssen, um mit der Workers-Laufzeitumgebung zu funktionieren. Wie oben dargelegt, wird Pyodide mit Emscripten erstellt, das das C-FFI von Python überschreibt. Jedes Mal, wenn ein Paket versucht, eine native Bibliothek zu laden, wird es stattdessen aus einem von der Workers-Laufzeitumgebung bereitgestellten WebAssembly-Modul geladen. Möglich macht dies eine dynamische Verlinkung, dank derer wir das C-FFI von Python überschreiben können, sodass Pyodide viele Python-Pakete unterstützen kann, bei denen Abhängigkeiten zu nativen Bibliotheken bestehen.
Dynamische Verlinkung bedeutet Zahlung nach Bedarf, statische Verlinkung dagegen Zahlung im Voraus – wenn Quellcode statisch mit Ihrer Binärdatei verlinkt ist, muss er zum Ausführen der Binärdatei im Voraus geladen werden, auch wenn er nie verwendet wird.
Dank dynamischer Verlinkung kann die Workers-Laufzeitumgebung die zugrunde liegenden WebAssembly-Module von Paketen für verschiedene, auf demselben Rechner laufende Worker zu nutzen.
Wir werden hinsichtlich der Funktionsweise der dynamischen Verlinkung in Emscripten nicht zu sehr ins Detail gehen. Der wichtigste Aspekt ist aber, dass die Emscripten-Laufzeitumgebung WebAssembly-Module aus einer in JavaScript bereitgestellten Dateisystem-Abstraktion abruft. Für jeden Worker generieren wir zur Laufzeit ein Dateisystem, dessen Struktur einer Python-Distribution ähnelt, bei der die Abhängigkeiten des Worker installiert sind, deren zugrunde liegende Dateien aber von den Workern gemeinsam genutzt werden. Dies ermöglicht die gemeinsame Verwendung von Python- und WebAssembly-Dateien durch mehrere Worker, die dieselbe Abhängigkeit importieren. Aktuell können diese Dateien zwischen verschiedenen Workern ausgetauscht werden, sie werden aber in jede neue Isolierung kopiert. Wir glauben, dass wir noch weiter gehen können, indem wir Copy-on-Write-Verfahren einsetzen, damit die zugrunde liegende Ressource von vielen Workern genutzt werden kann.
Unterstützung von Server- und Client-Bibliotheken
Python verfügt über eine Vielzahl beliebter HTTP-Client-Bibliotheken, darunter httpx, urllib3 und requests. Leider ist keine davon bei Pyodide sofort einsatzbereit. Die entsprechende Unterstützung gehörte zu den Dauerbrennern unter den Nutzeranfragen für das Pyodide-Projekt. Die HTTP-Client-Bibliotheken von Python arbeiten alle mit unformatierten Sockets, und das Browser-Sicherheitsmodell und CORS lassen dies nicht zu. Deshalb musste auf anderem Weg dafür gesorgt werden, dass sie in der Workers-Laufzeitumgebung funktionieren.
Asynchrone Client-Bibliotheken
Für Bibliotheken wie aiohttp und httpx, die Anfragen asynchron stellen können, kann die Fetch-API zum Stellen von Anfragen verwendet werden. Dafür wird die Bibliothek gepatcht und angewiesen, die Fetch-API von JavaScript zu verwenden, wodurch die Vorteile des FFI von Pyodide genutzt werden. Der httpx-Patch ist letzten Endes nicht allzu kompliziert und umfasst weniger als 100 Zeilen Quellcode. Noch weiter vereinfacht sieht er folgendermaßen aus:
Synchrone Client-Bibliotheken
from js import Headers, Request, fetch
def py_request_to_js_request(py_request):
js_headers = Headers.new(py_request.headers)
return Request.new(py_request.url, method=py_request.method, headers=js_headers)
def js_response_to_py_response(js_response):
... # omitted
async def do_request(py_request):
js_request = py_request_to_js_request(py_request)
js_response = await fetch(js_request)
py_response = js_response_to_py_response(js_response)
return py_response
Eine weitere Herausforderung bei der Unterstützung von HTTP-Client-Bibliotheken in Python besteht darin, dass viele Python-API synchron sind. Für diese Bibliotheken kann die Fetch-API also nicht direkt verwendet werden, da sie asynchron ist.
Glücklicherweise hat Joe Marshall kürzlich einen Beitrag zu urllib3 geleistet, der die Unterstützung von Pyodide in Webbrowsern folgendermaßen ermöglicht:
Prüfen, ob das Blockieren mit
Atomics.wait()
möglich ista. Wenn ja, einen Fetch-Worker-Thread startenb. Den Abrufvorgang an den Worker-Thread delegieren und die Antwort in einen SharedArrayBuffer serialisierenc. Im Python-Thread Atomics.wait verwenden, um die Antwort im SharedArrayBuffer zu blockierenFalls
Atomics.wait()
nicht funktioniert, kann auf eine synchrone XMLHttpRequest zurückgegriffen werden
Trotzdem unterstützen Cloudflare-Worker aktuell weder worker-Threads noch synchrone XMLHttpRequest, sodass keiner dieser Ansätze bei Python-Workern funktionieren wird. Wir unterstützen derzeit zwar keine synchronen Anfragen, es gibt aber trotzdem eine Lösung …
Stack-Switching bei WebAssembly
Es gibt einen Ansatz, mit dem wir synchrone Anfragen unterstützen können. Bei WebAssembly existiert ein Stufe 3-Vorschlag für die Unterstützung von Stack-Switching und v8 verfügt über eine entsprechende Implementierung. Pyodide-Mitwirkende arbeiten seit September 2022 an einer Unterstützung von Stack-Switching in Pyodide und stehen kurz vor dem Ziel.
Im Zuge dieser Unterstützung stellt Pyodide eine Funktion namens run_sync
zur Verfügung, die den Abschluss eines „awaitable“ blockieren kann:
FastAPI und das Gateway-Interface für asynchrone Server von Python
from pyodide.ffi import run_sync
def sync_fetch(py_request):
js_request = py_request_to_js_request(py_request)
js_response = run_sync(fetch(js_request))
return js_response_to_py_response(js_response)
FastAPI ist eine der beliebtesten Bibliotheken zur Definition von Python-Servern. FastAPI-Anwendungen verwenden ein Protokoll namens Asynchronous Server Gateway Interface (ASGI). Das bedeutet, dass FastAPI selbst niemals von einem Socket liest oder in ihn schreibt. Eine ASGI-Anwendung erwartet, mit einem ASGI-Server verbunden zu sein, und zwar normalerweise mit uvicorn. Der ASGI-Server verwaltet alle Raw Sockets für die Anwendung.
Praktisch bedeutet dies für uns, dass FastAPI in Cloudflare Workers ohne Patch oder Änderungen an FastAPI selbst funktioniert. Wir müssen lediglich uvicorn durch einen geeigneten ASGI-Server ersetzen, der innerhalb eines Worker ausgeführt werden kann. Unsere erste Implementierung befindet sich hier in dem von uns unterhaltenen Fork von Pyodide. Wir hoffen, dass wir einen umfassenderen Funktionsumfang und eine Testabdeckung hinzufügen und diese Implementierung dann in Pyodide einbinden können.
Sie können das selbst ausprobieren, indem Sie cloudflare/python-workers-examples klonen und npx wrangler@latest dev
im Verzeichnis des FastAPI-Beispiels ausführen.
Importieren von Python-Paketen
Python-Worker unterstützen eine Untergruppe von Python-Paketen, die direkt von Pyodide bereitgestellt werden, darunter numpy, httpx, FastAPI und Langchain. Die Kompatibilität mit der Pyodide-Laufzeitumgebung wird sichergestellt, indem Paketversionen an Pyodide-Versionen angeheftet werden. Gleichzeitig kann Pyodide interne Implementierungen patchen, wie oben am Fall von httpx bereits gesehen.
Um ein Paket zu importieren, können Sie es einfach ohne Versionsnummer in Ihre requirements.txt
-Datei einfügen. Eine spezielle Version des Pakets wird direkt von Pyodide bereitgestellt. Aktuell können Sie Pakete bei der lokalen Entwicklung verwenden und in den kommenden Wochen werden Sie Worker einsetzen können, die Abhängigkeiten in einer requirements.txt
-Datei definieren. Im weiteren Verlauf dieses Beitrags werden wir auf die Überlegungen eingehen, die wir im Hinblick auf die Verwaltung neuer Versionen von Pyodide und Paketen anstellen.
Wir unterhalten unseren eigenen Pyodide-Fork. Das ermöglicht es uns, Patches speziell für die Workers-Laufzeitumgebung bereitzustellen und unsere Unterstützung von Paketen in Python-Workern schnell zu erweitern. Gleichzeitig wollen wir unsere Änderungen auf Pyodide rückübertragen, damit das gesamte Entwickler-Ökosystem davon profitiert.
Python-Pakete sind allerdings oft groß und speicherintensiv und können beim Importieren eine Menge Arbeit machen. Wie lässt sich also sicherstellen, dass die benötigten Pakete ohne lange Kaltstartzeiten bezogen werden können?
Schnellere Kaltstarts mit Speicher-Snapshots
In dem Beispiel zu Beginn dieses Beitrags haben wir bei der lokalen Entwicklung erwähnt, dass Sie Pyodide in Ihren Worker einspeisen können. Pyodide selbst hat einen Umfang von 6,4 MB und Python-Pakete können auch recht groß sein.
Wenn wir Pyodide einfach in Ihren Worker einfügen und bei Cloudflare hochladen würden, wäre das ein ziemlich großer Worker, der in eine neue Isolierung geladen werden müsste. Das bedeutet, dass Kaltstarts langsam erfolgen würden. Auf einem schnellen Computer mit einer guten Netzwerkverbindung benötigt Pyodide etwa zwei Sekunden für die Initialisierung in einem Webbrowser, eine Sekunde Netzwerkzeit und eine Sekunde CPU-Zeit. Es wäre nicht akzeptabel, Pyodide jedes Mal zu initialisieren, wenn Sie Ihren Quellcode für jede Isolierung aktualisieren, in der Ihr Worker im Cloudflare-Netzwerk läuft.
Wenn Sie stattdessen npx wrangler@latest deploy ausführen, geschieht Folgendes:
Wrangler lädt Ihren Python-Quellcode und Ihre
requirements.txt
-Datei in die Workers API hochWir senden Ihren Python-Quellcode und Ihre
requirements.txt
-Datei zur Validierung an die Workers-LaufzeitumgebungWir erstellen eine neue Isolierung für Ihren Worker und injizieren automatisch Pyodide sowie alle Pakete, die Sie in Ihrer
requirements.txt
-Datei angegeben habenWir durchsuchen den Quellcode des Worker nach Import-Anweisungen, führen diese aus und erstellen dann einen Snapshot des linearen WebAssembly-Speichers des Worker. Faktisch führen wir die teure Arbeit des Importierens von Paketen nicht zur Laufzeit, sondern zum Implementierungszeitpunkt durch
Wir implementieren diesen Snapshot zusammen mit dem Python-Quellcode Ihres Worker im Cloudflare-Netzwerk
Genau wie bei einem JavaScript-Worker führen wir den Top-Level-Bereich des Worker aus
Wenn eine Anfrage bei Ihrem Worker eingeht, laden wir diesen Snapshot und verwenden ihn, um Ihren Worker in einer Isolierung zu booten und so teure Initialisierungszeit zu vermeiden:
So nimmt ein Kaltstart eines einfachen Python-Worker weniger als eine Sekunde in Anspruch. Doch damit geben wir uns noch nicht zufrieden. Wir sind zuversichtlich, dass wir dies noch deutlich weiter senken können. Wie wir das anstellen wollen? Durch die Wiederverwendung von Speicher-Snapshots.
Wiederverwendung von Speicher-Snapshots
Wenn Sie einen Python-Worker hochladen, generieren wir einen einzigen Speicher-Snapshot der Top-Level-Importe des Worker, einschließlich Pyodide und etwaiger Abhängigkeiten. Dieser Snapshot ist spezifisch für Ihren Worker. Er kann nicht weitergegeben werden, obwohl der größte Teil seines Inhalts mit dem anderer Python-Worker übereinstimmt.
Stattdessen können wir einen einzigen, gemeinsamen Snapshot im Voraus erstellen und ihn in einer Gruppe von „warmgelaufenen“ Isolierungen vorladen. In diesen Isolierungen wäre die Pyodide-Laufzeitumgebung bereits geladen und einsatzbereit, sodass ein Python-Worker genau wie ein JavaScript-Worker funktionieren würde. Der zugrunde liegende Interpreter und die zugrunde liegende Ausführungsumgebung werden in beiden Fällen von der Workers-Laufzeitumgebung bereitgestellt und sind bei Bedarf unverzüglich verfügbar. Der einzige Unterschied besteht darin, dass bei Python der Interpreter in WebAssembly innerhalb des Worker ausgeführt wird.
Snapshots sind ein gängiges Muster bei Laufzeit- und Ausführungsumgebungen. Node.js verwendet V8-Snapshots, um die Startzeit zu verkürzen. Sie können Snapshots von Firecracker-microVM erstellen und die Ausführung in einem anderen Prozess fortsetzen. Hier können wir noch viel mehr tun – nicht nur für Python-Worker, sondern auch für in JavaScript geschriebene Worker: So lassen sich Snapshots von kompiliertem Quellcode aus dem Top-Level-Bereich und vom Zustand der Isolierung selbst erstellen. Worker sind so schnell und effizient, dass wir bisher keine Snapshots auf diese Weise erstellen mussten. Wir glauben jedoch, dass sich die Performance noch beträchtlich steigern lässt.
Dies ist unser größter Beitrag zur Reduzierung der Kaltstartzeiten im weiteren Verlauf des Jahres 2024.
Zukunftssichere Kompatibilität mit Pyodide-Versionen und Kompatibilitätsdatumsangaben
Wenn Sie einen Worker bei Cloudflare implementieren, erwarten Sie, dass er auf unbestimmte Zeit läuft, selbst wenn Sie ihn nie wieder aktualisieren. Bestimmte Worker, die 2018 implementiert wurden, funktionieren im Produktivbetrieb immer noch einwandfrei.
Wir erreichen dies durch die Verwendung von Kompatibilitätsdatumsangaben und Kompatibilitäts-Flags, die explizite Opt-in-Mechanismen für neues Verhalten und möglicherweise nicht abwärtskompatible Änderungen bieten, und zwar ohne Auswirkungen auf bestehende Worker.
Das Ganze klappt zum Teil, weil dabei die Funktionsweise des Internet und der Webbrowser übernommen wird. Sie veröffentlichen eine Webseite mit JavaScript und erwarten zu Recht, dass sie dauerhaft funktioniert. Webbrowser und Cloudflare-Worker haben gegenüber Entwicklern die gleiche Verpflichtung zur Stabilität.
Bei Python besteht jedoch eine Herausforderung: Sowohl für Pyodide als auch für CPython existieren verschiedene Versionen. Regelmäßig werden aktualisierte Versionen veröffentlicht, die teils tiefgreifende Änderungen umfassen. Zudem bietet Pyodide eine Reihe von integrierten Paketen mit jeweils einer angehefteten Versionsnummer. Daher stellt sich die Frage: Wie sollen wir Ihnen erlauben, Ihren Worker auf eine neuere Version von Pyodide zu aktualisieren?
Die Antwort lautet: mit Kompatibilitätsdatumsangaben und Kompatibilitäts-Flags.
Eine neue Version von Python wird jedes Jahr im August veröffentlicht und eine neue Version von Pyodide erscheint sechs Monate später. Wird eine neue Pyodide-Version veröffentlicht, nehmen wir diese in Workers auf, indem wir sie mit einem Kompatibilitäts-Flag versehen, der erst nach einem bestimmten Kompatibilitätsdatum aktiviert wird. So können wir kontinuierlich Updates bereitstellen, ohne zu riskieren, dass durch Änderungen Störungen verursacht werden. Das erlaubt es uns, die für JavaScript gemachten Zusagen auf Python auszuweiten.
Jede Python-Version hat ein Unterstützungszeitfenster von fünf Jahren. Sobald dieses Fenster für eine bestimmte Python-Version abgelaufen ist, werden keine neuen Sicherheits-Patches mehr angewendet, sodass man sich auf diese Version nicht mehr verlassen kann. Wir wollen dieses Risiko mindern und gleichzeitig unserer Verpflichtung zu Stabilität und langfristiger Unterstützung so weit wie möglich gerecht werden. Deshalb wird nach fünf Jahren jeder Python-Worker, der sich in einem Python-Release außerhalb des Unterstützungszeitfensters befindet, automatisch in die nächstälteste Python-Version verschoben. Da es sich bei Python um eine ausgereifte und stabile Sprache handelt, gehen wir davon aus, dass Ihr Python-Worker in den meisten Fällen problemlos weiterlaufen wird. Wir würden allerdings empfehlen, das Kompatibilitätsdatum Ihres Worker regelmäßig zu aktualisieren, damit er innerhalb des Unterstützungszeitfensters bleibt.
Wir beabsichtigen, mit demselben Opt-in-Mechanismus auch zwischen Python-Releases Aktualisierungen vorzunehmen und weitere Python-Pakete hinzuzufügen. Ein Kompatibilitäts-Flag ist eine Kombination aus der Python-Version und dem Veröffentlichungsdatum einer Reihe von Paketen, zum Beispiel python_3.17_packages_2025_03_01.
So funktionieren Bindungen in Python-Workern
Wie bereits erwähnt, stellt Pyodide ein Foreign Function Interface (FFI) für JavaScript bereit. Das bedeutet, dass JavaScript-Objekte, -Methoden, -Funktionen und mehr direkt in Python verwendet werden können.
Somit werden vom ersten Tag an alle API, die Bindungen zu anderen Cloudflare-Ressourcen herstellen, von Cloudflare-Workern unterstützt. Das von Handlern in Python-Workern bereitgestellte env-Objekt ist ein JavaScript-Objekt, für das Pyodide eine Proxy-API bietet. Darüber werden Typübersetzungen automatisch aus verschiedenen Sprachen übernommen.
Um beispielsweise von einem Python-Worker aus in einen KV-Namespace zu schreiben und von dort zu lesen, würden Sie Folgendes schreiben:
Das funktioniert auch für Web-API: Sehen Sie, wie „Response“ aus dem js-Modul importiert wird? Auf diese Weise lässt sich jeder beliebige allgemeine JavaScript-Quellcode importieren.
from js import Response
async def on_fetch(request, env):
await env.FOO.put("bar", "baz")
bar = await env.FOO.get("bar")
return Response.new(bar) # returns "baz"
Holen Sie dieses JavaScript aus meinem Python!
Sie lesen diesen Beitrag wahrscheinlich, weil Sie nicht in Javascript, sondern stattdessen in Python schreiben wollen. from js import Response
passt einfach nicht zu Python. Uns ist das bewusst und wir haben diese Herausforderung bereits bei einer anderen Sprache (Rust) gemeistert. Wir glauben, dass wir es für Python noch besser machen können.
Wir haben workers-rs im Jahr 2021 eingeführt, um das Schreiben von Workern in Rust zu ermöglichen. Für jede JavaScript-API in Workers haben wir zusammen mit Open-Source-Anbietern Bindungen geschrieben, die eine idiomatischere Rust-API offenlegen.
Dasselbe planen wir für Python-Worker, angefangen mit Bindungen für Workers AI und Vectorize. Doch während workers-rs die Verwendung und Aktualisierung externer Abhängigkeiten erfordert, werden die mit Python-Workern bereitgestellten API direkt in die Workers-Laufzeitumgebung integriert. Sie müssen einfach nur Ihr Kompatibilitätsdatum aktualisieren, um über die neuesten, am besten auf Python abgestimmten API zu verfügen.
Es geht jedoch um mehr als nur darum, die Bindung an Ressourcen auf Cloudflare Python-gerechter zu gestalten – es geht um die Kompatibilität mit dem Ökosystem.
Wir haben kürzlich workers-rs so angepasst, dass http-Typen verwendet werden. Das macht es leicht, axum zum Routing einzusetzen. Dasselbe streben wir jetzt auch für Python-Worker an. Beispielsweise bietet die Python-Standardbibliothek eine Raw Socket-API, von der viele Python-Pakete abhängen. Workers stellt bereits connect() bereit, eine JavaScript-API für die Arbeit mit unformatierten Sockets. Wir sehen die Chance, zumindest einen Teil der Socket-API der Python-Standardbibliothek in Workern zur Verfügung zu stellen, sodass eine breitere Palette von Python-Paketen auf Workern funktioniert und Patches seltener erforderlich sind.
Letztendlich hoffen wir, den Anstoß für die Erstellung einer standardisierten Serverless-API für Python zu geben. Diese sollte für jeden Python-Entwickler leicht zu verwenden sein und die gleichen Möglichkeiten wie JavaScript bieten.
Wir fangen mit Python-Workern gerade erst an
Echte Unterstützung für eine neue Programmiersprache zu bieten, bedeutet eine große Investition, die weit über das Funktionieren von „hello world“ hinausgeht. Die Entscheidung für Python ist sehr bewusst gefallen, schließlich handelt es sich dabei um die zweitbeliebteste Programmiersprache nach JavaScript. Es ist unser festes Ziel, die Performance weiter zu steigern und unsere Unterstützung für Python-Pakete zu erweitern.
Wir sind all denen, die an Pyodide arbeiten, und der Python-Community im Allgemeinen dankbar und würden uns freuen, von Ihnen zu hören. Schauen Sie doch einfach in dem Discord-Kanal von Cloudflare für Entwickler vorbei, der Python Workern gewidmet ist, oder diskutieren Sie auf Github darüber, was Sie sich als Nächstes von uns wünschen und welche Python-Pakete wir noch unterstützen sollen.