Cloudflare
Durable Objects
Estado
Concorrência
Serverless

Langlebige Cloudflare-Objekte: Konsistenter Zustand am Netzwerkrand – was sich wirklich ändert

Was langlebige Objekte tatsächlich im Zustandsmodell von Cloudflare verändern, wie die serielle Ausführung die Konsistenz gewährleistet und was sie tatsächlich kostet.

Langlebige Cloudflare-Objekte: Konsistenter Zustand am Netzwerkrand – was sich wirklich ändert

Arbeitnehmer sind von Natur aus staatenlos. Jede Anfrage kommt in einem sauberen Isolat an, ohne Erinnerung an das, was zuvor passiert ist, ohne dass der Status mit anderen parallel laufenden Instanzen geteilt wird. Genau diese Isolation ermöglicht es Ihnen, Millionen von Anfragen ohne Koordination zwischen Instanzen zu skalieren. Langlebige Gegenstände brechen diesen Vertrag absichtlich – und wenn Sie verstehen, warum und was genau sich ändert, entscheiden Sie darüber, ob Sie sie richtig verwenden oder darunter leiden.

Was ein langlebiges Objekt eigentlich ist

Ein dauerhaftes Objekt ist eine JavaScript-Klasse mit dauerhaftem Speicher und einem Ausführungsdetail, das alles verändert: Anforderungen kommen seriell an. Innerhalb einer Instanz gibt es keinen Wettbewerb. Während Methode fetch() eines DO eine Anfrage verarbeitet, werden alle anderen, die bei demselben DO ankommen, außerhalb in die Warteschlange gestellt.

Dadurch wird eine ganze Kategorie von Fehlern beseitigt, die durch den gleichzeitigen Zugriff auf den gemeinsamen Status entstehen. Innerhalb eines DO gibt es keine Rennbedingung. Wenn Sie einen Zähler erhöhen, den Wert beibehalten und antworten, können keine anderen Anforderungen zwischen diesen Vorgängen verschachtelt werden. Der Ablauf wird durch die Plattform garantiert.

Jede DO-Instanz existiert in einem einzelnen Cloudflare PoP – dem Point of Presence, der der ersten Anfrage, die sie erstellt hat, am nächsten liegt. Nachfolgende Anfragen an denselben DO werden an diesen spezifischen PoP weitergeleitet, unabhängig davon, wo sich der Client befindet. Wenn Ihre Instanz in Frankfurt erstellt wurde und ein Kunde aus São Paulo eine Anfrage an sie stellt, wird die Anfrage nach Frankfurt weitergeleitet. Dies hat erhebliche Auswirkungen auf die Latenz bei geografisch verteilten Anwendungsfällen und ist vor dem Entwurf wichtig zu wissen.

Warum KV und D1 nicht das gleiche Problem lösen

Der natürliche Vergleich erfolgt mit KV und D1, den anderen Persistenzoptionen auf der Plattform. Der Unterschied liegt nicht in der Bequemlichkeit, sondern im Konsistenzmodell.

Der KV ist letztendlich konsistent. Das Schreiben auf einen PoP breitet sich mit einer messbaren Verzögerung, typischerweise zwischen einigen Millisekunden und einer Minute, auf andere aus. Bei Messwerten in verschiedenen PoPs können unterschiedliche Versionen desselben Werts angezeigt werden. Für den Lesecache und für Einstellungen, die sich selten ändern, funktioniert KV einwandfrei. Für jede Operation, die „Lesen, Berechnen, Schreiben“ erfordert, mit der Garantie, dass dazwischen kein anderer Schreibvorgang stattgefunden hat – das funktioniert nicht.

D1 mit Transaktionen löst die Atomizität für Lese- und Schreibvorgänge auf, führt jedoch eine regionsübergreifende Latenz für Schreibvorgänge ein. Alle Schreibvorgänge gehen an die primäre Region der Bank, die sich möglicherweise in einem anderen PoP als Ihr Worker befindet. Für viele Anwendungen ist dies akzeptabel. Bei Zuständen, die mit geringer Latenz und hoher Frequenz geändert werden müssen, oder bei der Echtzeitkoordination zwischen Clients verändern die Latenzkosten jedes Schreibvorgangs in eine primäre Region das Problem.

Ein DO behält den Zustand im Speicher bei und bleibt atomar über die Speicher-API bestehen. Operation await this.ctx.storage.put('count', this.count) ist linearisierbar: Jede nachfolgende Anfrage an denselben DO sieht ausnahmslos den geschriebenen Wert. Diese Garantie ermöglicht in Kombination mit der seriellen Ausführung die Erstellung atomarer Zähler, verteilter Sperren, geordneter Ereignisprotokolle und die Koordination zwischen gleichzeitigen Clients, ohne dass die Implementierung der Parallelitätskontrolle in der Anwendung komplex ist.

Wie sich die serielle Ausführung auf den Durchsatz auswirkt

Die serielle Ausführung ist Garantie und Flaschenhals zugleich. Ein DO, der jede Anfrage in 5 ms verarbeitet, kann etwa 200 Anfragen pro Sekunde verarbeiten. Wenn Ihre Anwendung 500 Anforderungen/s an denselben DO weiterleitet, werden 300 davon in die Warteschlange gestellt und erhöhen die Latenz.

Diese Decke existiert absichtlich. Wenn ein einzelner DO zum Engpass wird, ist die Lösung Sharding: Anstelle einer festen ID für eine Ressource verteilen Sie sie anhand eines numerischen Suffixes. Für eine durch userId identifizierte Ressource verteilt die Strategie user-{userId}-shard-{userId.charCodeAt(0) % 10} die Last auf zehn Instanzen, jede mit ihrer eigenen Durchsatzobergrenze. Die Auswahl des zu verwendenden Shards muss deterministisch sein, damit Lese- und Schreibvorgänge vom selben Client immer dieselbe Instanz erreichen.

Durch den Ruhezustand ändert sich das Kostenmodell, viele Instanzen aktiv zu halten. Wenn für einen DO keine ausstehenden Anfragen vorliegen, wird er automatisch in den Ruhezustand versetzt – im Ruhezustand fallen keine Rechenkosten an. Das Aufwachen aus dem Ruhezustand dauert weniger als Millisekunden. Bei Anwendungen mit vielen spärlichen DOs (eine pro Benutzer, eine pro Raum, eine pro Sitzung) sind die tatsächlichen Rechenkosten proportional zur aktiven Nutzung und nicht zur Gesamtzahl der Instanzen.

Was die Preisstufe von Ihnen verlangt

Für langlebige Objekte ist der Workers Paid-Plan mit einem Mindestbetrag von 5 $/Monat erforderlich. Von da an setzen sich die Kosten aus drei Komponenten zusammen: Anfragen (0,15 $/Million, mit 1 Million kostenlos/Monat), Berechnung in GB-Sekunden (12,50 $/Million GB-Sekunden, mit 400.000 kostenlos/Monat) und Speicher (0,20 $/GB-Monat, mit 1 GB kostenlos).

Die GB-Sekunde ist der verwirrende Teil. Ein DO, der 1 Sekunde lang mit 128 MB Speicher ausgeführt wird, verbraucht 0,125 GB-Sekunden. Bei 400.000 kostenlosen GB-Sekunden pro Monat entspricht dies einer Ausführungszeit von 3,2 Millionen Sekunden auf minimalem Platzbedarf. Eine durchschnittliche 10-ms-Anfrage bei 128 MB verbraucht 0,00125 GB-Sekunden, sodass das kostenlose Kontingent etwa 320 Millionen 10-ms-Mindestspeicheranfragen abdeckt. Wenn Ihr DO umfangreiche Berechnungen durchführt, einen großen Zustand im Speicher beibehält oder viele Anfragen pro Sekunde verarbeitet, steigt der GB-Sekunden-Verbrauch proportional an.

Ein weiterer Kostenfaktor: Alarm-API. DO kann eine alarm()-Methode so planen, dass sie zu einem späteren Zeitpunkt ausgeführt wird – auch wenn sie dazwischen in den Ruhezustand wechselt. Die Kosten betragen 0,15 $/Million Alarmaufrufe, die gleiche Tabelle wie bei normalen Anfragen.

Wann sollte man sich für langlebige Objekte entscheiden?

Die Frage, die darüber entscheidet, ob DO das richtige Tool ist, ist einfach: Erfordert der Status, den Sie verwalten müssen, einen serialisierten Zugriff von konkurrierenden Clients? Wenn ja, tun Sie es. Wenn nicht, gibt es eine einfachere und günstigere Option.

Fälle, in denen DO etwas löst, das nichts anderes auf der Plattform mit der gleichen Garantie löst: atomare Zähler mit hoher Schreibfrequenz, Anwesenheitskoordination in Echtzeit (wer ist mit einem Raum verbunden), nach Ankunft geordnete Ereigniswarteschlangen, verteilte Sperren mit Zeitüberschreitung über die Alarm-API und kollaborative Bearbeitungssitzungen, bei denen mehrere Clients dasselbe Dokument ändern.

Was kein DO-Anwendungsfall ist: relationale Datenspeicherung mit Ad-hoc-Abfragen (D1 ist viel besser geeignet), Lese-Caching mit gelegentlichen Schreibvorgängen (KV ist günstiger und global verteilt), Dateispeicherung (R2) und jeder Zustand, der auf natürliche Weise einem einzelnen Schreibvorgang zugeordnet werden kann, ohne gleichzeitige Lesevorgänge, die vom vorherigen Zustand abhängen.

Was in der Produktion überwacht werden muss

Zwei Indikatoren, die früh bei Problemen mit DOs auftreten: zunehmende Warteschlangenlatenz (ein Zeichen dafür, dass ein DO zu einem Engpass geworden ist und Sharding benötigt) und ein über die Erwartungen hinaus ansteigender GB-Sekunden-Verbrauch (ein Zeichen dafür, dass DOs ihren Zustand länger als nötig im Speicher behalten oder mit unnötiger Arbeit aktiv gehalten werden).

Die Speicher-API verfügt über ein Betriebsdetail, das Sie überrascht: list() gibt standardmäßig maximal 128 Einträge pro Aufruf zurück. Bei größeren Datensätzen muss für die Iteration der Cursor verwendet werden. Wenn man dies in der Entwicklung ignoriert und es in der Produktion mit tausend Schlüsseln herausfindet, entstehen erhebliche Kosten für Anfragen und Reaktionszeit.

Bei der Entscheidung, DO zu verwenden oder nicht, kommt es weniger auf die Leistung als vielmehr darauf an, welche Konsistenzgarantie Ihre Anwendung erfordert. Das Verständnis dieser Anforderung vor der Auswahl des Tools erspart Ihnen später eine teure Migration.

Lesen Sie auch