|
Software: Projektmanagement, Architektur, Methoden. |
||
| home blog 30. jul 23. jun 25. mai 24. jan 2009 |
Weblog Archiv 2010ACID in verteilten Systemen, 30.7.2010In verteilten Systemen (a.k.a SOAs) braucht das Thema Datenkonsistenz erschreckend mehr Aufmerksamkeit als in einem System ohne Verteilung, das sich einer ausgereiften Datenbank bedienen kann.Datenkonsistenz? Da erinnern wir uns doch mal schnell an die Informationssysteme Vorlesung und das strapazierte Beispiel von der Überweisung eines Geldbetrags zwischen zwei Konten. Die Überweisung ist nur dann korrekt ausgeführt, wenn beiden Konten jeweils ein Buchungseintrag hinzugefügt wurde, nämlich eine Belastung auf dem Quellkonto und eine Gutschrift auf dem Zielkonto. Führt ein Fehler dazu, dass z.B. die Gutschrift auf dem Zielkonto fehlt, obwohl die Belastung auf dem Quellkonto vorhanden ist, dann ist -- den Daten zufolge -- Geld "verschwunden". Das widerspricht offensichtlich der beabsichtigten Realität, d.h. es gibt eine Inkonsistenz. Und selbst wenn beide Einzelbuchungen erfolgen, kann eine Abfrage, die zeitlich dazwischenkommt, ein -- so nicht reproduzierbares -- inkonsistentes Bild hervorbringen. Datenbanktransaktionen und das ACID Paradigma sind in einem nicht-verteilten System das geeignete Mittel, die typischen vier Anomalien (Lost update, Dirty read, Non-repeatable read und Phantom read) zu verhindern und Fehlern in Teilen mit Rückabwicklung des Ganzen zu begegnen (a.k.a Rollback). In einem verteilten System hingegen haben wir möglichweise mehrere Datenbanken, und wenn sich dann eine aus fachlicher Sicht geschlossene Operation über mehrere Teilsysteme erstreckt, wobei Zugriffe auf die jeweiligen Datenbanken stattfinden, dann reichen uns die so bequemen Datenbanktransaktionen innerhalb der beteiligten Teilsysteme nicht mehr aus. Nehmen wir als Beispiel eine Reisebuchung. Sie besteht aus drei Einzelbuchungen, die bei unterschiedlichen Unternehmen stattfinden, denn wir benötigen für unsere Reise ein Hotel, einen Flug und einen Mietwagen. Wir stellen uns die Reise in einem Webportal zusammen, geben unsere Kreditkartendaten ein, und drücken auf den "Jetzt alles verbindlich buchen!" Knopf. Was wir hier benutzen, ist ein verteiltes System, das aus wenigstens fünf Teilsystemen besteht. Das Webportal muss für die Reise an jedes der drei Buchungssysteme eine Nachricht schicken, und unsere Kreditkarte wird durch eine weitere Nachricht an den Server des Kreditkartenunternehmens belastet. Aus Benutzersicht handelt es sich um eine geschlossene Operation. Mit einem Flug ohne Hotel und Mietwagen ist uns wahrscheinlich nicht geholfen. Und eine Belastung unserer Kreditkarte ohne eine einzige erfolgte Buchung wäre sehr ärgerlich.
![]() Wir erwarten also bestimmte transaktionale Eigenschaften, aber in diesem Beispiel längst nicht alle vier. Wir hätten gerne A(tomicity), also Ganz-oder-Gar-nicht. Für C(onsistency), also dass unsere Kreditkarte nur um den Betrag belastet wird, der auch tatsächlich den erfolgreichen Buchungen entspricht, wären wir auch dankbar. Auf I(solation) können wir dagegen verzichten. Aber D(urability), also dass die beteiligten Systeme unsere Buchungen nicht zwei Stunden später wieder "vergessen" haben, ist uns wieder sehr wichtig. Ein Ansatz, der uns bei der Datenkonsistenzwahrung in verteilten Systemen viel Komfort verspricht, sind verteilte Transaktionen, z.B. realisiert durch ein 2PC Protokoll und überwacht durch einen zentralen Transaktionskoordinator. Um diesen einzusetzen bedarf es aber einiger starker Voraussetzungen:
a) und b) sind meist nicht sicherzustellen, speziell im SOA Umfeld gilt "loose coupling", womit auch zeitliche Entzerrung durch Asynchronität gemeint ist, als Erfolgsfaktor bei der technischen Realisierung. Kurz: in der Praxis müssen wir i.d.R. auf verteilte Transaktionen verzichten. Die Konsequenz ist, dass wir uns selbst wieder um Datenkonsistenz Gedanken machen müssen. Konkret bedeutet das zweierlei: erstens sollten wir solchen Entwurfsgrundsätze folgen, die das generelle Problem mildern, und zweitens müssen wir diejenigen Interaktionen zwischen Teilsystemen identifizieren, die bei Fehlschlägen oder durch fehlende Isolation Inkonsistenzen verursachen können. Langlaufende fachliche Transaktionen sind dafür gute Kandidaten. Für jedes einzelne Inkonsistenzrisiko müssen wir uns dann eine Lösung überlegen. Mühsam, oder? Genau. Aber niemand hat behauptet, dass der Bau verteilter Systeme ein Kinderspiel ist. Der Rest dieses Textes beleuchtet einige praktikable Lösungsideen im Hinblick auf die Eigenschaften des ACID Paradigmas, die wir uns damit ein bißchen zurückerobern können.
Entwurfsgrundsatz: Zustandsminimierung
Entwurfsgrundsatz: Transaktionale Teilsysteme
Entwurfsgrundsatz: Idempotenz
Kompensation Der Koordinator der langlaufenden Transaktion muss also Buch führen, was bereits ausgeführt wurde, und bei einem Fehler mechanisch mit einer (umgedrehten) Folge von Kompensationen reagieren. Da diese Funktionalität gut generalisierbar ist, wird die Ausführung solcher Prozesse gerne an Workflow- oder BPEL-Server übertragen, die zudem auch eine Überwachung von z.B. über Tage laufende Prozesse ermöglichen.
![]() Mittels Kompensation und transaktionalen Teilsystemen erreichen wir so immerhin A(tomicity) und D(urability). C(onsistency) wird dadurch aber noch nicht durchgehend gesichert.
Versionszähler (a.k.a Optimistic Locking) Um Daten vor konkurrierenden Änderungen zu schützen, gibt es zwei Techniken: Sperren und Versionszähler. Gerade in der unvorhersagbaren Welt verteilter Systeme sind Sperren selten sinnvoll. Sie behindern Parallelität (und damit mindern sie Durchsatz), und sie könnten Datensätze unakzeptabel lange blockieren, wenn z.B. der Prozess, der die Sperre angefordert hat, "verstorben" ist. Daher wird statt eines Sperrvermerks ein Versionszähler zu betroffenen Geschäftsobjekten hinzugefügt. Stimmt die Version, die kurz vor Abschluss des Prozesses im Geschäftsobjekt zu finden ist, nicht mehr mit der überein, die zu Beginn des Prozesses gelesen wurde, kann man auf einen Störenfried schließen, der offensichtlich schneller war... und muss den Verlierer dieses Wettlaufs zurückrollen. Dadurch erhalten wir aber ein gutes Stück C(onsistency).
![]()
Schwebeverarbeitung Hier hilft uns eine simple Idee, die mir als "Schwebeverarbeitung" bekannt ist. Der Bearbeitungsprozess erzeugt zunächst eine Kopie des Geschäftsobjekts, die sogenannte "Schwebe". Alle Operationen richten sich an diese Schwebe, und erst der fachliche Abschluss führt zur Ersetzung des bisherigen Originals durch den Stand der Schwebe. Diese Idee bringt uns I(solation) im Bezug auf das betroffene Geschäftsobjekt. Sollte man mehrere Geschäftsobjekte in verschiedenen Teilsystemen bearbeiten müssen, so ist die Schwebeverarbeitung in allen Teilsystemen zu implementieren.
![]() Aus den Lösungsideen geht zum einen hervor, dass wir nicht auf Konsistenz verzichten müssen. Es wird aber auch deutlich: die Anstrengungen, die man für transaktionale Eigenschaften in verteilten Systemen unternehmen muss, können beachtlich sein. Unnötige Verteilung, weil es gerade en-vogue ist, verbietet sich daher. Dort, wo Verteilung unvermeidlich ist, können wir uns meistens genügend ACID erkämpfen, um der Fachlichkeit gerecht zu werden. Mit ziemlicher Sicherheit werden Fachbereichler bzgl. ACID schnell maximale Ansprüche stellen. Dann muss der Softwarearchitekt zusammen mit Fachbereichlern und Projektleitung Kompromisse aushandeln, denn er muss auch Entwicklungskosten, Performanz, Robustheit und Wartbarkeit im Auge behalten.
Große Softwareprojekte und Lernen, 23.6.2010Die halbe Wahrheit ist: ein Softwareprojekt erzeugt ein Softwareprodukt. Die andere Hälfte ist: notwendigerweise entsteht dazu immer eine Projektorganisation, also eine Struktur aus Rollen und Teams und den Prozessen, die von Mitarbeitern ausgeführt werden, damit am Ende ein auslieferfähiges Release entsteht. Große Softwareprojekte sind komplex, und folgerichtig ist auch die Organisation komplex. Außerdem sind Softwareprojekte per Definition einzigartig, jedes Projekt "erfindet" also seine individuelle Projektorganisation, obwohl es Ähnlichkeiten gibt.Einzigartig und Komplex. Das klingt nicht danach, als würden die beteiligten Menschen sofort alles richtig machen können. Und daher ist es keine Überraschung, dass kaum ein Drittel aller Softwareprojekte, die der CHAOS Report 2009 der Standish Group untersucht, die ursprünglichen Ziele trifft. Alle anderen Projekte verpassen die Ziele oder erbringen gar kein nutzbares Ergebnis. Die Ursachen von Schwierigkeiten entstammen der Gemengelage aus Zeit- und Kostendruck, unklarer oder unrealistischer Zielsetzung, sich ändernden Rahmenbedingungen, mangelndem Rückhalt in der durchführenden Organisation oder auch Qualifikationsdefiziten der beteiligten Projektmitarbeiter. Fakt ist: diese Gemengelage existiert schon lange, und wir beklagen sie, aber sie wird auch in der Zukunft immer bleiben. Wir -- als Softwareingenieure -- können mit dem Finger auf andere zeigen und behaupten: unter diesen Bedingungen kann man keinen Erfolg haben. Aber ändern wird sich dadurch nichts. Nehmen wir es daher hin: große Softwareprojekte sind riskant, weil sie in widriger Umgebung starten, schwierig zu organisieren sind, und praktisch niemand der Beteiligten mit diesem individuellen Vorhaben Erfahrung hat, denn ein Projekt ist eine einmalige Angelegenheit. Aber wir sind dieser Situation nicht ausgeliefert, ganz und gar nicht. Sie entspricht grundsätzlich all jenen Situationen, in denen wir lernen müssen. Und da Menschen zumindest in ihren jungen Jahren ständig lernen, besitzen wir alle längst die Schlüsselqualifikation, um große Projekte in den Griff zu kriegen. Wie funktioniert praktisches Lernen? In meinen eigenen Worten etwa so:
Bis hierher habe ich nur eine abstrakte Idee genannt, was auch große Projekte beherrschbar macht. Ich möchte nun mit ein paar Beispielen zeigen, wie konkrete Rückkopplungen aussehen und wirken:
Schätzungen und Ist-Aufwandserfassung
Benutzertests
Continuous Integration
Frühe Lasttests Ich könnte diese Beispielbeschreibungen fortsetzen, doch ich beschränke mich auf ein paar weitere Schlagworte zu Maßnahmen, die uns beim Lernen helfen: Code Reviews, Unittests, Softwaremetriken, Lessons Learned Meetings, Feedbackgespräche unter vier Augen, kurze Releasezyklen, System- und Integrationstests, Security Audits, Architekturdurchstich, Laufzeitmonitoring, ja selbst so etwas Selbstverständliches wie ein Compiler erzeugt laufend Rückkopplungen Richtung Entwickler. Nur wenige Maßnahmen sind an ein Iterationsende gebunden, manches kann als fester Prozessschritt eingeführt, anderes als Einzelaufgabe behandelt oder gar durch geeignete Werkzeuge automatisiert werden. Als Projektleiter habe ich eine Ahnung, wo die Fallstricke liegen können, doch erst die konkreten Rückkopplungen retten mein Team vor dem Unfall. Große Softwareprojekte sind schwierig, aber nicht unbeherrschbar. In jedem einzelnen muss das Team erst lernen, es zu beherrschen. Das Team muss sich also die Frage stellen, wie es effizient lernen kann. Und das wichtigste Mittel dazu sind gezielt gesetzte Rückkopplungen.
Raus aus dem Wolkenkuckucksheim, 25.5.2010Softwarearchitekten besitzen viel Erfahrung mit dem Bau komplexer Softwaresysteme. Das ist der Grund, warum sie in einem Projekt die Verantwortung für das technische große Ganze tragen sollen. Damit haben sie primär zwei Aufgaben: erstens erarbeiten sie im Team mit Analysten und Entwicklern die Architektur, darunter die fachliche und technische Zerlegung des Gesamtsystems sowie kritische technische Lösungen. Und zweitens stellen sie sicher, dass die Ideen und Festlegungen tatsächlich im Code ankommen. Ist Letzteres nicht gewährleistet, kann man sich Ersteres im Grunde sparen.Aber wie schafft der Architekt dies? Welche Mittel hat er dazu? Er kann ein Softwarearchitekturdokument schreiben, z.B. ein 100-seitiges Papier, das die Aufgabenstellung (also Architekturziele, Rahmenbedingungen, architekturrelevante Anforderungen), und die Lösung (z.B. fachliche Zerlegung, technische Schichten, Produkt- und Werkzeuginfrastruktur, Lösungen zu Einzelaspekten) enthält. Das ist aufwändig, doch gerade im Hinblick auf die Aufgabe, die ja die Triebkraft hinter der Wahl bestimmter Lösungen ist, ist es wertvoll. Nun muss nur noch jeder Entwickler das Dokument vollständig lesen, verstehen und während seiner Arbeit stets in allen Aspekten berücksichtigen. Hier hakt es in der Praxis bereits gewaltig. Denn das Dokument kann nur "flach" beschreiben, wie das System aussehen soll. Die Architektur wird nicht plastisch. Es fehlt etwas, das man "anfassen" kann oder sogar beim Ablauf beobachten könnte. Kurz: ein Dokument ist zur Anleitung, wie ein System konkret gebaut werden soll, eine Ergänzung, reicht aber nicht aus. Das nächste, etwas mächtigere Mittel, um das System im Sinne der Architektur zu formen, ist ein lauffähiger Prototyp, der an exemplarischen Anwendungsfällen zeigt, wie das System zu bauen ist. So ein Prototyp ist auch im Rahmen von frühen Lasttests zwecks Prüfung technischer Lösungen nützlich. Und beim Bau des Prototyps entstehen nebenbei Entscheidungen für oder gegen bestimmte Werkzeuge, die wiederum wichtig sind, damit es leicht ist, im Sinne der Architektur zu programmieren. Der Quellcode des Prototypen kann prima für gemeinsame Walkthroughs von Entwicklern und Architekten genutzt werden. Es entsteht eine befruchtende Diskussion, aus der das ganze Team etwas über das System, seine Architektur und wie sie zu verwirklichen ist, lernt. Teams lernen während eines Projekts, wie sie ihre Projektarbeit besser gestalten. Das gilt in gleichem Maße für Architekturentscheidungen, die sich als "gut" oder "verbesserungswürdig" erweisen können. Geänderte Architekturentscheidungen sollten sich möglichst bald auf bereits bestehende Systemteile auswirken, d.h. dass Entwickler von diesen Änderungen "entwicklerfreundlich" informiert werden. Der Architekt benötigt also einen Update-Mechanismus. Mails oder ein persönlicher Hinweis sind zur Erzeugung der Aufmerksamkeit geeignet, der Inhalt, also was warum geändert wurde, ist in einer zentralen Wikiseite mit Einträgen in der Reihenfolge Neu-Alt-NochÄlter schon besser aufgehoben. Entstehen größere Baustellen, so ist statt einer Mail ein Eintrag in ein Ticketingsystem ratsam, der die Anpassung an eine geänderte Entscheidung zum Inhalt hat. So ein Eintrag kann im Rahmen der Iterationsplanung dann als Aufgabenpaket berücksichtigt werden. Wir sind mit den o.g. Mitteln schon soweit, dass Entwickler die Architektur effizient verstehen lernen und Änderungen an dieser mitbekommen. Aber wie können Architekten feststellen, ob die tatsächliche Realisierung mit der Architektur zusammenpasst? Ein wirksames, lange bekanntes Mittel sind Code Reviews. Architekten überprüfen durch das Lesen von Quellcode, ob die Implementierung die Lösungen der Softwarearchitektur berücksichtigt. Ist das an einer Stelle nicht der Fall, so kann das ein guter Anlass für den Architekten sein, mehr über das System und die bisherige Lösungsidee zu lernen. Das verbessert die Architektur und den Architekten. Es kann natürlich auch sein, dass der Entwickler jetzt mehr über die Architektur lernen kann. Das verbessert den Entwickler. Es ensteht zudem ein wertvoller Eindruck von der allgemeinen Codequalität, wie ihn Tools bis heute nicht vermitteln können. Code Reviews haben aber den Nachteil, dass sie in großen Teams nur noch stichprobenartig erfolgen können. Ihre Durchführung skaliert nicht besonders gut. Durch die zusätzliche Verwendung von statischer Codeanalyse lassen sich einige wenige Aspekte vollautomatisch prüfen. Dazu zählt die Begrenzung auf zulässige Abhängigkeiten für Komponenten oder die Verwendung bestimmter Muster. Die statische Codeanalyse wird die Architekten aber auch darauf hinweisen, wo viel oder komplexer Code entsteht. Das erleichtert die Auswahl, was von einem Menschen via Code Review geprüft werden sollte. Reviews und automatische Prüfungen schlagen erst an, wenn ein Missstand besteht, und somit eine Behebung erforderlich ist. Damit wirken sie eigentlich zu spät, um Fehler gar nicht erst entstehen zu lassen. Um Fehler und Inkonsistenzen auszuschließen, setzen Architekten Model-driven software development (MDSD) ein. Gemäß dieses Ansatzes werden bestimmte Aspekte des Systems wie z.B. seine Gesamtstruktur, Formulare und deren Abfolgen, persistente Datenstrukturen und Schnittstellen zu Fremdsystemen in Modellen beschrieben. In diesen Modellen spielen technische Entscheidungen keine Rolle. Erst bei der Erzeugung von Code aus den Modellen werden die fachlichen Modellinhalte mit Technik versehen. Dieser generierte Code kann z.B. mittels Vererbung manuell ergänzt werden. Auf diese Weise sind Fehler ausgeschlossen und späte Änderungen technischer Lösungen erfordern lediglich die Anpassung des Generators sowie die Neugenerierung des Codes. Bei MDSD handelt es sich damit um ein äußerst mächtiges Mittel, um einer Architektur zur konsistenten Implementierung zu verhelfen. Ein Softwareteam besitzt also eine ganze Reihe von Werkzeugen, um einer Architektur Leben einzuhauchen und vor allem die ungewollte Abweichungen von richtigen Lösungsideen zu vermeiden. Es ist neben der Gestaltung des Softwaresystems eine sehr wichtige Aufgabe von Softwarearchitekten, diese Werkzeuge einzuführen und ihre Verwendung zur Gewohnheit des Teams werden zu lassen. Denn nur dann bleibt die Architektur kein Wolkenkuckucksheim.
Risiken und Nebenwirkungen von Wiederverwendung, 24.1.2010Die Situation kommt sicherlich häufiger vor: Ein Softwareprojekt übergibt erfolgreich seine erste Version an den Betrieb, und es dauert nicht lange, da steht bereits das nächste fachlich und technisch ähnliche Projekt vor der Tür. Im letzten Projekt hatte man sich einige Mühe gegeben, eine saubere Infrastruktur bestehend aus Entwicklungsumgebung, Buildverfahren, Generatoren, Laufzeitprodukten, Bibliotheken und kodierten querschnittlichen Lösungen zu etablieren. Und was liegt näher als diese Infrastruktur gleich weiter zu verwenden?Für die Widerverwendung gibt es jetzt zwei Wege. Man kann a) alle Projekte dieselbe Infrastruktur gleichzeitig nutzen lassen, oder b) man erschafft einen dauerhaften Branch, den man dann den Wünschen des neuen Projekts ohne sonderliche Rücksicht anpasst.
Der Reflex des guten Softwareingenieurs ist klar: Keine Duplikation, um keinen Preis! Das würde zu Mehrfachpflege führen, oder schlimmer noch: man lässt zeitweise die Divergenz zu und muss sich dann wieder mit der Konsolidierung mühen. Dieser Reflex ist gesund, aber man sollte eine bewusste Entscheidung treffen, ob man eine gemeinsame Infrastruktur oder verschiedene ähnliche haben möchte, denn die technischen und organisatorischen Konsequenzen, die dem Nachgeben des Reflexes folgen, können teurer und unerfreulicher sein als das divergierende Nebeneinander. Letztlich ist hier eine Entscheidung mit strategischem Weitblick und langfristiger Kosten-Nutzen-Betrachtung nötig. Konsequenzen bei Zusammenhalten:
Wenn die Antwort ist: ja, noch einige, dann sollte man über eine gemeinsame
Infrastruktur nachdenken.
|
||