Cloudflare D1
Produção
SQLite
Performance
Limites

D1 in der Produktion: Leistung, Grenzen und was sich nicht alleine skalieren lässt

Die D1-Entwicklung fühlt sich schnell und reibungslos an. Die Produktion weist spezifische Grenzen auf, die architektonische Entscheidungen erfordern, die in der Schnellstartanleitung nicht erwähnt werden.

D1 in der Produktion: Leistung, Grenzen und was sich nicht alleine skalieren lässt

Die Entwicklungserfahrung mit D1 ist wirklich gut: ein lokales SQLite, das wrangler dev automatisch erstellt, Abfragen, die in wenigen Millisekunden antworten, kein zu konfigurierender Server, keine zu verwaltende Verbindungszeichenfolge. Diese Nullreibung in der Entwicklung führt tendenziell zu der Illusion, dass sich die Bank in der Produktion genauso verhalten wird. Das wird es nicht. Die Obergrenze von 2 GB pro Bank, die sich summierenden Unteranforderungen, die Kosten für von UPDATE geschriebene Zeilen und die Durchsetzung von Fremdschlüsseln, die eine manuelle Zustimmung pro Sitzung erfordern, sind die vier Grenzwerte, die in der Produktion auftreten und in der Schnellstartanleitung nie erwähnt werden.

Die 2-GB-Obergrenze, die niemand plant

D1 legt eine Grenze von 2 GB pro Datenbank fest. Diese Zahl ist fest – es besteht keine Möglichkeit, sie für eine bestimmte Bank durch den Kauf zusätzlicher Kapazität zu erhöhen. Der kostenpflichtige Plan ermöglicht bis zu 10 D1-Banken, was theoretisch insgesamt 20 GB bedeutet, die auf separate Instanzen verteilt werden.

Für eine einfache CRUD-Anwendung mit bescheidenem Datenvolumen reichen 2 GB für Jahre. Bei Anwendungen mit Protokollen, Ereignisverlauf, Benutzerdaten-Uploads oder Tabellen, die mit der Nutzung wachsen, wird das Limit früher als erwartet angezeigt. Das Problem ist nicht das Erreichen von 2 GB selbst – es ist der Moment, in dem Sie merken, dass es ankommen wird, mit Daten in Produktion und ohne definierte Partitionierungsstrategie.

Der sauberste Weg für Anwendungen mit vorhersehbarem Wachstum besteht darin, die Daten von Anfang an nach Domänen zu partitionieren: eine Datenbank für aktive Transaktionsdaten, eine andere für den Verlauf, eine andere für Protokolle. Eine SaaS-Anwendung kann nach Mandantenbereichen partitionieren – Mandanten 1 bis 1000 in Bank A, 1001 bis 2000 in Bank B. Der Worker entscheidet anhand der Mandanten-ID, auf welche Bank er zugreift, ohne dass der Benutzer es merkt. Diese Architektur muss durchdacht werden, bevor die ersten Daten eintreffen, da die Umgestaltung der Partitionierung mit einer Produktionsdatenbank nahe der Grenze ein heikler Vorgang ist.

Die versteckten Kosten von Unteranfragen

Jede in D1 ausgeführte Abfrage zählt als Unteranforderung im Budget des Workers. Das Limit für Worker beträgt 1000 Unteranfragen pro Aufruf. Dies scheint weitläufig, bis Sie herausfinden, was eine einzelne HTTP-Anfrage bewirkt: Authentifizieren des Tokens (1 Abfrage), Laden des Benutzers (1 Abfrage), Überprüfen von Berechtigungen (1 Abfrage), Abrufen der Ressourcenliste (1 Abfrage) und so weiter. Zehn Abfragen an einem Endpunkt sind üblich.

Das N+1-Problem transformiert diese Zahl schnell. Ein Endpunkt, der 50 Bestellungen auflistet und dann die Artikel aus jeder Bestellung einzeln abruft, führt 1 + 50 = 51 Abfragen aus. Kombiniert mit 5 KV-Lesevorgängen für den Cache und 3 R2-Zugriffen für Metadaten verwendet diese einzelne Anfrage 59 Unteranfragen. Immer noch im Rahmen, aber mit einem kleinen Spielraum für komplexere Endpunkte.

db.batch() löst N+1, ohne die Datenstruktur zu ändern: Sie gruppieren mehrere Abfragen in einem einzigen Aufruf und sie werden alle in einer einzigen Unteranfrage ausgeführt. Das Ergebnis kommt als Array mit einem Element pro Abfrage zurück. Für das Muster „Bestellungen und Artikel“ reduziert db.batch() mit den dynamisch erstellten Abfragen 51 Unteranfragen auf 2 – eine Abfrage für die Bestellungen, eine mit IN für alle Artikel gleichzeitig.

Schreibverstärkung: Was 1 US-Dollar pro Million geschriebener Zeilen wirklich bedeutet

Das D1-Abrechnungsmodell für Schreibvorgänge erfolgt pro betroffener Leitung, nicht pro Vorgang. Ein UPDATE, der 5.000 Zeilen ändert, kostet 5.000 Schreibvorgänge, unabhängig davon, ob es sich um einen einzelnen Anruf bei der Bank handelt. Bei 1 US-Dollar pro Million geschriebener Zeilen kostet dieses UPDATE 0,005 US-Dollar pro Ausführung.

Diese Zahl scheint gering zu sein, aber Batch-Vorgänge haben einen kumulativen Effekt. Ein täglicher Job, der den Status von 100.000 Datensätzen im Rahmen der nächtlichen Verarbeitung aktualisiert, kostet 0,10 US-Dollar pro Lauf, allein für diesen Job 3 US-Dollar pro Monat. Multipliziert mit mehreren ähnlichen Jobs können die Kosten für Schreibvorgänge leicht die Kosten für Lesevorgänge übersteigen.

Das kostenlose Kontingent hat ein Limit von 100.000 geschriebenen Zeilen pro Tag. Ein einzelner Batch-Aktualisierungsvorgang kann dieses gesamte Limit verbrauchen. Das bedeutet, dass die kostenlose Stufe nicht mit Verarbeitungspipelines kompatibel ist, die Massenaktualisierungen durchführen – für jede Arbeitslast mit Massenschreibvorgängen ist der kostenpflichtige Plan die einzige Möglichkeit.

Muster, die die Schreibkosten senken: Nur Anhängen statt Aktualisieren (Einfügen eines neuen Statusdatensatzes statt Aktualisieren des vorhandenen), periodische Komprimierung statt kontinuierlicher Aktualisierungen und weniger häufige Verarbeitung in größeren Stapeln anstelle granularer und häufiger Aktualisierungen.

FOREIGN KEYS und PRAGMA: der Trick pro Sitzung

SQLite erzwingt standardmäßig keine Fremdschlüssel. Dieses Verhalten wird von D1 ohne Änderung übernommen. Wenn Sie FOREIGN KEY (user_id) REFERENCES users(id) im Schema definieren und eine Zeile mit einem user_id einfügen, die in Tabelle users nicht vorhanden ist, akzeptiert D1 das INSERT ohne Fehler – es sei denn, Sie haben PRAGMA foreign_keys = ON in dieser Sitzung ausgeführt.

Das entscheidende Detail liegt „in dieser Sitzung“. PRAGMA bleibt zwischen Verbindungen nicht bestehen. Jeder Worker-Aufruf, der eine Fremdschlüsselerzwingung erfordert, muss PRAGMA als ersten Vorgang ausführen. Wenn Ihr Code die Datenbank über einen Helfer initialisiert, fügen Sie dort PRAGMA hinzu – und dokumentieren Sie dies, da es irgendwann der Aufmerksamkeit von jemandem im Team entgeht.

Die Kosten dafür, dies nicht zu tun, sind unbedeutend: Sie sammeln verwaiste Datensätze ohne Fehler im Protokoll an. Um das Problem zu entdecken, müssen Sie manuell nach fehlerhaften Referenzen suchen. Um es zu beheben, müssen Sie entscheiden, ob die ungültigen Datensätze gelöscht oder die fehlenden übergeordneten Datensätze erstellt werden sollen. Wenn die Menge der beschädigten Daten groß ist, wird die Korrektur zu einer heiklen Migration in die Produktion.

Ein Startup-Helfer, der PRAGMA immer vor jeder anderen Operation ausführt, ist die einfachste mögliche Investition gegen dieses Problem. Die Abfrage PRAGMA foreign_keys = ON dauert weniger als eine Millisekunde. Die Zeit zum Debuggen ungültiger Daten in der Produktion ist erheblich länger.

Lesen Sie auch