~/blog/2014/07/microservices-auch-was-fuer-kleine.md

Microservices – auch was für kleinere Projekte?

5 min
Titelbild: Microservices – auch was für kleinere Projekte?

Keine Zeit? Hier die Kurzversion in 30 Sekunden.

  • Seit dem Artikel von James Lewis und Martin Fowler im März ist Microservices das Schlagwort der Stunde: viele kleine, unabhängig deploybare Dienste statt eines großen Blocks.
  • Vorteile: getrennte Deployments, freie Technologiewahl, gezielte Skalierung.
  • Kosten: verteiltes System, Netzwerkfehler, Monitoring, Datenkonsistenz, Betrieb, Schnittstellenversionierung.
  • Für kleinere Projekte ist meist ein gut geschnittener Monolith mit klaren Modulen die bessere Wahl.
  • Spring Boot macht kleine Dienste einfach, aber das Werkzeug ersetzt keine Architektur.

Seit ein paar Monaten lese ich überall das Wort Microservices. Ausgelöst hat es der ausführliche Artikel von James Lewis und Martin Fowler im März, der einen Architekturstil beschreibt, den manche große Unternehmen schon länger praktizieren. Die Beschreibung ist sachlich und differenziert, die Diskussion drumherum ist es weniger. Wer gerade einen Spring-Boot-Dienst in zehn Minuten zum Laufen gebracht hat, sieht schnell in jedem Modul den nächsten eigenen Dienst. Ich halte das für eine Verführung und möchte aufschreiben, warum.

Worum es geht

Die Grundidee: Statt einer großen Anwendung, die als Ganzes gebaut und ausgeliefert wird, besteht das System aus vielen kleinen Diensten. Jeder ist um eine fachliche Fähigkeit herum geschnitten, läuft als eigener Prozess, hat meist seine eigene Datenhaltung und spricht mit den anderen über einfache Mittel, häufig HTTP mit JSON. Jeder lässt sich unabhängig von den anderen ausliefern, ersetzen oder neu schreiben.

Das klingt nach dem alten Wunsch nach Modularität, und im Kern ist es das auch. Neu ist die Konsequenz: Die Modulgrenze ist nicht mehr ein Paket oder eine Bibliothek, sondern eine Netzwerkgrenze.

Was dafür spricht

Die Vorteile sind echt. Ein einzelner Dienst lässt sich ausliefern, ohne die ganze Anwendung neu zu verteilen und zu testen. Teams, die an verschiedenen Teilen arbeiten, müssen sich weniger abstimmen. Jeder Dienst darf eine andere Technik nutzen: Der eine läuft auf Java, ein anderer in PHP, ein rechenintensiver vielleicht in C++. Das ist für Umgebungen reizvoll, in denen ohnehin mehrere Sprachen zusammenkommen. Und schließlich kann man gezielt skalieren: Läuft nur ein Teil des Systems unter Last, vervielfacht man nur diesen, nicht alles.

Ich nehme diese Argumente ernst. Es sind aber Argumente, die ab einer gewissen Größe zählen. Eine unabhängige Auslieferung ist wertvoll, wenn viele Menschen sich sonst gegenseitig im Weg stehen. Bei einer Handvoll Entwickler tun sie das meist nicht.

Was es kostet

Die Kosten tauchen in den Vorträgen und Artikeln meist weiter hinten auf. Sobald ein Aufruf über das Netz geht, kann er scheitern, langsam sein oder doppelt ankommen. Was in einem Prozess ein Methodenaufruf war, wird zu einer Anfrage, bei der man Zeitüberschreitungen, Wiederholungen und Ausfälle bedenken muss. Das ist keine Kleinigkeit, es verändert die Art, wie man jede Schnittstelle entwirft.

Hinzu kommt die Datenkonsistenz. Eine Transaktion über mehrere Dienste gibt es nicht mehr einfach so. Hat jeder Dienst seine eigene Datenbank, muss man mit zeitweise unterschiedlichen Ständen leben oder aufwendig dafür sorgen, dass sie sich angleichen. Das ist ein fachliches Problem, kein technisches, und es lässt sich nur mit den Fachleuten klären.

Dann der Betrieb. Aus einer Anwendung werden zwanzig, die gebaut, installiert, überwacht und mit Logs ausgewertet werden wollen. Ohne weitgehende Automatisierung beim Ausliefern und ohne ordentliches Monitoring wird das schnell unübersichtlich. Wenn etwas schiefgeht, will man nachvollziehen, welcher Dienst an welcher Stelle gewartet hat. Dafür braucht man Werkzeuge, die man bei einem Monolithen nie vermisst hat.

Und schließlich die Schnittstellen. Sobald Dienste unabhängig ausgeliefert werden, laufen immer verschiedene Versionen gleichzeitig. Man muss Änderungen so gestalten, dass alte Aufrufer weiter funktionieren, oder Versionen parallel betreiben. Was im Monolithen der Compiler erledigt, muss hier durch Disziplin und Tests ersetzt werden. Eine Umbenennung, die vorher fünf Sekunden dauerte, ist plötzlich ein Projekt.

Mein Eindruck für kleinere Projekte

Ich glaube, dass sich für die meisten kleineren Projekte der Aufwand nicht rechnet. Wer ein überschaubares Team hat und eine Anwendung, die ohne Mühe auf einem Server läuft, bekommt die Kosten sofort und die Vorteile vielleicht irgendwann. Meine Faustregel wäre: Erst einen gut geschnittenen Monolithen bauen, mit klaren Modulen, sauberen Schnittstellen zwischen ihnen und möglichst wenig Abhängigkeiten quer durch den Code. Dann hat man die fachlichen Grenzen im Griff, und wenn wirklich der Tag kommt, an dem ein Teil unabhängig skalieren oder ausgeliefert werden muss, lässt er sich herauslösen.

Der umgekehrte Weg ist schwerer. Wer von Anfang an in Dienste schneidet, muss die Grenzen festlegen, bevor er die Fachlichkeit richtig versteht. Falsch gezogene Grenzen sind in einem Monolithen ärgerlich und in einem verteilten System teuer. Man kann sie nicht mal eben mit dem Refactoring der IDE verschieben.

Dabei ist die Gegenüberstellung gar nicht so scharf, wie sie wirkt. Zwischen „alles in einem Block“ und „zwanzig Dienste“ gibt es viel Platz. Eine Anwendung, die ein, zwei Teile abspaltet, weil sie sich wirklich unterscheiden (etwa eine Berechnung mit anderem Lastprofil oder eine Anbindung, die ein anderer Technologiestack bedient), hat nicht gleich ein Microservice-Problem. Das ist eher ein pragmatisches Aufteilen, wie es auch früher schon vorkam.

Eine Beobachtung noch: Die Beispiele, mit denen für Microservices geworben wird, stammen fast immer von Organisationen mit sehr vielen Entwicklern und sehr viel Last. Das sagt nichts gegen den Stil, aber es sagt etwas darüber, für wen er entstanden ist. Was dort ein Gewinn ist, kann bei einem kleinen Team schlicht eine Last sein.

Das Werkzeug ist nicht die Architektur

Spring Boot und ähnliche Ansätze haben die Schwelle, einen kleinen eigenständigen Dienst zu bauen, deutlich gesenkt. Das finde ich gut. Aber dass etwas einfach geht, heißt nicht, dass man es tun sollte. Ein ausführbares JAR zu erzeugen ist der leichte Teil, der schwere kommt danach: Betrieb, Überwachung, Fehlerbehandlung zwischen den Diensten, durchdachte Schnittstellen. Das nimmt einem kein Framework ab.

Mit dem Stil selbst habe ich kein Problem, er löst reale Probleme in großen Organisationen. Ich mag nur nicht die Haltung, ihn als Gütesiegel für moderne Architektur zu verstehen. Eine Architektur ist gut, wenn sie zu den Problemen passt, die man hat. Bei kleineren Projekten ist das Problem selten die Größe des Deployments. Meist liegt es bei unklaren Zuständigkeiten im Code, und die löst man mit Struktur, nicht mit Netzwerkgrenzen.

$ whoami
Christian Ackermann

Softwareentwickler (C++, C, Java, C#) aus der Oberpfalz, bloggt seit 2001 über Linux, Technik und Kochen.