Application côté Serveur
Web Assembly & NodeJS
Problématique et objectif
De la même manière que le navigateur a des performances limitées, car le JavaScript n’est pas conçu pour cela, les applications back-end Node.js subissent le même problème (même si ce n'est pas toujours visible).
Une application backend Node.js aura du mal à supporter des charges ou des traitements lourds.
Nous allons explorer les solutions disponibles pour résoudre ce problème, car certaines librairies NodeJS utilisent déjà le WebAssembly afin d'améliorer les performances.
En effet, Node.js est tout à fait en mesure de prendre en charge des modules wasm
: “https://nodejs.dev/en/learn/nodejs-with-webassembly/”.
Nous allons créer un outil permettant d’encoder et de compresser une vidéo en utilisant FFmpeg côté serveur.
FFmpeg.WASM
FFmpeg est une collection de programmes conçus pour effectuer des opérations sur des flux audio ou vidéo (enregistrement, lecture ou conversion d'un format à un autre). Cette bibliothèque est largement employée par d'autres applications ou services tels que VLC, iTunes, etc.
FFmpeg a été initialement écrit en C et C++, mais un portage WebAssembly existe sous la forme d'une librairie Node.js : “https://github.com/ffmpegwasm/ffmpeg.wasm”.
Prérequis
Pour suivre ce TP, vous devez avoir :
- Un environnement de développement avec Node.js installé et à jour.
- Quelques connaissances dans la librairie Express.
- Un peu d'expérience en développement Web.
- Une vidéo de bonne qualité et pas trop longue :
mp4/mkv
- 1080p
- Pas plus de 4 min.
1. Mise en place
Nous allons créer un premier projet pour la partie serveur.
Créez un dossier ffmpeg-api
et exécutez la commande suivante à l'intérieur :
Il faut ensuite installer les librairies dont nous aurons besoin avec la commande suivante :
@ffmpeg/ffmpeg @ffmpeg/core
librairies servant à l'utilisation de FFmpegexpress
framework API légermulter
Middleware permettant de prendre en charge les fichiers envoyés par formulaire sous le formatmultipart/form-data
p-queue
Gestionnaire de file d'attente avec support de concurrence
Nous allons installer la définition des types nécessaires pour typescript
Pour automatiser le lancement de notre serveur, nous allons ajouter les scripts suivants dans le fichier package.json
:
dev
: lancenodemon
en lui demandant d'observer le dossiersrc
pour les changements. À chaque changement, il relance le serveur avects-node
.build
: compile le projet avec tsup et génère le fichier de destination.start
: une fois le projet compilé, permet de lancer le serveur en production.start:build
: compile en production, puis lance l’application directement.
L’argument --experimental-wasm-threads
permet au WebAssembly de gérer le multi-threading. C’est une fonctionnalité encore expérimentale et qui peut être instable.
Il ne nous manque plus que le fichier tsconfig.json
à la racine du projet qui configure la façon dont TypeScript fonctionne.
baseUrl
: indique à TS que le dossier courant est l'entrée du projet.target
: vers quelle version de JavaScript nous allons compiler.module
: quels types de modules JavaScript nous allons utiliser.paths
: définit les chemins personnalisés du projet.ts-node
: demande àts-node
d'utiliser nos chemins personnalisés.
2. Préparation du serveur express
Nous allons mettre en place la base pour notre serveur express en créant le fichier src/index.ts
avec le code suivant :
Nous allons essayer cela, en lançant la commande suivante et en nous rendant sur le lien http://localhost:3000 :
Quand nous essayons d'atteindre notre page, nous obtenons le message Cannot GET /
, ce qui signifie que notre serveur Express est bien en ligne. Il ne nous reste plus qu'à lui définir les routes dont nous aurons besoin prochainement.
3. Préparation du client web
Nous allons maintenant préparer la partie client, qui servira à envoyer la vidéo à convertir à notre serveur.
Dans un dossier à côté de celui de notre serveur ffmpeg-api
, nous allons créer un nouveau dossier ffmpeg-client
. Dans ce dossier, nous allons créer le fichier index.html
avec le contenu suivant pour commencer :
Nous allons de ce pas créer le fichier client.js
qui est importé par cette page, avec le contenu suivant :
Avec ce petit code, allons vérifier que notre client réagit comme on le souhaite lorsqu’on sélectionne un fichier vidéo.
Notre fichier est correctement chargé dans le navigateur, il nous reste simplement à l'envoyer au serveur et à récupérer le fichier vidéo retour.
Avant cela, nous allons faire quelques petits ajouts à ce fichier client.js
pour y voir plus clair sur nos intentions.
API_ENDPOINT
: est la route destinée à envoyer la vidéo au serveur pour la conversion.submitButton.addEventListener
: vérifie désormais si le fichier sélectionné est bien une vidéo. Il s'occupera également de récupérer la réponse et d'afficher la vidéo convertie sur la page web.function error*(message)
: petite fonction utilitaire pour afficher les erreurs sur la page web.async function uploadVideo*(video)
: fonction servant à envoyer le fichier vidéo au serveur et à récupérer sa réponse.- Les API web utilisent généralement le format
JSON
pour leurs requêtes. Le problème avec une vidéo, c'est qu'il faut l'encoder enbase64
avant de pouvoir encapsuler ses données dans unJSON
... Cela augmenterait alors la taille de la vidéo (déjà assez lourde) d'au moins 30 % ! - Pour remédier à cela, nous allons utiliser le format multipart requests à la place.
- Ce format permet d'envoyer des fichiers binaires tels quels, sans avoir à les encoder au préalable.
- Les API web utilisent généralement le format
4. Récupération de la vidéo côté serveur
Maintenant que tout est en place côté client, il faut que notre serveur Express puisse recevoir la vidéo, pour ensuite la convertir en utilisant la librairie FFmpeg en WebAssembly (qui est le but principal).
Il faut également s'assurer que le fichier reçu est bien un fichier vidéo, d'une taille raisonnable (comme le fichier sera stocké en mémoire, il vaut mieux qu'il ne dépasse pas un certain poids pour ne pas mettre le serveur à mal).
Nous allons utiliser le middleware "multer" évoqué plus tôt qui va nous permettre de définir une règle pour les fichiers envoyés via le format multipart requests.
Nous avons maintenant tout ce qu’il faut pour créer notre route /convert
:
uploadMiddleware.single('video')
va mettre en place le middleware défini plus haut et bloquer la requête si le fichier ne respecte pas le format attendu.- Le contenu du fichier sera dans
req.file.buffer
.
5. Conversion de la vidéo avec FFmpeg
Nous allons enfin pouvoir utiliser notre remarquable librairie en WebAssembly ! Elle était, à l'origine, pensée pour être exécutée côté navigateur, mais avec les versions les plus récentes de Node.js, c'est désormais possible sans problème.
Dans un premier temps, nous mettrons en place l'instance de FFmpeg afin de pouvoir l'utiliser au moment opportun :
- *
async function* getFFmpeg()
renvoie l’instance de l’outil FFmpeg, une fois que ce dernier aura été chargé (cela peut prendre plusieurs secondes au démarrage)
Nous pouvons maintenant appeler cette fonction dans notre route convert
:
- Nous récupérons bien une instance de FFmpeg
- Nous définissons aussi quelques variables, dont une pour accueillir les données de la vidéo une fois cette dernière convertie
Il faut ensuite donner à l’instance FFmpeg notre vidéo d’entrée pour que ce dernier puisse la traiter
Il ne nous reste plus qu’à lancer la commande FFmpeg avec les bons arguments afin de convertir notre vidéo ! Nous pouvons retrouver tous ces arguments sur la documentation officielle “https://trac.ffmpeg.org/wiki/Encode/H.264”
i
: Flux d'entréevcodec
: Codec à utiliser (x264, x265, VP8, VP9, WAV, etc.)crf
: Facteur de taux constant, contrôle le débit de sortie, qui varie entre 0 et 51. Plus la valeur est élevée, plus la vidéo sera légère, mais de moins bonne qualité et vice versa. La valeur par défaut est 23.preset
: Préréglage qui fournira une certaine vitesse d'encodage. Un préréglage plus lent fournira une meilleure compression de la vidéo, mais à un temps d'encodage beaucoup plus long. Cette option doit être ajustée avec lecrf
afin de trouver le bon équilibre poids/qualité/temps d'encodage.c:a
: Encodage de l'audio (MP3, AAC, etc.)b:a
: Débit de sortie du flux audio- Le dernier argument sera le nom du fichier de sortie.
Il faut alors attendre que l'encodage de la vidéo se termine... Selon le poids et la durée de la vidéo, cela peut prendre plus ou moins de temps.
Pour récupérer notre vidéo de sortie, nous allons à nouveau utiliser ffmpeg.FS
pour lire les données du système de fichiers.
Nous pouvons ensuite supprimer notre fichier d’entrée et de sortie du contexte FFmpeg pour libérer de la mémoire.
Avec un peu de refacto, notre fichier final sera le suivant :
Voilà un exemple de conversion avec une célèbre vidéo :
6. Bonus : Gérer plusieurs demandes de conversion
Il n'est pas encore possible d'utiliser WebAssembly et Node.js pour traiter deux requêtes en parallèle (du moins, pas avec la librairie ffmpeg.wasm
). La bibliothèque de threads WebAssembly a été introduite dans V8/Chrome depuis 2019. Cependant, cela reste encore expérimental dans Node.js et il faut activer le flag --experimental-wasm-threads
pour l'utiliser.
Même si vous essayez (avec le flag) de lancer deux conversions simultanément, vous obtiendrez la sortie suivante :
Ne pouvant donc pas gérer plusieurs demandes en même temps, il est nécessaire de mettre en place un système de file d'attente afin d'éviter de faire planter notre serveur.
Pour cela, nous allons importer la librairie p-queue qui permet de créer des files d'attente concurrentielles.
Cela nous permet de créer une file d'attente concurrentielle de maximum un traitement à la fois.
Il suffit alors d'utiliser cette file au moment où nous devons encoder.
Récapitulatif
Ressources
https://www.secondstate.io/articles/getting-started-with-rust-function/
https://ffmpegwasm.netlify.app/
https://nodejs.dev/en/learn/nodejs-with-webassembly/
https://trac.ffmpeg.org/wiki/Encode/H.264
https://www.ffmpeg.org/ffmpeg-filters.html
https://ffmpeg.org/ffmpeg.html
https://ffmpeg.org/documentation.html
https://jeromewu.github.io/ffmpeg-wasm-a-pure-webassembly-javascript-port-of-ffmpeg/
WASI
Introduction
Pour l'exécution côté serveur, on entend bien sûr une exécution très éloignée du navigateur. Nous allons nous concentrer dans ce chapitre sur l'exécution en dehors de celui-ci.
Nous pouvons mettre en évidence deux méthodes d'exécution hors navigateur :
- Avec Node.js, abordé dans un autre chapitre ;
- Et avec la méthode qui nous intéresse, le WebAssembly, qui pourrait s'approcher de la JVM si nous nous limitons à un point de vue conceptuel.
Nous parlerons de la spécification WASI, qui a pour objectif de rendre le WebAssembly un runtime portable, c'est-à-dire exécutable sur n'importe quel environnement.
Nous parlerons ensuite bien sûr de ces runtime qui respectent la spécification WASI et qui nous permettent de développer des applications serveur compilées en wasm.
WASI qu'est-ce que c'est ?
Tout d'abord, il est important de dire que les personnes travaillant sur les spécifications WASI sont un sous-groupe des personnes travaillant sur le WebAssembly; ce ne sont peut-être pas les mêmes personnes, mais elles gardent un contact étroit.
Les promesses de WASI seront les mêmes que celles du WASM :
- Sécurité
- Polyglotte (Go, Rust, C+C++, etc.)
- Rapide
- Léger
Les fondations de cette spécification, d'un point de vue sécurité, font que les modules respectant les spécifications WASI ne peuvent pas :
- Accéder au système d'exploitation
- Obtenir l'accès à la mémoire que le « host » n'a pas fournie
- Faire des appels sur le réseau
- Lire ou écrire dans des fichiers
WASI est une spécification qui permet d'accéder, en toute sécurité et en toute isolation, au système sur lequel tourne le module WASM.
Nous pouvons alors dire qu'un module WebAssembly s'exécute complètement isolé de la fonctionnalité native du système hôte sous-jacent. Cela signifie que, par défaut, les modules Wasm sont conçus pour effectuer uniquement des calculs purs.
En conséquence, l'accès aux ressources de niveau "OS" telles que les descripteurs de fichiers, les sockets réseau, l'horloge système et les nombres aléatoires n'est pas possible depuis WASM. Cependant, il existe de nombreux cas où un module Wasm doit faire plus que du calcul pur ; il doit interagir avec la fonctionnalité native de l’OS.
Nous constatons que pour créer et déployer des applications serveur, nous avons besoin d'un runtime qui respecte les spécifications WASI. Nous allons donc examiner en détail les trois plus populaires pour vous aider à mieux comprendre leurs avantages et inconvénients et à choisir celui qui convient le mieux à vos besoins.
WasmEdge
WasmEdge est un runtime WebAssembly léger, performant et extensible pour les applications cloud native, edge et décentralisées. Développé par Second State, il alimente les applications sans serveur, les fonctions embarquées, les microservices, les contrats intelligents et les appareils IoT.
Comparé aux conteneurs Linux, WasmEdge peut être jusqu'à 100 fois plus rapide au démarrage, 20% plus rapide à l'exécution et consommer jusqu'à 1/100 de la taille d'une application similaire en conteneur Linux. Cela signifie qu'il est possible de créer des applications plus rapides, plus légères et plus faciles à gérer. WasmEdge offre aux développeurs une plus grande flexibilité et des performances optimisées pour leurs applications, avec des temps de démarrage plus rapides et une consommation de mémoire réduite.
Ce runtime a été optimisé pour l'edge computing et ses principaux cas d'utilisation sont :
- Applications Jamstack, avec un front-end statique et un backend serverless de type FaaS (Function as a Service)
- Automobile
- IOT
WasmEdge fournit également un ensemble d'extensions pour des cas d'utilisation plus avancés, tels que :
- Tensorflow
- Ethereum
- Network sockets
WasmEdge a également décidé d'implémenter le support des sockets en se basant sur la spécification actuelle des WASI-sockets.
Un autre avantage de WasmEdge est qu'il s'agit d'un projet sandbox officiel hébergé par la CNCF.
Wasmtime
Le premier avantage de ce runtime est qu'il est développé par la Bytecode Alliance, qui, comme vous le verrez, est une organisation composée de membres très compétents dans le domaine de l'informatique.
Il permet l'exécution du code WebAssembly en dehors du Web et peut être utilisé à la fois comme un utilitaire de ligne de commande ou comme une bibliothèque intégrée dans une application plus large.
Wasmtime prend en charge un riche ensemble d'API pour interagir avec l'environnement hôte via le standard WASI.
Wamstime utilise le compilateur Cranelift pour tous les cas d'utilisation, ce qui, par rapport à Wasmer, peut entraîner une perte de performance dans certains cas d'utilisation. N'oublions pas que, par rapport aux autres runtimes, celui-ci est développé par la Bytecode Alliance, qui ne tire aucun profit du projet.
Wasmer
La première version de Wasmer est sortie en 1 janvier 2021. D'un point de vue utilisation, Wasmer s'utilise comme une bibliothèque que l'on intègre dans n'importe quel langage de programmation supporté.
Wasmer vous permet d'exécuter des modules WebAssembly de manière autonome ou intégrée dans un grand nombre de langages. Wasmer est conçu pour fournir trois caractéristiques clés :
- Permettre aux programmes de s'exécuter dans n'importe quel langage de programmation.
- Permettre à des binaires extrêmement portables de fonctionner sans modification sur n'importe quel système d'exploitation.
- Agir comme un pont sécurisé pour les modules Wasm afin d'interagir avec les fonctionnalités natives du système d'exploitation, via des interfaces binaires d'application (ABI) telles que WASI.
Pour le premier cas, ils offrent des intégrations pour plusieurs langages, ce qui permet à un module Wasm d'être exécuté à partir de n'importe quel langage de programmation.
Pour le second cas, ils offrent leur Runtime autonome pour exécuter des binaires Wasm sur n'importe quelle plateforme et chipset.
Pour illustrer ses capacités, les développeurs de Wasmer ont compilé et exécuté une version non modifiée du serveur web Nginx, en utilisant des appels WASI pour interagir avec le système hôte.
Pour résumer les caractéristiques principales de Wasmer :
- Pluggabilité : Compatible avec les différents frameworks de compilation, tout ce dont vous avez besoin (par exemple, Cranelift).
- Vitesse/Sécurité : Capable d'exécuter Wasm à une vitesse presque native dans un cadre complètement sandboxé.
- Universalité : Fonctionne sur n'importe quelle plateforme (Windows, Linux, etc.) et n'importe quel chipset.
- Support : Conforme aux normes de la suite de tests WebAssembly avec une large base de soutien de la communauté des développeurs et des contributeurs.
Gros point positif pour ce runtime : il met à disposition un gestionnaire de paquet nommé wapm qui est destiné à héberger des modules WebAssembly incluant des binaires et des bibliothèques universelles.
Bytecode alliance
Pour faire simple, WASM/WASI sont les spécifications du W3C et la Bytecode Alliance s'occupe de leur implémentation. La Bytecode Alliance est un projet visant à créer un environnement d'exécution multiplateforme et multipériphérique basé sur les avantages du WebAssembly. Ses membres fondateurs sont Mozilla, Fastly, Intel et Red Hat, et ils ont été rejoints par d'autres grands noms, tels qu'Amazon, ARM, Docker, Google, Microsoft et d'autres. Ils visent à créer de nouvelles fondations pour nos logiciels en se basant sur les standards WebAssembly et WebAssembly System Interface (WASI).
Les membres de cette alliance contribuent à plusieurs projets open source de l'alliance Bytecode, notamment :
- Wasmtime, un runtime léger et performant ;
- Lucet, un environnement d'exécution avec compilation hors ligne pour WebAssembly et WASI axé sur les applications à faible latence et à haute concurrence ;
- WebAssembly Micro Runtime (WAMR), un environnement d'exécution WebAssembly basé sur un interpréteur pour les périphériques embarqués ;
- Cranelift, un générateur de code multiplateforme misant sur la sécurité et la performance, écrit en Rust.
Lequel choisir ?
Le choix va beaucoup dépendre de votre cas d'utilisation et de quand vous allez devoir le faire. En effet, le WebAssembly et encore plus le WASI sont des standards avec des implémentations très récentes. Il faudra donc effectuer un benchmark des performances des runtimes en fonction de vos besoins et des points clés de votre application.
Pourquoi utiliser un runtime serveur WASI plutôt qu’un runtime classique ?
- La portabilité de votre application vous permettra d'exécuter le même code sur n'importe quel système d'exploitation, y compris sur des puces très légères.
- La sécurité du standard WASI vous permettra d'exécuter votre code dans un environnement sandboxé et donc sûr pour l'hôte.
- La taille du binaire compilé sera beaucoup plus petite qu'un conteneur Docker, par exemple.
- La rapidité de démarrage et d'exécution est sans égale :
- Le WebAssembly est 100 fois plus rapide au démarrage qu'un conteneur très léger basé sur Linux.
- Le WebAssembly est aussi 10 à 50 % plus rapide à l'exécution.
Pourquoi ne pas toujours le faire si seuls des avantages en découlent ?
- Le WebAssembly côté serveur est une technologie très récente et, par conséquent, elle évolue constamment.
- Si vous n'avez pas de problématique de portabilité, comme l'edge computing par exemple.
- Si les problématiques de sécurité ne sont pas essentielles pour vous.
- En raison de sa jeunesse, le WebAssembly ne bénéficie pas d'une grande communauté, contrairement à Docker par exemple. Il se peut donc que vous n'ayez pas de réponse à certaines de vos questions lors d'un problème rencontré.
- Aujourd'hui, les spécifications WASI ne permettent pas de tout faire. Il est par exemple impossible pour l'instant de faire du vrai multi-threading côté serveur.
Aller plus loin
Ces runtimes ne sont bien évidemment pas seuls et certains runtimes beaucoup plus spécialisés sont apparus, nous vous laisserons jeter un coup d'œil si cela vous intéresse.
“https://hacks.mozilla.org/2019/03/standardizing-wasi-a-webassembly-system-interface/”
Ressources
Serverless / Cloud
Partie 1
Partie 2
Ressources
https://github.com/suborbital/atmo
https://shopify.engineering/shopify-webassembly