Der Begriff Serverless umfasst eine breite Palette von Technologien, die von den großen Cloud-Hosting-Providern angeboten werden: AWS, GCP und Azure. In diesem Blogeintrag konzentrieren sich unsere Xperten auf eine der wichtigsten Serverless-Technologien, die von jedem der großen Cloud-Provider angeboten wird: Function as a Service (FaaS). Diese Funktion bietet einen schnellen Weg zum Schreiben von Code, der sich auf das zu lösende Problem und nicht auf Boilerplate konzentriert, von vornherein hoch skalierbar ist und Kosten entsprechend der Nutzung erzeugt.
Ihre inhärente Skalierbarkeit und die enge Kopplung von Kosten und Nutzung machen FaaS zu einer besonders nützlichen Technologie für Anwendungsfälle, in denen die Auslastung stark variiert, wie bspw. im IoT-Sektor oder bei ETL Workloads. Sie bietet auch eine Möglichkeit, ein Orchestration-Layer vor bestehenden Backends zu schaffen und so größere Transformationen zu ermöglichen, ohne die Nutzenden zu beeinträchtigen.
Vergleich der unterschiedlichen Clouds
Es existieren bereits Tausende von Artikeln, die erklären, was FaaS ist und warum man es nutzen sollte. Ziel unseres Blogartikels ist es, die unterschiedlichen Angebote zu vergleichen und einen kompakten Überblick zu geben. Als Technologieanbieter mit Erfahrung in allen großen Clouds befinden wir uns in einer äußerst vorteilhaften Position, um einen solchen Vergleich anzustellen. Jeder Cloud-Provider bietet eine etwas andere technische Umsetzung von FaaS, die ihre eigenen Vor- und Nachteile mit sich bringt. Daher ist ein solcher Vergleich vor allem nützlich, um zu entscheiden, welcher Provider sich am besten für Projekte, in denen FaaS umfassend genutzt wird, eignet. Im Nachfolgenden werden wir die FaaS-Angebote der einzelnen Cloud-Provider (Google Cloud Functions von Google, Azure Functions von Azure und Lambda Functions von AWS) genauer unter die Lupe nehmen und sie in einer Reihe wichtiger Bereiche miteinander vergleichen.
Im Rahmen dieser Evaluierung konzentrieren wir uns auf den Anwendungsfall der Entwicklung einer einfachen API, deren Aufgabe es ist, Client-Anfragen entgegenzunehmen, sie umzuwandeln und an verschiedene Backend-Dienste zu verteilen. Bei diesem Anwendungsfall ist die Betriebszeit wichtiger als bei anderen FaaS-Anwendungsfällen wie bspw. der Batch-Verarbeitung oder dem Streaming von Ereignissen von IoT-Geräten.
Ausführungsumgebung
Einer der Fallstricke bei der Entwicklung von Systemen, die in jeder Größenordnung funktionieren müssen, besteht darin, sie synchron zu konzipieren und dabei die Tatsache zu ignorieren, dass die meisten Informationsabläufe in der realen Welt asynchron sind. Denken wir bspw. an ein Gespräch: Wenn eine Person spricht, ist es nicht so, dass die andere nichts tut; im besten Fall hört sie zu, im schlimmsten Fall ignoriert sie, was gesagt wird, und überlegt, was sie als Nächstes sagen will. Der Standardansatz sollte nicht darin bestehen, eine Kette von REST-Requests zu erstellen, nur um Nutzenden sofort eine Response geben zu können. Das wäre eine besonders schlechte Umsetzung: anfällig und unflexibel für zukünftige Änderungen.
FaaS macht es einfach, Systeme asynchron zu entwerfen und zu implementieren. Das Problem mit der Asynchronität ist jedoch, dass sie Entwickelnde zwingt, über Gleichzeitigkeit nachzudenken. Wenn das System in rasantem Tempo skaliert werden kann und der Arbeitsablauf nicht unbedingt direkt an einen User-Request gekoppelt ist (man denke bspw. an eine Warteschlange von Ereignissen, die sich seit einer Stunde aufbaut), dann muss jeder Teil des Systems so aufgebaut sein, dass er nachgelagerte Systeme nicht durch DDoS beeinträchtigt.
Gleichzeitigkeit bei horizontaler Skalierung
Die Ausführungsumgebung jeder Unit in FaaS definiert, wie sie mit Gleichzeitigkeit umgeht. Und die Unit ist auch ein guter Ausgangspunkt für unseren Vergleich. Um FaaS in vollem Umfang nutzen zu können, ist es sinnvoll, jede Funktion als Nanoservice zu betrachten, die eine einzelne Funktionalität unabhängig von anderen Funktionen bereitstellt. Während alle drei FaaS-Angebote es Entwickelnden ermöglichen, die Ober- und sogar Untergrenzen für die Skalierung nach Units (horizontale Skalierung) zu definieren, befinden sich diese Units bei Azure auf einer anderen Abstraktionsebene als bei AWS und GCP. Die konfigurierbare Unit für FaaS in AWS und GCP einschließlich ihrer Ausführungsumgebung ist eine einzelne Funktion. Im Kontext einer REST-API könnte dies mit einem Endpunkt abgebildet werden. Dies macht es einfach und wie selbstverständlich, die Skalierung auf einer End-to-End-Basis zu definieren – jede Funktion/jeder Endpunkt kann unabhängig von den anderen konfiguriert und bereitgestellt werden.
Die konfigurierbare Unit von Azure Functions ist die Function App – eine Gruppe von Funktionen, die zusammen bereitgestellt werden und als eine logische Einheit funktionieren. Um unser Beispiel der REST-API wieder aufzugreifen, könnte es sich hierbei um eine ganze REST-Ressource handeln (GET, POST, PUT, DELETE statt nur GET). Anhand dieses Beispiels wird ein Nachteil dieses Ansatzes sofort deutlich: Der POST-Endpunkt kann nicht unabhängig vom GET-Endpunkt skaliert werden. Nehmen wir einmal an, bei der REST-Ressource würde es sich um /orders handeln. In diesem Fall hätten die Erstellung neuer und der Abruf bestehender Bestellungen höchstwahrscheinlich unterschiedliche Skalierungsparameter. Würde man die Erstellung mit der gleichen maximalen Rate wie den Abruf zulassen, könnte dies zu Problemen in den nachgeordneten Bereichen führen.
Natürlich lässt sich dieses Problem ganz einfach lösen, indem man die Endpunkte für das Abrufen und das Anlegen von Aufträgen in getrennte Function Apps aufnimmt. Es ist jedoch wichtig, dieses Konzept zu verstehen, bevor man entscheidet, wie die einzelnen Function Apps gestaltet werden sollen.
Gleichzeitigkeit innerhalb einer Unit
Neben der Gleichzeitigkeit über mehrere Funktionsinstanzen bietet Azure Functions eine weitere Möglichkeit, mehrere Requests gleichzeitig zu bedienen: Gleichzeitigkeit in jeder Funktion selbst. Um zu verstehen, was dies bedeutet und warum Azure dies anbietet, ist eine kurze Erklärung eines wichtigen Teils des FaaS-Lebenszyklus erforderlich: der Cold Start. Die horizontale Skalierung hat ihren Preis: Jedes Mal, wenn eine Funktionsunit zur Bereitstellung von Datenverkehr erstellt wird, dauert es einige Zeit, bis sie online ist. Das ist verständlich: Eine völlig neue Ausführungsumgebung – JRE, Node-Instanz etc. – wird zusammen mit dem Code und den Abhängigkeiten der Funktion aufgespult (Spooling).
Während alle Provider die Wiederverwendung bestehender Units erlauben, die derzeit nicht genutzt werden, ermöglicht Azure Functions, dass eine bestehende Funktionsunit mehrere Requests gleichzeitig bedienen kann (GCP bietet diese Funktion mit Cloud Functions V2 ebenfalls an, sie ist jedoch nicht standardmäßig aktiviert). Dies ermöglicht eine gute Nutzung der vorhandenen Ressourcen, stellt aber gleichzeitig auch ein großes Problem dar, wenn die Funktion überhaupt einen State hat. Zur Veranschaulichung: Man stellt sich bspw. vor, dass jede Funktionsausführung mit einer sogenannten Korrelations-ID verknüpft ist, damit eine Prozessausführung in den Protokollen über mehrere Teile des Systems hinweg verfolgt werden kann – ein gängiges Muster für verteilte Systeme. Diese Korrelations-ID wird von einem Gateway generiert und über einen HTTP-Header an die Funktion weitergegeben, und die Funktion konfiguriert ihren Logger so, dass er sie in jeden von ihr erzeugten Protokolleintrag aufnimmt. Solange sich die Anwendung in der Entwicklung befindet und nicht skaliert werden muss, funktioniert dies reibungslos. Sobald jedoch der Datenverkehr für die Anzahl der laufenden Units zu groß wird, verwendet Azure die vorhandenen Einheiten wieder gleichzeitig. Werden hier keine Vorsichtsmaßnahmen getroffen, überschreibt die neue Ausführung, die gestartet wurde, während die vorherige noch lief, die Korrelations-ID und beide Ausführungen erhalten dieselbe Korrelations-ID.
Auch hierfür gibt es Lösungen: für eine Node-Ausführungsumgebung wäre das bspw. ein asynchroner lokaler Speicher, aber da dieses Verhalten standardmäßig für Azure Functions aktiviert ist, besteht eine sehr hohe Wahrscheinlichkeit, in der Produktion das erste Mal über dieses Problem zu stolpern.
Deployment
Deployment ist bei jedem Cloud-Provider relativ einfach. Jeder verfügt über eine CLI, mit der die Funktionen direkt bereitgestellt werden können, während IaC-Tools wie Terraform, CloudFormation und Bicep eine Möglichkeit bieten, den gewünschten Bereitstellungsstatus deklarativ zu definieren.
Keine Ausfallzeiten
Der Anwendungsfall für unsere Evaluierung ist eine einfache nutzungsorientierte API. Die Betriebszeit ist eine wesentliche Kennzahl für solche APIs. Während sich asynchrone Ereignisverarbeitungs-Funktionen (Event Processing Functions) leicht von kurzen Ausfallzeiten erholen können, wenn die Ereignisverarbeitungs-Infrastruktur (Event Processing Infrastructure) entsprechend konfiguriert wurde, können und wollen Nutzende nicht unbegrenzt warten. AWS Lambdas und GCP Cloud Functions sind von Haus aus mit Zero Downtime Deployments ausgestattet. Wenn der Code einer Funktion geändert oder ihre Ausführungsumgebung neu konfiguriert wird (bspw. Aktualisierung der Node-Version), werden die Requests nahtlos an die neue Funktionsinstanz übertragen, sobald diese bereit ist. Daraufhin wird die alte Instanz gelöscht.
Bei Azure Functions hingegen ist dies nicht der Fall. Die Function App muss nach einem Deployment neu gestartet werden, was eine gewisse Ausfallzeit verursacht. Die Verwendung eines Mechanismus namens Deployment Slots ermöglicht es Entwickelnden, Ausfallzeiten zu vermeiden, wenn das Deployment nur Code-Änderungen umfasst. Wird jedoch die Konfiguration der Ausführungsumgebung geändert (bspw. wenn die Version der Laufzeitumgebung geändert wird), dann werden die Slots neu erstellt und somit neu gestartet, was zu Ausfallzeiten führt. Für ein wirklich ausfallfreies Deployment ist eine komplexere Einrichtung erforderlich. Auch hier ist die Abstraktionsebene der konfigurierbaren Einheit das schwache Glied. Die Gruppierung der Funktionen zu einer Function App hat zu zwei erheblichen Problemen im Deployment geführt:
- Deployments für eine der Funktionen in der App wirken sich auf den Lebenszyklus aller Funktionen in der App aus.
- Die Auswirkungen dieses Deployments werden noch dadurch verstärkt, dass es kein Zero Downtime Deployment gibt.
Lokale Umgebung
Die Möglichkeit, FaaS lokal zu testen, ist ein wesentliches Merkmal, da andernfalls jedes Mal, wenn eine Änderung getestet werden muss, ein Cloud-Deployment erforderlich wäre. Dies stellt eine interessante Herausforderung für Cloud-Provider dar, da diese eine installierbare Umgebung bauen müssen, die eben auch auf einem lokalen Rechner problemlos läuft. Die Herausforderung hier besteht im Emulieren der Umgebung, in der die Funktionen ausgeführt werden, sobald diese in der Cloud bereitgestellt wurden.
GCP bietet hierfür das Functions Framework, AWS ein CLI-Tool namens SAM (Serverless Application Model) und Azure seine Core Tools.
Zu guter Letzt: ein Vorteil für die Gruppierung von Funktionen
Um auf unsere einfache Rest-API zurückzukommen: Sie besteht aus einer Reihe von Ressourcen, die jeweils mehrere Endpunkte haben. Das bedeutet, dass wir mehrere Funktionen (die jeweils einen Endpunkt bedienen) auf einmal entwickeln, testen und bereitstellen wollen. An dieser Stelle ist das Konzept einer übergreifenden Functions App sehr nützlich. Das Core-Tools-Framework von Azure Functions kann als Entwicklungsabhängigkeit installiert werden und erstellt beim Start eine lokale Umgebung, in der alle in der Functions App definierten Funktionen bereitgestellt werden. Mit einem einzigen Befehl erhalten Entwickelnde einen lokalen HTTP-Server mit einem Endpunkt für jede HTTP-Funktion und eine einfache Möglichkeit zum Debuggen ihres Codes. Die wahre Stärke von FaaS ist jedoch, dass die Funktionen durch fast alles ausgelöst werden können, nicht nur durch einen http-Request. Wir werden im nächsten Abschnitt näher darauf eingehen, möchten aber an dieser Stelle nur zwei Beispiele für andere nützliche Funktionsauslöser nennen: Event-Hubs-Ereignisse oder die Erstellung eines Datenbankeintrags. Das Core-Tools-Framework ermöglicht es auch, auf diese Weise ausgelöste Funktionen lokal zu debuggen. Das bedeutet, dass eine Message, die von einem IoT-Gerät auf der anderen Seite der Welt erzeugt wird, auf einem lokalen Rechner konsumiert und debuggt werden kann. Hierbei handelt es ich um ein äußerst leistungsstarkes wie hilfreiches Werkzeug.
Das Functions Framework von Google kann auch als Entwicklungsabhängigkeit installiert werden, jedoch nur eine Funktion auf einmal emulieren, was seine Nützlichkeit für die lokale Ausführung einer API deutlich einschränkt. Eine Möglichkeit für ein Deployment aller Funktionen besteht darin, ein benutzerdefiniertes Express-Setup zu erstellen und jede Funktion darin über ein benutzerdefiniertes Skript bereitzustellen. In diesem Fall wird die Cloud-Laufzeitumgebung jedoch nicht mehr emuliert.
Die SAM CLI von AWS geht in eine andere Richtung. Die CLI muss unabhängig vom Projekt installiert werden und verwendet Docker zur Emulation der Lambda-Funktionen. Bei der Ausführung einer lokalen API kombiniert es den Funktionscode mit der Definition in der CloudFormation (IaC)-Vorlage und führt für jede Funktion in der API einen Docker-Container aus. Auf diese Weise kann die Cloud-Umgebung sehr genau emuliert werden, aber es ist eine viel komplexere lokale Einrichtung erforderlich als bei Azure oder GCP.
Integration mit anderen Diensten
Wie bereits erwähnt, liegt die eigentliche Stärke von FaaS in seiner Flexibilität: Funktionen können von fast jedem anderen Dienst ausgelöst werden, der vom Hosting-Cloud-Provider angeboten wird. Unser Anwendungsfall einer einfachen API kratzt kaum an der Oberfläche dessen, was möglich ist.
Obwohl ein Vergleich der PaaS-Dienste (Platform as a Service), die von jedem Cloud-Provider angeboten werden, den Rahmen dieses Artikels übersteigen würde, spielen sie eine Rolle bei der Bewertung der Stärke des FaaS-Angebots in jeder Cloud.
Gateway
Beim Aufbau eines Systems mit öffentlich zugänglichen REST-APIs ist ein Gateway ein nahezu unverzichtbares Werkzeug für die Implementierung übergreifender Standards und Funktionen (API-Konsistenz, Authentifizierung, Logging) und die Bereitstellung eines Abstraktions-Layer, das die API von der für ihre Implementierung verwendeten Technologie trennt.
Sowohl Azure als auch AWS verfügen über sehr starke Gateway-Angebote, die sich beide nahtlos in ihre FaaS-Produkte integrieren lassen. Tatsächlich können AWS Lambdas nur dann durch einen externen HTTP- Call ausgelöst werden, wenn ein API-Gateway dafür konfiguriert wurde. Beide Gateway-Produkte ermöglichen die Definition von APIs mit OpenAPI-Definitionen (Swagger) und bieten neben der JWT-Authentifizierung auch Mechanismen zur Definition von bspw. Traffic-Drosselung sowie IP Blocklisting. Die API-Endpunkte werden für den Call bestimmter Funktionen konfiguriert.
GCP schneidet hier als der am wenigsten Ausgereifte der drei großen Cloud-Provider ab. Es liegt bereits ein paar Jahre zurück, seit wir uns das letzte Mal mit der Konfiguration eines Gateways für eine auf Cloud-Funktionen basierende API intensiv auseinandergesetzt haben. Damals schien Apigee die einzige Option zu sein, aber es war keine direkte Integration mit dem Cloud-Functions-Produkt möglich. Seither hat Google seine Anwendung mit einem Produkt namens API Gateway aufgerüstet, bisher hatten wir jedoch noch keine Möglichkeit, Google Cloud Functions mit der neuen Integration zu testen.
Event-driven
FaaS ist Event-driven. Funktionsausführungen werden als Reaktion auf Events ausgelöst, bspw. ein HTTP-Request, ein Event-Stream, ein Timer oder unzählige andere Dinge, die vom jeweiligen Cloud-Provider getrackt werden können. Dies fördert und unterstützt die Implementierung von Event-driven Architekturen und führt organisch zu Workflows, bei denen die Schwerstarbeit von nicht blockierenden Funktionen erledigt wird.
GCP bietet mit PubSub einen herrlich einfachen Publish-/Subscribe-Service, der es Entwickelnden ermöglicht, Topics zu definieren, in denen Messages veröffentlicht werden sollen, sowie Subscriptions für diese Topics, die entscheiden, wohin diese Messages gehen sollen. Die Integration mit Cloud Functions ist beim Subscripton-Typ „Push“ nicht unbedingt erforderlich, da Funktionen hier als einfache HTTP-Endpunkte behandelt werden können. Der Service unterstützt die Filterung von Messages, so dass Subscriber über ihre Subscription jene Messages auswählen können, die sie konsumieren möchten, und die OICD-basierte Authentifizierung zwischen Topic und Subscription-Endpunkt.
In AWS funktioniert EventBridge ähnlich wie PubSub. Anstelle von Topics verwendet EventBridge Eventbusse (Pipelines) und anstelle von Subscriptions müssen Entwickelnde Regeln definieren, die jedes Mal ausgewertet werden, wenn ein Event auf dem Bus eintrifft. Eine typische Regel leitet das Event an einen HTTP-Endpunkt oder einen anderen AWS-PaaS-Service wie Lambda weiter.
Die Integration von Azure Function mit dem Rest des Azure-Ökosystems ist erstklassig. Ereignisgesteuerte (Even-driven) Funktions-Trigger und -Outputs werden als Teil des Quellcodes der Funktionen selbst definiert. Eine Funktion so zu konfigurieren, dass sie auf einen Event-Hubs-Stream hört, die Messages verarbeitet und dann die Ergebnisse an einen nachgelagerten Service eines anderen Typs wie Service Bus veröffentlicht, ist so einfach wie ein paar Einträge in einer JSON-Datei (bei Verwendung von TypeScript). Dies ermöglicht die schnelle Erstellung asynchroner Workflows, bei denen jeder Teil der Workflow-Kette auf die für seinen Anwendungsfall am besten geeignete Technologie zugreifen kann.
Fazit
Es existieren noch viele andere relevante Aspekte, die beim Vergleich der FaaS-Angebote der einzelnen Cloud-Provider zu berücksichtigen sind – bspw. Logging und Monitoring – die aus Platzgründen hier jedoch nicht mehr erläutert werden können. Abschließend lässt sich sagen, dass die FaaS-Angebote der drei großen Cloud-Provider eine hervorragende Möglichkeit für Entwicklungsteams darstellen, sich auf den geschäftlichen Nutzen zu konzentrieren und nicht auf Infrastruktur, Skalierung und Orchestrierung. Keiner der drei Cloud-Provider verfügt über die perfekte Kombination von Funktionen. GCP Cloud Functions bietet die schnellste Möglichkeit, eine API nur mit Funktionen zu erstellen und bereitzustellen, wohingegen Azure Functions über bessere Tools und eine ausgereiftere Reihe von IoT-PaaS-Services verfügt, in die es sich integrieren lässt. AWS Lambda ist ähnlich, hat aber ein komplexeres lokales Setup, das die Anfangsgeschwindigkeit eines Teams verringert. Je nach den Anforderungen der jeweiligen Anwendung können die drei Lösungen anhand der erläuterten Aspekte geprüft werden, um einen passenden Cloud-Provider auszuwählen.
Quellen:
Softwareentwicklung
Erfahren Sie mehr über unsere Leistungen