Cloudflare Workers
D1
KV
R2
Bindings

Arbeiter + D1 + KV + R2: Erstellen von Bindungen im selben Dienst

Die Zusammensetzbarkeit von Bindungen in einem einzelnen Handler unterscheidet Workers von einem einfachen Proxy – aber die Beschränkung der Unteranforderungen verursacht die Kosten für jeden Vorgang.

Arbeiter + D1 + KV + R2: Erstellen von Bindungen im selben Dienst

Das Wertversprechen von Workers wird deutlich, wenn man sieht, was in einem einzelnen Handler möglich ist: einen Datensatz von D1 mit SQL abrufen, einen Eintrag in KV prüfen, ein Objekt von R2 abrufen, einen internen Dienst über Service Binding aufrufen und asynchrone Arbeit in eine Warteschlange einreihen – alles innerhalb desselben Aufrufs, wobei jede Bindung als Eigenschaft von env eingefügt und mit einer Codezeile verfügbar ist. Kein separates SDK zum Instanziieren, keine Netzwerkkonfiguration zum Verwalten, keine Anmeldeinformationen, die in Umgebungsvariablen herumschwirren. wrangler.toml deklariert die Bindungen, die Laufzeit liefert sie. Das Problem besteht darin, dass jeder dieser Vorgänge eine Unteranforderung des Aufrufbudgets verbraucht und die Rechnung früher erscheint, als es scheint.

Das Bindungsmodell und was es löst

Durch Bindungen verbindet die Workers-Laufzeit ihren Code mit den Funktionen der Plattform, ohne Anmeldeinformationen oder Netzwerkkonfiguration preiszugeben. Wenn Sie [[d1_databases]] in wrangler.toml mit einem database_id deklarieren, fügt die Laufzeit ein Objekt mit der D1-Schnittstelle in env.DB ein. Wenn Sie [[kv_namespaces]] mit einem id deklarieren, fügt die Laufzeit die KV-Schnittstelle in env.CACHE ein. Es gibt kein [Authentifizierungs-32-Token, keine Endpunkt-URL, kein SDK eines Drittanbieters – die Bindung ist ein direkter Aufruf in die Cloudflare-Infrastruktur.

Dies hat Konsequenzen für Sicherheit und Betrieb. Ein kompromittierter Worker hat keine Anmeldeinformationen zum Herausfiltern – er kann nur die für ihn deklarierten Bindungen mit den Berechtigungen betreiben, über die diese Bindungen verfügen. Für einen Worker, der nur aus dem KV lesen muss, deklarieren Sie die Bindung als schreibgeschützt und der Worker kann buchstäblich nicht schreiben, unabhängig davon, was der Code zu tun versucht. Diese Trennung der Funktionen ist stärker als eine Berechtigungsprüfung im Code.

Für Staging und Produktion verfügt jede Umgebung über eigene Bindungs-IDs in wrangler.toml. Der Code ändert sich nicht – env.DB bleibt env.DB – aber beim Staging verweist er auf eine andere D1-Bank, einen anderen KV-Namespace, einen anderen R2-Bucket. Es besteht kein Risiko, dass der Staging-Code Produktionsdaten berührt, da die Bindungen physisch getrennt sind.

Das Unteranforderungsbudget und wie es verbraucht wird

Der kostenlose Plan umfasst 50 Unteranfragen pro Aufruf. Der kostenpflichtige Plan hat 1000. Jeder Vorgang, der isolierte Ergebnisse hinterlässt, zählt: fetch() für jede URL, env.KV.get(), env.KV.put(), env.DB.prepare().run(), env.BUCKET.get(), env.QUEUE.send(), env.SERVICE.fetch(). Ein Anruf bei env.DB.batch() mit 5 Abfragen zählt als 1 Unteranfrage – dieses Detail ist entscheidend.

Ein typischer Handler einer API, die ein angereichertes Benutzerprofil zurückgibt: Sucht nach dem Benutzer in D1 anhand der ID (1), sucht nach Präferenzen in KV (2), sucht nach einem Profilfoto in R2 (3), ruft einen Autorisierungsdienst über Service Binding auf (4). Insgesamt: 4 Unteranfragen auf der glücklichen Route. Skaliert auf 1000 gleichzeitige Benutzer und liegt immer noch innerhalb der Grenze – 4 Unteranfragen pro Aufruf sind kein Problem.

Das Problem tritt bei Schleifen auf. Ein Handler, der eine Liste von Elementen verarbeitet und pro Element eine D1-Abfrage durchführt – das klassische N+1 – explodiert schnell. Fünfzig Artikel mit jeweils einer Abfrage erreichen beim ersten Aufruf das Gratistarif-Limit. Mit 200 Artikeln im kostenpflichtigen Plan liegt es immer noch innerhalb von 1000, aber die kumulierte Latenz von 200 aufeinanderfolgenden D1-Abfragen wird Hunderte von Millisekunden betragen. Der Fehler, wenn das Limit erreicht ist, kommt als Netzwerkfehler in der Unteranforderung, die überschritten wurde – keine eindeutige Nachricht im Hauptteil der Antwort an den Client, nur eine Ausnahme im Ende.

Konkrete Optimierungen durch Bindung

Für D1 ist db.batch() die wichtigste Optimierung. Anstatt prepare().run() nacheinander für mehrere Abfragen auszuführen, übergeben Sie ein Array von Anweisungen an batch() und erhalten ein Array von Ergebnissen – zum Preis einer gesamten Unteranfrage. Bei Abfragen, die nicht voneinander abhängen (z. B. Suche nach Konfigurationen unterschiedlichen Typs), eliminiert die Stapelverarbeitung gleichzeitig die sequentielle Latenz und den Aufwand für Unteranfragen.

Parallele D1-Abfragen – Promise.all([db.query1, db.query2]) – zählen weiterhin als separate Unteranfragen, werden jedoch parallel ausgeführt und die Latenz wird durch die langsamste Anfrage und nicht durch die Summe bestimmt. Verwenden Sie Promise.all(), wenn die Ergebnisse unabhängig sind und Sie die Charge aus einem anderen Grund nicht benötigen. Verwenden Sie batch(), wenn Sie die Unteranforderungskosten konsolidieren möchten.

Für KV ist der Modulcache das wertvollste Muster. KV hat eine Netzwerklatenz – bestenfalls einige Millisekunden – und sich selten ändernde Konfigurationsdaten müssen nicht bei jedem Aufruf erneut gelesen werden. Ein Modul kann eine Variable im globalen Gültigkeitsbereich deklarieren:

let cachedConfig = null; export default { async fetch(request, env) { if (!cachedConfig) { cachedConfig = JSON.parse(await env.CONFIG.get('app-config')); } // usa cachedConfig } }

Solange sich das Isolat im PoP befindet, wird der Wert bei nachfolgenden Aufrufen wiederverwendet, ohne dass eine Unteranforderung verschwendet wird. Das Risiko besteht in der Veraltung – wenn sich die Konfiguration ändert, verwendet das zwischengespeicherte Isolat immer noch die alte Version, bis es verworfen wird. Für Daten, die nahezu in Echtzeit vorliegen müssen, ist KV mit niedrigem cacheTtl oder Aufruf-Neulesen besser geeignet. Für Feature-Flags und Infrastrukturkonfigurationen, die sich selten ändern, ist der Modulcache effizient.

Für R2 unterstützt die Bindung Bereichsanfragen – env.BUCKET.get(key, { range: { offset, length } }) – wodurch Sie nur den Teil eines Objekts abrufen können, den Sie benötigen, und nicht die gesamte Datei. Bei großen Dateien, bei denen Sie einen Header, eingebettete Metadaten oder die ersten paar Zeilen einer CSV-Datei benötigen, spart die Bereichsanfrage Speicherplatz und Übertragungszeit. Die Antwort ist ein ReadableStream, der ohne Pufferung direkt an den Client weitergeleitet werden kann.

Zusammensetzung mehrerer Bindungen in derselben Anfrage

Die wahre Stärke entfaltet sich, wenn Sie mehrere Datentypen benötigen, um eine Antwort zu erstellen. Eine Produkt-API, die eine Suchseite zurückgibt: SQL-Abfrage in D1 nach IDs, die dem Filter entsprechen, paralleles Lesen in KV für Preise (die sich häufig ändern und in KV für die Leseleistung leben) und vorsignierte URL von R2 für das Hauptbild jedes Produkts.

Diese Zusammensetzung funktioniert auf natürliche Weise – env.DB, env.PRICES, env.ASSETS sind alle im selben Handler verfügbar. Das Design, das nicht gut funktioniert, besteht darin, dies nacheinander für jedes Element in einer Liste durchzuführen. Die richtige Version: Abfrage D1 gibt 20 IDs zurück, Promise.all() für die 20 KV-Lesevorgänge parallel (20 Unteranfragen, aber parallel), Promise.all() für die 20 R2-URLs (20 weitere Unteranfragen). Gesamt: 41 Unteranfragen (1 D1 + 20 KV + 20 R2), innerhalb des bezahlten Limits von 1000, wobei die Latenz durch den langsamsten der 40 parallelen Abrufe bestimmt wird, nicht durch die Summe von 41.

Überwachung der Verwendung von Unteranforderungen

Die Laufzeit macht die Anzahl der Unteranforderungen pro Aufruf nicht direkt im Ende verfügbar. Der Ansatz besteht darin, zu instrumentieren: Erstellen Sie einen einfachen Wrapper, der bei jedem Bindungsvorgang einen Zähler erhöht und die Gesamtsumme am Ende des Handlers protokolliert. Mit dieser über Logpush exportierten Zahl können Sie erkennen, wann eine Bereitstellung einen erhöhten Verbrauch aufweist – bevor ein Grenzfall erreicht wird, der den Grenzwert in der Produktion überschreitet.

Ein Handler, der 500 Unteranforderungen verwendet, obwohl er bei korrekter Stapelverarbeitung und Parallelität 50 verwenden könnte, verschwendet unnötig Latenz und Budget. Die Neugestaltung dieses Handlers nach einem Vorfall in der Produktion ist kostspieliger, als den Verbrauch von Anfang an zu messen und zu korrigieren, solange N noch klein ist.

Lesen Sie auch