Architekturprinzipien

Der Begriff „Architektur“ bzw. „Software-Architekt“ hat in der Softwareentwicklung nicht den besten Ruf. Man denkt unwillkürlich an umfangreiche Konzepte, fehlenden Pragmatismus, Elfenbeintürme, Architektur-Reviews, merkwürdige UML-Diagramme voller Halbwahrheiten, „upfront design“, usw. usf. So ganz ohne Leitlinien geht es aber natürlich auch nicht…
Gerade bei einer Neuentwicklung auf der grünen Wiese hat man also eine faire Chance, es in diese oder jene Richtung komplett zu versauen. In Lhotse haben wir uns im Laufe des Projektes auf folgende Prinzipien geeinigt:

  1. Makro-Architektur:
    1. Vertikaler Systemschnitt
    2. Shared Nothing
    3. RESTful Architecture
    4. Zentrale Verantwortung für Daten und Datenversorungsprozesse
  2. Mikro-Architektur
    1. Buy when not core
    2. Gemeinsame Basistechnologien

1. Makro-Architektur

Makroarchitektur
Bei der Makro-Architektur geht es um die „Architektur im Großen“: Welche Teilsysteme gibt es, wie arbeiten sie zusammen und an welche Prinzipien müssen sich alle Teilsysteme und Teams halten.

1.A) Vertikaler Systemschnitt

Eine „klassische“ Architektur teilt sich in Schichten und Module:

Schichtenarchitektur

Häufig spiegelt sich dass dann auch in der Team-Struktur wieder: Ein oder wenige Teams pro Layer. Diese Aufteilung ist nicht ganz unproblematisch:

  • Für viele Anforderungen muss die Arbeit mehrere Teams eng koordiniert werden weil mehrere Schichten betroffen sind.
  • Die Anzahl der Teams ist in der Regel schlecht skalierbar. Die Anzahl der Entwickler pro Team sowieso.
  • Engpässe in einzelnen, zentralen Teams werden zu einem Problem für andere Teams.
  • Die Code-Base wird sehr schnell sehr groß. Niemand durchblickt mehr das Gesamtsystem.
  • Die Teams sind durch die gemeinsame Code-Base eng verbunden. Selbst das updaten einer verwendeten Library kann zum Geduldspiel werden.

Man könnte noch lange so weitermachen, aber hier geht es um die Lhotse-Architektur: und da haben wir uns für einen vertikalen Schnitt durch das System entschieden:
vertikale Schnitte
Jedes grüne Kästchen ist eine eigenständige Anwendung mit eigener Datenhaltung und eigenem Frontend. Manche Teams haben mehr als eine „Vertikale“, aber keine Vertikale wird von mehreren Teams entwickelt.

Eigene Datenhaltung bedeutet dabei auch, dass sich Vertikalen keinen Zustand über die selbe Datenbank teilen dürfen. Eine geteilte Datenbank würde wieder eine enge Kopplung zwischen den Systemen erzeugen: sie müssten sich über das Schema der Daten einig sein.
Der interne Aufbau einer Vertikalen liegt vollständig in der Verantwortung des Teams. Insbesondere gibt es keine gemeinsame Code-Base. Das führt zwar gelegentlich zu Parallelentwicklung, dafür müssen aber nicht die abweichenden Bedürfnisse mehrerer Teams in eine gemeinsam genutzte Library gedengelt werden.

1.B) Shared Nothing

Der Begriff „shared nothing“ drückt aus, dass sich die Teilsysteme untereinander, aber auch die einzelnen Instanzen einer geclusterten  Vertikalen, keinen gemeinsamen Zustand in der Anwendung halten. Es gibt also keine In-Memory Caches, über die sich Cluster-Knoten miteinander unterhalten müssen, keine HTTP-Sessions, die zwischen den Instanzen repliziert werden, usw.
Da sich einzelne Instanzen nichts teilen, müssen sie auch nicht miteinander kommunizieren und kein Load-Balancer muss Rücksicht darauf nehmen, welche „Session“ auf welche Knoten geleitet werden muss (das ist u.a. bei Deployments sowie für die Ausfallsicherheit wichtig).
Zustand in jeder Form wird also nur außerhalb des Systems gestattet: Im Browser, der Datenbank, einem Memcache oder in einem HTTP-Cache. Probleme mit der Cache-Coherency werden auf diese Weise vollständig vermieden.

1.C) RESTful Architecture

Natürlich müssen die einzelnen Vertikalen trotz aller Eigenständigkeit miteinander kommunizieren. Außerdem gibt es den Bedarf, für beispielsweise externe Apps oder ähnliches technische Schnittstellen bereitzustellen. Und dann gibt es natürlich auch noch die Integration der Teilsysteme in eine gemeinsame Shop-GUI und die Konfiguration des Shops durch eine Backoffice-Application (aka „Shopoffice“).
Damit alle diese Dinge möglich sind, haben wir uns auf eine REST Architektur geeinigt. REST heißt dabei nicht „wir tauschen XML-Dokumente für HTTP aus und verwenden auch PUT und DELETE“; wir nehmen das etwas ernster:

  • Definierte Resourcen
  • Verwendung definierter Media-Types (falls möglich vorhandene, ansonsten Vendor-Specific)
  • Beachtung der definierten HTTP Verben, Status Codes und Header
  • Das unaussprechliche HATEOAS Prinzip (erinnert mich immer an PCMCIA – People Can’t Memorize Computer-Industries Acronyms), also die Verwendung von Links bzw. „Hyermedia Controls“ in den Ressourcen um die Möglichkeiten der Zustandsübergänge (das ST in REST) navigierbar zu gestalten.
  • Unterstützung von HTTP Caches.

Unter anderem ist dabei ein Open-Source Projekt (http://github.com/otto-de/jsonhome) entstanden, mit dem eine Anwendung ein json-home Dokument veröffentlichen oder auch konsumieren kann.

1.D) Daten und Datenversorung

Da es sich bei der Anwendung um einen Online-Shop handelt, stehen die Chancen gut, dass viele Teilsysteme etwas über Produkte wissen müssen. Wenn sie aber keine gemeinsame Datenbank verwenden, wie werden dann Informationen geteilt? Dafür haben wir zwei Varianten:

  • Schaffung von Redundanzen über Datenversorgungsprozesse.
  • Ad-Hoc Anfragen über REST Schnittstelle

Die Datenversorgung erfolgt dabei stets asynchron im Hintergrund. Kein Kunde soll auf derartige Prozesse warten müssen. Inkonsistenzen zwischen den Systemen werden dabei bewusst in Kauf genommen.
Die Zugriffe zwischen den Systemen erfolgen dabei grundsätzlich über PULL-Mechanismen wie z.B. AtomPub Feeds da sich auf diese Weise die Kopplung zwischen den Systemen reduzieren lässt. Da wir ohnehin mit Inkonsistenzen rechnen müssen, kommt es auch nicht auf ein paar Sekunden Zeitversatz gegenüber einer Aktualisierung per PUSH an.
Sind solche Inkonsistenzen nicht akzeptabel, dürfen Systeme ausnahmsweise auch Ad-Hoc Anfragen an andere Vertikalen stellen; das versuchen wir jedoch zu vermeiden, da es eine enge Kopplung der Systeme mit sich bringt.
Der Kern des Architektur-Prinzips ist jedoch, dass für alle Daten nur eine Vertikale führend ist. Alle anderen Systeme greifen nur über REST-Schnittstellen auf die führende Vertikale zu und halten sich bei Bedarf redundante Daten. Die „Wahrheit“ über ein Datum liegt in der Hand des führenden Systems.

2. Mikro-Architektur

Im Gegensatz zur Makro-Architektur geht es bei der Mikro-Architektur um die „Architektur im Kleinen“, also die der einzelnen Vertikalen:
Mikro-Architektur
Da die Hoheit über die Vertikalen in der Verantwortung der zuständigen Teams liegen, gibt es hier keine übergreifenden Richtlinien. Es ist nicht vorgeschrieben, welche Frameworks verwendet werden müssen oder wie die Struktur der Anwendung auszusehen hat. Nur ein paar flankierende Absprachen:

2.A) Buy when non core

Wir sehen unsere Kernkompetenz nicht darin, beispielsweise eine eigene Datenbank oder eigene Frontend-Frameworks zu entwickeln. Stattdessen bedienen wir uns hier am Markt und kaufen entweder Dinge ein oder verwenden schlicht ein „Produkt“ aus dem Open-Source Umfeld. Was genau als „Core“ definiert ist, liegt dagegen wieder in der Entscheidung des Teams.

2.B) Gemeinsame Basistechnologien

Zur Zeit verwenden alle Teams die MongoDB als DBMS. Im Prinzip könnte eine Vertikale sich auch für eine andere Persistenzlösung entscheiden. Allerdings würde das betriebliche Aufwände nach sich ziehen, weshalb wir solche Änderungen vermeiden wollen.
Es gibt noch einige weniger weitere Dinge wie Tomcat als Servlet-Container oder auch gemeinsam genutzte Monitoring-Werkzeuge, die zur Zeit in allen Teams ähnlich verwendet werden, grundsätzlich aber als Bestandteil der Mikro-Architektur definiert sind. Die Teams könnten sich also gegen solche Grundlagen entscheiden, sind aber dazu angehalten, vorher in den Ring zu steigen und ihr Vorhaben in „großer Runde“ durchzuboxen.
Anfangs haben wir auch noch eine „common“ Library entwickelt und teamübergreifend genutzt. Mittlerweile sind wir jedoch zu dem Schluss gekommen, dass die Nachteile einer solchen Bibliothek (hinterrücks werden wieder Abhängigkeiten zu 3rd-Party Libraries eingeführt) dir Vorteile überwiegen.
Was viel besser funktioniert:

„Wenn es Code gibt, den sich Teams teilen wollen, entwickelt es als Open-Source Projekt, veröffentlicht es auf GitHub und behandelt es wie eine 3rd-Party Dependency“.

Bisher sind auf diese Weise drei GitHub Projekte (http://github.com/otto-de) entstanden:

  • hmac-auth: zwei Libraries (client, server) für die HMAC-Authentication + Authorization beim Zugriff auf REST Ressourcen.
  • jsonhome: Libraries zum publizieren und konsumieren von json-home Dokumenten.
  • wickettester: Unit-Testing für Wicket-basierte Web Applications.

Zwei weitere sind in Vorbereitung:

  • mongo-migrations: Tools für die on-the-fly Migration von MongoDB Dokumenten.
  • job-execution-framework: Ein Framework für die Steuerung, Ausführung und Kontrolle von asynchron laufenden Jobs.