Read in English · Leer en español · Lire en français
Abschnitt 7.1 von 7.8 · Mittelstufe · 24 Min. Lesezeit
Datenhoheit und Datenbank pro Service
Videolektion
5 Min. · auf Englisch gesprochen, mit UntertitelnNur anhören (Audio)
Transkript lesen
Code lässt sich relativ leicht in Services aufteilen, Daten nicht. In dieser Lektion lernen Sie die erste und wichtigste Datenfrage kennen: Wem gehören welche Daten?
Stellen Sie sich eine Pinnwand in einer Küche vor, auf der jeder die Bestellungen des Tages wegwischen und neu schreiben kann. Mit zwei Köchen funktioniert das. Mit zehn werden Bestellungen überschrieben, und niemand weiß, wer was geändert hat. Eine bessere Küche gibt jeder Station ihre eigene Tafel. Der Grill schreibt die Grillbestellungen. Andere Stationen lesen sie oder bitten den Grill, sie zu ändern. Das ist Datenhoheit: festzulegen, welcher Teil eines Systems welche Information ändern darf.
Ein System of Record ist die maßgebliche Quelle für ein Datum: der Ort, dessen Wert gilt, wenn Kopien voneinander abweichen. In einer Onlinebank ist das zentrale Hauptbuch das System of Record für Kontostände. Der Kontostand in der Mobile-App ist eine Kopie. Der Eigentümer ist die einzige Komponente, die die Daten schreibt, ihre Regeln durchsetzt, ihr Schema festlegt und sie für andere veröffentlicht. Eine nützliche Regel: Der Service, der in eine Tabelle schreibt, besitzt sie.
Dieses Diagramm zeigt zwei Wege, drei Services einer Essenslieferplattform ihre Daten zu geben. Links teilen sie sich alle eine Datenbank. Jede Tabelle wird zur öffentlichen Schnittstelle, sodass das Umbenennen einer Spalte Services brechen kann, von denen niemand wusste, dass sie sie lesen, und Schemaänderungen ein koordiniertes Release brauchen. Außerdem werden Geschäftsregeln in jedem Service dupliziert, der eine Tabelle schreibt, und der aufwendige Bericht eines Service bremst alle anderen aus. Rechts sehen Sie Datenbank pro Service. Jedes Schema ist privat, es gibt keine Joins über Datenbanken hinweg, und Daten überqueren Grenzen nur über API-Aufrufe und Events. Jeder Eigentümer kann sein Schema allein ändern. Der Preis ist, dass Joins, Transaktionen und Integritätsprüfungen über Services hinweg durch APIs, Events, Sagas und Reporting-Pipelines ersetzt werden müssen.
Schwierig wird Datenhoheit, wenn zwei Services dieselben Daten ändern. Das ist gemeinsame Datenhoheit, und es gibt vier Wege, sie aufzulösen. Sie können die Tabelle aufteilen, sodass jeder Service die Spalten besitzt, die er schreibt. Sie können eine kleine gemeinsame Datendomäne behalten: einfach, aber eine bewusst begrenzte gemeinsame Datenbank. Sie können delegieren: Einen Eigentümer wählen und den anderen Service ihm Anfragen schicken lassen, statt selbst zu schreiben. Das ist die häufigste Antwort für Bestellungen, Zahlungen und Buchungen. Oder Sie legen die Services zusammen, die ehrliche Antwort, wenn sie zu fein geschnitten wurden.
Hier ist ein ausgearbeitetes Beispiel. Drei Services schreiben alle die Statusspalte einer gemeinsamen Bestelltabelle, und Kunden haben gesehen, wie Bestellungen von „abgeholt“ zurück auf „in Zubereitung“ springen. Das ist gemeinsame Datenhoheit. Der letzte Schreibvorgang gewinnt, verspätete Nachrichten setzen den Status zurück, und die Stornierungsregel wird in zwei Services geprüft, und zwar unterschiedlich. Der entscheidende Schritt ist zu bemerken, dass eine Spalte drei Tatsachen verbirgt: den Zustand des Küchenbons, den Zustand des Lieferauftrags und den Status, den der Kunde sieht und der beide kombiniert. Das Aufteilen der Tabelle wurde verworfen, weil die Stornierungsregel dann immer noch kein Zuhause hätte. Auch das Zusammenlegen wurde verworfen: drei Teams mit sehr unterschiedlicher Last. Also wird Bestellungen zum einzigen Schreiber des Bestellstatus. Der Status bewegt sich nur vorwärts, sodass eine verspätete Nachricht zwar aufgezeichnet wird, ihn aber nicht zurücksetzen kann.
Hier ist das Ergebnis. Restaurant- und Liefer-Service besitzen ihre eigenen Tatsachen und veröffentlichen Events wie FoodReady und PickedUp. Der Bestell-Service ist der einzige Schreiber des Status und speichert ihn in seiner eigenen Datenbank. Eine Stornierung vom Restaurant ist eine Anfrage an seine API, die er annehmen oder ablehnen kann. Alle anderen, etwa die Kunden-App und das Support-Tool, lesen den Status über die API. Jede Tatsache hat genau einen Schreiber.
Andere Services können die API des Eigentümers aufrufen, was aktuell ist, sie aber an ihn koppelt, oder eine lokale, durch Events gespeiste Kopie halten, was schnell, aber nur eventually consistent ist. Der häufige Fehler ist, eine Kopie zum Master werden zu lassen. Sobald ein Leser seine eigene Kopie korrigiert, gibt es zwei Systems of Record, und sie werden auseinanderlaufen. Zwischen Teams legt ein Datenvertrag das Schema, die Bedeutung jedes Felds, Qualitätserwartungen und Aktualität fest, idealerweise automatisch geprüft. Fragen Sie also für jede wichtige Tatsache: Wer ist der einzige Schreiber, wo werden ihre Regeln durchgesetzt, und wie veraltet darf jede Kopie sein? Lautet eine Antwort „kommt darauf an“, ist das Design nicht fertig.
Der vollständige Abschnitt behandelt außerdem Reporting über Services hinweg und Datenverträge im Detail. Als Nächstes betrachten wir in Abschnitt sieben Punkt zwei die Wahl einer Datenbank. Bis gleich.
Was Sie wissen müssen
- Ein System of Record ist die maßgebliche Quelle für ein Datum; Datenhoheit bedeutet, dass genau eine Komponente es schreibt, seine Regeln durchsetzt, sein Schema verantwortet und es für andere veröffentlicht.
- Eine gemeinsame (Integrations-)Datenbank koppelt Services über Schema, Deployment, Semantik und Last zur Laufzeit; Fowler (2004) und die meisten Architekten raten davon zwischen unabhängig entwickelten Anwendungen ab.
- Datenbank pro Service hält die Daten jedes Service privat (eigene Tabellen, eigenes Schema oder eigener Server), um den Preis von Joins, Transaktionen und referenzieller Integrität über Services hinweg, die durch APIs, Events, Sagas und Reporting-Pipelines ersetzt werden müssen.
- Gemeinsame Datenhoheit lässt sich durch Aufteilen der Tabelle, eine kleine gemeinsame Datendomäne, Delegation an einen Eigentümer oder Zusammenlegen der Services auflösen, jeweils mit einer eigenen Abwägung (The Hard Parts, 2021).
- Daten, die einem nicht gehören, lassen sich über synchrone Aufrufe, Column Schema Replication, replizierte Caches oder Events lesen; jede Kopie ist schreibgeschützt und möglicherweise veraltet.
- Datenverträge legen Schema, Bedeutung, Qualität, Service-Levels und Verantwortung für Daten fest, die Teams miteinander teilen, und sind am nützlichsten, wenn sie automatisch geprüft werden.
- Für jede wichtige Tatsache kann ein solides Design ihren einzigen Schreiber benennen, sagen, wo ihre Regeln durchgesetzt werden, und wie veraltet jede Kopie sein darf.
Warum Datenhoheit die erste Datenfrage ist
Kapitel 6 hat Systeme in Services aufgeteilt, die unabhängig deployt, skaliert und geändert werden. Code lässt sich relativ leicht aufteilen, Daten nicht. Eine Bestellung verweist auf einen Kunden, einen Speisekartenartikel, ein Restaurant und einen Kurier, und jedes Team will einen Teil davon lesen und oft auch ändern. Die wichtigste Frage einer verteilten Datenarchitektur ist deshalb nicht, welche Datenbank man kauft, sondern wem welche Daten gehören: Welche Komponente darf sie ändern, welche setzt ihre Regeln durch, und wie bekommen alle anderen sie zu sehen?
Datenhoheit ist wichtig, weil Daten Code überdauern. Services werden alle paar Jahre neu geschrieben; Kundenkonten, Hauptbücher und Patientenakten werden jahrzehntelang aufbewahrt. Eine falsch geregelte Datenhoheit erzeugt die teure Art von Architekturschulden: zwei Services, die sich nicht getrennt releasen lassen, weil sie sich Tabellen teilen, ein Statusfeld, das drei Teams abwechselnd überschreiben, oder ein Monatsbericht, den niemand reproduzieren kann, weil die Zahlen aus fünf Kopien derselben Daten stammen, die voneinander abweichen.
Dieser Abschnitt nutzt zwei durchgehende Beispiele, die im ganzen Kapitel wiederkehren: eine Essenslieferplattform (Kunden, Restaurants, Kuriere, Bestellungen) und eine Onlinebank (Kunden, Konten, Zahlungen, ein Hauptbuch).
System of Record und Datenhoheit
Ein System of Record ist die maßgebliche Quelle für ein bestimmtes Datum: der Ort, dessen Wert gilt, wenn Kopien voneinander abweichen. In der Bank ist das zentrale Hauptbuch das System of Record für Kontostände; der Kontostand in der Mobile-App, der im Cache der Betrugserkennung und der in der Ladung des Data Warehouse von letzter Nacht sind alles Kopien. In der Essenslieferplattform ist der Speisekarten-Service des Restaurants das System of Record für Preise, obwohl der Suchindex, der Offline-Cache der Mobile-App und die Bestellpositionen alle Kopien davon halten.
Datenhoheit macht daraus eine Verantwortung. Die besitzende Komponente (und das Team dahinter) ist die einzige, die die Daten schreibt, ihre Invarianten (Regeln, die immer gelten müssen, etwa ein Hauptbucheintrag wird nie bearbeitet, nur storniert
) durchsetzt, ihr Schema festlegt und sie über eine von ihr kontrollierte Schnittstelle für andere veröffentlicht. Datenhoheit ist eine Designentscheidung, keine Eigenschaft der Datenbank: Zwei Services können auf demselben Datenbankserver liegen und trotzdem eine klare Datenhoheit haben, und zwei Services mit getrennten Datenbanken können immer noch um dieselben Tatsachen streiten, wenn die Grenze zwischen ihnen schlecht gezogen ist.
Eine nützliche Regel aus Software Architecture: The Hard Parts (Neal Ford, Mark Richards, Pramod Sadalage und Zhamak Dehghani, 2021) lautet: Der Service, der in eine Tabelle schreibt, besitzt sie. Lesen lässt sich günstig teilen, Schreiben nicht, denn wer schreibt, muss die Regeln durchsetzen.
Die gemeinsame Datenbank und warum sie Services koppelt
Der einfachste Weg, mehrere Anwendungen mit denselben Daten arbeiten zu lassen, ist, ihnen allen Zugriff auf eine Datenbank zu geben. Martin Fowler nannte das eine Integrationsdatenbank (2004): eine Datenbank, die der Integrationspunkt zwischen Anwendungen ist, im Gegensatz zu einer Anwendungsdatenbank, die nur einer Anwendung dient. Sie wirkt verlockend: keine APIs zu bauen, Joins funktionieren über alles, und eine Transaktion kann Bestellungen und Zahlungen gemeinsam aktualisieren. Fowlers Urteil, das die meisten Architekten teilen, lautet, dass Integrationsdatenbanken vermieden werden sollten, weil die Kopplung, die sie erzeugen, tief und größtenteils unsichtbar ist.
Betrachten Sie die Formen von Kopplung, die sie mit sich bringt. Schemakopplung: Jede Tabelle und jede Spalte ist faktisch eine öffentliche API. Wer orders.state in orders.status umbenennt, bricht jeden Service, der sie liest, und niemand kann sicher sagen, welche das sind, weil SQL-Abfragen über viele Codebasen, Berichte und Tabellenkalkulationen verstreut sind. Deployment-Kopplung: Eine Schemaänderung muss mit jedem Team abgestimmt werden, das die Tabelle berührt, sodass Services, die unabhängig releast werden sollten, gemeinsam releast werden. Semantische Kopplung: Das Schema muss jedem Client dienen, also wird es allgemeiner und verwirrender (Fowler merkt an, dass es am Ende Konzepte zu vereinheitlichen versucht, die in getrennte Bounded Contexts gehören), und die Geschäftsregeln für eine Tabelle landen dupliziert, und zwar uneinheitlich, in jedem Service, der sie schreibt. Laufzeitkopplung: Die langsame Berichtsabfrage oder der sperrintensive Batch-Job eines Service beeinträchtigt jeden anderen Service, und die Datenbank wird zu einem gemeinsamen Single Point of Failure und zu einer Obergrenze für Verbindungen.
Nichts davon ist fatal für eine einzelne Anwendung oder einen modularen Monolithen, dessen Module die Tabellen der anderen respektieren, oder für die grobgranulare servicebasierte Architektur, in der sich eine Handvoll Domänenservices oft bewusst eine Datenbank teilt. Zum Problem wird es, wenn Teams unabhängige Deploybarkeit erwarten, aber die gemeinsamen Tabellen behalten; das ist ein klassischer Weg zu einem verteilten Monolithen.
Gemeinsame Datenbank im Vergleich zu Datenbank pro Service
Datenbank pro Service
Die Alternative, die mit Microservices verbunden ist, ist das Muster Datenbank pro Service: Die persistenten Daten jedes Service sind privat, und andere Services erreichen sie nur über die API des Service oder die Events, die er veröffentlicht. Chris Richardsons Musterkatalog (microservices.io) merkt an, dass das nicht einen eigenen Server pro Service bedeuten muss. Es gibt drei Stufen der Trennung, und die wichtige Grenze ist die erste.
Drei Wege, die Daten eines Service privat zu halten
| Niveau | Was getrennt ist | Stärke der Grenze | Typische Verwendung |
|---|---|---|---|
| Eigene Tabellen pro Service | Jeder Service hat seine eigenen Tabellen in einer gemeinsamen Datenbank, mit eigenen Zugangsdaten, die nur diese Tabellen erreichen. | Am schwächsten: beruht auf Berechtigungen und Disziplin; teilt weiterhin Kapazität und Ausfälle. | Früh in einer Migration; kleine Systeme; modulare Monolithen. |
| Schema pro Service | Jeder Service hat sein eigenes Schema (Namensraum) auf einem gemeinsamen Server. | Klare Datenhoheit und Zugriffskontrolle; teilt weiterhin Performance und Verfügbarkeit des Servers. | Die meisten Teams, die mit mehreren Services beginnen; günstiger im Betrieb. |
| Datenbankserver pro Service | Jeder Service hat seine eigene Datenbankinstanz, möglicherweise eines anderen Typs. | Am stärksten: getrennte Skalierung, getrennte Ausfälle und freie Technologiewahl. | Services mit sehr unterschiedlicher Last, Datenform oder unterschiedlichem Verfügbarkeitsbedarf. |
Die Vorteile spiegeln die Kosten der gemeinsamen Datenbank: Eigentümer ändern ihre Schemas frei, skalieren und optimieren ihre Speicher unabhängig, wählen die richtige Art von Datenbank und begrenzen Ausfälle. Die Kosten sind real und sollten in jedem Entscheidungsdokument genannt werden. Joins über Services hinweg entfallen und müssen durch API-Komposition oder replizierte Daten ersetzt werden. Eine einzelne ACID-Transaktion kann nicht mehr Bestellungen und Zahlungen zugleich umfassen; serviceübergreifende Änderungen werden zu Sagas mit kompensierenden Schritten. Referenzielle Integrität über Services hinweg (jede Bestellung verweist auf einen echten Kunden) wird nicht mehr durch Fremdschlüssel durchgesetzt. Reporting braucht eine eigene Pipeline. Und es gibt mehr Datenbanken, die gesichert, gepatcht und überwacht werden müssen.
The Hard Parts fasst die Entscheidung als Gleichgewicht von Kräften. Disintegratoren drängen zum Aufteilen der Daten: Änderungskontrolle (wie viele Services brechen, wenn sich eine Tabelle ändert), Verbindungslimits, Skalierbarkeit, Fehlertoleranz, der Wunsch nach unabhängig deploybaren Einheiten und der Bedarf an unterschiedlichen Datenbanktypen. Integratoren ziehen Daten zusammen: Beziehungen, die referenzielle Integrität brauchen, und Operationen, die wirklich eine einzige atomare Transaktion brauchen. Wenn zwei Tabellen immer gemeinsam in einer Transaktion geändert werden und kein unabhängiges Eigenleben haben, spricht das stark dafür, dass sie demselben Eigentümer gehören.
Wenn mehrere Services dieselben Daten schreiben
Fragen der Datenhoheit werden schwierig, wenn mehr als ein Service dieselbe Entität berechtigterweise ändert. The Hard Parts unterscheidet drei Situationen. Bei alleiniger Datenhoheit schreibt nur ein Service die Tabelle, und er besitzt sie einfach. Bei allgemeiner Datenhoheit schreiben die meisten Services dieselbe Tabelle (etwa ein Audit-Log oder eine Reihe von Konfigurationseinstellungen); die übliche Antwort ist, einen eigenen Service zu schaffen, der die Daten besitzt, und allen anderen eine API zu geben, oft eine asynchrone, damit die Schreiber nicht ausgebremst werden. Bei gemeinsamer Datenhoheit schreibt eine kleine Zahl von Services, typischerweise zwei, dieselbe Tabelle, meist innerhalb derselben Domäne. Das Buch bietet vier Techniken für gemeinsame Datenhoheit an.
Gemeinsame Datenhoheit auflösen
| Technik | So funktioniert es | Wichtigste Abwägung |
|---|---|---|
| Tabelle aufteilen | Die Tabelle so aufteilen, dass jeder Service die Spalten, die er schreibt, in einer eigenen Tabelle besitzt. Aus der Zeile in orders der Essenslieferplattform werden eine Bestellung im Besitz von Bestellungen und ein Küchenbon im Besitz der Küche. | Klare Datenhoheit, aber das Anlegen oder Löschen der übergeordneten Entität muss nun zwischen Services synchronisiert werden, und jede Gesamtsicht braucht beide. |
| Datendomäne | Die Tabelle für die wenigen beteiligten Services gemeinsam behalten, in einem kleinen gemeinsamen Schema, das sie gemeinsam kontrollieren. | Einfach und schnell, Transaktionen bleiben möglich, aber es ist eine bewusst begrenzte gemeinsame Datenbank: Die Services sind bei Schemaänderungen gekoppelt, und die Grenze zwischen ihnen wird schwächer. |
| Delegieren | Einen Eigentümer wählen; der andere Service schickt ihm Anfragen (synchrone Aufrufe oder asynchrone Befehle), statt selbst zu schreiben. Der Eigentümer wird nach der primären Domäne gewählt (wessen Konzept ist es?) oder nach betrieblichen Eigenschaften (welcher Service braucht die schnellsten, am besten verfügbaren Schreibvorgänge?). | Klare Datenhoheit und Regeln an einem Ort, um den Preis eines zusätzlichen Netzwerk-Hops, einer neuen Ausfallart und möglicherweise Eventual Consistency für den delegierenden Service. |
| Services zusammenlegen | Die Services, die sich die Daten teilen, zu einem Service zusammenführen. | Beseitigt das Problem und stellt lokale Transaktionen wieder her, schafft aber einen größeren Service mit gröberem Deployment, gröberer Skalierung und gröberer Fehlerisolation. |
Keine Technik ist kostenlos, und die richtige hängt davon ab, wie oft sich die Daten ändern, wie streng ihre Regeln sind und wie unabhängig die Teams sein müssen. In der Praxis ist Delegation die häufigste Antwort für Geschäftsentitäten mit einem Lebenszyklus (Bestellungen, Zahlungen, Buchungen), das Aufteilen der Tabelle funktioniert gut, wenn die gemeinsam geschriebenen Spalten eigentlich verschiedene Tatsachen sind, die nur zufällig zusammen gespeichert wurden, und das Zusammenlegen ist die ehrliche Antwort, wenn die beiden Services von vornherein zu fein geschnitten wurden.
Daten lesen, die Ihnen nicht gehören
Sobald die Datenhoheit geklärt ist, muss jeder andere Service die Daten immer noch lesen. Die Küchenanzeige braucht die Lieferhinweise des Kunden; die Betrugserkennung braucht die jüngsten Zahlungen des Kontos. Es gibt vier grundlegende Optionen, und jede wägt Aktualität gegen Kopplung ab. Beachten Sie, dass ereignisgesteuerte Designs, die in Kapitel 6 behandelt wurden, die dritte und vierte liefern.
Wege, die Daten eines anderen Service zu lesen
| Option | So funktioniert es | Aktualität | Kopplung und Kosten |
|---|---|---|---|
| Synchroner Aufruf | Die API des Eigentümers in dem Moment fragen, in dem die Daten gebraucht werden. | Aktuell, zum Zeitpunkt des Aufrufs. | Laufzeitkopplung: Ist der Eigentümer langsam oder ausgefallen, gilt das auch für den Aufrufer; Latenzen summieren sich entlang einer Kette. |
| Column Schema Replication | Die wenigen benötigten Spalten (zum Beispiel Name und Adresse des Restaurants) in die eigene Datenbank des Lesers kopieren, aktuell gehalten durch Events oder Change Data Capture. | Eventually consistent: veraltet um die Replikationsverzögerung. | Schnelle lokale Lesezugriffe und Joins; der Leser muss mit verspäteten oder verpassten Aktualisierungen umgehen und darf die Kopie nie beschreiben. |
| Replizierter Cache | Ein In-Memory Data Grid (etwa Hazelcast oder Apache Ignite) hält in jedem konsumierenden Service eine Kopie der Daten des Eigentümers, synchronisiert durch das Grid. | Nahezu in Echtzeit. | Sehr schnelle Lesezugriffe; Speicherkosten, Laden beim Start und die betriebliche Komplexität des Grids. |
| Events (Event-Carried State Transfer) | Der Eigentümer veröffentlicht Events, die die geänderten Daten enthalten; Leser behalten, was sie brauchen. | Eventually consistent. | Lose zeitliche Kopplung; Leser müssen mit Duplikaten, Reihenfolge und Schemaevolution umgehen. |
Fowler nennt die vierte Option Event-Carried State Transfer: Events wie MenuItemPriceChanged enthalten die neuen Werte, sodass Konsumenten nicht zurückrufen müssen. Ein verwandter Mechanismus, Change Data Capture (CDC), liest das Log der Datenbank des Eigentümers und macht aus jeder committeten Änderung ein Event; das vermeidet das Dual-Write-Problem (in die Datenbank schreiben und getrennt davon ein Event veröffentlichen, sodass das eine gelingen und das andere scheitern kann). Die Transactional Outbox, die dasselbe Problem innerhalb der Anwendung löst, wird bei den Sagas behandelt.
Reporting über Services hinweg
Datenbank pro Service erschwert eine alte Aufgabe: den übergreifenden Bericht. Umsatz pro Restaurant pro Woche, abzüglich Erstattungen, aufgeschlüsselt nach Kuriertyp
braucht Bestellungen, Zahlungen, Restaurants und Lieferungen. Ihn durch seitenweises Aufrufen von vier APIs zu erstellen ist langsam und belastet operative Services mit Reporting-Last. Ihre Datenbanken direkt abzufragen führt die Kopplung der gemeinsamen Datenbank durch die Hintertür wieder ein.
Die übliche Antwort ist, Reporting vom operativen Betrieb zu trennen. Fowler beschrieb eine Reporting-Datenbank: einen eigenen Speicher, für Abfragen gestaltet und aus den operativen Systemen gespeist, den Reporting-Nutzer frei lesen dürfen. Heute ist das meist ein Data Warehouse oder Lakehouse, gespeist durch CDC oder Events, behandelt in Analytics und Streaming. Für kleine, nahezu echtzeitnahe Ansichten innerhalb des Produkts (das Dashboard eines Restaurants mit den heutigen Bestellungen) kann ein Service aus Events sein eigenes Read Model aufbauen, die Idee hinter CQRS. In beiden Fällen hängen die Pipelines, die das Reporting speisen, von den Datenstrukturen der Eigentümer ab, und deshalb brauchen sie Verträge.
Datenverträge
Ein Datenvertrag ist eine explizite, versionierte Vereinbarung zwischen dem Eigentümer eines Datenbestands und seinen Konsumenten. Er legt das Schema fest (Felder, Typen, welche null sein dürfen), die Bedeutung jedes Felds (ist amount in Pence oder Pfund, mit oder ohne Mehrwertsteuer?), Qualitätserwartungen (keine doppelten Bestellkennungen, höchstens 0,1 % der Zeilen ohne Restaurant), Service-Levels (wie aktuell die Daten sind, wie lange sie aufbewahrt werden), wer sie verantwortet und wie Änderungen angekündigt werden. Er überträgt die Idee eines API-Vertrags auf Daten, die über Events, CDC-Streams und Warehouse-Tabellen zwischen Teams fließen.
Verträge sind am nützlichsten, wenn sie maschinenlesbar sind und automatisch geprüft werden: eine Pipeline, die eine Schemaänderung ablehnt, die registrierte Konsumenten brechen würde, oder ein Test, der fehlschlägt, wenn eine Qualitätsregel verletzt wird. Der Open Data Contract Standard (ODCS), gepflegt vom Bitol-Projekt unter der Linux Foundation AI & Data, definiert ein YAML-Format für Schema, Qualitätsregeln, Service-Levels, Rollen und Support-Kanäle. Datenverträge sind nicht kostenlos: Jemand muss sie schreiben und pflegen, und zu strenge Verträge bremsen Änderungen. The Hard Parts stellt strenge Verträge (präzise, überprüfbar, eng koppelnd) lockeren gegenüber (leichter weiterzuentwickeln, aber mit mehr Testbedarf) und schlägt Consumer-Driven Contracts, bei denen Konsumenten angeben, was sie tatsächlich nutzen, als Mittelweg vor. Wie man ein Schema ändert, ohne Konsumenten zu brechen, ist Thema der Schemaevolution.
Ausgearbeitetes Beispiel
Ausgearbeitetes Beispiel: Wem gehört der Bestellstatus in der Essenslieferplattform?
Die Plattform hat drei Services, die sich für den Fortschritt einer Bestellung interessieren: Bestellungen (nimmt Bestellungen an und kassiert), Restaurant (das Küchentablet: nimmt Bestellungen an, meldet das Essen als fertig) und Lieferung (weist Kuriere zu, verfolgt Abholung und Zustellung). Heute schreiben alle drei die Spalte status einer gemeinsamen Tabelle orders. Kunden haben gesehen, wie Bestellungen von Abgeholt
zurück auf In Zubereitung
springen, und niemand kann sagen, wo die Stornierungsregeln liegen.
| Diagnose | Gemeinsame Datenhoheit über eine Spalte mit drei Schreibern. Jeder schreibt, was er weiß, der letzte Schreibvorgang gewinnt, und verspätete Nachrichten setzen den Status zurück. Die Regel eine Bestellung kann nur storniert werden, bevor die Küche beginntwird in zwei Services geprüft, und zwar unterschiedlich. |
|---|---|
| Die Tatsachen trennen | Eigentlich gibt es drei Tatsachen: den Zustand des Küchenbons (im Besitz des Restaurants), den Zustand des Lieferauftrags (im Besitz der Lieferung) und den Bestellstatus, den der Kunde sieht, der beide kombiniert und die Geschäftsregeln trägt (Stornierung, Erstattungen, Entschädigung). |
| Option: Tabelle aufteilen | Jedem Service eine eigene Statusspalte geben und die Leser sie kombinieren lassen. Verworfen: Jeder Leser (App, Support-Tool, Benachrichtigungen) müsste die Kombination neu implementieren, und die Stornierungsregel hätte immer noch kein Zuhause. |
| Option: Services zusammenlegen | Alle drei zu einem Service zusammenführen. Verworfen: drei Teams, sehr unterschiedliche Last (Standortaktualisierungen der Kuriere kommen alle paar Sekunden) und unterschiedliche Release-Zyklen. |
| Entscheidung: delegieren | Bestellungen ist der einzige Schreiber von order_status. Restaurant und Lieferung besitzen ihre eigenen Tatsachen und veröffentlichen Events (OrderAccepted, FoodReady, PickedUp, Delivered). Bestellungen konsumiert sie und wendet einen Zustandsautomaten an. Eine Stornierung vom Restaurant-Tablet ist eine Anfrage an die API von Bestellungen, die sie annimmt oder ablehnt. |
| Zustandsautomat | PLACED, ACCEPTED, PREPARING, READY, PICKED_UP, DELIVERED, wobei CANCELLED nur aus PLACED oder ACCEPTED erlaubt ist. Übergänge gehen nur vorwärts, sodass ein verspätetes FoodReady, das nach PickedUp ankommt, aufgezeichnet wird, den Status aber nicht ändert; Duplikate werden ignoriert. |
| Konsequenzen | Ein Ort für die Regeln; keine Rücksprünge; jedes Team releast unabhängig. Kosten: Der Status hinkt der Küche um die Event-Verzögerung hinterher (typischerweise deutlich unter einer Sekunde), Bestellungen muss verfügbar sein, um Stornierungen anzunehmen, und die Events werden zu einem Vertrag, der versioniert werden muss. |
Der entscheidende Schritt war nicht, eine Technik aus der Tabelle zu wählen, sondern zu bemerken, dass eine Spalte drei verschiedene Tatsachen mit drei natürlichen Eigentümern verbarg.
Halten Sie die Entscheidung als Architecture Decision Record fest, einschließlich der verworfenen Optionen; das nächste Team, das fragt Warum können wir den Status nicht einfach aus der Kurier-App aktualisieren?
, wird ihn brauchen.
Bestellstatus nach der Entscheidung
Häufige Fehler
Services aufteilen, aber die gemeinsamen Tabellen behalten. Teams bekommen die Betriebskosten der Verteilung ohne die Unabhängigkeit, die sie erkaufen sollte. Wenn sich Services nicht ohne eine abgestimmte Schemaänderung deployen lassen, sind sie nicht unabhängig. Entity-Services. Ein CustomerService, der nichts als Anlegen, Lesen, Aktualisieren und Löschen über einer Tabelle bietet und von jedem anderen Service aufgerufen wird, ist eine gemeinsame Datenbank mit zusätzlicher Latenz; Datenhoheit sollte Geschäftsfähigkeiten und ihren Regeln folgen, nicht Substantiven. Kopien, die unbemerkt zu Mastern werden. Siehe die Warnung oben. Reporting bis zum Schluss ignorieren. Der erste Finanzbericht nach der Aufteilung zeigt oft, dass niemand geplant hat, wie die Daten wieder zusammengeführt werden. Datenhoheit als rein technisch behandeln. Datenhoheit ist auch organisatorisch: Jemand muss für die Qualität der Daten geradestehen, und nach dem Gesetz von Conway halten Datengrenzen, die quer zu Teamgrenzen verlaufen, meist nicht lange.
Ein guter Test für jedes Datendesign besteht darin, für jede wichtige Tatsache drei Fragen zu stellen: Wer ist der einzige Schreiber, wo werden ihre Regeln durchgesetzt, und wie veraltet darf jede Kopie sein? Lautet eine Antwort kommt darauf an
oder mehrere Services
, ist das Design nicht fertig.
Prüfen Sie Ihr Verständnis
Zwei Services aktualisieren jeweils die Spalte status derselben Tabelle. Um welche Art von Datenhoheit handelt es sich, und nennen Sie zwei Wege, sie aufzulösen.
Gemeinsame Datenhoheit. Sie lässt sich auflösen, indem man die Tabelle aufteilt, Schreibvorgänge an einen Eigentümer delegiert, eine kleine gemeinsame Datendomäne behält oder die Services zusammenlegt.
Warum untergräbt eine gemeinsame Datenbank unabhängiges Deployment?
Jede Tabelle ist faktisch eine öffentliche Schnittstelle, sodass eine Schemaänderung mit jedem Service abgestimmt werden muss, der sie liest oder schreibt, was gemeinsame Releases erzwingt.
Was ist beim Lesen der Daten eines anderen Service der Unterschied zwischen Column Schema Replication und einem synchronen Aufruf?
Replikation hält eine lokale, eventually consistent Kopie, die schnell und verfügbar ist, auch wenn der Eigentümer ausgefallen ist; ein synchroner Aufruf liefert aktuelle Daten, koppelt aber Verfügbarkeit und Latenz des Aufrufers an den Eigentümer.
Was fügt ein Datenvertrag über ein Schema hinaus hinzu?
Die Bedeutung der Felder, Qualitätserwartungen, Vorgaben zu Aktualität und Aufbewahrung, die Verantwortung sowie die Art, wie Änderungen angekündigt und versioniert werden.
Quellen und weiterführende Literatur
Haben Sie eine Frage zu diesem Abschnitt? ArchitectBot, der KI-Lernassistent des Kurses, antwortet zuerst aus dem Kurs.
Fragen: „Wir teilen unseren Monolithen in Services auf, aber sie nutzen alle noch dieselbe Tabelle orders. Wie sollten wir entscheiden, welcher Service welche Daten besitzt?“