AWS Lambda Snapstart
Zunächst hatte ich überlegt, meinen Microservice als Spring Native Anwendung in die Lambda zu bringen, um schnellere Startup-Zeiten zu erreichen und gleichzeitig den Ressourcenverbrauch zu minimieren. Aber als ich mich so durch die Lambda Einstellungen in der AWS Console geklickt habe, ist mir die unscheinbare Option “SnapStart” aufgefallen. Zum Glück hat der Info Link tatsächlich direkt Hinweise gegeben, um was es sich dabei handelt.
Lambda SnapStart is a performance optimization that helps reduce startup latency at no additional cost. On supported runtimes, you can activate SnapStart for published versions. When you publish a function version, Lambda takes a snapshot of the memory and disk state of the initialized execution environment, encrypts the snapshot, and caches it for low-latency access. When you invoke the function version for the first time, and as the invocations scale up, Lambda resumes the initialized snapshot and then invokes the function handler.
Perfekt dachte ich. Damit bekomme ich meine Anwendung bestimmt schnell gestartet. Allerdings habe ich mich gefragt: “Wie machen die das und geht das auch mit meiner SpringBoot Anwendung?”
Coordinated Restore at Checkpoint (CRaC)
Beim Durchlesen der Dokumentation wurde dann schon etwas klarer, was alles unterstützt wird und was nicht, dazu kommen wir gleich noch. Auf der dritten oder vierten Seite der AWS Dokumentation wurde es dann auch endlich verraten. Die Java Anwendung wird unter CRaC gesetzt. CRaC ist ein OpenSource-Projekt, welches einer Java Anwendung ermöglicht, koordiniert durch Runtime Hooks ein Speicherabbild der JVM zu erstellen. Dieses wird einfach abgespeichert und zu einem späteren Zeitpunkt erneut geladen.
Das bedeutet konkret, die Java Anwendung wird gestartet und, nachdem sie ready ist, mit einen Speicherabbild wieder beendet. Die lange Startup-Zeit ist somit “gesichert” im Speicherabbild. Genau dieses wird dann später verwendet und über die JVM wieder gestartet.
Die Runtime Hooks sind sehr einfach. Innerhalb des eigenen Java Codes muss das Interface
org.crac.Resource implementiert werden. Dieses beinhaltet zwei Methoden
beforeCheckpoint() und
afterRestore(). Die Klasse, die das Interface implementiert muss dann noch im
Context registriert werden. Da es sich um ein extra OpenSource-Projekt handelt, ist CRaC
“noch” nicht im normalen OpenJDK oder änlichen Derivaten eingebaut. In der AWS Lambda wird dies für Java 11 und 17 direkt unterstützt. Will man eine Anwendung mit CRaC außerhalb einer Lambda betreiben, ist es am einfachsten das
Azul JDK zu verwenden.
Frameworks wie Micronaut und Quarkus unterstützen CRaC bereits seit gut einem Jahr.
Spring Boot mit CRaC
Bei Spring(Boot) ist es aktuell noch in der Entwicklung. Mit dem Spring Framework 6.1 und damit Spring Boot 3.2 wird die CRaC Unterstützung auch dort Einzug halten. Diese soll im November 2023 veröffentlicht werden. Aktuell ist aber bereits vieles davon im Snapshot bzw. Milestone funktionsfähig.
Der Vorteil ist, es muss an einer bestehenden Spring Boot Anwendung, die auf der Version 3.2 basiert, nicht wirklich etwas verändert werden. Innerhalb von Spring wurde das Ressource Interface implementiert und in den Lifecycle Mechanismus von Spring entsprechend eingehängt.
Es stellt sich die Frage “Was muss berücksichtigt werden beim Erstellen bzw. Restore eines Checkpoints bzw. SnapShots?”:
- Offene Netzwerkverbindungen (Sockets)
- Offene Dateien
- Zufallswerte
Starker Einfluss auf die Startup-Zeit
Gerade die Herstellung von Netzwerkverbindungen dauert in jeder Art von Anwendung leider eine gewisse Zeit. Das bedeutet auch, dass CRaC zwar so einiges beschleunigt, aber die Zeiten, die beim Verbindungsaufbau “verloren” gehen, damit leider auch nicht aufgefangen werden können.
Innerhalb von Spring wird dafür gesorgt, dass über die Datasource-Pools (z.B. Hikari) vor dem Erstellen Resource#beforeCheckpoint() die Datenbank-Verbindungen beendet werden und beim Starten des Speicherabbilds Resource#afterRestore() diese wieder hergestellt werden. Der Datasource-Pool kann danach wieder mit entsprechenden Verbindungen vorgefüllt werden.
Der Start einer Spring Boot Anwendung wird durch den Einsatz von CRaC ungemein schnell. Die Verbindungsaufbauten wird man leider nicht los. Trotzdem bewegen sich die eigentlichen Startup-Zeiten im niedrigen Millisekunden-Bereich.
Leider nicht die ganze Wahrheit
Die eigentlichen Startup-Zeiten, die auf vielen Seiten (z.B.
CRaC in Github) zu CRaC beschrieben werden, haben super niedrige Werte. So startet eine Spring Boot Anwendung anstelle von 3898 Millisekunden in 38 Millisekunden. Ein Traum, wenn man ehrlich ist. Allerdings sind diese Beispiele immer ohne Remote-Verbindungen, was auch dafür sorgt, dass es in der Realität noch ein wenig länger dauert.
Aber der entscheidende Faktor ist das eigentliche Laden des Speicherabbilds. Dies dauert z.B. innerhalb einer Lambda gerne um die 1 bis 2 Sekunden.
CloudWatch Log-Auszug