Dwa sposoby na obsłużenie większego obciążenia
Wstęp
Gdy usługa zaczyna nie nadążać z rosnącym obciążeniem, operator staje przed wyborem. Zwiększyć moc istniejącej maszyny (więcej CPU, więcej RAM, szybsze dyski). Albo dodać więcej maszyn, z których każda wykonuje tę samą pracę.
Pierwsza ścieżka to skalowanie pionowe (scale up). Druga to skalowanie poziome (scale out).
Ta lekcja wyjaśnia, dlaczego niemal każda nowoczesna architektura webowa wybiera skalowanie poziome oraz jaka cecha obciążenia czynia ten wybór możliwym. Odpowiedź kryje się w jednym słowie: stan.
Na koniec zrozumiesz:
- Krzywe kosztów skalowania pionowego vs. poziomego oraz gdzie każde z nich ma zastosowanie
- Co w praktyce oznaczają terminy „stanowy” (stateful) i „bezstanowy” (stateless) oraz dlaczego jeden z nich mnoży się tanio
- Matematykę, która wyznacza rozmiar floty replik przy obciążeniu oczekiwanym i szczytowym
- Zasadę zapasu wydajności (headroom), która zapobiega załamaniu warstwy po przekroczeniu kolana kolejki oczekujących
- Gdzie musi znajdować się stan (nigdy nie znika) oraz jak wypchnąć go z warstw, które wymagają skalowania
Dlaczego skalowanie poziome wygrywa po przekroczeniu progu
Skalowanie pionowe: Jeden większy serwer
Zalety: prostota. Brak zmian w kodzie. Brak konieczności koordynacji. Ten sam proces ma teraz więcej CPU.
Wady: limit. Największa komercyjnie dostępna maszyna wirtualna ma skończoną ilość pamięci RAM i rdzeni. Powyżej tego poziomu, żadne pieniądze nie kupią więcej zasobów. Koszty rosną superliniowo po przekroczeniu optymalnego punktu oferty dostawcy. Awaria tej jednej maszyny powoduje wstrzymanie całego serwisu.
Skalowanie poziome: wiele mniejszych maszyn
Zalety: brak limitu (z wyjątkiem Twojej gotowości do płacenia za maszyny i ich koordynacji). Pojemność rośnie liniowo wraz z liczbą replik, przewidywalnie. Awaria pojedynczej repliki usuwa 1/N pojemności, a nie 100%.
Wady: wymaga, aby obciążenie pracy to wspierało. Niektóre obciążenia (pojedyncza duża baza danych, serwer gry z stanem przechowujący aktywne sesje) opierają się skalowaniu poziomemu. Koordynacja i dystrybucja obciążenia stają się kwestiami operacyjnymi.
Punkt przełomowy: każdy serwis produkcyjny, który musi przetrwać awarię dowolnej pojedynczej maszyny, musi działać na co najmniej dwóch maszynach. Akceptując dwie maszyny, już wybrałeś skalowanie poziome. Od tego momentu pytanie brzmi nie „czy?”, ale „jak tanio możemy dodać kolejną replikę?”
Kluczowy czynnik umożliwiający: obciążenie pracy, które nie przechowuje stanu na poziomie pojedynczego żądania na samej maszynie. Wtedy dowolna replika może odpowiedzieć na dowolne żądanie, a dodanie repliki zwiększa pojemność bez konieczności koordynacji.
Stanowy vs bezstanowy w praktyce
Stan nigdy nie znika, tylko się przemieszcza
Komponent stanowy: przechowuje informacje, których utrata zmieniłaby zachowanie systemu. Baza danych przechowująca konta użytkowników. Pamięć podręczna (cache) przechowująca tokeny sesji. Worker przypinający długotrwałe połączenie strumieniowe do konkretnego użytkownika.
Komponent bezstanowy: nie przechowuje żadnych informacji, których utrata miałaby znaczenie. Warstwa webowa, która odczytuje żądanie, zapytuje bazę danych i zapisuje odpowiedź. Każde żądanie jest samodzielne; warstwa nie pamięta nic między żądaniami.
Kluczowa obserwacja: stan nigdy nie znika z systemu. Przenosi się do warstwy zaprojektowanej do jego przechowywania (baza danych, klastr Redis, magazyn obiektów). Dzięki temu warstwy obsługujące ruch mogą być bezstanowe, a warstwy bezstanowe skalują się poziomo, ponieważ każdy replika może obsłużyć dowolne żądanie.
Test praktyczny: gdybyś losowo zabił jeden proces w tej warstwie i go zrestartował, czy którykolwiek użytkownik doświadczyłby błędnej odpowiedzi lub utraty sesji? Jeśli tak, warstwa przechowuje stan. Jeśli nie, nie przechowuje.
Przykłady
- Proces webowy w Pythonie, który odczytuje żądania, zapytuje Postgres i zwraca JSON: bezstanowy. Stan znajduje się w Postgres.
- Proces webowy w Pythonie, który przechowuje koszyki zakupowe użytkowników w pamięci lokalnej: stanowy. Zabicie procesu powoduje utratę koszyków.
- Serwer WebSocket utrzymujący otwarte połączenia z użytkownikami czatu: stanowy w sensie połączeń. Zabijanie procesu zrywa połączenia; klienci muszą się połączyć ponownie. Często można je skalować poziomo z odpowiednią ostrożnością (sesje przyklejone, spójne hashowanie).
- Cache Redis przed bazą Postgres: stanowy dla zawartości cache, ale akceptowalny, jeśli brak w cache jest tolerowany. Awaria repliki oznacza brak w cache, a nie utratę danych.
Projektowanie pod skalowanie poziome = wypychanie stanu z warstwy, która musi się skalować.
Audyt podejrzanej warstwy
Zespół uruchamia API rekomendacji na 6 maszynach wirtualnych backendu za proxy odwróconym. Aplikacja: odczytuje identyfikator użytkownika z żądania, pobiera ostatnią aktywność użytkownika z Postgres, uruchamia algorytm punktacji i zwraca listę rekomendowanych elementów. Dwa niestandardowe zachowania:
- Aplikacja utrzymuje cache „ostatniej aktywności użytkownika” w pamięci procesu, wypełniany przy pierwszym żądaniu dla danego użytkownika i ponownie używany przy kolejnych żądaniach.
- Aplikacja używa sesji przyklejonych (sticky sessions): gdy użytkownik trafi na maszynę wirtualną #3, wszystkie jego kolejne żądania trafiają na maszynę wirtualną #3 (proxy jest skonfigurowany do przyklejonego routingu na podstawie ciasteczka).
Wzór na repliki
Najprostszy wzór na pojemność
Gdy warstwa stanie się bezstanowa, jej skalibrowanie sprowadza się do arytmetyki. Potrzebujesz wystarczającej liczby replik, aby obciążenie w stanie ustalonym przybywało i odchodziło w tym samym tempie, z zapasem na nagłe skoki.
Wzór:
repliki = ⌈ (szczytowe_obciążenie × współczynnik_skoju) / wydajność_jednej_repliki ⌉ + zapas
Gdzie:
- szczytowe_obciążenie: maksymalne utrzymywane żądania na sekundę, których oczekujesz w normalnej eksploatacji
- współczynnik_skoju: mnożnik pokrywający krótkotrwałe skoki powyżej szczytu (zwykle 1,5x do 2x dla przewidywalnego ruchu, 3x lub więcej dla wirusowego / nieprzewidywalnego)
- wydajność_jednej_repliki: żądania na sekundę, które jedna replika obsługuje przy akceptowalnym opóźnieniu i wykorzystaniu (zwykle mierzony przy 70% CPU, a nie przy nasyceniu)
- zapas: dodatkowe repliki, aby awaria kilku replik nie doprowadziła do załamania warstwy (zwykle 1-2 repliki dla małych flot, 10-20% dla większych)
Przykład obliczeniowy: backend obsługuje 100 żądań/s przy 70% wykorzystania CPU na replikę. Szczyt obciążenia wynosi 600 żądań/s. Oczekujesz okazjonalnych skoków obciążenia 2-krotnych. Chcesz przetrwać awarię 2 replik.
repliki = ⌈ (600 × 2) / 100 ⌉ + 2 = 12 + 2 = 14 replik
Zasada 80%
Pojemność pojedynczej repliki to nie punkt nasycenia. Mierz pojemność przy 70–80% wykorzystania CPU, nigdy przy 100%.
Powyżej około 80% wykorzystania krzywe kolejkowania rosną gwałtownie: kolejka, która przy 60% wykorzystania działała w 10 ms, przy 90% wykorzystania działa w 80 ms. To opóźnienie, a nie przepustowość, ulega najpierw degradacji. (W towarzyszącej lekcji geometry_of_stateless_horizontal_scaling wyprowadzono tę krzywą matematycznie.)
Autoskalowanie vs. statyczne przydzielanie zasobów
Statycznie: przydziel zasoby na szczyt obciążenia z zapasem na skoki i zaakceptuj koszt działania przy niskim wykorzystaniu poza godzinami szczytu.
Autoskalowanie: kontroler dodaje i usuwa repliki na podstawie obserwowanego wykorzystania, docelowego opóźnienia lub głębokości kolejki.
Zastrzeżenie dotyczące autoskalowania: czas uruchomienia na zimno ma znaczenie. Jeśli nowy replikant potrzebuje 2 minut na start, autoskalowanie nie może zareagować na 30-sekundowy wzrost obciążenia. Dojrzałe systemy autoskalowania utrzymują ciepły pulę wstępnie zaalokowanych replikantów tuż poniżej progu skalowania w górę.
Dobierz rozmiar floty dla nowej usługi
Twoja zespół planuje uruchomienie API metadanych wideo. Testy wydajności pokazują, że pojedynczy replikant obsługuje 250 żądań/s przy 70% wykorzystania CPU i opóźnieniu p99 wynoszącym 50 ms. Dział marketingu prognozuje szczytowe obciążenie na poziomie 4 000 żądań/s w godzinach szczytu. Zaplanowane wydarzenie promocyjne może powodować krótkotrwały wzrost do 3-krotności szczytu. Usługa ma przetrwać jednoczesną awarię 3 replikantów, nie przekraczając 80% wykorzystania na pozostałych replikantach.
Zimny start, powolne wygaszanie i inne rzeczywiste ograniczenia
Rzeczywiste floty mają rzeczywiste ograniczenia
Wzór zakłada, że repliki pojawiają się natychmiast, natychmiast przyjmują ruch i natychmiast go zrzucają. Żadne z tych założeń nie jest prawdziwe w środowisku produkcyjnym.
Zimny start: nowa replika potrzebuje czasu na uruchomienie systemu operacyjnego, start procesu, załadowanie konfiguracji, rozgrzanie pamięci podręcznej oraz przejście testów zdrowia. Może to trwać od 5 sekund (restart kontenera) do 5 minut (pełne uruchomienie maszyny wirtualnej + pobranie obrazu). Automatyczne skalowanie nie może reagować na skoki obciążenia krótsze niż ten opóźniony czas.
Powolne wygaszanie: replika usuwana z puli potrzebuje czasu na zakończenie trwających żądań przed zamknięciem. W przeciwnym razie użytkownicy zobaczą obcięte odpowiedzi. Odwrócone proxy wspierają wygaszanie (przestanie przyjmować nowe żądania, zakończenie aktywnych), ale trwa to od kilku sekund do kilku minut.
Ciepły pulpit (warm pool): produkcyjne floty utrzymują pulę wstępnie skonfigurowanych, ale bezczynnych replik, gotowych do przejęcia ruchu po sygnale. To wymiana niewielkiego, stałego kosztu na szybką reakcję na nagły wzrost obciążenia.
Opróżnianie połączeń (connection draining) vs natychmiastowe zamknięcie (immediate kill): łagodne wyłączenie ma znaczenie. Sygnał SIGTERM, który inicjuje opróżnianie połączeń, trwa dłużej niż SIGKILL, ale nie przerywa żądań użytkowników.
Okno sprawdzania stanu (health check window): replika, która właśnie się uruchomiła, może przejść pierwszy test stanu, zanim jej pula połączeń z bazą danych będzie „ciepła”; wtedy proxy wysyła rzeczywisty ruch, a pierwsze kilkanaście żądań jest wolne. Dostosuj testy stanu tak, aby testowały rzeczywistą ścieżkę, a nie tylko żywotność procesu.
Kreep przyklejania (stickiness creep): nawet nominalnie bezstanowe warstwy z czasem nabierają cech przyklejania (pamięci podręczne CDN, pamięci podręczne resolverów DNS, pule połączeń). Bądź podejrzliwy wobec „identycznych replik”, które mimo to zachowują się różnie.
Ciepły pulpit (Warm Pool) czy reaktywna autoskalowalność?
Twoje API metadanych wideo (to samo z poprzedniego pytania, rozmiarowane na 51 replik dla szczytu stałego + nagłego wzrostu) doświadcza 30-sekundowego skoku do 5-krotności normalnego obciążenia za każdym razem, gdy zostanie wgrane nowe wirusowe wideo. Obecna autoskalowalność potrzebuje 90 sekund, aby dodać nową replikę od zera (pobranie obrazu + rozgrzanie). W ciągu tych 90 sekund opóźnienia rosną gwałtownie, a niektóre żądania kończą się przekroczeniem limitu czasu.
Projektowanie warstwy bezstanowej w ramach ograniczeń
Synteza
Dowiedziałeś się, dlaczego skalowanie poziome wygrywa po przekroczeniu małego progu, co oznacza stan w praktyce, jak rozmiarować flotę w przypadku obciążenia oczekiwanego i szczytowego oraz gdzie skalowanie poziome traci skuteczność na granicach.
Zastosuj wszystkie cztery.
Zaprojektuj warstwę backendu dla feed.example.com, API strumienia społecznościowego. Ograniczenia: pojemność na replikę 200 żądań/s przy 70% obciążenia CPU; oczekiwane szczytowe obciążenie 1500 żądań/s; współczynnik nagłego wzrostu 2,5x (okresowe trendy); przeżycie 2 jednoczesnych awarii replik; czas zimnego startu 60 sekund; nagłe wzrosty mogą trwać 45 sekund; budżet pozwala na pewną ilość pojemności w stanie gotowości, ale nie na stałe zabezpieczenie 2,5x pojemności.
Kierunek dalszego rozwoju tego kursu
Dokąd zmierza ten kurs
Masz teraz działający model mentalny warstwy bezstanowej: dlaczego się skaluje, jak ją rozmiarować, co się psuje na jej granicach oraz dokąd musi przenieść się stan, gdy wypychasz go z warstwy, która musi rosnąć.
Następna lekcja w tym kursie (cs_distsys_ingress_egress_separation) zajmuje się subtelniejszym problemem: nawet idealnie rozmiarowana warstwa bezstanowa może zawieść w zaskakujący sposób, gdy ruch przychodzący i wychodzący współdzielą tę samą ścieżkę sieciową. Klasycznym przykładem jest proxy próbujący połączyć się z samym sobą; rozwiązanie polega na podziale jednej warstwy na dwie o różnych odpowiedzialnościach.
Lekcja towarzysząca: geometry_of_stateless_horizontal_scaling wyprowadza krzywą kolejkowania, zastosowanie prawa Little'a do floty replik oraz geometryczne znaczenie kolana przy 80% wykorzystania.
Brawo. Dalej.