Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Architecture du stockage cas Décidé

Trois couches : des segments append-only qui sont la source de vérité, un index LSM reconstructible qui n'est qu'un accélérateur, et une petite DB de contrôle — le seul état irremplaçable. Les blobs ne passent jamais par le LSM.

En clair — l'entrepôt, le fichier de fiches, le registre Imagine un entrepôt. Les segments, ce sont de grandes caisses scellées où l'on range les colis les uns à la suite des autres — on ne rouvre jamais une caisse pleine pour la réorganiser. Le fichier de fiches (l'index), c'est le meuble à tiroirs qui dit « le colis n°b291 est dans la caisse 42, position 17 » : il fait gagner du temps, mais si on le perdait, on pourrait le reconstituer en ouvrant les caisses — chaque colis porte son étiquette. Le registre du bureau (la DB de contrôle), lui, contient ce qu'aucune caisse ne contient : qui sont les clients, quels réglages ils utilisent, la liste de leurs sauvegardes. C'est le seul document qu'il ne faut jamais perdre — et il est tout petit.

Ce qu’on doit stocker — quatre classes d’objets

Les décisions du modèle de manifestes donnent quatre classes aux profils très différents :

ObjetTailleConvergent ?Check d’existence ?Lecture typique
Chunks0,5–8 Mioouioui (dédup)restauration
Manifestes80 o – 100 Kioouioui (dédup)restauration, GC
Répertoires / racinespetitsnonnon (jamais dédupliqués)début de backup, navigation, GC
État mutable (tenants, profils épinglés, refs de snapshots, époques GC, quotas)minusculetransactionnel

Les trois premières classes sont immuables et adressées par contenu → elles vont dans un magasin adressé par contenu (CAS). La quatrième est mutable et transactionnelle → une vraie base de données. On ne mélange jamais les deux.

Les trois couches

# de haut en bas
DB de contrôle (PostgreSQL) # tenants, profils épinglés, refs de snapshots, époques GC
Index CAS (LSM) # addr → (segment, offset, len, classe, époque)
Segments append-only (disque) # les blobs eux-mêmes, scellés une fois pleins

Les trois couches — chemin d'écriture & hiérarchie de vérité

DB DE CONTRÔLE · POSTGRESQL tenants · profils épinglés · refs de snapshots · époques GC · quotas mutable · transactionnel · ne voit jamais un blob SEUL ÉTAT IRREMPLAÇABLE minuscule → répliqué (HA) INDEX CAS · LSM addr → (segment, offset, len, classe, époque) fiches de ~50 o · blooms par SSTable · 100 To ≈ 2,5 Go ACCÉLÉRATEUR DÉRIVÉ jetable SEGMENTS APPEND-ONLY écrits une fois · scellés pleins · ~256 Mio–1 Gio MÉTADONNÉES · SSD manifestes · répertoires · racines petits, cache RAM agressif DONNÉES · HDD chunks 0,5–8 Mio gros, écrits séquentiellement [ addr | len | payload ] — chaque entrée se revérifie en rehachant SOURCE DE VÉRITÉ ÉCRITURE PUT chunk ① append ② fsync ③ index ④ ack après durabilité index perdu ? scan des caisses → reconstruit

L'écriture descend (durable avant visible : append, fsync, puis seulement l'index et l'ack). L'index ne fait qu'accélérer — perdu, un scan des segments le reconstruit. La DB de contrôle ne voit jamais un blob — et c'est le seul étage qu'on ne sait pas reconstruire, d'où : minuscule, répliqué.

La hiérarchie de vérité Chaque entrée d'un segment est encadrée [addr | len | payload] : on peut reconstruire l'index entier en scannant les segments, et revérifier chaque adresse en rehachant. L'index LSM n'est donc qu'un accélérateur dérivé, jetable. La DB de contrôle est le seul état irremplaçable — et comme elle est minuscule, la répliquer et la sauvegarder est trivial (le point « méta-durabilité » de la section E). C'est PostgreSQL : la haute disponibilité du plan de contrôle est une exigence, et sa réplication et son failover sont natifs et éprouvés (section J). restic a cette propriété — index reconstructible depuis les packs — et elle l'a sauvé de nombreux bugs.

Décision — segments + index, pas de blobs dans le LSM

C’était l’embranchement laissé ouvert aux points ouverts : segments avec index séparé, ou LSM à value-log (WiscKey / BlobDB) tenant les blobs directement. La décision chunks de 2 Mio le tranche.

En clair — on ne déménage pas les pianos Un LSM range ses données en strates qu'il réécrit régulièrement pour rester rapide (la « compaction » — imagine un déménageur qui réorganise les étagères en permanence). Réorganiser des fiches de 50 octets, très bien. Réorganiser des pianos de 2 Mio, encore et encore : c'est de l'amplification d'écriture massive, le disque passe sa vie à recopier les mêmes gros blobs. Les moteurs « value-log » (WiscKey, BlobDB) évitent ça en… rangeant les gros objets à part dans un journal append-only — c'est-à-dire en réinventant nos segments à l'intérieur du moteur de quelqu'un d'autre, avec moins de contrôle sur le GC.

Donc : les blobs vont dans des segments append-only (~256 Mio–1 Gio, scellés une fois pleins), le LSM n’indexe que des fiches de ~50 octets (addr → segment, offset, len). Le LSM est parfait pour ce travail : écritures massives pendant les backups, lookups ponctuels, clés uniformément aléatoires (des hashes — donc jamais de range scan). Ordre de grandeur : 100 To stockés ≈ 50 M de chunks ≈ 2,5 Go d’index. Dérisoire.

Segments données vs segments métadonnées

Les manifestes vont dans le même CAS que les chunks — convergents, immuables, checkés et dédupliqués pareil — mais pas dans les mêmes caisses :

  • Segments données — les chunks. Gros, écrits séquentiellement, peuvent vivre sur HDD.
  • Segments métadonnées — manifestes, répertoires, racines. Entrées petites, empaquetées ensemble, sur SSD, cachées agressivement en RAM.
Pourquoi séparer — le catalogue reste près du comptoir Trois chemins chauds ne lisent que des métadonnées : le début de chaque backup (le client stateless télécharge l'arbre du snapshot parent — racine + nœuds répertoire — pour détecter les changements), la navigation / restauration partielle (descendre le DAG avant de toucher un seul chunk), et la phase mark du GC (parcourir tous les arbres). Si les manifestes étaient éparpillés au milieu de caisses de chunks de 2 Mio, chacun de ces parcours paierait des seeks à travers des téraoctets de données froides. Une bibliothèque garde son catalogue près du comptoir, pas dispersé dans les rayonnages. Précédent direct : restic sépare tree packs et data packs, kopia préfixe q (métadonnées) vs p (données).

Les répertoires et racines (non convergents) vont aussi dans le CAS — même adressage, même GC, même vérification d’intégrité — simplement sans check d’existence : ils sont toujours neufs par construction. Les racines sont en plus référencées depuis la DB de contrôle (la liste des snapshots d’un tenant — l’équivalent des refs de Git), et ce sont les racines du GC.

Bloom filters — accélérateur, jamais oracle

En clair — le portier à la mémoire floue Un bloom filter, c'est un portier qui a une mémoire compacte mais floue. Il sait dire deux choses : « celui-là, je ne l'ai jamais vu, certain » (toujours vrai), ou « il me dit quelque chose… » (parfois faux — c'est le faux positif). On ne lui demande jamais de confirmer une présence, seulement d'écarter vite les absents.
Le piège de correction — à ne jamais franchir Si le serveur répondait « présent » sur la foi du bloom seul, un faux positif ferait sauter au client l'upload d'un chunk que le serveur n'a pas. Résultat : corruption silencieuse, découverte le jour de la restauration — le pire scénario possible pour un système de sauvegarde. La règle absolue : le bloom n'a le droit de répondre que « absent, c'est sûr » (fast path : upload). Toute réponse « présent » vient de l'index authoritaire. Un faux positif ne coûte alors qu'un lookup LSM inutile — jamais la correction.

Deux étages utiles : les blooms par SSTable du LSM (pendant un backup de données neuves, la majorité des checks sont des miss — sans bloom, chaque miss toucherait tous les niveaux du LSM), et éventuellement un bloom global en RAM devant l’API d’existence pour répondre « absent » sans toucher le disque.

Le contrat de durabilité

# chemin d'écriture d'un blob — l'ordre est vital
append au segment ouvert → fsync (groupé) → insertion index → ack au client
« Présent » est une promesse La dédup transforme chaque réponse « présent » en promesse de durabilité : le client, sur cette foi, ne réuploadera pas le chunk. Si le serveur répond « présent » (ou ack « stocké ») avant que le blob soit réellement sur disque, un crash au mauvais moment = un chunk que tout le monde croit sauvegardé et que personne n'a. D'où l'ordre : durable d'abord, visible ensuite. Le fsync groupé (plusieurs blobs par sync) garde le débit.

Ce qui reste ouvert

  • Moteur LSM concret — RocksDB (éprouvé, blooms intégrés, dépendance C++) vs pur Rust (fjall). Choix d'implémentation, pas d'architecture.
  • Taille des segments et seuils de compaction — liés à la mécanique du GC (un segment dont le taux d'entrées vivantes tombe sous un seuil est réécrit ; les époques protègent les uploads en cours). Se décide avec le sujet GC.
  • Backends objet (S3 & co) — les segments s'y mappent naturellement (1 segment = 1 objet), mais le multi-backend reste le sujet « backends pluggables » de la section D.
  • La forme de l'API d'existence — batch, latence, ce qu'elle révèle : toujours le point ouvert de la section B. Le stockage lui donne son socle : bloom → LSM → réponse authoritaire.