Suche
Teil 13: Verbesserung der Resilienz

Jeder macht mal Fehler: Wie Sie die Resilienz von Websystemen verbessern

Daniel Takai
Untitled

© IStockphoto.com / Bonezboyz

Es ist einfacher mit einem optimalen Systemzustand zu planen. Aber da Websysteme komplex sind und es immer Störungen geben wird, sollte man pessimistisch sein. Nach Murphys Law wird schief gehen, was schief gehen kann.

Die Resilienz beschreibt, wie gut ein System mit Fehlern umgehen kann. Je mehr Transaktionen ein System unter Störungen erfolgreich verarbeiten kann, desto höher ist seine Resilienz oder Widerstandsfähigkeit. Der Architekt hat also eine sehr schwierige Aufgabe. Auf der einen Seite weiß er, dass die Menge möglicher Störungen unendlich groß ist, aber er weiß auch, dass er noch in diesem Jahrhundert liefern muss, d. h. Zeit und finanzielle Mittel beschränkt sind. Es stellt sich also die Frage, ob man das Vorgehen bei der Fehleranalyse systematisieren kann, um einen bestmöglichen Schutz bei vertretbarem und angemessenem Aufwand sicher zu stellen. Die in diesem Artikel vorgestellten Methoden (Abb. 1) haben die Systematik der Analyse sowie der Verbesserung des Schutzes eines Websystems zum Ziel.

Abb. 1: Prozesse, welche die Resilienz verbessern

Abb. 1: Prozesse, welche die Resilienz verbessern

Ein Faktor Widerstandsfähigkeit eines Websystems zu erhöhen, ist der Schutz vor verteilten Denial-of-Service-Angriffen (DDOS). Die Betriebssysteme und sonstige Software, die heute auf Computern läuft, weisen eine große Anzahl von Sicherheitslücken auf. Dies machen sich neben Regierungen auch kriminelle Organisationen zu nutze, die durch Einbrüche nicht nur an sensible oder persönliche Informationen gelangen, sondern auch an mehr Rechenkapazität und Bandbreite. Ein fremdkontrollierter Rechner heißt Bot (Abkürzung für Robot). Ein Verbund von fremdkontrollierten Rechnern heißt Botnet (Deutsch: Botnetz). Primärer Einsatzzweck von Botnetzen sind verteilte DDOS-Angriffe sowie der Versand von Spam. DDOS-Angriffe werden häufig mit Lösegeldforderungen verbunden: Entweder du zahlst, oder wir machen dein System kaputt. Ein Angriff durch ein Botnetz mit vielen tausend Knoten, kann Millionen an Requests pro Minute auf ein Websystem auslösen und damit seine Kapazität sprengen.

Botnetze wurden zusammen mit dem Internet Relay Chat (IRC) entwickelt, um die Administration der wachsenden Anzahl von Rechnern zu vereinfachen. Der allererste Bot, der ausschließlich für die Administration von IRC-Servern gedacht war, hieß Eggdrop und wurde 1993 von Jeff Fisher entwickelt [1]. Eggdrop ist heute noch im Einsatz. Ein Administrator kann lokal ein Kommando absetzen und dieses wird dann auf allen Servern ausgeführt. Schnell erkannten zwielichtige Personen das kriminelle Potenzial und begannen auf den IRC-Servern eigene Prozesse zu fahren.

Um einen Eindruck von der Gewalt eines solchen Botnetz zu erhalten, lohnt sich ein Blick auf die entdeckten und gesprengten Botnetze der Vergangenheit. 2010 wurde das Mariposa-Botnetz mit 12 Millionen kompromittierten Computern entdeckt. Das in 2011 entdeckte Metulji-Botnetz kontrollierte insgesamt 20 Millionen Rechner. Microsoft hob gemeinsam mit dem Financial Services Information Sharing and Analysis Center (FSISAC) in 2010 das Zeus-Botnetz mit 13 Millionen beteiligten Computern aus. Im März 2013 wurde das Spamhaus-Projekt angegriffen. Dabei erreichten die Angreifer mehr als 300 Gigabit/s.

Die Architektur der Botnetze entwickelte sich seit 1993 ständig weiter, um der Entdeckung zu entgehen und selbst widerstandsfähiger zu werden. Zu Beginn war es eine sogenannte Command-and-Control-Architektur, bei der ein einziger Server alle anderen Rechner fern steuert. Lief die Kommunikation zwischen den Rechnern zu Beginn noch unverschlüsselt und immer über denselben Port, so wurde dies bald durch verschlüsselte Verbindungen über verschiedene Ports umgestellt, um die Entdeckung zu erschweren. Bald wandelte sich die Architektur auch zu einem Peer-to-Peer-Ansatz, sodass es heute keinen Single Point of Failure (SPOF) in den Botnet-Architekturen gibt.

Ein Websystem zu schützen heißt also, es vor DDOS-Angriffen zu verteidigen. Die einzige wirklich effektive Möglichkeit dies zu tun, ist mehr Kapazität als der Angreifer zu haben. Dies ist aber nur selten zu finanzieren und zu realisieren. Verschiedene Firmen bieten heute so genannte Intrusion Detection Systeme (IDS) an, die bekannte DDOS-Angriffe erkennen und zuverlässig abwehren können. Diese bieten also einen guten Schutz vor den bereits bekannten Angriffen. Jedoch ist man nach wie vor neuen Angriffstypen ausgeliefert. Einige CDN-Anbieter offerieren Zusatzpakete oder haben DDOS-Schutz als festen Bestandteil ihres normalen Angebots im Portfolio. Die Verbindung von CDN- und DDOS-Schutz ist für Websysteme durchaus sinnvoll, denn es lassen sich zwei Fliegen mit einer Klappe schlagen: Das System wird geschützt, und Inhalte können weltweit mit niedriger Latenz ausgeliefert werden.

Canary Deployment: Störungen frühzeitig erkennen

Minenarbeiter haben früher Vögel mit in die Tiefe genommen, um vor giftigen Gasen, wie Kohlenmonoxid, frühzeitig durch das Ableben des Vogels gewarnt zu werden. Was Tierschützern heute sauer aufstößt, war damals eine Überlebensstrategie. Der englische Begriff „Canary in a Coalmine“, oder kurz einfach „Canary“, ist heute eine gebräuchliche Metapher für einen Sensor, der ungünstige Bedingungen für ein Softwaresystem frühzeitig erkennt und kommuniziert.

Bei einem Canary Deployment werden Änderungen auf Produktion zunächst auf wenigen Servern ausgespielt, die anschließend nur einen bestimmten Teil des Produktions-Traffics erhalten. Während einer Inkubationsperiode werden nun Störungen auf den aktualisierten Servern beobachtet. Steigt die Anzahl der Störungen im Vergleich zu bereits gemessenen Daten, kann man davon ausgehen, dass das Release nicht in Ordnung ist und kann so unter Umständen eine Katastrophe verhindern. Dieser Prozess heißt auch „Baking the Build“ [2].

Diese Methode ist eng mit den so hab genannten Blue/Green Deployments verwandt, bei denen zwei verschiedene Umgebungen (die blaue und die grüne Umgebung) mit unterschiedlichen Softwareversionen bestückt werden. Produktions-Traffic läuft nur auf die grüne Umgebung. Nach erfolgreicher Installation auf der blauen Umgebung wird durch Konfiguration des Load Balancers auf die grüne Umgebung umgestellt, sodass nun diese produktiv ist [3]. Der Unterschied zwischen Blue/Green und Canary Deployments ist, dass bei Canary Deployments Produktions-Traffic auf die Umgebung geleitet wird, wohingegen bei Blue/Green zunächst traditionell getestet wird, d. h. durch ein dediziertes Team oder Skripte.

Lesen Sie auch: 5 Methoden, wie Sie Websysteme hochverfügbar machen

Voraussetzung für Canary Deplyoments ist neben einer horizontal skalierten Architektur mit vielen Servern auch die Fähigkeit, Fehler entdecken zu können. Das heißt, ein fortgeschrittenes Monitoring im Einsatz zu haben, um entscheiden zu können, ob ein Release fehlerhaft ist oder nicht. Störungen können und sollten auf dem Server oder dem Browser beobachtet werden. Beispielsweise können auf dem Browser JavaScript-Fehler gefangen werden und auf dem Server Datenbankfehler. Ein Release ist genau dann fehlerhaft, wenn sich eine steigende Anzahl von Störungsmeldungen beobachten lässt. Das Buch von Google über Site Reliability Engineering enthält weitere Informationen sowie eine elaborierte Berechnungsmethode für die Entscheidungsgrundlage [2].

Da für funktionierende Canary Deployments viele Voraussetzungen gegeben sein müssen, ist ihre Anwendung heute nur Internetdiensten möglich, die über eine entsprechende Infrastruktur verfügen. Prinzipiell ist der Einsatz sinnvoll, wenn keine Kapazitätsprobleme und wenigstens zwei Server in Betrieb sind sowie Investitionsbereitschaft vorhanden ist. Canary Deployments bieten die folgenden Vorteile:

  • Canary Deployments bieten einen effektiven Schutz vor Fehlern in der Software und erhöhen so die Widerstandsfähigkeit des Systems.
  • Rollbacks auf eine alte Version sind einfach, denn die Benutzer können schnell auf die stabile Version der Anwendung umgeleitet werden. Sobald kein Traffic mehr auf den aktualisierten Servern vorhanden ist, kann man in Ruhe die Logs analysieren.
  • Canary Deployments sind konzeptionell eng verwandt mit A/B-Tests und Feature Toggles. Bei einem A/B-Test werden auch bestimmte Benutzergruppen auf eine bestimmte Softwareversion geleitet. Man kann also auch an dieser Stelle zwei Fliegen mit einer Klappe schlagen.
  • Nicht funktionale Parameter, wie die Kapazität, können auf der Produktionsumgebung durch Canary Deployments gemessen werden.

Methode: Canary Requests

Eine weitere Technik um die Widerstandsfähigkeit eines Websystems zu erhöhen, ist die Prüfung von eingehenden Requests, um den so genannten Query of Death zu verhindern. Eine Query of Death ist ein HTTP Request, der zum Versagen einer Anwendung führt. Beispielsweise ist Stack Overflow abgestürzt, weil ein Benutzer mehr als zwanzigtausend Leerzeichen in einem Codebeispiel an den Server geschickt hat. Stack Overflow benutzt zur Entfernung von White Spaces eine RegEx Library, die in Kombination mit den Pattern für die Leerzeichenerkennung einen Ausfall durch lange Berechnungszeit verursachte. Denn der entsprechende Post wurde sehr oft aufgerufen [6]. Eingehende Requests stellen also eine Gefahr oder einen Angriffsvektor dar.

Um die Gefahr zu verringern, setzt Google einen Service ein, der eingehende Requests untersucht [4]. Ist der Request bekannt, d. h., der Service hat diesen Request schon mal gesehen, so wird dieser normal weiter verarbeitet. Ist der Request jedoch unbekannt, so schickt der Service diesen an einen bestimmten Server, um das Antwortverhalten zu prüfen. Dauert die Antwort lange oder verhält sich der Server ungewöhnlich, so landet der Request auf einer Blacklist. Gleichzeitig wird ein Ticket ausgelöst, damit ein Site Reliability Engineer das Problem untersuchen kann. Ähnlich wie bei den Canary Deployments, ist es auch hier notwendig erkennen zu können, ob sich ein System fehlerhaft verhält oder nicht. Es wird also ein fortgeschrittenes Monitoring benötigt.

Canary Requests sind zweifellos eine teure Methode in mehrfacher Hinsicht (Abb. 2). Zum einen muss jeder eingehende Request untersucht werden. Je nachdem wie umfangreich diese Analyse ausfällt, addiert sich die Latenz zu der Latenz der Anwendung, d. h., unser Service wird langsamer. Außerdem muss der Service entwickelt und betrieben werden, was Entwicklungszeit und Laufzeitumgebungen benötigt. Ein weiteres Problem kann der Cache Warmup nach einem Release sein. Möglicherweise unterliegt der Service plötzlich Anforderungen an die Stabilität von URLs über Releases hinweg. Für Google ist das sicher kein Problem. Hier kann und wird der Service bei vielen unterschiedlichen Anwendungen wieder verwendet und rechnet sich dann. Es ist schade, dass es hierfür heute kein Open-Source-Projekt gibt, das diese Funktion anbietet.

Abb. 2: Wie Canary Requests funktionieren

Abb. 2: Wie Canary Requests funktionieren

Methode: Graceful Degradation

In der Frontend-Entwicklung bedeutet Graceful Degradation, dass sich die User Experience (UX) dem Endgerät anpasst, d. h. modernere Funktionen nur nutzt, wenn sie auch unterstützt werden. Dieses Prinzip lässt sich auf verschiedene Weise auf die Architektur eines Websystems übertragen.

Fällt ein Service aus, so erkennen dies Upstream-Komponenten und liefern für den Ausfall eine qualifizierte Antwort. Fällt beispielsweise ein Availability-Service aus, der über die Verfügbarkeit von Artikeln in einem Lager berichten kann, so kann der Checkout-Prozess eines Onlineshops trotzdem erfolgreich beendet werden, aber selbstverständlich nicht ohne den Benutzer entsprechend zu benachrichtigen, dass das Lieferdatum unbekannt ist.

Eine andere Anwendung von Graceful Degradation ist bei steigender Last eingehende Requests schneller zu bearbeiten, um mehr Transaktionen durchführen zu können. Als Beispiel sei hierfür die Berechnung von Werbung bei Twitter genannt. Eilmeldungen von großem öffentlichen Interesse können bei Twitter plötzlich massiv steigenden Traffic verursachen, der nicht vorhersehbar ist. Hierauf reagieren wiederum Werbetreibende, die die Gunst der Stunde nutzen möchten, um dem Publikum passende Werbung zu präsentieren. Das führt zu einem Anstieg des Bedarfs an Werbeplätzen. Das Ziel von Twitter ist nun, Queries an das Werbesystem schnell zu beantworten und unter den gegebenen Umständen trotzdem die beste Anzeigenqualität zu gewährleisten. Die beste Anzeigenqualität ist definiert als das höchste Ratio von Benutzerinteraktionen (Klicks) und der gesamten Anzahl gelieferter Anzeigen.

Lesen Sie auch: Resilient Microservices – ein Architekturmuster für die Praxis

Schlüsselmetrik ist dabei ein Qualitätsfaktor, über den verschiedene Parameter gesteuert werden können, wie die Auktionstiefe oder die Engagement Rate, also wie wahrscheinlich es ist, dass ein bestimmter Benutzer mit einer bestimmte Anzeige interagiert. Die Engagement Rate ist bei Twitter die teuerste Berechnung. Je mehr Arbeit pro Query investiert wird, desto mehr Umsatz wird generiert. Twitter kann nun über einen einfachen Parameter die Rechenzeit pro Anfrage variieren. Es gibt also ein Kontrollrad, das bestimmt, wie viele Anfragen ein Knoten verarbeiten kann. Je höher der gewünschte Umsatz, desto weniger Anfragen können beantwortet werden. Je mehr Last auf dem System, desto geringer die Qualität der Antwort und desto geringer der Umsatz. Die Einbußen in der Qualität der Antworten können nun in Echtzeit gegen die Kosten der Vergrößerung der Kapazität gerechnet werden. Das ist eine großartige Engineering-Leistung.

Ausfälle künstlich herbeiführen

Bei Google lief man Gefahr, dass Kernservices zu stabil waren, sich dementsprechend andere Systeme zu sehr auf diese verließen und deswegen bei sporadischen Ausfällen des Kernservice nicht widerstandsfähig genug waren. Da diese Services nur selten ausfielen, plante man also kontrollierte Ausfälle, um das versprochene Servicelevel auch erfüllen zu dürfen. Die Ausfallzeiten können für Tests und Verbesserungen der Produktionsumgebung genutzt werden. Damit verhindert man auch, dass ein faktisches SLA plötzlich zu einem vertraglichen SLA wird, nur weil man die Leistung übererfüllt. Zudem zwingt man so die abhängigen Dienste dazu, Ausfälle tatsächlich zu behandeln und mit qualifizierten Fehlern zu reagieren. Das kommt der Gesamtqualität des Systems zu Gute. Ein künstlich herbeigeführter, kontrollierter Ausfall heißt Planned Outage [2].

Antizipation als Erfolgsfaktor

Bei kritischen und hochverfügbaren Systemen besteht der Wunsch nach einer sehr geringen Fehlerrate. In der Architektur bedeutet dies, dass jede Komponente aber auch das Zusammenspiel der Komponenten, problemlos funktionieren sollte. Zudem sollte im Falle einer Störung mit demselben möglichst gelassen und souverän verfahren werden. Um das erreichen zu können, müssen Störungen erkannt werden. Hierfür gibt es drei verschiedene Techniken: Gefahrenanalyse, Fault-Tree-Analyse und die qualifizierte Prozessanalyse.

Im Flugzeugbau wird hierfür die Gefahrenanalyse eingesetzt. Dazu probiert man, alle möglichen Gefahren in einem Katalog zusammenzufassen. Bei einer Gefahrenanalyse werden die möglichen Fehler nach der Größe der Gefahr beurteilt, die sie auslösen können [5]. Beispielsweise stürzt bei einem katastrophalen Fehler das Flugzeug ab. Die Schwere des Fehlers in Kombination mit der Wahrscheinlichkeit seines Auftretens löst Maßnahmen zur Minderung des Risikos aus. Die Gefahrenanalyse funktioniert Top-down und basiert auf Erfahrung. Gefahrenkataloge können wieder verwendet werden, und somit verbessert sich die Analyse ständig. Allerdings besteht die Gefahr, dass bei Änderungen an der Konstruktion Gefahren übersehen werden.

Die Fault-Tree-Analyse ist eine weitere analytische Top-down-Methode, die ausgehend von einem als kritisch eingestuften Systemzustand, den Systemkontext und seine Funktionsweise analysiert, um Störungen aufzudecken, die zu einem Ausfall des Systems führen könnten. Störungen können das Versagen einer Komponente oder der Hardware oder ein menschlicher Fehler sein. Dieses bei der NASA zur Analyse von Komponenten in der Weltraumfahrt eingesetzte Verfahren ist in [6] dokumentiert. Zur Notation werden Elemente der logischen Analyse eingesetzt (and, or oder xor), bei der ein Fault Tree konstruiert wird. Abbildung 3 ist ein Beispiel für eine einfache Fault-Tree-Analyse.

Abb. 3: Eine einfache Fault-Tree-Analyse

Abb. 3: Eine einfache Fault-Tree-Analyse

In der Architektur von Websystemen haben wir vereinfachte Voraussetzungen, die uns bei der Analyse unterstützen können. Ich nenne dieses Vorgehen die qualifizierte Prozessanalyse (QPA). Bei Websystemen liegen Geschäftsfälle vor, die den Einsatz unseres Systems beschreiben. Geschäftsfälle werden durch Geschäftsfunktionen anhand von Regeln erfüllt. Für jeden Anwendungsfall können wir die beteiligten Komponenten analysieren und im Hinblick auf Störungen untersuchen. Nehmen wir als Beispiel die Geschäftsfunktion Check-out, bei der ein Vertrag mit dem Kunden formuliert werden muss. Abbildung 4 zeigt, wie der Prozess mittels BPMN modelliert wird. Bei der QPA wird jeder Schritt des Geschäftsfalls in Hinblick auf seine physischen Abhängigkeiten und dabei möglichen auftretenden Fehlerfälle hin untersucht.

Abb. 4: Check-out-Prozess

Abb. 4: Check-out-Prozess

Die Idee ist, dass wir nicht ignorieren können, wenn etwas nicht funktioniert und wir deswegen darauf vorbereitet sein müssen. Deswegen planen wir die Fehlererfassung systematisch ein, um reagieren zu können. Bei einer Architektur, die auf entfernten Services beruht, bedeutet dies im Wesentlichen das Versagen eines Service verlässlich beobachten zu können. Das vorliegende Beispiel führt den Geschäftsvorfall durch:

  1. Der Kunde wählt Check-out und wird zum Warenkorb geführt.
  2. Der Profile-Service antwortet mit der Liefer- und Rechnungsadresse des Kunden, sowie der präferierten Zahlungsart.
  3. Der Pricing-Service berechnet die Preise aller Artikel im Warenkorb und abhängig von der Lieferadresse auch die anfallenden Steuern.
  4. Der Shipping-Service berechnet anhand der Lieferadresse die Versandkosten und Lieferdauer.
  5. Der Verfügbarkeitsservice gibt an, ab wann die Artikel versendet werden können.
  6. Wenn bei den vorangegangenen Schritten ein schwerer Fehler aufgetreten ist, weil beispielsweise der Profile-Service nicht verfügbar war, wird eine Fehlerseite angezeigt, dass der Einkauf nicht möglich ist.
  7. Bevor es ans Bezahlen geht, wird die Bestellung mit allen Konditionen, sowie Hinweisen bezüglich des Liefertermins angezeigt. Wenn bei den vorangegangenen Schritten ein nicht schwerer Fehler aufgetreten ist, so wird dieser je nach Fehler an dieser Stelle unterschiedlich behandelt. Ein Abbruch des Verkaufs ist jedoch nicht zwingend notwendig, es sei denn der Kunde wünscht dies, beispielsweise weil die Lieferfrist eines Artikels nicht angegeben werden kann, da der Verfügbarkeitsservice nicht erreichbar war.

Für jeden Service, den wir in diesem Prozess befragen, können wir die folgenden Fragen stellen, die Michael Nygard bereits 2007 in seinem Buch gestellt hat [7]:

  1. Was passiert, wenn der Service nicht verfügbar ist?
  2. Was passiert, wenn die Verbindung zum Service während einer Transaktion abbricht?
  3. Was passiert, wenn der Service zu langsam antwortet?
  4. Was passiert, wenn der Service eine falsche Payload liefert?
  5. Was passiert, wenn die Kapazitätsgrenze des Service überschritten wird, weil zu viele Anfragen ankommen?

Interessanterweise schrieb Nygard damals: „I’m getting tired already, and that’s just the beginning of everything that can go wrong. So, the exhaustive brute-force approach is impractical for anything but life-critical systems or Mars rovers.“ Dank Open Source können diese Fälle heute, neun Jahre später, mit einer einfachen Konfiguration abgefangen werden. Die Fälle 1 bis 4 stellen einen Ausfall des Service dar. Mit einem Circuit Breaker lassen sich zumindest die ersten drei Fälle zuverlässig erkennen und als Fehler registrieren. Fall 4 ist leicht zu erkennen, da wir die Payload nicht parsen können, und somit können wir auch hier einen Fehler melden. Fall 5 können wir schließlich per Throttling ausschalten. In jedem Fall aber müssen wir die Fälle unterscheiden können. Dabei ist es möglich, heute auf Open-Source-Komponenten wie Apache ServiceMix und Hystrix zurückzugreifen, die diese Features unterstützen. Abbildung 5 zeigt eine mögliche Architektur auf Basis von ServiceMix.

Abb. 5: Open-Source-Resilienz mit ServiceMix und Hystrix

Abb. 5: Open-Source-Resilienz mit ServiceMix und Hystrix

Qualitätsszenarien als Diskussionsgrundlage

Die folgenden Szenarien können als Vorlage für die Diskussion mit dem Team dienen, um herauszufinden, wie widerstandsfähig das System sein soll:

  • Der Ausfall des gesamten Systems über mehrere Tage während der Vorweihnachtszeit, beispielsweise durch einen Denial-of-Service-Angriff, ist kein Problem, da der wesentliche Umsatz über die Filialen läuft.
  • Kauft ein Kunde einen Artikel, von dem nicht bekannt ist, ob er lieferbar ist oder nicht, soll der Abverkauf trotzdem fortgesetzt werden.
  • Nach einem ungeplanten Ausfall können Betrieb und Entwicklung innerhalb einer Woche ein Post mortem durchführen, um mögliche Verbesserungen zu identifizieren.

Fazit

Bei kritischen oder sensiblen Systemen höre ich öfter den Satz „Failure is not an option“. Dem kann ich mit ganzem Herzen zustimmen, denn tatsächlich ist ein Versagen keine Option, sondern unvermeidlich! Deswegen ist man gut beraten, mittels Reflexion und Analyse, aber auch durch Erhebung valider Anforderungen die für ein System richtigen Maßnahmen rechtzeitig zu identifizieren. Um eine hohe Widerstandsfähigkeit herstellen zu können, sind Investitionen nötig. Die Aufgabe des Architekten ist es, die Anforderungen zu hinterfragen und realisierbare Schritte zu verhandeln, die zum einen dem Auftraggeber mehr Sicherheit gewährleisten, aber auch finanzierbar und angemessen sind.

Lesen Sie auch: Resilient Software Design: Muster für ausfallsichere Architekturen

Geschrieben von
Daniel Takai
Daniel Takai
Daniel Takai ist Technologiemanager bei der Unic AG in Bern. Er ist dort für die Entwicklungsprozesse und Softwarearchitekturen verantwortlich.
Kommentare

Hinterlasse eine Antwort

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