Вибір технічного стеку
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 зап/с |
| Час відповіді p50 | 28мс | <100мс |
| Час відповіді p95 | 112мс | <300мс |
| Час відповіді p99 | 289мс | <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 спроектована для довгострокової перспективи. Грайте сьогодні та відчуйте гру, побудовану для будь-якого зростання.
Приєднатися до бети →