Jubako et Arx, un conteneur universel et son format d’archive

Posté par  (site web personnel) . Édité par orfenor, Yves Bourguignon, vmagnin, palm123, Ysabeau 🧶 🧦, patrick_g, Ltrlg et Julien Jorge. Modéré par Ysabeau 🧶 🧦. Licence CC By‑SA.
Étiquettes :
61
4
nov.
2022
Technologie

Jubako, quezako ?

重箱 (Jūbako) est le nom japonais des boîtes à bento. Ce sont des boîtes compartimentées qui peuvent se composer en fonction de ce qu’il y a à stocker dedans (en général un repas).

Et ça tombe bien, parce que Jubako, c’est un format de conteneur qui permet de stocker différentes données et méta-données. J’ai tendance à parler de conteneurs plutôt que d’archives, en effet « archive » est un mot orienté qui fait penser aux archives de fichiers, alors que Jubako se veut généraliste : un conteneur Jubako pourrait être utilisé pour plein d’autres choses : empaquetage d’applications, pack de ressources dans un binaire, conteneur multimédia, etc.

Vous pouvez voir Jubako comme étant au stockage ce que XML est à la sérialisation. XML définit comment sérialiser du contenu (sous forme d’un arbre de nœuds avec des attributs) mais ne définit pas quels sont ces nœuds et attributs. Chaque cas d’usage a sa propre structure. Pour Jubako c’est pareil, il définit comment stocker des données dans un fichier « d’archive » mais il ne définit pas quelles sont ces données. Chaque cas d’usage aura sa propre structure de données.

Jubako et Arx sont sous licence MIT.

Sommaire

Dans cette dépêche on présente Jubako, un méta-format de conteneur et la bibliothèque associée pour lire et écrire ce format, ainsi qu’Arx, un outil se basant sur Jubako pour faire des archives de fichiers, un peu comme tar mais en mieux différemment. La partie Jubako peut être un peu longue, si vous êtes plus intéressé par la finalité du Jubako, vous pouvez directement sauter à la partie Arx pour voir l’usage qui en est fait.

Un peu de contexte

Mon métier, c’est développeur logiciel, en tant qu’indépendant.
Mon client principal depuis six ans, c’est la fondation Kiwix pour qui je maintiens les outils de base de Kiwix, c’est-à-dire les outils bas niveau en C++ pour lire et créer les archives Zim (pour ce qui nous concerne ici).
Les archives Zim sont des archives de « contenu » qui pourraient s’apparenter à des zip, mais avec un format interne qui permet une meilleure compression tout en permettant un accès en lecture quasiment en random access.
Le format ZIM fonctionne bien, mais il souffre de quelques erreurs de jeunesse. À la base, ZIM c’est « Zeno IMproved », et Zeno… je sais pas trop d’où ça vient. Il y a bien tntzenoreader qui date de 2007. Mais s’il lit les fichiers Zeno, ce n’est pas lui a priori qui crée le format. (Soit dit en passant, c’est probablement la source de libzim vu que je reconnais pas mal de similitudes de code entre les deux projets). De mémoire, Zeno était associé à l’époque où la fondation Wikimedia parlait de mettre Wikipedia sur CD-ROM.

Enfin bon voilà, un peu de dette technique certes, mais surtout un format très orienté pour le cas d’usage de stocker des pages web. Par exemple, chaque entrée a obligatoirement une url, un titre et un mimetype.

Et puis un jour, celui qu’on ne nomme plus arriva avec son confinement généralisé. Durant cette période, j’ai commencé à travailler sur un nouveau format d’archive, inspiré du format ZIM mais avec comme objectif d’en faire un format généraliste qui pourrait être utilisé par d’autres projets.

Ainsi est né doucement le projet Jubako.

Le format Jubako

Le format Jubako est donc un format (binaire) de fichiers pour des conteneurs.

Jubako est un meta-format. Comme XML, il ne définit pas la structure de données à stocker. Ce sera donc au « Vendeur » (qui utilise Jubako dans son projet) de définir la façon dont les données seront stockées. Il est conçu avec le cas d’usage suivant : un conteneur « read-only » créé une fois et distribué à un ensemble d’utilisateurs qui vont lire le conteneur. La lecture du conteneur doit pouvoir se faire sur des machines de relativement faible puissance et en temps « quasi » constant sans décompression préalable. À l’inverse, il est attendu que la machine sur laquelle est fait le conteneur soit un peu plus puissante (ou tout du moins, qu’il soit accepté que la création prenne plus de temps).

Un conteneur Jubako est composé de plusieurs sous-parties appelées pack :

  • Les contentPacks, qui contiennent les données proprement dites. Les contentPacks sont les plus gros en taille et contiennent les données « brutes », sans métadonnées. C’est un peu un conteneur de blob si on reprend la terminologie de git.
  • Le directoryPack qui lui contient les entrées et métadonnées associées. C’est le pack le plus complexe et le plus « configurable ». Globalement, il stocke des entrées (Entry). Chaque entrée étant composée de métadonnées et pouvant pointer vers un, plusieurs ou aucun contenu (stocké dans les contentPacks).
  • Le manifestPack qui relie tous ces différents packs ensemble pour former un tout cohérent.

Un conteneur Jubako est donc obligatoirement composé d’un manifestPack et d’un directoryPack et d’un ou plusieurs contentPacks (parfois aucun). Les packs peuvent être stockés séparément (sous forme de fichiers dans un dossier par exemple) ou concaténés ensemble pour ne former qu’un seul fichier.

Notez que si un conteneur est logiquement composé de plusieurs contentPack, il est normal que certains contentPack puissent être manquants lors de la lecture. Cela permet d’avoir un système d’extension de conteneur.
Par exemple, pour un packaging d’application, l’application elle-même pourrait être stockée dans un contentPack toujours présent mais les traductions dans différentes langues serait stockées dans des packs distincts. Le conteneur Jubako référence toutes les entrées (et donc toutes les traductions) mais un utilisateur peut avoir en local un conteneur qui contient seulement le contentPack « application » et la traduction dans sa langue.
Ou encore, le projet kiwix pourrait créer des archives avec les images dans des packs séparés, l’utilisateur téléchargerait des archives « sans » images. Et lorsqu’il a une bonne connexion, télécharge les packs manquants.

Bien sûr, c’est au vendeur de faire en sorte que son application sache quoi faire si un pack est manquant (planter, afficher un joli message d’erreur, proposer à l’utilisateur de télécharger le pack…)

Pour ce qui est des entrées stockées dans le directoryPack, c’est là aussi au vendeur de les définir. Le format des entrées est fixe pour chaque cas d’usage (ou alors il vous faudra gérer de la compatibilité entre les versions).
Pour permettre à Jubako (le lecteur, pas le format) de lire le conteneur, le format des entrées est stocké dans le conteneur lui-même.
Le schéma de données suit le principe suivant :

  • Plusieurs types d’entrées peuvent être stockés dans un seul conteneur. Elles sont stockées (et peuvent être récupérées) séparément. Cela permet par exemple de séparer les données selon leur signification dans le conteneur. Par exemple, les fichiers stockés dans une archive de fichier versus les métadonnées de cette même archive (auteur, date de création…).
  • Un type d’entrée est composé d’un ou plusieurs variants. C’est l’équivalent d’une union pour celles et ceux qui font du c/c++. Par exemple, le type des entrées dans une archive de fichier sera composée de trois variants Fichier, Dossier, Lien.
  • Chaque variant est composé d’un ensemble d’attributs (l’équivalent d’un struct en c/c++). Chaque attribut a un type défini (entier signé, non signé, tableau de données (de taille fixe ou non), identifiant de contenu), ce qui permet à Jubako de savoir comment lire ces attributs. Vous noterez que l’identifiant de contenu n’est qu’un attribut parmi d’autres. Une entrée peut donc pointer sur plusieurs contenus, ou aucun.

Chaque variant a sa propre structure, mais il est tout de même conseillé d’avoir des attributs communs entre les variants pour pouvoir faire de la recherche d’entrées basée sur ces attributs.

Toutes les entrées d’un même type sont stockées ensemble dans un sous conteneur (tableau) appelé EntryStore. Comme toutes les entrées ont la même taille, les attributs de taille variable sont déportés dans un ValueStore.
Enfin, des index permettent de pointer sur un sous-ensemble d’entrées dans un EntryStore en particulier. Ces index sont nommés et sont les points d’entrées pour la lecture des conteneurs.
Une application va identifier l’index dont elle a besoin et va « suivre » le fil de la structure de donnée pour lire les entrées et accéder au contenu.

Il y a encore pas mal de choses spécifiées (plus ou moins) telles que des redirections ou des entrées qui étendent (rajoutent des attributs) à d’autres entrées. Mais d’une part, cette présentation est déjà bien trop longue et d’autre part, c’est pas encore implémenté, donc on verra plus tard si vous le voulez bien.

La bibliothèque Jubako

Il s’agit de l’implémentation de référence (et unique, pour le moment) pour le format Jubako.
Elle est écrite en Rust parce que c’est à la mode c’est un des rares langages (à ma connaissance) qui soit en même temps de bas niveau (comme le C) et qui fournisse aussi des structures de haut niveau dans sa bibliothèque standard (comme le Python). Qui plus est, il fournit un certain nombre de garanties dans sa gestion mémoire qui permet d’avoir une certaine confiance sur un sujet aussi complexe/sensible que de générer/lire des fichiers binaires. (Ça n’empêche pas les bugs, je vous le garantis aussi :) )

(Je me devais de vous dire dans quel langage était écrit Jubako, mais ce n’est pas le plus important. Oui je sais qu’il y a plein de langages de qualité qui auraient pu être utilisés.)

Je ne vais pas trop m’étendre sur cette partie, l’API de la bibliothèque est pas encore sèche et la doc est inexistante pour le moment. Mais voici quand même un petit aperçu de la création et lecture d’un conteneur Jubako :

(Vous noterez une petite incohérence entre le nom des fonctions et des variables/définitions du format. C’est qu’en écrivant cette dépêche, je me suis rendu compte que la terminologie n’était pas bonne. J’ai écrit la dépêche avec une terminologie modifiée mais le code est encore avec « l’ancienne ».)

use jubako as jbk;
use jbk::reader::EntryTrait;
use std::error::Error;
use std::rc::Rc;
use typenum::{U31, U40, U63};

// Cela vous permettra de différencier votre conteneur parmi la multitude de conteneurs jubako qui seront créés (oui, oui, j’y crois)
const VENDOR_ID: u32 = 0x01_02_03_04;

fn main() -> Result<(), Box<dyn Error>> {
    let mut content_pack = jbk::creator::ContentPackCreator::new(
        "test.jbkc",
        jbk::Id(1), // L’id du pack tel que référencé dans le reste du conteneur
        VENDOR_ID,
        jbk::FreeData::<U40>::clone_from_slice(&[0x00; 40]), // Mettez ce que vous voulez, c’est pour vous
        jbk::CompressionType::Zstd,                          // L’algo de compression à utiliser
    );
    content_pack.start()?;

    let mut directory_pack = jbk::creator::DirectoryPackCreator::new(
        "test.jbkd",
        jbk::Id(0),
        VENDOR_ID,
        jbk::FreeData::<U31>::clone_from_slice(&[0x00; 31]),
    );

    // Les entrées ont une taille fixe. Donc pour les valeurs de taille variable (chaînes de caractères), il nous faut un stockage particulier
    let value_store = directory_pack.create_key_store(jbk::creator::KeyStoreKind::Plain);

    // On definit une entrée composée de deux variants
    let entry_def = jbk::creator::Entry::new(vec![
        jbk::creator::Variant::new(vec![
            jbk::creator::Key::PString(0, Rc::clone(&value_store)), // Une chaîne de caractères, à stocker dans value_store
            jbk::creator::Key::new_int(),                           // Un entier
            jbk::creator::Key::ContentAddress,                      // Un "pointeur" sur du contenu.
        ]),
        jbk::creator::Variant::new(vec![
            jbk::creator::Key::PString(0, Rc::clone(&value_store)),
            jbk::creator::Key::new_int(), //
            jbk::creator::Key::new_int(), //
        ]),
    ]);

    // Le store qui contiendra nos entrées.
    let entry_store_id = directory_pack.create_entry_store(entry_def);
    let entry_store = directory_pack.get_entry_store(entry_store_id);

    // On ajoute le contenu de notre entrée :
    let content: Vec<u8> = "Du super contenu de qualité pour notre conteneur de test".into();
    let mut reader = jbk::creator::BufStream::new(content, jbk::End::None);
    let content_id = content_pack.add_content(&mut reader)?;
    entry_store.add_entry(
        0, // On utilise le variant 0
        vec![
            jbk::creator::Value::Array("Super".into()),
            jbk::creator::Value::Unsigned(50),
            jbk::creator::Value::Content(jbk::creator::Content::from((
                jbk::Id(1), // L'id de notre pack
                content_id, // L'id du contenu dans le pack
            ))),
        ],
    );

    entry_store.add_entry(
        1, // On utilise le variant 1
        vec![
            jbk::creator::Value::Array("Mega".into()),
            jbk::creator::Value::Unsigned(42),
            jbk::creator::Value::Unsigned(5),
        ],
    );

    entry_store.add_entry(
        1, // On utilise le variant 1
        vec![
            jbk::creator::Value::Array("Hyper".into()),
            jbk::creator::Value::Unsigned(45),
            jbk::creator::Value::Unsigned(2),
        ],
    );

    // On créé un index qui nous permettra de retrouver nos entrées.
    directory_pack.create_index(
        "mon petit index a moi",
        jubako::ContentAddress::new(0.into(), 0.into()), // Un pointeur vers du contenu qui peut servir à stocker ce que vous voulez. (Rien en l’occurrence ici)
        0.into(),                                        // Notre index n'est pas trié
        entry_store_id,
        jubako::Count(3), // On a trois entrées
        jubako::Idx(0),   // Et on commence à l'offset 0 dans le entry_store.
    );

    let directory_pack_info = directory_pack.finalize()?;
    let content_pack_info = content_pack.finalize()?;
    let mut manifest_creator = jbk::creator::ManifestPackCreator::new(
        "test.jbkm",
        VENDOR_ID,
        jbk::FreeData::<U63>::clone_from_slice(&[0x00; 63]),
    );

    manifest_creator.add_pack(directory_pack_info);
    manifest_creator.add_pack(content_pack_info);
    manifest_creator.finalize()?;

    // Vous avez maintenant 3 fichiers "test.jbkm", "test.jbkc" et "test.jbkd".
    // N'en faisons qu'un seul
    jbk::concat(&["test.jbkm", "test.jbkc", "test.jbkd"], "test.jbk")?;
    // On a maintenant un 4ème fichier "test.jbk" qui contient les trois autres.

    // Un peu de lecture
    let container = jbk::reader::Container::new("test.jbkm")?; // ou "test.jbkm"
    let directory = container.get_directory_pack()?;
    let index = directory.get_index_from_name("mon petit index a moi")?;
    let resolver = directory.get_resolver(); // C'est nécessaire pour retrouver les infos dans value_store
    let finder = index.get_finder(Rc::clone(&resolver)); // On va enfin pouvoir lire nos données.

    let entry = finder.get_entry(jbk::Idx(0))?;
    assert_eq!(entry.get_variant_id(), 0); // On a bien le variant 0
    assert_eq!(
        resolver.resolve_to_vec(&entry.get_value(0.into())?)?,
        Vec::from("Super")
    );
    assert_eq!(
        resolver.resolve_to_unsigned(&entry.get_value(1.into())?),
        50
    );
    let value_2 = entry.get_value(2.into())?;
    let content_address = resolver.resolve_to_content(&value_2);
    // On affiche le contenu sur la sortie standard
    let reader = container.get_reader(content_address)?;
    std::io::copy(
        &mut reader.create_stream_all(),
        &mut std::io::stdout().lock(),
    )?;

    let entry = finder.get_entry(jbk::Idx(1))?;
    assert_eq!(entry.get_variant_id(), 1); // On a bien le variant 1
    assert_eq!(
        resolver.resolve_to_vec(&entry.get_value(0.into())?)?,
        Vec::from("Mega")
    );
    assert_eq!(
        resolver.resolve_to_unsigned(&entry.get_value(1.into())?),
        42
    );
    assert_eq!(resolver.resolve_to_unsigned(&entry.get_value(2.into())?), 5);

    let entry = finder.get_entry(jbk::Idx(2))?;
    assert_eq!(entry.get_variant_id(), 1); // On a bien le variant 1
    assert_eq!(
        resolver.resolve_to_vec(&entry.get_value(0.into())?)?,
        Vec::from("Hyper")
    );
    assert_eq!(
        resolver.resolve_to_unsigned(&entry.get_value(1.into())?),
        45
    );
    assert_eq!(resolver.resolve_to_unsigned(&entry.get_value(2.into())?), 2);

    Ok(())
}

L’outil Arx

Jubako c’est beau, mais franchement une lib… t’as pas mieux ? Un vrai cas d’usage ?

Si ! Et c’est Arx, un outil pour faire des archives de fichiers un peu comme Tar ou Zip. Et en plus, ça sert de démonstrateur pour Jubako.

Tar

Petite digression sur tar. Surtout si vous ne savez pas comment est structurée une archive tar.
Tar c’est vieux, très vieux (1979). Ça date d’une époque où les disquettes 3.5 étaient une révolution (elles apparaissent en 1982)

Une archive tar, c’est des entrées (header + contenu) mises bout à bout. C’est tout.
Un tar.gz, c’est un tar compressé avec gzip. Voilà.

Un tar ça marche bien pour du « streaming ». On crée l’archive simplement en parcourant le dossier à archiver et écrivant les entrées dans un « flux » (la sortie standard par exemple) dès qu’on les lit. On passe le flux à gzip et voilà.
Pour décompresser on fait l’inverse.
Et, comme on compresse toute l’archive « par l’extérieur », c’est probablement là qu’on a les meilleurs ratios de compression.
Par contre… accéder à une entrée en particulier… Ben il faut parcourir toute l’archive pour trouver l’entrée. Et pour parcourir toute l’archive, il faut la décompresser. Et ça prend du temps.

 Le format Arx

Il n’y a qu’un seul type d’entrée dans Arx et il est composé de trois variants :

  • Fichier
  • Dossier
  • Lien symbolique

Pour le moment, aucune méta-donnée sur les fichiers n’est stockée. Donc pas de owner, group, droit d’accès ou attributs étendus. Il vous faudra attendre un peu pour ça :) ARX utilise une structure en arbre. Chaque dossier pointe vers un « range » d’entrées qu’il contient. Chaque entrée (y compris les dossiers) contient l’id de son dossier parent. Le nom des entrées est le « basename ». On ne stocke pas tout le chemin de l’entrée.

Cette structure en arbre permet d’accélérer la recherche d’entrée puisqu’on n’a pas besoin de faire une recherche « linéaire » sur toutes les entrées. Cela permet aussi de gagner de la place puisqu’on ne stocke pas le chemin complet. En contrepartie, trouver le chemin complet à partir d’une entrée nécessite de remonter tous ses parents. Mais c’est un cas de figure assez rare.

L’outil arx est relativement simple et permet cinq opérations :

  1. créer une archive à partir d’un ou plusieurs dossiers/fichiers,
  2. extraire une archive dans un dossier,
  3. lister les entrées dans une archive,
  4. dumper (j’ai pas de traduction française) une entrée d’une archive,
  5. monter l’archive dans un point de montage.

Le code d’arx est assez simple (pour le moment). Il y a sept fichiers qui ne dépassent pas les 500 lignes de code chacun. Je vous invite vivement à aller le voir.

Un peu de comparaison

Mais du coup, arx, ça vaut quoi par rapport à tar ?

Faisons donc un peu de tests. Pour tester arx, j’ai utilisé les sources du kernel Linux (on est sur linuxfr ou pas ?). J’en ai fait trois cas de test différents:

  • Les sources au complet
  • Le dossier Documentation seulement
  • Le dossier drivers seulement

De plus, arx utilise zstd comme algo de compression. Donc pour éviter de comparer des pommes avec des poires, il faut comparer à un tar.zst, pas un tar.gz. Il y a bien une option chez tar pour compresser en utilisant zstd mais ça utilise pas la même config (niveau de compression) que arx, qui prend le parti de compresser au maximum au détriment du temps de compression. Du coup, l’archive tar.zst est faite avec : tar c linux-5.19 | zstd -19 -T8 -o linux.tar.zst. (niveau de compression 19, 8 threads).
Enfin, pour le contexte de test, les sources à compresser sont sur un SSD mais tout le reste c’est du tmpfs. Ce qui limite les entrées/sorties au minimum (mais en vrai ça change pas les ordres de grandeurs).

Voici les chiffres bruts, l’analyse arrive après :

Pour la partie documentation seulement (9194 entrées) :

Taille Création Extraction Listing Dump Dump / entry Mount diff
Source 58 MB 66ms
Tar zstd 7.8 MB 8s3 68ms 51ms 2m9s 43ms 2m38s
Arx 8.3 MB 8s7 100ms 5ms 8s9 3.4ms 324ms
Ratios 1.06 1.05 1.47 0.1 0.07 0.002

Pour la partie drivers seulement (33056 entrées) :

Taille Création Extraction Listing Dump Dump / entry Mount diff
Source 865 MB 490ms
Tar zstd 73 MB 1m7 688ms 570ms 1h36 520ms 2h41m
Arx 80 MB 3m25 930ms 19ms 35s 3.1ms 1s75
Ratios 1.09 3 1.35 0.03 0.006 0.00018

Et pour les sources complètes (81958 entrées) :

Taille Création Extraction Listing Dump Dump / entry Mount diff Compilation
Source 1326 MB 880ms 32m
Tar zstd 129 MB 1m37s 1s130ms 900ms 6h20m 833ms
Arx 140 MB 4m45s 1s47ms 45ms 1m28s 4ms 4s2 48m
Ratios 1.08 2.93 1.3 0.05 0.0045

La taille

  • Les sources décompressées du kernel font 1,3 Go.
  • L’archive tar.zst fait 129 Mo. (Un ratio de compression de 9,76%).
  • L’archive arx elle fait 140 Mo. (Un ratio de compression de 10,56%).

Arx compresse moins bien. On a environ 8 % d’écart entre les deux archives. On s’y attendait au vu des structures de chaque archive, mais perso je trouve pas ça dégueu. Surtout comparé au 1,3 Go d’origine. Et pour info, le tar.gz utilisé par tout le monde, fait 200 Mo et ça a pas l’air de gêner grand monde alors bon, 140 Mo, ça va.

Le temps de création

La création de l’archive kernel.tar.zst se fait en 1m37s alors que l’arx se fait en 4m45s. Sans surprise ici, tar est bien plus rapide. On a globalement un rapport de 3 entre les temps de création. C’est en accord avec le cas d’usage de Jubako (temps de création plus lent, mais exploitation plus rapide) mais la création est probablement la partie la moins optimisée pour le moment. Il devrait être possible d’améliorer les perfs du côté de Jubako pour limiter l’écart (notamment compresser les contenus en parallèle).

Le temps d’extraction

Le temps d’extraction se fait dans des temps relativement similaires : 1s1 pour tar et 1s5 pour arx. Là aussi, c’est en accord avec la structure des données des deux formats. Tar n’a qu’un seul flux à décompresser. Alors que Jubako doit parcourir les entrées et doit ensuite décompresser plusieurs flux internes (avec tout le temps d’initialisation associé). Mais, là encore, il y a probablement matière à amélioration du côté de Jubako/arx.

Le temps de listing

Ici on n’extrait pas le contenu, mais on liste les fichiers dans l’archive. Il faut 10 à 20 fois moins de temps à arx pour lister le contenu de l’archive par rapport à tar. Là aussi, c’est cohérent avec la structure de données des deux formats. Jubako n’a aucun contenu à décompresser et il n’a que le directoryPack à lire, alors que tar doit décompresser toute l’archive.

On commence à toucher à des cas d’usage pour lesquels Jubako a été conçu : accéder aux données sans extraire toute l’archive.

Le temps de dumping

Par dumping j’entends extraire un seul fichier en particulier d’une archive.
Le cas de test est d’extraire un fichier sur trois de l’archive.
Pour le kernel au complet, ça veut dire extraire 27319 fichiers de l’archive (et donc lancer 27319 fois tar/arx)

Il faut 1m30 à arx pour faire le travail. Du côté de tar il faut… 6h20.
Ça fait une moyenne de 4ms par entrée du côté d'arx et 0,8s pour tar.
C’est un ratio de 200 !!

Si on réduit la taille des archives (ce qui limite la quantité de données à décompresser pour tar), le ratio baisse un peu, mais on reste quand même avec arx 12 fois plus rapide que tar pour la documentation. (3064 fichiers à dumper)

On voit ici clairement l’avantage de Jubako sur tar. C’est prévisible, rien de magique, on travaille juste avec un format de fichier fait pour ça, c’est normal que ça dépote.

Cela met aussi en évidence le fait qu’arx met un temps relativement constant pour extraire un fichier, quelle que soit la taille de l’archive.
À l’inverse pour tar, les temps d’extraction augmentent avec la taille des archives.

Mount

Un mount tout seul ne prend pas de temps, il faut voir quand on veut lire les fichiers. Le test correspond donc à un mount suivi d’un diff -r entre ce qui a été monté et les sources d’origine. C’est le temps de diff qui est mesuré. L’archive tar est montée avec l’outil archivemount.

Bon, là c’est clair, arx est bien bien bien plus rapide que tar : un diff sur une archive arx monté est de 490 à 5500 fois plus rapide qu’un diff sur une archive tar. (Je n’ai pas osé faire le test sur le kernel complet, mais je vous laisse le faire si vous voulez). On notera quand même que le diff prend quatre à cinq fois plus de temps qu’un diff simple entre deux dossiers (sans mount).

Mais diff, c’est un cas vachement particulier, on parcourt certes tous les fichiers, mais c’est assez séquentiel et on y accède qu’une seule fois. Ça donnerait quoi avec un vrai cas d’usage ?

Du coup, compilons un kernel…
La compilation du kernel simplement (configuration par défaut, make -j8, les sources extraites dans le fs, sur sdd) prend 32 minutes sur ma machine. La même compilation mais sur une archive montée (archive elle-même stockée sur le sdd, pas dans tmpfs) prend 48 minutes. Alors oui, ça prend 1,5 fois plus de temps, mais sachez que l’implémentation actuelle de arx mount est mono-threadée, donc le make -j8 en prend un coup de base. Mais vous n’avez utilisé que 140 Mo d’espace disque au lieu de 1,3Go pour stocker les sources du kernel.

Utilisation mémoire

Niveau utilisation mémoire, Jubako est, normalement, relativement sobre.

La partie index (stockée dans le directoryPack) est directement exploitable par Jubako. Rien n’est compressé et Jubako est conçu pour lire directement les données stockées dans le fichier. Bien sûr, ça engendre beaucoup d’I/O et c’est donc au détriment des performances. Pour réduire ça, différentes stratégies sont utilisées (bufferisation, mmap, cache…). Mais dans un contexte vraiment limité en mémoire, c’est désactivable (sur le principe, il y a pas vraiment d’options aujourd’hui dans l’implémentation). De toute façon, le directoryPack de l’archive de l’ensemble des sources du kernel fait 2 Mo. Donc on pourrait tout mettre en ram sans que ça ne pose de problème.

Au niveau des données compressées, ça nécessite un peu plus de données. Les données sont regroupées en « clusters » et les clusters sont compressés indépendamment. Donc pour accéder à un contenu, il faut décompresser au pire un cluster entier. L’implémentation actuelle crée des clusters de 4 Mo maximum. Donc sans cache, accéder à un contenu couterait 4 Mo max (plus les structures internes utilisées par les algorithmes de (dé)compression).

En pratique, il y a du cache mis en place (et pas obligatoirement de la manière la plus optimisée). Jubako fait de la décompression partielle : il commence à décompresser un cluster jusqu’aux données auxquelles on veut accéder. Mais pour éviter de décompresser à nouveau le début du cluster plus tard, on garde en mémoire le contexte de décompression en mémoire. Donc chaque cluster a certes 4 Mo maximum, mais on garde plus en mémoire. Et actuellement, on a un cache de 20 clusters en mémoire. Donc environ 80 Mo plus les 20 contextes de décompression (et je n’ai pas mesuré leur taille).

Au global, un arx mount pendant une compilation de kernel consomme 310 Mo au maximum (Maximum resident size). Il y a un peu de travail d’implémentation et d’ajustement pour avoir le meilleur compromis mémoire utilisée/performance. Et probablement de la configuration nécessaire pour s’adapter aux différents cas d’usage.

Conclusion

Voilà pour une première présentation de Jubako et Arx.

On est loin d’avoir un produit fini. Les specs de Jubako ne sont pas complètes. L’implémentation n’implémente pas tout ce qui est spécifié (et la spec risque de va changer avec l’implémentation).
La bibliothèque Jubako elle-même est encore très jeune. Elle fonctionne, mais elle n’est probablement pas exempte de bugs, l’api est à améliorer, sans parler des performances.

Pour ce qui est de Arx, là aussi on en est au début. Arx stocke très peu de métadonnées sur les fichiers pour le moment, mais c’est une base et elle sert de très bon démonstrateur pour Jubako.

La différence entre ce qui est perdu (taille de l’archive, temps de compression) et ce qui est gagné (utilisation d’une archive sans la décompresser entièrement) est plus que raisonnable pour moi (surtout pour une première version).

Il y a encore pas mal de choses à faire mais c’est un premier jalon important pour moi de voir un projet imaginé il y a maintenant deux ans prendre forme et arriver à un résultat assez probant. (J’ai d’ailleurs eu du mal à me restreindre à faire une dépêche courte, j’en suis désolé… ou presque).

Jubako et Arx peuvent avoir leur utilité dans des cas d’usage particuliers. Pour ce qui est de la classique archive de fichiers qui sera extraite, il est fort probable que tar reste la référence. Mais Jubako ouvre une porte sur de toutes nouvelles façons de faire. Il serait possible de diffuser du contenu et de le lire sans le décompresser. Imaginez un peu :

  • Une distribution Linux basée sur Jubako pour ses paquets. Des paquets qui ne seraient jamais décompressés mais montés à la demande…
  • Un système de sauvegarde (une sauvegarde incrémentale ne serait qu’une archive Jubako qui réutilise le contentPack des sauvegardes précédentes)…
  • Si python savait lire des archives Jubako…
  • Si on diffusait nos sites web statiques à coup de conteneur Jubako (ou de serveurs autonomes qui contiennent des conteneurs Jubako sous forme de ressources intégrées) …
  • Si les navigateurs web savaient lire du Jubako et qu’on packageait nos applis JavaScript à coup de Jubako…

Comment ça je m’emballe ?!

Aller plus loin

  • # Bravo !

    Posté par  . Évalué à 7.

    Sujet très intéressant et article très bien écrit.

    • [^] # Re: Bravo !

      Posté par  . Évalué à 5.

      Je trouve l'idée très sympa, je suis étonné qu'il n'y ait pas déjà des format d'archive proposant ce genre de fonctionnalités.

      Par contre, je trouve l'impossibilité de faire de modification sur les fichiers, ou d'ajouter des fichiers, assez limitante. En tout cas, il ne me semble pas avoir vu d'indication comme quoi c'était possible.

      Même dans le cadre de la distribution d'une application, il n'est pas rare d'avoir quelques fichiers écrits sur le système de fichier, certains font ça proprement en fonction de l'OS sur lequel ils tournent, d'autres font ça juste à côté du binaire, ce qui a des avantages et inconvénients.

      J'imaginais aussi pouvoir faire packager un projet musical, tout en ayant la possibilité d'aller le modifier une fois monté. L'essentiel des accès sont en lecture, mais quelques modification pourraient encore être faites niveau métadonnées par exemple.

      Est-ce que cette limitation est d'ordre technique sur le format de fichier choisi ? ou une contrainte choisie pour optimiser ?

      Bon boulot en tout cas :-)

      • [^] # Re: Bravo !

        Posté par  (site web personnel) . Évalué à 7.

        Est-ce que cette limitation est d'ordre technique sur le format de fichier choisi ? ou une contrainte choisie pour optimiser ?

        Si tu parles de Jubako quand tu dis format de fichier, c'est moi qui l'ai créé, donc on peut pas vraiment parler de contrainte :)

        En fait ce n'est pas facile de créer des formats de fichier modifiables. Quand t'y pense, même un simple fichier texte est rarement modifier. En général il est recréer. Et c'est compréhensible, si tu rajoutes un caractère en plein milieu, ça veut dire que tu dois décaler la deuxième partie du fichier, donc réécrire la moitié du fichier. C'est bien plus simple de considéré le fichier comme "recréable" plutôt que comme modifiable.

        De même au niveau de l'implémentation. Si tu considères ton conteneur comme non modifiable ça simplifie grandement le code. Tu peux mettre en place du cache ou du multithreading bien plus facilement que si tu dois gérer des potentielles modifications concurrentes.
        Pareil pour la création, c'est plus simple d'implémenté un créateur qui prend du contenu et l'écrit dans un fichier sans se poser la question de fournir un accès en lecture performant sur ce même contenu.

        Mais Jubako permettrait de créer des conteneurs facilement recréable grâce au concept de Pack. Si tu as une archive avec N contentPacks, tu peux créer un N+1ieme pack qui contient ton contenu à rajouter et tu refais seulement le directoryPack pour rajouter un entrée qui pointe vers ton nouveau contenu. (Ou tu fais pointer une entrée existante)
        On peut même aller un peu plus en considérant que le N+1 pack est celui qui contient les modifications et qu'on recrée ce pack à chaque fois.

        J'envisage même de potentiellement créer un "sous-format" de type "Overlay" pour standardiser ça.

        J'imaginais aussi pouvoir faire packager un projet musical, tout en ayant la possibilité d'aller le modifier une fois monté. L'essentiel des accès sont en lecture, mais quelques modification pourraient encore être faites niveau métadonnées par exemple.

        Dans ton cas tu pourrais stocker les métadonnées soit sous la forme de propriétés des entrées soit comme deuxième contenu associé aux entrées (mais stocker dans un pack différent que tes données). Quand tu modifies les métadonnées, tu ne modifie que le directoryPack (dans le premier cas) ou que le pack de métadonnées (et si tu te débrouille bien, tu n'as pas à recréer le directoryPack).

        Mais niveau implémentation, on est encore loin de tout ça. C'est que le début :)

        Matthieu Gautier|irc:starmad

        • [^] # Re: Bravo !

          Posté par  (site web personnel) . Évalué à 4.

          Ton système d'overlay ressemble au système de fichier de docker non ?

          "La première sécurité est la liberté"

          • [^] # Re: Bravo !

            Posté par  (site web personnel) . Évalué à 3.

            Tout à fait. Et aussi à unionfs et overlayfs et …

            Dans l'idée, tu pourrais réimplémenter docker en utilisant une solution basé sur Jubako
            (Je dis pas que c'est une bonne idée par contre).

            Matthieu Gautier|irc:starmad

            • [^] # Re: Bravo !

              Posté par  (site web personnel) . Évalué à 3.

              Dans ce cas, c'est quoi la différence ?

              Je vois bien la complexité d'un code de système de fichier, mais je vois bien sa vitesse. Par exemple, il existe aussi le système de fichier de CD/DVD utilisé depuis Knoppix qui est aussi compressé.

              Qu'apporte Jubako de plus ?

              "La première sécurité est la liberté"

              • [^] # Re: Bravo !

                Posté par  (site web personnel) . Évalué à 3. Dernière modification le 07 novembre 2022 à 12:14.

                Jubako n'est pas un système de fichier. Certes il y a Arx qui implémente une archive de fichiers et donc ressemble un peu à un FS readonly, mais ce n'est qu'un cas d'usage de Jubako parmi d'autres.

                • Pour un stockage style kiwix, tu as besoins de l'url de la ressource web et son mimetype, sans système d’arborescence.
                • Pour un fichier d'archive, il faut une arborescence et le stockage de toutes les métadonnées d'un fs.
                • Pour un outils de backup, tu as potentiellement moins d'info à stocker (les dates de création/modification ?) ou plus (un checksum du contenu).
                • Pour un outils d'archivage, tu es probablement peu intéressé par les droits du fs, l'owner/group… mais tu veux d'autres info (catégorisation, …)
                • Pour un distribution de logiciel type rpm/deb, tu te fous des métadonnées, tu veux savoir où installer des fichiers. Par contre tu veux des métadonnées de plus haut niveau (description, version, dépendances, …)
                • Un système comme textbundle n'a pas à être obligatoirement structuré avec un zip.
                • J'ai entendu parlé d'usage où on diffusait des mise à jour de programme télé aux décodeurs des particuliers avec des bdd sqlites. Jubako pourrait être utilisé dans ce cas (je sais pas si ça serait plus adapté ou non, mais c'est un cas d'usage qui sort du classique archive de fichiers)

                UnionFs, SquashFS, le système de Docker ou Knoppix sont orientés FS. C'est assez générique et tout le monde y est habitué ("tout est fichier" de Unix) mais Jubako sort de ce paradigme et te permet de faire autrement. Arx nous ramène à ça mais je l'ai juste implémenté en premier parce que c'était assez facile et avec des résultats facilement comparables à l'existant. Un avantage de Jubako c'est sa modularité.

                Une autre différences, c'est de découplage entre la partie "index" (directory dans la terminologie Jubako) et le contenu. Par exemple pour un système de backup, tu pourrais envoyer tout le contenu sur un serveur distant et garder en local que le directory. Lors des sauvegarde incrémentale, tu as accès, en local, à la liste des fichiers déjà sauvegarder et leur checksums. Tu pourrais faire ton backup totalement en offline, sans avoir toute les sauvegarder en local (et leur occupation disque) et quand même faire de l'incrémental. Tu peux bien sûr faire la même chose avec un json que tu gardes en local, mais Jubako te fournit tout de base.


                J'ai évité d'implémenter un équivalent à zim en premier cas d'usage justement pour pas être comparé seulement à zim et limité jubako à un successeur du format zim. Ne comparez pas que Arx avec l'existant :)


                J'ai quand même fait un comparatif avec squashfs, ça manque effectivement à la dépêche. J'ai refait les mêmes tests que dans la dépêche (linux complet) avec squasfs. Par défaut squasfs utilise les 16 cœurs de ma machine mais arx est monothreadé. Donc il y a deux séries de tests, un avec squashfs 16 cœurs (pour voir des performances "optimales") et 1 cœur (pour comparer correctement)

                Taille Création Extraction Listing Dump Dump / entry Mount diff
                Source 1326 MB 880ms
                squashfs 177 MB 1m16s 0s730ms 30ms 1m02s 2ms 2,8
                squashfs monothread 177 MB 4m38s 1s070ms 30ms 1m02s 2ms 2,8
                Arx 140 MB 4m45s 1s47ms 45ms 1m28s 4ms 4s2

                Globalement :
                - arx compresse mieux
                - Est plus lent que squashfs mais reste dans des ordres de grandeurs comparables.

                Et il faut se souvenir que squashfs est implémenté dans le kernel (Je sais pas quelle partie est réellement dans le kernel et dans l'userpace pour le listing des fichiers mais pour ce qui est du mount, c'est totalement dans le kernel). Ça m'aurait surpris que arx soit plus rapide qu'un module kernel.

                C'est aussi une différence de Jubako avec les autres systèmes. Jubako c'est un lib, elle se veut indépendante de l'OS. À terme, Jubako sera disponible sous Windows et Mac aussi (voir même dans le web en compilant jubako en wasm). Squashfs, t'es limité à linux.

                Matthieu Gautier|irc:starmad

        • [^] # Re: Bravo !

          Posté par  . Évalué à 2.

          Si tu as une archive avec N contentPacks, tu peux créer un N+1ieme pack qui contient ton contenu à rajouter et tu refais seulement le directoryPack pour rajouter un entrée qui pointe vers ton nouveau contenu. (Ou tu fais pointer une entrée existante)

          J'imagine qu'en cas de suppression, il faudrait rebundler les content packs en recréant celui qui contient le fichier supprimé par exemple.

          Dans ton cas tu pourrais stocker les métadonnées soit sous la forme de propriétés..

          Dans ce cas, il faudrait une implementation dédiée. J'imaginais plutôt pouvoir monter le fichier à la manière d'une iso, effectuer les modifications nécessaires, et le bundle serait recréé au démontage (ou à la volée).

    • [^] # Re: Bravo !

      Posté par  (site web personnel) . Évalué à 4.

      Merci. Il faut aussi remercier les relecteur·trice·s qui ont grandement amélioré ma première version.

      Matthieu Gautier|irc:starmad

  • # Une base de données ?

    Posté par  . Évalué à 2.

    Hello !

    Merci pour cette dépêche dont j’ai beaucoup aimé la lecture !

    Je me demande s’il est envisageable d’utiliser une base de données style SQLite pour répondre à certaines problématiques posées dans la dépêche.
    En effet, décompresser toute ou partie d’une archive très rapidement, assembler n’importe quel(s) type(s) d’objet(s) dans un fichier unique, définir sa propre nomenclature en fonction du domaine ciblé, bah… db.sqlite le fait très bien, et avec de très bonnes performances. Ça résout également, à ma connaissance, la possibilité de modifier tout ou partie d’un objet sans avoir à recréer l’archive entière (même si ce n’est pas l’objet du format j’en conviens). Là où j’émets un prout cependant, c’est le support de la compression… Je ne sais pas du tout comment ça se passe avec SQLite.

    Qu’en pensez-vous ?

    • [^] # Re: Une base de données ?

      Posté par  (site web personnel) . Évalué à 3.

      Il faudrait voir ce que donne un sqlite compressé read-only. Mais en général, les performances d'accès à un fichier sont bien plus rapide que les performances d'accès à un élement d'une base de donnée.

      Sinon son système ressemble à un système de fichier complet read-only. Je ne sais pas ce que donnerait ext4+zstd dans un fichier.

      "La première sécurité est la liberté"

      • [^] # Re: Une base de données ?

        Posté par  (site web personnel) . Évalué à 4. Dernière modification le 06 novembre 2022 à 19:33.

        J'ai pas trop regarder du coté de sqlite. Mais à première vue je dirais déjà que c'est pas fait pour la même chose. Sqlite est plutôt orienté pour du stockage de "petites" données (métadonnées), même si il sait stocker des grosses données. Jubako est plutôt orienté "grosses" donnée (contenu de fichier par exemple, même si il sait stocker des métadonnées aussi.
        De la même manière sqlite est orienté pour faire des requêtes complexes. C'est pas le cas de Jubako.

        Après il y a aussi la question de la compression. Compresser un base de donnée sqlite revient au même problème que tar, il faut tout décompresser pour accéder à la base. Ou alors
        il faut stocker le contenu de chaque "fichier" compressé individuellement mais là on serait dans le cas de zip et donc avec des taux de compression moindre.
        Jubako (comme zim) est au niveau intermédiaire et compresse les données par "cluster", ce qui permet d'avoir un meilleur taux de compression que zip mais un accès plus "random" que tar.

        Matthieu Gautier|irc:starmad

        • [^] # Re: Une base de données ?

          Posté par  . Évalué à 5.

          Merci pour cette réponse ! Ça clarifie pas mal de choses pour moi :)

          Après une petite recherche sur sqlite, j’ai sûrement bien sous-estimé les inconvénients de stocker de gros fichiers dans ce genre de dbs…
          Ça n’a vraiment pas l’air fait pour.

          Une question annexe, car étant donné ma première expérience malheureusement assez mauvaise avec Zim + Kiwix, j’espère pouvoir profiter de ton expertise :

          L’objectif, c’était de consulter un site hors-ligne, et éventuellement d’en garder une copie s’il venait à disparaître.

          J’ai immédiatement pensé à la solution wget -R, mais j’ai eu vent de l’existence de Zim par les communautés qui font des miroirs de Wikipedia. Je me suis donc dit : « Pourquoi pas ? Cette solution semble plus résiliente ».

          Mais j’ai vite déchanté : mon premier problème concerne la difficulté de créer la dite archive. Impossible de trouver un outil à la ligne de commande facilement installable, donc j’ai finis par me rabattre, à contre-cœur, sur un site « Zim It » (dont je ne retrouve plus l’adresse…) qui m’a permis, étant donné l’URL, de télécharger l’archive Zim correspondante.

          Ensuite, il s’est agit d’être en mesure de consulter cette archive, et depuis mon téléphone de préférence. J’ai naturellement trouvé Kiwix, que j’ai pu installer sans trop de problème.
          Cependant, je l’ai trouvé à l’usage beaucoup plus lent que mon navigateur, et malgré quelques fonctionnalités sympathiques (découvrir de nouveaux sites…), pas si ergonomique (mais j’ai l’impression que ç’a changé dans les dernières versions) \o/

          Et enfin, pour couronner le tout, la taille de l’archive était nettement plus importante que la taille du site téléchargé avec wget -R (avec du -h) ! /o\

          J’ai donc abandonné l’idée, je suis revenu à une version hors ligne dans mon navigateur, téléchargée avec un wget -R bien plus simple à effectuer, plus petit, et plus rapide à consulter.

          Pour résumer, mes questions sont les suivantes :

          • Qu’est-ce que j’ai raté pour créer l’archive facilement, en tant que commun des mortel·le·s ?
          • Est-ce que la taille de l’archive est dû au site utilisé, ou bien est-ce un « problème » connu ? (pour référence, le site c’est https://www.libraryofjuggling.com)
          • Suis-je vraiment le type d’utilisateur·ice envisagé par Zim ?
          • Si oui, comment peut-on améliorer cette première expérience avec Zim ?

          Je suis très enthousiaste de voir ce nouvel outil apparaître, parce que je pense qu’il y a un réel besoin autour de ces problématiques, et c’est chouette que l’on puisse généraliser et affiner ce qui existe avec Zim :)

          • [^] # Re: Une base de données ?

            Posté par  (site web personnel) . Évalué à 6. Dernière modification le 07 novembre 2022 à 13:25.

            Mais j’ai vite déchanté : mon premier problème concerne la difficulté de créer la dite archive. Impossible de trouver un outil à la ligne de commande facilement installable, donc j’ai finis par me rabattre, à contre-cœur, sur un site « Zim It » (dont je ne retrouve plus l’adresse…) qui m’a permis, étant donné l’URL, de télécharger l’archive Zim correspondante.

            Ce n'est pas évident du tout de refaire des sites locaux à partir de site "en-ligne". On a des outils spécialisés pour les gros sites qu'on fourni (mwoffliner pour tout les mediawiki, sotoki pour stackoverflow, …). Mais ils sont vraiment spécifique au format du site.
            Et ils ne sont pas forcement sobre en ressources et en temps.

            Le service zimit est là : https://youzim.it/

            Qu’est-ce que j’ai raté pour créer l’archive facilement, en tant que commun des mortel·le·s ?

            Pas vraiment, non. Mais si t'es content·e de la version offline créée avec wget -R, tu peux la mettre dans un zim avec zimwriterfs.

            Est-ce que la taille de l’archive est dû au site utilisé, ou bien est-ce un « problème » connu ? (pour référence, le site c’est https://www.libraryofjuggling.com)

            Non, c'est pas connu, et c'est surprenant. Si t'as le temps, tu peux créer une issue sur libzim avec toute les infos, ça nous aiderait beaucoup.

            Après, on stocke une base de données xapian dans le zim pour faire de la recherche fulltext, c'est peut-être elle qui gonfle l'archive.

            Suis-je vraiment le type d’utilisateur·ice envisagé par Zim ?

            Oui :)
            On est plus orienté utilisateurs finaux qui vont juste consulté les archives mais c'est principalement du à la difficulté de scrapper des sites de manière générique (d'où la création de youzim.it). Dans l'idée on s'adresse aussi au gens qui veulent créer leurs archives.

            Si oui, comment peut-on améliorer cette première expérience avec Zim ?

            Déjà, en remontant l'information. Ça nous permettra au moins de savoir où il faut s'améliorer.
            Ensuite, c'est un projet libre. Nous sommes grandement ouvert aux contributions, n'hésitez pas à venir nous parler et à corriger les problèmes.

            Matthieu Gautier|irc:starmad

            • [^] # Re: Une base de données ?

              Posté par  . Évalué à 2. Dernière modification le 10 novembre 2022 à 01:01.

              Hello !

              Le service zimit est là : https://youzim.it/

              Merci :)
              Est-ce normal que le service mette plus de deux jours pour me répondre…?
              Je peux également ouvrir un ticket avec des données plus précises là-dessus, mais je suppose que ça dépend simplement des ressources de l’infra, donc ce n’est peut-être pas nécessaire ?

              […] Pas vraiment, non. Mais si t'es content·e de la version offline créée avec wget -R, tu peux la mettre dans un zim avec zimwriterfs.

              Oh ! Merci, j’ai essayé ça, et je suis très satisfait de cette solution pour le moment \o/

              D’ailleurs, c’est -r pour le téléchargement récursif ; je me suis trompé sur la casse…

              Non, c'est pas connu, et c'est surprenant. Si t'as le temps, tu peux créer une issue sur libzim avec toute les infos, ça nous aiderait beaucoup.

              OK, je trouve un peu de temps pour ouvrir un ticket car j’ai réussi à reproduire le problème :/

              Après, on stocke une base de données xapian dans le zim pour faire de la recherche fulltext, c'est peut-être elle qui gonfle l'archive.

              OK, au vu des ratios, c’est une explication qui se tiendrait plutôt bien je pense.
              Après, je demanderai dans le ticket comment je peux vérifier ça manuellement ;)

              Merci beaucoup pour le travail de la communauté, et j’espère pouvoir contribuer un peu de mon temps et de mon énergie car le projet me parle !

              À bientôt o/

              • [^] # Re: Une base de données ?

                Posté par  (site web personnel) . Évalué à 3.

                Est-ce normal que le service mette plus de deux jours pour me répondre…?

                De manière surprenante, on a eu plein de requêtes depuis que j'ai mis le lien ici :)
                On avait un grosse centaine de tâches en attente alors que d'habitude on est à moins de 10.
                Et comme on a un seul worker, faut prendre son mal en patience.

                Ça devrait être un peu plus normal maintenant.

                Matthieu Gautier|irc:starmad

                • [^] # Re: Une base de données ?

                  Posté par  . Évalué à 0.

                  De manière surprenante, on a eu plein de requêtes depuis que j'ai mis le lien ici :)

                  Hahaha ! Excellent !

                  J’ai créé un ticket ici pour la question de la taille de l’archive du coup.

                  Pour ce qui est de l’accessibilité pour les personnes débutantes, ou qui ne connaissent pas mais à qui ça pourrait être utile, j’essaierai, si j’en ai le temps, de m’investir sur un autre ticket !

      • [^] # Re: Une base de données ?

        Posté par  . Évalué à 7.

        Il faudrait voir ce que donne un sqlite compressé read-only

        ça existe !

        Mais en général, les performances d'accès à un fichier sont bien plus rapide que les performances d'accès à un élement d'une base de donnée

        ça se discute…

    • [^] # Re: Une base de données ?

      Posté par  (site web personnel) . Évalué à 6.

      Là où j’émets un prout cependant, c’est le support de la compression… Je ne sais pas du tout comment ça se passe avec SQLite.

      Il existe un format d'archives basée sur sqlite : sqlar

      Le post ci-dessus est une grosse connerie, ne le lisez pas sérieusement.

  • # tard…

    Posté par  (site web personnel, Mastodon) . Évalué à 3.

    Je ne pige pas : avec arx peut-on streamer sur une cassette magnétique ?

    “It is seldom that liberty of any kind is lost all at once.” ― David Hume

  • # Dar

    Posté par  . Évalué à 4.

    Je ne connais l'outil dar (disk archive) que de nom, mais à l'époque ou j'en avais entendu parlé, on me l'avait vendu comme "tar, mais avec des capacités d'accès aléatoire, car conçu pour des disques et non des bandes".

    https://github.com/Edrusb/DAR (développé par un français en plus)

    Je découvre à l'instant qu'il s'agit en fait d'un outil de backup, qui possède des fonctionnalités que tar n'a pas, comme le backup incrémental (ce qui pourrait faire office d'overlay ?).

    Les toutes dernières versions semble également supporter zstd.

    Bref, je serai curieux de voir si dar répond au mêmes besoins qu'arx, et ou il se place d'un point de vue performance.

    • [^] # Re: Dar

      Posté par  (site web personnel) . Évalué à 2.

      Je connaissais pas du tout dar. Je vais regarder.

      À première vue, Dar est quand même bien orienté FS. Jubako se veut plus généraliste.
      J'ai encore des comparaisons à faire :)

      Matthieu Gautier|irc:starmad

    • [^] # Re: Dar

      Posté par  . Évalué à 2.

      des fonctionnalités que tar n'a pas, comme le backup incrémental

      tar supporte les archives incrémentales (--listed-incremental)

  • # Ressemble beaucoup à Frost

    Posté par  . Évalué à 8.

    C'est assez marrant en soi, car ton projet ressemble beaucoup à ce que j'avais fait avec Frost. Le programme est un logiciel de backup avec déduplication basée sur le contenu (donc capable de détecter l'ajout d'un caractère dans un fichier texte et de ne stocker qu'un bloc autour du changement).

    Et on retrouve les mêmes idées que pour Jubako, notamment pour le format d'archive.
    J'avais commencé avec du SQLite3 pour l'index (ce que tu appelles Directory + Metadata) mais c'était vraiment trop lent lorsque le nombre de fichier devenait grand (et ce, malgré les index dans SQLite).

    Du coup, j'ai créé une archive à la mano, qui est mmappée, et modifiable par génération (c'est à dire que la nouvelle génération peut soit référencer la précédente, soit l'écraser totalement ou partiellement, suivant ce qui est le plus efficace).

    Elle fonctionne en gros comme Jubako, mes ContentPack c'est des fichiers indépendants (qui sont compressés, coupés en bloc de taille fixe, puis chiffrés) et mon fichier d'index qui contient les Directory + Metadata (comme c'est du backup, j'y stocke toutes les métadonnées POSIX).

    Ça fait 7 ans, utilisé quotidiennement sur 34To et je ne regrette pas du tout d'y avoir investi du temps, c'est super efficace, ça m'a sauvé plus d'une fois.

    Si je peux de donner des conseils sur l'évolution du format, il faut absolument regarder du côté des structures mmapables (le gain de performance est délirant) et du stockage de différences (je détecte les différences via un rolling checksum à points de division fixe, ce qui permet de s'adapter à de nombreux formats, comme le texte ou les formats binaires). Ainsi, tu peux découper tes content pack en fonction du contenu (et en virant donc tous ce qui se déduplique facilement). Tu peux aussi gérer l'ajout dans un conteneur, simplement en stockant la différence avec la version présente dans le conteneur.

    Dans mon cas, je recalcule toujours la différence entre la version actuelle et la dernière version et je modifie la dernière version pour arriver à la version actuelle, ce qui est beaucoup plus efficace que le contraire.

    • [^] # Re: Ressemble beaucoup à Frost

      Posté par  (site web personnel, Mastodon) . Évalué à 4.

      Comment Frost se compare par rapport à Borg ?

      “It is seldom that liberty of any kind is lost all at once.” ― David Hume

      • [^] # Re: Ressemble beaucoup à Frost

        Posté par  . Évalué à 9. Dernière modification le 08 novembre 2022 à 10:35.

        Borg est apparu après que j'ai commencé Frost.

        J'ai d'ailleurs une issue encore ouverte qui me demande de comparer avec Frost.
        Donc pour résumer à ce que j'ai compris:

        1. Borg fait de la déduplication comme Frost, en utilisant un rolling checksum (type Adler32 chez Frost, ppargon je crois chez Borg)
        2. Borg est écrit en python (Frost est natif, non interprété) avec le code de la crypto en C/Cython, Frost utilise OpenSSL et est donc accéléré en HW si disponible.
        3. Borg compresse en LZ4, Zlib, Zstd (Frost, c'est seulement en Zlib et BSC, une sorte de LZMA 2x plus efficace). J'avais des plans pour implémenter Zstd, mais finalement, BSC, ça marche suffisamment bien, j'ai eu la flemme de changer.
        4. Chaque chunk de données est identifié par son checksum, un hash SHA1 (soit 24 octets) chez Frost, la corruption est très improbable. Chez Borg, c'est un SHA256 (soit 32 octets). Je m'attends à ce que l'index chez Borg soit au moins 33% plus gros.
        5. Borg v2 fait de l'authenticated encryption (AES_OCB/AEAD) alors que Frost fait du Encrypt en AES_CTR then MAC.
        6. Les 2 fournissent un "driver" FUSE pour monter les archives
        7. Borg ne fait pas de mmap sur ses index, et donc il faut avoir suffisamment de mémoire vive pour charger l'index en entier dans la mémoire. Ce qui limite, AMHA, à environ 100k fichiers par Go de mémoire sur Borg. Frost n'a pas ce problème.
        8. Borg est beaucoup plus maintenu, il a en place une sorte d'organisation qui peut recevoir des dons (support pour les personnes en difficulté). Frost, c'est pas le cas.
        9. Borg fonctionne à la fois comme serveur et comme client, il peut être appelé par SSH pour gérer l'envoi et la réception de chunks/multichunks. Ce qui nécessite une bonne connexion de données, pour la sauvegarde à distance et surtout la possibilité de lancer un processus sur un serveur distant. Frost lui travaille au niveau du système de fichier, donc il est indépendant du serveur et se base sur FUSE pour monter un serveur distant localement. Typiquement, ça veut dire que Frost fonctionnera sur de l'Amazon S3, du SSH/SFTP, du FTP, du Webdav, car il existe des "drivers" FUSE pour chacun de ces formats.
        10. De plus, Frost utilise un index des fichiers local donc démarre beaucoup plus rapidement la sauvegarde, comparé à Borg qui doit le télécharger à chaque backup. Pour donner un ordre d'idée de la taille de l'index, il fait 1.7GB pour un backup de 136G (compressé) depuis 7 ans.

        Voilà.

    • [^] # Re: Ressemble beaucoup à Frost

      Posté par  (site web personnel) . Évalué à 4.

      Trop bien ! Je savais que j'avais pas trouvé tout les formats d'archivage, j'en ai un de plus à analyser :)

      Tu as l'air d'avoir déjà fait pas mal de choses qui sont dans ma todo list (déduplication, chiffrement, parité, …) Je vais regarder ce que tu as fait, voir te piquer des idées (l'épaule des géants, tout ça)

      il faut absolument regarder du côté des structures mmapables (le gain de performance est délirant)

      C'est quoi que tu appelles structures mmapables ? Jubako mmap déjà pas mal de chose. Est-ce que c'est des structures avec des particularités (et lesquelles) ou c'est "juste" que l'implémentation doit faire du mmap plutôt que du read dans le fichier ?

      du stockage de différences (je détecte les différences via un rolling checksum à points de division fixe, ce qui permet de s'adapter à de nombreux formats, comme le texte ou les formats binaires). Ainsi, tu peux découper tes content pack en fonction du contenu (et en virant donc tous ce qui se déduplique facilement).

      Oui, c'est quelque chose que j'avais déjà identifié en travaillant sur les diff entre les zims. C'est dans ma todo pour faire des diffs efficaces.

      Dans mon cas, je recalcule toujours la différence entre la version actuelle et la dernière version et je modifie la dernière version pour arriver à la version actuelle, ce qui est beaucoup plus efficace que le contraire.

      Je suis pas sur de comprendre. Tu fais un diff "inversé" comme dans les packs git (si tu connais) ? La version "pleine" est toujours la dernière version (la plus couramment lue), et le diff permet de retrouver les anciennes versions.

      Si c'est le cas, ça veut dire que tes backups passés sont modifiés en fonction des données actuelles ?

      Matthieu Gautier|irc:starmad

      • [^] # Re: Ressemble beaucoup à Frost

        Posté par  (site web personnel) . Évalué à 8.

        il faut absolument regarder du côté des structures mmapables (le gain de performance est délirant)

        C'est quoi que tu appelles structures mmapables ? Jubako mmap déjà pas mal de chose. Est-ce que c'est des structures avec des particularités (et lesquelles) ou c'est "juste" que l'implémentation doit faire du mmap plutôt que du read dans le fichier ?

        Je me répond à moi même. J'ai vu que dans ton code, tu mmap des régions de ton fichier et tu construit directement (avec un inplace new) tes structures sur les régions mmappées (donc directement dans le fichier).

        Du coup, j'ai une autre question : Comment tu gères l'endianness et la portabilité entre différentes plateformes  ?

        Matthieu Gautier|irc:starmad

      • [^] # Re: Ressemble beaucoup à Frost

        Posté par  . Évalué à 5.

        Bon, comme tu l'as vu toi même, effectivement, l'idée c'est de mmaper un fichier (quitte à le réagrandir avec ftruncate), et de créer directement les structures de données dans l'espace mémoire mappé du fichier (en C++ avec le placement new, je ne sais pas en Rust comment faire). Ce qui évite toute copie de données (vu que tu travailles directement dans le fichier) mais ça nécessite de ne pas avoir de pointeur mais des index dans d'autres conteneur (ce qui n'est pas plus mal en fait, ça rend le code beaucoup plus stable).

        Du coup, lors d'une itération de ton archive, tu peux directement "pointer" sur les données précédentes, que tu n'as même pas besoin de lire physiquement. Ce n'est pas le cas en mode "read" où tu dois charger tout l'index en mémoire pour le modifier.

        Évidemment, le prix à payer, c'est la portabilité, c'est à dire que sur les machines big endian (est-ce que ça existe encore ces choses ?) ne peuvent pas lire une archive LE, car il faut swapper les entiers avant de les utiliser. Mais pour une solution de backup, vu que tu restaures l'archive sur la même machine que tu sauvegardes, l'endianness ne change pas, ce n'est pas un problème.

        Je fais effectivement un diff inversé (pas sur les données, seulement sur l'index), comme git pack car à chaque fois que tu lances un backup, c'est beaucoup plus rapide que d'avoir à reconstituer l'index à partir de la genèse. Donc, oui, je modifie l'index en fin de backup en stockant la version la plus récente comme une nouvelle itération, comprendre la hiérarchie complète des fichiers, à la fin de l'index avec un lien vers l'index de la version précédente. Puis dans le header de l'archive, je change le pointeur du catalogue vers la dernière version.

        J'ai également une fonction de purge qui élimine les révisions les plus anciennes (probablement la fonction la plus dure à coder pour un système de backup qui déduplique).

        La documentation du format est dans le header "Frost.hpp" vers la ligne 268.

        • [^] # Re: Ressemble beaucoup à Frost

          Posté par  (site web personnel) . Évalué à 5.

          je ne sais pas en Rust comment faire

          Rust lui-même ne sait pas faire :)
          Il faut initialiser la structure sur la stack et ensuite la déplacer dans le heap (ou ailleur). Rust évolue dans ce sens mais c'est pas encore ça. (https://github.com/rust-lang/rust/issues/63291)

          Évidemment, le prix à payer, c'est la portabilité, c'est à dire que sur les machines big endian (est-ce que ça existe encore ces choses ?) ne peuvent pas lire une archive LE, car il faut swapper les entiers avant de les utiliser. Mais pour une solution de backup, vu que tu restaures l'archive sur la même machine que tu sauvegardes, l'endianness ne change pas, ce n'est pas un problème.

          Il y a effectivement peu de machines big endian. Mais je ne suis pas près à parier que ça sera toujours le cas. En tous cas, pas pour un format de conteneur qui se veut portable et durable. Apple à déjà changer l'endianness de ses machines et les processeurs arm peuvent fonctionner dans les deux modes. On est pas à l’abri d'un changement d'endianness pour l'iphone 42

          Du coup, jubako "charge" des octets en mémoire (avec du mmap) et parse ces octets pour en récupérer des valeurs et reconstruire mes structures (et inversement à l'écriture). C'est un peu moins efficace car je dois parser chaque valeur individuellement (même si ça se résume à un byteswap) plutôt que directement charger ma structure.
          Mais ça me permet de gérer l'endianness et de me passer du padding au passage.

          J'ai également une fonction de purge qui élimine les révisions les plus anciennes (probablement la fonction la plus dure à coder pour un système de backup qui déduplique)

          J'imagine bien. J'ai pas hâte de me pencher sur la question :)


          En tous cas, ton approche avec Frost est super intéressante.
          Au lieu de faire des backups "incrémentales" immuables d'un coté et gérer les différents backups (suppression des anciens fichiers) d'un autre, tu as un seul format mutable qui intègres l’ensemble des N révisions.

          Je suis partagé entre "c'est une super idée, il faut effectivement avoir un système cohérent" et mon approche vieille école "c'est n'importe quoi, on ne modifie pas un backup une fois créé".

          Matthieu Gautier|irc:starmad

          • [^] # Re: Ressemble beaucoup à Frost

            Posté par  (site web personnel, Mastodon) . Évalué à 2.

            Il y a effectivement peu de machines big endian. Mais je ne suis pas près à parier que ça sera toujours le cas.

            Y en a peu, mais ça existe. Et puis je suppose que tu veux pouvoir être porté sur des archis qui sont probablement un peu anciennes.

            Mais ça me permet de gérer l'endianness et de me passer du padding au passage.

            Après, j'ai envie de dire que c'est de la data et que tu peux fixer le format de ton archive, c'est indépendant de celle de l'architecture sur laquelle le programme va tourner : y a des formats d'images et vidéo (et peut-être audio) qui sont en grand bouthistes…

            “It is seldom that liberty of any kind is lost all at once.” ― David Hume

          • [^] # Re: Ressemble beaucoup à Frost

            Posté par  . Évalué à 3.

            Les données (chunks/multichunks) sont immutables. C'est uniquement l'index qui mute et qui est ensuite recopié sur le serveur/répertoire à distance (puis swappé/rename avec le précédent de manière atomique).
            Donc c'est assez sûr en fait, tu ne peux avoir de corruption à aucun moment sur le répertoire de backup.

            En cas de corruption du support de stockage, détecté par un checksum/hash qui ne colle pas, vu que l'index contient toutes les révisions précédentes, au pire tu retrouveras une version précédente.

            Par contre, en cas de corruption du support de stockage sur les données, là, je n'ai pas de protection (type Reed Solomon, ECC, Turbocodes). Vu que j'utilise des machines en RAID1, je me dis que c'est peu probable que ça arrive pile sur 2 disques au même endroit en même temps. Il faudrait ajouter de la redondance idéalement…

            • [^] # Re: Ressemble beaucoup à Frost

              Posté par  . Évalué à 3.

              en RAID1, je me dis que c'est peu probable que ça arrive pile sur 2 disques au même endroit en même temps

              À condition de prendre des disques venues de chaînes de fabrication différentes, sinon… Les chaînes de fabrication bougent tout le temps, pour suivre la disponibilité des composants du PCB. Du coup, la qualité des composants bouge aussi tout le temps. Il faut donc éviter d'acheter deux disques de même modèle et même marque en même temps, ça permet d'être sur des composants différents. Deux disques de fabricants différents c'est plus fiable, si les composants du PCB changent.

        • [^] # Re: Ressemble beaucoup à Frost

          Posté par  . Évalué à 7.

          Mais pour une solution de backup, vu que tu restaures l'archive sur la même machine que tu sauvegardes […]

          Il n'y a que moi que cette phrase choque ???

          • [^] # Re: Ressemble beaucoup à Frost

            Posté par  (site web personnel, Mastodon) . Évalué à 2.

            Ça dépend… Même machine, mais je suppose des partitions différentes ?

            “It is seldom that liberty of any kind is lost all at once.” ― David Hume

          • [^] # Re: Ressemble beaucoup à Frost

            Posté par  . Évalué à 3. Dernière modification le 08 novembre 2022 à 17:02.

            Pardon, j'aurais du dire sur la même architecture, plutôt que la même machine. C'est pas une archive, c'est un backup. Typiquement si tu as un problème (disque, machine qui crame), tu va changer le matériel défectueux et restaurer ta sauvegarde. Personnellement, vu que je sauvegarde /etc avec, je me vois mal changer d'architecture entre la restauration et la sauvegarde.

            Si tu veux passer sur un nouveau type d'architecture, c'est possible, mais il faut lancer une VM pour faire tourner Frost dans l'ancien endianness, restaurer puis re-sauvegarder hors de la VM. Je n'en ai jamais eu la demande, je pense que c'est juste un problème théorique qui a aucune utilité pratique.

            Dans la pratique, je suis passé d'un backup en ARM64 vers AMD64 sans problème lorsque j'ai changé mon NAS. Architecture différente mais compatible en endianness.

            • [^] # Re: Ressemble beaucoup à Frost

              Posté par  . Évalué à 3.

              Personnellement, vu que je sauvegarde /etc avec,

              On sauvegarde souvent des données seules et si ça fait suite à une panne non réparable du PC, on en change pour un différent, l'ancien modèle n'existant probablement plus. Ta phrase ne précisait pas le contexte particulier dans lequel tu la formulais.

              Alors c'est sans doute un problème théorique sans impact en pratique si les machines sont maintenant toutes (ou presque) little endian mais ton affirmation telle que formulée était de nature à interpeller.

            • [^] # Re: Ressemble beaucoup à Frost

              Posté par  . Évalué à 1.

              Bonjour, comment as-tu fait pour utiliser Frost sur ARM64? Si j'essaie de compiler depuis Github, j'obtiens une erreur d'instruction assembleur:

              ./ClassPath/src/Compress/../../include/Compress/../File/../Platform/Platform.hpp:153:21: error: unrecognized instruction mnemonic, did you mean: bit, cnt, hint, ins, not?
              __asm__("int $3\n" : : );
              ^

              • [^] # Re: Ressemble beaucoup à Frost

                Posté par  . Évalué à 1.

                Bon j'ai fini par réussir à compiler, le problème était que j'ai un Mac qui est un peu trop récent.

                Est-ce que c'est normal d'avoir un Warning dans le test de roundtrip?

                Restore: ./testRestore [0/11]
                WARNING (2022): This file already exists and is different in the restoring folder, and no overwrite specified

                Merci!

  • # cotar

    Posté par  . Évalué à 5.

    Salut, en ce qui concerne le fait de pouvoir accéder à un élément contenu dans un tar, le projet cotar étend le format pour permettre de ne parcourir que l'en-tête et une table de hash.

    Le cas d'usage est de pouvoir mettre à disposition une archive contenant des millions de fichiers sans déclencher la facturation put/get par fichiers, de rajouter de nouveaux fichiers sans coût prohibitif, de faire un bonne grosse manipulation séquentielle sur des serveurs avec des dédés magnétiques. Et, surtout, ça reste un tar compatible avec nos habitudes :)

    • [^] # Re: cotar

      Posté par  (site web personnel) . Évalué à 6.

      Je connaissait pas. Mais ça répond à un besoin différent.

      Le but de cotar, c'est de payer moins chez amazon S3 qui facture (entre autres) au nombre de fichiers.
      Le but est de tout mettre dans un tar (et pas un tar.gz) et d'avoir effectivement un table de hash en local (ou téléchargée sur le moment) pour savoir dans quel range il faut lire le fichier dans le tar.

      Mais ça marche que sur des tar non compressé. Et t'as pas de "discovery", la table de hash ne contient pas le path des fichiers.

      C'est utile pour gruger un peu amazon S3, mais ça a peu d'utilité en local où tu veux compresser tes données.

      Matthieu Gautier|irc:starmad

      • [^] # Re: cotar

        Posté par  . Évalué à 4.

        ça ne poursuit pas tous les objectifs de jubako/arx, je le pointais pour nuancer les comparaisons list/dump de la dépêche.

        L'avantage ne se limite pas à aws, ça me permettrait de mettre à disposition un ensemble de fichiers qui gagnent peu à être recompressé (des images jpeg) en ayant les avantages d'une unique archive (copier, faire un chmod sur un tar plutôt que plusieurs millions de jpg) sans rendre l'accès à un sous-ensemble prohibitif. C'est la démarche des formats "cloud" comme COG, ZARR, COPC, TileDB, etc.

  • # la part des anges

    Posté par  . Évalué à 5.

    Un peu déçu, j'espérais des commentaires sur Jubako alors que ça tourne plutôt sur Arx.

    Sauf sur les sauvegardes, on n'a pas de réactions sur les toutes nouvelles façons de faire que tu imagines et qui font rêver. Quelqu'un connaissant le sujet veut-il rêver pragmatiquement?

  • # Commentaire supprimé

    Posté par  . Évalué à 0. Dernière modification le 17 novembre 2022 à 07:44.

    Ce commentaire a été supprimé par l’équipe de modération.

  • # Proxmox backup server

    Posté par  . Évalué à 3.

    Super intéressant et ambitieux comme projet.

    Pour l'optimisation de Arx je te suggère de regarder du côté de proxmox backup serveur, la finalité n'est pas la même mais il est écrit en RUST, est vraiment convaincant et dispose de son propre format d'archive.

    My two cents et encore merci pour ton projet.

  • # et qu'utilise web.archive.org pour ses archives ?

    Posté par  (site web personnel) . Évalué à 3.

    Est-ce qu'archive.org, et en particulier leur plateforme d'archivage du Web, pourrait être intéressé par ces techniques ? êtes-vous en contact ?

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.