Dieser Beitrag wurde von Colin Weld und Connor Adams geschrieben. Lies ihn hier oder auf unserem Blog.
Bei Modal bauen wir unter anderem Sandboxes. In Sandboxes laufen Agents, und Agents fressen Software. Heute betreibt Modal Millionen von Sandboxes pro Tag, unterstützt bis zu fünfzigtausend gleichzeitige Sandboxes pro Kunde und deckt eine Vielzahl von Anwendungsfällen in großem Maßstab ab, von Reinforcement Learning bis zu Hintergrund-Agents.
Zunehmend benötigen unsere Nutzer immer mehr Sandboxes, die mit immer höheren Raten erstellt werden. Reinforcement Learning kann den Betrieb von Millionen gleichzeitiger Sandboxes erfordern und zu Beginn von Rollouts Bursts von Hunderttausenden von Sandboxes erzeugen. Auch Agents benötigen zunehmend massive Skalierung und hohe gleichzeitige Erstellungsraten, um Traffic-Spitzen zu bewältigen.
Unsere bestehende Sandbox-Plattform ist wirklich gut, aber sie war nicht für diese Größenordnungen ausgelegt; das gilt auch für keine andere existierende Lösung. Wir sind besessen von Skalierung und Leistung und wollen, dass unsere Infrastruktur das Wachstum von Agents beschleunigt, nicht bremst. Also gingen wir zurück ans Reißbrett.
In den letzten Monaten haben wir unsere Kern-Sandbox-Plattform von Grund auf neu aufgebaut – sowohl für Skalierung als auch Zuverlässigkeit. In unserem neuen System können Nutzer Millionen von Sandboxes gleichzeitig betreiben und Zehntausende von Sandboxes pro Sekunde erstellen. Wir haben alle zentralen Engpässe aus unserer Steuerungsebene entfernt, sodass es keine praktischen Skalierungsgrenzen gibt, und wir haben jeden Teil der Container-Scheduling- und -Startvorgänge optimiert, indem wir den Scheduling-Pfad auf eine Schicht von Load-Balancern vereinfacht haben, die Container direkt auf unserer Worker-Flotte erstellen.
Als Demonstration der Leistungsfähigkeit unserer Plattform haben wir eine Million Sandboxes gleichzeitig betrieben und alle 1 Million in weniger als einer Minute erstellt.

Beweis, dass wir viele Sandboxes betreiben können.
Warum die meisten Lösungen nicht skalieren
Der Betrieb von 1 Million Sandboxes stößt an die Grenzen jeder Container-Plattform, sowohl aufgrund der schieren Anzahl von Containern als auch, weil der Betrieb so vieler Sandboxes viele Zehntausend Rechenknoten erfordert. Es wird viele Operationen geben, die entweder O(Container), O(Knoten) oder beides sind, was dazu führt, dass traditionelle Container-Plattformen an Skalierungsgrenzen stoßen.
Für Kubernetes zum Beispiel:
- der Scheduling-Algorithmus ist im schlimmsten Fall O(n x p) für n Knoten und p Pods, und das Scheduling ist standardmäßig serialisiert.
- jeder Pod verursacht im Laufe seiner Lebensdauer mehrere Schreibvorgänge in etcd (dem zentralen dauerhaften Speicher von Kubernetes), was bei hohen Pod-Erstellungsraten oder hohem Pod-Churn zu ernsthaften Problemen führen kann, und etcd ist innerhalb eines Keyspace nicht nativ shardbar.
- jeder Knoten muss mindestens einmal pro Heartbeat-Intervall in etcd schreiben, um seine Lebendigkeit zu signalisieren, sodass die grundlegende etcd-Schreiblauf O(Knoten) ist, völlig unabhängig von der Pod-Erstellung.

Annäherung an den Kubernetes-Scheduling-Ablauf. Neue Pods werden vom API-Server in etcd (einem stark konsistenten dauerhaften Speicher) geschrieben. Der Kubernetes-Scheduler überwacht auf neue, nicht zugewiesene Pods und weist sie über einen Aufruf des API-Servers Knoten zu, der wiederum in etcd schreibt; nachdem dieser Schreibvorgang durchgeführt wurde, kann ein Knoten den Pod starten.
Kubernetes kann skaliert werden, aber es erfordert ernsthafte Arbeit. Um eine große Anzahl von Knoten zu betreiben, muss etcd im Allgemeinen umgeschrieben oder ersetzt werden. Die Unterstützung eines hohen Scheduling-Durchsatzes erfordert den Aufbau eines komplexen Scatter-Gather-Systems, um den Scheduling-Algorithmus zu parallelisieren, während gleichzeitig eine einzige Quelle der Wahrheit für den Pod-Zustand erhalten bleibt. Sharding und Parallelisierung sind standardmäßig nicht einfach, da Kubernetes auf starker Konsistenz als Rückgrat seines Designs beruht.
Modals ursprüngliche Sandbox-Architektur hat ähnliche Probleme. Wie Kubernetes verlassen wir uns im gesamten Backend auf starke Konsistenz, sodass das Erstellen und Planen von Sandboxes eine globale Koordination und O(Sandboxes) Schreibvorgänge in Postgres erfordert, das wir nicht trivial sharding können.

Modals ursprüngliche Sandbox-Steuerungsebenen-Architektur. Wenn Sandboxes erstellt werden, werden sie in eine Warteschlange gestellt und in Postgres geschrieben. Das Scheduling ist optimistisch und wird parallel ausgeführt, wobei eine zentrale Koordination erforderlich ist, um Konflikte zu vermeiden. Die Zuweisung einer Sandbox zu einem Worker (Rechenknoten) erfordert einen zusätzlichen Schreibvorgang in Postgres.
Da wir nicht auf Kubernetes aufbauen, konnten wir viele Teile dieses Systems skalieren. Beispielsweise ist das Scheduling standardmäßig parallelisiert, was uns ermöglicht, sehr hohe Burst-Sandbox-Erstellungsraten zu erreichen. Aber als wir auf immer größere Zahlen von Knoten und Sandboxes skalierten, stießen wir immer wieder auf neue Engpässe, die von Operationen herrührten, die entweder O(Sandboxes) oder O(Knoten) waren, aber nicht einfach zu skalieren waren.
Zum Beispiel führen wir für jede Sandbox, die abgeschlossen wird, einen dauerhaften Workflow aus, sodass hohe Sandbox-Churn-Raten massive Ereignis-Rückstände erzeugen würden. Wir stießen wiederholt auf RPCs, die mit einer Rate von O(Sandboxes) aufgerufen wurden und unerwartete Lastprobleme im gesamten System verursachten. Und die schiere Anzahl von Knoten, die zum Betreiben großer Zahlen von Sandboxes erforderlich ist, verursachte mehrere nachgelagerte Probleme bei der Knotenverwaltung und der automatischen Skalierung. Schließlich, obwohl wir damit umgehen konnten, hatte sich herausgestellt, dass es eine schlechte Idee war, eine nicht geshardete Postgres-Instanz im kritischen Pfad aller Sandbox-Erstellungen und -Schedulings zu belassen.
Unendliche Skalierung freischalten
Wir erkannten schnell, dass die Erreichung der gewünschten Skalierung ein Überdenken unserer Architektur von Grund auf erforderte. Wir wollen Millionen von Sandboxes betreiben und Zehntausende von Sandboxes pro Sekunde erstellen, was viel bessere Skalierungseigenschaften erfordert, als alles, was existiert. Anstatt zu versuchen, das Vorhandene weiterzuentwickeln, glaubten wir, dass der schnellste und sauberste Weg war, neu zu beginnen.
Um für Skalierung zu optimieren, entschieden wir, dass alles, was O(Sandboxes)- oder O(Knoten)-Last verursacht, standardmäßig horizontal skalierbar sein muss, der Sandbox-Erstellungspfad so einfach wie möglich sein sollte und alles andere nebensächlich ist. Die Lösung, zu der wir kamen, unterscheidet sich deutlich von bestehenden Systemen. Wir haben vollständig auf jede Art von zentraler Koordination verzichtet und globale Konsistenz gegen Skalierbarkeit und Leistung auf dem gesamten kritischen Pfad für den Betrieb und die Erstellung von Sandboxes eingetauscht. So funktioniert es:
- Anstelle eines einzelnen, serialisierten Schedulers betreiben wir eine Flotte von Scheduling-Servern, die Sandbox-Erstellungsanfragen gleichzeitig bearbeiten. Um eine Erstellungsanfrage zu bearbeiten, führt ein Scheduling-Server einen schnellen Scheduling-Algorithmus gegen zwischengespeicherte Daten im Arbeitsspeicher aus. Das Ergebnis ist, dass das Scheduling horizontal skaliert und eher wie Lastverteilung aussieht als traditionelles Container-Scheduling.
- Anstelle eines zentralen, dauerhaften Datenspeichers, der als Quelle der Wahrheit für Sandbox- und Worker-Zustand dient (wie die meisten Container-Plattformen funktionieren), ist jeder Worker in unserem neuen System seine eigene Quelle der Wahrheit. Worker veröffentlichen ihren Zustand periodisch in einen Redis-Stream. Die Scheduling-Server konsumieren diesen Zustand asynchron und verwenden ihn, um Scheduling-Entscheidungen zu treffen. Sobald ein Scheduling-Server entschieden hat, auf welchem Worker eine Sandbox erstellt werden soll, kontaktiert er den Worker direkt per RPC, um die Erstellung einer Sandbox anzufordern. Worker akzeptieren die Scheduling-Anfrage, wenn sie freie Ressourcen haben, oder lehnen sie andernfalls ab.
- Wir haben überhaupt keine Datenspeicher im kritischen Pfad der Sandbox-Erstellung, was die Skalierbarkeit und Zuverlässigkeit verbessert. Obwohl wir Sandbox-Metadaten und -Ergebnisse in dauerhaften Speicher schreiben müssen, tun wir dies weitgehend asynchron.
- Abgesehen von Sandbox-Erstellungen haben wir keine RPCs, die O(Sandboxes) sind. Worker bündeln Steuernachrichten für mehrere Sandboxes in einzelnen RPCs, im Sinne von Ideen aus datenorientiertem Design.

Unser endgültiges Design, das erste Mal, als wir es auf dem Whiteboard skizzierten.

Sandbox-Erstellungspfad in Modals v2-Sandbox-Architektur. Sandbox-Erstellungsanfragen werden von horizontal skalierten Scheduling-Servern bearbeitet, die dann mit einem schnellen In-Memory-Lastverteilungsalgorithmus einen Worker auswählen und den Worker (Rechenknoten) direkt kontaktieren, um eine Sandbox zu erstellen. Sandbox-Objekte werden in Redis gespeichert, aber nicht im kritischen Pfad.
Das Ergebnis ist, dass der Sandbox-Erstellungspfad nur zwei Netzwerk-Hops und eine günstige CPU-Operation erfordert. Es gibt keine zentralen Engpässe oder Koordinationskosten, keine Single Points of Failure und folglich keine praktische Obergrenze für die aggregierte Sandbox-Skalierung oder den Sandbox-Erstellungsdurchsatz. Wir können nach Bedarf weitere Scheduler oder Worker hinzufügen. Der am nächsten liegende Engpass ist, dass alle Worker ihren Zustand in einen einzigen Redis-Stream veröffentlichen, aber Lasttests haben ergeben, dass dies bis weit über 100.000 Worker hinaus praktikabel bleibt; und wir sind ohnehin nicht auf die Reihenfolge im Stream angewiesen, sodass es einfach wäre, weitere Streams hinzuzufügen. Durch das Design vermeiden wir die Probleme, die bestehende Lösungen am Skalieren hindern.
Der Bau dieser Lösung war nicht einfach! Der gesamte Entwicklungsprozess hat Monate Arbeit in Anspruch genommen und erstreckte sich über die meisten großen Systeme in unserem Backend. Wir verbrachten Stunden an Whiteboards. Vier von uns zogen in ein Ferienhaus in Miami Beach, um einen Prototypen des neuen Systems zu bauen, das wir wollten, ohne Ablenkungen. Wir verbrachten acht Tage damit, Code zu schreiben, bis wir körperlich nicht mehr konnten, spielten Schnellschach zur Erholung, sprangen ins Meer und gingen dann direkt zurück zum Code, um unser neues System sauber und funktionsfähig zu bekommen.

Unser bester Ingenieur entspannt sich in Miami Beach.
Als die Kernkomponenten funktionierten (und wir zurück in New York waren), mussten wir auch jede einzelne Sandbox-Funktion und die gesamte Sandbox-Beobachtbarkeit auf unserem neuen System neu implementieren. Dieses Projekt erforderte auch Änderungen an unserem Kern-Worker-Management-Stack sowie an unserer Container-Laufzeitumgebung. Ein interessantes Problem, auf das wir stießen, war zum Beispiel, dass unsere neuen Sandbox-Scheduler Container so schnell auf Worker schieben konnten, dass viele gleichzeitig startende Container um die rtnl-Sperre im Linux-Kernel konkurrierten, wenn sie Container-Netzwerkregeln einrichteten, und Dutzende von Sekunden zum Starten benötigten. Also mussten wir unsere Container-Netzwerkeinrichtung für Sandboxes ändern, nur damit unsere Worker nicht explodierten, wenn sie mit Sandbox-Erstellungen überflutet wurden.
Wie unsere Leistung abschneidet
Wir haben unser System bewertet, indem wir 1 Million Sandboxes so schnell wie möglich hochgefahren haben. Auf hoher Ebene können wir eine Million Sandboxes in weniger als einer Minute erstellen, wobei der primäre Engpass der Benchmark selbst ist. Die individuelle Sandbox-Zeit bis zur Interaktivität bleibt konstant niedrig, und wir sehen keine echten Leistungseinbußen mit der Skalierung.

Verteilung und eCDF der Sandbox-Erstellungsanfragen. Eine Sandbox-Erstellungsanfrage wird zurückgegeben, wenn unsere Scheduling-Server erfolgreich eine Sandbox einem Worker zugewiesen haben und sie zu starten begonnen hat.
Wir glauben, dass dies angesichts unseres Designs zu erwarten ist. Es gibt keine Koordination im Scheduling-Pfad, daher sollte das Scheduling unabhängig von Parallelität und Skalierung sehr schnell bleiben. Unserer Meinung nach haben wir keine ernsthaften Grenzen für das gleichzeitige Sandbox-Scheduling oder die Skalierung außerhalb der verfügbaren Kapazität, und die Verwaltung der Kapazität ist etwas, das wir bereits gut können.

Streudiagramm von 10.000 Sandbox-Startzeiten aus unserem 1M-Sandbox-Test, zufällig aus den 1M Sandboxes ausgewählt.
Sandbox-Startzeiten in unserem neuen System (die Latenz vom ersten Versuch des Clients, eine Sandbox zu erstellen, bis die Sandbox Benutzercode ausführen kann) liegen im Median unter einer halben Sekunde und bleiben bei Skalierung stabil. Sie sind auch wesentlich schneller als in unserem alten System, hauptsächlich weil das Scheduling so viel schneller ist – es dauert jetzt nur noch Dutzende Millisekunden. Der lange Schwanz der Latenz ist etwas länger, als wir es uns wünschen. Wir führen einen Großteil dieses Schwanzes auf Kernel- und Netzwerk-Konflikte (einschließlich der zuvor erwähnten rtnl-Sperr-Konflikte) zurück, wenn viele Sandboxes gleichzeitig auf demselben Worker starten, und wir arbeiten daran, ihn zu reduzieren. Auch der Schwanz bei Skalierung ist real. Wir erwarten, dass sich dies verbessert, wenn wir den Container-Startpfad optimieren.
Insgesamt sind wir mit diesen Leistungszahlen sehr zufrieden. Da Agents die Welt übernehmen, können wir offensichtlich mit ihnen skalieren.
Überzeugen Sie sich selbst
Bald wird dieses neue System das gesamte Sandbox-Scheduling bei Modal unterstützen, aber es ist bereits in der Beta verfügbar. Sie können sich mit einer einfachen Änderung in Ihrem Code anmelden. Wenn Sie viele Sandboxes betreiben müssen, probieren Sie es aus und sprechen Sie mit uns!
Danksagungen
Viele Menschen haben Blut, Schweiß und Tränen in dieses Projekt gesteckt. Unser Miami-POC wurde von Colin Weld (mir), Daniel Shaar, Walter Tang und Gleb Posobin gebaut und dann von Walter, Colin, Connor Adams, Akshay Balwally, Tom Wildenhain, Scott Hao und Taylor Baldwin in die Produktion gebracht.





