more fixes

This commit is contained in:
Quentin 2024-07-31 20:05:15 +02:00
parent 7bde101e33
commit fab99bab6d
Signed by: quentin
GPG key ID: E9602264D639FF68

View file

@ -40,7 +40,7 @@ On peut au besoin, se baser sur [l'image API 3.0](https://iiif.io/api/image/3.0/
L'aspect opérationnel quant à lui revêt des défis non triviaux, spécifiquement quant on a une approche *computing within limits*. L'aspect opérationnel quant à lui revêt des défis non triviaux, spécifiquement quant on a une approche *computing within limits*.
En effet, la conversion d'une image n'est pas une opération négligeable en terme de consommation de CPU & RAM. En effet, la conversion d'une image n'est pas une opération négligeable en terme de consommation de CPU & RAM.
À celà s'ajoute deux pré-requis particulièrement fort liés à l'aspect "à la volée" du service : À celà s'ajoute deux pré-requis particulièrement fort liés à l'aspect "à la volée" du service :
1) la conversion doit être réalisée en "temps réelle" et 2) l'arrivée des requêtes n'est pas prédictible ou uniformément étalée. 1) la conversion doit être réalisée de manière "intéractive" et 2) l'arrivée des requêtes n'est pas prédictible ou uniformément dispersée.
On peut avoir un premier aperçu des enjeux liés à ce service à travers un benchmark, réalisé vers 2019 - il y a 5 ans à l'écriture de ce billet - par [un dévelopeur](https://gist.github.com/DarthSim) d'une de ces solutions, et intitulé [imgproxy vs alternatives benchmark](https://gist.github.com/DarthSim/9d971d2859f3714a29cf8ce094b3fc55). Le test consiste à redimensionner une image JPEG de 29Mo pour une résolution de 7360x4912 (typiquement une photo prise par un appareil photo réflexe) vers une résolution de 500x500, toujours en JPEG. Le benchmark semble être configuré avec 4 requêtes en parallèle. imgproxy, thumbor, et imaginary se démarquent particulièrement des autres logiciels par leurs bonnes performances : environ 10 images par secondes, entre 200Mo et 400Mo de mémoire vive consommées, autour de 500ms de processing par image. On peut avoir un premier aperçu des enjeux liés à ce service à travers un benchmark, réalisé vers 2019 - il y a 5 ans à l'écriture de ce billet - par [un dévelopeur](https://gist.github.com/DarthSim) d'une de ces solutions, et intitulé [imgproxy vs alternatives benchmark](https://gist.github.com/DarthSim/9d971d2859f3714a29cf8ce094b3fc55). Le test consiste à redimensionner une image JPEG de 29Mo pour une résolution de 7360x4912 (typiquement une photo prise par un appareil photo réflexe) vers une résolution de 500x500, toujours en JPEG. Le benchmark semble être configuré avec 4 requêtes en parallèle. imgproxy, thumbor, et imaginary se démarquent particulièrement des autres logiciels par leurs bonnes performances : environ 10 images par secondes, entre 200Mo et 400Mo de mémoire vive consommées, autour de 500ms de processing par image.
@ -68,13 +68,13 @@ Une fois la charge absorbée, le service *recover* et repasse dans son mode norm
Ce *failure mode* doit forcément être très efficace, sinon il ne sert à rien. Ce *failure mode* doit forcément être très efficace, sinon il ne sert à rien.
On peut d'abord envisager un mode de fonctionnement très direct pour notre *failure mode* : envoyer un code d'erreur HTTP, comme le standard `503 service unavailable` ou le non-standard `529 service overloaded`. Plus ambitieux, on peut envoyer une image placeholder à la place, sans directive de cache bien entendu, ce qui permettrait de donner une indication visuelle plus claire aux internautes, et potentiellement de moins casser le site web. On peut d'abord envisager un mode de fonctionnement très direct pour notre *failure mode* : envoyer un code d'erreur HTTP, comme le standard `503 service unavailable` ou le non-standard `529 service overloaded`. Plus ambitieux, on peut envoyer une image placeholder à la place, sans directive de cache bien entendu, ce qui permettrait de donner une indication visuelle plus claire aux internautes, et potentiellement de moins casser le site web.
Cette image placeholder serait pré-calculée au démarrage du service pour tous les formats supportés (JPEG, HEIC, etc.) et stockée en mémoire vive. Se pose encore la question de la taille : si on envoie une taille différente de celle attenduee, on peut "casser" le rendu du site. À contrario, générer une image à la bonne taille à la volée demande des calculs, bien que si on complète avec une couleur uniforme, ces calculs puissent possiblement être triviaux en fonction du format considéré. Enfin, le problème majeur, c'est que les images sont intégrées de pleins de manières différentes à travers un site web, parfois mélangées avec des filtres : comment s'assurer que notre placeholder sera correctement reçu et compris ? Cette image placeholder serait pré-calculée au démarrage du service pour tous les formats supportés (JPEG, HEIC, etc.) et stockée en mémoire vive. Se pose encore la question de la taille : si on envoie une taille différente de celle attendue, on peut "casser" le rendu du site. À contrario, générer une image à la bonne taille à la volée demande des calculs, bien que si on complète avec une couleur uniforme, ces calculs puissent possiblement être triviaux en fonction du format considéré. Enfin, le problème majeur, c'est que les images sont intégrées de pleins de manières différentes à travers un site web, parfois mélangées avec des filtres : comment s'assurer que notre placeholder sera correctement reçu et compris ?
*Dans le cadre du développement d'une première itération, la solution des codes d'erreur semble de loin être la meilleure.* *Dans le cadre du développement d'une première itération, la solution des codes d'erreur semble de loin être la meilleure.*
## Files d'attente ## Files d'attente
Reste maintenant à définir comment on bascule dans ce *failure mode*. Et pour se faire, on va partir de conceptions single-thread et multi-thread naïves pour comprendre comment elles échouent. En single-thread, lorsque plusieurs requêtes seront reçuees, elles vont s'accumuler soit dans le noyau, soit dans le runtime (eg. nodejs) et une seule sera processée (car on suppose un processus CPU bound sans IO). Les requêtes vont donc s'accumuler, quelques unes vont être process, mais la plupart vont timeout. En multi-thread, on va progresser sur la conversion de plusieurs requêtes en parallèle mais très lentement à chaque fois, au point qu'on va aussi timeout probablement. Dans le cas du multi-thread, on risque aussi d'épuiser les ressources du serveur. Reste maintenant à définir comment on bascule dans ce *failure mode*. Et pour se faire, on va partir de conceptions single-thread et multi-thread naïves pour comprendre comment elles échouent. En single-thread, lorsque plusieurs requêtes seront reçues, elles vont s'accumuler soit dans le noyau, soit dans le runtime (eg. nodejs) et une seule sera processée (car on suppose un processus CPU bound sans IO). Les requêtes vont donc s'accumuler, quelques unes vont être process, mais la plupart vont timeout. En multi-thread, on va progresser sur la conversion de plusieurs requêtes en parallèle mais très lentement à chaque fois, au point qu'on va aussi timeout probablement. Dans le cas du multi-thread, on risque aussi d'épuiser les ressources du serveur.
À la place, on va placer les traitements d'image dans un ou plusieurs fils dédiés mais toujours un nombre inférieur à notre nombre de CPU, pour garder un serveur réactif. Lorsqu'on veut réaliser un taitement, on place notre requête dans une file d'attente. Lorsqu'un fil a fini son traitement, il prend un nouveau *job* dans cette file d'attente. Cette file d'attente est bornée, elle peut donc être pleine, auquel cas on passe dans le *failure mode* tant qu'elle ne s'est pas vidée. Ici, on a formulé notre problème selon [un modèle académique](https://fr.wikipedia.org/wiki/Th%C3%A9orie_des_files_d%27attente) bien connu, et surlequel on peut envisager itérer. À la place, on va placer les traitements d'image dans un ou plusieurs fils dédiés mais toujours un nombre inférieur à notre nombre de CPU, pour garder un serveur réactif. Lorsqu'on veut réaliser un taitement, on place notre requête dans une file d'attente. Lorsqu'un fil a fini son traitement, il prend un nouveau *job* dans cette file d'attente. Cette file d'attente est bornée, elle peut donc être pleine, auquel cas on passe dans le *failure mode* tant qu'elle ne s'est pas vidée. Ici, on a formulé notre problème selon [un modèle académique](https://fr.wikipedia.org/wiki/Th%C3%A9orie_des_files_d%27attente) bien connu, et surlequel on peut envisager itérer.
@ -90,13 +90,13 @@ Idéalement, le cache serait imputé par utilisateur-ice, directement dans leur
*Si on pourrait être tenté dans une première itération de ne pas utiliser S3 pour le cache mais le filesystem ou la mémoire vive, je pense que c'est une erreur. Si le CDN se reschedule sur un autre noeud, on perd le cache, et on risque de passer trop souvent dans le failure mode inutilement, créant du désagrément et de l'incompréhension pour rien auprès des utilisateur-ices.* *Si on pourrait être tenté dans une première itération de ne pas utiliser S3 pour le cache mais le filesystem ou la mémoire vive, je pense que c'est une erreur. Si le CDN se reschedule sur un autre noeud, on perd le cache, et on risque de passer trop souvent dans le failure mode inutilement, créant du désagrément et de l'incompréhension pour rien auprès des utilisateur-ices.*
*On peut aussi être tenté d'utiliser des outils de caching existants plutôt que de ré-implémenter notre propre politique de cache. D'abord ça n'est pas évident que ce soit possible dans notre cas. Ensuite les outils de caching ne sont pas prévus pour notre cas d'usage où on a besoin de stocker dans S3. Enfin, ça nous rendrait impossible l'implémentation ultérieure de l'imputation du stockage à l'utilisateur final.* *On peut aussi être tenté d'utiliser des outils de caching existants plutôt que de ré-implémenter notre propre politique de cache. D'abord ça n'est pas évident que ce soit possible dans notre cas d'usage où on a besoin de stocker dans S3. Ensuite, ça nous rendrait impossible l'implémentation ultérieure de l'imputation du stockage à l'utilisateur final.*
## Conclusion ## Conclusion
Dans ce billet de blog, on a vu que la conversion et redimensionnement des images à la volée consommait beaucoup de ressources CPU & RAM. Dans ce billet de blog, on a vu que la conversion et redimensionnement des images à la volée consommait beaucoup de ressources CPU & RAM.
De ce fait, c'est un défi à mettre en oeuvre dans un environnement contraint en ressources (computing within limits). De ce fait, c'est un défi à mettre en oeuvre dans un environnement contraint en ressources (computing within limits).
En s'autorisant un *failure mode*, on peut cependant s'assurer d'une certaine résilience du système face à des pics de charge trop importants, et donc assurer la viabilité d'un tel service. La théorie des fil d'attentes et l'exemple de CoDel est un exemple de comment & quand basculer entre le *normal mode* et le *failure mode*. En s'autorisant un *failure mode*, on peut cependant s'assurer d'une certaine résilience du système face à des pics de charge trop importants, et donc assurer la viabilité d'un tel service. La théorie des fil d'attentes et CoDel sont un exemple de comment & quand basculer entre le *normal mode* et le *failure mode*.
Enfin, un système de cache bien conçu permettrait une réduction significative de l'utilisation CPU+RAM pour un coût supplémentaire en stockage modique. Enfin, un système de cache bien conçu permettrait une réduction significative de l'utilisation CPU+RAM pour un coût supplémentaire en stockage modique.
Idéalement, le coût supplémentaire en stockage serait imputé à l'utilisateur ; on peut aussi envisager utiliser le cache pour un traitement asynchrone des images, comme la génération d'un grand nombre de miniatures qui ne peut pas être fait de manière synchrone en environnement contraint. Idéalement, le coût supplémentaire en stockage serait imputé à l'utilisateur ; on peut aussi envisager utiliser le cache pour un traitement asynchrone des images, comme la génération d'un grand nombre de miniatures qui ne peut pas être fait de manière synchrone en environnement contraint.