- Nanit nutzte AWS S3 in einer Videobearbeitungs-Pipeline zur Analyse des Schlafzustands von Babys, doch bei Tausenden Uploads pro Sekunde machten die PutObject-Anfragekosten den Großteil der Gesamtkosten aus
- Außerdem führte die Mindestaufbewahrungsdauer von 1 Tag bei S3-Lifecycle-Regeln dazu, dass für Videos, die tatsächlich innerhalb von 2 Sekunden verarbeitet wurden, 24 Stunden Speichergebühren anfielen
- Um das zu lösen, baute das Team das Rust-basierte In-Memory-Storage-System N3, während S3 nur noch als Overflow-Puffer genutzt wird
- N3 ist über SQS FIFO vollständig mit der bestehenden Verarbeitungspipeline kompatibel und bewahrt strikte Reihenfolgegarantien sowie Zuverlässigkeit
- Das Ergebnis waren etwa 500.000 US-Dollar Kosteneinsparung pro Jahr sowie eine einfache und stabile Architektur
Hintergrund
Überblick über die Videobearbeitungs-Pipeline
- Die Kameras von Nanit zeichnen Video-Chunks auf, fordern beim Camera Service eine S3 presigned URL an und laden dann direkt in S3 hoch
- AWS Lambda veröffentlicht den Objektschlüssel in einer SQS-FIFO-Queue (Shardierung nach baby_uid), und Videoverarbeitungs-Pods konsumieren aus SQS, laden die Daten aus S3 herunter und führen anschließend die Schlafzustands-Inferenz aus
- Vorteile dieses Setups
- S3 als Landepunkt plus SQS-Queueing entkoppelt Kamera-Uploads von der Videoverarbeitung und verhindert Videoverluste selbst bei Wartung oder vorübergehenden Ausfällen
- Verfügbarkeit und Dauerhaftigkeit müssen dank S3 nicht selbst verwaltet werden
- Mit SQS FIFO + Group ID bleibt die Reihenfolge pro Baby erhalten, und die Verarbeitungs-Nodes können weitgehend zustandslos bleiben
- S3-Lifecycle-Regeln übernehmen die Garbage Collection, sodass verarbeitete Videos nicht nachverfolgt werden müssen
Warum eine Änderung nötig war
- PutObject-Kosten dominierten: Die Videos sind kurzlebige Objekte, die nur einige Sekunden im Landing-Bereich liegen, aber bei Tausenden Uploads pro Sekunde wurden die Anfragekosten pro Objekt zum größten Kostentreiber
- Wenn man zur Reduzierung der Latenz die Chunking-Frequenz erhöht (also mehr kleinere Chunks sendet), steigt der Preis linear, weil jeder zusätzliche Chunk eine weitere PutObject-Anfrage verursacht
- Speicher wurde doppelt berechnet: Obwohl die Verarbeitung in etwa 2 Sekunden abgeschlossen war, führten Lifecycle-Löschregeln dennoch zu rund 24 Stunden Speichergebühren
- Gesucht war ein Design, das Zuverlässigkeit und strikte Reihenfolgegarantien beibehält, dabei aber die Kosten pro Objekt im Normalfall vermeidet und kostenpflichtigen Warte-Speicher minimiert
Plan
-
Designprinzipien
- Einfachheit durch Architektur: Komplexität nicht durch clevere Implementierung, sondern auf Design-Ebene eliminieren
- Korrektheit: Ein vollständig transparenter Ersatz für den Rest der Pipeline
- Optimierung für den Normalfall: Für den häufigsten Fall entwerfen und S3 für Edge Cases als Sicherheitsnetz verwenden; da der Verarbeitungsalgorithmus gegen kleine Lücken robust ist, hatte Einfachheit Vorrang vor komplexen Garantien
-
Treiber des Designs
- Kurzlebige Objekte: Segmente existieren im Landing-Bereich nur wenige Sekunden
- Reihenfolge: Strikte Sequenzierung pro Baby (nicht zuerst das Neueste verarbeiten)
- Durchsatz: Tausende Uploads pro Sekunde, 2–6 MB pro Segment
- Client-Einschränkungen: Die Kameras haben begrenzte Retry-Anzahlen, erneutes Senden kann nicht vorausgesetzt werden
- Betrieb: Auch bei Wartung oder Scale-up muss ein Backlog von Millionen Einträgen tolerierbar sein
- Keine Firmware-Änderungen: Muss mit bestehenden Kameras funktionieren
- Verlusttoleranz: Sehr kleine Lücken sind akzeptabel und werden vom Algorithmus maskiert
- Kosten: S3-Kosten pro Objekt im Normalfall vermeiden und kostenpflichtigen Warte-Speicher minimieren
Überblick über das Design (N3 im Normalfall + S3-Overflow)
-
Architektur
- N3 ist ein maßgeschneiderter Landing-Bereich, der Videos nur so lange im Speicher hält, wie die Verarbeitung zum Leeren braucht (etwa 2 Sekunden), und S3 nur dann verwendet, wenn N3 die Last nicht tragen kann
- Zwei Komponenten
- N3-Proxy (zustandslos, Dual-Interface)
- extern (internetverbunden): nimmt Kamera-Uploads über presigned URLs an
- intern (privat): stellt dem Camera Service presigned URLs aus
- N3-Storage (zustandsbehaftet, nur intern): speichert hochgeladene Segmente im RAM und stellt sie mit Pod-adressierbaren Download-URLs in SQS ein
- Videoverarbeitungs-Pods konsumieren aus SQS FIFO und laden aus dem Speicher herunter, auf den die URL zeigt (N3 oder S3)
-
Normaler Ablauf (Happy Path)
- Die Kamera fordert beim Camera Service eine Upload-URL an
- Der Camera Service fordert über die interne API von N3-Proxy eine presigned URL an
- Die Kamera lädt das Video an den externen Endpoint von N3-Proxy hoch
- N3-Proxy leitet es an N3-Storage weiter
- N3-Storage hält das Video im Speicher und stellt es mit einer auf sich selbst zeigenden Download-URL in SQS ein
- Der Verarbeitungs-Pod lädt es von N3-Storage herunter und verarbeitet es
-
Zweistufiges Fallback
- Tier 1: Fallback auf Proxy-Ebene (pro Anfrage)
- Wenn N3-Storage einen Upload nicht annehmen kann – etwa wegen Speicherdruck, Verarbeitungs-Backlog oder Pod-Ausfall –, lädt N3-Proxy stellvertretend für die Kamera in S3 hoch
- Die Kamera hat die N3-URL bereits erhalten, bevor der Fehler erkannt wird
- Tier 2: Cluster-weites Rerouting (gesamter Traffic)
- Wenn N3-Proxy oder N3-Storage ungesund sind, stellt der Camera Service keine N3-URLs mehr aus und gibt direkt S3-presigned-URLs zurück
- Bis zur Wiederherstellung von N3 fließt der gesamte Traffic über S3
-
Warum in zwei Komponenten aufgeteilt
- Ausfallradius: Wenn der Storage abstürzt, kann der Proxy weiterhin nach S3 routen; wenn der Proxy abstürzt, ist nur der Traffic dieses Nodes betroffen, nicht der gesamte Storage-Cluster
- Ressourcenprofil: Der Proxy ist CPU-/netzwerkintensiv (TLS-Terminierung), der Storage speicherintensiv (Videohaltung), mit unterschiedlichen Instanztypen und Skalierungsanforderungen
- Sicherheit: Der Storage kommt niemals direkt mit dem Internet in Berührung
- Sichere Rollouts: Updates des Proxys (zustandslos) berühren den Storage mit aktiven Daten nicht
Validierung des Designs
-
Was validiert werden musste
- Kapazität und Sizing: Reale Upload-Dauern im Netz der Clients, benötigte Rechenleistung und Größe des Upload-Puffers
- Storage-Modell: Ob alles im RAM gehalten werden kann oder Disk nötig ist
- Resilienz: Wie man günstig Load Balancing betreibt und fehlerhafte Nodes behandelt
- Betriebspolitik: Anforderungen an GC, erwartete Retries und ob Löschen bei GET ausreicht
- Unbekannte Unbekannte: Welche Edge Cases auftreten, wenn die Idee auf die Realität trifft
-
Ansatz 1: Synthetische Stresstests
- Es wurde ein Load-Generator gebaut, der das System mit unterschiedlicher Parallelität, langsamen Clients, Dauerlast und Verarbeitungs-Downtime an seine Grenzen bringt
- Ziel: Grenzwerte finden, unerwartete Bottlenecks aufdecken und eine deterministische Baseline für die Kapazitätsplanung schaffen
-
Ansatz 2: Produktions-PoC (Mirror Mode)
- Synthetische Tests konnten reales Kameraverhalten nicht nachbilden: instabiles Wi-Fi, verschiedene Firmware-Versionen, unvorhersehbare Netzbedingungen
- Mirror Mode: n3-proxy schreibt zunächst nach S3 (für die Produktionsaufbewahrung) und zusätzlich auch in das PoC-N3-Storage (angebunden an Canary-SQS und den Videoprozessor)
- Zielkohorten: nach Firmware-Versionen / Baby-UID-Listen
- Datenparität: Vergleich des Schlafzustands zwischen PoC und Produktion, Untersuchung von Abweichungen
- Observability: Dashboards nach Pfad (N3 vs. S3), Queue-Tiefe, Latenz/RPS, Error Budget, Egress-Analyse
- Feature Flags (mit Unleash) waren entscheidend: Kohorten konnten ohne Deployment in Echtzeit umgeschaltet werden, erst schmale Teilmengen (ältere Firmware, Kameras mit schwachem Wi-Fi) testen und bei Problemen sofort zurückrollen
-
Erkenntnisse
- Bottlenecks: TLS-Terminierung verbrauchte den Großteil der CPU; AWS Burstable Networking drosselte nach Verbrauch der Credits
- Reiner In-Memory-Storage ist praktikabel: Reale Verteilungen von Upload-Zeiten und Parallelität zeigten, dass sich das Working Set mit ausreichender Reserve im RAM halten lässt; Disk war nicht nötig
- TCP-Timestamp-Overhead: Rund 85 % der insgesamt übertragenen Bytes entfielen auf ACK-Frames; durch Deaktivieren der TCP-Timestamps (
sysctl -w net.ipv4.tcp_timestamps=0) wurden 12 Byte pro ACK eingespart
- Risiko: Bei hoher Bytezahl auf demselben Socket können Sequence Numbers umschlagen, wodurch verzögerte Pakete falsch zusammengeführt und Daten beschädigt werden könnten
- Gegenmaßnahmen: (1) neuer Socket pro Upload, (2) Wiederverwendung von Sockets zwischen n3-proxy und n3-storage nur bis etwa 1 GB Transfer
- Memory Leak: Nach dem ersten Launch stieg der Speicherverbrauch von n3-proxy kontinuierlich an
jemalloc-Profiling zeigte Wachstum in hyper-BytesMut-Puffern pro Verbindung
- Einige Client-Verbindungen stoppten während der Übertragung und wurden nicht bereinigt, wodurch Puffer erhalten blieben und der Speicher weiter anwuchs
- Fix: Sockets kurzlebiger machen und harte Timeouts setzen
- Keep-alive deaktivieren: Jede Verbindung direkt nach Abschluss eines Uploads schließen
- Timeouts verschärfen: Header-/Socket-Timeouts beenden hängende Uploads und geben Puffer frei
Storage
-
In-Memory-Storage
- Das Team startete mit dem einfachsten Weg: In-Memory-Storage, um I/O-Tuning zu vermeiden und intuitive Datenstrukturen zu verwenden
- Videos werden in
Arc<DashMap<Ulid, Bytes>> gespeichert; jeder Video-Upload erhöht bytes_used, jeder Download löscht das Video und verringert den Wert wieder
- Ab etwa 80 % der Kapazität werden Uploads abgewiesen, um OOM zu vermeiden; gleichzeitig wird n3-proxy signalisiert, keine Upload-URLs mehr zu signieren
- Über ein
control-Handle lassen sich Uploads und Garbage Collection manuell pausieren
-
Graceful Restart
- Da der Storage nur im Speicher liegt, musste verhindert werden, dass bei Neustarts In-Flight-Daten verloren gehen
- Ablauf eines Graceful Restarts
- Das Pod erhält
SIGTERM (das StatefulSet rollt jeweils nur einen Pod gleichzeitig)
- Das Pod wird Not Ready und aus dem Service entfernt (keine neuen Uploads mehr)
- Downloads für bereits hochgeladene Videos werden weiterhin bedient
- Sobald Downloads zum Stillstand kommen (keine jüngsten Reads mehr → Verarbeitung ist leer gelaufen)
- wartet das Pod auf Abschluss offener Anfragen
- dann Neustart und Wechsel zum nächsten Pod
- Im Normalbetrieb leeren sich Pods innerhalb weniger Sekunden
-
GC
- Es kommen zwei Aufräummechanismen zum Einsatz
- Löschen beim Download: Das Video wird direkt nach dem Download gelöscht; im PoC wurde null Re-Downloads beobachtet, und da der Videoprozessor intern Retries ausführt, sind weder Datenhaltung noch Tracking eines „verarbeitet“-Status nötig
- TTL-GC für Nachzügler: Löschen beim Download deckt keine Segmente ab, die vom Prozessor übersprungen wurden (nicht heruntergeladen → nicht gelöscht)
- Daher wurde eine leichtgewichtige TTL-GC ergänzt: periodisches Scannen der In-Memory-DashMap und Entfernen von Einträgen, die älter als ein konfigurierbarer Schwellenwert sind (z. B. mehrere Stunden)
- Wartungsmodus: Bei geplanter Verarbeitungs-Downtime kann die GC über interne Steuerung pausiert werden, damit während eines Konsumstopps keine Videos gelöscht werden
Fazit
-
Wichtigste Ergebnisse
- Durch den Einsatz von S3 als Fallback-Puffer und N3 als primären Landing-Bereich wurden rund 500.000 US-Dollar pro Jahr eingespart, ohne die Architektur kompliziert oder unzuverlässig zu machen
- Zentrale Erkenntnis: Die meisten „Build vs. Buy“-Entscheidungen fokussieren sich auf Features, aber in großem Maßstab verändert die Ökonomie die Rechnung
- Für kurzlebige Objekte (im Normalfall etwa 2 Sekunden) sind Replikation oder ausgefeilte Dauerhaftigkeit unnötig; ein einfacher In-Memory-Store reicht aus
- Wenn sich die Verarbeitung verzögert oder Wartung die Lebensdauer der Objekte verlängert, werden die Zuverlässigkeitsgarantien von S3 gebraucht
- Das Beste aus beiden Welten: N3 behandelt den Normalfall effizient, S3 liefert Dauerhaftigkeit, wenn Objekte länger leben müssen
- Wenn es Probleme mit N3 gibt (Speicherdruck, Pod-Absturz, Cluster-Probleme), werden Uploads nahtlos nach S3 failovert
-
Erfolgsfaktoren
- Das Problem wurde vorab klar definiert: Einschränkungen, Annahmen und Grenzen verhinderten Scope Creep
- Frühe Validierung mit einem PoC im Mirror Mode: Bottlenecks (TLS, Network Throttling) wurden gefunden und Annahmen vor dem Commit überprüft
- Verhinderte Overengineering und spätere Rückschritte
-
Wann sollte man so etwas bauen?
- Maßgeschneiderte Infrastruktur lohnt sich, wenn bei ausreichender Größenordnung relevante Kosteneinsparungen möglich sind und zugleich spezifische Rahmenbedingungen eine einfache Lösung erlauben
- Der Engineering-Aufwand für Bau und Betrieb des Systems muss geringer sein als die eingesparten Infrastrukturkosten
- Bei Nanit machten die konkreten Anforderungen (temporärer Speicher, Verlusttoleranz, S3-Fallback) es möglich, etwas Einfaches zu bauen, dessen Wartungskosten niedrig bleiben
- Fehlt einer der beiden Faktoren, sollte man bei Managed Services bleiben
- Würden sie es wieder tun? Ja – das System läuft stabil in Produktion, und das Fallback-Design ermöglicht Komplexitätsvermeidung ohne Einbußen bei der Zuverlässigkeit
3 Kommentare
Ich frage mich, ob man nicht einfach EC2 oder EKS-Pods die Videos direkt hätte hochladen und verarbeiten lassen können.
Wenn man sogar einen Proxy gebaut hat, hätte EKS-Autoscaling je nach Pod-Last doch ebenfalls gut funktioniert, würde ich meinen.
Für die Videoverarbeitung muss man die Datei normalerweise nicht komplett in den Arbeitsspeicher laden; wenn man stattdessen temporäre Dateien auf der lokalen SSD jeder Instanz angelegt und verarbeitet hätte, wäre vermutlich auch kein S3-Fallback nötig gewesen.
Das wirkt wie ein Beispiel für den falschen Einsatz von Serverless und S3.
Allerdings scheint mir die Lösung noch merkwürdiger zu sein.
Hacker-News-Kommentare
Wirklich ein sehr lesenswerter Artikel. Ich finde es großartig, wenn solche technischen Herangehensweisen geteilt werden.
Selbst wenn ich nie genau dasselbe Problem habe, lernt man schon viel daraus, einfach zu sehen, mit welcher Denkweise daran herangegangen wurde.
Ehrlich gesagt wäre das vermutlich viel sauberer gewesen, wenn man von Anfang an kein Serverless verwendet hätte.
Es wirkt, als hätte man Daten mit einer Lebensdauer von nur wenigen Sekunden mit Gewalt in das AWS-Serverless-Paradigma gepresst und dadurch unnötige Kosten und Komplexität erzeugt.
Trotzdem war der Wechsel zu einer speicherbasierten Lösung eine gute Entscheidung.
Es hieß zwar, dass TLS-Handshakes viel CPU verbrauchen, aber ich glaube nicht, dass das der Hauptengpass ist.
Trotzdem fand ich diesen Versuch eines workflow-spezifischen Systemdesigns interessant.
Genau genommen war das, anders als der Titel suggeriert, nicht „eine eigene S3-Implementierung“, sondern eher eine Architektur mit einem In-Memory-Cache vor S3.
Cool ist es trotzdem, aber kein vollständiger selbst gebauter S3-Ersatz.
Unabhängig vom Titel war es ein interessantes Projekt.
Im typischen HN-Stil würde ich gern eher über die Firma Nanit selbst sprechen.
Nanit betreibt cloudbasierte Babyphone-Kameras. Alle Video- und Audiodaten werden ohne E2EE hochgeladen.
Die Hardware ist teuer, ohne Abo ist sie fast unbrauchbar, und man muss zusätzlich einen Ständer für 200 $ kaufen, damit die Schlaftracking-Funktion freigeschaltet wird.
Schade ist, dass so ein Modell am Ende genau diese Cloud-Abhängigkeit weiter verstärkt.
Trotzdem war es eine gute Entscheidung, wie in diesem Artikel beschrieben die S3-Abhängigkeit zu reduzieren und auf eigenen Speicher umzusteigen.
Andere Produkte hatten instabile Apps. Eine Local-first- und E2EE-Lösung wäre schön, aber in der Praxis war Benutzbarkeit wichtiger.
Wenn man wirklich E2EE will, müsste die Analyse lokal stattfinden und nur das Ergebnis hochgeladen werden.
Der Artikel wirkte ein wenig so, als hätte man erst selbst ein Problem geschaffen und sich dann für dessen Lösung gefeiert.
Hätte man stattdessen von Anfang an Hardware mit lokalem Speicher verkauft, wäre es einfacher und günstiger gewesen.
Ein cloudzentriertes Design wirkt inzwischen wie ein Ansatz aus dem Jahr 2015.
Der Artikel war gut, aber mich hätte auch interessiert, wie viel Kosteneinsparung es gebracht hätte,
delete on readdirekt auf S3 umzusetzen.Falls S3 sekundengenau abgerechnet würde, hätte die Ersparnis durchaus erheblich sein können.
Außerdem ähnelt diese Lösung im Grunde der S3-Option „reduced redundancy“.
Es heißt, es seien 500.000 $ eingespart worden, aber ohne die Gesamtkosten zu kennen, ist das schwer einzuordnen.
Ob das 500.001 $ von 500.000 $ oder 500.000 $ von 55 Millionen $ sind, macht einen großen Unterschied.
Das wirkt, als hätte man zuerst die falsche Architektur gewählt und dann mit einem Cache darübergestrichen.
Es gibt keinen wirklichen Grund, durchschnittlich 2 Sekunden lange Videos nach S3 hochzuladen, außer vielleicht für redundante Speicherung.
Hätte man sie einfach direkt auf dem Server verarbeitet, hätte man S3, SQS und Lambda komplett eliminieren können.
Ich verstehe nicht, warum man ein so einfaches Problem so kompliziert gemacht hat.
Das klingt wie die klassische Lektion: „Konzentriert euch auf die App-Entwicklung und haltet die Infrastruktur einfach.“
Vermutlich wäre es besser gewesen, den Cache direkt in die Videoverarbeitungsserver einzubauen.
Ein treffenderer Titel wäre vielleicht eher „Wir haben S3 falsch benutzt“ gewesen.
Am Ende hat man sich doch einen eigenen In-Memory-Store gebaut, obwohl etwas wie Redis wahrscheinlich gereicht hätte.
Wenn das selbst gebaute System ausfällt, verschwinden dann die Videos?
Hätte man von Anfang an Kinesis oder SQS verwendet, wäre das wahrscheinlich deutlich besser gewesen.