Werkzeugwahl, Teil 2: Der Stack steht, bevor das Problem klar ist
Viele Teams legen ihre Datenbank fest, bevor sie das Problem kennen. Das Produkt ist dabei selten der Fehler, die falsche Werkzeugkategorie dagegen sehr oft.
(Bild: KI / iX)
- Golo Roden
Im ersten Teil dieser Serie ging es um die Frage, welche Programmiersprache ein Team einsetzen sollte, wenn Agenten den Code schreiben. Meine Antwort war, dass die Sprache an Gewicht verliert: Entscheidend ist nicht mehr, was sich angenehm schreiben lässt, sondern was sich deterministisch prüfen lässt. Am Ende blieb allerdings eine Frage offen, die sich mit diesem Argument nicht erledigt, nämlich die nach der Art des Werkzeugs.
Wie diese Frage in vielen Projekten beantwortet wird, zeigt eine Szene, die mir in unterschiedlichen Varianten immer wieder begegnet. Ein Projekt startet, das erste fachliche Gespräch hat noch nicht stattgefunden, aber der Stack steht bereits fest: diese Sprache im Frontend, jene im Backend, und die Datenbank ist „bei uns eben PostgreSQL“. In anderen Häusern heißt sie Microsoft SQL Server oder MongoDB, aber das Muster ist dasselbe. Niemand hat gefragt, was das Produkt eigentlich speichern soll, und trotzdem ist die Antwort schon gegeben.
Das Produkt ist zweitrangig, die Kategorie nicht
In dieser Szene werden zwei Entscheidungen vermengt, die sehr unterschiedliches Gewicht haben. Die erste betrifft das Produkt innerhalb einer Kategorie: Ob ein Team PostgreSQL oder eine andere relationale Datenbank einsetzt, ist in den meisten Fällen eine Frage von Gewohnheit, Lizenz und vorhandenem Wissen. Das ist das Gegenstück zur Sprachwahl aus dem ersten Teil, und für das Ergebnis ist es ähnlich selten entscheidend.
Die zweite Entscheidung betrifft die Kategorie selbst. Ob Daten in eine relationale oder in eine dokumentenbasierte Datenbank gehören, ob Dateien in einem Objektspeicher liegen sollten oder ob zwei Systeme über eine Message-Queue miteinander sprechen, ist keine Geschmacksfrage. Es ist eine Architekturentscheidung, weil sie bestimmt, welche Probleme ein System von Haus aus löst und welche das Team selbst lösen muss.
Videos by heise
Ein fester Stack trifft diese zweite Entscheidung, bevor irgendjemand die Anforderungen kennt. Er legt nicht nur fest, welches Produkt eingesetzt wird, sondern auch, welche Kategorien überhaupt infrage kommen. Was nicht im Stack steht, wird gar nicht erst erwogen, und zwar nicht, weil jemand es abgewogen und verworfen hätte, sondern weil niemand auf die Idee kommt, danach zu fragen.
Bemerkenswert ist dabei, wie ungleich die Aufmerksamkeit verteilt ist. Über Produkte, Datenbanken, Frameworks und Sprachen streiten Teams oft mit großer Leidenschaft. Die Frage nach der Kategorie, die für das Ergebnis viel schwerer wiegt, kommt in diesen Debatten dagegen kaum vor. Es wird viel über Technologie gesprochen und wenig darüber, welches fachliche Problem sie eigentlich lösen soll.
Wie teuer das werden kann, zeigt sich selten am ersten Tag. Es zeigt sich dort, wo ein Team mit großem Aufwand etwas nachbaut, das eine andere Kategorie von Haus aus mitbringt. Zwei Beispiele dafür begegnen mir besonders häufig.
Daten, die nur wachsen
Das erste Beispiel betrifft Daten, die ausschließlich angehängt werden, zum Beispiel ein Protokoll fachlicher Vorgänge, eine Folge von Messwerten oder ein Buchungsjournal. Sie haben oft die Eigenschaft, dass ein Eintrag, der einmal geschrieben wurde, nie wieder geändert oder gelöscht wird. Neue Einträge kommen hinzu, alte bleiben, wie sie sind. Das ist kein exotischer Sonderfall, sondern in vielen Domänen die natürliche Form der Daten.
Eine relationale Datenbank ist um ein anderes Modell herum gebaut. Ihr Alltag besteht aus Einfügen, Ändern und Löschen, und vieles an ihr ist darauf ausgelegt, dass sich Zeilen verändern. Wer dort Daten ablegt, die nur wachsen, kann das tun, muss aber einiges selbst nachbauen, was die Datenbank für diesen Fall nicht mitbringt.
Das beginnt bei der Unveränderlichkeit. Nichts in einer gewöhnlichen Tabelle hindert eine Anwendung daran, einen Eintrag nachträglich zu ändern oder zu löschen. Das Team muss es also selbst unterbinden, über Berechtigungen, über Trigger oder schlicht über Disziplin, und Disziplin ist erfahrungsgemäß die schwächste dieser Varianten.
Subtiler ist die Reihenfolge. Fortlaufende IDs werden beim Einfügen vergeben, sichtbar werden die Zeilen aber erst, wenn die jeweilige Transaktion abgeschlossen ist. Unter Last kann deshalb ein Eintrag mit einer höheren ID früher sichtbar werden als einer mit einer niedrigeren.
Eine Anwendung, die neue Einträge abholt, indem sie sich die höchste bisher gesehene ID merkt, überspringt in diesem Fall einen Eintrag, und zwar still, ohne Fehlermeldung und nur gelegentlich. Solche Fehler gehören zu der Sorte, die monatelang niemand bemerkt.
Hinzu kommt die Frage, wie andere Teile des Systems von neuen Einträgen erfahren. Eine relationale Datenbank ist dafür gebaut, auf Anfragen zu antworten, nicht dafür, von sich aus Bescheid zu geben. Also fragen die Interessierten in regelmäßigen Abständen nach, ob etwas Neues da ist. Dieses sogenannte Polling erzeugt Last und bringt Verzögerung mit sich.
Für jedes dieser Probleme gibt es Lösungen, und manche Datenbanken bringen dafür eigene Mechanismen mit. Aber jede dieser Lösungen ist Code, den das Team schreibt, testet und betreibt, um eine Eigenschaft zu bekommen, die ein für diese Art von Daten gebautes Werkzeug von Haus aus hat.
Dateien in der Datenbank
Das zweite Beispiel ist noch alltäglicher. Eine Anwendung soll Dokumente, Bilder oder Anhänge speichern, und im Stack ist kein Objektspeicher vorgesehen. Also liegt es nahe, die Dateien eben in der Datenbank abzulegen.
Ein Objektspeicher ist ein Dienst, der Dateien beliebiger Größe unter einem Schlüssel ablegt und wieder ausliefert. Das bekannteste Beispiel ist Amazon S3, dessen Schnittstelle inzwischen auch viele andere Anbieter und selbst betriebene Lösungen sprechen. Er ist genau für diese Aufgabe gebaut, und trotzdem fehlt er in erstaunlich vielen Stacks.
Dateien in der Datenbank abzulegen ist möglich und in einem gewissen Rahmen auch vertretbar; ein paar kleine Dateien richten keinen Schaden an. Mit der Zeit aber wächst die Datenbank um ein Vielfaches dessen, was die eigentlichen Daten an Größe ausmachen. Backups dauern länger, Wiederherstellungen ebenfalls, und die Replikation schiebt Megabyte durch das Netz, die mit der Fachlichkeit nichts zu tun haben. Außerdem läuft jede Datei, die ein Browser anfordert, durch die Anwendung und durch die Datenbank, statt direkt ausgeliefert zu werden.
Die Alternative ist unspektakulär. Die Datei liegt im Objektspeicher, und in der Datenbank steht nur die Referenz darauf, also ein Schlüssel oder eine URL. Die Datenbank bleibt schlank und tut, wofür sie gebaut ist, und der Objektspeicher tut dasselbe.
Dass diese Lösung so oft nicht gewählt wird, liegt selten daran, dass jemand sie abgewogen und für schlechter befunden hätte. Sie stand schlicht nicht zur Auswahl.
Ein fantastischer Schraubenzieher
An dieser Stelle ist mir eine Klarstellung wichtig, weil solche Beispiele schnell als Kritik an den genannten Produkten missverstanden werden. PostgreSQL ist eine fantastische Datenbank, und Ähnliches lässt sich über Microsoft SQL Server und MongoDB in ihren jeweiligen Kategorien sagen. Keines dieser Produkte ist das Problem.
Das Problem ist der Einsatz fĂĽr eine Aufgabe, fĂĽr die ein Werkzeug nicht gebaut ist. Ein fantastischer Schraubenzieher bleibt ein fantastisches Werkzeug, aber er ist das falsche, wenn ein Nagel in die Wand soll. Mit genug Geduld gelingt selbst das, nur sieht das Ergebnis entsprechend aus, und der Schraubenzieher hat danach auch gelitten.
Dasselbe Muster zeigt sich jenseits der Datenbanken. Zwei Systeme sollen lose gekoppelt Nachrichten austauschen, und eine Message-Queue wäre dafür das naheliegende Werkzeug. Sie wird aber nicht in Betracht gezogen, weil sie nicht im Stack vorgesehen ist, und so entsteht eine Tabelle, die als Warteschlange herhalten muss, samt eigener Logik für Sperren, Wiederholungen und Aufräumen. Auch das funktioniert, irgendwie, und trotzdem wäre ein Werkzeug, das für diese Aufgabe gebaut wurde, die bessere Wahl gewesen.
Ich habe vor einigen Monaten beschrieben, wie Architekturen an Adjektiven statt an Zahlen hängen, sobald es um Datenmengen geht. Der feste Stack ist ein Verwandter dieses Musters: Auch hier entscheidet eine Annahme, die niemand ausgesprochen hat, über die Form des Systems.
Warum es feste Stacks trotzdem gibt
Es wäre unredlich, feste Stacks als bloße Gedankenlosigkeit abzutun, denn es gibt gute Gründe für sie. Unternehmen wollen keinen Technologiezoo, in dem jedes Team einsetzt, was ihm gerade gefällt, und in dem nach fünf Jahren niemand mehr weiß, welche Datenbank in welchem Projekt läuft. Jede zusätzliche Technologie will betrieben, überwacht, aktualisiert und abgesichert werden, und für jede braucht es Menschen, die sich damit auskennen.
Diese Gründe teile ich. Nur liegt zwischen Schwarz und Weiß ein ziemlich breiter Bereich. Auf der einen Seite steht der Stack, der mit Scheuklappen eine einzige Datenbank für alles vorschreibt, auf der anderen das Projekt, das selbst innerhalb des relationalen Spektrums jedes Mal ein anderes Produkt wählt. Beides halte ich für falsch.
Der Mittelweg, den ich für sinnvoll halte, lässt sich in einem Satz zusammenfassen: pro Kategorie standardisieren, aber nicht eine Kategorie für alles. Eine relationale Datenbank als Hausstandard ist vernünftig, ebenso ein bestimmter Objektspeicher und eine bestimmte Message-Queue. Unvernünftig wird es erst, wenn der Standard einer Kategorie dazu führt, dass jedes Problem in diese Kategorie gepresst wird, auch dann, wenn es dort konzeptionell nicht hingehört.
Praktisch heißt das, den Standard als Normalfall zu verstehen und nicht als Verbot. Wo eine Kategorie konzeptionell nicht passt, muss eine andere zulässig sein, und die Begründung dafür sollte aus der Fachlichkeit kommen, nicht aus der Vorliebe eines Teams.
Dazu gehört, am Anfang eines Projekts nicht zu fragen, welche Datenbank im Stack steht, sondern welche Form die Daten haben: Werden sie geändert oder nur angehängt? Handelt es sich um Datensätze oder um Dateien? Müssen Systeme sofort antworten, oder genügt es, wenn eine Nachricht zuverlässig ankommt? Erst die Antworten darauf zeigen, welche Kategorien überhaupt infrage kommen.
Was ändern Agenten an dieser Rechnung?
Bleibt die Frage, ob Agenten an dieser Abwägung etwas ändern. Im ersten Teil habe ich argumentiert, dass sie das Schreiben von Code billig machen. Das Argument „diese Technologie kennt bei uns niemand“ verliert damit tatsächlich an Gewicht, zumindest was den Code betrifft: Einem Agenten ist es gleichgültig, ob er mit einer relationale Datenbank, einem Objektspeicher oder einer Message-Queue programmiert.
Dieselbe Gleichgültigkeit hat allerdings eine Kehrseite. Ein Agent baut die Tabelle, die eine Warteschlange spielen soll, in wenigen Minuten, samt Sperren und Wiederholungen, und er wird nicht einwenden, dass es dafür ein besseres Werkzeug gäbe. Wie im ersten Teil beschrieben, sagt ein Agent nie, dass er etwas nicht kann, und ebenso selten, dass etwas keine gute Idee ist. Die Umgehungslösung wird damit billiger zu schreiben, aber nicht billiger zu betreiben, und sie fällt anfangs noch weniger auf als früher.
Hinzu kommt, dass das Schreiben der kleinste Teil im Leben einer Software ist. Geschrieben wird Code einmal, gelesen wird er deutlich öfter, wenn ein Feature hinzukommt oder ein Fehler behoben werden muss, und auch das übernehmen zunehmend Agenten. Betrieben aber wird er die ganze Zeit. Genau dort steckt das Wissen, das kein Agent mitbringt: wie sich ein System stabil und verlässlich betreiben lässt, wie es überwacht wird, wie Backups funktionieren und was im Ernstfall um drei Uhr nachts zu tun ist.
Ganz irrelevant ist das Argument gegen zusätzliche Technologien also nicht, es verschiebt sich nur vom Code in den Betrieb. Dort hat es jedoch eine Eigenschaft, die ich in „Der Monolith ist die teure Variante“, der fünften Ausgabe der the native papers, ausführlicher beschrieben habe: Betriebswissen ist generisch. Ein Objektspeicher funktioniert bei einem Unternehmen fast genauso wie im Unternehmen nebenan, und deshalb lässt sich das Wissen darüber beschaffen. Es lässt sich einstellen, schulen oder gleich als Managed Service buchen.
Für die Reibung, die eine falsche Kategorie erzeugt, gilt das nicht. Sie steckt in den Umgehungslösungen, die sich mit der Zeit durch das ganze System ziehen: in der selbst gebauten Unveränderlichkeit, in der Tabelle, die eine Warteschlange spielt, in den Dateien, die jedes Backup aufblähen. Diese Reibung lässt sich weder einkaufen noch wegschulen. Entscheidend ist deshalb nicht, ob eine zusätzliche Kategorie Aufwand verursacht, sondern ob sich dieser Aufwand beschaffen lässt oder dauerhaft im eigenen System festsitzt.
Mittel zum Zweck
Am Ende laufen beide Teile dieser Serie, die Sprachwahl wie die Werkzeugwahl, auf denselben Grundsatz hinaus. Technologie ist Mittel zum Zweck und kein Selbstzweck. Sie muss das fachliche Problem lösen oder zumindest dazu beitragen, dass es sich lösen lässt, und sie ist umso besser geeignet, je besser sie das tut.
Ein fester Stack kehrt diese Reihenfolge um. Er beantwortet die Frage nach dem Werkzeug, bevor die Frage nach dem Problem überhaupt gestellt wurde, und er tut das nicht nur für das Produkt, sondern für die Kategorie. Wer so vorgeht, spart sich am Anfang eine Diskussion und bezahlt sie später in Form von Umgehungslösungen, deren Kosten niemand mehr auf die ursprüngliche Entscheidung zurückführt.
Agenten ändern an dieser Rechnung weniger, als es auf den ersten Blick scheint. Sie machen das Schreiben billig und nehmen damit der Sprachwahl viel von ihrem Gewicht. Das Entscheiden nehmen sie niemandem ab und den Betrieb ebenso wenig. Beides bleibt die Aufgabe von Menschen, die das Problem verstanden haben, bevor sie das Werkzeug wählen.
(mro)