Wer anfängt große Projekte mit Azure Logic Apps zu entwickeln und ordentlich Last durch diese durchpumpen will, wird zwangsläufig auf folgenden Fehler stoßen:
Ärgerlich. Sehr ärgerlich. Und aus meiner Sicht auch das größte Problem, mit dem man zu kämpfen hat, wenn man große Datenmengen durch die Cloud bringen will.
Jeglichen Konnektoren, die einem in einer Logic App zu Verfügung stehen, liegen gewissen Limitierungen für ihren Durchfluss zu Grunde. Diese Limitierungen wurden von Microsoft eingeführt, um eine faire Gleichberechtigung garantieren zu können. Wie? Gleichberechtigung? Nun leider ja, aber es ist eigentlich auch ganz verständlich. Was viele Leute oft vergessen, ist, dass die Programmierung mit Serverless Cloud Komponten nie wirklich Serverless ist. Klar werden die Komponenten bunt hin und her deployed und laufen mal hier und mal dort, jedoch laufen sie auch immer auf einem realen Server in einem realen Rechenzentrum. Das Produktteam um die Logic Apps wollte hier die Last limitieren, um das Szenario zu verhindern, das andere Kunden unter der Lastspitze eines anderen leiden müssen.
Zu finden sind diese Limitierungen unter: https://docs.microsoft.com/de-de/connectors/
Jeder Konnektor hat hierbei verschiedene Limitierungen bezüglich Dateigrößen, Parallelität oder Durchsatz.
Ein kleines Beispiel hierfür ist der File-Connector, der über das On-Premises-Data-Gateway (https://docs.microsoft.com/de-de/data-integration/gateway/service-gateway-onprem-indepth) auf das Dateisystem eines Servers zugreifen kann, um dort Nachrichten abzuholen. Dieser Konnektor kann maximal 30 MB Dateien abholen und pro 60 Sekunden maximal 100 Dateien verarbeiten. Dies bedeutet: Wenn in den ersten drei Sekunden 100 Nachrichten verarbeitet wurden, gehen in den nächsten 57 Sekunden alle weiteren Dateien auf den besagten HTTP 429 Fehler – Rate Limit Exceeded. Um einen weiteren Irrglauben hierzu aus der Welt zu schaffen: Diese Limitierungen beziehen sich aber nur auf eine API-Connection-Objekt und nicht auf das gesamte Data Gateway.
Was ist ein API-Connection-Objekt? Beim Entwickeln einer Logic App müssen in den so genannten Connector-Shapes, also Shapes in den man eine Verbindung zu Systemem oder technischen Komponente aufbauen möchte, eine Verbindung hinterlegt werden. Dies ist im einfachsten Fall eines Service Bus nur der Namespace des Service Bus. Im Falle eines SQL-Servers zum Beispiel sieht dies schon komplexer aus, da hierbei auch User- und Passwortinformationen gespeichert werden neben des eigentlichen Connection Strings. Damit diese Informationen nicht als Plaintext in der Logic App liegen und damit für alle Augen sichtbar sind, erstellt die Design Engine aus jedem dieser Informationen ein API-Connection-Object in dem die Daten geschützt abliegen.
Während der Laufzeit löst die Logic App Runtime die richtige Verbindung aus diesem Objekt auf und kann Daten von oder zu diesem System schicken.
Als erstes ist es wichtig diesen Fehler aus der Welt zu schaffen. Hierzu nutzen wir eine Kombination aus zwei verschiedenen Features der Logic Apps. Zum einen kann man im Trigger der Logic App den Grad an Parallelität angeben. So lässt sich steuern, wie viele Instanzen maximal zeitgleich bearbeitet werden. Jedoch haben wir hier immer noch keine Kontrolle darüber, wie lange die Bearbeitung dieser Logic Apps dauert, d.h. selbst wenn ich maximal eine Logic App parrallel laufen lasse, diese jedoch in 0.1 Sekunden fertig ist, werde ich auch hiermit ggf. an Grenzen kommen und damit an unseren altbekannten Fehler. Das lässt sich jedoch mit dem Einsatz eines Delay-Shapes in einer Logic App abschaffen. Wenn ich also einen maximalen Grad an Parallelität von 25 angebe und Delay mit einer Minute einbaue, habe ich dafür gesorgt, dass in einer Minute maximal 25 Nachrichten verarbeitet werden. Nun habe ich feste Zahlen, die ich an meine Limits anpassen kann.
Das Negative an dieser Lösung ist jedoch, dass ich die Fehler zwar abgeschalten habe, meinen Durchsatz jedoch nicht erhöht habe. Um dieses Problem zu lösen und einen höheren Durchsatz zu erreichen gibt es aus meiner Sicht 4 Verschiedene Verbesserungsmöglichkeiten.
Möglichkeit 1: Splitten der Connections
In vielen Szenarien habe ich mit demselben System mehrere Interaktionen pro Durchlauf. So wird zum Beispiel bei einer Verbindung mit einem Dateisystem, bei dem eine Datei abgeholt werden soll, mindestens drei Befehle ausgeführt. Als erstes die Info, das hier eine neue Datei liegt, die darauf wartet, abgeholt zu werden. Als nächstes das eigentliche Abholen über das Shape „Get-Content-Using-Path“ und als drittes das Löschen der Originaldatei. Wenn man mit nur einem API-Objekt für alle drei Shapes arbeitet, könnte man an dieser Stelle nur 33 Nachrichten pro Minute verarbeiten. Hinterlegt man hinter jeder dieser Aktionen eine separate Verbindung, können schon 100 Nachrichten verarbeitet werden und der Durchsatz wurde um den Faktor 3 multipliziert. Dies funktioniert auch, wenn alle Verbindungen auf haargenau das gleiche System und den gleichen Ordner zeigen. Es liegt einfach nur an dem Verbindungsobjekt.
Möglichkeit 2: Lasst den Zufall entscheiden
Eine weitere Möglichkeit, um den Durchsatz zu erhöhen, ist, den Zufall entscheiden zu lassen, wie viele Nachrichten verarbeitet werden können. Klingt im ersten Moment seltsam, aber es ist tatsächlich eine valide Möglichkeit, die auch als Geheimtipp aus dem Microsoft Produkt Team so zu verlauten ist.
Wir haben im vorherigen Absatz schon gehört, dass wir nicht in der Logic App die Verbindungsdetails hinterlegt haben, sondern in einem separaten Objekt. Die einzelnen Shapes haben dann nur noch die ID des Objekts gespeichert.
(Informationen des Shapes in JSON-View)
(Referenzen zu den Connection-Objekten)
Dies kann man sich zu Nutzen machen, indem man die eigentliche ID erst zu Laufzeit angibt, bzw. zusammenbaut. Statt fest eine ID zu hinterlegen, gibt man im Feld „Connection“ des ersten Bildes einen Ausdruck an, der erst zur Laufzeit berechnet wird.
Hierzu legt man sich zuerst mehrere Connection-Objekte an, die auf dasselbe Ziel zeigen.
Der einzige Unterschied in einem Aufruf ist der Name des Objektes, also filesystem_*.
Dies macht man sich zu Nutze, in dem man die ID des Objektes mit einem String.Concat zusammenbaut und an Stelle der Zahl eine Random-Funktion benutzt, um diese zu ermitteln.
Man kann zwar nun im Designer das Shape nicht mehr auslesen, da die Verbindung fehlt, es ist nun jedoch möglich, im gleichen Shape mehrere mögliche Verbindungen hinterlegen zu können und überlässt es dem Zufall, welche Verbindung ausgewählt wird. Im schlechtesten Fall wird zwar immer die gleiche Verbindung ausgewählt, im Besten doch könnten wir für dieses Shape den Durchsatz um den Faktor X multiplizieren, wobei X die Anzahl der hinterlegten Verbindung ist. In der Realität sollte der Wert irgendwo in der Mitte dazwischen liegen.
Möglichkeit 3: Pseudo-Loadbalancing mit Queues
Vorab: Diese Möglichkeit bietet sich uns zwar nur im Versand an ein System. Beim Abholen von einem System habe ich hierzu leider noch keine Möglichkeit gefunden.
Beim Versenden an andere Systeme bietet sich uns die Möglichkeit, den Durchsatz im Grunde auf Unendlich zu setzen. Hierzu nutzen wir ein Pattern, in dem wir eine Azure Service Bus Queue und geklonte Logic Apps verwenden. Die Funktionalität von Queues ist nämlich die, dass sobald ein Endpunkt eine Nachricht aus der Queue geholt hat, diese Nachricht für andere Abholer nicht mehr sichtbar ist. So kann ich meine Logic App genau auf den Durchfluss anpassen, der maximal möglich ist und klone diese dann solange, bis ich meinen gewünschten Durchsatz erreicht habe.
Aus meiner Sicht ist das mit Abstand die beste Möglichkeit, wie man zumindest beim Versand eine Steigerung auf eine saubere Art und Weise erreichen kann.
Kleiner Tipp: Diese Methode funktioniert mit allen Prozessen und Arten von Logic Apps