In kleinen Schritten zum Ziel

Microservices aus einem Monolithen extrahieren
Keine Kommentare

Da steht er nun, der seit Jahrzehnten gewachsene Monolith, und wird zunehmend schwieriger zu warten und zu erweitern. Ein undifferenzierter Rewrite wäre zu teuer und keine Garantie, dass es diesmal besser würde. Um den Monolith dennoch zu bändigen, können wir ihn im großen Maßstab einem Refactoring unterziehen, d. h. verhaltenserhaltend, aber strukturverbessernd, überarbeiten. Das geht beispielsweise, indem wir Microservices extrahieren – und das am liebsten in kleinen Schritten.

Typischerweise ist die Unübersichtlichkeit des Monoliths ein großes Problem. Sein schierer Umfang, eine mangelhafte Dokumentation und die Abwesenheit seiner ursprünglichen Entwickler erschweren uns häufig das Verständnis seiner internen Komponenten und ihrer Abhängigkeiten. Vielleicht gibt es Indizien wie Namespaces – aber niemand kann garantieren, dass die Ordnungsprinzipien über alle Entwickler hinweg mit dem gleichen Elan befolgt wurden. Spätestens beim Einsatz schwarzer Magie hilft auch keine statische Codeanalyse mehr und wir leben in ständiger Angst, dass eine Änderung unbeabsichtigte Nebeneffekte haben könnte.

Sobald wir den alten Code verstanden haben, jucken uns wahrscheinlich schon mehrere Refactorings in den Fingern: Hier schnell etwas geradeziehen, jenes nebenher vereinfachen. Aber der Monolith ist komplex, und ehe wir uns versehen, grinst uns ein sehr zotteliges Yak an, das wir zumindest in diesem Moment doch lieber nicht rasieren wollen. Deshalb ist es wichtig, eine Strategie mit vielen Reißleinen zu haben. Sobald wir bemerken, dass wir uns verrannt haben, müssen wir schnell wieder auf den letzten Commit fliehen, in Sicherheit durchatmen und dann noch einmal frisch anfangen können. Aber Moment mal, ist das nicht ein Standardproblem der Softwareentwicklung? Ja, ist es! Und es gibt auch eine Standardlösung: automatisierte Softwaretests.

Automatisierte Blackboxtests

Im besten Fall decken wir mit z. B. in Behat geschriebenen Blackboxtests alle Funktionen des als Microservice zu extrahierenden Features ab. Wir klicken das Feature im Monolith einmal komplett durch und halten unsere Beobachtungen als Testerwartungen fest. So können wir nicht nur später die Funktionalität absichern, sondern lernen auch nach und nach die Details des zu extrahierenden Features kennen. (Kasten: „Datenbankdumps mit slimdump) Beispielsweise können wir HTTP-Statuscode und spezifische Seiteninhalte für die Featurestartseite testen, etwa für eine Log-in-Funktion, Erstellen-, Lesen-, Bearbeiten- und Löschenoperationen, das Abschicken einer Suchmaske und die entsprechende Ergebnisliste. Bei diesem explorativen Ansatz sollten wir berücksichtigen, ob es verschiedene Benutzergruppen (z. B. Administratoren) und damit einhergehende Rechte gibt und für jedes dieser Rechte den Zugriffsschutz testen.

Unit-Tests sind in diesem Kontext weniger relevant. Zu diesem Zeitpunkt wissen wir noch nicht genug über den Monolith bzw. den zu extrahierenden Microservice, als dass wir zu jeder getesteten Unit sagen könnten: „Das ist ihr Kontext, und daher ist sie relevant bzw. irrelevant für uns.“

Tipp: Datenbankdumps mit slimdump

Mit den Blackboxtests können wir uns der Frage annähern, welche Tabellen der Microservice benötigt bzw. welche Datenzeilen wir für unsere Test-Fixtures benötigen. Später werden wir das noch weiter eingrenzen können, aber für den Moment wollen wir unser Wissen schon einmal festhalten. Dazu können wir beispielsweise in slimdump, einem Tool für hochgradig konfigurierbare MySQL-Dumps, eine Konfigurationsdatei anlegen, versionieren und mit unseren Kollegen teilen. Neben der Tabellen- und Datenzeilenauswahl können wir beispielsweise konfigurieren, dass Benutzernamen und E-Mail-Adressen einer Usertabelle nur anonymisiert gedumpt werden, oder dass wir aus Performancegründen in jener Tabelle nur zehn Prozent der Datensätze und keine BLOBs dumpen wollen.

Grüne Wiese oder Klon?

Wie starten wir konkret mit dem Microservice? Bei Null auf der grünen Wiese oder als Klon des Monoliths, von dem wir dann alles wegschneiden, was nicht zum Microservice gehört? Für beide Varianten gibt es gute Gründe, aber für mich reduziert sich die Entscheidung auf die Abwägung dieser drei wesentlichen Kriterien:

  1. Die Menge der Einschränkungen: Auf der grünen Wiese starten wir mit minimalen Einschränkungen, im Klon nehmen wir zunächst dessen komplette technische Welt mit.
  2. Die Nutzung alter Metadaten: Meiner Erfahrung nach ist insbesondere die Commit Message History oftmals die einzige Chance, eine Stelle mit besonders verrücktem Code zu verstehen – insbesondere, wenn durch eine Ticketnummer der Kontext der letzten Codeänderungen deutlich wird.
  3. Die Latenz bis zur Liveschaltung des Microservice: Starten wir unseren Microservice auf der grünen Wiese, haben wir eine sehr hohe Latenzzeit, bis er live geschaltet werden kann; er muss erst von Grund auf entwickelt werden. Wenn wir dagegen den Microservice als Klon des Monolithen erstellen und auf einem eigenen Host betreiben, ist er prinzipiell sofort live übertragbar. Wir richten einfach irgendeinen Proxy (z. B. Varnish oder mit Apache Rewrite-Rules) vor dem Originalmonolith ein, der Requests an den Microservice an dessen Host und alle anderen Requests wie gehabt an den Monolithhost leitet. Vielleicht müssen wir uns noch um Details bezüglich Cookies, Sessions und URL-Rewriting kümmern – aber das bedeutet immer noch erheblich weniger Latenz als die komplette Neuentwicklung auf der grünen Wiese.

In meiner Erfahrung überwiegen die Argumente für den Start mit dem Klon. Ich vermute, dieser Weg ist im Allgemeinen auch ökonomischer, denn so viel Spaß die grüne Wiese auch bereiten mag, sie scheint mir nur ein Euphemismus für einen teilweisen Rewrite zu sein, der Klon dagegen die Basis für ein Refactoring. (Kasten: „Lauffähigen Monolithen behalten“)

International PHP Conference 2018

Getting Started with PHPUnit

by Sebastian Bergmann (thePHP.cc)

Squash bugs with static analysis

by Dave Liddament (Lamp Bristol)

API Summit 2018

From Bad to Good – OpenID Connect/OAuth

mit Daniel Wagner (VERBUND) und Anton Kalcik (business.software.engineering)

Wahrscheinlich gibt es auch Projekte mit besonderen Umständen, in denen die grüne Wiese die deutlich bessere Entscheidung ist – an solchen habe ich aber noch nicht gearbeitet. Daher behandelt der weitere Verlauf des Artikels den Klonweg.

Tipp: Lauffähigen Monolith behalten

Falls wir den Monolith bereits installiert haben, sollten wir ihn tunlichst bis zur Liveschaltung des Microservice behalten, denn wir werden im Laufe des Projekts anhand von Heuristiken Entscheidungen treffen, die sich erst Tage später als falsch herausstellen können. Vielleicht schneiden wir zu viel Code weg oder vereinfachen ihn zu stark und stellen erst im Nachhinein fest, dass uns ein Test fehlte, der genau das angezeigt hätte. In solchen Momenten ist es äußerst praktisch, schnell in einer lauffähigen Version des Monoliths nachsehen zu können, wie eine bestimmte Verarbeitung konkret ablief.

Erkennung ungenutzter Ressourcen

Wenn wir unseren künftigen Microservice als Klon des Monoliths aufgesetzt haben, stellt sich die Frage: Wie erkennen wir dann die ungenutzten Ressourcen, die wir wegschneiden müssen, damit nur der Microservice übrig bleibt? Allgemein gilt die folgende Gleichung: „Ungenutzte Ressourcen = alle Ressourcen – genutzte Ressourcen.“

Alle Ressourcen einer Art stehen typischerweise bereits als Liste bereit (z. B. bei Dateien mit ls), die genutzten Ressourcen ermitteln wir mithilfe unserer Blackboxtests. Dazu müssen wir nur eine passende Form des Coverage Loggings aktivieren, die Tests ausführen und dann die Coverage in ein aussagekräftiges Format bringen. Schließlich bilden wir deren Differenz und erhalten damit die ungenutzten Ressourcen.

Unsere Tests haben also eine Doppelrolle: Erstens sichern wir mit ihnen die Korrektheit des Codes und zweitens verwenden wir ihre Coverage zur Ermittlung der ungenutzten Ressourcen. Schauen wir uns nun im Detail an, wie die Ermittlung der genutzten Ressourcen bei verschiedenen Ressourcenarten funktioniert.

Genutzte PHP-Dateien

Die meisten PHP-Frameworks verarbeiten Requests über einen Frontcontroller. Darin können wir uns leicht einhaken und z. B. mit Xdebug die Code Coverage loggen und die Pfade der genutzten Dateien in einer Datei used-files.txt ausgeben lassen (Listing 1).

<?php
// Coverage sammeln lassen
xdebug_start_code_coverage();
// Original Front-Controller
$app = new App();
$app→handle($_REQUEST);
// Pfade genutzter Dateien schreiben
$outFile = fopen('used-files.txt', 'a');
fwrite(
  $outFile,
  implode(PHP_EOL, array_keys(xdebug_get_code_coverage()))
);
fclose($outFile);

Wir können dafür beispielsweise auch Sysdig einsetzen, ein Tool zur Überwachung und Analyse von System Calls und Linux Kernel Events.Dessen großer Vorteil ist, dass wir damit nicht nur verwendete PHP-Dateien erfassen, sondern alle geöffneten Dateien – also beispielsweise auch Konfigurationsdateien und View Templates. Der größte Nachteil ist aber, dass der benötigte Umfang nur auf Linux verfügbar ist.

Genutzte Composer-Pakete und MySQL-Tabellen

Wenn wir die Dateien, die in einem Unterverzeichnis des vendor-Verzeichnisses des Composers liegen, aus der Datei used-files.txt aus dem vorigen Abschnitt auf die Dateien filtern, können wir aus ihren Pfaden unmittelbar die genutzten Composer-Pakete ablesen.
Ein Coverage Logging ist leicht zu aktivieren, z. B. mit folgenden SQL Statements:

SET global general_log = 1;
SET global log_output = 'table';

Führen wir nun unsere Tests aus, werden die SQL Queries in der Tabelle mysql.general_log geloggt (die wir deshalb vorher vermutlich mit TRUNCATE löschen möchten). Aus diesen Abfragen können wir die Namen der genutzten Tabellen extrahieren. Das ist händisch allerdings schnell zu mühsam. Zum einen werden es typischerweise sehr viele Queries sein, zum anderen müssten wir bei jedem Query genau hinschauen, an welchen Stellen Tabellennamen vorkommen können; etwa kommagetrennt in der FROM-Klausel, in der JOIN-Klausel und in Subqueries.

Genutzte Frontend-Assets

Die Pfade zu genutzten Frontend-Assets wie Bildern, Schriftarten, JavaScript- und CSS-Dateien finden wir als Hits in den Webserver-Access-Logs. Mit einem regulären Ausdruck können wir sie herausfiltern. Für das Apache Standard-Access-Log-Format ist das z.B. #“(?:get|post) ([a-z0-9\_\-\.\/]*)#i. Doch dabei gibt es einige Schwierigkeiten:

  • Assetsdownload: Das Standard-Behat-Set-up verwendet Goutte als Webbrowser, der keine Bilder, kein JavaScript und kein CSS runterlädt und ausführt. Das heißt, diese Hits fehlen im Logfile. Als Lösung können in Behat aber auch andere Browser bzw. Browsertreiber eingebunden werden, mittels Selenium auch Firefox, Chrome oder sogar eine Armada von Browser-Stack.
  • Konkatenationen: Jahrelang haben wir die Performance von Webanwendungen verbessert, indem wir JavaScript und CSS in wenigen Dateien konkateniert haben, um so die Anzahl der TCP-Verbindungen an unseren Server zu senken. Dieser Ansatz ist mit HTTP/2 überholt, wird aber noch oft in Legacy-Monolithen zu finden sein. Dann ist eine Aussage wie „screen.css und app.js werden genutzt“ nur wenig hilfreich. Hier könnte es am einfachsten sein, die Konkatenation auszuschalten und die Quelldateien direkt im HTML-Code einzubetten, sofern noch leicht herauszufinden ist, welche Datei auf welche Seite gehört. Falls nicht, könnte die Coverage auf Zeilenebene innerhalb der Dateien in Verbindung mit Sourcemaps ausgewertet werden. Die zeilenbasierte Coverage stellt wiederum ein eigenes Problem dar.
  • Automatisierte Zeilen-Coverage in JavaScript und CSS: Für JavaScript gibt es eine Vielzahl von Coverage Logging Tools wie Istanbul, JSCover und Blanket.js. Diese können an JS Test Runner wie Karma oder Jasmin angebunden werden. Zusammen mit den eigentlichen JavaScript-Tests kommt hier möglicherweise einiges an Aufwand hinzu.

Bei CSS ist die Lage noch schwieriger, aber immerhin gerade in Bewegung; beispielsweise hat Chrome seit Version 59 ein eigenes Panel für die CSS Coverage. Die Ermittlung der Coverage funktioniert grob so, dass für alle Selektoren in den geladenen CSS-Dateien geprüft wird, ob sie für das geladene Dokument zutreffen. Falls ja, werden sie und die damit verbundenen Statements als benutzt markiert. Das ist zwar nicht perfekt, scheint aber eine brauchbare Heuristik zu sein. Leider sind weder Ein- noch Ausgabe für diesen Prozess leicht automatisierbar. Es bleibt zu hoffen, dass entsprechende Methoden mittelfristig in dem Puppeteer API ergänzt werden.

Wer diesen Prozess unbedingt jetzt schon automatisieren will, kann sich z. B. mit dem Firefox-Plug-in Dust-Me Selectors behelfen. Dort kann eine Sitemap eingegeben und die resultierende Coverage auf Datei- und Zeilenebene als JSON exportiert werden.

Automatisierung mit dem zauberlehrling

Der zauberlehrling ist ein Open-Source-Tool, das bei der Extraktion von Microservices unterstützen soll. Insbesondere automatisiert es einige Schritte zur Erkennung ungenutzter Ressourcen:

  • bin/console show-unused-php-files –pathToInspect –pathToOutput –pathToBlacklist usedFiles zeigt die ungenutzten PHP-Dateien an. Als Eingabe dient die weiter oben erstellte used-files.txt. Außerdem können der zu untersuchende Pfad auf dem Dateisystem, eine Ausgabedatei und eine Blacklist konfiguriert werden, um z. B. Temp-Verzeichnisse auszuschließen oder solche, von denen bekannt ist, dass sie nicht von den Blackboxtests abgedeckt werden (z. B. Pfade für Unit-Tests).
  • bin/console show-unused-composer-packages –vendorDir composerJson usedFiles zeigt die vermeintlich ungenutzten Composer-Pakete. Die Qualität der Aussage korreliert unmittelbar mit dem Inhalt der usedFiles-Datei. Für den zauberlehrling gilt ein Paket als genutzt, wenn mindestens eine Datei darin genutzt wird. Enthält die usedFiles-Datei beispielsweise nur mit Xdebug ermittelte PHP-Dateien, werden Composer-Pakete, die ausschließlich aus View-Templates oder Konfiguration bestehen, niemals als genutzt erkannt und immer als vermeintlich ungenutzt ausgegeben werden. Mit sysdig erstellte usedFiles-Dateien sind daher hier vorteilhafter.
  • bin/console show-unused-mysql-tables ermittelt mit einem SQL-Parser aus der MySQL-Logtabelle die genutzten Tabellen, bildet die Differenz zu allen und zeigt die vermeintlich ungenutzten Tabellen an.
  • bin/console show-unused-public-assets –regExpToFindFile –pathToOutput –pathToBlacklist pathToPublic pathToLogFile zeigt die vermeintlich ungenutzten Frontend-Assets, nimmt die Pfade des Public-Verzeichnisses und Access-Logs als Eingabe und kann mit dem regulären Ausdruck zur Erkennung der Dateipfade im Access-Log, der Ausgabedatei und einer Blacklist (wie bei den ungenutzten PHP-Dateien) konfiguriert werden.

Die Automatisierung durch den zauberlehrling ermöglicht das Arbeiten in kurzen Entwicklungszyklen. Nach einem initialen Coverage-Durchlauf können diese wie folgt aussehen:

  1. Ungenutzte Ressourcen löschen
  2. Tests ausführen (der Geschwindigkeit halber ohne Coverage)
  3. ggf. gelöschte Ressourcen wiederherstellen, Code oder Tests fixen
  4. Comitten
  5. Zurück zu 1 oder Abbruch

Sind die erkannten ungenutzten Ressourcen gelöscht, sollte wieder ein Testlauf mit Code Coverage durchgeführt werden. Es lohnt sich auch ein Blick auf die Liste der genutzten Dateien – vielleicht sind hier noch „low hanging fruits“ zu erkennen. Wenn beispielsweise in einem Composer-Paket nur noch wenige Dateien benötigt werden, können wir es vielleicht als Abhängigkeit entfernen. Vielleicht finden wir auch im Kontext unseres Microservice überflüssige Abstraktionen, die wir jetzt vereinfachen können. Anschließend darf man natürlich nicht vergessen, die Tests wieder auszuführen.

PHP Magazin

Entwickler MagazinDieser Artikel ist im PHP Magazin erschienen. Das PHP Magazin deckt ein breites Spektrum an Themen ab, die für die erfolgreiche Webentwicklung unerlässlich sind.

Natürlich können Sie das PHP Magazin über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist das Entwickler Magazin ferner im Abonnement oder als Einzelheft erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -