Étude de cas entrepreneurial micro-SaaS Convertisseur AIFF à MP3 Partie 1
Book a callLe développement de l’API constitue le cœur technique de ce projet, visant à offrir une solution robuste pour la conversion de fichiers audio AIFF en MP3 via une interface web accessible. Cette section retrace les étapes entreprises pour concevoir, tester et déployer cette API, en mettant en lumière les défis rencontrés et les solutions mises en œuvre. Les principaux défis avaient été de garantir une gestion efficace des fichiers en termes de gestion de la mémoire de stockage du serveur, de la détermination des moments d'exécutions et de la sélection de l'algorithme d'optimisation d'usage des ressources physiques d'ordinateur.
Pour surmonter cela, j’ai mis en place une file d’attente (« queue »), garantissant une expérience utilisateur fonctionnelle. Cette solution respectant les contraintes matérielles du serveur. Dans cette partie, j'expliquerai pourquoi la solution dite « file d'attente » est la meilleure à ce stade d'avancement.
Toutes ces considérations sont importantes pour éviter que le serveur ne « crashe » en raison d'un manque de mémoire ou d'une surcharge de capacité de traitement.
La mise en place de l’environnement de développement a été une étape cruciale pour poser les bases d’un processus itératif efficace.
J’ai débuté en initiant une instance locale Node.js.
npm init
Après l'installation des dépendances nécessaires via npm, s'est ensuivi, incluant express, multer, dotenv, fs, et path.
Ces packages ont été sélectionnés pour leur capacité à gérer les requêtes HTTP, les téléversements de fichiers, le chargement de variables dites « d'environnement » et les opérations sur le système de fichiers.
Afin de faciliter le débogage des tests locaux, j’ai configuré le fichier launch.json dans Visual Studio Code pour sélectionner l'instance Nodemon qui roule pour la relier à l'instance de débogage.
Nodemon permet un redémarrage automatique du serveur à chaque modification du code, ce qui accélère ainsi le processus de développement.
Autrement dit, le fichier launch.json, qui est le fichier de configuration du système de débogage sur Visual Studio Code, a été configuré de manière à permettre le choix de l’instance Nodemon en cours d’exécution afin d’effectuer un débogage sur celle-ci. Voici un extrait typique de cette configuration :
{ "version": "0.2.0", "configurations": [ { "type": "node", "request": "attach", "name": "Attach to Nodemon Process", "processId": "${command:PickProcess}", "skipFiles": [ "/**" ], "localRoot": "${workspaceFolder}", "remoteRoot": "${workspaceFolder}" } ] }
Annexe 4.2.3 /Users/Zouhir/Documents/NODE2/aiffamp3/.vscode/launch.json
Pour effectuer les conversions audio, j’ai installé l'outil FFmpeg sur deux environnements distincts : localement sur macOS Ventura 13.1 (22C65) et sur le serveur qui fonctionne sous la distribution Ubuntu 20.04.5 LTS.
L’installation a été réalisée à l’aide de Homebrew pour la version locale et avec la commande apt pour la version serveur.
Les commandes utilisées pour les installations étaient respectivement :
brew install ffmpeg
apt install ffmpeg
Cet outil « open-source » est essentiel pour la conversion des fichiers AIFF en MP3.
Afin de valider son bon fonctionnement, j’ai effectué des tests de conversion localement et dits « distants » (c'est-à-dire sur le serveur) manuellement sur quatre fichiers AIFF de tailles variées.
Ces fichiers incluent : long.aiff, qui pèse 2,57 Go et a une durée de 3 heures, 43 minutes et 23 secondes ; med60.aiff, qui fait 649,2 Mo et dure 1 heure, 1 minute et 20 secondes ; med30.aiff, qui a une taille de 320,9 Mo pour une durée de 30 minutes et 19 secondes ; et enfin short.aiff, qui fait 8,2 Mo et dure 1 minute et 25 secondes.
À titre d'information, j’ai utilisé le logiciel SCP pour effectuer le transfert de fichiers entre mon ordinateur local et le serveur.
En raison de contraintes de mémoire en environnement de production, le serveur est limité à un traitement de fichiers ne dépassant pas 1,2 Go. C’est la raison pour laquelle je n’ai pas réalisé de tests manuels de conversion pour le fichier long.aiff, qui excède cette limite sur le serveur.
Tous les tests manuels ont réussi. J'ai pu conclure que ffmpeg fonctionnait effectivement bien localement et en production en opérant manuellement.
Dans cette section, je vais détailler les spécifications de la machine locale et du serveur distant utilisés pour ce projet.
Le serveur utilisé dans la version 1 est hébergé dans le cloud via Linode, avec un plan "Shared CPU" comprenant :
La machine locale utilisée pour le développement présente les caractéristiques suivantes :
Le fonctionnement souhaité de l’application dépend fortement des contraintes matérielles imposées par le serveur distant, qui dispose d’un seul CPU et de 2 Go de RAM.
Dans ce contexte, une tâche intensive comme la conversion de fichiers audio AIFF en MP3 nécessite une gestion efficace des ressources.
J’ai donc opté pour un algorithme basé sur une file d’attente (queue), qui traite les requêtes de manière séquentielle.
Cette approche est idéale pour un serveur avec un seul CPU, car elle évite de surcharger le système en exécutant plusieurs tâches simultanément, ce qui pourrait entraîner des goulets d’étranglement ou des crashs dus à une consommation excessive de mémoire ou de puissance de calcul.
En informatique, la gestion des processus et des threads, la programmation asynchrone, ainsi que l’optimisation des ressources matérielles, sont des concepts fondamentaux.
Un thread est une unité d’exécution au sein d’un processus, et chaque CPU peut exécuter un ou plusieurs threads en fonction de ses capacités.
Dans notre cas, Node.js repose sur un modèle single-threaded : il utilise un seul thread principal pour gérer les requêtes via une boucle d’événements (event loop).
Cela signifie que toutes les opérations non bloquantes (comme les requêtes HTTP) sont traitées de manière asynchrone, tandis que les tâches bloquantes, comme la conversion de fichiers avec FFmpeg, sont déléguées à des processus externes. Ces processus s’exécutent sur le CPU, mais leur gestion reste coordonnée par le thread principal de Node.js.
La file d’attente que j’ai implémentée fonctionne ainsi : lorsqu’une requête de conversion est reçue, elle est ajoutée à une liste (queue), et une fonction récursive traite chaque élément l’un après l’autre.
Cette approche garantit que le serveur ne dépasse pas ses limites matérielles, notamment la mémoire disponible (2 Go) et la capacité de calcul d’un seul CPU.
Par exemple, si plusieurs utilisateurs soumettent des fichiers volumineux en même temps, un traitement parallèle risquerait d’épuiser la RAM ou de provoquer des erreurs.
Avec une file d’attente, les tâches sont mises en attente et exécutées séquentiellement, ce qui offre une stabilité accrue et une meilleure prévisibilité des performances.
Ce choix est également influencé par les enseignements en informatique sur les structures de données et les algorithmes.
Une file d’attente (FIFO – First In, First Out) est une structure classique pour gérer des tâches dans un environnement à ressources limitées.
De plus, pour analyser la complexité temporelle et spatiale des algorithmes : ici, la file d’attente offre une complexité linéaire (O(n)), où n est le nombre de tâches, ce qui est acceptable pour une application comme la nôtre.
La notation "complexité linéaire" (O(n)) fait partie de l’analyse en notation Big-O, qui mesure comment le temps d’exécution ou l’espace mémoire requis par un algorithme évolue en fonction de la taille de l’entrée (n).
Dans notre cas, n représente le nombre de fichiers dans la file d’attente. Une complexité O(n) signifie que le temps de traitement augmente proportionnellement au nombre de tâches : si nous avons 10 fichiers, le temps sera environ 10 fois plus long que pour 1 fichier.
Cela reflète notre algorithme, car chaque fichier est traité successivement.
Cette simplicité est un atout dans un contexte avec un seul CPU, car elle évite des calculs supplémentaires ou une gestion complexe des ressources.
En revanche, une approche parallèle nécessiterait plusieurs threads ou processus, ce qui n’est pas réalisable avec un seul CPU sans compromettre les performances.
Dans l’avenir, une version 2 du projet pourrait évoluer vers un algorithme multi-threadé pour gérer une charge plus importante.
Cela impliquerait l’utilisation de modules comme worker_threads dans Node.js.
Cependant, une telle évolution nécessiterait une mise à niveau matérielle, par exemple vers une configuration Linode avec 4 CPU, afin de répartir efficacement la charge entre plusieurs cœurs.
En attendant, la file d’attente reste la solution la plus adaptée à notre contexte actuel, car elle maximise l’utilisation des ressources limitées.
Le code implémenté pour gérer la conversion des fichiers AIFF en MP3 introduit plusieurs contraintes qui influencent directement le fonctionnement de la file d’attente. Ces contraintes sont liées aux limites matérielles définies.
Voici une analyse détaillée :
Limite de mémoire totale (MAX_MEMORY) : La constante MAX_MEMORY est fixée à 32 Go (32 * 1024 * 1024 * 1024 octets). Cela représente la capacité maximale de mémoire que la file d’attente peut utiliser pour stocker les fichiers en attente de traitement. Si la somme des tailles des fichiers dans la file dépasse cette limite, les nouvelles requêtes sont rejetées avec un message d’erreur (HTTP 503 : "Server memory limit reached"). Cette contrainte est cruciale, car elle protège le serveur d’une surcharge mémoire.
Taille maximale d’un fichier (fileSize) : La configuration de multer impose une limite de 1 Go (1 * 1024 * 1024 * 1024 octets) par fichier téléchargé. Cela signifie que tout fichier AIFF dépassant 1 Go est refusé dès l’upload (HTTP 400 : "File too large"). Cette restriction garantit que les fichiers individuels restent gérables pour le serveur, mais elle limite la flexibilité pour les utilisateurs souhaitant convertir des fichiers plus volumineux.
Taille maximale de la file d’attente (MAX_QUEUE_SIZE) : La constante MAX_QUEUE_SIZE est définie à 6 éléments, ce qui signifie que la file d’attente ne peut contenir plus de 6 requêtes en attente à tout moment. Si cette limite est atteinte, les nouvelles requêtes sont rejetées (HTTP 503 : "Server queue limit reached"). Cette contrainte vise à éviter une accumulation excessive de tâches, mais elle peut poser problème lors de pics de demande.
Limitation de débit (RATE_LIMIT) : Le middleware rateLimiter restreint chaque adresse IP à 10 requêtes maximum toutes les 15 minutes (15 * 60 * 1000 ms). Si cette limite est dépassée, l’utilisateur reçoit une erreur (HTTP 429 : "Too many requests"). Cette mesure protège le serveur contre les abus, mais elle peut ralentir les utilisateurs fréquents.
Ces contraintes modifient la file d’attente en imposant des seuils stricts sur la quantité et la taille des fichiers qu’elle peut traiter à un moment donné. Par exemple, avec une limite de 32 Go de mémoire et une taille maximale de 1 Go par fichier, le serveur pourrait théoriquement gérer jusqu’à 32 fichiers en file d’attente si chaque fichier atteignait exactement 1 Go. Cependant, la restriction à 6 éléments dans MAX_QUEUE_SIZE réduit cette capacité à seulement 6 fichiers maximum, même si la mémoire disponible permettrait d’en stocker davantage.
Pour estimer combien de fichiers notre application peut traiter par jour, supposons que les requêtes varient entre 1 et 50 par heure sur 24 heures, avec une distribution inégale (heures creuses et heures de pointe). Voici une analyse simple basée sur le code et des hypothèses réalistes, sans tenir compte des contraintes du limiteur de débit ni de la taille maximale de la file d’attente :
Temps de traitement : Convertir un fichier AIFF en MP3 avec FFmpeg prend environ 2 minutes (120 secondes) pour un fichier moyen de 500 Mo, selon des tests typiques avec un seul CPU.
Capacité horaire : En 60 minutes, le serveur peut traiter 60 / 2 = 30 fichiers par heure s’il travaille sans arrêt.
Moyenne des requêtes : Avec 1 à 50 requêtes par heure, prenons une moyenne de 25 requêtes par heure. Sur 24 heures, cela fait 25 * 24 = 600 requêtes par jour en moyenne, en théorie.
Sans la limite de 6 éléments dans la file d’attente ni le limiteur de débit, le serveur peut traiter toutes les requêtes tant que la mémoire totale (32 Go, définie par MAX_MEMORY) n’est pas dépassée.
Avec un débit maximal de 30 fichiers par heure, le serveur gère facilement la moyenne de 25 requêtes par heure. Sur 24 heures, cela donne environ 720 fichiers traités par jour (30 * 24), en supposant que la taille cumulée des fichiers reste inférieure à 32 Go.