Skalowanie aplikacji on-prem. Jak przejść od monolitu do klastra

Skalowanie aplikacji on-prem bez chmury? Poznaj 3 sprawdzone strategie: od tuningu bare metal, przez Docker i Traefik, po klastry wysokiej dostępności.

Kiedy uruchamiasz nowy projekt, a szczególnie monolityczny, świat wydaje się prosty. Wrzucasz kod na jedną maszynę, konfigurujesz serwer WWW, dorzucasz bazę danych i wszystko działa wyśmienicie. Monolit potraktowany domyślnymi konwencjami jest niesamowicie produktywny. Problemy zaczynają się wtedy, gdy projekt zaczyna generować realny ruch, a budżet infrastrukturalny lub specyfika danych (np. wymogi prawne, wrażliwe dane klientów) zmuszają Cię do pozostania w ekosystemie on-premises, czyli na własnych serwerach, bez elastycznej chmury AWS czy Google Cloud.

Wtedy często pojawia się panika. Z każdej strony słychać głosy, że trzeba natychmiast przepisać wszystko na mikroserwisy, wdrożyć Kubernetes i zatrudnić armię DevOpsów. To bzdura, którą słyszę bardzo często i powoduje ona u mnie negatywne emocje. Dobrze napisana aplikacja pozwala skalować infrastrukturę bez wywracania kodu do góry nogami. Zamiast ślepo gonić za trendami, przechodzę przez trzy logiczne etapy na skalowanie aplikacji on-prem, dopasowując architekturę do faktycznego obciążenia.

Złota zasada skalowania

Na samym początku trzeba też wspomnieć, że zawsze obowiązuje złota zasada skalowania, o której łatwo zapomnieć w szale infrastrukturalnym: zawsze skaluj tylko te części systemu, które tego realnie wymagają. Jeśli baza danych radzi sobie świetnie, a aplikacja dławi się jedynie przy masowej wysyłce powiadomień lub generowaniu ciężkich raportów PDF, rozbudowa całej warstwy HTTP nie rozwiąże problemu. W takim scenariuszu zamiast mnożyć serwery WWW, wystarczy wydzielić same procesy odpowiedzialne za obsługę kolejek zadań asynchronicznych. Identycznie podchodzi się do separacji ruchu, bo czasem wystarczy przenieść samo statyczne API dla aplikacji mobilnej na osobny proces/kontener, pozostawiając resztę systemu nietkniętą. Precyzyjne uderzenie w wąskie gardło oszczędza czas, zasoby sprzętowe i drastycznie zmniejsza ryzyko awarii podczas wdrożenia.

Skalowanie aplikacji on-prem poprzez wyciśnięcie soków z bare metal

Zanim zacznę rozpraszać aplikację na wiele maszyn i walczyć z opóźnieniami sieciowymi, zawsze maksymalizuję wydajność tego, co już mam. Skalowanie pionowe (vertical scaling) na jednej maszynie on-prem to najtańsza i najbardziej wydajna metoda, ponieważ eliminuje jakikolwiek narzut na wirtualizację czy komunikację między serwerami.

Większość ludzi myśli, że skalowanie ruchu wymaga mnożenia instancji aplikacji. Na jednym serwerze to błąd. Tutaj ruch skaluje się poprzez precyzyjne dostrojenie procesów obsługujących język programowania (np. PHP-FPM, Node.js, Python) oraz wyeliminowanie operacji I/O na dysku.

Moim pierwszym krokiem jest optymalizacja trybu pracy menedżera procesów. Weźmy za przykład PHP-FPM, gdzie od razu zmieniam tryb pracy, z dynamicznego na statyczny (pm = static). W trybie dynamicznym system bezustannie tworzy i zabija procesy worker-ów, co przy nagłym skoku ruchu dławi procesor. Tryb statyczny rezerwuje stałą pulę pamięci RAM na procesy, które nieustannie czekają na żądania.

Jeśli serwer dysponuje 32 GB RAM, system operacyjny i baza danych potrzebują np. 12 GB, a średni proces konsumuje 50 MB, ustawiam stałą pulę na około 400 robotników. Serwer WWW (Nginx) na tej samej maszynie po prostu przekazuje cały ruch z portu 443 bezpośrednio do tego gniazda Unix.

Do tego dochodzi żelazny punkt wdrożenia produkcyjnego w CI/CD: pełne buforowanie konfiguracji, tras i widoków w pamięci RAM. Aplikacja przestaje marnować cykle procesora na parsowanie plików przy każdym request’cie. Całość działa na jednym serwerze, baza danych współdzieli pamięć poprzez szybkie sockety lokalne, a opóźnienia sieciowe wynoszą okrągłe zero.

Skalowanie aplikacji on-prem: bare metal

Skalowanie aplikacji on-prem: konteneryzacja i mikro-skalowanie na jednej maszynie

Co robię, gdy aplikacja rośnie? Gdy procesy zaczynają się nawzajem blokować, a ja chcę ułatwić sobie wdrożenia i odizolować od siebie komponenty, wciąż mając do dyspozycji tylko jeden fizyczny serwer? Wtedy wkracza konteneryzacja, ale bez skomplikowania, które niesie ze sobą Kubernetes.

Na tym etapie dzielę zasoby jednej maszyny za pomocą Docker Compose, a jako punkt wejścia (Reverse Proxy) stosuję Traefika. Dlaczego akurat Traefik? Ponieważ posiada on świetną funkcję Service Discovery. Traefik potrafi nasłuchiwać demona Dockera i automatycznie konfigurować routing do kontenerów na podstawie przypisanych im w locie etykiet (labels).

W tej architekturze aplikacja musi stać się całkowicie bezstanowa (stateless). Wszystkie sesje, pamięć podręczna i dane tymczasowe wędrują z plików lokalnych do osobnego kontenera z bazą Redis lub bezpośrednio do bazy danych. Dysk kontenera musi być traktowany jako ulotny.

Gdy ruch rośnie, nie bawię się w ręczną edycję plików konfiguracyjnych. Zamiast tego używam mechanizmu skalowania kontenerów. Jedną komendą potrafię powielić instancje aplikacji wewnątrz tej samej maszyny:

docker compose up -d --scale app=4

Docker uruchamia cztery niezależne kontenery z aplikacją, a Traefik w ułamku sekundy wykrywa nowe procesy i zaczyna równomiernie rozkładać między nie ruch na porcie 443. Zyskuję pełną izolację, łatwość cofania wersji (rollback) i elastyczność, wciąż nie opuszczając jednego serwera.

Skalowanie aplikacji on-prem: mirko skalowanie

Skalowanie aplikacji on-prem: pełne rozproszenie on-prem

Kiedy pojedyncza maszyna fizyczna osiąga granice swoich możliwości (zarówno procesora, jak i pamięci), czas na rozdzielenie architektury na dedykowane serwery. To ostateczny krok skalowania on-prem (horizontal scaling), który wdrażam.

Moje klasyczne, najbardziej stabilne podejście polega na postawieniu na froncie jednej, dedykowanej maszyny pełniącej wyłącznie rolę Load Balancera (używam do tego często Nginx lub HAProxy, im prościej tym lepiej). Jej jedynym zadaniem jest przyjmowanie ruchu ze świata, terminacja połączeń SSL i kierowanie czystego ruchu HTTP w głąb bezpiecznej sieci wewnętrznej do maszyn aplikacyjnych.

Za Load Balancerem znajdują się osobne serwery aplikacyjne (Node 1, Node 2, Node X). Lecz aby ta struktura działała:

  • Baza danych: Wędruje na dedykowany serwer (często w układzie Primary/Replica, gdzie aplikacja zapisuje dane na serwerze głównym, a czyta z repliki).
  • Pliki użytkownika: Przenoszę na osobną maszynę np. z MinIO (lokalny odpowiednik AWS S3), z którym aplikacja komunikuje się przez API, zamiast polegać na lokalnym dysku twardym.

Można iść o krok dalej…

Aby wycisnąć absolutne maksimum z tej architektury, czasami łączę podejście drugie z trzecim. Serwery aplikacyjne stojące za głównym Load Balancerem nie muszą być surowymi systemami operacyjnymi.

Główny Load Balancer (np. Nginx) kieruje ruch do dwóch oddzielnych maszyn fizycznych. Na każdej z tych maszyn uruchamiam Dockera oraz lokalny serwer Traefik. Ruch z głównego Load Balancera trafia do Traefika na danej maszynie, a ten rozrzuca go na lokalnie wyskalowane (komendą --scale) kontenery z aplikacją.

W takim scenariuszu kluczowa staje się poprawna konfiguracja nagłówków proxy w kodzie. Ponieważ ruch przechodzi przez kilka warstw (Główny Load Balancer -> Lokalny Traefik -> Kontener), aplikacja musi wiedzieć, którym adresom IP w sieci wewnętrznej może ufać. Konfiguruję warstwę middleware tak, aby akceptowała nagłówki X-Forwarded-For oraz X-Forwarded-Proto z sieci lokalnej (konfiguracja Trusted Proxies). Dzięki temu aplikacja poprawnie rozpoznaje prawdziwy adres IP klienta oraz wie, czy użytkownik połączył się bezpiecznym protokołem HTTPS.

W ten sposób uzyskuję potężną, redundantną architekturę na własnym sprzęcie o wysokiej dostępności. Awaria jednego serwera aplikacyjnego lub błąd wewnątrz jednego z kontenerów nie kładzie całego systemu. Ruch jest automatycznie kierowany do sprawnych zasobów, a ja mogę spać spokojnie bez dotykania drogich rozwiązań chmurowych.

Skalowanie aplikacji on-prem: pełne rozproszenie

Skalowanie aplikacji on-prem bez kubernetesa?

Wdrożenie Kubenertesa we własnej serwerowni to zupełnie inna gra niż kliknięcie „Utwórz klaster” w chmurze publicznej. On-premise nie dostajesz w pakiecie gotowego load balancera, elastycznego storage typu EBS ani automatycznego zarządzania siecią. Wszystko, od konfiguracji sieci podów (SDN np. Calico lub Cilium), przez lokalny load balancing (np. MetalLB), aż po rozproszoną warstwę danych (np. Ceph czy Longhorn), musisz wdrożyć, monitorować i utrzymywać samodzielnie. To generuje ogromny koszt operacyjny, określany jako Kubernetes Day 2 operations. Aktualizacje wersji klastra, zarządzanie certyfikatami wewnętrznymi czy debugowanie problemów sieciowych między podami potrafią sparaliżować mały zespół.

Na podstawie mojego doświadczenia, uważam, że opisana architektura oparta na Docker Compose i Traefiku pozwala na liniowy wzrost wydajności przy minimalnym skomplikowaniu, dzięki czemu kopie bezpieczeństwa, monitoring oraz automatyzacja wdrożeń pozostają proste, przewidywalne i zrozumiałe bez dedykowanego zespołu DevOps i drogiej chmury.

Autor wpisu

blog@orbisbit.com

Komentarze

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Wymagane pola są oznaczone *

Sprawdź również