Cloudflare
Durable Objects
Estado
Concorrência
Serverless

Cloudflare Durable Objects : un état cohérent à la périphérie – ce qui change vraiment

Ce que les objets durables changent réellement dans le modèle d'état de Cloudflare, comment l'exécution en série garantit la cohérence et ce que cela coûte réellement.

Cloudflare Durable Objects : un état cohérent à la périphérie – ce qui change vraiment

Les travailleurs sont apatrides par conception. Chaque requête arrive dans un isolat propre, sans mémoire de ce qui s'est passé auparavant, sans état partagé avec d'autres instances exécutées en parallèle. Cet isolement est exactement ce qui vous permet de faire évoluer des millions de requêtes sans coordination entre les instances. Les objets durables rompent intentionnellement ce contrat – et comprendre pourquoi et ce qui change exactement détermine si vous les utiliserez correctement ou si vous en souffrirez.

Qu'est-ce qu'un objet durable ?

Un Objet Durable est une classe JavaScript avec un stockage persistant et un détail d'exécution qui change tout : les requêtes arrivent en série. Il n'y a pas de concurrence au sein d'une instance. Pendant que la méthode fetch() d'un DO traite une requête, toutes les autres qui arrivent au même DO sont mises en file d'attente à l'extérieur.

Cela élimine toute une catégorie de bugs résultant de l’accès simultané à l’état partagé. Il n'y a pas de condition de concurrence critique au sein d'un DO. Si vous incrémentez un compteur, conservez la valeur et répondez, aucune autre requête ne peut être entrelacée entre ces opérations. La séquence est garantie par la plateforme.

Chaque instance DO existe dans un seul PoP Cloudflare, le point de présence le plus proche de la première requête qui l'a créée. Les demandes ultérieures adressées au même DO sont acheminées vers ce PoP spécifique, quel que soit l'endroit où se trouve le client. Si votre instance a été créée à Francfort et qu'un client de São Paulo lui fait une demande, la demande est transmise à Francfort. Cela a de réelles implications en matière de latence pour les cas d'utilisation géographiquement répartis, et il est important de le savoir avant de concevoir.

Pourquoi KV et D1 ne résolvent pas le même problème

La comparaison naturelle est avec KV et D1, les autres options de persistance de la plateforme. La différence n’est pas une question de commodité, mais une question de modèle de cohérence.

KV est finalement cohérent. L'écriture sur un PoP se propage aux autres avec un délai mesurable, généralement compris entre quelques millisecondes et une minute. Les lectures dans différents PoP peuvent voir différentes versions de la même valeur. Pour le cache de lecture, pour les paramètres qui changent rarement, KV fonctionne parfaitement. Pour toute opération qui nécessite "lire, calculer, écrire" avec la garantie qu'aucune autre écriture n'a eu lieu entre les deux, cela ne fonctionne pas.

D1 avec transactions résout l’atomicité pour les lectures et les écritures, mais introduit une latence inter-régions pour les écritures. Toutes les écritures sont envoyées à la région principale de la banque, qui peut se trouver dans un PoP différent de celui de votre collaborateur. Pour de nombreuses applications, cela est acceptable. Pour un état qui doit être modifié avec une faible latence et une fréquence élevée, ou pour une coordination en temps réel entre clients, le coût de latence de chaque écriture dans une région principale modifie le problème.

Un DO conserve son état en mémoire et persiste de manière atomique via l'API de stockage. L'opération await this.ctx.storage.put('count', this.count) est linéarisable : toute requête ultérieure au même DO verra la valeur qui a été écrite, sans exception. Cette garantie, combinée à l'exécution en série, permet de créer des compteurs atomiques, des verrous distribués, des journaux d'événements ordonnés et une coordination entre clients simultanés sans la complexité liée à la mise en œuvre du contrôle de concurrence dans l'application.

Comment l'exécution en série affecte le débit

L’exécution en série est à la fois la garantie et le goulot d’étranglement. Un DO qui traite chaque requête en 5 ms peut gérer environ 200 requêtes par seconde. Si votre application achemine 500 requêtes/s vers le même DO, 300 d'entre elles sont mises en file d'attente et ajoutent de la latence.

Ce plafond existe par conception. Si un seul DO devient un goulot d'étranglement, la solution est le sharding : au lieu d'un ID fixe pour une ressource, distribuez-la par suffixe numérique. Pour une ressource identifiée par userId, la stratégie user-{userId}-shard-{userId.charCodeAt(0) % 10} répartit la charge entre dix instances, chacune avec son propre plafond de débit. Le choix de la partition à utiliser doit être déterministe afin que les lectures et écritures du même client atteignent toujours la même instance.

La mise en veille prolongée modifie le modèle de coût consistant à maintenir de nombreuses instances actives. Lorsqu'un DO n'a aucune requête en attente, il se met automatiquement en veille prolongée — aucun coût de calcul pendant la mise en veille prolongée. Le réveil de l’hibernation prend moins d’une milliseconde. Pour les applications comportant de nombreux DO épars (un par utilisateur, un par salle, un par session), le coût de calcul réel est proportionnel à l'utilisation active, et non au nombre total d'instances.

Ce que le niveau de prix exige de vous

Les objets durables nécessitent le plan Workers Paid, avec un minimum de 5 $/mois. À partir de là, le coût comporte trois éléments : les requêtes (0,15 $/million, dont 1 million gratuit/mois), le calcul en Go-secondes (12,50 $/million de Go-secondes, avec 400 000 gratuits/mois) et le stockage (0,20 $/Go-mois, avec 1 Go gratuit).

La seconde Go est la partie déroutante. Un DO exécuté avec 128 Mo de mémoire pendant 1 seconde consomme 0,125 Go-seconde. Avec 400 000 Go-secondes gratuites par mois, cela équivaut à 3,2 millions de secondes d'exécution avec une empreinte minimale. Une requête moyenne de 10 ms à 128 Mo consomme 0,00125 Go-seconde, de sorte que le niveau gratuit couvre environ 320 millions de requêtes de mémoire minimales de 10 ms. Si votre DO effectue des calculs lourds, maintient un état important en mémoire ou traite de nombreuses requêtes par seconde, la consommation en Go-seconde augmente proportionnellement.

Autre élément de coût : l'API d'alarme. DO peut planifier l'exécution d'une méthode alarm() à une date ultérieure, même si elle hiberne entre les deux. Le coût est de 0,15 $/million d'appels d'alarme, soit le même tableau que les demandes normales.

Quand choisir des objets durables

La question qui détermine si DO est le bon outil est simple : l’état que vous devez gérer nécessite-t-il un accès sérialisé de la part de clients concurrents ? Si oui, FAIRE. Sinon, il existe une option plus simple et moins chère.

Cas dans lesquels DO résout quelque chose que rien d'autre sur la plateforme ne résout avec la même garantie : compteurs atomiques avec fréquence d'écriture élevée, coordination de présence en temps réel (qui est connecté à une pièce), files d'attente d'événements classées par arrivée, verrous distribués avec timeout via l'API d'alarme et sessions d'édition collaboratives où plusieurs clients modifient le même document.

Ce qui n'est pas un cas d'utilisation DO : stockage de données relationnelles avec des requêtes ad hoc (D1 est bien mieux adapté), mise en cache de lecture avec des écritures occasionnelles (KV est moins cher et distribué mondialement), stockage de fichiers (R2) et tout état qui correspond naturellement à une seule écriture sans lectures simultanées qui dépendent de l'état précédent.

Que surveiller en production

Deux indicateurs apparaissent au début des problèmes avec les DO : l'augmentation de la latence de la file d'attente (un signe qu'un DO est devenu un goulot d'étranglement et a besoin d'être fragmenté) et la consommation de Go-secondes dépassant les attentes (un signe que les DO maintiennent leur état en mémoire plus longtemps que nécessaire, ou sont maintenus actifs avec un travail inutile).

L'API de stockage a un détail opérationnel qui surprend : list() renvoie par défaut un maximum de 128 entrées par appel. Pour les ensembles de données plus volumineux, l'itération doit utiliser le curseur. Ignorer cela en développement et le découvrir en production avec mille clés a un réel coût en requêtes et en temps de réponse.

La décision d'utiliser DO ou non dépend moins des performances que de la garantie de cohérence requise par votre application. Comprendre cette exigence avant de choisir l'outil évite une migration coûteuse plus tard.

A lire aussi

-Le modèle de programmation Objets Durables : ce qui ne ressemble à rien de ce que vous avez jamais utilisé