Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Microservice Workflows mit Consumer-Driven-Contracts

Microservice Workflows mit Consumer-Driven-Contracts

Allen voran geht es mir um die Vereinfachung der Softwareentwicklung auch auf Systemebene. Ich meine die folgenden Artikel zeigen zumindest mittel- und langfristig genau das Gegenteil und sind ein gutes Beispiel ausufernder Systemlandschaften durch selbstverschuldet aufgesetzte Komplexität.

Wer Microservice richtig richtig macht, braucht keine Workflow Engine, kein BPMN, kein ESB, kein Message Broker, keine Aktor-Modelle und Frameworks, keine Enterprise Application Server, kein Event-Sourcing, kein CQRS, kein Event-Driven Enterprise FooBar.

Im Kern geht es in den Artikeln um Interaktion zwischen verteilten Systemen aka Microservices und deren Ochestrierung bzw. Choreografie hin zu Prozessen bzw. Workflows.

Ich meine, für ein aufgabengerechtes Microservice Design werden lediglich Grundlagenkenntnisse in DDD/OOA, Consumer-Driven-Contracts und HTTP benötigt. Die praktische Umsetzungen begnügt sich mit einer leichtgewichtige HTTP Server Implementierung wie bsw. NodeJS, Flask oder Nancy.

Meine Kommentare zu den Artikeln

Die pure Anzahl der genannten Lösungen, Konzepte und Technologien bringt mich leider in die Verlegenheit, nur einen Teil kommentieren zu können.

Tobias gibt sich mühe, aber ihm gelingt aus meiner Sicht weder die visuelle Darstellung noch die "Code nahe" Umsetzung der nötigen Prozessschritte und dessen Reihenfolge. Kein Wort zu Edge-Cases wie Race-Conditions, Message-Laufzeiten, Message-Versionen und Zustandshaltung. Mir bleibt unklar wann, welche Nachricht eintrifft und wie sich der interne Zustand wann und wie ändert. Eine lockere Grafik ist nett, aber ein Zustandsautomat hätte es aus meiner Sicht besser auf den Punkt gebracht. Mich überzeugen Ralfs visuelle Darstellung da schon eher.

Autonomy und Reduktion

Tobias verwendet nahezu inflationär Messages aka Events/Commands und Handler aka Microservices. Seine Grafik zeigt eindrucksvoll wie komplex ein Zusammenspiel für einen kleinen Workflow werden kann. Zumindest bemerkt er am Ende seines Artikels die Problematik und gibt am Ende ein passendes Fazit dazu ab. Unerwähnt bleibt leider auch hier die passende Architektur zur praktischen Umsetzung. Der Leser wird allein mit jeder Menge Messaging-FooBar zum "ausprobieren" gelassen.

Verteilte Systeme

Ralfs Beitrag geht eher den konzeptionellen Weg. Er versucht das Problem erst einmal "lokal" aber event getrieben lösen zu wollen. Das ist vom Ansatz her nicht schlecht, doch nur der kleinste Teil der Aufgabe. Ralf beschreibt Prozessfluss durch Events in einem lokalen "Lebensraum". Er blendet in seinen Erläuterungen die Kommunikation zwischen abgeschlossenen verteilten Systemen mittels "Das Microservicespezifische ist dann drumherum zu wickeln." und "Onion Architektur" einfach aus. Interessant, aber genau genommen der genau gegenteilige Designansatz zu Microservices.

Microservices werden von "außen" nach "innen" entworfen und implementiert.

Das heißt, die Konsumentenschnittstellen (API-Contract) und das Runtime-Environment eines Microservices sind weit "wichtiger" als die interne Implementierung. Ich meine, genau hier liegt auch der Wert einer Microservice Architektur. Es wird sich von Anfang an auf die Lauffähigkeit und einfache Verwendung im Gesamtsystem konzentriert. Am Anfang steht eben der "konsumentenfreundliche" und lauffähige API-Kontrakt als Consumer-Driven-Contract.

Microservices sind keine Nanoservices

Leider bleibt der Grund des gewählten Schnitts der Domänen, also Consistency Boundary und Bounded Context, in Tobias Design unklar. Z.B. die Stornierung einer Bestellung ist offensichtlich elementarer Teil des Bestellvorgangs. Warum in seiner beschriebenen Umsetzung nicht? Warum werden mehrere "Wrapper-Microservices" als "Event-Erzeuger" in seinen Grafiken sichtbar? In seinem Beispiel scheint es irgendwie in Ordnung zu sein, in der Realität haben diese -IMHO unnötigen- Entscheidung meist große Auswirkung auf die Komplexität und Wartbarkeit.

Microservices müssen eine "konsumentenfreundlich" Schnittstelle bzw. API haben

Natürlich können Daten als Events zwischen verteilten Funktionseinheiten aka Microservices "fließen". Letztlich ist jeder technisch angehauchte Ansatz, micro wie makro, darauf reduzierbar. Wie und warum verteilte Systeme und deren Kommunikation über die gewünschten Grenzen mit Konsumenten hinweg entstehen, muss neben der Geschäftsdomänenanalyse und Systemdesigns der Anfang einer konkreten konsumentengetriebenen API sein. Richtlinien und Regeln der Kommunikation entstehen bei genügender Akzeptanz und Breite aus der Notwendigkeit des gemeinsamen Erreichens eines Ziels heraus. Eine Kommunikationsschnittstelle muss demzufolge flexibel genug sein, um sehr viele unterschiedliche Entwicklungsrichtungen über die Zeit dauerhaft zu ermöglichen.

Kommunikation über C# Events sind es nicht. Workflow-Engines zu proprietär. Message Broker/Bus schon eher, leider jedoch nicht breit genug aufgestellt und meist zentralisiert organisiert. Microservice Architektur hat ein wichtiges Ziel: Entkopplung von Abhängigkeiten auf allen Ebenen. Bleibt End-2-End Kommunikation. Für mich das Mittel der Wahl. Nun kann man sich über die Kommunikationsprotokolle noch streiten. Ich für meinen Teil wähle HTTP und JSON. Sie sind einfach, flexibel und sehr weit verbreitet.

Microservices sind explorativ

Kleine oder große Bildchen mit Pfeilen sind ein Mittel der zwischenmenschlichen Kommunikation eines möglichen Designs. Da stimmen Ralf, Tobias und ich offenbar überein. Sicher mag es sehr stabile Geschäftsdomänen mit hohem Dokumentationsbedarf geben. Genau dafür gibt es dann auch UML, BPMN und Co. Die Konzepte, Empfehlungen, Regel und Richtlinien hinter Microservices bemühen sich jedoch, auf Domänen mit vielen, kleinen und schnelle Änderungen mittels Fail-Fast zu reagieren. Heißt, an den meisten Tagen wird ein autonomer Microservice schneller entwickelt und vollständig in Produktion gestellt als ein genaues Designdiagramm erstellt wurde.

Autonomie von Microservices

Tobias erwähnt am Anfang seines Artikels den Einsatz von Event-Souring. Neben der kritisch zu sehenden Systemrelevanz fehlt der Grund für diese Empfehlung. Der von Tobias häufiger erwähnte "Rückfall" in ein Pull-Model ist meist die logische Konsequenz einer übermäßig komplexen und schwer verständlichen asynchronen Kommunikationen zwischen verteilten Systemen. Wie Tobias den aktuellen Zustand des Bestellprozesses abfragen kann, ist dann wohl die Lösung des Event-Source Geheimnis. Zustandsabfrage wäre in einem Event-Driven Szenario ohne den Einsatz eines Event-Log nicht möglich und ist offensichtlich der Grund der Empfehlung. Wie sinnvoll damit umgegangen wird bleibt leider offen.

Kurzum bleiben zu viele praktische Herausforderungen an eine Event getriebene Architektur unausgesprochen auch wenn nur der Happy-Day geschildert wird. In diesem Punkt scheinen mir die Argumentation zu Workflow-Engines von Bernd Rücker, Daniel Meyer praxistauglicher und vermitteln den Schluss, dass es ohne Gehirnschnecke, Megainfrastruktur sowie technisch konzeptionellem Overkill bsw. zentrale Message-Broker und Event-Sourcing Datenbanken zumindest schnell und irgendwie einfach funktionieren kann. Ich teile den Einsatz einer oder mehrerer dedizierter oder integrierter Workflow-Engines zur Ochestrierung verteilter Systeme und das Big-Design-Upfront mittels BPMN zwar nicht, dennoch wurden ganz treffend die zunehmende Komplexität asynchroner Kommunikation und gute praxistaugliche Alternativen wie Datenreplikation angesprochen. Die Abbildung von koordinierten Geschäftsvorgängen ist Teil der Geschäftsdomäne, damit Teil des Bounded Context und damit Teil des Microservice. Autonomie und Datenhoheit auf allen Ebenen, einfache aber breite Kommunikationsprotokollunterstützung gepaart mit expliziten Input-, Output- und Fehler- Kontrakten entkoppeln das Gesamtsystem und reduzieren die Abstimmungsaufwände.

Contract API Drafts

Als Einstieg wähle ich eine sehr einfache deklarative Form der Systembeschreibung mittels Resource-URLs, statt abstrakter und meist Herstellergetriebener UML/BPMN Diagramme oder Workflow-Engines. Von einer kleinen Design-Skizze abgesehen, lenkt nichts vom eigentlichen Zweck der "Konsumentengetriebenen API" Entwicklung ab. Nachfolgend ein erster HTTP/JSON-API basierter Systementwurf im JSON Format.

{
  "orders_url" : "https://ecommerce.service.io/orders{/order_id}",
  "reservations_url" : "https://ecommerce.service.io/reservations{/order_id}",
  "payments_url" : "https://ecommerce.service.io/payments{/order_id}",
  "shipments_url" : "https://ecommerce.service.io/shipments{/order_id}"
  "status_url" : "https://ecommerce.service.io/status{/order_id}"
}

Diese Beschreibung enthält mit seinen impliziten Konventionen bereits so viele Informationen, um den geübten HTTP/RESTful API Entwickler eine ressourcenbasierte Vorstellung der involvierten Anwendungssysteme aka Microservices und erste Details der möglichen Operationen darauf zu geben. Oft kann mit einer ersten Implementierung der Konsumenten bereits begonnen werden. Bei ersten Integrationstests helfen Mocks der Kontrakt-Implementierungen. Diese Art der rudimentären Definition der Konsumenten-Kontrakte ist für den ersten Einstieg ausreichend. In die praktische Umsetzung müssen Schritt für Schritt und immer aus der Sicht und dem Nutzen der API Konsumenten die Kontrakte verfeinert werden. Da dies auf konzeptioneller Ebene in den genannten Artikeln bereits zu einem großem Teil geschehen ist, belasse ich es vorerst bei den Einstiegspunkten in die API.

Beispiel einer Microservice API Implementierung

service.get('/orders/:id?', (req,res) => {
  res.send({});  
});

service.post('/orders', (req,res) => {
  res.status(201).send({});  
});

Beispiel eines Konsumenten

request.get('https://ecommerce.service.io/orders')
  .then(result => {
    console.log(result);
  });

Microservices Architekturen adressieren vor allem einen schnellen und flexiblen Integrationsprozess von verteilten Systemen.

In der Praxis (JavaScript) helfen zusätzliche Frameworks wie Nock oder Consumer-Contracts den Entwicklungsprozess und nötige Integrations-Tests schnell und einfach zu entkoppeln.

Verteilte Microservice Workflows

Wie angesprochen möchte ich zeigen, dass mit einfachsten Mitteln Ochestrierung oder auch Choreografie mehrerer Microservices hin zu einem komplexeren Prozess ohne weiteres möglich ist. Dabei braucht sich der ausschließliche Einsatz des HTTP-Protokolls hinter den genannten Lösungen wie Workflow-Engines, ESB oder Message-Broker in Punkto Stabilität, Zuverlässigkeit und Skalierung keinesfalls verstecken. HTTP ist zuverlässig, verständlich und durch seine Verbreitung leicht in jede Programmiersprache und Platform zu integrieren. Für die vorgestellte Lösung ist HTTP die einzige konzeptionelle und in seinen Implementierung in der jeweiligen Platform technologische Basis.

Da die Aufgabe meist vielschichtige Aspekte umfasst, beginne ich mit einer einfachen deklarativen Prozessbeschreibung (HTTP URLs und JSON) der einzelnen Schritte des Prozesses und der beteiligten Microservices. Es entsteht ein grob beschriebener endlicher Zustandsautomaten mit seinen Aktionen und Übergängen. Aktionen und Übergänge sind dabei immer nur einfache HTTP-URLs. Aktion werden von Konsumenten als aufgerufen, Übergänge werden als HTTP-Callbacks bzw. Webhooks an den jeweiligen Konsumenten gemeldet.

Vereinfacht dargestellt geht es um folgenden Bestellprozess.

Veranlasse Bestellung -> Veranlasse Reservierung -> Veranlasse Zahlung -> Veranlasse Versand
      |                            |                  |       |  |               |
   Abbruch                      Abbruch            Abbruch    |--|            Abbruch
                                                          Wiederholung

Auf konzeptioneller wie auch technischer Ebene empfehle ich das lesen von Railway Oriented Programming. Ein echter "eye opener" Flussorientierter und asynchroner Prozessmodellierung.

Folgend ein Beispiel für den Bestellprozess:

{
  "create_order_line": [{
    //Veranlasse Bestellung
    "create_orders_url":"https://ecommerce.service.io/orders/create",
    //Erfolgreich - veranlasse Reservierung
    "orders_success_url": "https://ecommerce.service.io/reservations/{order_id}/create",
    //Fehler - melde Abbruch der Bestellung
    "orders_failure_url": "https://ecommerce.service.io/status/{order_id}/push",
  },{
    //Veranlasse Reservierung
    "create_reservations_url": "https://ecommerce.service.io/reservations/{order_id}/create",  
    //Veranlasse Zahlung
    "reservations_success_url": "https://ecommerce.service.io/payments/{order_id}/create",
    //Fehler - Veranlasse Abbruch des Bestellprozess
    "reservations_failure_url": "https://ecommerce.service.io/orders/{order_id}/abort"
  },{
    //Veranlasse Zahlung
    "create_payments_url": "https://ecommerce.service.io/payments/{order_id}/create",
    //Erfolgreich - veranlasse Versand
    "payments_success_url": "https://ecommerce.service.io/shipments/{order_id}/create",
    //Fehler - melde Zahlungen fehlgeschlaggen
    "payments_failure_url": "https://ecommerce.service.io/status/{order_id}/push",
    //Fehler - veranlasse Abbruch des Bestellprozess
    "payments_abort_url": "https://ecommerce.service.io/orders/{order_id}/abort"
  },{
    //Versand veranlassen
    "create_shipment_url": "https://ecommerce.service.io/shipments/{order_id}/create",
    //Erfolgreich - veranlasse erfolgreichen Abschluss des Bestellprozesses
    "shipments_success_url": "https://ecommerce.service.io/orders/{order_id}/complete",
    //Fehler - veranlasse Abbruch des Bestellprozess
    "shipments_failure_url": "https://ecommerce.service.io/orders/{order_id}/abort"  
  }]
}

Das Prinzip ist extrem einfach, immer gleich und kann individuell z.B. um HTTP-Verbs, Data-Payload und Meta-Daten ergänzt werden. Wer möchte, kann die Implementierung auch bis zur Umsetzungen eines Routing Slip weiter voran treiben.

Zusätzlich werden die HTTP Endpunkte der entsprechenden Microservice ergänzt und um die gewünschte Funktionalität in einer beliebigen Programmiersprache bzw. Runtime-Platform ergänzt.

/orders/create
/orders/{order_id}/complete
/orders/{order_id}/abort

/reservations/{order_id}/create
/reservations/{order_id}/abort

/payments/{order_id}/create
/payments/{order_id}/abort

/shipments/{order_id}/create
/shipments/{order_id}/abort

/status/{order_id}/push

I. Prozessflussumsetzung mittels Ochestrierung

Bei der Ochestrierung werden Microservices zu einem kombiniertem Prozess ausgeführt. Heißt, ein Konsument (z.B. UI oder Microservice) integriert, steuert und kontrolliert den Prozessfluß. Das könnte mit JavaScript als sequentialisierter asynchroner Promise-Flow z.B. so aussehen:

  request(create_order_line.create_orders_url).catch(request(create_order_line.orders_failure_url))
  .then(request(create_order_line.create_reservations_url).catch(request(create_order_line.reservations_failure_url)))
  .then(request(create_order_line.create_payments_url).catch(request(create_order_line.payments_failure_url)))
  .then(request(create_order_line.create_shipment_url).catch(request(create_order_line.shipments_failure_url)))
  .then(request(create_order_line.shipments_success_url));

Natürlich ist auch eine parallele Ausführung möglich. Ja nach Gegebenheiten und Anforderungen an Geschwindigkeit, Latenz und Korrelation kann entschieden werden, welche Form passender ist.

  var orderId = clientSideGeneratedOrderId();
  Promise.all([
    request(create_order_line.create_orders_url, orderId),
    request(create_order_line.create_reservations_url, orderId),
    request(create_order_line.create_payments_url, orderId,
    request(create_order_line.create_shipment_url, orderId)
  ])
  .then(request(create_order_line.orders_success_url, orderId)
  .catch(error => {
    //Fehlerkompensation durch Aufruf der entsprechenden failure HTTP-Callback bzw. Webhook URLs
  })

Der Prozessfluss kann laut Deklaration schnell und einfach an ansprechender Stelle angepasst werden. Anpassungen müssen jedoch an allen Stellen einer Implementierung vorgenommen werden.

Prozessflussumsetzung mittels Choreografie

Ein anderer Ansatz ist die Choreografie. Hierbei hat jeder Microservice seine "eigene" Aufgabe in der Gesamtkomposition des Prozessflusses. Die Teildeklarationen werden als Teilaufgabe des Microservice implementiert. Jeder erfolgreiche oder fehlerhafte Prozessschritt ruft über die deklarierten success/failure Webhook-URLs den nächsten Microservice-HTTP-Endpunkt bis der Gesamtprozess erfolgreich oder fehlerhaft abgeschlossen ist. Wer möchte kann, wie im folgenden Beispiel, hierbei ebenfalls auf einen Fail-Fast mittels sequenzieller oder Long-Running mittels paralleler Ausführungsform wählen. Parallel stattfindene Zustandsänderungen müssen bei der parallelen Ausführung mittels der HTTP-Callback bzw. Webhook-URLs korreliert werden. Das ist natürlich etwas umständlicher und deshalb favorisiere ich die folgende sequentialisierte asynchone Ausführungsform.

//Orders Microservice
server.get('/orders/create', (req, res) => {
  createOrder()
    .then(orderId => request(create_order_line.orders_success_url, orderId))
    .catch(_ => request(create_order_line.orders_failure_url)
    .then(result => res.send(result));
});

//Orders Microservice
server.get('/orders/{order_id}/complete', (req, res) => {
  var orderId = req.params.orderId;
  completeOrder(orderId)
    .then(result => res.send(result));
});

//Orders Microservice
server.get('/orders/{order_id}/abort', (req, res) => {
  var orderId = req.params.orderId;
  abortOrder(orderId)
    .then(result => res.send(result));
});

//Reservations Microservice
server.get('/reservations/{order_id}/create', (req, res) => {
  var orderId = req.params.orderId;
  createReservation(orderId)
    .then(result => request(create_order_line.reservations_success_url, orderId))
    .catch(result => request(create_order_line.reservations_failure_url, orderId)
    .then(result => res.send(result));
});

//Payments Microservice
server.get('/payments/{order_id}/create', (req, res) => {
  var orderId = req.params.orderId;
  createPayment(orderId)
    .then(result => request(create_order_line.payments_success_url, orderId))
    .catch(result => request(create_order_line.payments_failure_url, orderId)
    .then(result => res.send(result));
});

//Shipments Microservice
server.get('/shipments/{order_id}/create', (req, res) => {
  var orderId = req.params.orderId;
  createReservation(orderId)
    .then(result => request(create_order_line.shipments_success_url, orderId))
    .catch(result => request(create_order_line.shipments_failure_url, orderId)
    .then(result => res.send(result));
});

Fazit

Schade, auch ich habe mich in diesem Artikel nur auf den "Happy-Day" bezogen. Ergänzungen oder weitere Workflows sind natürlich umsetzbar, aber aus meiner Sicht eher Teil einer Artikelserie. Zu viele wichtige Aspekte, wie Datenreplikation, Fail-Over, Retry etc. sollten aus meiner Sicht nicht unbeschrieben bleiben.

ralfw commented Dec 10, 2015

Leider hast du meinen Beitrag missverstanden. Mir sind Events völlig egal. Ich habe nur gesagt, dass man so denken kann. Nicht egal ist mit hingegen, dass Microservices in Datenflüssen verbunden sein sollten. Und das wiederum bedeutet, dass Microservices einander nicht (!) kennen. Sie folgen dem PoMO: Principle of Mutual Oblivion.

Warum PoMO? Weil das Entkopplung bedeutet. Und weil das ist, wie die Natur organisiert ist. So sind komplexe System zusammengebaut.

Wie das technisch realisiert ist, ist mir ebenfalls egal. Kann man mit Json oder XML oder Workflow-Engine oder sonstwie machen. Oder HTTP und REST oder RabbitMQ oder oder oder.

Der zentrale Punkt für mich ist: Ein Microservice Y weiß nichts über andere Microservices X oder Z. Y verspricht lediglich, auf bestimmten Endpunkten zu lauschen (Daten zu konsumieren) und bestimmte Ergebnisse in gewissen Fällen zu liefern (Daten zu produzieren). Was seine Datenproduktion und -konsumption für anderen bedeutet, ist ihm egal. Keine Kenntnis der Umwelt, von upstream oder downstream Microservices.

Wenn du dann in solchen Datenfluss mehrere Pfade einbauen willst (railway oriented programming), dann mach es halt. Aber begrenze dich nicht auf 1 Weiche oder so. Daten können viel freier fließen. Und wenn du End2End-Kommunikation willst, dann bau sie halt ein. Das ist doch kein Widerspruch zu Datenflüssen.

Aber ob sich Request/Response-Kommunikation mit Datenflüssen/PoMO verträgt... Ich finde, das passt nicht zusammen. Auch wenn sie noch so bequem aussieht. Request/Response im Sinne eines Dienstleistungsaufrufs eines Microservices bei einem anderen, bedeutet Kenntnis der Umwelt. Das geht für mich nicht. Request/Response hat für mich nur Platz in einer Integration mehrerer Microservices - die allerdings eine separate Instanz wäre.

Ich bin also ganz für autonome Microservices (was für mich ein Pleonasmus ist). Aber auch die autonomsten Microservices brauchen einen Zusammenhang, in den sie gestellt werden. Der muss entworfen und dargestellt werden. Das war mein zweiter Punkt im Artikel. Deshalb bin ich nicht gegen BPMN und sogar für noch leichtgewichtigere visuelle Notation.

Owner

MikeBild commented Dec 10, 2015

Vielen Dank Ralf. Ich verstehe deinen Beitrag und dein PoMO schon ganz gut. Aber ;) ohne Zelle kein Organismus und ohne Organismus keine Zelle. PoMO ist gut, denn es spricht auch die Kommunikation an. "So werden komplexe Systeme gebaut" - nichts dagegen, denn ich bin gleicher Meinung Was ich nicht verstehe, sind Prinzipien ohne Deckel und Boden. Abstrakte "Weltformeln" mit dem Anspruch allgemeiner Gültigkeit. Nett, denn die passen immer überall, oder auch nirgends rein. Das mag machmal ganz nett sein, hilft in konkreten Szenarien wie die von Tobias geschilderten wie man sieht nur bedingt weiter. Sorry, von Reduzierung auf das Wesentliche kann ich bei Tobias Ausführungen nicht viel entdecken.

Ja, zur Laufzeit sollten Microservices weder wissen noch! versprechen. Dennoch existieren Microservices nicht ohne passendes! System. Der Wert wäre Null. Wie Zellen ohne Organismus würden sie einander nicht brauchen und einfach vergessen und sterben. Eine Kraft oder Medium oder System mit Kommunikationsschnittstellen wird benötigt, um dem einzelnen Microsystem die Möglichkeit Teil eines Makrosystems zu sein.

Jetzt sagst du, dir ist egal wie das geschieht. Foo oder Bar - Einerlei. Auch hier ist deine Distanz ja nett, mir aber aus genannten Grund "Makrosystem aus Microsystemen ganz konkret" für eine beliebige Domäne zu unpräzise. PoMO degeneriert zu - Dass ist doch jetzt auch irgendwie PoMO oder? Ich bin der Meinung, Regeln oder Prinzipien sollten die möglichen Varianten eingrenzen statt erweitern. Ob das bei PoMO jetzt zutrifft, kann ich gerade auch nicht beurteilen.

Ein Designkonzept wie ROP hat ebenfalls seine eigenen wichtigen Prinzipien. Die Beschränkung ein Eingang, zwei Ausgänge sind ein wichtiger und gewollter Kontrakt und stellen eine möglichst reibungslose Kommunikation zwischen autonomen Einheiten her. Wie bei Microservices sich Abhängigkeit der Autonomie unterordnen muss, müssen sich nach ROP viele Ausgangsvarianten genau zwei, Erfolgreich oder Fehler, unterordnen. Das ist okay. Wer mehr braucht muss sein Problem eingrenzen bzw. teilen. Der Gewinn einer einheitlichen Kommunikation und damit Komposition zu höheren Systemen ist viel größer.

Solange es sich bei End2End mittels Request/Response um einen nebenläufigen! Vorgang handelt, ist es für mich das einzige in der Masse verständliche Kommunikationsmuster. Makrosysteme aus Microsystemen brauchen leicht verständliche, einfache Kommunikationsmuster. Eine akzeptable Einschränkung mit hohem Nutzen wie ich finde.

HTTP koppelt verteilte Systeme aka Microservices weder eng noch lose. HTTP koppelt Systeme temporär! und dynamisch. Ein dynamischer Endpunkt wird aufgerufen, der Kontrakt wird ausgehandelt, die Kommunikation findet statt oder schlägt fehl und dann wird die Kommunikation rückstandslos beendet bzw. verworfen. Der nächste Aufruf kann! ein ganz anderen Kontrakt aushandeln oder ablehnen. Nicht ist sicher und deshalb passt es so gut.

Du schreibst du würdest Request/Response nur bei der Orchestrierung mehrerer Microservices einen Platz einräumen. Völlig unnötig solange Request/Response ein nebenläufiger aka paralleler Vorgang ist, lassen sich über HTTP jede Menge leistungsfähige temporär gekoppelte Kommunikationsformen nutzen. Meine Code-Beispiele sind allesamt Sequenzen von nebenläufigen HTTP Request/Response. Eine Kopplung gibt es nur im Augenblick der einzelnen Ausführung ohne andere Teile der eigenen Instanz, oder abhängig von anderen Microservices zu sein.

"Aber auch die autonomsten Microservices brauchen einen Zusammenhang, in den sie gestellt werden." Ohh ja! Dazu sind leichtgewichtige konsumentengetrieben Kontrakte neben Entwurf und vereinfachter Darstellung genau das Richtige. BPMN ist da eher etwas für die Doku.

ralfw commented Dec 10, 2015

Und wieder reden wir aneinander vorbei. Habe ich etwas gegen HTTP? Nein. Habe ich etwas gegen Request/Response? Nein. Davon kannst du parallel soviel absetzen, wie du magst.

Wogegen ich aber etwas habe, das ist: Microservice WarenReservierungService (WRS) ruft (!) Microservice GeldeinzugService (GES). Das Rufen ist für mich das Problem. Dass WRS überhaupt GES kennt, ist für mich ein Problem.

Und bei all deinen Anrufungen der Praxis und des Konkreten: Ich bleibe dabei, dass die ganze Praxis und das ganze Konkrete sind heillos verstrickt, solange es nicht durch Theorie informiert ist. Wie auch umgekehrt: Theorie hebt heillos ab, wenn sie sich denn gar nicht um Praxis schert.

In einer Welt jedoch, in der die meisten unaufhörlich "Praxis! Praxis!" skandieren, stelle ich mich einfach mal in die andere Ecke und rufe "Prinzip! Prinzip!".

Natürlich existieren Microservices nicht ohne "passendes System". Wenn du meinen Artikel und die Kommentare aufmerksam liest, dann bin ich ganz vorne mit dabei, wenn es darum geht, "das System" zu zeigen. Aber dieses System existiert immer zuerst im Kopf! Als Modell. Und wer das nicht ausdrücken kann, ohne Code zu schreiben, der ist einfach arm dran.

Nichts weiter habe ich getan, als ein solches System hinzumalen. Ich habe noch nicht einmal behauptet, dass es vollständig ist. Lediglich Tobias' Vorlage habe ich vereinfacht dargestellt. Und da kommst du daher und sagst, so kann es nicht funktionieren. Darauf sage ich: Mir egal. Weil es mir nicht ums Funktionieren geht mit irgendwelchen bestimmten Events - sondern mir gehts um Darstellung.

Deshalb zeige ich auch keine Implementation. Und wenn, dann nur skizziert, um ein Prinzip zu verdeutlichen. Aber ich behaupte gar nichts in Bezug auf die Implementation. REST oder RabbitMQ... das - ist - mir - egal.

Klar, solche Details müssen am Ende geklärt werden. Nur brauche ich dafür zuerst (!) ein big picture. Ein ganzes "passendes System", in dem alle Funktionsbestandteile überhaupt erstmal verortet sind.

Du willst hier einen Implementationsstil verkaufen. Das ist doch völlig ok. Mach ruhig. Male Json hin, brich eine Lanze für HTTP, Request/Response. Dagegen sage ich nichts.

Aber, bitte, stelle das doch nicht zwanghaft einem simplen Prinzip und einer simplen Darstellungsweise entgegen. Sieh doch, dass wir über unterschiedliche Schichten (besser: Strata) derselben Sache reden. Wie bei OSI. Du betrachtest ein tieferliegendes Stratum.

Dem spreche ich nichts ab außer alleiniger Wichtigkeit.

Owner

MikeBild commented Dec 10, 2015

Wie ich schon sagte, ich habe weder etwas gegen geliebte Prinzipien noch gegen die Theorie. Beide müssen den Weg in die Praxis finden.

Du verstehst tatsächlich meinen Einwand nicht. Ich breche keine Lanze für HTTP und JSON. Das ist genauso (un)wichtig für die Praxis wie du z.B. C# Events, Akka bzw. Aktor-Modell im Artikel verwendest um auf das "große Ganze" aufmerksam machen zu wollen.

Noch einmal: A kennt B nicht! A ist egal das es B gibt, weil es auch C sein könnte. B is egal das es A bis Z hoch x gibt. Es gibt nur eine "Adresse" und da wird bei Bedarf "hingefunkt". Das machen mit HTTP alle so.

Womit ich ein Problem habe ist, dass alle z.B. C kennen. Das macht C einzigartig. Das ist systemrelevante Abhängigkeit um das "große Ganze" einheitlich Beschreiben zu wollen. Ein Kommunikationsmedium (kein End2End) von dem alle einheitlich abhängen ist die Achillesferse. Tobias macht das gleich zwei mal. Broker mit Fan-Outs und zentrale Event-Log Systeme um konkret zu werden. Das System und seine Teile ist nicht mehr in der Lage sich selbst zu beschreiben. Es braucht immer und überall C. Das ist kein Gewinn. Das ist bedenklich.

Der zweite nicht verstandene Einwand ist das "große Ganze" selbst. Keine Zentrale, kein "Big Picture Design". Ein System, im großen wie im kleinen, muss (!) selbstbeschreibend und somit ein bisschen chaotisch sein. Regeln und Prinzipien helfen einen Teil davon in einen sinnvollen Zusammenhang zu bringen. Ein komplexes System existiert einfach und passt in keinen einzelnen Kopf. Nur das Modell muss ungenau genug sein, um dort hinein zu passen. Du kritisierst fehlenden Ausdruck im Code. Ich kritisiere fehlenden Ausdruck und hohen Abstimmungsbedarf in einem Big-Pic Modell. Diesbezüglich sind wir dann auch unterschiedlicher Meinung. Passt.

ralfw commented Dec 11, 2015

Na, das ist doch mal knackiger :-) Damit kann ich mehr anfangen:

  • Eine systemrelevante Größe finde ich genauso nachteilig wie du. Schön, dass du den Punkt herausarbeitest. Es sollte sie nicht geben. Oder wenn, dann auf einer Ebene von "Commodity", so dass ihre Verfügbarkeit maximal ist. Irgendwo muss man ein Fundament haben, auf dem man steht. Sei das verlässlicherer Strom aus der Steckdose oder sonstwas. Auch ein Kommunikationsmedium muss zumindest als gemeinsam anerkannt sein, wenn man sich als Microservices unterhalten will. Ob das ständig verfügbar ist, ist eine andere Frage. (Genauso, ob ein Microservice ständig verfügbar ist.)

Dass ich allerdings eine bestimmte Technologie oder so als systemrelevante Größe gefordert hätte, kann ich nicht sehen. Ich sag nur: Irgendwie müssen halt Microservices miteinander reden. Und lass die Technologiewahl nicht das Prinzip aufweichen, dass die Microservices einander nicht kennen.

  • Kein Big Picture...? Puh. Ich bin ja ein Freund von Emergenz, aber die Frage ist, was emergieren sollte. Und selbst wenn etwas emergiert - so wie ein Organismus -, dann heißt das nicht, dass man das Ergebnis nicht beschreiben kann. Nicht umsonst gibt es die altehrwürdigen Disziplinen der Anatomie und Physiologie. Und Emergenz bedeutet auch nicht, dass ich nicht entwerfen, also planen darf.

Wenn ich 10 Microservices verschaltet darstelle, um ein (Sub)System zu beschreiben, das ein bestimmtes Verhalten hat, dann geht daran derzeit für mich nichts vorbei. Wie anders sollte ich eine Bestellabwicklung auf die Straße bringen? Oder ein CRM oder oder oder?

Wir bleiben die Intelligent Designer unserer Systeme im Hinblick auf Verhalten. Sie sollen uns ja dienen.

Emergenz sehe ich eher bei nicht-funktionalen Eigenschaften im Spiel, z.B. Wandlungsfähigkeit oder Skalierbarkeit. Womit wir wieder bei Prinzipien sind. Wenn ich lokal handle mit der Maxime "Systemrelevante Größe vermeiden" oder PoMO, dann mag das lokal suboptimal aussehen - aber global emergiert eine Eigenschaft, global wird dann etwas optimiert.

In dieser Hinsicht - da stimme ich mir dir überein - sollten Modelle auch ungenau sein oder Raum lassen. Das ist ein Grund, warum ich nicht so ein Freund von haarkleinen Notationen bin wie UML oder BPMN. Jedenfalls nicht für den Entwurf. Sie wollen, dass wir genauer sind, als wie wir sein können oder sollten.

Für Emergenz ist aber eines nötig: Genauigkeit im Kleinen. Wenn im Großen etwas emergieren soll, weil es ja in keinen Kopf passt, dann braucht es im Kleinen eben klare Regeln, die quasi in Blindheit für das Ganze vertrauensvoll angewandt werden sollen. Welches sind deine Regeln? In meinen Regelkasten gehört z.B. PoMO :-)

Erzähl uns doch von deinem. Was ist für dich unverbrüchlich? Was ist für dich "tugendhaft"?

Owner

MikeBild commented Nov 1, 2016

Vielleicht nicht unverbrüchlich, aber tugendhaft oder "eine Vision" von Entwicklung im Kleinen wie im Großen ist für mich:

A) Strikte Trennung von Logik und Seiteneffekten
A`) siehe A ;)
B) Ausdrucksstarke Tests
C) Monitoring
D) Automatisierung

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment