Wenn ein Team beschließt, aus einem gewachsenen Monolithen Stück für Stück etwas herauszulösen, ist die erste Frage fast immer die falsche. Sie lautet: Wie schneiden wir? Fachliche Grenzen, Domänen, Kontexte, es gibt dazu eine Menge guter Literatur.
Die Frage, die zuerst beantwortet werden muss, ist eine andere und viel praktischere: Welcher Teil kommt als Erster raus? Denn der erste Schnitt entscheidet, ob es einen zweiten gibt. Geht er gut, ist das Vorhaben begründet und das Team hat das Verfahren gelernt. Geht er schief, steht in der nächsten Sitzung, dass man es probiert hat und dass es nicht funktioniert.
Dieser Artikel beschreibt, woran ich den ersten Kandidaten erkenne, welche drei Teile fast immer passen und welche drei man nie zuerst anfasst. Wie die Integration danach technisch aussieht, habe ich an anderer Stelle beschrieben; hier geht es um die Auswahl davor.
Vier Kriterien für den ersten Kandidaten
Ein guter erster Kandidat erfüllt vier Bedingungen. Keine davon handelt von sauberer Fachlichkeit, und das ist Absicht: Beim ersten Mal geht es um das Verfahren, nicht um die Architektur.
Erstens: Er hat eine Last, die man sieht. Der Teil verbraucht spürbar Rechenzeit, Speicher oder Wartezeit. Damit ist der Erfolg messbar, ohne dass jemand an eine Verbesserung glauben muss, und der Nutzen steht in derselben Sitzung, in der die Kosten stehen.
Zweitens: Die Grenze ist schmal. Es gibt wenige Stellen im Monolithen, die diesen Teil aufrufen, und wenige Stellen, an denen er zurückruft. Je schmaler die Grenze, desto kleiner die Schnittstelle, die Sie erfinden müssen, und desto weniger Gelegenheit, sie falsch zu erfinden.
Drittens: Er teilt wenig Zustand. Der Teil liest zwei oder drei Tabellen und schreibt in eine. Wenn er dagegen quer durch das Schema liest, ist er kein Service, sondern eine Sicht auf den Monolithen, und das Herauslösen verschiebt das Problem nur über das Netz.
Viertens: Ein Fehler tut weh, aber nicht am Umsatz. Der erste Service wird Fehler haben, und das gehört dazu. Ein Teil, dessen Ausfall eine Verzögerung bedeutet, ist dafür geeignet. Ein Teil, dessen Ausfall eine Bestellung verhindert, nicht.
Wo die vier Kriterien in Konflikt geraten, gewinnt das zweite. Eine schmale Grenze rettet einen mittelmäßigen Kandidaten; eine breite ruiniert den besten.
Die Grenze messen, statt sie zu schätzen
Ob eine Grenze schmal ist, lässt sich nicht im Gespräch klären. Zwei Messungen reichen, und beide dauern eine Stunde.
Wer ruft den Kandidaten auf? In einem PHP-Monolithen ist das eine Suche über die Einstiegspunkte der Klassen, die zum Kandidaten gehören:
# Alle Stellen, die Klassen aus dem Kandidaten-Namensraum benutzen
grep -rn "App\\Bildverarbeitung\\" src/ --include="*.php" \
| grep -v "^src/Bildverarbeitung/" \
| cut -d: -f1 | sort -uFünfzehn Dateien sind eine schmale Grenze. Zweihundert sind keine, und dann ist die Antwort nicht „trotzdem machen", sondern einen anderen Kandidaten suchen.
Welche Tabellen liest und schreibt er? Das steht am zuverlässigsten nicht im Code, sondern im Abfrageprotokoll. Eine Stunde Produktionsverkehr mit eingeschaltetem Protokoll, gefiltert auf die Anfragen, die durch den Kandidaten laufen, liefert eine ehrliche Liste. Der Code liefert eine unvollständige, weil er die Abfragen nicht kennt, die jemand zur Laufzeit zusammensetzt.
Die zweite Messung ist die, die Vorhaben rettet. Ein Teil, der auf dem Papier schön abgegrenzt aussieht und in Wirklichkeit vierzehn Tabellen liest, davon vier schreibend, ist kein erster Kandidat.
Drei Kandidaten, die fast immer passen
In den Systemen, die ich gesehen habe, waren es fast immer dieselben drei Ecken, und zwar aus denselben Gründen.
Bild- oder Dateiverarbeitung. Hochladen, umrechnen, verkleinern, in einen Speicher legen. Das ist rechenintensiv, hat eine schmale Grenze (rein: eine Datei, raus: ein Pfad), teilt fast keinen Zustand, und ein Fehler bedeutet eine Verzögerung. Dazu kommt ein praktischer Vorteil: Diese Arbeit nimmt im Monolithen Arbeiterprozesse in Beschlag, die dann für Anfragen fehlen.
Exporte und Berichte. Ein Vorgang, der viele Daten liest, lange läuft und ein Ergebnis erzeugt. Die Grenze ist schmal, weil es meist einen Aufruf gibt, und der Nutzen ist sofort sichtbar, weil solche Läufe im Monolithen regelmäßig alles andere ausbremsen. Achtung beim dritten Kriterium: Ein Bericht liest naturgemäß breit. Das ist hier in Ordnung, solange er nur liest.
Suche und Zuliefer-Schnittstellen. Alles, was Daten aus dem Monolithen entgegennimmt, aufbereitet und in einer eigenen Form ausliefert. Der Zustand ist abgeleitet und damit jederzeit neu herstellbar, was den Rückweg einfach macht: Wenn es nicht funktioniert, schalten Sie zurück und werfen den Index weg.
Allen dreien gemeinsam ist, dass sie am Rand liegen. Der erste Service wird nicht aus der Mitte geschnitten.
Drei Stellen, die Sie nie zuerst anfassen
Genauso verlässlich sind die Stellen, an denen ein erster Schnitt schiefgeht. Alle drei sind verlockend, weil sie fachlich klar abgegrenzt wirken.
Anmeldung und Rechte. Fachlich sieht das aus wie ein eigener Dienst. In der Praxis hängt jede einzelne Anfrage daran, der Zustand ist geteilt (Sitzungen), und ein Fehler sperrt alle Nutzer gleichzeitig aus. Dazu kommt: Der Nutzen ist unsichtbar, weil sich für niemanden etwas verbessert.
Bestellung, Zahlung, Vertrag. Der Kern des Geschäfts ist der Teil mit den meisten Sonderfällen, den meisten Abhängigkeiten und der geringsten Toleranz für Fehler. Er wird irgendwann geschnitten, aber nicht als Erstes, und nicht bevor das Team das Verfahren an etwas Harmlosem gelernt hat.
Stammdaten. Kunden, Artikel, Preise. Alles liest sie, vieles schreibt sie, und sie sind der geteilte Zustand schlechthin. Ein Stammdatendienst ist ein sinnvolles Ziel und ein katastrophaler Anfang, weil jede Verzögerung und jede Ungenauigkeit sofort überall sichtbar wird.
Wie die Naht zwischen altem und neuem Teil aussieht, ohne dass die alten Begriffe mitwandern, steht in Einen Anti-Corruption Layer zwischen Go und PHP bauen.
Der Schnitt: die Daten bleiben zunächst, wo sie sind
Die häufigste Übertreibung beim ersten Service ist, ihm sofort eine eigene Datenbank zu geben. Das ist das richtige Fernziel und der falsche erste Schritt, weil es zwei schwierige Dinge in einen Schritt legt: einen neuen Dienst und eine Datenmigration.
Für den ersten Schnitt ist es vollkommen in Ordnung, dass der neue Dienst dieselbe Datenbank liest wie der Monolith. Das ist kein sauberer Zielzustand, und es hat einen entscheidenden Vorteil: Der Rückweg bleibt trivial. Wenn der Dienst nicht taugt, schalten Sie den Aufruf im Monolithen zurück, und es gibt nichts zurückzumigrieren.
Zwei Regeln halten diesen Zwischenzustand sauber, und beide gehören ins Protokoll der Entscheidung.
Der neue Dienst schreibt nicht in Tabellen, in die der Monolith auch schreibt. Lesen: ja. Gemeinsames Schreiben erzeugt Fälle, die niemand mehr nachvollziehen kann, sobald zwei Systeme unterschiedliche Annahmen über Sperren haben.
Der Zwischenzustand bekommt ein Enddatum. Nicht, weil er schlecht wäre, sondern weil er sonst dauerhaft wird und die geteilte Datenbank in zwei Jahren zu dem Grund wird, aus dem niemand mehr etwas ändern kann.
Nach vorn wird der Dienst über eine schmale Schicht angesprochen, die das alte Datenmodell nicht in den neuen Code lässt. Das Muster heißt Anti-Corruption Layer und ist der Unterschied zwischen einem neuen Dienst und einem zweiten Stück Altsystem.
Was passiert, wenn die Schnitte entlang der Substantive statt entlang der Transaktionsgrenzen liegen, steht in Der verteilte Monolith.
Woran Sie merken, dass Sie falsch geschnitten haben
Vier Anzeichen, und jedes davon ist ein Grund, zurückzudrehen statt weiterzumachen. Der Rückweg ist an dieser Stelle billig; er wird es nie wieder sein.
Jede Änderung braucht beide Seiten. Wenn seit Wochen keine Anpassung mehr nur im Dienst oder nur im Monolithen möglich war, liegt die Grenze an der falschen Stelle. Statt einer Trennung haben Sie eine zusätzliche Verbindung gebaut.
Die Schnittstelle wächst. Aus drei Aufrufen sind zwölf geworden, und die neuen heißen so, wie interne Methoden heißen. Das ist das Zeichen dafür, dass der Monolith den Dienst als Erweiterung seiner selbst benutzt.
Die Antwortzeit ist schlechter geworden. Ein Aufruf, der vorher ein Methodenaufruf war, ist jetzt ein Netzaufruf. Wenn er in einer Schleife steht, wird aus einer Millisekunde eine Sekunde. Das ist kein Argument gegen Services, sondern eines gegen diesen Schnitt.
Niemand kann sagen, wem ein Fehler gehört. Wenn die erste Frage bei jedem Zwischenfall lautet, ob es am Dienst oder am Monolithen liegt, fehlt die Beobachtbarkeit, oder die Verantwortung ist nicht getrennt. Beides ist reparierbar, und beides gehört repariert, bevor der zweite Dienst entsteht.
Was nach dem ersten Service kommt
Wenn der erste Schnitt hält, ist das Wertvollste daran nicht der Dienst. Es sind die Dinge, die nebenbei entstanden sind und die es vorher nicht gab: ein Weg, zwei Systeme gemeinsam auszurollen, ein Weg, einen Aufruf zurückzuschalten, ein Ort, an dem man sieht, was zwischen ihnen passiert.
Das ist der Grund, warum der erste Kandidat nach Lernbarkeit ausgewählt wird und nicht nach Wichtigkeit. Der zweite Schnitt kostet einen Bruchteil des ersten, und der dritte wird langweilig. Genau dann darf er in die Mitte gehen.
Und wenn sich beim Messen herausstellt, dass es keinen schmalen Kandidaten gibt, ist auch das ein Ergebnis. Dann ist der nächste Schritt nicht ein Service, sondern Ordnung innerhalb des Monolithen: Module mit Grenzen, die man einhalten kann. Das ist unspektakulär, es ist billiger, und es ist die Voraussetzung dafür, dass ein späterer Schnitt überhaupt möglich wird.
Wie ich Go-Services neben einem bestehenden PHP-System aufsetze, steht auf einer eigenen Seite, samt der Frage, wann das die falsche Antwort ist.
Dieser Artikel gehört zu einer Reihe über Systeme, die es schon gibt. Der Rückblick ordnet alle Artikel der Reihe nach Anlass.

