Monolith vs. Microservices Part 4
Skalieren wie netflix – das richtige für mein Projekt
Neben Themen wie unabhängiger Weiterentwicklung und unabhängigen Deployments spielen meist nicht-funktionale Anforderungen eine große Rolle bei der Entscheidung für oder gegen Microservices. Auch Moni und Michi beziehen nicht-funktionale Anforderungen in ihre Diskussion mit ein.
Zusammenfassung
Unterschiedliche nicht-funktionale Anforderungen sind ein starkes Indiz dafür, Microservices einzusetzen. Das Thema Skalierbarkeit wird als eines der markantesten Merkmale einer Microservice-Architektur propagiert. Gerade Netflix hat sehr mit den Erkenntnissen ihrer Entwicklung geworben und bei Netflix spielt Skalierbarkeit nun mal eine große Rolle. Aber trifft dies auf die eigene Enterprise-Anwendungen auch zu? Ist hier eine hohe Skalierbarkeit notwendig?
Beim Thema Verfügbarkeit und Robustheit kann eine monolithische Architektur im Gegensatz zu Microservices nicht glänzen. Allerdings bekommt man Robustheit und Verfügbarkeit nicht geschenkt, nur weil man Microservices macht. Es erfordert ein Umdenken beim Design der Anwendung und bei der Implementierung von Schnittstellen. Lose Kopplung und hohe Kohäsion sind im Kontext von Microservices wichtiger denn je.
Skalierbarkeit & Ablaufumgebung
Michi: „Unsere Zugriffszahlen werden sich je nach Tageszeit unterscheiden. Wir erwarten zur Mittagszeit und abends besonders viele Zugriffe. Rechenleistung kostet Geld. Ungenutzte Rechenleistung möchte ich aber nicht bezahlen. Meine Architektur erleichtert das unabhängige und schnelle Skalieren einzelner Services je nach Last.“
Moni: „Cloud-Anbieter bieten Services an, um auch Monolithen schnell zu deployen und nach Lastanforderung automatisch zu skalieren.“
Michi: „Ich kann für jeden meiner Services die optimale und damit kosteneffiziente Hardware wählen. Für die Kunden-App benötigen wir Hardware, um hohen Traffic zu verarbeiten. Die Routenplanung erfordert hingegen Hardware mit hoher CPU-Leistung. Das Problem bei deiner Architektur ist aber, dass du einen Kompromiss eingehen und eine einheitliche Hardware für alle fachlichen Prozesse benutzen musst.“
Das Argument Skalierbarkeit wird im Kontext von Microservices immer wieder benutzt – und es ist auch richtig, dass sich kleinere Services unabhängig voneinander skalieren lassen, während man bei einem Monolithen immer die ganze Anwendung skalieren muss. ABER: Viele Anwendungen haben das Problem der Skalierbarkeit gar nicht. Natürlich gibt es Anwendungsfälle, bei denen Skalierbarkeit unterschiedlicher Microservices notwendig ist (z. B. zur Berechnung von Updates für Fahrzeuge), aber Skalierbarkeitsprobleme sind eher bei Anwendungen wie Netflix oder Amazon mit einer Vielzahl an Nutzern zu verorten.
Ein klassischer LAMP-Stack (Linux, Apache, MySQL, PHP) auf 5000€ Hardware kann zum Beispiel ohne Probleme 6000 gleichzeitige Requests verarbeiten. Wenn man davon ausgeht, dass ein Request zu 99% in 200ms abgearbeitet ist und jeder Benutzer alle 10 Sekunden eine Anfrage an das System schickt, dann kann ein System auf dieser Hardware mit bis zu 300000 Benutzern umgehen (6000 * (10s/200ms)). Wenn man jetzt noch davon ausgeht, dass nur ca 5% der registrierten Benutzer eines Systems gleichzeitig aktiv sind, kann man mit einem LAMP-Stack 6000000 Benutzer bedienen. Und 300000 gleichzeitig aktive, sowie 6000000 registrierte Benutzer sind wohl für einen Großteil der Enterprise-Anwendungen ausreichend. (Siehe https://www.ufried.com/blog/microservices_fallacy_2_scalability/)
Viel wichtiger bei Betrachtung der nicht-funktionalen Anforderungen ist der zweite Punkt, den Michi anspricht. Wenn sich die nicht-funktionalen Anforderungen für unterschiedliche Teile der Anwendung unterscheiden, dann ist das ein starkes Indiz, das für eine Umsetzung als Microservice-Architektur spricht. Als Beispiel wird hier genannt, dass die Kunden-App für den Bestellprozess hohen Traffic verarbeiten muss, wohingegen die Routenplanung Hardware mit hoher CPU-Leistung für die Optimierung benötigt. Ein weiteres Beispiel kann sein, dass ein Teil der Anwendung (z. B. die Produktpflege) besonders sicher sein muss und daher durch eine eigene Firewall geschützt ist – dafür hat dieser Teil der Anwendung aber nur wenige Nutzer und muss deshalb nicht so hohen Traffic unterstützen wie die Kunden-App zum Bestellprozess. Bei einer Microservice-Architektur müssen also keine Kompromisse bei der Wahl der Ablaufumgebung und der Hardware gemacht werden.
Bei einem Monolithen hingegen muss dieser Kompromiss eingegangen werden, denn die komplette Anwendung läuft als einzelner Prozess auf der gleichen Hardware und im gleichen Netzwerk.
Verfügbarkeit
Michi: „Meine Architektur ist robust. Wenn ein Service ausfällt, zum Beispiel wegen einem Out-of-memory Fehler, können die anderen Services trotzdem weiter funktionieren – gegebenenfalls mit Einschränkung.“
Moni: „Punkt für dich.“
Robustheit und Verfügbarkeit sind zwei weitere Merkmale, für die bei einer Microservice-Architektur geworben wird. Prinzipiell ist es auch möglich, dass Microservice-Architekturen robust sind und den Ausfall einzelner Services verkraften können – aber nur, wenn man es richtig macht.
Was bedeutet „richtig machen“?
Zum einen ist bei einer Microservice-Architektur bzw. generell bei verteilten Systemen ein Umdenken notwendig. In der Ausbildung und im Studium lernen Entwickler:innen oft: „wenn x dann y“. In verteilten Systemen gilt folgende Abwandlung: „wenn x dann vielleicht y“. Und dieses kleine Wort vielleicht verändert einfach alles: In verteilten Systemen kommt es zu ganz neuen Spielarten von Fehlern: Das Netzwerk kann Aussetzer haben, Services können keine, die falsche oder eine bösartige Antwort schicken oder Requests können in einen Timeout laufen. Das kann zum Beispiel dazu führen, dass Anfragen zwischen Services verloren gehen, unvollständig sind oder doppelt ankommen.
Um dem entgegenzuwirken und tatsächlich für Robustheit und Verfügbarkeit zu sorgen, sollte man jeden Aufruf über das Netzwerk (und damit jede Kommunikation über die eigenen Servicegrenzen hinweg) als Sollbruchstellen ansehen. Was meinen wir damit? Zunächst sollten Aufrufe über Servicegrenzen hinweg so wenig wie möglich vorkommen. Viele Aufrufe von Schnittstellen aus dem eigenen Service heraus ist ein Indiz für eine hohe Kopplung an andere Microservices und damit für einen schlechten Schnitt der Services. Sollten trotzdem Aufrufe notwendig sein, gibt es für diese Sollbruchstellen sogenannte Resilience-Pattern. Beispiele dafür sind:
- Daten-Replikation: Die Anwendung benutzt replizierte Daten. Dadurch muss kein anderer Service aufgerufen werden. Es kann aber passieren, dass die replizierten Daten nicht mehr aktuell sind. An dieser Stelle greift nämlich das CAP-Theorem, das wir später in der Artikelserie behandeln werden.
- Retries: Die Anwendung ruft nach einer fehlerhaften Antwort den aufgerufenen Service erneut auf. Das kann dazu führen, dass der kaputte Service noch weiter unter Last gesetzt wird und noch kaputter wird.
- Circuit-Breaker: Zwischen den Aufruf wird eine Sicherung eingebaut. Wenn der aufgerufene Service fehlerhafte Antworten liefert, fällt die Sicherung & weitere Aufrufe schlagen direkt fehl, ohne dass der kaputte Service weiter aufgerufen wird. Nach einer gewissen Zeit werden wieder Anfragen an den aufgerufenen Service weitergeleitet. Sollten diese wieder fehlschlagen, fällt die Sicherung wieder. Wenn sie erfolgreich sind, werden wieder alle Aufrufe zum aufgerufenen Service geleitet. Das kann dazu führen, dass Aufrufe fehlschlagen, obwohl der aufgerufene Service schon wieder lauffähig ist und korrekte Antworten liefern würde.
Aber all diese Pattern gibt es nicht geschenkt, sie müssen bei der Programmierung der Schnittstellenaufrufe mitberücksichtigt und implementiert werden. Nur so wird eine Microservice-Architektur hoch verfügbar und robust und kann gegebenenfalls trotzdem weiterarbeiten, auch wenn einzelne Services nicht mehr verfügbar sind.