Du serpent à la rouille - Partie 1

Tirer des câbles

À la base, je ne suis pas développeur. Enfin, pas vraiment, surtout sysadmin/réseauteur en pratique.

J’ai pratiqué de manière très rapide Pascal/Delphi au lycée, et puis les autres langages classiques en études supérieures. Et puis en rentrant dans la vie active, j’ai du me lancer très vite et de manière très efficace dans le merveilleux monde de Python (que je n’avais jamais pratiqué auparavant).

Et j’aime beaucoup Python, on fait des choses simple rapidement, on fait des choses complexes élégamment, et on peut aussi faire des horreurs de type :

truc = [i.rsplit("/",1)[0] for i in list(set(liste_de_machins)) if len(i) >= 4]

Je suis pas vraiment sûr que ça fonctionne en l’état, mais vous avez l’idée. Dans mon cas, ça a été pour utiliser massivement les librairies pour exploiter des infra OpenStack. Et vu que OpenStack c’est à la base principalement en Python, bah, voilà.

4-5 ans plus tard, me voilà parlant couramment le Fourchelang, et l’utilisant même pour quelques petits projets persos, dont celui dont je veux vous parler.

Stocker des trucs

Il y a quelques temps, je cherchais les alternatives viables à Nextcloud. Pas tant que j’en étais déçu, mais plus par curiosité de ce qui se fait sur le marché.

Eh bien mes amis, quelle tristesse. Faut croire que le domaine de “stocker des trucs” c’est pas ultra motivant.

Globalement on a, en terme de service auto-hébergé de stockage de fichiers :

  • Nextcloud, le petit frère qui a réussi
  • ownCloud, le Oracle OpenOffice du stockage
  • Seafile, moins connu, mais fonctionnalités intéressantes
  • Pydio, qui est plutôt orienté entreprises
  • Et tout une palanquée de trucs terrifiants pour nerds monochromes et monospaces, tel que git-annex.

Qu’on ne se méprenne pas, git-annex c’est un très beau logiciel qui marche vachement bien. Mais pour le tout venant c’est quand même pas la même experience faut avouer.

Dans cette liste il faut bien mettre d’un côté NC et OC, de l’autre, eh bien les autres. Nextcloud depuis quelques versions pousse vraiment l’aspect “hub collaboratif” de leur outil. Ça donne des fonctions de type kanban, vidéoconf, calendrier, contacts, intégration onlyoffice. C’est donc une appli de stockage, mais avec une galaxie d’autres fonctions qui gravitent autour.

Je ne me suis pas renseigné sur l’état de ownCloud de nos jours, parce que honnêtement, tout le monde s’en fout.

En face, Seafile ne fait QUE du stockage, et il le fait très bien, mais il ne fait que ça. Donc si vous cherchez une solution tout intégrée avec plein d’addons utiles, passez votre chemin. À titre personnel j’utilise Nextcloud depuis plusieurs années et il me sert d’équivalent a la plupart de ce que fournit Google.

Mais je digresse.

La tristesse je disais. Je me suis donc intéressé à Seafile, qui est basé sur a priori du Django ultra-customisé, qui est en partie payant, et qui a quelques petites fonctionnalités vraiment cool.

  • La gestion du chiffrement dans un système de “coffre fort”
  • La dé-duplication des données au sein de ce “coffre fort”

Mais voilà, c’est en partie payant, et moi j’aime pas ça, j’aime que tout soit gratuit et immédiat. Du coup j’ai décidé de bricoler un Seafile-like complet avec mes connaissances en Python/Django. Gratuit et immédiat.

Découper des serpents

Donc je me dis, de quoi ai-je besoin pour faire ça ?

  • Une interface web (ça, facile, même si c’est moche je connais bien Django)
  • Une manière de découper les fichiers de manière intelligente (aucune idée de comment ça fonctionne)
  • Une manière de stocker les fichiers de manière intelligente (idem)

Et évidemment, comme je suis quelqu’un de raisonnable, je voulais que mon appli puisse traiter des fichiers de plusieurs Gibioctet le plus RAPIDEMENT POSSIBLE. Il faut savoir poser des limites.

Équipé de ça, je vais fouiller la documentation de logiciels de backup qui couvrent la fonction la plus floue pour moi : le découpage. Typiquement, borg et restic font un “chunking”.

C’est à cet instant que je me suis perdu dans le monde merveilleux des maths, avec le CDC, Content Defined Chunking, l’algorithme de Rabin-Karp et autres joyeusetés. Honnêtement, j’y comprends pas grand chose dans les détails de comment ça fonctionne, mais en gros voilà le topo. Imaginez qu’on a un gros fichier qu’on veut sauvegarder, potentiellement certaines parties de ce fichier se répètent, on va donc utiliser ce fameux CDC pour découper le fichier, mais de manière intelligente.

Découpage méthodique

Section 1 (512ko) Section 2 (512ko)
AAAAABAAABBBAAABAA BAAAABAAABBBBAAAA

Ici on a donc un “fichier” de 1024ko, les deux sections de 512ko sont différentes, donc on stocke 2 “chunks”. Maintenant modifions notre fichier d’une lettre

Section 1 (512ko) Section 2 (512ko)
AAAAABAAABBBAAABAA BAABABAAABBBBAAAA

Le second bloc a été modifié, mais pas le premier, on peut donc garder notre copie de la première section, et re-sauvegarder la seconde qui a été modifiée. Mais imaginons qu’on modifie différemment le fichier comme ceci :

Section 1 (512ko) Section 2 (512ko)
AAAAABAAABBBAAABBB ABAAABAAABBBBAAAA

Si on considère que la section modifiée (donc de AABA vers BBAB) s’étale sur deux sections, il faut forcément tout re-sauvegarder.

Pire encore ! Si je rajoute un A au tout début de mon fichier (par exemple dans un fichier texte, je rajoute une ligne au tout début) les données dans le fichier vont être décalées d’une “lettre” vers la droite ! De fait, à ma prochaine sauvegarde, l’intégralité des chunks aura été modifié et je devrais re-sauvegarder toutes mes données.

Le Content Defined Chunking permet de s’affranchir de ce problème. Son objectif est de chercher des sous-chaînes dans une plus grande quantité d’information, via une fonction de hashing. On va donc ce retrouver avec une suite de Sections comme l’exemple au dessus, sauf que ces sections auront des longueurs variables en fonction de la logique interne de l’algorithme. Dans ce contexte, rajouter des données au début d’un fichier n’est plus un problème car les données supplémentaires seront dans leur propre section, et les données suivantes resterons dans la section assignée au premier abord.

C’est là que mes compétences sur le CDC s’arrêtent parce que je ne comprends honnêtement pas grand chose à ce qui se passe dans les détails.

Si ça vous intéresse, il existe un papier sur une variante de l’algo que j’ai utilisé par la suite qui vient de ce site


Ceci dit il faut bien prendre en compte un truc : De nos jours, la plupart des fichiers qu’on manipule sont compressés, que ça soit des fichiers Microsoft Office, des images, des iso, la vidéo mkv HDR 2160p de vos vacances dans le Poitou, …

Quasiment tout est compressé. Donc on n’y gagne pas forcément des caisses à dédupliquer un jeu de fichier, parce que par définition, les fichiers chiffrés (correctement) auront une entropie quasi-maximale, et donc très peu de répétition, ou de re-compressabilité, et donc très peu de taux de duplication de données.

C’est pour ça que compresser un fichier déjà compressé fait gagner soit très peu d’espace disque, soit en perdre carrément si les données technique de l’archive sont trop lourdes.

De fait, mon projet est purement pour l’exercice de pensée, pour le fun si vous voulez, je ne compte pas détrôner les alternatives que j’ai mentionnées plus haut. Si ça marche suffisamment bien pour que j’en sois satisfait : Cool.

Développement

Mais revenons à nos reptiles. Il existe globalement une seule librairie Python qui implémente cet algorithme. Très concrètement, cette librairie prends 4 entrées dans sa fonction principale

  • Le fichier à chunker
  • La taille minimale d’un chunk
  • La taille moyenne d’un chunk
  • La taille maximale d’un chunk

Les trois derniers paramètres sont assez importants puisqu’ils vont influer entre autres sur la durée de traîtement total, la variation de taille des objets de sortie, et le nombre de ces objets.

Armé de cette belle librairie, je me lance, et en assez peu de temps j’ai un système qui fonctionne, avec un espèce de “repository” qui me permet de suivre quels fichiers j’ai sauvegardé, de quels chunks il se compose et à quelle position. Sans rentrer dans les détails de structure c’est assez ressemblant à ça (mais dans une base sql quelconque):

{
    "filename": "toto.qcow2",
    "chunks": [
        {
            "digest": "b6c7f5f163d3b86dda47911e8d7f488a",
            "path": "repository/b/6/b6c7f5f163d3b86dda47911e8d7f488a.chunk",
            "position": 573890
        },
        {
            "digest": "074efc3b31ed27df0aa1aeb4575c590f",
            "path": "repository/0/7/074efc3b31ed27df0aa1aeb4575c590f.chunk",
            "position": 0
        }
    ]
}

J’ai décidé de mettre les chunks de destination dans des sous-dossiers basés sur le hash parce que que je trouvais ça cool et aussi parce que ça évite qu’un dossier ait une INFINITÉ de fichiers dedans. Vis à vis de la gestion du filesystem il me semble que c’est mieux.

Tout les chunks sont compressés en zlib avant d’être écrit dans le chemin, et pour restaurer un fichier je n’ai “seulement” qu’à itérer sur tout les chunks mentionné pour un fichier, dans l’ordre des position, récupérer le chunk, décompresser, et écrire dans un fichier de sortie. En faisant un hash avant et après j’ai pu constater que c’était les mêmes, PoC validé !

Mais au regard des performances j’étais un peu déçu, d’un autre côté Python est connu pour ce genre de problèmes, étant un langage globalement inteprété (ou JIT dans le meilleur des cas), on manque plein d’optimisation et la surcouche ajoute une lourdeur de traîtement. Qui plus est, je pense que vu que je faisais beaucoup d’IO système, le GIL pouvait potentiellement me poser problème.

Cython

Il existe une “extension” à Python permettant de développer du code intermédiaire entre le C et le Python, et qui propose ensuite de compiler ce code en librairie C standard. Son petit nom, Cython. J’ai jamais trop développé en C avant, donc je me disais que ça permettrait un bon compromis entre une syntaxe que je comprends à peu près, et une augmentation potentiellement assez drastique des performances. J’ai donc commencé à tenter de “convertir” la partie la plus complexe (d’un point de vue CPU) de mon code existant Python vers cette nouvelle syntaxe Cython.

Et mes aïeux quel enfer.

Autant vous dire que j’ai lâché l’affaire assez vite : Je comprends que pour certaines personnes et certains usages ça doit être absolument génial, mais c’était bien au delà de mon niveau, et bien trop proche du C en terme de “facilité d’utilisation” pour que je puisse penser en faire quoi que ce soit. Rien que lire un fichier en Cython demande une gestion de mémoire et de buffer explicite et je suis vraiment pas venu pour ça moi. Également, les interactions entre C(ython) et Python, bien que l’implémentation de référence de Python soit elle-même en C, ne m’a pas paru très intuitive. Encore une fois c’est probablement parce que au delà d’un jeu de “plus ou moins” j’ai jamais vraiment touché de C.

Rust

Pour le coup, vu la tournure qu’ont pris les évènements depuis la dernière avancée de ce billet (Fin novembre 2020 pour le début, début février 2021 pour ce que vous êtes en train de lire), je pense que ça va se terminer dans un second billet que vous trouverez sur la homepage, ou ici même si j’oublie pas d’y mettre le lien.