Les développeurs créant des applications dans Workers se concentrent sur ce leur travail, plutôt que sur l'infrastructure nécessaire, et bénéficient de la portée mondiale du réseau de Cloudflare. De nombreuses applications nécessitent des données persistantes, qu'il s'agisse de projets personnels ou de charges de travail essentielles. Workers propose diverses options de base de données et de stockage adaptées aux besoins des développeurs, telles que le stockage clé-valeur et le stockage d'objets.
Les bases de données relationnelles constituent aujourd'hui l'épine dorsale de nombreuses applications. D1, le complément de base de données relationnelle de Cloudflare, est désormais proposé en disponibilité générale. Notre parcours, de la version alpha lancée fin 2022 à la disponibilité générale au mois d'avril 2024, a évolué autour de la possibilité, pour les développeurs, de créer des charges de travail de production dans l'environnement familier que représentent les données relationnelles et le langage SQL.
Qu'est-ce que D1 ?
D1 est la base de données relationnelle serverless et intégrée de Cloudflare. Pour les applications Workers, D1 offre l'expressivité de SQL, grâce au dialecte SQL de SQLite, ainsi que des intégrations d'outils pour développeurs, notamment des ORM (Object-Relational Mapper, mappeur objet-relationnel) tels que Drizzle ORM. D1 est accessible par l'intermédiaire de Workers ou d'une API HTTP.
L'approche serverless permet de proposer l'absence de provisionnement, la reprise après sinistre par défaut avec Time Travel et une tarification en fonction de l'utilisation. D1 propose une généreuse offre gratuite qui permet aux développeurs de tester différentes solutions dans D1, puis de transférer ces tests en phase de production.
Comment rendre les données mondiales ?
La disponibilité générale de D1 priorise la fiabilité, ainsi que l'expérience des développeurs. Aujourd'hui, nous avons l'intention d'étendre D1 afin d'offrir une meilleure prise en charge des applications distribuées à l'échelle mondiale.
Dans le modèle Workers, une requête entrante invoque une exécution serverless dans le datacenter le plus proche. Une application Workers peut évoluer à l'échelle mondiale, en fonction des requêtes d'utilisateurs. Cependant, les données des applications restent stockées dans des bases de données centralisées; et le trafic des utilisateurs à l'échelle mondiale doit tenir compte des allers-retours d'accès aux emplacements des données. Par exemple, une base de données D1 réside aujourd'hui dans un emplacement unique.
Workers prend en charge Smart Placement afin de tenir compte de la localisation des données fréquemment consultées. Smart Placement invoque une instance Workers plus proche des services backend centralisés, tels que les bases de données, afin de réduire la latence et d'améliorer les performances des applications. Nous avons abordé la question du placement de Workers dans les applications mondiales, mais nous devons résoudre la problématique du placement des données.
La question est donc de savoir comment D1, la solution de base de données intégrée à Cloudflare, peut améliorer la prise en charge du placement des données pour les applications mondiales. La réponse est la réplication en lecture asynchrone.
Qu'est-ce que la réplication en lecture asynchrone ?
Dans un système de gestion de base de données basé sur un serveur (tel que Postgres, MySQL, SQL Server ou Oracle), un réplica en lecture est un serveur de base de données distinct qui sert de copie en lecture seule, presque à jour, du serveur de base de données principal. Un administrateur crée un réplica en lecture en démarrant un nouveau serveur depuis un instantané du serveur principal, puis en configurant le serveur principal afin qu'il transmette de manière asynchrone des mises à jour au serveur réplica. Puisque les mises à jour sont asynchrones, le réplica lu peut être antérieur à l'état actuel du serveur principal. L'écart entre le serveur principal et un réplica est appelée « retard de réplication ». Il est possible de configurer plusieurs réplicas en lecture.
La réplication en lecture asynchrone est une solution éprouvée pour améliorer les performances des bases de données :
Il est possible d'augmenter le débit en répartissant la charge sur plusieurs réplicas.
Il est possible de réduire la latence des requêtes lorsque les réplicas sont proches des utilisateurs qui génèrent les requêtes.
Notez que certains systèmes de base de données proposent également la réplication synchrone. Dans un système répliqué synchrone, les écritures doivent attendre que tous les réplicas aient confirmé l'écriture. Les systèmes répliqués synchrones ne peuvent fonctionner qu'à la vitesse du réplica le plus lent, et s'arrêtent en cas de défaillance d'un réplica. Si nous essayons d'améliorer les performances à l'échelle mondiale, nous avons intérêt à éviter, autant que possible, la réplication synchrone !
Modèles de cohérence et réplicas en lecture
La plupart des systèmes de base de données proposent des modèles de cohérence de type Read Committed, isolation d'instantané ou sérialisables, en fonction de leur configuration. Par exemple, Postgres utilise par défaut le mode Read Committed, mais peut être configuré pour utiliser des modes plus forts. SQLite fournit une isolation d'instantané en mode WAL. Il est plus facile de programmer avec des modes plus forts, tels que l'isolation d'instantané ou la sérialisation, car ils limitent les scénarios de concurrence système autorisés, ainsi que les conditions de course en concurrence dont doivent se préoccuper les programmeurs.
Les réplicas en lecture sont mis à jour indépendamment, de sorte que le contenu de chaque réplica peut différer à tout moment. Si toutes vos requêtes sont transmises au même serveur, qu'il s'agisse du serveur principal ou d'un réplica en lecture, vos résultats devraient être cohérents, quel que soit le modèle de cohérence fourni par votre base de données sous-jacente. Si vous utilisez un réplica en lecture, les résultats risquent d'être légèrement anciens.
Dans une base de données sur serveur avec des réplicas en lecture, il est important d'utiliser le même serveur pour toutes les requêtes d'une session. Si vous passez d'un réplica en lecture à un autre au cours d'une même session, vous compromettez le modèle de cohérence fourni par votre application, ce qui peut fausser vos hypothèses concernant le comportement de la base de données et entraîner des résultats erronés dans votre application.
ExamplePrenons l'exemple de deux réplicas, A et B. Le réplica A présente un retard de 100 ms par rapport à la base de données principale, et le réplica B présente un retard de 2 secondes par rapport à la base de données principale. Supposons qu'un utilisateur souhaite :
Exécuter la requête 11a. Effectuer un calcul sur la base des résultats de la requête 1
Exécuter la requête 2 sur la base des résultats du calcul effectué en (1a)
Au temps t=10 s, la requête 1 est transmise au réplica A, puis est renvoyée. La requête 1 observe ce à quoi ressemblait la base de données primaire à t=9,9 s. Supposons qu'il faille 500 ms pour effectuer le calcul, de sorte qu'à t=10,5 s, la requête 2 est transmise au réplica B. N'oubliez pas que le réplica B présente un retard de 2 secondes par rapport à la base de données primaire, de sorte qu'à t=10,5 s, la requête 2 observe ce à quoi ressemblait la base de données à t=8,5 s. Pour l'application, les résultats de la requête 2 donnent l'impression que la base de données a reculé dans le temps !
Formellement, il s'agit d'une cohérence en mode Read Committed, puisque vos requêtes ne voient que des données validées et qu'aucune autre garantie n'est offerte – pas même celle que vous puissiez lire vos propres écritures. Bien que le modèle de cohérence Read Committed soit valide, il est difficile d'établir un raisonnement concernant l'ensemble des conditions de course permises par le modèle de cohérence Read Committed, ce qui rend difficile l'écriture correcte d'applications.
Modèle de cohérence de D1 et réplicas en lecture
Par défaut, D1 propose le mode d'isolation d'instantané que fournit SQLite.
L'isolation d'instantané est un modèle de cohérence familier, dont la plupart des développeurs apprécient la facilité d'utilisation. Nous mettons en œuvre ce modèle de cohérence dans D1, en garantissant qu'il existe au plus une copie active de la base de données D1 et en acheminant toutes les requêtes HTTP vers cette seule base de données. S'assurer qu'il n'existe au plus qu'une copie active de la base de données D1 constitue un problème épineux avec les systèmes distribués ; cependant, nous l'avons résolu en développant D1 avec Durable Objects. Les instances Durable Objects garantissent l'unicité globale ; aussi, une fois que nous dépendons de Durable Objects, le routage des requêtes HTTP est facile : il suffit de les transmettre à la base de données Durable Objects de D1.
Cette astuce ne fonctionne pas si plusieurs copies actives de la base de données sont présentes, car il n'existe aucun moyen fiable à 100 % d'examiner une requête HTTP générique entrante et de l'acheminer vers le même réplica dans 100 % des cas. Malheureusement, comme nous l'avons vu dans l'exemple de la section précédente, si nous n'acheminons pas les requêtes liées vers le même réplica dans 100 % des cas, le meilleur modèle de cohérence que nous puissions fournir est le modèle Read Committed.
Étant donné qu'il est impossible d'acheminer systématiquement les requêtes vers un réplica particulier, une autre approche consiste à acheminer les requêtes vers n'importe quel réplica et à s'assurer que le réplica choisi répond aux requêtes conformément à un modèle de cohérence qui « a du sens » pour le programmeur. Si nous sommes prêts à inclure une horloge de Lamport dans nos requêtes, nous pouvons implémenter la cohérence séquentielle avec n'importe quel réplica. Le modèle de cohérence séquentielle possède des propriétés importantes telles que « read my own writes » (lire mes propres écritures) et « writes follow reads » (les écritures suivent les lectures), ainsi qu'un ordre total des écritures. L'ordre total des écritures signifie que chaque réplica observe la validation des transactions dans le même ordre, ce qui est précisément le comportement que nous souhaitons mettre en œuvre dans un système transactionnel. La cohérence séquentielle s'accompagne d'une mise en garde selon laquelle toute entité individuelle du système peut être arbitrairement obsolète ; cependant, cette mise en garde est, pour nous, une caractéristique, car elle nous permet de prendre en compte le retard des réplicas lors du développement de nos API.
L'idée est que si D1 fournit aux applications une horloge de Lamport pour chaque requête de base de données et que ces applications informent D1 sur la dernière horloge de Lamport qu'elles ont observée, nous pouvons laisser chaque réplica déterminer de quelle manière les requêtes doivent être exécutées selon le modèle de cohérence séquentielle.
Une approche robuste, mais simple, pour mettre en œuvre la cohérence séquentielle avec les réplicas consiste à :
Associer une horloge de Lamport à chaque requête adressée à la base de données. Un jeton de validation augmentant de façon monotone s'avère efficace, à cette fin.
Transmettre toutes les requêtes d'écriture à la base de données principale, afin de garantir l'ordre total des écritures.
Transmettre des requêtes en lecture à n'importe quel réplica, mais demander au réplica de retarder le traitement de la requête jusqu'à ce que le réplica reçoive, de la base de données principale, des informations mises à jour postérieures à l'horloge de Lamport contenue dans la requête.
L'intérêt de cette mise en œuvre est qu'elle est rapide dans le scénario courant où une charge de travail intense en lecture est toujours transmise au même réplica et s'exécute correctement, même si les requêtes sont acheminées vers différents réplicas.
Avant-première : la réplication en lecture sur D1 avec Sessions
Pour apporter la réplication en lecture à D1, nous allons étendre l'API D1 avec un nouveau concept : Sessions. Une instance Sessions encapsule toutes les requêtes représentant une session logique pour votre application. Par exemple, une instance Sessions peut représenter toutes les requêtes provenant d'un navigateur web particulier ou toutes les requêtes provenant d'une application mobile. Si vous utilisez Sessions, vos requêtes utiliseront la copie de la base de données D1 qui correspond le mieux à votre requête, qu'il s'agisse de la base de données principale ou d'un réplica proche. L'implémentation de Sessions dans D1 garantit la cohérence séquentielle de toutes les requêtes dans l'instance Sessions.
Étant donné que l'API Sessions modifie le modèle de cohérence de D1, les développeurs doivent s'inscrire pour utiliser la nouvelle API. Les méthodes existantes de l'API D1 restent inchangées et proposeront le même modèle de cohérence avec isolation d'instantané qu'auparavant. Toutefois, seules les requêtes transmises à l'aide de la nouvelle API Sessions utiliseront des réplicas.
Voici un exemple de l'API Sessions de D1 :
L'implémentation de Sessions dans D1 utilise des jetons de validation. Les jetons de validation identifient une requête validée particulière transmise à la base de données. Au sein d'une instance Sessions, D1 utilise le jeton de validation afin d'assurer que les requêtes sont ordonnées de manière séquentielle. Dans l'exemple ci-dessus, l'instance Sessions de D1 garantit que la requête « SELECT COUNT(*) » est exécutée après la requête « INSERT » de la nouvelle commande, même si nous changeons de réplique entre les attentes.
export default {
async fetch(request: Request, env: Env) {
// When we create a D1 Session, we can continue where we left off
// from a previous Session if we have that Session's last commit
// token. This Worker will return the commit token back to the
// browser, so that it can send it back on the next request to
// continue the Session.
//
// If we don't have a commit token, make the first query in this
// session an "unconditional" query that will use the state of the
// database at whatever replica we land on.
const token = request.headers.get('x-d1-token') ?? 'first-unconditional'
const session = env.DB.withSession(token)
// Use this Session for all our Workers' routes.
const response = await handleRequest(request, session)
if (response.status === 200) {
// Set the token so we can continue the Session in another request.
response.headers.set('x-d1-token', session.latestCommitToken)
}
return response
}
}
async function handleRequest(request: Request, session: D1DatabaseSession) {
const { pathname } = new URL(request.url)
if (pathname === '/api/orders/list') {
// This statement is a read query, so it will execute on any
// replica that has a commit equal or later than `token` we used
// to create the Session.
const { results } = await session.prepare('SELECT * FROM Orders').all()
return Response.json(results)
} else if (pathname === '/api/orders/add') {
const order = await request.json<Order>()
// This statement is a write query, so D1 will send the query to
// the primary, which always has the latest commit token.
await session
.prepare('INSERT INTO Orders VALUES (?, ?, ?)')
.bind(order.orderName, order.customer, order.value)
.run()
// In order for the application to be correct, this SELECT
// statement must see the results of the INSERT statement above.
// The Session API keeps track of commit tokens for queries
// within the session and will ensure that we won't execute this
// query until whatever replica we're using has seen the results
// of the INSERT.
const { results } = await session
.prepare('SELECT COUNT(*) FROM Orders')
.all()
return Response.json(results)
}
return new Response('Not found', { status: 404 })
}
Il existe plusieurs options pour démarrer une instance Sessions dans un handler fetch de Workers. db.withSession(<condition>)
accepte les arguments suivants :
Argument condition
Comportement
<commit_token>
(1) démarre l'instance Sessions à partir d'un jeton de validation spécifié
(2) les requêtes ultérieures ont une cohérence séquentielle
first-unconditional
(1) si la première requête est une lecture, lire les informations qu'a le réplica actuel et utiliser le jeton de validation de cette lecture comme base pour les requêtes suivantes. Si la première requête est une écriture, elle est transmise au serveur principal, et le jeton de validation de l'écriture est utilisé comme base pour les requêtes suivantes.
(2) les requêtes ultérieures ont une cohérence séquentielle
first-primary
(1) exécute la première requête (lecture ou en écriture) en fonction du serveur principal
(2) les requêtes ultérieures ont une cohérence séquentielle
npx wrangler d1 create northwind-traders
# omit --remote to run on a local database for development
npx wrangler d1 execute northwind-traders --remote --file=./schema.sql
npx wrangler d1 execute northwind-traders --remote --file=./data.sql
null
ou argument manquant
# database schema & data
npx wrangler d1 export northwind-traders --remote --output=./database.sql
# single table schema & data
npx wrangler d1 export northwind-traders --remote --table='Employee' --output=./table.sql
# database schema only
npx wrangler d1 export <database_name> --remote --output=./database-schema.sql --no-data=true
traitement identique à first-unconditional
# To find top 10 queries by average execution time:
npx wrangler d1 insights <database_name> --sort-type=avg --sort-by=time --count=10
Il est possible de couvrir plusieurs requêtes avec une instance Sessions en réutilisant le jeton de validation de la dernière requête de la session après un aller-retour, afin de démarrer une nouvelle session. Ceci permet aux agents utilisateurs individuels (par exemple, une application web ou une application mobile) de s'assurer que toutes les requêtes observées par l'utilisateur sont cohérentes d'un point de vue séquentiel.
La réplication en lecture de D1 sera intégrée, n'entraînera pas de coûts d'utilisation ou de stockage supplémentaires et ne nécessitera aucune configuration de réplica. Cloudflare surveillera le trafic D1 d'une application et créera automatiquement des réplicas de base de données, afin de répartir le trafic des utilisateurs sur plusieurs serveurs plus proches des utilisateurs. Conformément à notre modèle serverless, les développeurs utilisant D1 n'ont pas besoin de se préoccuper du provisionnement et de la gestion des réplicas. Au lieu de cela, ils peuvent se concentrer sur la création d'applications tenant compte des compromis en matière de réplication et de cohérence des données.
Nous travaillons activement sur la réplication globale en lecture et sur la mise en œuvre de la proposition ci-dessus (faites-nous part de vos commentaires dans le canal #d1 du Discord pour développeurs de Cloudlfare). D'ici-là, la disponibilité générale de D1 propose plusieurs nouveaux ajouts passionnants.
Découvrez la disponibilité générale de D1
Depuis la version bêta ouverte de D1, lancée en octobre 2023, nous nous sommes concentrés sur la fiabilité, l'évolutivité et l'expérience des développeurs qu'exigent les services essentiels. Nous avons investi dans plusieurs nouvelles fonctionnalités qui permettent aux développeurs de créer et de déboguer plus rapidement des applications avec D1.
Développer de plus grandes applications avec des bases de données plus vastesNous avons écouté les développeurs qui demandaient des bases de données plus vastes. D1 prend désormais en charge des bases de données jusqu'à 10 Go, avec 50 000 bases de données sur l'offre payante Workers. Grâce à l'évolutivité horizontale de D1, les applications peuvent modéliser des scénarios d'utilisation de bases de données par entité commerciale. Depuis la version bêta, les nouvelles bases de données D1 traitent 40 fois plus de requêtes que les bases de données de la version alpha de D1 sur une période donnée.
Importation et exportation de données en vracLes développeurs importent et exportent des données pour une multitude de raisons :
Tests de migration de bases de données vers/depuis différents systèmes de bases de données
Copies de données pour le développement local ou les tests
Sauvegardes manuelles pour les besoins particuliers, tels que la conformité
Il était auparavant possible d'exécuter des fichiers SQL avec D1, et nous améliorons désormais wrangler d1 execute –file=<filename>
afin d'assurer que les importations de grands volumes de données sont des opérations atomiques et qu'elles ne laissent jamais votre base de données dans un état intermédiaire. wrangler d1 execute
opère désormais en mode local-first, par défaut, afin de protéger votre base de données de production distante.
Pour importer notre base de données de démonstration Northwind Traders, vous pouvez télécharger le schéma et les données, puis exécuter les fichiers SQL.
La base de données D1 peut être exportée (données et schéma, schéma uniquement ou données uniquement) vers un fichier SQL avec les instructions suivantes :
Débogage des problèmes de performance des requêtesComprendre les performances des requêtes SQL et déboguer les problèmes de lenteur des requêtes est une étape cruciale pour les charges de travail de production. Nous avons ajouté la fonctionnalité expérimentale wrangler d1 insights
afin d'aider les développeurs à analyser les indicateurs de performance des requêtes, également disponibles par l'intermédiaire de l'API GraphQL.
Outils pour développeursDifférents projets de développeurs communautaires prennent en charge D1. Parmi les nouveaux ajouts figurent Prisma ORM (dans la version 5.12.0), qui prend maintenant en charge Workers et D1.
Prochaines étapes
Les fonctionnalités actuellement disponibles avec la disponibilité générale et notre architecture de réplication en lecture globale ne constituent qu'un premier pas pour répondre aux besoins d'applications de développeurs des bases de données SQL. Si vous n'avez pas encore utilisé D1, vous pouvez vous lancer dès maintenant, consulter la documentation pour développeurs de D1 pour trouver des idées ou rejoindre le canal #d1 du Discord pour développeurs de Cloudflare pour échanger avec d'autres développeurs D1 et notre équipe d'ingénieurs produits.