Suche
Einfach mal loslegen

Deployment und Monitoring von Microservices: Ein Erfahrungsbericht

Alexander Heusingfeld

@shutterstock/shpakdm

Beim Deployment von Microservices verschwimmen die Grenzen zwischen Mikro- und Makroarchitektur. Während das Team die Mikroarchitektur für jeden Service individuell entscheiden kann, muss man sich bei Makroarchitekturthemen mit anderen Teams zusammensetzen. Es ist deswegen wichtig zu wissen, welche Entscheidungen ein Team für sich treffen kann und welche nicht. Ein Erfahrungsbericht zeigt, wie der Weg mit Learning by Doing funktionieren kann.

Bei einem Kundenprojekt bot es sich an, die Anforderungen mit drei Microservices umzusetzen, die jeweils über ein kleines REST-API verfügten. Nachdem wir verschiedene Microservices-Frameworks ausprobiert hatten, entschieden wir uns letztlich für Spring Boot. Durch seine Opinionated Presets bietet dieses Framework eine konstant hohe Entwicklungsgeschwindigkeit. Schließlich bekamen wir von unseren Kollegen im Operations-Team die Nachricht, dass sie für unsere Services eine Continuous-Delivery-Pipeline einrichten wollen. Dazu müssten wir uns aber entscheiden, wie denn die Services deployt werden sollen.

Da wir bereits zwei unserer drei so genannten Innovation Tokens für den Microservices-Ansatz und Spring Boot verbraucht hatten, haben wir die Vor- und Nachteile eines Docker-basierten Deployments nicht evaluiert. Bei der Idee hinter Innovation Tokens geht es darum, dass man nicht zu viele Innovationen auf einmal einführt. Einerseits ist bei neuen Technologien nicht sicher, wie stabil sie bereits sind bzw. unter welchen Szenarien sie sich nicht wie spezifizierte verhalten und man zusätzliche Zeit in ihre Stabilisierung investieren muss. Andererseits bringen sie immer Unbekannte mit sich, weil das Team noch keine Erfahrung mit ihnen gesammelt hat und hierfür Zeit aufbringen muss. Die Theorie besagt, wenn man zu viele Innovationen auf einmal einführt, überfordert dies die Menschen im Projekt, weil man die Anzahl der beweglichen und unbekannten Teile soweit erhöht, dass sie den Überblick verlieren. Durch diese Überforderung kommt Unzufriedenheit und zusätzliche Instabilität ins Projekt, was die Produktivität senkt. Stattdessen limitiert man die Anzahl der mit einem Projekt einführbaren Innovationen auf ein Maß, das das Team tragen kann.

Deshalb entschieden wir uns, als Deployment-Artefakt durch das Maven-Plug-in von Spring Boot ein self executable JAR erstellen zu lassen. Für dessen Aufruf schrieben wir dann ein Shellskript, das wir auf den Linux-VMs, die als Zielsysteme geplant waren, zum Starten und Stoppen unserer Services verwenden konnten. Dies funktionierte so gut, dass wir uns mit dem Operations-Team daran machten, den Deployment-Prozess zu automatisieren.

Konfigurationsmanagement: Nur auf den ersten Blick intuitiv

Hier unterstützte uns Spring Boot auf unerwartete Weise, als es um die instanzspezifische Konfiguration unserer Microservices ging: Konfigurationswerte (auch Properties) werden mit Spring Boot in einer application.properties– oder application.yaml-Datei abgelegt, die entweder im JAR, im Dateisystem oder remote via HTTP verfügbar sein muss. Diese Dateien bieten einerseits eine Unterstützung für Profile. Wenn man z. B. die Anwendung mit dem Profil test startet, werden alle Properties aus der application.yaml geladen und anschließend durch die Werte der gleichnamigen Properties überschrieben, die in der Datei application-test.yaml definiert wurden. So kann man umgebungsspezifische Werte in eigenen Dateien verwalten. Andererseits gibt es auch die Möglichkeit, bestimmte Properties beim Start der Anwendung zu überschreiben. Entweder man gibt die Property als Aufrufparameter mit oder man definiert eine Umgebungsvariable mit dem gleichen Namen. Nutzern des Typesafe-Stacks mögen diese Fähigkeiten bekannt vorkommen, denn Typesafe Configuration bietet Ähnliches. Zunächst haben wir die Variante mit Umgebungsvariablen ausprobiert, da sie auch nach Rücksprache mit dem Operations-Team intuitiv zu sein schien. In der Realität führte sie aber zu Missverständnissen und Konfigurationsfehlern. Denn es wurden eben keine Parameter explizit übergeben, sondern implizit die Variablen verwendet. Man musste also beim Starten der Applikation prüfen, ob alle notwendigen Variablen gesetzt sind. Was als notwendig angesehen wurde, hing hierbei allerdings von der Umgebung ab. Zu allem Überfluss war das Überprüfen der Variablen auch nicht intuitiv. Denn während die Parameter dem Properties-Namensmuster wie server.port folgten, mussten die Umgebungsvariablen in Großbuchstaben und mit Unterstrichen statt Punkten gesetzt werden – in unserem Fall SERVER_PORT.

Nach einigen Gesprächen mit anderen Teams entschieden wir uns dann, die application.yaml-Dateien im JAR zu belassen und einige Werte via Aufrufparameter zu überschreiben. Dieser Varianten die umgebungsspezifischen Konfigurationswerte als Aufrufparameter zu übergeben, wurde dann als favorisierte Variante von allen Teams als Makroarchitekturentscheidung festgehalten.

Kleine Experimente werden möglich

Es zeigte sich relativ schnell, dass die Feedback-Loops, also die Zeit vom git push des Developers über den Jenkins-Build bis zur Rückmeldung, ob die Änderung erfolgreich deployt wurde und durch die automatisierten Cucumber-Akzeptanztests gelaufen ist, deutlich kürzer waren als bei unseren anderen größeren Systemen. Eine Änderung an einem Microservice brauchte nur noch ca. sechs Minuten, bis sie zum manuellen Abnahmetest in der Produktion bereitstand. Durch dieses schnelle Feedback kam es immer häufiger zu Situationen, die wir „kleine Experimente“ nannten: Wir konnten Änderungen am Verhalten des Systems schnell ausprobieren, ohne zu lange über die Auswirkungen spekulieren zu müssen. Denn wir konnten sie auch schnell wieder ausbauen. Auch die Einstellung unseres Managementteams wurde durch diese Beschleunigung verändert: Sie bemerkten, dass diese kleinen Änderungen deutlich weniger Risiko bedeuteten als die bisherigen großen Releases mit vielen Änderungen. Also entschieden sie, dass wir noch häufiger möglichst kleine Änderungen vornehmen sollten. Einige davon durften dann sogar ohne manuellen Abnahmetest in Produktion gehen. Denn man wusste ja, dass man Änderungen, die zu Fehler führten, binnen Minuten entweder zurückdrehen oder durch eine weitere Auslieferung beheben konnte.

Die Integration ist ein Knackpunkt

Trotz dieser Erfolgserlebnisse hatten wir immer noch das Zitat eines Kollegen aus dem Operations-Team in den Ohren, der seinerzeit bei der Vorstellung unseres Microservices-Migrationsprojekts meinte „Microservices sind auch nicht besser als das, was wir heute machen. Entwickler ignorieren, dass die Komplexität nicht verschwindet, sondern auf die Integrationsphase der Services verschoben wird. Denn darum müssen sich eben nicht mehr die Entwickler, sondern es muss sich die Betriebsabteilung darum kümmern!“. Wir wollten nun einerseits beweisen, dass Microservices auch im Betrieb viele Vorteile bringen, andererseits interessierte uns, welche Probleme hier wohl auf uns warten würden. Deshalb beschlossen wir, die Integrationsphase bereits während der Featureentwicklung zu starten. Konkret stellten wir direkt nach dem Aufsetzen der Deployment-Pipeline die Konnektivität zwischen den deployten Serviceinstanzen her. Und in der Tat tauchten hier viele Fragen auf, die wir so nicht vorhergesehen hatten.

Da unsere Service-APIs andere Services anbinden, stellten wir uns die Frage: Wie können wir so genannte Deployment-Monolithen verhindern? Deployment-Monolithen sind eine Ansammlung kleiner Services, deren Kohäsion, was APIs und Daten angeht, aber so hoch ist, dass sie nur gemeinsam in Produktion gebracht werden können. Unsere Lösung hierfür waren einerseits abwärtskompatible APIs und Datenstrukturen für unsere eigenen Services und andererseits Consumer-driven Contracts für die APIs anderer Services. Letzteres waren aufgezeichnete Requests an die APIs der anderen Systeme. Diese gaben wir dann den anderen Teams, die sie in die Deployment-Pipeline ihrer Services einbinden konnten. Hierdurch bekam das andere Team direktes Feedback, wenn eine ihrer Änderungen die Abwärtskompatibilität brach, auf die sich einer unserer Services verließ.

Die nächste Frage betraf das Thema SSL Offloading: Wie kommen die entsprechenden SSL-Zertifikate auf die Instanzen, die durch das AWS Autoscaling beliebig erstellt und zerstört werden können? Die Instanzen unserer Services sind auf zwei AWS Availability Zones – vergleichbar mit zwei Rechenzentren – verteilt, um Ausfallsicherheit zu gewährleisten. Wie können wir gewährleisten, dass unsere Microservices, die andere Services aufrufen, diejenigen Instanzen bevorzugen, die in der gleichen Availability Zone deployet sind, um die Latenz gering zu halten? Diesem Problem begegneten wir mit einem entsprechenden Konzept zur Service Discovery. Clients verwendeten in ihren Anfragen an die Service Registry entsprechende Attribute wie die Availability Zone. Die Service Registry sortierte dann die Liste der zurückzugebenden Serviceinstanzen nach diesen Attributen. In unserem Fall nutzten wir hierfür Netflix Eureka. Gleiches lässt sich aber auch mit HashiCorps Consul realisieren.

Aber auch scheinbar trivialere Dinge bereiteten einige Umstände. Als wir beispielsweise die erste Verbindung zwischen den Instanzen zweier Services herstellen wollten, hatten wir zwar den entsprechenden Port in der Firewall freigeschaltet, allerdings kamen die Requests nur bei einer der drei deployten Serviceinstanzen an.

Ohne ein zentrales Logging geht es nicht

Das Troubleshooting hierzu erwies sich als äußerst mühselig, da wir uns immer per SSH auf den jeweiligen Server verbinden mussten, um dort die Logdateien einsehen zu können. Hierbei kam schnell der Wunsch auf, diese Dateien in einem zentralen Logsystem vorhalten und durchsuchen zu können. Da wir aber nicht viel Zeit mit Einrichtung und initialer Konfiguration verlieren wollten, setzten wir auf einem Testsystem kurzerhand einen Docker-basierten ELK-Stack auf. In diesem Docker-Image waren Elasticsearch, Logstash und Kibana bereits vorkonfiguriert enthalten. Auf den Hosts unserer Services installierten wir jeweils eine weitere Logstash-Instanz, die, mit einer entsprechenden Konfiguration, die Logdateien direkt nach Elasticsearch importierte, sodass wir diese in Kibana durchsuchen konnten. Obwohl wir diesen Docker-basierten Stack für die Produktion erstmal ausschlossen, funktionierte er im Testsystem gut und ließ uns so Erfahrung mit diesen Technologien sammeln.

Durch dieses zentralisierte Logmanagement waren wir beim Troubleshooting nun wesentlich schneller. Allerdings stellten wir fest, dass sich bestimmte Fehlerszenarien nur nachvollziehen ließen, wenn wir die Anwendung im Debug-Level laufen ließen. Das Debug-Level aber immer zu aktivieren, würde aber zu viele unnötige Daten erzeugen, welche die Infrastruktur belasteten. Denn wir brauchten es ja nur für bestimmte Requests. Um das Debug-Level nun ohne Konfigurationsanpassung oder Neustart der Services für diese bestimmten Requests aktivieren zu können, implementierten wir ein Konzept zum „per Request Debugging“. Da wir den Request gerne auch in den Logs der anderen Services verfolgen wollten, handelte es sich hierbei um ein serviceübergreifendes Thema, was damit zur Makroarchitektur gehört. Wir beschrieben das Logging-Konzept deshalb im gemeinsamen Confluence Space für alle Teams, damit auch andere Services per Request Debugging implementieren konnten. Dieses Logging-Konzept wurde später auch um ein gemeinsames Verständnis der Semantik von Logleveln und einige andere Punkte erweitert. Denn es hatte sich herausgestellt, dass auch andere Teams an den Logs unserer Services interessiert waren, so wie wir die Logs ihrer Services in Kibana zum Troubleshooting benötigten.

Bei Monitoring und Alarming kommt es aufs Detail an

Die neuen Möglichkeiten, schnelles Feedback auf Änderungen zu erhalten, führten dazu, dass das Management wie bereits erwähnt mutig wurde, und einige Änderungen ohne manuelles Testing in die Produktionsumgebung releast wurden. Man stellte uns die Frage: „Woran könnt ihr erkennen, ob das Deployment der letzten Änderung zu Problemen in Produktion führt?“ Diese Frage lässt sich schnell mit einem „Wir schauen aufs Monitoring“ beantworten. Allerdings liegt die Herausforderung hier im Detail, was genau für welche Art Änderung überwacht werden muss, und vor allem wie.

Beim Blick auf unser Monitoring stellten wir fest, dass das Operations-Team bisher nur Nagios im Einsatz hatte, um die Hostsysteme zu überwachen. Nagios führte so genannte Checks aus, um Systemmetriken wie CPU- und RAM-Auslastung, Netzwerk-I/O und laufende Prozesse einzusammeln, und konnte Alarme per E-Mail verschicken, wenn diese Systemmetriken definierte Grenzwerte überschritten.

Es gab aber keinerlei Überwachung von Applikations- oder Businessmetriken. Applikationsmetriken wären zum Beispiel die Auslastung eines Tomcat-Threadpools und seiner Queue oder die Garbage-Collector-Statistiken. Businessmetriken können das Verhältnis von Warenkorb-Check-outs zu abgeschlossenen Orders, angefangenen zu abgeschlossenen Registrierungen, die Anzahl erfolgreicher Log-ins im Verhältnis zu der Anzahl an Aufrufen der Log-in-Seite oder einfach die durchschnittliche Verweildauer auf der Webseite sein. Um bei den Businessmetriken Anomalien festzustellen, müssen sich Product Owner und Team intensiv Gedanken machen, woran man den Normwert festmacht. Denn Normwerte unterliegen immer Schwankungen. So reicht es nicht, die Anzahl der erfolgreichen Log-ins zu überwachen. Denn diese wird Sonntagmorgens um 03:00 Uhr vermutlich deutlich niedriger sein als Montagabends um 18:00 Uhr.

Neben der Festlegung, welche Metriken wir denn überwachen mussten, blieb für uns noch die Entscheidung offen, wie wir diese Metriken am besten visualisieren und überwachen. Hier schied Nagios relativ schnell aus, denn dafür war es nicht konzipiert. Zum Glück hatten wir uns noch eines unserer drei Innovation-Tokens aufbewahrt, sodass wir uns einige Monitoringstacks und hier vor allem Time Series Databases ansehen konnten. Zunächst evaluierten wir die Time-Series-Datenbank Graphite, die seit Jahren auf dem Markt ist, deren Weiterentwicklung in den letzten Monaten allerdings nur sehr langsam voranging. Im Vergleich dazu erschien uns InfluxDB und der darum entstehende Monitoringstack eine gute Option zu sein. Wir setzten deshalb eine zentrale InfluxDB auf und wählten Grafana für die Visualisierung der Metriken. Auf jedem Host in unserem Testsystem installierten wir Telegraf, das die Systemmetriken sammelte und in die InfluxDB exportierte. In unsere Spring-Boot-Anwendungen bauten wir die Dropwizard Metrics Library ein, die Applikations- und Businessmetriken über einen Exporter ebenfalls an InfluxDB senden konnte. Da unser Operations-Team sehr viel Expertise mit Nagios aufgebaut hatte und es unbedingt weiterhin für das Alarming verwenden wollte, definierten wir mit ihnen zusammen Nagios-Checks, die Metriken über das InfluxDB-HTTPS-API abfragten und beim Überschreiten von Schwellwerten Alarme verschickten. Anschließend führten wir einige Lasttests durch, um die Stabilität dieses Stacks unter der erwarteten Last zu prüfen.

Abb. 1: Screenshots eines Grafana-Beispiel-Dashboards mit verschiedenen Graphtypen

Abb. 1: Screenshots eines Grafana-Beispiel-Dashboards mit verschiedenen Graphtypen

Die Benutzeroberfläche von Grafana überzeugte uns auf Anhieb. Wir konnten verschiedene Dashboards für unsere Stakeholder und deren unterschiedliche Fragestellungen erstellen und diese sowohl untereinander als eine Art Drill-down als auch mit externen Systemen wie Kibana-Dashboards verlinken. So hatte jeder Stakeholder sein eigenes Dashboard (Abb. 1), und wir konnten uns auf unseren Dashboards zunächst einen Überblick verschaffen, in welchen Metriken sich das Nutzerverhalten widerspiegelte und entsprechend in Nagios Alarme auf diese Metriken oder Fehlerraten konfigurieren. Der typische Troubleshooting-Prozess sah dann so aus, dass wir von Nagios einen Alarm per E-Mail bekamen, die einen Link auf das zugehörige Grafana-Dashboard enthielt. Dort verschafften wir uns einen Überblick der Lage und klickten uns bei Bedarf bis in Kibana-Dashboards durch, um dort – im von Grafana weitergereichten Zeitintervall – die Logevents nach Ursachen der Anomalie zu durchsuchen.

Mit dieser Überwachung im Rücken konnten wir nun Fehler nach einem Deployment in den meisten Fällen relativ schnell feststellen. Um das Risiko weiter zu reduzieren und noch schneller reagieren zu können, diskutierten wir die Einführung eines Gatekeeper-Service. Dieser Service erlaubt es, Traffic Shaping vorzunehmen. Der Traffic an unsere Systeme wird dabei in kleinen Schritten (z. B. plus 5 Prozent alle 5 Minuten) auf eine gerade neu deployte Version eines Service umgeleitet. Laufen die Requests an den neuen Service auf einen Fehler oder überschreiten Toleranzgrenzen, wird der umgeleitete Trafficanteil wieder reduziert. Ansonsten wird die Trafficzuleitung erhöht, bis der Service 100 Prozent des Produktionstraffics behandelt. Ist dies für einen Zeitraum von z. B. 30 Minuten stabil, wird die alte Version dieses Service abgeschaltet. Durch diese Art des Traffic Shapings spart man nicht nur die Zeit für Lasttests, sondern auch die manuellen Tests einer Änderung. Diese Tests werden dann von einem gewissen Prozentsatz der Benutzer der Produktionssysteme durchgeführt.

Ein Produkt, das ein solches Featurepaket mitbringen soll, ist fabio von eBay. Unser Team möchte es als Nächstes evaluieren, sobald es wieder Innovation-Tokens zur Verfügung hat.

Abb. 2: Schaubild einer beispielhaften Monitoringinfrastruktur für Microservices

Abb. 2: Schaubild einer beispielhaften Monitoringinfrastruktur für Microservices

Fazit: Microservices-Deployment ist Teamwork

Geht man es richtig an, bringt der Umstieg auf Microservices Vorteile an vielen Stellen für Entwicklung und Betrieb. Den Teams muss die notwendige Unabhängigkeit eingeräumt werden, sodass sie den Anforderungen mit den passenden Antworten begegnen können. Microservices bedürfen aber auch eines gewissen Engagements der Teams. Denn die richtigen Entscheidungen müssen sie im durch die Makro- und Domänenarchitektur vorgegebenen Rahmen selbst treffen und für die Konsequenzen daraus einstehen. Gerade der Blick auf das Deployment der eigenen Services und die daraus entstehenden Fragen können für viele Teams eine neue Perspektive eröffnen. Da Microservices sehr klein sind und eine geringe fachliche Komplexität haben, können Teams sich diese neue Sichtweise Schritt für Schritt erschließen, ohne direkt davon überfordert zu werden.

Setzt man sich schließlich das Ziel, die Deployments der Services vollständig zu automatisieren, muss man sich im Vorfeld genau überlegen, an welchen Kennzeichen man nicht erfolgreiche Deployments und deren Fehlerszenarien erkennt. Diese Fragen werden in vielen Fällen auch den Product Owner und die Businessseite betreffen. Auf die herausgearbeiteten Kennzeichen muss dann das Monitoring angepasst werden. Die notwendige Monitoringinfrastruktur, um automatisierte Überwachung von Microservices-Deployments gewährleisten und den Teams ihre Autonomie bieten zu können, sollte nicht unterschätzt werden. Abbildung 2 zeigt ein Beispiel für die unterschiedlichen Komponenten, die für Logging, Metriken und Alarming neben den normalen, wertschöpfenden Services zusätzlich betrieben werden müssen.

Ein Gatekeeper-Service wie fabio erlaubt es, den Produktionstraffic schrittweise auf neue Services zu leiten. Er kann dabei helfen, das Risiko einer vollautomatisierten Deployment-Pipeline in die Produktionsumgebung weiter zu begrenzen, da neue Features erst von einer ausgewählten Menge an Benutzern getestet werden. Wie weit man damit gehen möchte, ist natürlich jedem selbst überlassen, allerdings ist es aus meiner Sicht beruhigend zu wissen, dass man alle Optionen hat und selbst abwägen kann, welche man wählt.

Aufmacherbild: just do it, handwritten with white chalk on a blackboardvon Shutterstock / Urheberrecht: shpakdm

Geschrieben von
Alexander Heusingfeld
Alexander Heusingfeld
Alexander Heusingfeld ist Senior Consultant bei innoQ. Als Berater, Entwickler und Architekt unterstützt er Kunden vor allem mit seinen langjährigen Kenntnissen von Java- und JVM-basierten Systemen. Meist beschäftigt er sich hier mit dem Design, der Evaluierung und Implementierung von Architekturen für moderne Webanwendungen und Microservices in Softwaremodernisierungsprojekten. Sein aktueller Fokus gilt den Themen Teamorganisation und Softwareevolution. Er trägt gerne zu Open-Source-Projekten bei, spricht bei IT-Konferenzen, Meetups sowie User Groups und bloggt gelegentlich unter http://goldstift.de/.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.