Wykonaj matematykę: skalowanie aplikacji mikroserwisów za pomocą orkiestratorów

Opublikowany: 2022-03-11

Nie jest zaskoczeniem, że architektura aplikacji mikrousług nadal ingeruje w projektowanie oprogramowania. Znacznie wygodniej jest dystrybuować obciążenie, tworzyć wdrożenia o wysokiej dostępności i zarządzać uaktualnieniami, jednocześnie ułatwiając programowanie i zarządzanie zespołem.

Ale historia z pewnością nie jest taka sama bez orkiestratorów kontenerowych.

Łatwo jest chcieć korzystać ze wszystkich ich kluczowych funkcji, zwłaszcza automatycznego skalowania. Cóż to za błogosławieństwo, obserwowanie, jak wdrożenia kontenerów zmieniają się przez cały dzień, delikatnie dostosowane do bieżącego obciążenia, uwalniając nasz czas na inne zadania. Jesteśmy dumni z tego, co pokazują nasze narzędzia do monitorowania kontenerów; tymczasem właśnie skonfigurowaliśmy kilka ustawień — tak, to (prawie) wszystko, czego potrzeba, aby stworzyć magię!

Nie oznacza to, że nie ma powodów do dumy: jesteśmy pewni, że nasi użytkownicy mają dobre doświadczenia i że nie marnujemy pieniędzy na przewymiarowaną infrastrukturę. To już całkiem spore!

I oczywiście, jaka to była podróż, żeby się tam dostać! Ponieważ nawet jeśli na końcu nie ma zbyt wielu ustawień do skonfigurowania, jest to o wiele trudniejsze, niż zwykle myślimy, zanim zaczniemy. Minimalna/maksymalna liczba replik, progi skalowania w górę/w dół, okresy synchronizacji, opóźnienia chłodzenia — wszystkie te ustawienia są bardzo ze sobą powiązane. Modyfikacja jednego najprawdopodobniej wpłynie na inny, ale nadal musisz stworzyć zrównoważoną kombinację, która będzie pasować zarówno do Twojej aplikacji/wdrożenia, jak i Twojej infrastruktury. A jednak w Internecie nie znajdziesz żadnej książki kucharskiej ani magicznej formuły, ponieważ w dużym stopniu zależy to od Twoich potrzeb.

Większość z nas najpierw ustawia je na wartości „losowe” lub domyślne, które następnie dostosowujemy zgodnie z tym, co znajdujemy podczas monitorowania. To dało mi do myślenia: co by było, gdybyśmy byli w stanie ustalić bardziej „matematyczną” procedurę, która pomogłaby nam znaleźć zwycięską kombinację?

Obliczanie parametrów orkiestracji kontenera

Kiedy myślimy o automatycznym skalowaniu mikrousług dla aplikacji, w rzeczywistości patrzymy na poprawę w dwóch głównych punktach:

  1. Zapewnienie szybkiego skalowania wdrożenia w przypadku gwałtownego wzrostu obciążenia (aby użytkownicy nie mieli do czynienia z przekroczeniem limitu czasu lub HTTP 500)
  2. Obniżenie kosztów infrastruktury (tj. zapobieganie niedociążeniu instancji)

Zasadniczo oznacza to optymalizację progów oprogramowania kontenera w celu skalowania w górę i w dół. (Algorytm Kubernetesa ma dla nich jeden parametr).

Pokażę później, że wszystkie parametry związane z instancją są powiązane z progiem upscale. To jest najtrudniejsze do obliczenia – stąd ten artykuł.

Uwaga: Jeśli chodzi o parametry, które są ustawiane dla całego klastra, nie mam dla nich żadnej dobrej procedury, ale na końcu tego artykułu przedstawię oprogramowanie (statyczna strona internetowa), które uwzględnia je podczas obliczania parametry autoskalowania instancji. W ten sposób będziesz mógł różnicować ich wartości, aby rozważyć ich wpływ.

Obliczanie progu skalowania

Aby ta metoda działała, musisz upewnić się, że Twoja aplikacja spełnia następujące wymagania:

  1. Obciążenie musi być równomiernie rozłożone na każdą instancję aplikacji (w sposób okrężny)
  2. Czas żądania musi być krótszy niż interwał sprawdzania obciążenia klastra kontenerów .
  3. Musisz rozważyć uruchomienie procedury na dużej liczbie użytkowników (określonych później).

Główną przyczyną tych warunków jest fakt, że algorytm nie oblicza obciążenia jako przypadającego na użytkownika , ale jako rozkład (wyjaśniony później).

Uzyskiwanie wszystkich Gaussa

Najpierw musimy sformułować definicję szybkiego wzrostu obciążenia lub innymi słowy najgorszego scenariusza. Dla mnie dobrym sposobem na przetłumaczenie tego jest posiadanie dużej liczby użytkowników wykonujących czynności pochłaniające zasoby w krótkim czasie — i zawsze istnieje możliwość, że dzieje się tak, gdy inna grupa użytkowników lub usług wykonuje inne zadania. Zacznijmy więc od tej definicji i spróbujmy wydobyć trochę matematyki. (Przygotuj aspirynę.)

Przedstawiamy kilka zmiennych:

  • $N_{u}$, „wielka liczba użytkowników”
  • $L_{u}(t)$, obciążenie generowane przez pojedynczego użytkownika wykonującego „operację zasobożerną” ($t=0$ wskazuje na moment rozpoczęcia operacji przez użytkownika)
  • $L_{tot}(t)$, całkowite obciążenie (generowane przez wszystkich użytkowników)
  • $T_{tot}$, „krótki okres”

W świecie matematycznym, mówiąc o dużej liczbie użytkowników wykonujących tę samą czynność w tym samym czasie, rozkład użytkowników w czasie jest zgodny z rozkładem Gaussa (lub normalnym), którego wzór to:

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

Tutaj:

  • µ jest wartością oczekiwaną
  • σ to odchylenie standardowe

I jest to przedstawione na wykresie w następujący sposób (przy $µ=0$):

Wykres rozkładu Gaussa, pokazujący, w jaki sposób 99,7% obszaru mieści się między plusem minus trzy sigma

Prawdopodobnie przypomina niektóre zajęcia, które uczęszczałeś – nic nowego. Jednak mamy tu do czynienia z naszym pierwszym problemem: Aby być matematycznie dokładnym, musielibyśmy wziąć pod uwagę zakres czasu od $-\infty$ do $+\infty$, którego oczywiście nie można obliczyć.

Ale patrząc na wykres, zauważamy, że wartości poza przedziałem $[-3σ, 3σ]$ są bardzo bliskie zeru i niewiele się różnią, co oznacza, że ​​ich wpływ jest naprawdę znikomy i można go odłożyć na bok. Jest to tym bardziej prawdziwe, ponieważ naszym celem jest przetestowanie skalowania naszej aplikacji, więc szukamy odmian dla dużej liczby użytkowników.

Dodatkowo, ponieważ przedział $[-3σ, 3σ]$ zawiera 99,7 procent naszych użytkowników, jest on wystarczająco blisko sumy, aby nad nim pracować, a my wystarczy pomnożyć $N_{u}$ przez 1,003, aby nadrobić różnica. Wybranie tego przedziału daje nam $µ=3σ$ (ponieważ będziemy pracować od $t=0$).

Jeśli chodzi o korespondencję z $T_{tot}$, wybranie jej jako równej $6σ$ ($[-3σ, 3σ]$) nie będzie dobrym przybliżeniem, ponieważ 95,4 procent użytkowników znajduje się w przedziale $[- 2σ, 2σ]$, co trwa 4σ$. Zatem wybranie $T_{tot}$ jako równego 6σ$ doda połowę czasu tylko dla 4,3% użytkowników, co nie jest tak naprawdę reprezentatywne. Tak więc wybieramy $T_{tot}=4σ$ i możemy wywnioskować:

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

Czy te wartości zostały właśnie wyciągnięte z kapelusza? TAk. Ale taki jest ich cel i nie wpłynie to na procedurę matematyczną. Te stałe są dla nas i określają pojęcia związane z naszą hipotezą. Oznacza to tylko, że teraz, gdy mamy je ustawione, nasz najgorszy scenariusz można przetłumaczyć jako:

Obciążenie wygenerowane przez 99,7 procent $N{u}$, wykonując operację zużywającą $L{u}(t)$ i gdzie 95,4 procent z nich robi to w czasie $T{tot}$.

(O czym warto pamiętać podczas korzystania z aplikacji internetowej).

Wprowadzając poprzednie wyniki do funkcji rozkładu użytkownika (gaussowskiego), możemy uprościć równanie w następujący sposób:

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

Od teraz, mając zdefiniowane $σ$ i $µ$, będziemy pracować na przedziale $t \in [0, \frac{3}{2}T_{tot}]$ (trwający $6σ$).

Jakie jest całkowite obciążenie użytkownikami?

Drugim krokiem w automatycznym skalowaniu mikrousług jest obliczenie $L_{tot}(t)$.

Ponieważ $G(t)$ jest rozkładem , aby pobrać liczbę użytkowników w określonym momencie, musimy obliczyć jego całkę (lub użyć jego funkcji rozkładu skumulowanego). Ale ponieważ nie wszyscy użytkownicy rozpoczynają swoje operacje w tym samym czasie, wprowadzenie $L_{u}(t)$ i zredukowanie równania do użytecznej formuły byłoby prawdziwym bałaganem.

Aby to ułatwić, użyjemy sumy Riemanna, która jest matematycznym sposobem przybliżenia całki za pomocą skończonej sumy małych kształtów (będziemy tutaj używać prostokątów). Im więcej kształtów (podziałów), tym dokładniejszy wynik. Kolejna zaleta korzystania z podpodziałów wynika z faktu, że możemy uznać, że wszyscy użytkownicy podpodziału rozpoczęli swoją działalność w tym samym czasie.

Wracając do sumy Riemanna, ma ona następującą własność związaną z całkami:

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

Z $x_k$ zdefiniowanym w następujący sposób:

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

Dzieje się tak, gdy:

  • $n$ to liczba podpodziałów.
  • $a$ to dolna granica, tutaj 0.
  • $b$ to górna granica, tutaj $\frac{3}{2}*T_{tot}$.
  • $f$ to funkcja — tutaj $G$ — przybliżająca jej powierzchnię.

Wykres sumy Riemanna dla funkcji G. Oś X przechodzi od zera do trzech połówek T-sub-tot, a pojedynczy prostokąt jest podświetlony, pokazując, że jest pomiędzy x-sub-k-minus-1 i x- sub-k.

Uwaga: liczba użytkowników obecnych w podpodziale nie jest liczbą całkowitą. Jest to powód dwóch warunków wstępnych: posiadania dużej liczby użytkowników (aby część dziesiętna nie miała zbyt dużego wpływu) oraz potrzeby równomiernego rozłożenia obciążenia na każdą instancję.

Zauważ również, że widzimy prostokątny kształt podpodziału po prawej stronie definicji sumy Riemanna.

Teraz, gdy mamy wzór sumy Riemanna, możemy powiedzieć, że wartość obciążenia w czasie $t$ jest sumą liczby użytkowników każdego podpodziału pomnożoną przez funkcję obciążenia użytkownika w odpowiednim czasie . Można to zapisać jako:

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

Po zastąpieniu zmiennych i uproszczeniu formuły otrzymujemy:

\[L_{ tot }( 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 } )\]

I voila ! Stworzyliśmy funkcję ładowania!

Znajdowanie progu skalowania

Na koniec wystarczy uruchomić algorytm dychotomii, który zmienia próg, aby znaleźć najwyższą wartość, przy której obciążenie na instancję nigdy nie przekracza maksymalnego limitu w całej funkcji obciążenia. (To właśnie robi aplikacja).

Dedukowanie innych parametrów orkiestracji

Po znalezieniu progu skalowania w górę ($S_{up}$) inne parametry są dość łatwe do obliczenia.

Od $S_{up}$ poznasz maksymalną liczbę instancji. (Możesz również poszukać maksymalnego obciążenia funkcji ładowania i podzielić przez maksymalne obciążenie na instancję, zaokrąglając w górę).

Minimalna liczba ($N_{min}$) instancji musi być zdefiniowana zgodnie z Twoją infrastrukturą. (Polecam posiadanie co najmniej jednej repliki na AZ.) Ale należy również wziąć pod uwagę funkcję obciążenia: Ponieważ funkcja Gaussa rośnie dość szybko, rozkład obciążenia jest na początku bardziej intensywny (na replikę), więc należy może chcieć zwiększyć minimalną liczbę replik, aby złagodzić ten efekt. (Najprawdopodobniej zwiększy to Twoje $S_{up}$.)

Wreszcie, po zdefiniowaniu minimalnej liczby replik, można obliczyć próg zmniejszania skali ($S_{down}$), biorąc pod uwagę następujące kwestie: Ponieważ skalowanie w dół pojedynczej repliki nie ma większego wpływu na inne instancje niż w przypadku skalowania w dół z $N_{min}+1$ do $N_{min}$, musimy upewnić się, że próg skalowania w górę nie zostanie uruchomiony zaraz po skalowaniu w dół. Jeśli jest to dozwolone, wywoła to efekt jojo. Innymi słowy:

\[( N_{ min } + 1) S_{ w dół } < N_{ min } S_{ w górę }\]

Lub:

\[S_{ w dół } < \frac{N_{ min }}{N_{min}+1}S_{ w górę }\]

Możemy również przyznać, że im dłużej klaster jest skonfigurowany tak, aby czekać przed skalowaniem w dół, tym bezpieczniej jest ustawić $S_{down}$ bliżej wyższego limitu. Po raz kolejny będziesz musiał znaleźć równowagę, która Ci odpowiada.

Zwróć uwagę, że podczas korzystania z systemu orkiestracji Mesosphere Marathon z jego autoskalerem maksymalna liczba instancji, które można usunąć jednocześnie ze skalowania w dół, jest powiązana z AS_AUTOSCALE_MULTIPLIER ($A_{mult}$), co oznacza:

\[S_{ w dół } < \frac{S_{ w górę }}{ A_{mult} }\]

A co z funkcją ładowania użytkownika?

Tak, to trochę problem, a nie najłatwiejszy do matematycznego rozwiązania — jeśli w ogóle jest to możliwe.

Aby obejść ten problem, pomysł polega na uruchomieniu jednej instancji Twojej aplikacji i wielokrotnym zwiększeniu liczby użytkowników wykonujących to samo zadanie, aż obciążenie serwera osiągnie maksimum, które zostało przypisane (ale nie przekroczy). Następnie podziel przez liczbę użytkowników i oblicz średni czas żądania. Powtórz tę procedurę dla każdej akcji, którą chcesz zintegrować z funkcją ładowania użytkowników, dodaj trochę czasu i gotowe.

Zdaję sobie sprawę, że procedura ta implikuje uwzględnienie, że każde żądanie użytkownika jest stale obciążone jego przetwarzaniem (co jest oczywiście niepoprawne), ale masa użytkowników spowoduje ten efekt, ponieważ każdy z nich nie znajduje się na tym samym etapie przetwarzania w tym samym czasie . Sądzę więc, że jest to dopuszczalne przybliżenie, ale po raz kolejny sugeruje, że masz do czynienia z dużą liczbą użytkowników.

Możesz także spróbować z innymi metodami, takimi jak wykresy płomieni procesora. Ale myślę, że bardzo trudno będzie stworzyć dokładną formułę, która połączy działania użytkownika ze zużyciem zasobów.

Przedstawiamy app-autoscaling-calculator

A teraz, w przypadku małej aplikacji sieci Web, o której mowa w całym tekście: jako dane wejściowe przyjmuje funkcję ładowania, konfigurację koordynatora kontenera i kilka innych parametrów ogólnych i zwraca próg skalowania w górę oraz inne dane dotyczące wystąpień.

Projekt jest hostowany na GitHub, ale ma również dostępną wersję na żywo.

Oto wynik podany przez aplikację internetową, porównany z danymi testowymi (na Kubernetes):

Wykres przedstawiający liczbę instancji i obciążenie na instancję w czasie

Skalowanie mikroserwisów: koniec z grzebaniem w ciemności

Jeśli chodzi o architektury aplikacji mikrousług, wdrożenie kontenera staje się centralnym punktem całej infrastruktury. Im lepiej skonfigurowany jest koordynator i kontenery, tym płynniejsze będzie środowisko uruchomieniowe.

Ci z nas w dziedzinie usług DevOps zawsze poszukują lepszych sposobów dostrajania parametrów orkiestracji dla naszych aplikacji. Przyjmijmy bardziej matematyczne podejście do autoskalowania mikrousług!