tl;dr:
FaaS machen m.E. in gewissen Situationen absolut Sinn, aber…
extended version: Bei dem Projekt ging es eigentlich um einen Proof of Concept zur Preispunkt- und Wertbeitragsanalyse von Produktbestandteilen in Bundle-Produkten und größeren physikalischen Produkten. Abseits des rein analytischen Parts wollte ich auch technologisch mal wieder was Neues ausprobieren. Also orientierte ich mich an einer schon lange existierenden Diskussion:
Was ist die richtige Größe für Microservices?
Diese Diskussion wird es hier und jetzt nicht geben. Jedoch wollte ich einmal das kleinste Extrem ausprobieren: Functions-as-a-Service (FaaS).
Ok, als einzelner Entwickler ist das schon etwas overengineered, aber es war eine gute Übung mit ein paar Learnings für mich.
Rahmenbedingungen:
- GitLab CE
- Google Cloud Functions
- Google Cloud PubSub
- Google Memorystore
- NodeJS
- FaaS only
- Keine RPCs
Mein Repository sah von der Struktur so aus:
Repository/
├── functions/
│ ├── pubsub/
│ │ ├── template_pubsub/
│ │ │ ├── node_modules/
│ │ │ ├──.env.yml
│ │ │ ├──.gcloudignore
│ │ │ ├── index.js
│ │ │ ├── package.json
│ │ │ └── package-lock.json
│ │ ├── ...
│ │ ├── ...
│ ├── http/
│ │ ├── template_http/
│ │ │ ├── node_modules/
│ │ │ ├──.env.yml
│ │ │ ├──.gcloudignore
│ │ │ ├── index.js
│ │ │ ├── package.json
│ │ │ └── package-lock.json
│ │ ├── ...
│ │ ├── ...
├──.gitignore
├──.gitlab-ci.yml
Insgesamt bestand das Projekt aus ca. 200 Cloud Functions, wovon die meisten von einem PubSub getriggered wurden.
Die HTTP Trigger wurden nur zur Übergabe neuer Startseiten für den Web Crawler oder zur Abfrage von Resultaten genutzt.
Die einzelnen Functions waren wirklich klein gehalten: eine Methode für den Trigger, eine bis drei kleine Helper-Methoden für die Datenverarbeitung und eine zum Datenversand in andere PubSub Channels.
Insgesamt bin ich zu 90% von FaaS überzeugt, es kommt aber wie immer auf den Use Case an.
Selbst eine “schlafende” Funktion, welche erst “aufgeweckt” werden muss für einen Request / Trigger war im Median innerhalb von 0.1 Sekunden hochgefahren und begann mit der Ausführung. Dies ist zwar für Realtime-Anwendungen nicht wirklich vertretbar (vor allem wenn mehrere Functions sequenziell abgearbeitet werden müssen), jedoch könnte man sicherlich einen Warm-up-Trigger schreiben: Welche Functions werden in längeren Sequenzen zu Beginn gefeuert, sodass die folgenden Functions bereits hochgefahren sind.
Das waren meine größten Herausforderungen:
- Repository-Struktur
- Deployment
- Deployment-Dauer
- Abwärtskompatibilität der Functions
- Shared Modules
Nichts davon ist jetzt ein großes Novum, aber ich musste schon an der ein oder anderen Stelle von den gewohnten Wegen abweichen.
Mein Fazit:
FaaS sind wunderschön! Sie zwingen einen, seine Architekturentscheidungen konstant zu überprüfen und sich auch wirklich daran zu halten. Mal schnell einen Umweg zu nutzen, ist kaum möglich.
Der benötigte Overhead ist jedoch immens. Wie man an der Repository Struktur sieht, ist vieles mehrfach vorhanden.
Wenn man die Funktionalität der Cloud Functions auf atomarer Ebene schreibt, fühlt es sich irgendwie seltsam an, für eine Function von 5-10 Zeilen gefühlt 100 Zeilen Boilerplate an anderen Stellen zu benötigen (Da fällt mir nur ‘Welcome to JavaLand’ ein…).
Gefühlt ist aktuell die CI/CD-Welt (von GitLab zumindest) noch nicht wirklich auf so kleinteilige Deployments ausgelegt. Die oben genannte Bash-Magic in einem Deployment-File macht das Ganze nicht wirklich übersichtlicher. Reine Docker-Deployments können so schön übersichtlich sein, das aktuelle CI-File macht keinen Spaß mehr zu lesen oder gar zu erweitern.
Ich sehe ganz klare Vorteile von FaaS für große Organisationen mit vielen Entwicklern, die eine niedrige Cycle Time benötigen. Oder bei Teams, die aufgrund ihrer Git-Strategie mit “Fast Forward Merges only” arbeiten, entsprechend oft Rebasen müssen und hierbei relativ viel Zeit mit dem Beheben von Merge-Konflikten verbringen.
Als einzelner Entwickler würde ich beruflich für Proof of Concepts oder frühe Prototypen auf alle Fälle Richtung Monolith oder größerer Microservice-Architektur tendieren.
Bei MVPs deren Time-to-Market kritisch ist, würde ich eher zu einer guten Microservice-Architektur tendieren, da hier der Wechsel zu einer FaaS-Architektur bei Bedarf iterativ nachgezogen werden kann.
Die Testabdeckung ist zwangsweise sehr hoch. Am Ende kam ich bei 99% Zeilen-Testabdeckung und 100% Branch-Testabdeckung raus.
Die Wiederverwertbarkeit von Funktionen kann sicherlich auch projektübergreifend profitieren, daher wäre es für mich nun interessant, FaaS mal im Produktiveinsatz zu nutzen.
Ich werde auch für das nächste Pet-Project FaaS nutzen und das, obwohl der Overhead relativ hoch ist. Aber es macht am Ende wirklich Spaß, seine Requests durch die Cloud Functions fliegen zu sehen und sich intensiver mit dem neuen Architekturmuster zu beschäftigen.