Dual APIs – Events to APIs of the future
Klar ist: Das Arbeiten mit relationalen Datenbanken bleibt nicht immer einfach. Jeder, der bereits einen Application-Level-Cache vorschalten musste, um den Traffic auf die Datenbank zu minimieren, weiss, dass die zwei grossen Probleme dabei die folgenden sind:
- die Cache Invalidierung
- die Race-Conditions beziehungsweise das Dual-Write Problem.
Um Einträge zu invalidieren, muss entweder ein Algorithmus oder eine expiry time greifen. Solche Algorithmen sind meist erorr-prone. Ausserdem kann der expiry-time-Ansatz zu stale reads führen. Das bedeutet, dass die Daten auf der Datenbank bereits erneuert wurden. Um diesen Herausforderungen zu begegnen, könnten materialized-views auf der Datenbank eingesetzt werden. Das Grundproblem, weniger Traffic auf die Datenbank zu bekommen, wird dadurch aber nicht gelöst. Oft ist das zwar ausreichend, aber falls nicht, könnten distributed database eingesetzt und mit shards gearbeitet werden. Aber auch diese Komplexitätsebene ist herausfordernd – und daher mit Aufwand verbunden.
Event-Logs und Projektionen schaffen Abhilfe
Eine andere Möglichkeit ist die Verarbeitung von Events-Logs, also das Erstellen von «materialized-views» und Projektionen auf Applikationsseite. Dies kann auch als skalierbarer Application-Level-Cache gesehen werden, der aber mit den Vorteilen von materialized-views, also ohne die oben genannten Probleme zu den generellen Caches, einhergeht. Als Event-Log könnte etwa Kafka eingesetzt werden, der mit etwa einer Million Writes pro Sekunde nicht gerade gross ist. Das bringt wiederum Komplexität mit sich, denn es müssen mehrere Datenbanken auf der Infrastrukturebene orchestriert werden. Wird nur eine Datenbank eingesetzt, ist die Infrastruktur wieder auf dem gleichen Stand wie vorher und die Datenbank müsste skaliert werden. Zudem muss das Erstellen der Views nun auf Applikationsebene gehandhabt und skaliert werden. Konkret bedeutet das, dass auch die Eventverarbeitung entsprechend skaliert werden muss. Das Gute: Bei Kafka zum Beispiel ist die Partitionierung kein Nach-Gedanke, sondern durch das Design immer bereits zu Beginn angedacht. Kurz: Vieles der Komplexität aus der Distributed Systems Welt wird an die Entwickler exponiert, da Datenbanken von der Skalierbarkeit viel abstrahieren. Nicht ausser Acht zu lassen ist dabei auch das Know-how zu z. B. Kafka, das für den Entwicklungsprozess verfügbar sein muss.
Beide Systeme sind somit nicht perfekt, haben aber ihre Daseinsberechtigung. Beim Bauen von APIs sollte also berücksichtigt werden, dass die Host-Systeme der Nutzer unterschiedlich sein können und verschiedene Use Cases abgedeckt werden müssen.
Eine Dual API sollte somit immer mehrere Interfaces wie REST und Events anbieten. Auch Firmen wie Solace haben dies bereits integriert und nutzen es schon in ihren Pitches. Oft werden heutzutage auch Webhooks für Events genutzt. Sie verfehlen aber ihren Sinn, wenn, wie oben beschreiben, die Nutzer der API die eigene Projektion oder das eigene Readmodel und somit die Application-Layer «cache» anhand der konsumierten Events der API aufbauen wollen. Das kann dazu führen, dass sie nicht von Availability Problemen der API direkt betroffen sind. Die Garantie, dass keine Events verloren gehen und die Reihenfolge eingehalten wird, muss gegeben sein. Systeme wie Kafka garantieren das, Webhooks hingegen nicht. Wie eine solche Dual API aufgebaut werden könnte, schauen wir uns nun im Detail an.
Outbound via Outbox Pattern
APIs, die die Änderungen an Objekten nach aussen exponieren, sind hier eine Möglichkeit zur Umsetzung. Änderungen zu garantieren und konsistent nach aussen zu exponieren, ist nicht ganz trivial. Üblicherweise werden Events in ein Drittsystem geschrieben, welches Partitioning, Routing und viele Subscribers zulässt. Das Schreiben in ein solches Drittsystem ist wie bei jeder API nie fehlerfrei. Network Errors, System failures auf beiden Seiten und noch mehr können vorkommen – es existiert also ein Dual-Write Problem. Aus der Sicht des Services müssen die User in die Datenbank und das Drittsystem schreiben. Wenn das eine oder andere fehlschlägt, entstehen Inkonsistenzen – auch hier wieder: das Dual-Write Problem.
Hier kommt das Outbox Pattern ins Spiel. Der alternative Weg via Outbox Pattern bedeutet, die applizierten CRUD Operationen nach dem Schreiben in die Datenbank durch ein Bindeglied nach aussen zu exponieren. Üblicherweise wird atomically in eine Event-Tabelle (outbox table) geschrieben. Das Bindeglied konsumiert diese Events aus der Tabelle – üblicherweise vom WAL Log oder via Mechanismen zur Skalierbarkeit ohne Queries – und leitet diese an das Drittsystem. Vom Microservice selbst wird somit weiterhin eine transaktionsfähige Datenbank verwendet. Die Operationen, auf die Outbox-Tabelle allerdings vom Bindeglied konsumiert und ans Drittsystem weitergeleitet. Exponiert der Microservice nun zusätzlich noch ein REST-Interface, entsteht eine Dual API, also eine API, die via REST als auch via Events konsumierbar ist. Je nach Zielsystem und Anwendungsfall dessen kann dies zu erweitertem Potenzial der eigenen API und somit des eigenen Angebots führen. Auch kann es die Kommunikation zwischen eventbasierten Microservices stabilisieren, da eine REST Kommunikation und das Vertrauen auf externe Readmodels zwischen Mircoservices nachteilig sein. Durch diesen Ansatz können transaktionsfähige Datenbanken weiterhin erfolgreich eingesetzt werden, ohne auf die Events nach aussen zu verzichten.
Falls die Konsistenz der beiden APIs im Use Case eine hohe Relevanz hat, ist wichtig, dass nicht direkt WAL-Log Einträge, sondern effektive Änderungen – commited operations – ans Drittsystem weitergeleitet werden. Nicht jede distributed relationale / distributed NoSQL Datenbank unterstützt dies. MongoDB zum Beispiel bietet „change stream events“ – ein Feature, welches hierzu gebaut wurde. Auch nicht jedes Bindeglied unterstützt dies. Je nach Anwendungsfall ist also wichtig, für Dual APIs die richtige Datenbank sowie Bindeglieder zu nutzen.
Bindeglieder – CDC (change data capture)
Um in der Praxis die Kombination erfolgreich zu nutzen, sind Bindeglieder zwischen der Datenbank und einem Sink – ein Drittsystem/ Subsystem – notwendig, von welchem die API-Nutzer Events konsumieren. Diese Bindeglieder konsumieren die CRUD Operationen, die bei MongoDB zum Beispiel via das „change event stream“ Feature exponiert werden und lesen diese mit einem persistierten Cursor, um keine Events zu verpassen. Der Prozess wird auch abgekürzt CDC (change data capture) genannt. Diese Bindeglieder sind distributed und fault tolerant. Sie bieten somit die Garantie, dass wir Events at-least-one oder exactly-once – je nach Framework – in ein Subsystem schreiben können. Als Subsystem, an welches API-Konsumenten andocken können, eignen sich einige, unter anderem Kafka. Kafka ist in der Lage, Events persistent abzulegen, wovon event-sourced Architekturen profitieren. Die Idee dahinter ist es, möglichst vielen Host-System-Architekturen das Leben einfacher zu machen. Zu den bekannten Bindeglied Frameworks zählen unter anderem Kafka Connect, Debezium und Strimzi.
Inbound via REST and commands
Eine Dual API kann im Ökosystem via zwei Interfaces angesteuert werden: über eine klassische REST-Schnittstelle sowie die bis heute eher wenig genutzte Command-Schnittstelle. Je nach Anwendungsfall und Architektur des nutzenden Systems kann somit eine API in dessen Pattern geboten werden.
REST and events combined
Immer üblicher wird es, dass gewisse Informationen live konsumiert werden wollen, da zum Beispiel zeitkritische Entscheidungen gefällt werden müssen oder durch availability concerns gewisse Datenstände aus der API auf Konsumentenseite repliziert werden müssen. Anstatt via REST ein Polling und Diffing zu implementieren, helfen hier Outbound Queues, die konsumiert werden können.
The call for MetaAPIs
Metadaten einer API beschreiben die ein- und ausgehenden Schnittstellen und können auch Datenformate sowie SLA umschreiben. Mit optimierten Metadaten lassen sich Koordinierungsaufwände wie z. B. Versions-Upgrades von APIs verringern. Sie können die Agilität in der Entwicklung um vieles vereinfachen. Das Auslesen von Metadaten direkt aus der API sind bis heute in der Praxis leider selten anzutreffen. Oft sehen wir Swagger-Files zum Beispiel auf Swaggerhub, die bedauerlicherweise nur die API Endpunkte beschreiben. Unsere Annahme ist, dass die Relevanz von Meta-APIs, integriert z. B. als Endpunkt der API, sich mit dem zunehmenden Anwendungsbereich von synchronen und asynchronen APIs jedoch erhöhen, da sie aktiv dazu beitragen, Entwicklungszyklen zu beschleunigen.
- Für REST APIs ist OpenAPI der heutige Meta-API-Standard
- Für Event APIs sind schema registries und AsyncAPI der heutige Meta-API-Standard
AsyncAPI ist es bewusst, dass Dual APIs immer populärer werden. Aus diesem Grund wurde die Roadmap so ausgerichtet, dass sie OpenAPI, GraphQL und RPC API Formate mit AsyncAPI vereinen wollen. Wie AsyncAPI zeigt, ist der Trend zu erkennen, dass Entwickler die Wichtigkeit von Metadaten erkannt haben. Das Ökosystem rund um Metadaten wächst und Standards etablieren sich. Mit den heutigen Standards wird man nicht um eine getrennte Beschreibung der API via OpenAPI und AsyncAPI herumkommen. Das Bild sollte sich allerdings schon bald zum Besseren wenden.
Bei Metadaten Umschreibungen der Zukunft sollten jedoch nicht nur die Endpunkte und Datenformate abgerufen werden können, sondern auch Limitierungen oder SLAs eingesehen werden können. Deprecations, Updates und neue angefügte Endpunkte von Schnittstellen sollten ebenfalls abonniert werden können. IDEs sollten sich die Schemas von in der code base genutzten APIs holen können. Notifizierungen in den IDEs sowie auch generelle Push-Notifikationen für Änderungen sollten abonnierbar sein, um die Entwicklung zu erleichtern. Weiter sollten Mock-Datensätze generiert werden können, um Tests zu vereinfachen. Auch Monitoring und Informationen zur Nutzung der API könnten via Metadaten-Schnittstellen gelesen werden.
Which outbound queue – Kafka the standard
Kafka hat sich im Bereich Event-Streaming als Standard etabliert und bietet heute mit Confluence-Cloud gute, einfach skalierende Optionen , um Inbound Commands sowie auch Outbound Events in sogenannte Topics bzw. Queues zu schreiben. Während das Schreiben sehr einfach ist, ist das Konsumieren etwas komplexer, dafür aber auch höchst skalierbar. Für die Outbound-Queues ist somit abzuwägen, ob das Entwickler-Team mit der Komplexität von Kafka umgehen kann und diese gegen aussen an die API-Nutzer exponieren will. Weiter ist es auch wichtig zu wissen, dass Kafka nicht für unendlich viele Subscriber gedacht ist. Man kann das leicht umgehen, in dem man z. B. pro API-Key ein neues Topic aufsetzt. Sobald KRaft live und somit Zookeeper entfernt ist von Kafka, sollte dies kein Problem mehr sein. Egal welcher Eventbroker schlussendlich aber unter der API steckt, wäre es hilfreich, wenn sich hier ein Standard etabliert und das unterliegende System abstrahiert wird für den End-Konsumenten.
Fazit: Dual-APIs werden immer bedeutender
Längst ist es vielen Firmen bekannt, dass APIs zum Kernangebot ihres PaaS / SaaS Angebotes gehören. Firmen sind sogar schon so weit, dass API-First Applikationen gebaut werden und das Bauen von UIs oft schon den Kunden überlassen wird. Heute sind somit oft APIs statt effektiver Applikationen mit einem Frontend ein wichtigerer Bestandteil von Firmen. Mit dem Umdenken zu API-First wird es somit immer wichtiger, es den Kunden zu ermöglichen, dass sie die API passend für ihre unterliegende Architektur nutzen und integrieren können – sei dies nun via REST oder Events. Beide Schnittstellen haben ihre Anwendungsfälle. Mit Dual APIs kann somit das Angebot für den Kunden vergrössert werden.Wir beraten Sie gerne bei der Umsetzung und stehen Ihnen bei Fragen zum Thema zur Verfügung. Sprechen Sie uns einfach über unser Kontaktformular an und wir melden uns bei Ihnen zurück.Kennen Sie schon unseren Blog? Hier finden Sie weitere Texte rund um die Themen Stream Oriented Architectures, Big Data und Event Sourcing.
📝 Gefällt dir, was du liest?
Dann lass uns in Kontakt treten! Unser Engineering-Team wird sich so schnell wie möglich bei dir melden.