Вибір технічного стеку

VvW працює на FastAPI (асинхронний веб-фреймворк Python) із SQLAlchemy async ORM, що підключається до PostgreSQL у продакшені (SQLite у розробці). Redis обробляє сесії, кулдауни, обмеження частоти та короткострокові кеші. APScheduler управляє фоновими завданнями (скидання сезонів, щоденна генерація квестів, знімки таблиці лідерів).

Чому FastAPI, а не Django або Flask? Чистий async від початку. Один воркер FastAPI може обробляти сотні паралельних запитів без блокування. Для гри, де кожна дія гравця є API-викликом, це має величезне значення.

Чому async важливий для MMO

У синхронному веб-фреймворку запит, що виконує 3 запити до бази даних, займає ~15мс на запит = ~45мс загалом, і блокує потік воркера весь цей час. В асинхронному фреймворку ці 3 запити виконуються паралельно — загальний час знижується до ~15мс, а воркер вивільняється для обробки інших запитів під час очікування I/O.

Для гри з 1 000 одночасних гравців, кожен з яких робить 1 запит на секунду, ця різниця є різницею між сервером, що справляється з навантаженням, і тим, що нескінченно ставить у чергу. VvW використовує async def на кожному ендпоінті та await на кожному виклику бази даних.

Стратегія кешування Redis

Redis знаходиться між FastAPI та PostgreSQL для кожного читання на гарячому шляху:

Тип кешуTTLЩо кешується
Статичний JSON (lru_cache)Назавжди (час процесу)items.json, monsters.json, locations.json
Таблиці лідерів5 хвилинТоп-20 за категорією
Статистика персонажа60 секундОбчислення похідної статистики
Інформація клану5 хвилинНазва клану, рівень, кількість членів
HP Світового боса10 секундПоточний HP (гарячий під час рейдів)
КулдауниЗа дією (1–24 год)Останні мітки часу підземелля/npc/місії

Найважливіший кешований об'єкт — статичний JSON. Завантаження items.json (921 предмет, ~450КБ) з диска на кожен запит предмета було б катастрофічним. Python's @lru_cache завантажує його один раз при запуску процесу та обслуговує з пам'яті протягом усього часу існування процесу.

Оптимізація бази даних

Кожен стовпець зовнішнього ключа має індекс. Кожен часто запитуваний стовпець має індекс. Найбільш використовуваний шаблон запиту — "отримати персонажа за ID, потім отримати його інвентар" — уражає індекси в обох таблицях і повертається менш ніж за 2мс навіть при 100k рядках.

Ми використовуємо selectinload SQLAlchemy для завантаження відносин замість ліниво завантаження. Ліниве завантаження в асинхронному контексті спричиняє проблему N+1 запиту. selectinload запускає два запити всього замість N+1.

КЛЮЧОВА ОПТИМІЗАЦІЯ

Найбільшим покращенням продуктивності стало додавання складеного індексу на (character_id, item_id) для запитів інвентарю. Читання інвентарю впало з 12мс до 0.8мс після додавання цього індексу.

План горизонтального масштабування

Шлях від 1 сервера до 10 серверів вже спроектований:

  • Фаза 1 (поточна): Один VPS Hetzner, nginx як зворотній проксі, 4 воркери FastAPI через Uvicorn
  • Фаза 2 (1к гравців): Додати репліку PostgreSQL для читання. Маршрутизувати всі GET-запити до репліки, лише записи до первинної
  • Фаза 3 (5к гравців): Перенести Redis на виділений екземпляр. Додати 2-й сервер застосунків за балансувальником навантаження. Sticky sessions через Redis (не в пам'яті)
  • Фаза 4 (10к+ гравців): Пулування з'єднань PostgreSQL через PgBouncer. CDN для всіх статичних ресурсів, включаючи 1 426 програматичних сторінок. Розподілення WebSocket-сервера

Ключове архітектурне рішення, що робить це можливим: без локального стану у воркерах FastAPI. Кожен фрагмент стану живе в PostgreSQL або Redis. Це означає, що будь-який воркер може обробляти будь-який запит — ідеально для горизонтального масштабування.

Результати навантажувальних тестів

Ми симулювали 500 одночасних користувачів за допомогою Locust, кожен виконував реалістичну сесію: вхід → перегляд панелі → 5 полювань → перевірка інвентарю → отримання щоденної місії → вихід.

МетрикаРезультатЦіль
Запитів/секунду847 зап/с500 зап/с
Час відповіді p5028мс<100мс
Час відповіді p95112мс<300мс
Час відповіді p99289мс<500мс
Частота помилок0.02%<0.1%
Вичерпання пулу з'єднань БД0 подій0 подій

Вузьким місцем при 500 одночасних користувачах був ліміт пулу з'єднань PostgreSQL (20 з'єднань). Ми збільшили його до 50 та перетестували — p95 впав до 87мс. Масштабування фази 2 з PgBouncer вирішить це назавжди.

CDN для програматичних сторінок

Наші 1 426 програматичних SEO-сторінок (предмети, монстри, локації) є ідеальними кандидатами для CDN — вони є статичними HTML, згенерованими з JSON-даних. На CDN кожна сторінка обслуговується з регіонального вузла за менш ніж 20мс глобально. Ми обслуговуємо їх з nginx-кешованих статичних файлів у фазі 1 та перейдемо на CDN у фазі 3.

Побудовано для масштабування разом з вами

Архітектура VvW спроектована для довгострокової перспективи. Грайте сьогодні та відчуйте гру, побудовану для будь-якого зростання.

Приєднатися до бети →