- Nanit utilisait AWS S3 dans son pipeline de traitement vidéo pour l’analyse du sommeil des bébés, mais avec des milliers d’uploads par seconde, le coût des requêtes PutObject représentait l’essentiel de la dépense totale
- De plus, la limite minimale de conservation d’un jour des règles S3 Lifecycle imposait de payer 24 heures de stockage pour des vidéos en réalité traitées en moins de 2 secondes
- Pour résoudre ce problème, l’équipe a construit N3, un système de stockage en mémoire basé sur Rust, et n’a conservé S3 que comme tampon de débordement
- N3 reste totalement compatible avec le pipeline de traitement existant via SQS FIFO, tout en maintenant un ordre strict et la fiabilité
- Résultat : environ 500 000 dollars d’économies par an, avec une architecture à la fois simple et stable
Contexte
Aperçu du pipeline de traitement vidéo
- Les caméras de Nanit enregistrent des morceaux de vidéo, demandent une URL présignée S3 au Camera Service, puis uploadent directement vers S3
- Une fonction AWS Lambda publie la clé de l’objet dans une file SQS FIFO (partitionnée par baby_uid), puis des pods de traitement vidéo consomment depuis SQS, téléchargent depuis S3 et exécutent l’inférence de l’état de sommeil
- Avantages de cette configuration
- L’atterrissage dans S3 + la mise en file SQS découplent l’upload des caméras du traitement vidéo, ce qui évite toute perte de vidéo pendant la maintenance ou des interruptions temporaires
- Pas besoin de gérer soi-même la disponibilité et la durabilité grâce à S3
- SQS FIFO + group ID préservent l’ordre par bébé, tout en permettant aux nœuds de traitement de rester majoritairement sans état
- Les règles S3 Lifecycle assurent la collecte des déchets, sans avoir à suivre les vidéos déjà traitées
Pourquoi un changement était nécessaire
- Le coût de PutObject dominait : les vidéos sont des objets à très courte durée de vie qui ne restent que quelques secondes dans la zone d’atterrissage avant traitement ; à l’échelle de milliers d’uploads par seconde, le coût par requête d’objet devenait le principal facteur de coût
- Augmenter la fréquence de découpage (envoyer davantage de petits segments) réduit la latence, mais chaque segment supplémentaire ajoute une requête PutObject, donc le coût augmente linéairement
- Le stockage était une seconde taxe : même si le traitement se terminait en environ 2 secondes, les règles Lifecycle de suppression facturaient tout de même environ 24 heures de stockage
- Il fallait donc une architecture capable de préserver la fiabilité et l’ordre strict tout en évitant le coût par objet sur le chemin nominal et en minimisant le stockage pour lequel on paie en attente
Plan
-
Principes de conception
- Simplicité par l’architecture : éliminer la complexité au niveau du design, plutôt que par une implémentation astucieuse
- Exactitude : un remplacement complet et transparent pour le reste du pipeline
- Optimisation du chemin nominal : concevoir pour le cas général et utiliser S3 comme filet de sécurité pour les cas limites ; les algorithmes de traitement tolérant des trous occasionnels, la simplicité a été privilégiée plutôt qu’un empilement de garanties complexes
-
Moteurs de conception
- Objets à courte durée de vie : les segments ne restent dans la zone d’atterrissage que quelques secondes
- Ordre : séquençage strict par bébé (ne pas traiter les plus récents avant les plus anciens)
- Débit : des milliers d’uploads par seconde, de 2 à 6 Mo par segment
- Contraintes client : les caméras ont un nombre limité de tentatives, impossible de supposer une retransmission
- Exploitation : il faut tolérer un backlog de plusieurs millions d’éléments pendant la maintenance ou les montées en charge
- Aucun changement firmware : la solution doit fonctionner avec les caméras existantes
- Tolérance à la perte : de très petites coupures sont acceptables, les algorithmes les masquent
- Coût : éviter le coût S3 par objet sur le chemin nominal, et minimiser le stockage pour lequel on paie en attente
Vue d’ensemble de la conception (chemin nominal N3 + débordement S3)
-
Architecture
- N3 est une zone d’atterrissage sur mesure qui conserve les vidéos en mémoire uniquement pendant le temps nécessaire au drainage du traitement (environ 2 secondes), S3 n’étant utilisé que lorsque N3 ne peut pas absorber la charge
- Deux composants
- N3-Proxy (sans état, double interface)
- Externe (connecté à Internet) : accepte les uploads des caméras via des URL présignées
- Interne (privé) : émet des URL présignées au Camera Service
- N3-Storage (avec état, interne uniquement) : stocke les segments uploadés en RAM et les met en file dans SQS avec des URL de téléchargement adressables par pod
- Les pods de traitement vidéo consomment depuis SQS FIFO et téléchargent depuis le stockage pointé par l’URL (N3 ou S3)
-
Flux nominal (Happy Path)
- La caméra demande une URL d’upload au Camera Service
- Le Camera Service demande une URL présignée à l’API interne de N3-Proxy
- La caméra upload la vidéo vers le point de terminaison externe de N3-Proxy
- N3-Proxy transmet à N3-Storage
- N3-Storage conserve la vidéo en mémoire et la met en file dans SQS avec une URL de téléchargement pointant vers lui-même
- Le pod de traitement télécharge depuis N3-Storage puis traite
-
Repli à deux niveaux
- Niveau 1 : repli au niveau du proxy (par requête)
- Si N3-Storage ne peut pas accepter un upload à cause d’une pression mémoire, d’un backlog de traitement, d’une panne de pod, etc., N3-Proxy upload vers S3 à la place de la caméra
- La caméra a déjà reçu une URL N3 avant que la panne ne soit détectée
- Niveau 2 : reroutage au niveau du cluster (tout le trafic)
- Si N3-Proxy ou N3-Storage est en mauvais état, le Camera Service cesse d’émettre des URL N3 et renvoie directement des URL présignées S3
- Tout le trafic bascule alors vers S3 jusqu’au rétablissement de N3
-
Pourquoi séparer en deux composants
- Rayon d’impact des pannes : si le stockage tombe, le proxy peut toujours router vers S3 ; si le proxy tombe, seul le trafic de ce nœud est affecté et le cluster de stockage reste intact
- Profils de ressources : le proxy est gourmand en CPU/réseau (terminaison TLS), le stockage en mémoire (rétention des vidéos), avec des types d’instances et des besoins de scaling différents
- Sécurité : le stockage n’est jamais exposé à Internet
- Sécurité des déploiements : mettre à jour le proxy (sans état) ne touche pas le stockage (qui contient des données actives)
Validation de la conception
-
Ce qu’il fallait valider
- Capacité et dimensionnement : la durée réelle des uploads sur le réseau des clients, les ressources de calcul nécessaires et la taille du buffer d’upload
- Modèle de stockage : savoir si tout pouvait tenir en RAM ou s’il fallait du disque
- Résilience : comment faire du load balancing à bas coût et gérer les nœuds défaillants
- Politiques d’exploitation : besoins en GC, attentes de retry, et savoir si la suppression au GET suffisait
- Inconnues : quels cas limites apparaîtraient une fois l’idée confrontée à la réalité
-
Approche 1 : test de charge synthétique
- Construction d’un générateur de charge poussant le système dans ses retranchements avec différents niveaux de concurrence, des clients lents, une charge continue et des interruptions de traitement
- Objectif : trouver les limites, identifier les goulots d’étranglement inattendus et obtenir une base de référence déterministe pour la planification de capacité
-
Approche 2 : PoC en production (mode miroir)
- Les tests synthétiques ne peuvent pas reproduire le comportement réel des caméras : Wi-Fi instable, versions de firmware variées, conditions réseau imprévisibles
- En mode miroir, n3-proxy écrit d’abord dans S3 (conservation de production), puis aussi dans le N3-Storage du PoC (connecté à un SQS canari et à un processeur vidéo)
- Cohorte ciblée : par version de firmware / liste de Baby-UID
- Parité des données : comparaison des états de sommeil entre le PoC et la production, puis investigation des écarts
- Observabilité : tableaux de bord par chemin (N3 vs S3), profondeur de file, latence/RPS, budget d’erreur, analyse de l’egress
- Les feature flags (avec Unleash) ont été essentiels : basculer une cohorte en temps réel sans déploiement, tester des tranches étroites (anciens firmwares, caméras au Wi-Fi faible), puis restaurer immédiatement en cas de problème
-
Ce qui a été découvert
- Goulots d’étranglement : la terminaison TLS consommait l’essentiel du CPU, et le réseau bursting AWS se mettait à throttler après épuisement des crédits
- Le stockage purement mémoire est viable : la distribution réelle des temps d’upload et la concurrence ont confirmé que le working set pouvait tenir en RAM avec une marge de sécurité, sans disque
- Surcharge des timestamps TCP : environ 85 % des octets transmis étaient des trames ACK ; désactiver les timestamps TCP (
sysctl -w net.ipv4.tcp_timestamps=0) a permis d’économiser 12 octets par ACK
- Risque : avec un grand nombre d’octets sur une même socket, les numéros de séquence peuvent boucler et des paquets retardés peuvent être fusionnés à tort, provoquant une corruption
- Atténuation : (1) une nouvelle socket par upload, (2) recyclage des sockets n3-proxy ↔ n3-storage après environ 1 Go transféré
- Fuite mémoire : après le lancement initial, la mémoire de n3-proxy augmentait régulièrement
- Le profilage
jemalloc a montré une croissance dans les buffers BytesMut de hyper par connexion
- Certaines connexions client se figeaient pendant le transfert sans être nettoyées, laissant les buffers en mémoire
- Correction : raccourcir la durée de vie des sockets et imposer des limites de temps
- Keep-alive désactivé : fermeture immédiate de la connexion après chaque upload
- Timeouts renforcés : définition de timeouts d’en-tête/socket pour fermer les uploads figés et libérer les buffers
Stockage
-
Stockage en mémoire
- Départ par la voie la plus simple : stockage en mémoire pour éviter le tuning I/O et utiliser des structures de données intuitives
- Les vidéos sont stockées dans
Arc<DashMap<Ulid, Bytes>> ; chaque upload augmente bytes_used, chaque téléchargement supprime la vidéo et décrémente ce compteur
- À partir d’environ 80 % de la capacité, les uploads commencent à être refusés pour éviter un OOM, avec signal à n3-proxy pour qu’il cesse de signer des URL d’upload
- Une poignée
control permet de mettre manuellement en pause les uploads et la garbage collection
-
Redémarrage gracieux
- Comme le stockage est uniquement en mémoire, il faut éviter de perdre les données en cours lors d’un redémarrage
- Processus de redémarrage gracieux
- Le pod reçoit
SIGTERM (rolling update du StatefulSet un par un)
- Le pod passe en Not Ready et sort du Service (plus aucun nouvel upload)
- Il continue à servir les téléchargements des vidéos déjà uploadées
- Quand les téléchargements s’arrêtent (aucune lecture récente → drainage du traitement)
- Il attend la fin des requêtes ouvertes
- Puis redémarre, avant de passer au pod suivant
- En fonctionnement normal, un pod se vide en quelques secondes
-
GC
- Deux mécanismes de nettoyage sont utilisés
- Suppression au téléchargement : la vidéo est supprimée juste après téléchargement ; le PoC a confirmé zéro retéléchargement, et comme le processeur vidéo gère lui-même les retries, il n’est pas nécessaire de conserver les données ni de suivre un état « traité »
- GC TTL pour les retardataires : la suppression au téléchargement ne couvre pas les segments ignorés par le processeur (non téléchargés donc non supprimés)
- Ajout d’une GC TTL légère : scan périodique de la DashMap en mémoire et suppression des entrées plus anciennes qu’un seuil configurable (par ex. quelques heures)
- Mode maintenance : lors d’une interruption planifiée du traitement, la GC peut être mise en pause via un contrôle interne, pour éviter la suppression des vidéos tant que la consommation est à l’arrêt
Conclusion
-
Principaux résultats
- En utilisant S3 comme tampon de repli et N3 comme zone d’atterrissage principale, l’équipe a obtenu environ 500 000 dollars d’économies annuelles, tout en gardant un système simple et fiable
- Insight clé : la plupart des décisions « construire vs acheter » se concentrent sur les fonctionnalités, mais à l’échelle, l’économie change le calcul
- Pour des objets à courte durée de vie (environ 2 secondes en fonctionnement normal), ni réplication ni durabilité sophistiquée ne sont nécessaires ; un simple stockage en mémoire suffit
- Quand le traitement ralentit ou que la maintenance allonge la durée de vie des objets, les garanties de fiabilité de S3 redeviennent nécessaires
- Le meilleur des deux mondes : N3 traite efficacement le chemin nominal, S3 apporte la durabilité quand les objets doivent vivre plus longtemps
- Si N3 rencontre un problème (pression mémoire, crash de pod, souci de cluster), les uploads basculent proprement vers S3
-
Facteurs de réussite
- Définir clairement le problème en amont : contraintes, hypothèses et frontières ont empêché l’élargissement du périmètre
- Validation précoce avec un PoC en mode miroir : découverte des goulots d’étranglement (TLS, throttling réseau) et validation des hypothèses avant engagement
- Évite la sur-ingénierie et les retours en arrière
-
Quand faut-il construire ce type de système ?
- Il faut envisager une infrastructure sur mesure lorsqu’une échelle suffisante permet une réduction significative des coûts et que des contraintes spécifiques rendent possible une solution simple
- L’effort d’ingénierie nécessaire pour construire et maintenir le système doit être inférieur au coût d’infrastructure éliminé
- Dans le cas de Nanit, des besoins spécifiques (stockage temporaire, tolérance à la perte, repli S3) ont permis de construire quelque chose d’assez simple pour garder un faible coût de maintenance
- Si ces deux conditions ne sont pas réunies, mieux vaut rester sur un service managé
- Le referaient-ils ? Oui : le système tourne de manière stable en production, et la conception avec repli permet d’éviter la complexité sans sacrifier la fiabilité
3 commentaires
Je me demande s’il n’aurait pas suffi de laisser les instances EC2 ou les pods EKS recevoir directement les vidéos en upload puis les traiter.
S’ils sont allés jusqu’à créer un proxy, j’ai l’impression qu’un autoscaling EKS basé sur la charge des pods aurait aussi été tout à fait possible.
En général, le traitement vidéo n’a pas besoin de charger le fichier entier en mémoire ; j’ai l’impression qu’en créant simplement des fichiers temporaires sur le SSD local de chaque instance pour les traiter, ils n’auraient peut-être même pas eu besoin d’un repli sur S3.
On dirait un exemple de mauvaise utilisation du serverless et de S3.
Mais la solution semble encore plus étrange.
Avis Hacker News
Article vraiment instructif. J’apprécie beaucoup qu’on partage ce type de démarche technique
Même sans rencontrer exactement le même problème soi-même, il y a beaucoup à apprendre rien qu’en voyant la manière de raisonner
Franchement, ça aurait sans doute été bien plus propre s’ils n’avaient pas utilisé le serverless dès le départ
On a l’impression qu’ils ont essayé de faire entrer de force des données de quelques secondes dans le paradigme serverless d’AWS, ce qui a créé des coûts et une complexité inutiles
Cela dit, passer à une solution en mémoire était un bon choix
Ils disent que le handshake TLS consomme beaucoup de CPU, mais ça ne semble pas être le principal goulot d’étranglement
Cela dit, c’était intéressant de voir une conception système adaptée à ce workflow
En réalité, ce n’était pas vraiment « implémenter S3 soi-même » comme le titre le laisse entendre, mais plutôt une architecture avec un cache mémoire devant S3
C’est élégant, mais ce n’est pas un remplacement complet de S3
Quel que soit le titre, c’était un projet intéressant
Pour parler à la manière de HN, j’ai surtout envie de parler de l’entreprise Nanit elle-même
Nanit exploite des caméras de surveillance pour bébés basées sur le cloud. Toute la vidéo et l’audio sont envoyés sans E2EE
Le matériel est cher, presque inutilisable sans abonnement, et il faut en plus acheter un support à 200 $ pour débloquer le suivi du sommeil
C’est dommage que ce modèle renforce au final une dépendance au cloud
Cela dit, réduire la dépendance à S3 et passer à un stockage maison, comme dans cet article, est une bonne chose
Les autres produits avaient des applis peu fiables. Une solution local-first + E2EE serait bien, mais en pratique c’est surtout l’utilisabilité qui comptait
Si on veut une vraie E2EE, il faut faire l’analyse en local et n’envoyer que les résultats
Cet article donnait un peu l’impression de se féliciter d’avoir résolu un problème qu’ils avaient eux-mêmes créé
Ils auraient mieux fait de vendre dès le départ du matériel avec stockage local : cela aurait été plus simple et moins cher
Une conception centrée sur le cloud ressemble désormais à une approche de 2015
L’article était excellent, mais j’aurais aussi aimé connaître les économies réalisées en implémentant le « delete on read » sur S3
Si S3 facture à la seconde, les gains ont peut-être été importants
Cette solution ressemble d’ailleurs beaucoup à l’option « reduced redundancy » de S3
Ils parlent de 500 000 $ d’économies, mais on ne connaît pas le coût total
Ce n’est pas la même chose s’il s’agit de 500 001 $ sur 500 000 $ ou de 500 000 $ sur 55 millions de dollars
On a l’impression qu’ils ont d’abord choisi une mauvaise architecture, puis essayé de la rattraper avec du cache
Il n’y a aucune raison d’envoyer sur S3 des vidéos de 2 secondes en moyenne, à part pour faire du stockage redondant
S’ils avaient simplement tout traité directement sur le serveur, ils auraient pu éliminer S3, SQS et Lambda
Je ne comprends pas pourquoi ils ont autant compliqué un problème aussi simple
Ça ressemble à une leçon classique : concentrez-vous sur le développement de l’app et simplifiez l’infrastructure
Il aurait sans doute été plus judicieux de mettre directement le cache dans les serveurs de traitement vidéo
Un titre comme « Nous avons mal utilisé S3 » aurait sans doute été plus exact
Ils ont fini par construire leur propre magasin mémoire, alors qu’un outil comme Redis aurait probablement suffi
Si leur système maison tombe en panne, les vidéos disparaissent-elles ?
Il aurait été bien plus logique d’envoyer ça vers Kinesis ou SQS dès le départ