Heutige Systeme sind meist modular bzw. verteilt aufgebaut und kommunizieren über Schnittstellen miteinander. Diese Kommunikation funktioniert natürlich nur, wenn sich beide Systeme darüber einig sind wie Informationen ausgetauscht werden und vor Allem auch was diese Information bedeutet.
Eine gute Dokumentation ist unabhängig davon ob nur ein oder mehrere Teams an bzw. mit der Schnittstelle arbeiten, unerlässlich. Für synchrone REST-basierte Aufrufe hat sich bei den Microservices z. B. OpenAPI verbreitet, bei asynchronen Schnittstellen wird AsyncAPI immer beliebter.
Ist ein gemeinsames Verständnis für die Schnittstelle geschaffen und eine Kommunikation eingerichtet, kann ja eigentlich nichts mehr schief gehen, oder?
Kein Geheimnis dürfte sein, dass sich in den meisten Projekten vorhandene Anforderungen oder sich die zugrundeliegende Daten mit der Zeit ändern. Dementsprechend wird es vorkommen, dass Schnittstellen angepasst werden müssen.
Ohne eine Versionierung der Schnittstelle wird man hier unter Umständen schnell in ernsthafte Probleme laufen.
Fehlende Rückwärtskompatibilität - Alte Anwendungen, die plötzlich das neue Format nicht unterstützen, funktionieren nicht mehr korrekt.
Unklare Kommunikation - Die Kommunikation zwischen den ‘Systemen’, wenn mehrere Teams beteiligt sind, kann problematisch werden.
Mangelnde Kontrolle über Änderungen - Wann wurde was geändert. Hat man potentiell mehr als einen Client der Schnittstelle, kann die Verwirrung schnell groß werden.
…
Klar sollte sein: Schnittstellen müssen versioniert werden. Es spielt hierbei keine Rolle ob es sich um eine synchrone oder eine asynchrone Schnittstelle handelt.
Klar darf der Verweis zu semver.org hier nicht fehlen. Im weiteren möchte ich allerdings nicht auf ein Versionsnummernschema eingehen.
Bei jeder Änderung, die man an einer Schnittstelle vornimmt, muss man sich Gedanken darüber machen, ob die Änderung ein breaking change darstellt, also die Kommunikation ‘bricht’ und mindestens ein System nicht mehr korrekt funktioniert, oder ob die Auswirkungen der Änderung nicht so groß sind und alle Systeme weiterhin korrekt arbeiten.
Die Erkennung von “breaking changes” ist manchmal nicht so einfach, wie es auf den ersten Moment erscheint. Es ist Vorsicht geboten, denn man sollte nicht nur technische Aspekte betrachten! Änderungen können auch zu semantisch sein. Also Änderungen, die trotz einer fehlerfreien Übertragung dazu führen, dass mindestens ein System nicht mehr korrekt arbeitet. Ein Beispiel sind Werte, die in einer neuen Version eines Systems “plötzlich” vorausgesetzt werden, die in einer früheren Version optional waren.
Bei der Übertragung der Daten zwischen den Systemen werden meist plattformneutrale Datenformate wie JSON, Protobuf oder auch XML eingesetzt. Also Formate, die von den verschiedensten Programmiersprachen unterstützt werden und mit denen man auch “kleinere Änderungen” im Format vornehmen kann.. vergisst man einmal Schemata…
Ein immer wieder gern genommener Fehler: Ein Systems geht davon aus, dass das Datenformate, das verwendet wird, in einem anderen System auf eine bestimmte Weise interpretiert und verarbeitet wird.
Bei JSON können z. B. die Standardeinstellungen von Parsern gerne zu Problemen
führen. Werden neue Felder in die JSON-Struktur aufgenommen, kann es passieren,
dass ein Parser diese nicht, wie ursprünglich gedacht, ignoriert, sondern das
zusätzliche Feld als Fehler meldet. Wer mit Jackson, Java und JSON gearbeitet
hat, kennt in diesem Zusammenhang sicher auch die Option
FAIL_ON_UNKNOWN_PROPERTIES
.
new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
Für Go im Vergleich sind ersteinmal alle Felder eines Structs bei der Serialisierung/ Deserialisierung optional und müssen explizit geprüft werden. Am Geschicktesten geht das sicher mit Pointern und nil-Prüfungen.
type Article struct {
Id *string `json:"id,omitempty"`
...
}
Eine falsche Annahme wie: “Wir schicken ja nur ein Feld mehr, die werden das ja ignorieren”, kann recht schnell “auf den Brettern” enden und zu Fehlern führen.
Also Vorsicht bei einer Aussage wie: “Wir haben nix gemacht. Ist kein breaking change, nur ein neues Feld aufgenommen, aber das brauchen die ja nicht”.
Oftmals sind vermeidlich kleine Änderungen, nicht wie gedacht, doch ein “breaking change” und müssen auch dementsprechend behandelt werden!
VORSICHT bei Änderungen im Datenformat. Im Zweifelsfall eine neue Version der Schnittstelle veröffentlichen!
In heutigen Systemlandschaften wird meist asynchron kommuniziert und die einzelnen Systeme tauschen über Messaging-Lösungen Nachrichten aus. Die einzelnen Teilnehmer nutzen sogenannte Channels um ihre Nachrichten zu versenden bzw. zu empfangen.
In der Theorie handelt es sich bei den Channels um “virtual pipes”, die nicht zwangsläufig persistiert werden müssen. In der Praxis haben sich allerdings Messaging-Systeme etabliert, die Nachrichten auch persistent ablegen. Dort heißen dann die Channels meist Topic oder Queue und können auf verschiedenste Weise, z. B. mit Sicherheitseinstellungen, konfiguriert werden.
Wie die Nachrichten in einem Channel aufgebaut sind und welches Datenformat verwendet werden soll, müssen auch hier die Kommunikationsteilnehmer miteinander aushandeln. Analog zu einer synchronen Schnittstelle.
Ein Channel entspricht im Groben bei der synchronen Kommunikation einem Endpoint.
Es gelten also ersteinmal die gleichen Regeln in Bezug zu den “breaking changes” der übermittelten Nachrichten.
Unabhängig davon ob man nun eine synchrone oder eine asynchrone Schnittstelle nutzt, muss man (ich möchte hier explizit nicht sollte schreiben) eine Schnittstelle versionieren bzw. mit Informationen ausstatten in welchem Format und mit welcher Semantik die Datenübertragung stattfindet.
“Versionieren” hat, wie oben bereits beschrieben, nicht nur mit dem reinen technischen Format zu tun, sondern auch mit der Semantik die der Schnittstelle zu Grunde liegt.
Im Folgenden wird nur noch von Nachricht bzw. Nachrichten gesprochen. Das sollen zum Einen asynchrone Nachrichten sein, aber auch die Nutzlast von synchronen Endpunkt-Aufrufen bezeichnen.
Rein technisch gesehen hat man 5 Patterns wie man eine Schnittstelle mit Versionsinformationen ausstatten kann bzw. wie man sicherstellen kann, das Änderungen am Format kein “breaking change” wird:
Beispiel:
Rest:
.../v1/article
.../v2/article
Async:
Version 1 -> Topic 1
Version 2 -> Topic 2
Beispiel JSON Event Format for
CloudEvents
- Der Payload der Nachricht besteht in diesem Fall aus einem, durch die
Cloud-Events Spezifikation beschriebenen, Container-Format. In den einzelnen
Feldern sind Metainformationen zur Nachricht enthalten. So auch unter dem Feld
datacontenttype
die Referenz zum Datenformat, sowie die eigentlichen Nutzdaten
unter data_base64
.
{
"specversion" : "1.0",
"type" : "com.example.someevent",
"source" : "/mycontext",
"id" : "A234-1234-1234",
"time" : "2018-04-05T17:31:00Z",
"comexampleextension1" : "value",
"comexampleothervalue" : 5,
"datacontenttype" : "application/vnd.apache.thrift.binary",
"data_base64" : "... base64 encoded string ..."
}
Beispiel AVRO Object Container Files. Hier wird in jeder Nachricht das Schema sowie eine Liste von entsprechenden Nachrichten verschickt.
{
"namespace": "com.acme",
"protocol": "HelloWorld",
"types": [
{"name": "Greeting", "type": "record", "fields": [
{"name": "message", "type": "string"}]},
{"name": "Curse", "type": "error", "fields": [
{"name": "message", "type": "string"}]}
],
"messages": {
"hello": {
"request": [{"name": "greeting", "type": "Greeting" }],
"response": "Greeting",
"errors": ["Curse"]
}
}
}
Beispiel HTTP-Content-Type - Hier werden neben den Nutzdaten in protokollabhängiger Weise Metainformationen als HTTP-Header Werte übertragen. In diesem Fall wird “nur” eine Referenz zur eigentlichen Formatbeschreibung übertragen.
PUT http://www.source-fellows.com/api/audi
Content-Type: application/vnd.mydatatypeV1+json
{
"Kennzeichen": "RT-X-123"
}
So ist das Internet gedacht und sollte so funktionieren :-)
Für welche Option man sich am Ende entscheidet ist vom Kontext abhängig. Für eine maximale Kompatibilität bieten sich, meiner Meinung nach, Option 2 und Option 4 an. Das ist auch der Weg, den, wie oben gezeigt, die Cloud-Events eingeschlagen haben.
In der Praxis wird oftmals auf das erste Pattern gesetzt, also ein Endpunkt pro Version, was in manchen Umfeldern sicher recht einfach eingesetzt werden kann. Kommen allerdings mehrere Producer ins Spiel, die wiederrum unterschiedliche Datenversionen verwenden, kann es auf Consumer-Seite ebenfalls recht komplex werden. Hier werden dann mehrere Endpunkte benötigt. Plötzlich steht man einer n:m Situation gegenüber. Jeder Endpunkt muss einzeln verwaltet werden, etc, etc…
Klar ist auf der anderen Seite auch, dass die auswertenden Systeme für die beiden anderen Varianten (Pattern 2 und 4) darauf vorbereitet sein müssen, unterschiedliche Datenformate zu verarbeiten. Auch hier entsteht Aufwand.
Wie immer: It depends… ;-)
25.01.2024
Der Author auf LinkedIn: Kristian Köhler und Mastodon: @kkoehler@mastodontech.de
Der praktische Soforteinstieg für Developer und Softwarearchitekten, die direkt mit Go produktiv werden wollen.
zur Buchseite beim Rheinwerk Verlag Rheinwerk Computing, ISBN 978-3-8362-7559-0 (als PDF, EPUB, MOBI und Papier)
Source Fellows GmbH
Lerchenstraße 31
72762 Reutlingen
Telefon: (0049) 07121 6969 802
E-Mail: info@source-fellows.com