Verträge, Adressierung und APIs für Microservices

Regions-ID

REGION_ID ist ein abgekürzter Code, den Google anhand der Region zuweist, die Sie beim Erstellen Ihrer Anwendung ausgewählt haben. Der Code bezieht sich nicht auf ein Land oder eine Provinz, auch wenn einige Regions-IDs häufig verwendeten Länder- und Provinzcodes ähneln können. Bei Anwendungen, die nach Februar 2020 erstellt wurden, ist REGION_ID.r in den App Engine-URLs enthalten. Bei Anwendungen, die vor diesem Datum erstellt wurden, ist die Regions-ID in der URL optional.

Hier finden Sie weitere Informationen zu Regions-IDs.

Mikrodienste in App Engine rufen sich normalerweise gegenseitig mit HTTP-basierten RESTful APIs auf. Sie können Mikrodienste auch im Hintergrund mit Aufgabenwarteschlangen aufrufen. Dabei gelten die in diesem Artikel beschriebenen Grundsätze für die Erstellung einer API. Sie sollten sich an bestimmte Muster halten, damit Ihre auf Microservices basierende Anwendung stabil, sicher und leistungsfähig ist.

Starke Verträge einsetzen

Einer der wichtigsten Aspekte von auf Microservices basierenden Anwendungen ist die Fähigkeit, Microservices vollkommen unabhängig voneinander bereitzustellen. Um diese Unabhängigkeit zu erreichen, muss jeder Microservice seinen Clients – anderen Microservices – einen klar definierten versionierten Vertrag bereitstellen. Diese versionierten Verträge dürfen von keinem der Dienste gebrochen werden, bis bekannt ist, dass kein anderer Microservice auf einen bestimmten, versionierten Vertrag angewiesen ist. Bedenken Sie, dass andere Microservices möglicherweise auf eine vorherige Codeversion zurückgesetzt werden müssen, die einen älteren Vertrag erfordern. Diese Tatsache sollte in Ihren Einstellungs- und Deaktivierungsrichtlinien berücksichtigt werden.

Der Aufbau einer Kultur rund um starke, versionierte Verträge ist wahrscheinlich die größte organisatorische Herausforderung einer stabilen, auf Microservices basierenden Anwendung. Die Entwickler müssen verinnerlichen, wann eine abwärtskompatible und wann eine nicht abwärtskompatible Änderung erforderlich ist. Sie müssen wissen, wann eine neue Hauptversion benötigt wird. Sie müssen verstehen, wie und wann ein alter Vertrag ausgemustert werden kann. Mitarbeiter müssen geeignete Kommunikationstechniken einsetzen, einschließlich Einstellungs- und Deaktivierungsbenachrichtigungen, um auf Änderungen der Microserviceverträge aufmerksam zu machen. Auch wenn dies nach einer schweren Aufgabe klingt, werden Sie im Laufe der Zeit große Verbesserungen bei der Geschwindigkeit und der Qualität erzielen, wenn Sie diese Verfahren in Ihre Entwicklungskultur einarbeiten.

Microservices adressieren

Dienste und Codeversionen können direkt adressiert werden. Das bedeutet, dass Sie neue Codeversionen Seite an Seite mit vorhandenen Codeversionen bereitstellen und neuen Code testen können, bevor Sie ihn als Standardversion zur Bereitstellung festlegen.

Jedes App Engine-Projekt hat einen Standarddienst und jeder Dienst hat eine Standardcodeversion. Adressieren Sie mit der folgenden URL den Standarddienst der Standardversion eines Projekts:
https://PROJECT_ID.REGION_ID.r.appspot.com

Wenn Sie einen Dienst namens "user-service" bereitstellen, können Sie über die folgende URL auf die Standardbereitstellungsversion dieses Dienstes zugreifen:

https://user-service-dot-my-app.REGION_ID.r.appspot.com

Wenn Sie eine zweite, nicht standardmäßige Codeversion namens "banana" an den Dienst user-service bereitstellen, können Sie direkt über die folgende URL auf diese Codeversion zugreifen:

https://banana-dot-user-service-dot-my-app.REGION_ID.r.appspot.com

Beachten Sie, dass Sie über die folgende URL auf diese Codeversion zugreifen können, wenn Sie eine zweite, nicht standardmäßige Codeversion namens "cherry" an den Dienst default bereitstellen:

https://cherry-dot-my-app.REGION_ID.r.appspot.com

App Engine erzwingt die Regel, dass sich die Namen von Codeversionen im Standarddienst nicht mit Dienstnamen überschneiden dürfen.

Die direkte Adressierung spezifischer Codeversionen sollte nur für Smoke Tests und zur Vereinfachung von A/B-Tests, Rollforwards und Rollbacks genutzt werden. Stattdessen sollte der Clientcode nur die standardmäßige Bereitstellungsversion des Standarddienstes oder eines bestimmten Dienstes adressieren:


https://PROJECT_ID.REGION_ID.r.appspot.com

https://SERVICE_ID-dot-PROJECT_ID.REGION_ID.r.appspot.com

Dieser Adressierungsstil ermöglicht es den Microdiensten, neue Versionen ihrer Dienste bereitzustellen, einschließlich Fehlerbehebungen, ohne dass Änderungen an Clients erforderlich sind.

API-Versionen verwenden

Jede Microservice-API sollte eine API-Hauptversion in der URL haben, zum Beispiel:

/user-service/v1/

Mit dieser API-Hauptversion kann in den Logs eindeutig identifiziert werden, welche API-Version des Microservice aufgerufen wird. Außerdem liefert die Angabe der API-Hauptversion verschiedene URLs, sodass neue API-Hauptversionen Seite an Seite mit alten API-Hauptversionen bereitgestellt werden können:

/user-service/v1/
/user-service/v2/

Die API-Nebenversion muss nicht in die URL aufgenommen werden, da in API-Nebenversionen per Definition keine nicht abwärtskompatiblen Änderungen eingeführt werden. Die Angabe der API-Nebenversion würde die Anzahl der URLs unnötig erhöhen und Ungewissheit im Hinblick auf die Kompatibilität eines Clients mit einer neuen API-Nebenversion erzeugen.

Beachten Sie, dass in diesem Artikel von einer Umgebung für kontinuierliche Einbindung und Bereitstellung ausgegangen wird, in der der Hauptzweig immer in App Engine bereitgestellt wird. In diesem Artikel werden zwei unterschiedliche Versionskonzepte behandelt:

  • Codeversion: Verweist direkt auf eine App Engine-Serviceversion und stellt ein bestimmtes Commit-Tag des Hauptzweigs dar.

  • API-Version: verweist direkt auf eine API-URL und stellt die Form der Anfrageargumente, die Form des Antwortdokuments und das API-Verhalten dar.

In diesem Artikel wird außerdem davon ausgegangen, dass bei einer Bereitstellung mit einem einzigen Code sowohl die alte als auch die neue Version einer API in einer gemeinsamen Codeversion implementiert werden. Beispielsweise kann Ihr bereitgestellter Hauptzweig /user-service/v1/ und /user-service/v2/ implementieren. Wenn Sie neue Neben- und Patchversionen bereitstellen, können Sie den Traffic mithilfe dieser Methode auf zwei Codeversionen aufteilen, unabhängig von den API-Versionen, die durch den Code tatsächlich implementiert werden.

Ihre Organisation kann sich dafür entscheiden, /user-service/v1/ und /user-service/v2/ in verschiedenen Codezweigen zu entwickeln. Es kann also kein Code-Deployment beides gleichzeitig implementieren. Das Modell kann auch in App Engine angewendet werden. Zum Aufteilen des Traffics müssten Sie dann jedoch die API-Hauptversion in den Dienstnamen selbst verschieben. Ihre Clients würden zum Beispiel folgende URLs verwenden:

http://user-service-v1.my-app.REGION_ID.r.appspot.com/user-service/v1/
http://user-service-v2.my-app.REGION_IDappspot.com/user-service/v2/

Die API-Hauptversion wird in den Dienstnamen selbst verschoben, beispielsweise user-service-v1 und user-service-v2. (Die /v1/-, /v2/-Teile des Pfades sind in diesem Modell redundant und können entfernt werden, obwohl sie für die Loganalyse möglicherweise trotzdem nützlich sind.) Dieses Modell erfordert ein bisschen mehr Arbeit, da Sie wahrscheinlich Ihre Bereitstellungsskripts aktualisieren müssen, um neue Dienste bei wichtigen Änderungen der API-Version bereitzustellen. Außerdem sollten Sie die maximale Anzahl zulässiger Dienste pro App Engine-Anwendung im Auge behalten.

Abwärtskompatible und nicht abwärtskompatible Änderungen

Es ist wichtig, den Unterschied zwischen einer abwärtskompatiblen und einer nicht abwärtskompatiblen Änderung zu verstehen. Nicht abwärtskompatible Änderungen sind oft subtraktiv. Das bedeutet, dass sie einen Teil des Request- oder Antwortdokuments entfernen. Wenn Sie die Form des Dokuments oder den Namen der Schlüssel ändern, kann dies zu einer nicht abwärtskompatiblen Änderung führen. Neue erforderliche Argumente stellen immer nicht abwärtskompatible Änderungen dar. Nicht abwärtskompatible Änderungen können auch auftreten, wenn sich das Verhalten des Microservice ändert.

Abwärtskompatible Änderungen sind tendenziell additiv. Ein neues optionales Request-Argument oder ein neuer zusätzlicher Abschnitt im Antwortdokument sind abwärtskompatible Änderungen. Zur Durchführung abwärtskompatibler Änderungen ist die Wahl der Serialisierung bei der Übertragung von entscheidender Bedeutung. Viele Serialisierungsmethoden sind für abwärtskompatible Änderungen geeignet: JSON, Protocol Buffers oder Thrift. Bei der Deserialisierung ignorieren diese Serialisierungen zusätzliche, unerwartete Informationen. In dynamischen Sprachen werden die zusätzlichen Informationen einfach im deserialisierten Objekt angezeigt.

Betrachten Sie die folgende JSON-Definition für den Dienst /user-service/v1/:

{
  "userId": "UID-123",
  "firstName": "Jake",
  "lastName": "Cole",
  "username": "jcole@example.com"
}

Bei der folgenden einschneidenden Änderung müsste der Dienst als /user-service/v2/ neu versioniert werden:

{
  "userId": "UID-123",
  "name": "Jake Cole",  # combined fields
  "email": "jcole@example.com"  # key change
}

Die folgende abwärtskompatible Änderung erfordert jedoch keine neue Version:

{
  "userId": "UID-123",
  "firstName": "Jake",
  "lastName": "Cole",
  "username": "jcole@example.com",
  "company": "Acme Corp."  # new key
}

Neue abwärtskompatible API-Nebenversionen bereitstellen

Bei der Bereitstellung einer neuen API-Nebenversion ermöglicht App Engine die gleichzeitige Freigabe der neuen und der alten Codeversion. In App Engine können Sie zwar jede der bereitgestellten Versionen direkt aufrufen, aber nur eine Version ist die Standardversion für die Bereitstellung; denken Sie daran, dass es für jeden Dienst eine Standardversion für die Bereitstellung gibt. In diesem Beispiel verwenden wir die alte Codeversion apple, die die Standardbereitstellungsversion ist, und stellen die neue Codeversion als Nebenversion namens "banana" bereit. Beachten Sie, dass die Mikrodienst-URLs für beide dieselbe /user-service/v1/ haben, da wir eine einfache API-Änderung an der API bereitstellen.

App Engine bietet Mechanismen zur automatischen Migration des Traffics von apple zu banana. Dazu wird die neue Codeversion banana als Standardbereitstellungsversion markiert. Wenn die neue Standardbereitstellungsversion festgelegt ist, werden keine neuen Anfragen an apple weitergeleitet und alle neuen Anfragen werden an banana weitergeleitet. Auf diese Weise führen Sie ein Rollforward auf eine neue Codeversion aus, die eine neue API-Neben- oder API-Patchversion ohne Auswirkungen auf die Client-Microservices implementiert.

Bei einem Fehler wird die Wiederherstellung rückgängig gemacht, indem der obige Prozess umgekehrt wird: Setzen Sie die Standardbereitstellungsversion auf die alte zurück, apple in unserem Beispiel. Alle neuen Anfragen werden an die alte Codeversion weitergeleitet und keine neuen Anfragen werden an banana weitergeleitet. Aktive Requests können abgeschlossen werden.

Mit App Engine können Sie außerdem nur einen bestimmten Prozentsatz des Traffics an die neue Codeversion weiterleiten. Dieser Vorgang wird in App Engine oft als Canary-Release und das Verfahren als Trafficaufteilung bezeichnet. Sie können 1 %, 10 %, 50 % oder einen beliebigen Prozentanteil des Traffics an Ihre neuen Codeversionen leiten und können diesen Anteil im Laufe der Zeit anpassen. Sie könnten zum Beispiel eine neue Codeversion über einen Zeitraum von 15 Minuten bereitstellen, den Traffic dabei langsam erhöhen und auf Probleme achten, die ein Rollback erforderlich machen könnten. Mit demselben Mechanismus können Sie A/B-Tests für zwei Codeversionen durchführen: Legen Sie für die Trafficaufteilung 50 % fest und vergleichen Sie die Leistungs- und Fehlerratenmerkmale der beiden Codeversionen, um die erwarteten Verbesserungen zu bestätigen.

Die folgende Abbildung zeigt Einstellungen für die Trafficaufteilung in der Google Cloud Console:

Einstellungen zur Trafficaufteilung in der Google Cloud Console

Neue nicht abwärtskompatible API-Hauptversionen bereitstellen

Wenn Sie nicht abwärtskompatible API-Hauptversionen bereitstellen, ist der Rollforward- bzw. der Rollback-Vorgang derselbe wie für abwärtskompatible API-Nebenversionen. Normalerweise werden Sie jedoch keine Trafficaufteilung oder A/B-Tests machen, da die funktionsgefährdende API-Version eine neu veröffentlichte URL ist, beispielsweise /user-service/v2/. Haben Sie die zugrunde liegende Implementierung der alten API-Hauptversion geändert, sollten Sie die Trafficaufteilung möglicherweise trotzdem verwenden, um zu testen, ob die alte API-Hauptversion weiterhin wie erwartet funktioniert.

Bei der Bereitstellung einer neuen API-Hauptversion sollten Sie immer daran denken, dass möglicherweise auch noch alte API-Hauptversionen bereitgestellt werden. Beispiel: /user-service/v1/ wird möglicherweise noch bereitgestellt, wenn /user-service/v2/ veröffentlicht wird. Dieser Umstand ist ein wesentlicher Aspekt unabhängiger Codefreigaben. Sie können alte API-Hauptversionen erst deaktivieren, nachdem Sie überprüft haben, dass sie von keinem anderen Microservice benötigt werden. Dies beinhaltet Microservices, die möglicherweise auf eine ältere Codeversion zurückgesetzt werden müssen.

Stellen Sie sich beispielsweise vor, dass Sie einen Mikrodienst namens "web-app" haben, der von einem anderen Mikrodienst namens "user-service" abhängt. Stellen Sie sich weiterhin vor, user-service muss eine zugrunde liegende Implementierung ändern, die eine Unterstützung der von web-app derzeit verwendeten alten API-Hauptversion unmöglich macht, beispielsweise das Minimieren von firstName und lastName in ein einzelnes Feld namens "name". user-service muss also eine alte API-Hauptversion deaktivieren.

Für diese Änderung müssen drei separate Bereitstellungen vorgenommen werden:

  • Zuerst muss user-service /user-service/v2/ bereitstellen und gleichzeitig immer noch /user-service/v1/ unterstützen. Diese Bereitstellung erfordert möglicherweise das Schreiben von temporärem Code, um für Abwärtskompatibilität zu sorgen. Dies ist bei auf Microservices basierenden Anwendungen eine häufige Konsequenz.

  • Als Nächstes muss web-app aktualisierten Code bereitstellen, der die Abhängigkeit von /user-service/v1/ zu /user-service/v2/ ändert.

  • Wenn Team user-service bestätigt hat, dass web-app /user-service/v1/ nicht mehr benötigt und web-app kein Rollback mehr durchführen muss, kann das Team Code bereitstellen, der den alten /user-service/v1/-Endpunkt und den temporären Code zur Unterstützung entfernt.

All diese Aktivitäten mögen aufwendig wirken, sind jedoch ein unerlässlicher Teil für auf Microservices basierende Anwendungen und stellen den Prozess dar, der unabhängige Freigabezyklen bei der Entwicklung erst ermöglicht. Auch wenn dieser Artikel einen anderen Eindruck vermittelt, kann jeder der oben beschriebenen Schritte zeitlich unabhängig ausgeführt werden und die Rollforward- und Rollback-Prozesse beschränken sich auf einen einzigen Microservice. Nur die Reihenfolge der Schritte ist festgelegt. Die Schritte selbst können über viele Stunden, Tage oder sogar Wochen hinweg stattfinden.

Weitere Informationen