Как мы делали XVM — популярный мод для World of Tanks. Часть вторая: развитие серверной части

Как мы делали XVM — популярный мод для World of Tanks. Часть вторая: развитие серверной части

Приветствуем, уважаемое хабрасообщество! Сегодня мы продолжаем начатый в первой части рассказ о создании модификации XVM (eXtended Visualization Mod) для игры World of Tanks. Во второй части вас ждет описание истории развития серверной части мода.

Древнее царство

Как я уже говорил в первой части, самый первый бэкэнд мода работал на VPS, написан был на PHP и хранил базу игроков в виде файлов в файловой системе. Чтобы избежать лимита inode-ов 64K (лимит файлов в одном каталоге обычно настраивается, но на той VPS, похоже, настройка была залочена), применялась трехуровневая структура.

Первые две буквы ника игрока становились именем первой директории от корня БД. Следующие две буквы — именем вложенной в нее директории. И уже в ней-то и лежали plain-файлы с данными игроков и именами, равными никам.

Запросы выглядели так: http://domain.com/users/<nickname> .

На каждого игрока отправлялся свой запрос. То есть, из одного боя могло прийти до 30 запросов. Правда, на клиентской части применялось кеширование уже увиденных игроков, что слегка снижало нагрузку. Затем от этого кеширования отказались из-за крайне низкого процента попаданий в рандоме.

Несмотря на примитивность, у этой реализации было весомое преимущество — она работала даже на VPS и «тянула» сотню-другую запросов в секунду.

Надежда

После переезда на полноценный сервер была не менее оперативно запилена еще одна реализация, на сей раз на PHP + MySQL. Нам тогда вообще казалось, что для нашей задачи (запрос по одной записи по индексу), да еще запущенной на таком «мощном» железе (EQ4: Intel Core i7-920, 8 GB DDR3, 2x 750 GB SATA II HDD), можно использовать все антипаттерны проектирования — и оно все равно заработает. Реальность, как обычно, не подвела.

Итак, что же мы там «наколхозили»:

  1. Шаблон запроса, естественно, оставили как есть.
  2. Запрос проверялся регуляркой на похожесть на ник.
  3. Искали в БД этот ник.
  4. Если находили и он был обновлен не слишком давно, то отдавали его в ответе.
  5. Если не находили, либо запись была слишком старая, то начиналось самое интересное.

Видим, что у нас теперь есть не только id, но и готовая ссылка. Все, можно парсить.

К счастью, примерно тогда же появилось мобильное приложение World of Tanks Assistant, от Wargaming. Приложение вполне себе отображало статистику любого игрока. Добрые люди провели исследование и выяснили протокол обмена.

Данные грузились по адресу: http://worldoftanks.ru/uc/accounts//api//?source_token=Intellect_Soft-WoT_Mobile-unofficial_stats ApiVersion за время его использования нами и до появления официального API успел вырасти с 1.3 до 1.7. Каких-либо отличий для нас в них не было. Правда, проблема получения playerID прежде, чем делать запрос к API, никуда не делась.

И да, если кто еще не понял: запрос данных игрока происходил прямо во время обработки клиентского запроса.

Данный шедевр инженерной мысли стабильно работал под нагрузкой 200-300 запросов/секунду, что было лишь немногим больше, чем самый первый вариант, работавший на статических файлах. При повышении нагрузки мы упирались в CPU.

Эпоха возрождения

Поначалу моменты, когда сервер не справлялся, случались только по вечерам пятницы и субботы. Со временем эти «моменты» все увеличивались и увеличивались. Стало очевидно: что-то тут не так. Нагрузка у нас не рекордная, сервер не самый слабый. Проблема усугублялась тем, что опытных PHP-шников у нас не было. Как и не было опытных *nix специалистов.

Сели думать. Надумали следующее:

  1. Самое главное — мы занялись улучшением клиентской части, и нашли способ получать не только ники игроков, но и ID.
  2. Оптимизировать клиентские запросы, чтобы запрашивать игроков не по одному, а сразу всех из одного боя.
  3. Попробовать модную связку NodeJS + Mongo.

Запросы стали выглядеть так:

Первый же вариант показал обнадеживающие результаты — он тянул до 100-150 запросов/секунду. Новых запросов, на 30 игроков, т.е. это эквивалентно

4000 старых запросов по одному игроку. Прирост более чем в 10 раз на смене технологии нас очень впечатлил, и, поскольку на тот момент запас прочности был очень солидным, — мы стали развивать «статистические» возможности XVM.

В очередной раз сели думать и надумали сделать что-то типа REST-сервиса со следующими методами:

    /001/test — клиент при старте определяет с помощью этого метода, что сервер вообще жив. Кроме этого есть еще китайцы, которых, как известно много. Много настолько, что уже на этом этапе они создали у себя несколько серверов, используя наш код, а клиент отправлял запрос /test на каждый известный ему сервер и выбирал среди них лучший по времени отклика. Вариант /001 появился первым, так как требовал минимальной переделки клиента. Потом был оставлен для совместимости.

Надо сказать, до этого у меня не было опыта работы с каким-либо MVC-фреймворком, но, несмотря на это, разобрался очень быстро, и буквально за два вечера сделал первый работающий вариант. В общем, в адрес разработчиков express говорю самые теплые слова за возможность быстрого старта.

Получившийся сервер, во-первых, неплохо работал, а во-вторых, его код радовал глаз в отличие от старого варианта.

На этой волне воодушевления я решил «гулять так гулять!» и добавил в проект ODM mongoose. Код стал еще красивее, пока гонял тесты на локальной машине, буквально нарадоваться не мог, что все красиво лежит на своем месте и как все просто и логично работает.

Пришла пора деплоить эту красоту на сервер. Задеплоил. Проверил — работает! Отошел минут на 20. Прихожу, проверяю — не работает. Смотрю в консоли — процесс node запущен. Перезапускаю. Несколько секунд работает, потом — глухо.

Не буду долго тянуть: проблема была в CPU. Под обычной на тот момент вечерней нагрузкой вариант кода без mongoose шутя эту нагрузку переваривал, а вариант с mongoose (несмотря на всю свою красоту) — нет. Пришлось с грустью откатывать почти все изменения нескольких дней.

Тучи сгущаются

В конце 2012 года случился очередной всплеск активности пользователей XVM, и наш замечательный сервер перестал справляться с нагрузкой. Если честно, проблемы были и раньше, но не такие серьезные. Теперь же вечерами мод скорее не работал, чем работал. В качестве крайней меры мы даже прикрутили к серверу модуль toobusy, который позволял обрабатывать столько запросов, сколько тянуло железо, и пропускать остальное. Мониторинг показывал острую нехватку памяти для процесса mongo. Решили переезжать на другой сервер: EX-4S (i7-2600, 32 GB DDR3, 2x3 TB SATA III HDD).

Переехали. Полегчало. Правда, ненадолго.

Как только случились зимние каникулы, проблема опять возникла. Ценой всякого шаманства над кодом удалось довести скорость запросов со 150 до 180-200 в секунду, но этого все равно было мало. В какой-то из вечеров, когда все почти лежало, я ради проверки закомментировал блок кода, отвечавший за обновление “просроченных” игроков с WG API и… оно заработало. И неплохо: получили стабильные 240 с пиками до 260 запросов/сек. Через несколько дней родился код, в котором апдейтер игроков был выделен в отдельный независимый процесс, а непосредственно код, взаимодействующий с клиентами, только складывал ID игроков, требующих обновления, в отдельную коллекцию.

Этого апгрейда нам хватило где-то на месяц. И в очередной раз увидели все те же проблемы: вечерами начинались пропуски клиентских запросов. На сей раз уперлись в IOPS. Не очень долго думая, в марте 2013-го заказали для нашего сервера дополнительное оборудование: SSD-диск на 240Гб. Линейки серверов с предустановленными SSD-дисками тогда еще не было.

Помогло! Сервер стал тянуть до 380-400 запросов/сек. Примерно полгода все работало довольно ровно и не вызывало особых нареканий у пользователей. В августе 2013-го мы переехали на свежепоявившийся EX40-SSD, поскольку для наших целей он подходил лучше, а стоил дешевле, чем EX-4S с дополнительным SSD.

А в конце 2013….. как вы уже догадались, нас постигла та же самая проблема. Причем на сей раз очевидных и простых путей решения было не видно. Последние колдунства над кодом сделаны. Сервер весьма неплох. q4x2, который *nix специалист, в свою очередь, “подкрутил” сервер "по самое немогу".

# Kernel sysctl configuration file for Red Hat Linux # # For binary values, 0 is disabled, 1 is enabled. See sysctl(8) and # sysctl.conf(5) for more details.

# Controls IP packet forwarding net.ipv4.ip_forward = 0

# Controls source route verification net.ipv4.conf.default.rp_filter = 1

# Do not accept source routing net.ipv4.conf.default.accept_source_route = 0

# Controls the System Request debugging functionality of the kernel kernel.sysrq = 0

# Controls whether core dumps will append the PID to the core filename. # Useful for debugging multi-threaded applications. kernel.core_uses_pid = 1

# Controls the use of TCP syncookies net.ipv4.tcp_syncookies = 1

# Controls the default maxmimum size of a mesage queue kernel.msgmnb = 65536

# Controls the maximum size of a message, in bytes kernel.msgmax = 65536

# Controls the maximum shared segment size, in bytes kernel.shmmax = 68719476736

# Controls the maximum number of shared memory segments, in pages kernel.shmall = 4294967296 net.core.somaxconn = 262144 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_tw_recycle = 1 net.core.netdev_max_backlog = 10000 net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_max_syn_backlog = 262144 net.ipv4.tcp_max_tw_buckets = 720000 net.ipv4.tcp_timestamps = 1 net.ipv4.tcp_fin_timeout = 10 net.ipv4.tcp_keepalive_time = 1800 net.ipv4.tcp_keepalive_probes = 7 net.ipv4.tcp_keepalive_intvl = 30 net.core.wmem_max = 33554432 net.core.rmem_max = 33554432 net.core.rmem_default = 8388608 net.core.wmem_default = 4194394 net.ipv4.tcp_rmem = 4096 8388608 16777216 net.ipv4.tcp_wmem = 4096 4194394 16777216 net.ipv4.tcp_fin_timeout = 15 fs.epoll.max_user_watches = 1000000

fs.file-max = 5000000 net.core.netdev_max_backlog = 100000 net.core.optmem_max = 10000000 net.core.rmem_default = 10000000 net.core.rmem_max = 10000000 net.core.somaxconn = 1000000 net.core.wmem_default = 10000000 net.core.wmem_max = 10000000 net.ipv4.ip_local_port_range = 1024 65535 net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_max_syn_backlog = 12000 net.ipv4.tcp_max_tw_buckets = 2000000 net.ipv4.tcp_mem = 30000000 30000000 30000000 net.ipv4.tcp_rmem = 30000000 30000000 30000000 net.ipv4.tcp_wmem = 30000000 30000000 30000000 net.netfilter.nf_conntrack_max=1048576

В общем, все, что можно было сделать малой кровью, сделано. Уже начали подумывать о приобретении второго-третьего сервера и настраивать балансировку. Но как-то вечером в разговоре с q4x2 у нас вышел спор о том, что не все платформы одинаково полезны. А если проще, то он меня убеждал, что все наши беды из-за NodeJS, и что он готов сделать свой вариант сервера на Java, который “порвет” Node на британский флаг. Я в этом сильно сомневался, но согласился поучаствовать в эксперименте, и, раз уж такое дело, решил попробовать чего-нибудь нативного.

Поскольку, по своему обыкновению, захотелось чего-то нового, то выбор пал на D и фреймворк vibed. Удивительно, но даже мне, последние года три занимавшемуся почти исключительно JS, удалось относительно быстро состряпать работающий вариант. Разрабатывал я его под Windows. А вот при разворачивании под Centos 6.5 у нас возникли очень большие сложности с удовлетворением зависимостей. Линкеру приходилось вручную прописывать очевидные вещи.

Сложности до конца побороть не удалось, поэтому эксперимент с D был досрочно свернут. Сам язык нам очень понравился, но пока что у него, увы, проблемы с инструментарием.

Пока мы все-это писали, сервер вечерами не справлялся: не хватало CPU. И мы решились на еще один переезд, на сей раз на PX90-SSD (Xeon E5-1650 v2, 64 GB ECC DDR3, 2 x 240 GB SSD).

Рассвет новой эры

500 запросах/сек основным java-процессом используется одно ядро. Для Node-версии на четырехядернике такая нагрузка была недостижимой, а на шестиядернике она была очень близка к предельной.

Как раз в это время мы решили вводить активацию статистики на нашем сайте. Это означало, что полученные клиентами токены доступа будут ходить по сети при каждом запросе. Чтобы немного их обезопасить, сервер был размещен за HTTPS. И тут нас ждал очередной сюрприз.

Чуть выше я упомянул про детские болячки java-кода. А пока мы их пару недель лечили, java-код работал не намного лучше старого, и было очень похоже на то, что шестиголовый сервер не потянет проксю + https.

Чтобы не ждать чуда, был немедленно приобретен еще один сервер: EX40 (i7-4770, 32 GB DDR3, 2 x 2 TB HDD) с целью быть фронтендом и тянуть https. Немцы сервер сделали, но с одной маленькой, как потом выяснилось, проблемкой: ему присвоили IP в диапазоне 5.x.x.x. Фронтенд мы на нем сделали, и все замечательно работало, но на форуме начали копиться жалобы от людей, у которых ничего не работало.

Немного исследовав этот вопрос, мы выяснили, что проблема с 5.x.x.x — известная штука, связана она с приложением под названием Hamachi. Причем новые версии Hamachi переехали на 25.x.x.x (интересно, эти ребята принципиально не хотят использовать приватные сети для своих целей?), но нам от этого было не легче. Пришлось переезжать обратно на основной сервер, благо к тому моменту он уже мог тянуть все наше хозяйство единолично, да еще с солидным запасом.

Итого на данный момент у нас весь бэкэнд: наш код + БД работает на одном сервере. Load averages редко превышают единицу. Но сейчас мы задумали еще одну реорганизацию серверного хозяйства с переносом БД на отдельный сервер. Одна из причин: у Mongo очень тяжело с холодным стартом — после перезагрузки она не тянет и половины той нагрузки, которую тянет после прогрева (из-за этого, кстати, были проблемы 3-4 августа). Перенос на отдельный сервер позволит не трогать его. Другая причина — у нас появились некоторые идеи по развитию мода, которые потребуют значительного увеличения размера БД. Текущих 2*240Гб может не хватить. В целом, можно сказать, что проблема бэкэнда на нашей стороне решена. Осталась другая.

Борьба с Wargaming API

Второй по остроте проблемой после работоспособности самого сервера у нас всегда были обновления статистики игроков. По понятной причине, все наши пользователи так или иначе обращают внимание на игровую статистику, и большинство из них хотят видеть обновления этой статистики настолько часто, насколько это вообще возможно (в идеале — в реальном времени). А на пути к решению этой задачи у нас всегда стоял WG API. Точнее — его ограничения.

Самая главная для нас проблема — лимит на число запросов с одного IP. Во времена, когда публичного API еще не было, лимит с одного IP составлял около 10 запросов/сек. Нам этого было, мягко говоря, маловато. Поэтому выкручивались как могли: у нас было несколько прокси-серверов, через которые слались запросы к API.

С появлением API стало немного легче в плане доступного лимита. Нашему приложению присвоили премиум-статус, что в теории дает нам право делать по 50 запросов/сек. А дальше начинаются НО:

  1. В старой версии (не публичный API, данные для мобильного приложения) все нужные нам данные по каждому игроку были собраны в одном методе. А с появлением публичного API данные были перераспределены по разным методам, и нам пришлось делать на каждого игрока по два запроса (/account/info/, /account/tanks/).
  2. Эти методы поддерживают запрос сразу по нескольким игрокам (до 100). То есть, в теории мы бы могли запрашивать до 5000 игроков/сек. Но в реальности запрос к /account/tanks/ на 100 игроков занимал

На этом вторая часть повествования завершается. Впереди будет еще одна часть, которая расскажет о самом интересном - развитии клиентской части модификации.

📎📎📎📎📎📎📎📎📎📎