Rechnen Sie nach: Microservices-Anwendungen mit Orchestratoren skalieren

Veröffentlicht: 2022-03-11

Es ist keine große Überraschung, dass die Microservices-Anwendungsarchitektur weiterhin in das Softwaredesign eindringt. Es ist viel bequemer, die Last zu verteilen, hochverfügbare Bereitstellungen zu erstellen und Upgrades zu verwalten und gleichzeitig die Entwicklung und das Teammanagement zu vereinfachen.

Aber ohne Container-Orchestratoren ist die Geschichte sicherlich nicht dieselbe.

Es ist leicht, alle ihre Schlüsselfunktionen nutzen zu wollen, insbesondere die automatische Skalierung. Was für ein Segen ist es, den ganzen Tag über schwankende Containerbereitstellungen zu beobachten, die sanft dimensioniert sind, um die aktuelle Last zu bewältigen, und unsere Zeit für andere Aufgaben frei machen. Wir sind stolz darauf, was unsere Containerüberwachungstools zeigen; Inzwischen haben wir nur ein paar Einstellungen konfiguriert – ja, das war (fast) alles, was nötig war, um die Magie zu erschaffen!

Das heißt nicht, dass es keinen Grund gibt, darauf stolz zu sein: Wir sind uns sicher, dass unsere Benutzer gute Erfahrungen machen und wir kein Geld mit überdimensionierter Infrastruktur verschwenden. Das ist schon ziemlich beachtlich!

Und natürlich, was für eine Reise es war, dorthin zu gelangen! Denn auch wenn am Ende gar nicht so viele Einstellungen konfiguriert werden müssen, ist es um einiges kniffliger, als man gemeinhin denkt, bevor man loslegen kann. Minimale/maximale Anzahl von Replikationen, Upscale-/Downscale-Schwellenwerte, Synchronisierungsperioden, Cooldown-Verzögerungen – all diese Einstellungen sind eng miteinander verbunden. Die Änderung eines Elements wirkt sich höchstwahrscheinlich auf ein anderes aus, aber Sie müssen dennoch eine ausgewogene Kombination arrangieren, die sowohl zu Ihrer Anwendung/Bereitstellung als auch zu Ihrer Infrastruktur passt. Und dennoch werden Sie im Internet kein Kochbuch und keine Zauberformel finden, da dies stark von Ihren Bedürfnissen abhängt.

Die meisten von uns stellen sie zuerst auf „zufällige“ oder Standardwerte ein, die wir anschließend entsprechend den Ergebnissen unserer Überwachung anpassen. Das brachte mich zum Nachdenken: Was wäre, wenn wir in der Lage wären, ein „mathematischeres“ Verfahren zu etablieren, das uns helfen würde, die Gewinnkombination zu finden?

Berechnen von Container-Orchestrierungsparametern

Wenn wir über die automatische Skalierung von Microservices für eine Anwendung nachdenken, wollen wir eigentlich zwei Hauptpunkte verbessern:

  1. Stellen Sie sicher, dass die Bereitstellung im Falle eines schnellen Lastanstiegs schnell skaliert werden kann (damit Benutzer nicht mit Zeitüberschreitungen oder HTTP 500s konfrontiert werden).
  2. Senken der Infrastrukturkosten (d. h. Unterlastung von Instanzen verhindern)

Dies bedeutet im Grunde, die Container-Software-Schwellenwerte für das Hoch- und Herunterskalieren zu optimieren. (Der Algorithmus von Kubernetes hat einen einzigen Parameter für die beiden).

Ich werde später zeigen, dass alle instanzbezogenen Parameter an den Upscale-Schwellenwert gebunden sind. Dies ist am schwierigsten zu berechnen – daher dieser Artikel.

Hinweis: Bezüglich Cluster-weit gesetzter Parameter habe ich kein gutes Verfahren dafür, aber am Ende dieses Artikels stelle ich eine Software (eine statische Webseite) vor, die sie bei der Berechnung berücksichtigt die Autoscaling-Parameter einer Instanz. Auf diese Weise können Sie ihre Werte variieren, um ihre Auswirkungen zu berücksichtigen.

Berechnung des Scale-up-Schwellenwerts

Damit diese Methode funktioniert, müssen Sie sicherstellen, dass Ihre Anwendung die folgenden Anforderungen erfüllt:

  1. Die Last muss gleichmäßig auf jede Instanz Ihrer Anwendung verteilt werden (im Round-Robin-Verfahren).
  2. Die Anforderungszeiten müssen kürzer sein als das Lastprüfungsintervall Ihres Container-Clusters .
  3. Sie müssen berücksichtigen, dass das Verfahren für eine große Anzahl von Benutzern ausgeführt wird (später definiert).

Der Hauptgrund für diese Bedingungen ergibt sich aus der Tatsache, dass der Algorithmus die Last nicht pro Benutzer berechnet, sondern als Verteilung (später erklärt).

Erhalten aller Gaußschen

Zunächst müssen wir eine Definition für einen schnellen Lastanstieg bzw. ein Worst-Case-Szenario formulieren. Für mich kann man es gut übersetzen: Eine große Anzahl von Benutzern führt innerhalb kurzer Zeit ressourcenintensive Aktionen aus – und es besteht immer die Möglichkeit, dass dies geschieht, während eine andere Gruppe von Benutzern oder Diensten andere Aufgaben ausführt. Beginnen wir also mit dieser Definition und versuchen, etwas Mathematik zu extrahieren. (Halten Sie Ihr Aspirin bereit.)

Einführung einiger Variablen:

  • $N_{u}$, die „große Anzahl von Benutzern“
  • $L_{u}(t)$, die Last, die von einem einzelnen Benutzer erzeugt wird, der die „ressourcenverbrauchende Operation“ durchführt ($t=0$ zeigt auf den Moment, in dem der Benutzer die Operation startet)
  • $L_{tot}(t)$, die Gesamtlast (erzeugt von allen Benutzern)
  • $T_{tot}$, die „kurze Zeitspanne“

Wenn in der mathematischen Welt von einer großen Anzahl von Benutzern gesprochen wird, die gleichzeitig dasselbe tun, folgt die Verteilung der Benutzer im Laufe der Zeit einer Gaußschen (oder normalen) Verteilung, deren Formel lautet:

\[G(t) = \frac{1}{\sigma \sqrt{2 \pi}} e^{\frac{-(t-\mu)^2}{2 \sigma^2}}\]

Hier:

  • µ ist der Erwartungswert
  • σ ist die Standardabweichung

Und es wird wie folgt grafisch dargestellt (mit $µ=0$):

Ein Diagramm der Gaußschen Verteilung, das zeigt, wie 99,7 % der Fläche zwischen plus-minus drei Sigma liegen

Erinnert wahrscheinlich an einige Kurse, an denen Sie teilgenommen haben – nichts Neues. Allerdings stehen wir hier vor unserem ersten Problem: Um mathematisch genau zu sein, müssten wir einen Zeitbereich von $-\infty$ bis $+\infty$ betrachten, der offensichtlich nicht berechnet werden kann.

Aber wenn wir uns die Grafik ansehen, stellen wir fest, dass Werte außerhalb des Intervalls $[-3σ, 3σ]$ sehr nahe bei Null liegen und nicht viel variieren, was bedeutet, dass ihr Effekt wirklich vernachlässigbar ist und beiseite gelegt werden kann. Dies gilt umso mehr, da unser Ziel darin besteht, die Skalierung unserer Anwendung zu testen, also suchen wir nach Variationen für eine große Anzahl von Benutzern.

Da das Intervall $[-3σ, 3σ]$ 99,7 Prozent unserer Benutzer enthält, ist es außerdem nah genug an der Gesamtzahl, um daran zu arbeiten, und wir müssen nur $N_{u}$ mit 1,003 multiplizieren, um dies auszugleichen der Unterschied. Wenn wir dieses Intervall auswählen, erhalten wir $µ=3σ$ (da wir von $t=0$ aus arbeiten werden).

In Bezug auf die Entsprechung zu $T_{tot}$ ist es keine gute Annäherung, sie gleich $6σ$ ($[-3σ, 3σ]$) zu wählen, da 95,4 Prozent der Benutzer im Intervall $[- 2σ, 2σ]$, was $4σ$ dauert. Wenn Sie also $T_{tot}$ gleich $6σ$ wählen, wird die Zeit nur für 4,3 % der Benutzer um die Hälfte verlängert, was nicht wirklich repräsentativ ist. Daher nehmen wir $T_{tot}=4σ$ und können ableiten:

\(σ=\frac{T_{tot}}{4}\) und \(µ=\frac{3}{4} * T_{tot}\)

Wurden diese Werte einfach aus dem Hut gezaubert? Jawohl. Aber das ist ihr Zweck, und das wird das mathematische Verfahren nicht beeinflussen. Diese Konstanten sind für uns und definieren Begriffe, die sich auf unsere Hypothese beziehen. Das bedeutet nur, dass unser Worst-Case-Szenario jetzt, da wir sie eingestellt haben, übersetzt werden kann als:

Die von 99,7 Prozent von $N{u}$ erzeugte Last, die eine verbrauchende Operation $L{u}(t)$ ausführt und von der 95,4 Prozent von ihnen dies innerhalb der Dauer von $T{tot}$ tun.

(Daran sollten Sie sich erinnern, wenn Sie die Web-App verwenden.)

Indem wir frühere Ergebnisse in die Benutzerverteilungsfunktion (Gaussian) einfügen, können wir die Gleichung wie folgt vereinfachen:

\[G(t) = \frac{4 N_{u}}{T_{tot} \sqrt{2 \pi}} e^\frac{-(4t-3T_{tot})^2}{T_{tot }^2}\]

Von nun an, nachdem $σ$ und $µ$ definiert sind, werden wir an dem Intervall $t \in [0, \frac{3}{2}T_{tot}]$ arbeiten (das $6σ$ dauert).

Wie hoch ist die Gesamtbenutzerlast?

Der zweite Schritt bei der automatischen Skalierung von Microservices ist die Berechnung von $L_{tot}(t)$.

Da $G(t)$ eine Verteilung ist, müssen wir, um die Anzahl der Benutzer zu einem bestimmten Zeitpunkt abzurufen, ihr Integral berechnen (oder ihre kumulative Verteilungsfunktion verwenden). Da aber nicht alle Benutzer gleichzeitig ihre Operationen starten, wäre es ein echtes Durcheinander, wenn man versuchen würde, $L_{u}(t)$ einzuführen und die Gleichung auf eine brauchbare Formel zu reduzieren.

Um dies zu vereinfachen, verwenden wir eine Riemann-Summe, eine mathematische Methode zur Annäherung eines Integrals mithilfe einer endlichen Summe kleiner Formen (wir verwenden hier Rechtecke). Je mehr Formen (Unterteilungen), desto genauer das Ergebnis. Ein weiterer Vorteil der Verwendung von Unterabteilungen ergibt sich aus der Tatsache, dass wir davon ausgehen können, dass alle Benutzer innerhalb einer Unterabteilung ihren Betrieb zur gleichen Zeit aufgenommen haben.

Zurück zur Riemann-Summe hat sie die folgende Eigenschaft, die sich mit Integralen verbindet:

\[\int_{a}^{b} f( x )dx = \lim_{n \rightarrow \infty } \sum_{k=1}^{n} ( x_{k} - x_{k-1} ) f( x_{k} )\]

Mit $x_k$ wie folgt definiert:

\[x_{ k } = a + k\frac{ b - a }{ n }, 0 \leq k \leq n\]

Dies gilt, wenn:

  • $n$ ist die Anzahl der Unterteilungen.
  • $a$ ist die Untergrenze, hier 0.
  • $b$ ist die obere Grenze, hier $\frac{3}{2}*T_{tot}$.
  • $f$ ist die Funktion – hier $G$ – zur Approximation seiner Fläche.

Ein Diagramm der Riemann-Summe für die Funktion G. Die X-Achse geht von null bis zu drei Hälften von T-sub-tot, und ein einzelnes Rechteck ist hervorgehoben, das zeigt, dass es zwischen x-sub-k-minus-1 und x- liegt. sub-k.

Hinweis: Die Anzahl der Benutzer in einer Unterteilung ist keine ganze Zahl. Dies ist der Grund für zwei der Voraussetzungen: Eine große Anzahl von Benutzern (damit der Dezimalteil nicht zu sehr ins Gewicht fällt) und die Notwendigkeit, dass die Last gleichmäßig auf alle Instanzen verteilt wird.

Beachten Sie auch, dass wir die rechteckige Form der Unterteilung auf der rechten Seite der Definition der Riemann-Summe sehen können.

Da wir nun die Riemann-Summenformel haben, können wir sagen, dass der Ladewert zum Zeitpunkt $t$ die Summe der Anzahl der Benutzer jeder Unterteilung ist, multipliziert mit der Benutzerlastfunktion zum entsprechenden Zeitpunkt . Dies kann geschrieben werden als:

\[L_{ tot }( t ) = \lim_{n \rightarrow \infty} \sum_{ k=1 }^{ n } ( x_{k} - x_{k-1} )G( x_{k} ) L_{ u }( t - x_{k} )\]

Nach dem Ersetzen von Variablen und Vereinfachen der Formel wird dies zu:

\[L_{ ges }( t ) = \frac{6 N_{u}}{\sqrt{2 \pi}} \lim_{n \rightarrow \infty} \sum_{ k=1 }^{ n } (\ frac{1}{n}) e^{-{(\frac{6k}{n} - 3)^{2}}} L_{ u }( t - k \frac{3 T_{tot}}{2n } )\]

Und voila ! Wir haben die Ladefunktion erstellt!

Ermitteln des Scale-up-Schwellenwerts

Zum Schluss müssen wir nur noch einen Dichotomie-Algorithmus ausführen, der den Schwellenwert variiert, um den höchsten Wert zu finden, bei dem die Last pro Instanz in der gesamten Lastfunktion niemals ihre maximale Grenze überschreitet. (Das macht die App.)

Ableitung anderer Orchestrierungsparameter

Sobald Sie Ihre Scale-up-Schwelle ($S_{up}$) gefunden haben, lassen sich andere Parameter ganz einfach berechnen.

Von $S_{up}$ kennen Sie Ihre maximale Anzahl von Instanzen. (Sie können auch nach der maximalen Last Ihrer Ladefunktion suchen und durch die maximale Last pro Instanz aufgerundet aufteilen.)

Die Mindestanzahl ($N_{min}$) von Instanzen muss entsprechend Ihrer Infrastruktur definiert werden. (Ich würde empfehlen, mindestens eine Replik pro AZ zu haben.) Aber es muss auch die Lastfunktion berücksichtigt werden: Da eine Gaußsche Funktion ziemlich schnell ansteigt, ist die Lastverteilung (pro Replik) am Anfang intensiver, also Sie Möglicherweise möchten Sie die Mindestanzahl von Replikaten erhöhen, um diesen Effekt abzufedern. (Dies wird höchstwahrscheinlich Ihre $S_{up}$ erhöhen.)

Nachdem Sie schließlich die Mindestanzahl von Replikaten definiert haben, können Sie den Schwellenwert für das Herunterskalieren ($S_{down}$) berechnen, indem Sie Folgendes berücksichtigen: Da das Herunterskalieren eines einzelnen Replikats keine größeren Auswirkungen auf andere Instanzen hat als das Herunterskalieren von $N_{min}+1$ bis $N_{min}$, wir müssen sicherstellen, dass der Scale-up-Schwellenwert nicht direkt nach dem Herunterskalieren ausgelöst wird. Wenn es erlaubt ist, hat das einen Jo-Jo-Effekt. Mit anderen Worten:

\[( N_{ min } + 1) S_{ runter } < N_{ min }S_{ hoch }\]

Oder:

\[S_{ runter } < \frac{N_{ min }}{N_{min}+1}S_{ hoch }\]

Außerdem können wir zugeben, dass es umso sicherer ist, $S_{down}$ näher an der höheren Grenze festzulegen, je länger Ihr Cluster so konfiguriert ist, dass er vor dem Herunterskalieren wartet. Auch hier müssen Sie eine Balance finden, die zu Ihnen passt.

Beachten Sie, dass bei Verwendung des Mesosphere Marathon-Orchestrierungssystems mit seinem Autoscaler die maximale Anzahl von Instanzen, die auf einmal von der Herunterskalierung entfernt werden können, an AS_AUTOSCALE_MULTIPLIER ($A_{mult}$) gebunden ist, was Folgendes impliziert:

\[S_{ runter } < \frac{S_{ hoch }}{ A_{mult} }\]

Was ist mit der Benutzerladefunktion?

Ja, das ist ein kleines Problem und nicht das einfachste mathematisch zu lösen – wenn es überhaupt möglich ist.

Um dieses Problem zu umgehen, sollten Sie eine einzelne Instanz Ihrer Anwendung ausführen und die Anzahl der Benutzer erhöhen, die dieselbe Aufgabe wiederholt ausführen, bis die Serverlast das zugewiesene Maximum erreicht (aber nicht überschreitet). Teilen Sie dann durch die Anzahl der Benutzer und berechnen Sie die durchschnittliche Zeit der Anfrage. Wiederholen Sie diesen Vorgang mit jeder Aktion, die Sie in Ihre Benutzerladefunktion integrieren möchten, fügen Sie etwas Timing hinzu, und fertig.

Mir ist bewusst, dass bei diesem Verfahren berücksichtigt werden muss, dass jede Benutzeranfrage eine konstante Belastung für ihre Verarbeitung hat (was offensichtlich falsch ist), aber die Masse der Benutzer wird diesen Effekt erzeugen, da sich nicht jeder von ihnen gleichzeitig im selben Verarbeitungsschritt befindet . Ich denke, das ist eine akzeptable Annäherung, aber es unterstellt erneut, dass Sie es mit einer großen Anzahl von Benutzern zu tun haben.

Sie können auch andere Methoden ausprobieren, z. B. CPU-Flammendiagramme. Aber ich denke, es wird sehr schwierig sein, eine genaue Formel zu erstellen, die Benutzeraktionen mit dem Ressourcenverbrauch verknüpft.

Einführung des app-autoscaling-calculator

Und nun zu der überall erwähnten kleinen Web-App: Sie nimmt als Eingabe Ihre Ladefunktion, Ihre Container-Orchestrator-Konfiguration und einige andere allgemeine Parameter und gibt den Skalierungsschwellenwert und andere instanzbezogene Zahlen zurück.

Das Projekt wird auf GitHub gehostet, es ist aber auch eine Live-Version verfügbar.

Hier ist das Ergebnis der Web-App, ausgeführt mit den Testdaten (auf Kubernetes):

Ein Diagramm, das die Anzahl der Instanzen und die Last pro Instanz im Zeitverlauf zeigt

Microservices skalieren: Kein Tappen mehr im Dunkeln

Wenn es um Microservices-Anwendungsarchitekturen geht, wird die Containerbereitstellung zu einem zentralen Punkt der gesamten Infrastruktur. Und je besser der Orchestrator und die Container konfiguriert sind, desto reibungsloser wird die Laufzeit.

Diejenigen von uns im Bereich DevOps-Services suchen immer nach besseren Möglichkeiten, die Orchestrierungsparameter für unsere Anwendungen abzustimmen. Lassen Sie uns einen mathematischeren Ansatz für die automatische Skalierung von Microservices verfolgen!