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.
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 :
| Objet | Taille | Convergent ? | Check d’existence ? | Lecture typique |
|---|---|---|---|---|
| Chunks | 0,5–8 Mio | oui | oui (dédup) | restauration |
| Manifestes | 80 o – 100 Kio | oui | oui (dédup) | restauration, GC |
| Répertoires / racines | petits | non | non (jamais dédupliqués) | début de backup, navigation, GC |
| État mutable (tenants, profils épinglés, refs de snapshots, époques GC, quotas) | minuscule | — | — | transactionnel |
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
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é
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é.
[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.
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.
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
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é
append au segment ouvert → fsync (groupé) → insertion index → ack au client
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.