2 points par GN⁺ 2024-10-15 | 1 commentaires | Partager sur WhatsApp
  • Les Durable Objects de Cloudflare passent d’un stockage clé/valeur à un stockage relationnel basé sur SQLite, avec une architecture où la logique applicative et les données s’exécutent sur le même hôte physique
  • La mise à l’échelle consiste à répartir les Durable Objects par unité logique d’état — document, utilisateur ou shard de base de données — plutôt qu’à agrandir un seul objet ; un objet unique reste limité à une seule machine et à un seul thread
  • Dans les systèmes où l’unité d’état est claire, comme la réservation de vols, chaque vol peut disposer de son propre Durable Object et de sa propre base SQLite, ce qui peut créer des milliers de nouvelles bases de données par jour et par compagnie aérienne
  • La durabilité repose sur le streaming des entrées WAL, le stockage en object storage et la réplication vers des datacenters proches ; les données sont regroupées par lots tous les 16 Mo ou toutes les 10 secondes, avec une restauration à un point dans le temps jusqu’à 30 jours
  • L’API JavaScript adopte une approche bloquante pour des opérations de persistance rapides sur un seul thread ; l’emplacement d’un Durable Object est fixe après sa création, mais un déplacement dynamique est prévu à l’avenir

La conception des Durable Objects passée à SQLite

  • Les Durable Objects de Cloudflare ont été mis à niveau depuis un stockage clé/valeur vers un système relationnel basé sur SQLite
  • L’idée centrale est de placer la logique applicative au même endroit que les données qu’elle manipule
    • Un Durable Object se compose de code exécuté sur le même hôte physique que la base de données SQLite qu’il utilise
    • Les lectures et écritures sont traitées sans aller-retour réseau, ce qui facilite la réduction de la latence
  • Pour le traitement à grande échelle, l’approche consiste à créer davantage d’objets plutôt qu’à augmenter le débit d’un seul objet
    • Un objet unique s’exécute sur un seul thread d’une seule machine, ce qui impose une limite intrinsèque au débit
    • Il est plus facile de passer à l’échelle lorsque des objets distincts prennent en charge différentes unités logiques d’état, comme des documents, des utilisateurs ou des shards de base de données
  • Dans un système de réservation de vols, chaque vol peut être associé à un Durable Object dédié et à sa propre base de données SQLite
    • Avec cette structure, des milliers de nouvelles bases de données peuvent être créées chaque jour pour chaque compagnie aérienne
  • Chaque Durable Object possède un nom unique, et le réseau de Cloudflare achemine les requêtes vers l’emplacement de cet objet au sein du réseau mondial

Durabilité assurée par streaming WAL et réplication

  • Le système basé sur les Durable Objects, inspiré par Litestream, streame en continu la séquence des entrées WAL de chaque objet vers un object storage
    • Les lots sont effectués tous les 16 Mo ou toutes les 10 secondes
    • En rejouant les transactions enregistrées, une restauration à un point dans le temps est possible jusqu’à 30 jours
  • Pour renforcer la durabilité dans la fenêtre de 10 secondes, les écritures sont transmises juste après le commit à 5 réplicas situés dans des datacenters distincts à proximité
    • La réponse d’écriture n’est validée que lorsque 3 d’entre eux ont confirmé
  • Le système sous-jacent des Durable Objects est le Storage Relay Service, qui fait tourner depuis plus d’un an le système D1 SQLite distinct de Cloudflare

API et comportement de l’emplacement des objets

  • L’API JavaScript utilise une approche bloquante, et non async
    • L’objectif de conception est de fournir des opérations de persistance rapides sur un seul thread
    • L’exemple de code utilise volontairement un schéma de requêtes N+1 : il récupère une liste de documents, puis interroge séparément le nom de l’auteur de chaque document
    • SQLite est présenté comme bien adapté à ce type de schéma
  • Actuellement, un Durable Object ne change pas d’emplacement après sa création
    • Par défaut, il est instancié dans un datacenter proche de l’endroit où la première requête get() a été effectuée
    • Pour le créer manuellement à un autre emplacement, on peut fournir le paramètre optionnel locationHint à get()
    • Le déplacement dynamique des Durable Objects existants est prévu pour l’avenir
  • where.durableobjects.live est un site qui suit où les nouveaux Durable Objects sont créés dans le réseau Cloudflare
    • Lors d’une visite d’exemple, il indiquait qu’en chargeant la page depuis Half Moon Bay, le worker de San Jose avait créé un Durable Object à San Jose

1 commentaires

 
GN⁺ 2024-10-15
Avis sur Hacker News
  • Quelques autres points intéressants : l’API d’écriture est synchrone, mais elle comporte une attente asynchrone cachée ; si l’écriture échoue au moment de produire la réponse suivante, le runtime transforme la réponse en échec HTTP.
    Ainsi, même si l’utilisateur ne gère pas explicitement les erreurs ou les attentes, le runtime peut regrouper automatiquement les écritures et supposer de manière optimiste qu’elles réussiront.
    Il n’y a pas de transactions de lecture, alors que cela aurait sans doute été utile pour obtenir un pointeur vers un instantané à un moment précis.
    Chaque instance du runtime est limitée à 128 Mo de RAM.
    Les WebSockets peuvent passer en hibernation et ne sont pas facturés pendant leur sommeil, ce qui permet de garder les clients connectés même pendant qu’un DO dort.
    Il existe aussi une sorte de RPC automatique qui permet de traiter d’autres DO ou Workers comme de simples appels JS ; en réalité, cela peut appeler un autre datacenter, tandis que la sérialisation et le parsing sont pris en charge par le runtime.

    • Le volet RPC est assez intéressant. Plus de détails ici : https://blog.cloudflare.com/javascript-native-rpc/
    • L’attente asynchrone cachée de l’API d’écriture rappelle le commit_delay de PostgreSQL. Le principe n’est pas exactement le même, mais l’idée est proche : https://www.postgresql.org/docs/current/runtime-config-wal.h...
      Litestream, mentionné dans l’article, propose aussi une technique similaire.
    • Je me demande s’il existe ici des cas d’usage concrets nécessitant des transactions de lecture au niveau de la base de données.
      Dans SQLite classique, plusieurs processus peuvent accéder simultanément à la même base de données, ce qui rend les transactions de lecture utiles.
      Ici, comme un seul processus peut accéder à la base, on peut obtenir le même effet en effectuant toutes les lectures dans une seule fonction synchrone, ou en implémentant soi-même un verrou au niveau du processus.
  • Le fait que les entrées du WAL soient streamées par lots vers le stockage objet tous les 16 Mo ou toutes les 10 secondes semble signifier qu’il peut falloir jusqu’à 10 secondes avant qu’une écriture soit lisible de manière fiable à l’échelle mondiale.
    Je ne vois pas bien comment cela pourrait remplacer des clusters de bases de données régionaux qui répondent en quelques millisecondes à l’échelle d’un continent.
    Je comprends qu’ils utilisent un flux, mais celui-ci ne cible que 5 followers, alors que Cloudflare possède des centaines de datacenters.
    Si toutes les instances SQLite ne sont pas connectées en permanence, il est physiquement impossible de garantir des lectures à l’échelle de quelques secondes ; et même si elles le sont, la latence réseau peut poser problème.

    • À ma connaissance, écritures et lectures ne se font que dans le même processus, et le stockage à long terme n’intervient que lorsque le processus courant est en hibernation.
      Si l’on écrit puis lit immédiatement, le changement est visible tout de suite, car l’écriture met aussi à jour l’état en mémoire du processus courant.
      Si un autre processus, par exemple un autre DO ou Worker, veut accéder aux données, il doit passer par le DO qui les possède, donc envoyer une requête RPC ou HTTP, et il recevra les informations les plus récentes.
      L’hibernation n’intervenant qu’après une période d’inactivité, le seul cas où une écriture deviendrait indisponible, contrairement à ce qu’on pourrait penser, semble être celui où le DO ou le Worker plante juste après l’écriture.
    • Les écritures sont streamées quasi en temps réel vers 5 followers et confirmées presque immédiatement.
      L’article du blog Cloudflare traite ce point plus en détail. Cela permet donc de conserver des écritures rapides tout en assurant la durabilité.
    • Je pense qu’il y a une incompréhension du fonctionnement des Durable Objects. Tout le trafic destiné à un même objet est routé vers une seule machine où cet objet existe.
      Cette machine a toujours une vue cohérente de sa propre base SQLite.
      On peut créer des milliards d’objets, mais chaque objet possède sa propre base de données.
      Il n’existe pas de moyen de lire directement cette base depuis une autre machine que celle sur laquelle le DO s’exécute.
    • Les entrées WAL streamées vers le stockage objet sont probablement destinées aux sauvegardes.
      Chaque DO est unique au niveau mondial, et le DO d’un ID donné ne s’exécute qu’à un seul endroit à la fois, en utilisant SQLite sur le stockage local de ce datacenter.
  • Ce que je ne comprends toujours pas avec les Durable Objects, c’est leur emplacement physique.
    Je me demande s’ils sont situés dans la région qui a hébergé l’appel API à l’origine de leur première création.
    Si c’est le cas, je me demande aussi s’il existe un mécanisme qui déplace automatiquement un DO vers un autre emplacement lorsqu’il devient clair, par exemple, qu’il a été créé en Amérique du Nord mais que tout le trafic de lecture/écriture vient ensuite d’Australie.

    • Par défaut, ils se trouvent dans la région où ils ont été créés, mais on peut aussi spécifier locationHint. Pour l’Australie, il faut utiliser "oc" : https://developers.cloudflare.com/durable-objects/reference/...
      Il faut aussi noter que « la relocalisation dynamique des Durable Objects existants est prévue pour l’avenir ».
    • https://where.durableobjects.live est un bon site pour voir où se trouve un DO.
      Seuls environ 10 à 11 % des PoP Cloudflare hébergent des Durable Objects.
      Les requêtes arrivant sur un autre PoP pour créer un DO sont transmises à l’un des PoP proches qui les hébergent.
    • À l’heure actuelle, les Durable Objects ne changent pas d’emplacement après leur création, et la relocalisation dynamique des Durable Objects existants est prévue pour l’avenir : https://developers.cloudflare.com/durable-objects/reference/...
      Si je me souviens bien, Orleans (https://www.microsoft.com/en-us/research/wp-content/uploads/...) peut déplacer des acteurs entre machines, et ce modèle semblerait bien correspondre à une façon de déplacer des DO entre emplacements.
    • Les Durable Objects disposent d’un stockage persistant, et sont hydratés depuis ce stockage ; en ce sens, ils peuvent donc être déplacés vers n’importe quel datacenter Cloudflare.
      En revanche, il n’existe pas d’appel API pour déplacer un Durable Object.
      Il ne doit pas y avoir de connexion, puis il est recréé dans le datacenter le plus proche lors de la connexion suivante, ou de la première connexion.
      La mémoire disparaît alors, mais le stockage reste.
      Des fonctionnalités plus détaillées liées à l’hibernation ont été ajoutées récemment, donc cette explication est peut-être un peu datée.
    • Ce n’est pas une réponse à la question, mais je recommande https://where.durableobjects.live/.
  • Je me demande si je suis le seul à trouver difficile de comprendre ces nouvelles technologies cloud.
    J’ai plus de 15 ans d’expérience en développement web et j’ai utilisé une stack Laravel / Postgres / Redis, mais quand je lis ce genre d’article, j’ai simplement l’impression que ce n’est pas pour moi.

    • D’après l’article, pour un contexte utile sur la première version des Durable Objects, il faut lire Cloudflare’s durable multiplayer moat de Paul Butler, où il explique pourquoi ils sont populaires pour créer des applications collaboratives en temps réel basées sur WebSocket.
      Les applications collaboratives en temps réel qui viennent tout de suite à l’esprit sont Google Docs/Sheets, Notion, Miro ou Figma.
      Ce sont toutes des applications collaboratives à l’échelle mondiale, et je ne sais pas vraiment si une stack Laravel peut prendre en charge ce type de cas d’usage.
      Google a probablement dû implémenter la plupart de cela en interne, et a sans doute été précurseur dans l’usage des CRDT.
      Mais à mesure que les patterns émergent et que les briques deviennent des services SaaS, la collaboration en temps réel à grande échelle cesse d’être un énorme problème d’ingénierie, ce qui rend possibles des produits plus intéressants.
  • J’aime vraiment beaucoup la conception des Durable Objects. En particulier, j’apprécie le fait qu’il soit facile de comprendre comment cela fonctionne en interne.
    Contrairement à beaucoup d’autres solutions conçues pour les données temps réel, les Durable Objects ont une simplicité à la manière de Redis et de la cuisine italienne : on voit tous les ingrédients.
    Avec suffisamment de temps, de ressources et de datacenters, j’ai l’impression qu’un bon programmeur pourrait lire la documentation des DO et réimplémenter quelque chose de similaire.
    Cela rend les compromis associés plus faciles à évaluer.
    Cela dit, même si les DO semblent très adaptés pour créer des expériences temps réel rapides et à faible overhead, par exemple une fonctionnalité permettant à cinq personnes de modifier un document en temps réel, je crains qu’ils ne rendent l’analytique et les vues d’ensemble très difficiles à produire.
    Par exemple, répondre à des questions comme « quel groupe a modifié quel document la semaine dernière », et cela pourrait devenir encore plus difficile si les données sont placées dans SQLite.
    Il faudrait alors interroger d’une manière ou d’une autre un grand nombre de petites instances SQLite, puis fusionner les résultats ; je me demande donc si les DO proposent quelque chose pour cela.
    Au final, c’est pour cette raison que je reviens toujours à Postgres : on peut l’utiliser à la fois pour les fonctionnalités cœur de l’application, pour les vues d’ensemble, la BI, etc.

  • C’est une conception vraiment intéressante, mais ce genre de systèmes intelligents se situe toujours, à titre personnel, dans une vallée de l’étrange.
    Il n’y a exactement que deux cas où l’on en a besoin : soit il faut faire monter en charge intelligemment un système soumis à une très forte charge, soit on construit un projet jouet pour s’amuser.
    Pour un projet jouet, on peut utiliser à peu près n’importe quoi.
    Mais pour la production ou un usage professionnel, il faut quelque chose d’éprouvé.
    Si l’on ne sait pas qu’on en a besoin, c’est qu’on n’en a pas besoin, et une bonne vieille base Postgres ennuyeuse avec une VM suffit.
    À l’inverse, si l’on sait qu’on en a besoin, ça se complique. C’est encore nouveau et pas assez mûr, donc on risque fort de tomber sur beaucoup de cas limites bizarres, et on n’aura probablement pas envie de les déboguer ni de les accepter.
    Au final, je ne sais pas à qui s’adresse ce genre de système.
    C’est trop niche pour être adopté par beaucoup d’acteurs sérieux et mûrir facilement, et c’est trop complexe, avec trop de compromis, pour 99,9 % des entreprises.
    Le seul public évident semble être les développeurs qui voient quelque chose qui brille et bâtissent une entreprise dessus — ou pire, bâtissent l’entreprise de quelqu’un d’autre dessus — avant de le regretter rapidement et de migrer vers quelque chose de plus ennuyeux.

    • À première vue, la killer app des Durable Objects, c’est le multijoueur.
      Si vous voulez créer un autre Figma ou Google Docs, le modèle de programmation des Durable Objects est très pratique.
      Cet article va plus loin dans le détail : https://digest.browsertech.com/archive/browsertech-digest-cl...
      Cet ancien article est aussi assez pertinent : http://ithare.com/scaling-stateful-objects/
      Pour quelqu’un qui a lu l’article de Figma sur le multijoueur et s’est dit « c’est à peu près ce dont j’ai besoin », les Durable Objects semblent bien adaptés : https://www.figma.com/blog/rust-in-production-at-figma/
      Il existe aussi d’autres approches. J’ai déjà essayé des CRDT au-dessus de WebRTC, et ça donnait vraiment l’impression d’être une technologie du futur.
      Mais c’est beaucoup plus complexe qu’une base fondée sur un WebSocket quelque part dans le cloud et une instance unique de classe.
    • Les bases de données sont un domaine qui mûrit extrêmement lentement, comme les langages de programmation, mais tout ce qui s’éloigne de Postgres n’est pas pour autant une techno hipster qui brille.
      Colocaliser les données et le comportement réduit réellement la complexité de façon quantifiable.
      Cela élimine les inquiétudes liées à la latence et à la bande passante, ce qui réduit à la fois les préoccupations opérationnelles et celles de développement. L’impact du fameux problème N+1 s’en trouve aussi fortement réduit.
      On peut soutenir que Postgres accessible par le réseau est meilleur pour d’autres raisons, et c’est peut-être effectivement vrai.
      Mais SQLite fait partie de ce qu’il y a de plus ennuyeux et prévisible, et ses forces connues sont claires.
      C’est pourquoi il gagne aussi en popularité côté serveur.
      Cela dit, je n’aime pas trop l’approche consistant à créer beaucoup de petites bases de données, comme le proposent les Durable Objects.
      Cela rappelle les cauchemars du NoSQL et peut casser des invariants importants des bases relationnelles.
      À mon avis, SQLite est bien mieux utilisé comme base de données monolithique, à la manière du produit D1 de Cloudflare.
    • Si l’on est dans le cas n° 1, il suffit de discuter avec Cloudflare.
      Cloudflare a besoin de beaux cas clients, et il y a de bonnes chances que ses excellents ingénieurs vous aident à comprendre comment cela fonctionne, en quoi c’est utile et comment traiter les bugs, en échange d’une réussite client.
      Une fois validé, cela deviendra une relation de service, mais au début ce sera plutôt proche d’un partenariat.
    • Ici, je vois la distinction selon l’axe de la débogabilité et de l’observabilité interne.
      Beaucoup de services, comme des outils internes, n’ont pas besoin d’optimisation des performances ni d’observation interne poussée.
      Dans ces cas-là, je pense que les frameworks serverless conviennent bien, parce qu’ils réduisent beaucoup le temps passé sur le déploiement.
      Être rapide, c’est bien, mais c’est rarement une exigence centrale.
      En général, les exigences clés sont de pouvoir construire vite et d’avoir peu de maintenance.
      Cloudflare peut offrir ici une bonne expérience développeur, mais ce n’est pas son principal argument marketing, et il existe aussi beaucoup de services concurrents qui promettent d’accélérer ce type de développement.
      En revanche, quand on a besoin d’une forte débogabilité et d’une bonne observabilité interne, je ne pense pas que ce type de service convienne bien.
      Il faut savoir quelles métriques on peut obtenir, si l’on dispose d’informations pour comprendre pourquoi certains Durable Objects sont lents, si l’on peut y remédier, comment fonctionne la journalisation et combien elle coûte.
      Ce genre de service peut être intéressant pour qu’une startup construise dès le premier jour un système distribué intelligent et repousse les questions de montée en charge.
      Mais pour une entreprise en scale-up, je pense qu’on aura envie de migrer vers quelque chose offrant une visibilité plus profonde, et cette transition sera difficile.
    • Cette analyse est très perspicace. Je pense que la plupart des gens devraient faire exactement ce genre d’analyse avant de concevoir un système.
      Comme d’autres l’ont dit, le cas d’usage, c’est le multijoueur, car pour que l’app donne une bonne impression, tout le monde doit voir les changements le plus vite possible.
      Plus largement, le secteur du stockage essaie depuis longtemps de créer quelque chose de cohérent, à faible latence et adapté au multiutilisateur.
      Même physiquement, il y a généralement un compromis entre cohérence et latence, ce qui rend le problème très difficile.
      C’est pourquoi plusieurs modèles sont expérimentés, et une grande partie de ces expériences se font autour de SQLite. Pas toutes, avec des exceptions comme Yugabyte ou Cockroach.
  • La conception des DO continue de m’impressionner.
    Il est facile d’avoir une réaction réflexe en se disant que cette approche a quelque chose de mauvais, mais en réalité, je pense que beaucoup de vrais produits sont implicitement structurés de cette manière.
    Pour chaque unité atomique nécessitant une cohérence transactionnelle, beaucoup d’opérations complexes sont effectuées à une échelle très réduite.
    Avec le recul, ce que nous avions construit chez Framer pour le projet de prise en charge du multijoueur était une version plus appliquée de ce que font aujourd’hui les DO.
    Les modifications d’édition étaient répliquées à 60 FPS et appliquées dans le bon ordre sur tous les clients, et nous avions fini par construire quelque chose qui ressemblait à un WAL pour l’édition d’objets JSON.
    Ainsi, même si une instance de projet crashait, une sauvegarde pouvait reprendre comme si de rien n’était.
    Même si nous n’avions pas eu le temps de committer les patchs JSON dans le gigantesque objet de données du projet, le traitement se faisait comme décrit ici, toutes les N mises à jour ou toutes les M secondes.

  • C’est peut-être une question idiote, mais je me demande comment les migrations de schéma sont gérées dans ce genre de configuration
    D’après ce que je comprends, l’objectif semble être une base de données par tenant, voire une structure encore plus finement découpée
    Je me demande s’il existe une façon raisonnable de gérer les migrations de schéma, ou si l’on s’attend à ce que ces bases soient plus éphémères et qu’il faille prendre en charge plusieurs versions de DB/DO jusqu’à leur suppression
    Dans ma tête, ce serait amusant de créer un service de favoris avec un DO par utilisateur
    Mais dès qu’on veut ajouter un nouveau champ à une table existante, on se retrouve face au problème assez délicat de devoir appliquer ce changement à chaque DO individuel
    Peut-être que cet exemple concerne des données trop persistantes, et que cette conception vise plutôt des usages plus temporaires
    Si quelqu’un l’a déjà fait en pratique, j’aimerais savoir comment il s’y prend

    • Il faudra probablement implémenter les migrations soi-même
      Il existe une version pour SQLite écrite en Python, mais je ne sais pas si elle peut s’exécuter dans Durable Objects. Ce serait peut-être possible via WASM et PyOdide
      Sinon, il faudrait la porter en JavaScript
      https://github.com/simonw/sqlite-migrate
  • Ces temps-ci, Cloudflare semble pousser les développeurs à utiliser les DO partout, plutôt que les Workers
    Les connexions WebSocket des Workers expirent aussi au bout d’environ 30 secondes, et l’approche recommandée est d’utiliser les DO

    • Il me semble que, sur Cloudflare Workers, les Durable Objects ont toujours été l’approche recommandée pour les connexions WebSocket. Du moins, c’est ce dont je me souviens
      La démo de chat originale utilisait déjà les DO et WebSocket depuis 2020 : https://github.com/cloudflare/workers-chat-demo
  • Est-ce que cela veut dire que, dans les situations où SQLite pour DO échoue, on peut perdre jusqu’à 10 secondes de données ?

    • Il y a ce passage : « Pour garantir la durabilité au-delà de la fenêtre de 10 secondes, les écritures sont transmises dès leur commit à 5 réplicas dans des datacenters distincts proches, et 3 d’entre eux doivent les confirmer pour que l’écriture soit approuvée »
      Ici, Simon voulait probablement dire within, et non “beyond”