Wybór Stosu Technologicznego

VvW działa na FastAPI (asynchroniczny framework webowy Python) z asynchronicznym ORM SQLAlchemy łączącym się z PostgreSQL na produkcji (SQLite w development). Redis obsługuje sesje, czasy odnowienia, ograniczanie szybkości i krótkotrwałe cache'y. APScheduler zarządza zadaniami w tle (resety sezonu, dzienna generacja zadań, migawki rankingów).

Dlaczego FastAPI zamiast Django lub Flask? Czyste async od podstaw. Jeden worker FastAPI może obsługiwać setki równoczesnych żądań bez blokowania. Dla gry, gdzie każda akcja gracza to wywołanie API, ma to ogromne znaczenie.

Dlaczego Async Ma Znaczenie dla MMO

W synchronicznym frameworku webowym żądanie wykonujące 3 zapytania do bazy danych zajmuje ~15ms na zapytanie = ~45ms łącznie i blokuje wątek workera przez cały czas. W asynchronicznym frameworku te 3 zapytania odbywają się jednocześnie — całkowity czas spada do ~15ms, a worker jest zwolniony do obsługi innych żądań podczas oczekiwania na I/O.

Dla gry z 1 000 równoczesnych graczy każdy wykonujących 1 żądanie na sekundę, ta różnica to przepaść między serwerem obsługującym obciążenie a serwerem czekającym w nieskończoność. VvW używa async def na każdym endpointcie i await na każdym wywołaniu bazy danych.

Strategia Cachowania Redis

Redis siedzi między FastAPI a PostgreSQL dla każdego odczytu gorącej ścieżki:

Typ Cache'uTTLCo Jest Cachowane
Statyczny JSON (lru_cache)Na zawsze (życie procesu)items.json, monsters.json, locations.json
Rankingi5 minutTop 20 na kategorię
Statystyki postaci60 sekundObliczenia pochodnych statystyk
Info o klanie5 minutNazwa klanu, poziom, liczba członków
HP Bossa Świata10 sekundBieżące HP (gorące podczas rajdów)
Czasy odnowieniaNa akcję (1h–24h)Ostatnie znaczniki czasu lochu/npc/misji

Najważniejszym buforowanym obiektem jest statyczny JSON. Ładowanie items.json (921 przedmiotów, ~450KB) z dysku na każde żądanie przedmiotu byłoby katastrofalne. Pythonowe @lru_cache ładuje go raz przy starcie procesu i serwuje z pamięci przez cały czas życia procesu.

Optymalizacja Bazy Danych

Każda kolumna klucza obcego ma indeks. Każda często odpytywana kolumna ma indeks. Najczęściej trafiony wzorzec zapytań — „pobierz postać po ID, potem pobierz jej ekwipunek" — trafia w indeksy obu tabel i zwraca wyniki w mniej niż 2ms nawet przy 100k wierszach.

Używamy selectinload SQLAlchemy do ładowania relacji zamiast leniwego ładowania. Leniwe ładowanie w kontekście async powoduje problem N+1 zapytań. selectinload uruchamia dwa zapytania łącznie zamiast N+1.

KLUCZOWA OPTYMALIZACJA

Największą poprawą wydajności było dodanie złożonego indeksu na (character_id, item_id) dla zapytań ekwipunku. Odczyty ekwipunku spadły z 12ms do 0,8ms po dodaniu tego indeksu.

Plan Skalowania Poziomego

Ścieżka od 1 serwera do 10 serwerów jest już zaprojektowana:

  • Faza 1 (bieżąca): Pojedynczy VPS Hetzner, nginx jako reverse proxy, 4 workery FastAPI przez Uvicorn
  • Faza 2 (1k graczy): Dodaj replikę odczytu PostgreSQL. Kieruj wszystkie żądania GET do repliki, tylko zapisy do głównego
  • Faza 3 (5k graczy): Oddziel Redis na dedykowaną instancję. Dodaj 2. serwer aplikacji za load balancerem. Lepkie sesje przez Redis (nie w pamięci)
  • Faza 4 (10k+ graczy): Pooling połączeń PostgreSQL przez PgBouncer. CDN dla wszystkich statycznych zasobów, w tym 1 426 stron programatycznych. Separacja serwera WebSocket

Kluczowa decyzja architektoniczna umożliwiająca to: brak lokalnego stanu w workerach FastAPI. Każdy element stanu mieszka w PostgreSQL lub Redis. Oznacza to, że dowolny worker może obsłużyć dowolne żądanie — idealne dla skalowania poziomego.

Wyniki Testów Obciążeniowych

Symulowaliśmy 500 równoczesnych użytkowników przy użyciu Locust, każdy wykonujący realistyczną sesję: logowanie → wyświetlenie dashboardu → 5 polowań → sprawdzenie ekwipunku → odebranie dziennej misji → wylogowanie.

MetrykaWynikCel
Żądania/sekundę847 req/s500 req/s
Czas odpowiedzi p5028ms<100ms
Czas odpowiedzi p95112ms<300ms
Czas odpowiedzi p99289ms<500ms
Wskaźnik błędów0.02%<0.1%
Wyczerpanie puli połączeń DB0 zdarzeń0 zdarzeń

Wąskim gardłem przy 500 równoczesnych użytkownikach był limit puli połączeń PostgreSQL (20 połączeń). Zwiększyliśmy go do 50 i przetestowaliśmy ponownie — p95 spadło do 87ms. Skalowanie Fazy 2 z PgBouncer adresuje to trwale.

CDN dla Stron Programatycznych

Nasze 1 426 programatycznych stron SEO (przedmioty, potwory, lokacje) to idealni kandydaci na CDN — są statycznymi plikami HTML generowanymi z danych JSON. Na CDN każda strona jest serwowana z regionalnego węzła edge w mniej niż 20ms globalnie. Serwujemy je z plików statycznych cachowanych przez nginx w Fazie 1 i przeniesiemy na CDN w Fazie 3.

Zbudowane do Skalowania Razem z Tobą

Architektura VvW została zaprojektowana z myślą o długoterminowej perspektywie. Graj dziś i doświadcz gry zbudowanej do obsługi każdego wzrostu.

Dołącz do Bety →