Optibus erstellt kontinuierliche Planungen für den Massentransit: stadtweite Planung und Terminplanung, die bestimmen, wo Fahrzeuge und Fahrer eingesetzt werden sollten, wobei sichergestellt wird, dass der bereitgestellte Service sowohl kosteneffektiv als auch pünktlich ist. Wenn Sie mit Planungsproblemen vertraut sind (der Fachbegriff für diese Probleme lautet Fahrzeug- und Crew-Planung, auch bekannt als Umlauf- und Dienstplanung), wissen Sie wahrscheinlich, dass sich Optibus mit NP-Hard-Problemen beschäftigt. Wir tun dies, indem wir ausgeklügelte verteilte Optimierungsalgorithmen und AI anwenden (um unter anderem die Wahrscheinlichkeit von On-Time-Service vorherzusagen). Die Lösung dieser NP-Hard-Probleme ist rechenintensiv. Eine der Herausforderungen, vor denen wir stehen, ist die intelligente Nutzung und Parallelisierung von Cloud-Computing-Ressourcen, um hohe Verteilungskapazitäten mit einem minimalen Kostenaufwand zu erhalten.
In diesem Beitrag werde ich erläutern, warum wir Function-As-A-Service zusätzlich zur Verwendung cloudbasierter Ressourcen verwenden. Es ist wahr, dass man mit der Cloud immer mehr Ressourcen erhalten kann, wenn sie benötigt werden, oder Ressourcen reduzieren kann, wenn sie nicht benötigt werden. In „burst”-basierten, rechenintensiven Situationen wie unseren ist dies jedoch nicht so einfach. Und hier kommt FaaS ins Spiel. Benutzer von Optibus (typischerweise Transitplaner) nutzen unser System, um schnell Transitpläne und Zeitpläne zu erstellen. Dafür beginnt unser Backendsystem sofort mehrere „burst”-basierte rechenintensive Operationen, die alle parallel ausgeführt werden und die Daten über viele verschiedene Knoten verteilen.
Warum Function-As-A-Service (FaaS)?
Beginnen wir mit einem Beispiel: ein Szenario, in dem wir eine riesige Liste von Strings durchgehen und eine Parsinglogik anwenden müssen und dieses Szenario tritt als Reaktion auf ein sporadisches Ereignis auf:
- Wir haben einen Job, der sehr einfach verteilt werden kann.
- Das Aussehen und die Häufigkeit dieses Jobs sind nicht konsistent, er kommt in Schüben (bursts, en) und sein Timing kann nicht sehr gut vorhergesagt werden.
Wir könnten die Parsinglogik auf so viele Knoten wie möglich verteilen und jedem Knoten einen sehr kleinen Teil der Liste geben, um die Verteilung zu maximieren (theoretisch sogar einen einzelnen String). In diesem Fall analysiert jeder Knoten einige Strings und wir aggregieren die Ergebnisse danach. Dies ist ein ziemlich einfaches MapReduce-Problem.
Dieses einfache hypothetische Szenario tritt in Optibus in einer komplexeren Form auf. Anstatt eine große Liste von Strings zu scannen, müssen wir den Transport einer ganzen Stadt optimieren (stellen Sie sich dies als eine große Liste der transportbezogenen Nachfrage mit Angebotsbeschränkungen vor). Wie im String-Beispiel zerlegen wir das Problem in viele Unterprobleme (dieser Teil ist an sich nicht trivial und in Zukunft einen eigenen Blog-Beitrag wert). Wir optimieren dann jedes Teilproblem separat (hier ein Beispiel für Depots) und kombinieren es zu einem aggregierten Optimierungsproblem. Dieser Prozess wiederholt sich viele Male mit verschiedenen Parametern bis zur Zusammenführung.
Es gibt einen inhärenten Kompromiss zwischen Leistung und Effizienz, der durch die Anzahl der Knoten gesteuert wird, die wir verwenden werden. Eine große Anzahl von Knoten kontinuierlich am Laufen zu halten ist nur dann effizient, wenn diese die meiste Zeit genutzt werden. Wenn wir zu viele Knoten am Laufen haben, wird es sehr selten sein, dass wir diese mit 100 % Auslastung betreiben können. Das Ergebnis werden zu viele Leerlaufknoten sein, die darauf warten, Aufträge auszuführen. Dies ist ein teures Ergebnis. Eine andere Alternative besteht darin, die Knoten nur bei Bedarf zu erzeugen (nur wenn der Job erforderlich ist). In diesem Fall dauert das Erzeugen eines Knotens jedoch 30 – 60 Sekunden, bis er verfügbar ist: Sie zahlen sowohl für die Ausführungszeit als auch für die Zeit, in der der Knoten gestartet wird (Leerlauf). Das Erzeugen von 1.000 Knoten führt zu 500 – 1.000 Minuten an vergeudeten Kosten und einer zusätzlichen Latenz von 30 bis 60 Sekunden, bis die Verarbeitung des Jobs beginnt.
Die dritte Option besteht darin, Function-As-A-Service (FaaS) zu verwenden, um eine maximale Verteilung für solche „Burst”-Jobs zu erzielen. Das ist unsere Aufgabe bei Optibus. FaaS wie AWS Lambda gibt die Möglichkeit, nur eine einzelne Funktion zu implementieren, auszuführen und nur für den Teil der Laufzeit zu bezahlen. Die Bereitstellungszeit ist nichts, das Sie bezahlen oder etwa steuern. Es ist Sache des Cloudanbieters, verfügbare Container zum Ausführen der Methode zu haben. Da der Cloudanbieter Ressourcen über alle seine Konten freigeben kann, ist es für den Anbieter viel einfacher, die Infrastruktur besser auszunutzen und Knoten für „Burst”-Operationen zur Verfügung zu haben.
Was am interessantesten in der Welt des Massentransits ist, ist die enorme Menge an Arbeit, um optimierte Ergebnisse zu erhalten, die Kosten für den Massentransit-Anbieter reduzieren und einen besseren Service für Passagiere ermöglichen. Wir sprechen über Milliarden von Optionen, die in Betracht gezogen, optimiert und dann auch noch schnell zur Verfügung gestellt werden müssen. Deshalb war eines unserer ersten Designziele, Optimierungen in Sekunden oder Minuten ausführen zu können (und das können wir). Hier setzen wir auch FaaS ein, damit wir Leistung und Skalierung auf Abruf verbessern können.
FaaS in die Realität umsetzen
Die Verwendung von FaaS für „Burst”-Jobs ist mit einer Reihe von Komplikationen verbunden. Sie müssen sicherstellen, dass jeder Methodenaufruf innerhalb des Zeitlimits des FaaS-Anbieters endet (z. B. 5 Minuten für AWS Lambda), und Sie müssen Daten intelligent und effizient serialisieren und deserialisieren, um nicht zu viel konstanten Overhead zu verursachen. Sie müssen sicherstellen, dass auf die von der Funktion benötigten Daten in hohem Maße gleichzeitig zugegriffen werden kann, da Sie viele parallele Aufrufe haben werden. Dieser ständige Overhead ist der Engpass für die Distributionsleistung (Amdahl’s law). Ein anderes nicht triviales Problem ist der Verlust von JIT (Just-In-Time-Kompilierung) -Optimierungen in Sprachen, die JIT unterstützen. Der Grund ist, dass jede ausgeführte Funktion auf einem frischen Container starten kann, der keinen „Warmup”-Prozess durchlaufen hat, was wiederum zur Ineffizienz beiträgt (für die Sie bezahlen). Eine Möglichkeit, damit umzugehen, ist, während des Erstellungsprozesses “vorgewärmte” Container zu erstellen und deren Speicherstatus auf der Festplatte abzuspeichern. Anschließend wird der Funktionsinitialisierungscode gestartet, indem diese Container als Einstiegspunkt geladen werden.
Grundsätzlich wird die “Ineffizienz” durch Leerlaufknoten oder Erzeugungszeiten auf den konstanten Overhead verschoben, der nötig ist, um die Daten zur Funktion zu bringen und die Funktion effizient (JITed) laufen zu lassen. Wenn sie dies minimieren können Sie theoretisch 100 oder 1.000 gleichzeitige Funktionen verwenden, je nachdem, was der zugrunde liegende Anbieter Ihnen bereitstellt.
Als wir diesen Mechanismus entwickelt haben, haben wir verschiedene Alternativen für unsere FaaS-Plattform geprüft und haben uns für die Binaris-Plattform entschieden, weil Sie in erster Linie viel schneller und kostengünstiger als die Alternativen ist. Außerdem mussten Pypy-Python-Interpreter unterstützt werden, die in Binaris standardmäßig unterstützt werden. Durch die Kombination der beiden konnten wir unsere Leistung und Kostenauslastung in kritischen Aspekten unseres Systems um das 20-fache verbessern.
Frohes Skalieren!