Woodstock Backup - Protocol et Language de sauvegarde

Posté le 7. April 2023 dans Programmation

Woodstock Backup - Protocol et Language de sauvegarde

Note de 2023 : Ce billet a été écrit en avril 2021, il y a deux ans, mais n'a jamais été publié. Le temps passe vite.

Depuis lors, j'ai travaillé sur d'autres projets, mais aussi sur ce logiciel de sauvegarde. En progressant dans le développement du projet, j'ai pu optimiser les performances et me faire une opinion sur le choix que j'ai finalement fait, que je partagerai à la fin de l'article.

Je mettrai à jour mes conclusions en fonction de mes avancées sur le sujet.

Dans notre précédent article, nous avons vu comment dédupliquer les fichiers dans un pool sans utiliser btrfs, un système de fichier permettant la déduplication. Pour pouvoir copier les fichiers dans notre pool, le logiciel doit savoir comment écrire les fichiers de manière appropriée.

Si nous voulons continuer à utiliser rsync, nous pourrions envisager de créer un système de fichiers FUSE (Filesystem in User Space) pour faire le pont entre rsync et notre pool de stockage. Cela nous permettrait de continuer à utiliser rsync pour les sauvegardes et les restaurations. Cependant, cela nécessiterait la mise en œuvre d'un système de fichiers complet, y compris la lecture, l'écriture, la déduplication, etc., pour un usage très spécifique de rsync. En fin de compte, le système de fichiers ne serait utilisé que pour la sauvegarde (ajout de nouveaux éléments) et la restauration (lecture d'une sauvegarde). Il n'y aurait pas d'écriture aléatoire dans le système de fichiers, ni dans un fichier même.

Après avoir rapidement examiné cette possibilité, je l'ai écartée car elle me semble être une solution trop lourde pour mes besoins.

Par conséquent, nous devrons nous passer de rsync. Je n'ai pas trouvé de bibliothèque permettant d'implémenter facilement une synchronisation sur le protocole rsync. Nous devrons donc écrire notre propre protocole de synchronisation de fichiers.

Lorsque nous développerons ce protocole, nous devrons nous assurer de la sécurité. En effet, rsync est capable de synchroniser des fichiers via un tunnel ssh. Notre protocole devra empêcher les attaquants d'écouter ce qui est synchronisé et d'accéder aux fichiers sans autorisation.

Dans la suite de cet article, nous étudierons comment écrire notre protocole de synchronisation et nous comparerons les performances de sauvegarde entre le langage JavaScript (actuellement utilisé par le serveur de sauvegarde) et le langage C++.

Comme toujours, n'hésitez pas à me faire part de vos commentaires et de vos avis (système de commentaires en bas de la page).

Création de la communication périphérique/serveur

Nous allons aborder la question de la communication entre le client et le serveur. Afin d'éviter toute confusion entre le client et le serveur dans le sens réseau et le client et le serveur dans le sens applicatif, nous allons désigner :

  • Périphérique pour le client à sauvegarder
  • Serveur de sauvegarde pour le serveur de sauvegarde.

En ce qui concerne le protocole de communication, je pense utiliser une HTTP2 plutôt que d'ouvrir un socket TCP et travailler directement dessus. L'utilisation d'un protocole de haut niveau me permettra de m'appuyer sur des bibliothèques existantes et déjà largement utilisées. De plus, je n'aurai pas à gérer :

  • la compression
  • la prise en charge de TLS

En raison de la nature d'HTTP2, l'initiateur de la connexion est important : il faut déterminer qui est le serveur HTTP et qui est le client HTTP.

Dans la suite de cet article, nous allons essayer de répondre aux deux questions suivantes :

  • Comment effectuer la communication entre le serveur de sauvegarde et le périphérique à sauvegarder ?
  • Qui doit initier la connexion ?

    • Est-ce que le serveur de sauvegarde doit contacter le périphérique ?
    • Ou est-ce que le périphérique doit contacter le serveur de sauvegarde ?

Le serveur de sauvegarde initie la connection

Nous allons parler de deux éléments clés pour la sauvegarde: le manifeste et les chunks.

  • Manifeste: la liste des fichiers à sauvegarder avec le nom du fichier, les attributs, les acl, ... un hash du fichier et un hash des différents morceaux qui le constituent.
  • Chunk: un morceau de fichier (un fichier peut être découpé en plusieurs morceaux de taille donnée).
Le serveur initie la connection

Lorsqu'une sauvegarde doit démarrer, le serveur contacte le périphérique et initie la sauvegarde. Le périphérique envoie ensuite les différents fichiers au serveur qui peut alors demander au périphérique les morceaux de fichiers qui lui manquent.

Voici des exemples de logiciels de sauvegarde où le périphérique initie la connexion:

  • BackupPC - basé sur un rsync modifié pour gérer la partie pool
  • UrBackup - basé sur son propre client de sauvegarde à installer sur le périphérique

Lorsque le serveur initie la connexion sur le périphérique:

  • Le serveur décide quand faire la sauvegarde (en fonction de quand la dernière sauvegarde a été construite, des heures de travail, ...).
  • Le serveur a besoin de pouvoir accéder aux machines, mais les périphériques n'ont pas besoin d'avoir un accès direct au serveur de sauvegarde. Cela permet de ne pas compromettre le serveur de sauvegarde en cas de compromission d'un périphérique.
  • Le serveur doit détecter la présence du périphérique (si ce dernier n'est pas toujours sur le réseau) et donc faire un ping régulier, par exemple.

Il faudra prévoir un scheduler pour vérifier la présence des machines sur le réseau. Le serveur doit rester éveillé (et donc être actif) et ne peut pas juste attendre l'arrivée d'un périphérique.

Il doit y avoir un serveur (le serveur ne peut pas être juste un espace de stockage, comme un S3). Le serveur décide à partir du nouveau manifeste du client quels sont les fichiers qui ont changé et demande au client de lui envoyer ceux qui lui manquent.

Le serveur décide d'organiser son pool de sauvegarde comme il le souhaite, ainsi que le nombre de sauvegardes en parallèle. Il peut bloquer les sauvegardes si le pool est plein.

Le périphérique initie la connection

Le périphérique initie la connexion

Voici quelques exemples de logiciels de sauvegarde qui peuvent être déclenchés depuis le périphérique:

  • Borg (avec un lieu de stockage)
  • Restic (avec un lieu de stockage)
  • Burp (avec un serveur qui centralise les sauvegardes)

Le périphérique sait quand il est allumé et connecté au réseau. S'il y a une erreur de sauvegarde (par exemple, si le serveur de stockage n'est pas disponible), il peut réessayer plus tard. Il n'est donc pas nécessaire de planifier des sauvegardes sur le serveur, mais il est nécessaire d'avoir un plan de sauvegarde pour chaque périphérique.

Le périphérique doit être capable de se connecter au serveur, donc il est important d'ouvrir le flux de données du périphérique vers le serveur (par exemple, si le périphérique est un ordinateur hors réseau interne). Cela peut être résolu en créant un VPN.

Lorsque la sauvegarde est initialisée, le serveur peut renvoyer des informations au périphérique pour indiquer qu'il refuse la sauvegarde (pool plein, ...). Le périphérique devra alors réessayer plus tard.

Si un périphérique est compromis, toutes les sauvegardes de ce périphérique sont en danger (il existe des solutions pour bloquer les sauvegardes en lecture/écriture). Si le pool de sauvegarde est mutualisé entre plusieurs machines, cela peut compromettre l'ensemble du pool.

Le serveur reçoit un manifeste et une liste de chunks. Si nous souhaitons mutualiser le pool de sauvegarde, nous ne pouvons pas laisser le client accéder à l'intégralité du pool. Nous devons donc disposer d'un serveur qui gère les chunks et vérifie que les fichiers reçus correspondent bien aux sauvegardes effectuées. Il est important de s'assurer que les clients n'envoient pas de chunks qui ne sont pas relatifs à une sauvegarde ou qui ont un sha incorrect pour éviter que les fichiers soient mal rangés. Il faut aussi bloquer la modification des anciennes sauvegardes, et aussi conditionner la lecture de ces derniers.

Choix

Personnellement, j'ai une préférence pour l'initialisation de la sauvegarde par le serveur. Dans mon projet actuel, j'ai déjà prévu le code pour gérer la file d'attente, lancer rsync, ainsi que le planificateur. Il serait facile de remplacer ce code par celui de mon nouveau serveur.

Note de 2023: En avançant dans la réécriture, les dépendances ont été mises à jour et une grande partie du code a été modifié. Par conséquent, remplacer l'appel à rsync par un appel au nouveau système de sauvegarde n'a pas été aussi simple que prévu.

Il y a plusieurs raisons pour lesquelles je penche vers le contrôle du déclenchement par le serveur :

  • Le serveur sera le propriétaire du pool et des sauvegardes.
  • Le serveur décide du cycle de vie des sauvegardes (nombre de sauvegarde, durée de vie, ...)
  • Le serveur peut fusionner les sauvegardes de plusieurs périphériques dans le pool, donc il doit contrôler tout ce qui y entre.

Je tiens à souligner que la version finale sera terminée, tous les choix ici présent peuvent avoir été remis en question. En informatique, faire et défaire, c'est avancer.

Cinematique d'appel

Dans cet article, nous allons examiner la cinématique d'appel entre les requêtes et comment elle peut être utilisée pour améliorer la sauvegarde de contenu en fonction de nos besoins.

Pour sauvegarder le contenu d'un périphérique, celui-ci doit suivre les étapes suivantes :

  • Parcourir l'ensemble de ses fichiers et pour chaque fichier :

    • Récupérer les attributs du fichier, tels que le nom, la date de création, de modification, les droits, etc.
    • Calculer un hash pour déterminer si le fichier est modifié et quelle partie du fichier a été modifiée.
    • Envoyer cette liste de fichiers (manifest) au serveur de sauvegarde.
  • Envoyer le contenu nouveau ou modifié des fichiers, chunk par chunk, vers le serveur.

Le processus de sauvegarde peut être optimisé de la manière suivante :

  • Le périphérique peut recevoir la dernière sauvegarde qu'il a effectuée et la comparer avec ce qu'il a de son côté pour n'envoyer que le différentiel.
  • Il n'est pas nécessaire de calculer le hash du fichier entier s'il n'a jamais été sauvegardé. Ce hash peut être calculé lors de la récupération des données, ce qui évite de lire deux fois le fichier.

Voici la séquence utilisée pour la sauvegade:

Sequence de sauvegarde
  1. Le serveur doit commencer par s'authentifier auprès du client pour garantir que la communication se fait en toute sécurité. L'authentification peut se faire via un échange de clés publique/privée, avec la connaissance d'un mot de passe commun associé au device. L'objectif est que le client puisse s'assurer qu'il communique avec le bon serveur.
  2. Si une sauvegarde a déjà eu lieu par le passé, le serveur peut envoyer au client le contenu de la dernière sauvegarde. Cette liste comprend les fichiers avec leurs attributs et leur hash. Cette étape permet au client de savoir quels fichiers ont été ajoutés, supprimés ou modifiés depuis la dernière sauvegarde.

    Par rapport à rsync, le serveur n'a pas besoin de recalculer cette liste.

  3. Le client parcourt les différents dossiers demandés et génère une nouvelle liste. Il peut se contenter de n'envoyer que les fichiers qui ont changé depuis la dernière fois.

    Le serveur stocke cela pour préparer la nouvelle sauvegarde. Il est possible d'utiliser un système de journal pour écrire les modifications retournées par le client. Ces modifications ne sont persistées dans le fichier final qu'une fois la sauvegarde terminée.

  4. Le serveur utilise la liste reçue pour demander au client de lui renvoyer les fichiers modifiés. Si le client a calculé le hash des chunks (morceaux de fichier), le serveur peut ne demander que les morceaux qui ont été modifiés et pas l'intégralité du fichier.

    Il est important de trouver une taille idéale pour les chunks. Si la taille est trop petite, cela risque de multiplier la quantité de fichiers dans le pool sans aucun bénéfice. Si elle est trop grande, cela peut bloquer le transfert intégral du fichier sur le serveur.

    À l'avenir, il pourrait être envisageable d'avoir différents types de chunks pour déterminer ce qui doit être renvoyé dans les transferts réseaux et ceux pour le stockage du pool. Cependant, cela complexifie le stockage.

Choix du protocol

Pour envoyer des informations en flux continu du périphérique au serveur et vice versa (un stream pour le manifeste, un autre pour les fichiers, un autre pour les logs), le choix de protocole est crucial. Voici quelques options que nous allons considérer pour notre shortlist :

  • REST: Protocole simple et facilement utilisable pour transférer des fichiers via upload ou download. Pour le streaming, nous pouvons également envisager l'utilisation de Websocket.
  • gRPC: Protocole permettant d'utiliser HTTP/2 pour faire des requêtes avec du streaming bidirectionnel.
  • Protocole maison à base de socket: Maîtrise complète du flux (comme rsync par exemple).

Comme vu précédement, les protocoles basés sur HTTP, comme REST et gRPC, offrent des avantages tels que l'utilisation d'un protocole de haut niveau permettant de bénéficier de TLS, de la compression, de la mutualisation de la connexion, etc.

Le protocole maison, même s'il permettrait de bonnes performances, nécessite une plus grande complexité de développement, mais aussi un plus grand risque de bugs ou de failles de sécurité. Il est donc écarté de notre list pour le moment, mais nous pourrions l'envisager dans une future refonte si les performances sont vraiment mauvaises.

Pour déterminer le protocole le plus performant pour nos besoins, nous allons comparer les protocoles REST s/ HTTP/2 et gRPC.

Il est important de noter qu'il est crucial de choisir le langage ou le protocole le plus adapté à notre besoin et ne pas partir sur des "a priori". Souvent, on choisit un langage ou un protocole parce qu'on le connaît bien ou parce qu'on l'estime meilleur dans certaines situations. Mais chaque besoin étant différent, l'utilisation dans un contexte ne signifie pas qu'un langage ou un protocole est le meilleur dans tous les contextes.

Quel langage choisir

Le client de sauvegarde doit être compatible avec Windows, Linux et MacOS.

Ma première approche pour développer un logiciel performant consiste à utiliser le language C++ pour le périphérique et la partie synchronisation et gestion du pool pour serveur, tout en gardant Node.JS pour l'orchestration.

Comme une partie du serveur est développée en Node.JS, je vais également tester les performances dans ce langage. Si j'obtiens des performances similaires, le développement en Node.JS pourrait être plus simple (un seul language pour tout et moins de complexité à gérer pour le multithreading).

Si je décide de développer en C++, j'utiliserai Qt pour l'interface graphique, car je le connais bien.

L'écriture

gRPC est un framework moderne pour la création de services distants. Il est basé sur HTTP/2, qui offre des améliorations de performances significatives par rapport à HTTP/1.1. Pour le protocole REST, il existe des librairies permettant de créer des serveurs et clients HTTP/2 en C++, mais à mon grand désarroi c'est plus difficile que prévu.

  • cpprestsdk: Support HTTPS mais pas HTTP/2 (en 2023, il n'évoluera plus),
  • nghttp2: Librairie très bas niveau (trop bas niveau),
  • libnghttp2_asio: Librairie plus haut niveau, possède une dépendance sur boost. J'ai également l'impression que cette librairie ne permet pas de faire tout ce qu'on veut en HTTP/2 (par exemple le streaming d'un fichier n'est pas quelque chose de facile à écrire, contrairement à l'allocation complète en mémoire de la réponse à envoyer).

Pour le protocole gRPC, nous avons:

  • grpccpp: gRPC est de basé basé sur HTTP/2.

Coté Node.JS, j'utilise Nest.JS pour la partie serveur, je vais donc m'appuyer sur les librairies suivante pour le protocol gRPC.

L'utilisation d'HTTP/2 avec Axios (utilisé par Nest.JS) peut se faire en utilisant http2-wrapper:

import * as http2 from "http2-wrapper";
import { AxiosRequestConfig } from "axios";

const data = ProtoGetChunkRequest.encode(request).finish();
return this.httpService
  .request<Readable>({
    method: "post",
    url: `https://${this.hostToBackup}:3000/get-chunk`,
    data,
    transport: http2,
    responseType: "stream",
  } as AxiosRequestConfig)
  .pipe(map((response) => response.data));

Nous allons écrire un fichier de description protobuf (qui pourra être le même entre la version C++ et Node.JS). Le fichier va décrire chaque élément du fichier manifest, ainsi que les appels RPC entre le périphérique et le serveur.

syntax = "proto3";

package woodstock;

enum StatusCode {
  Ok = 0;
  Failed = 1;
}

message FileManifest {
  message FileManifestStat {
    int32 ownerId = 1;
    int32 groupId = 2;
    int64 size = 3;
    int64 lastRead = 4;
    int64 lastModified = 5;
    int64 created = 6;
    int32 mode = 7;
  }

  bytes path = 1;
  FileManifestStat stats = 2;
  repeated bytes chunks = 3;
  bytes sha256 = 4;
}

message FileManifestJournalEntry {
  enum EntryType {
    ADD = 0;
    MODIFY = 1;
    REMOVE = 2;
  }

  oneof entry {
    FileManifest manifest = 1;
    bytes path = 2;
  }
  EntryType type = 3;
}

message BackupConfiguration {
    message Share {
        string name = 1;
        repeated string includes = 2;
        repeated string excludes = 3;
        string pathPrefix = 4;
    }

    message Task {
        string command = 1;
        repeated Share shares = 2;
        repeated string includes = 3;
        repeated string excludes = 4;
    }

    message Operations {
        repeated Task tasks = 1;
        repeated Task finalizedTasks = 2;
    }

    Operations operations = 1;
}

message FileChunk {
  bytes data = 1;
}

message PrepareBackupRequest {
  BackupConfiguration configuration = 1;
  uint32 lastBackupNumber = 2;
  uint32 newBackupNumber = 3;
}

message PrepareBackupReply {
  StatusCode code = 1;
  bool needRefreshCache = 2;
}

message RefreshCacheReply {
  StatusCode code = 1;
}

message LaunchBackupRequest {
  uint32 backupNumber = 1;
}

message GetChunkRequest {
  bytes filename = 1;
  uint64 position = 2;
  uint64 size = 3;
  bytes sha256 = 4;
}

service WoodstockpériphériqueService {
  rpc PrepareBackup(PrepareBackupRequest) returns (PrepareBackupReply) {}

  rpc RefreshCache(stream FileManifest) returns (RefreshCacheReply) {}

  rpc LaunchBackup(LaunchBackupRequest) returns (stream FileManifestJournalEntry) {}

  rpc GetChunk(GetChunkRequest) returns (stream FileChunk) {}
}

Vient ensuite l'écriture de la partie périphérique et de la partie serveur. Je me base sur le Quick Start pour la partie C++.

L'exemple se veut simple, et surtout utilise l'API synchrone. gRPC propose également une API asynchrone mais qui est plus complexe à mettre en place.

L'API asynchrone devrait permettre d'améliorer les performances dans le cadre d'application multi-threadé. Pour notre cas de test nous allons commencer par utiliser l'API synchrone.

Dans le cas de l'API synchrone, le serveur reste multi-threadé (et peut donc aussi recevoir plusieurs requêtes en même temps). Vous trouverez les sources de la partie périphérique et de la partie serveur dans le ZIP suivant dans le dossier périphérique-sync.

Le développement de la partie HTTP/2 vient ensuite. La librairie n'est pas des plus faciles d'utilisation, je me concentre sur le développement à des fins de tests de perfs et non de sécurité (ou de non bug).

Benchmark

Un bench d'envoi de fichier via gRPC a déjà été fait par d'autres personnes. L'article se trouve ici : Sending files via gRPC. Je vous laisse le lire. Cet article a été fait en Go. Je suppose que le résultat devrait être très proche d'un résultat en C++.

Le résultat de cet article est que du pur HTTP/2 en Go est tout de même plus performant que gRPC (probablement du à la sérialisation et à la déserialisation des messages qui prend un peu plus de temps).

Pour réaliser les tests, nous avons utilisé le scénario suivant :

  • Sauvegarde d'environ 46 Go de données à partir d'un périphérique équipé d'un SSD ;
  • Le PC de destination est équipé d'un disque dur connecté en USB 3 ;
  • Les machines sont reliées par un tuyau de 1 Gb/s ;
  • Les chunks sont stockés au format du pool défini dans l'article [/post/woodstock_brtfs](Utilisation de Btrfs et son remplacement) ceci afin de representer au mieux notre cas d'utilisation.
  • Le hash est un SHA3_256, qui est plus performant qu'un SHA2_256.

Les tests ont été réalisés dans des conditions relativement réelles (calcul des hash, copie des fichiers dans un pool qui fait de la déduplication), ce qui prend en compte le transfert des fichiers mais aussi les calculs effectués autour.

Une fois le développement effectué, pour compiler la version compatible gRPC, il suffit de lancer la commande suivante :

cd périphérique-sync
mkdir build
cd build
cmake -DWITH_PROTOCOL_HTTP2=OFF -DWITH_PROTOCOL_GRPC=ON -DCMAKE_BUILD_TYPE=Release ../
make -j16

# Lancement du périphérique
./src/périphérique_daemon/périphérique

# Lancement du serveur
./src/server/server

Pour la partie HTTP/2, il faudra inverser les valeurs ON et OFF. Il est important de noter que la configuration du serveur est actuellement en dur et liée à la sauvegarde de l'ordinateur. Par conséquent, si vous souhaiter lancer le bench chez vous, cette configuration doit être modifiée pour adapter le serveur à votre propre configuration.

Une fois les tests effectués, j'ai réécris uniquement la partie serveur en NodeJS. La réécriture en Node.JS a été plus rapide que le développement en C++, car je savais exactement où nous allions et que le Javascript est plus simple à écrire (quand on utilise des frameworks comme RxJs, ...).

Voici le résultat du benchmark:

rSync gRPC HTTP/2
Server C++ / Périphérique C++ 47m 3h01 2h25
Server Node.JS / Périphérique C++ (concatMap) 4h57 1h50
Server Node.JS / Périphérique C++ (mergeMap) 60m

Je n'ai pas testé tous les cas de figure, j'ai principalement fait varier la partie serveur. Et je me suis concentré sur gRPC car je voulais comprendre pourquoi HTTP/2 était beaucoup plus rapide que gRPC en Node.JS.

Ce que le l'on peut remarquer:

  • En C++ pure:

    • rsync est le plus rapide
    • HTTP/2 est légèrement plus rapide que gRPC
    • La lenteur est problablement due au fait qu'on soit monothreadé
  • Sur une écriture monothreadé coté serveur:

    • C++ est plus rapide que Node.JS pour gRPC mais pas pour HTTP/2
  • La parallélisation avec NodeJS est beaucoup plus simple qu'en C++ : il est important de noter que l'amélioration visible en NodeJS vient du fait qu'à partir du moment où l'on ne fait que des I/O en asynchrone, le thread JavaScript effectue peu de traitement, ce qui permet d'avoir des traitements asynchrones multi-threadés.
  • Le transfert est nettement plus lent que rSync (même en HTTP/2)

La grande question est donc : dans quel langage doit-on écrire le périphérique et le serveur au vu de ces résultats ? L'écriture en C++ sera plus performante avec l'utilisation de threads. Cependant, le code en C++ sera plus complexe (surtout avec le multi-threading) et plus long à écrire, avec des risques de fuites mémoires et de segmentation fault.

L'écriture en JavaScript sera moins performante, mais la parallélisation sera plus facile, et le risque de fuite sera moins élevé. J'ai par contre peur que la consommation mémoire des objets JavaScript soit plus importante que celle des objets en C++.

Note de 2023: Le constat de la consommation mémoire des objets en Node.JS par rapport à C++ est vérifier. Je ferai un article dédié sur le sujet.

Amélioration possible du bench

  • Coté C++: J'ai testé RxCpp pour ajouter plus d'asynchronisme et traiter les objets en mode flux. Malheureusement, contrairement à Node.JS, la librairie RxCpp est mono-threadé. Du coup cela ne change rien. L'utilisation de RxCpp fait aussi des noeuds au cerveau à cause de l'utilisation des templates.
  • Coté C++: utiliser l'API Asynchrone de gRPC pourrait améliorer les choses.
  • Coté Node.JS: Il est possible d'améliorer les performances avec des bindings sur des librairies C++.

Pourquoi NodeJS / Pourquoi pas NodeJS

On m'a toujours dit, et je considère que c'est une bonne pratique, d'avancer vite pour avoir une première version et ensuite d'optimiser ce qui doit l'être (et uniquement ce qui doit l'être).

Clairement écrire en Javascript sera plus rapide que l'écriture en C++. Je pense donc commencer par un MVP en utilisant NodeJS. Et ensuite, si le besoin s'en fait sentir, je viendrai réécrire des parties en C++ soit via des bindings, soit via des binaires dédiés.

Je vois déjà les anti NodeJS qui vont me dire que faire un logiciel de sauvegarde avec NodeJS c'est pas terrible. Que NodeJS est un language pourris :) ou que c'est un language pour faire des sites Internet, ou que cela utilise un interpreteur.

Mais quand on pense que:

  • BackupPC est fait en Perl (avec une partie faite en C depuis la version 4)
  • Borg est fait en Python

Bref des logiciels de sauvegardes faits avec un language intreprété, il y en a.

Node.JS est basé sur le moteur V8 de Google dont les performances augmentent à chaque version. Donc la réponse pourrait simplement être: pourquoi pas ! Le plus important c'est de faire un logiciel fiable et qui fonctionne. Donc que l'on utiliser NodeJS, PHP ou autre pour faire le développement, au final : OSEF.

Je terminerai donc sur cette note finale. Je vais finaliser quelques tests et me lancer dans le développement.

Note de 2023 : Finalement, j'ai donc fait le développement en NodeJS. Les performances ont été grandement améliorées depuis le bench. Par contre la consomation en mémoire des objets NodeJS éclate les scores les plus pessimistes que j'avais :).

Une fois la première version sortie, je réécrirai certaines parties dans des modules natifs pour améliorer les performances. Voir le client complètement.

Par contre au lieu de choisir C++, je pense partir sur Rust que j'ai commencé à utiliser très récemment. Je ferai un article sur le bench que j'ai pu faire sur la consommation mémoire de nodejs vs rust sur le sujet.