Сообщений 6    Оценка 255 [+0/-1]         Оценить  
Система Orphus

История одного игрового кластера

Автор: Топоров Константин Леонидович
Перевод:
Источник:
Материал предоставил:
Опубликовано: 21.05.2012
Исправлено: 10.12.2016
Версия текста: 1.1
Предыстория
Оптимизация
Java или С++?
HTTP или TCP?
Boost или ACE?
Protobuf или Thrift?
Тестирование
Выпуск
Отказ от ответственности
Список литературы

Предыстория

Однажды (не так давно) автору этой статьи довелось поработать на одном игровом проекте. Суть была такова: была одна компания, и у нее была сетевая игра. Количество клиентов было невелико (несколько тысяч), но понемногу и достаточно стабильно увеличивалось. Все шло хорошо, но проблема заключалась в том, что серверная система подходила к пределу своих технических возможностей. Такая ситуация возникла не впервые и обычно решалась приобретением нового железа. Но бывает, что простое расширение без кардинальной переделки архитектуры не помогает. В нашем случае этот момент был уже близко, и многие участники игровой команды это осознали.

Здесь следует сделать отступление и рассказать об устройстве самого сервера. Первая его версия появилась на свет в начале двухтысячных. Изначально это был простой сервлет, написанный на java и работающий по http-протоколу под управлением Apache Tomcat. Было время, когда вся игровая логика была зашита в этот сервлет, и весь сервер состоял из одного java-процесса (плюс конечно база данных). Достаточно быстро он разделился на backend (отвечающий за игровую логику) и frontend (отвечающий за обмен данными с клиентом). По мере роста популярности игры количество backend'ов и frontend'ов увеличивалось, сервер превратился в кластер, и такая архитектура просуществовала до описываемого момента.

Схема эта вполне жизнеспособная и очень многие проекты на ней работают годами. Но параллельно шло развитие и самой игры, и основным последствием явилось постоянное увеличение трафика, генерируемого на сервере. Frontend’ы были вынуждены перекачивать большие объемы данных от backend’ов, время отклика не клиенте возрастало, пользователи жаловались на замедленную реакцию. Чтобы устранить эту проблему, разработчики уменьшали временной интервал между запросами, что приходило к увеличению нагрузки на frontend'ы, которые и так были заняты раздачей больших объемов данных.

Немного примерных цифр для более ясного представления всей картины. Среднее количество пользователей системы – порядка 5 тысяч, пиковое – несколько больше десяти. Пики случаются несколько раз в неделю по вечерам – это большая нервотрепка для команды поддержки. Один frontend способен обработать 300-500 пользовательских подключений, в зависимости от того, насколько интенсивно идут события с серверов игровой логики (aka backend). В любой момент времени запущены 20-30 frontend'ов и в моменты особо мощных всплесков интереса аудитории они не справляются. Иногда в такие моменты приходилось – какой ужас! – отключать SSL, чтобы только удержаться на плаву. Естественно, под большой нагрузкой проявлялись все скрытые проблемы, и несколько frontend'ов за вечер отваливалось. Каждые несколько месяцев в датацентре появлялся новый сервер, но ситуацию это улучшало ненадолго.

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

Решающим доводом оказалось то, что подходила к концу разработка браузерной версии игры. Работа над ней шла долго, с постоянным задержками, но в итоге окончание эпопеи все же наметилось. Кроме того, компания подготовила к выпуску несколько интересных нововведений в игре. Руководство хотело совместить эти два выпуска, сопроводив их мощной рекламной кампанией. Маркетологи предсказывали увеличение клиентской базы в 5(!) раз, и от этих предсказаний у главного админа поседели волосы на голове. Всем было ясно, что существующая система не выдержит такого притока пользователей. Вариант разом утроить количество серверов и состав команды поддержки серьезно не рассматривался. Пришло время что-то менять. Руководство выделило серьезный бюджет. Вот здесь и начинается история.

Оптимизация

Java или С++?

Сроки, как водится, поджимали. Пока все раскачивались, до даты планируемого выпуска осталось несколько месяцев. Наконец был нанят именитый архитектор и отдельная команда разработчиков – только на оптимизацию. В составе этой команды в проект попал и автор этой статьи. Архитектор оказался ярым противником языка java. По его мнению, там, где в требованиях стояло слово “производительность”, языку java места не было. Только C (ну или C++, если уж на C никак). Такая радикальная точка зрения может выглядеть достаточно странно, но он получил карт-бланш у руководства, уверив их, что сервер на C/C++ будет держать 10 тысяч соединений (вспомним про 300-500 на Tomcat). Вместе с тем, избавиться от java полностью было невозможно – сервера игровой логики были писаны именно на java, отшлифованы годами, и переписать это на другом языке было просто немыслимо.

Но frontend пришлось переписать с нуля. Справедливости ради стоит сказать, что локальные попытки ускорить frontend предпринимались и раньше. Была попытка использовать Comet, когда Tomcat стал его поддерживать. Но в ранних версиях эта функциональность сильно сбоила, серверные каналы закрывались без повода, данные терялись – в общем, проблем хватало. Также была попытка перевести frontend на Apache, написав для него отдельный модуль (для реализации некоторой логики, которая была на frontend). Но это решение не показывало существенного преимущества, зато сильно проигрывало в стабильности.

HTTP или TCP?

Главным решением стал отказ от http. Действительно, этот замечательный и универсальный протокол не справлялся с передачей большого количества серверного трафика. В первом приближении еще планировалось оставить http для запросов от клиента, а данные с сервера передавать по tcp-каналу (как это делает Comet). Но с другой стороны, если уж открывать tcp-канал, то почему бы не принимать в нем еще и запросы от клиента? Тогда не придется держать http-сервер, разрываться между двумя реализациями. Эта точка зрения возобладала, и http под фанфары вышел на пенсию.

Здесь следует сказать пару слов о том, что мы приобрели и что потеряли с переходом на tcp. Из плюсов:

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

Быстрота реакции. Клиент послал запрос пользователя – тут же игровой сервер инициирует ответное событие. Это может занять несколько десятков миллисекунд против как минимум двухсот в случае с http.

Быстрота доставки игровых событий. Игровой сервер только сгенерировал сообщение – и оно уже несется к адресату. Раньше приходилось складывать в очередь, ждать ближайшего http-запроса.

Передача бинарных данных. Так повелось, что данные с игровых серверов шли в байтах. Чтобы послать их через http, требовалось закодировать их в base64 (а иногда перед этим сжать zip'ом), что никак не способствовало росту производительности. С переходом на tcp все эти проблемы ушли.

Упростилось управление сессиями пользователей. Нет активности по http – кто знает, что там на другой стороне. Может, просто пинг задержался на frontend'е, а может, компьютер игрока уже выключен. А порвалась tcp-сессия, и сразу ясно – там проблемы.

Без минусов не обошлось:

Отсутствие готового решения. В сравнении с богатством выбора http-серверов на всевозможных языках и платформах с tcp дело обстояло весьма печально. И если опять же на java существовали перспективные варианты (Netty, позже превратившийся в Apache Mina), то поделки на C/C++ стабильностью и известностью не отличались. Исключение – ACE, но тащить к себе такого монстра не хотелось.

TCP-протокол сам по себе. При всех своих возможностях, этот протокол является обоюдоострым оружием и требует внимания на порядок больше, чем http. Сюда входят и неожиданно рвущиеся сессии, и переполняющиеся сетевые буфера. Но все решаемо при должном подходе.

Boost или ACE?

Итак, этап принятия решения завершен. У нас будет frontend на C++. Архитектор планировал вести разработку на чистом C, обрабатывая сокеты через epoll (на сервере был Linux). Но дело не спорилось. Мало того, что C в чистом виде – не самый удобный и функциональный язык, так еще и были недочеты в работе с самими сокетами. В итоге прототип отличался явной нестабильностью. Это был бы не конец света в любом другом случае, но времени было мало, и тратить его на отладку сырого решения было бы очень расточительно. Так встал вопрос об альтернативах. Лоб в лоб сошлись ASIO и ACE. ACE уже рассматривался ранее и был отвергнут, потому что тянул за собой слишком уж много. ASIO лишь некоторое время назад вошел в состав boost и не имел еще солидной репутации. Было решено сделать прототип и посмотреть. Надо сказать, что ASIO не разочаровал.

На создание прототипа, аналогичному тому, что у нас был на чистом C, ушла неделя (а на первый потратили месяц). Потеря производительности составила лишь несколько процентов. Зато ASIO-решение изначально не содержало детских болезней, присущих первому прототипу. Как следствие, гораздо лучше обстояло дело с общей стабильностью. Еще один день ушел на подключение SSL. Это было контрольным выстрелом в голову конкурентам. Повесить SSL поверх сокетов на чистом C и отладить решение до приемлемого состояния – в обычных условиях на это ушло бы не меньше недели. Больше сомневаться не приходилось. ASIO всех победил.

Несколько слов о технологии. ASIO (Asynchronous Input/Output) изначально разрабатывался энтузиастом по имени Christopher Kohlhoff, который в течение нескольких лет превратил его во вполне зрелый продукт. ASIO был включен в состав boost и продолжает развиваться в его составе. ASIO предоставляет отличную возможность построить надежный и быстрый tcp-сервер при минимальный затратах времени. Он избавляет от необходимости писать велосипеды. Таким вещи, как переключение TCP/SSL, делаются буквально в несколько строчек кода. Имеется в наличии реализации пула потоков, которая включается опять же очень быстро. В тестах нам удалось нагрузить на него пресловутые 10 тысяч соединений (хотя понятно, что в реальной жизни добиться такой производительности удается крайне редко). Позже мне доводилось применять ASIO на других проектах, и он никогда не подводил. Всем рекомендую.

Protobuf или Thrift?

Дальше на очереди был протокол обмена. От старого протокола решено было отказаться вместе с http. Слишком уж тесная связь была у них. Запросы с клиента передавались через заголовок в тексте, а обратно летел сжатый бинарный ответ. За годы работы к протоколу накопилось достаточно претензий. Основной из них была не самая удачная самодельная сериализация. Сериализация – это вообще больная тема, и не так-то просто подобрать эффективное и удобное решение. Но ситуация так сложилась, что незадолго до того в мир вышли две новые системы сериализации данных: Google Protobuf и Facebook Thrift (aka Apache Thrift). Обе системы были разработаны известными гигантами индустрии для своих реальных нужд и практически одновременно были открыты для публики. Как следствие, интерес в мире программистов был к ним очень велик. Мы не остались в стороне от прогресса и решили использовать самое передовое решение. Остался вопрос, кто из них нам подойдет?

Поначалу нам больше приглянулся Thrift. Более гибкий язык описания модели, несколько методов сериализации на все случаи жизни – от сверхкомпактного до human-readable, уже готовая серверная часть – казалось, большая часть работы уже сделана за нас, может быть, даже ASIO не понадобится. Но, увы, при ближайшем рассмотрении все оказалось не так радужно. Отсутствие единого базового класса в генерируемом для C++ коде подкашивало красивую схему диспетчеризации абстрактных сообщений. На серверные компоненты в составе Thrift взглянуть без слез оказалось невозможно. Три реализации сервера из четырех оказались написаны на блокирующих сокетах и закрывали соединение сразу после оправки ответа сервером. Это ставило крест на многих возможностях TCP, которые нам были очень нужны. Четвертая реализация на неблокирующих сокетах обещала больше, но чтобы добиться поведения, которого нам хотелось, пришлось бы лезть внутрь фреймворка и серьезно его править. Это был бы тупиковый путь. Нет, я вовсе не хочу сказать, что Thrift написан плохо – просто он написан для других целей. Он должен работать как http в режиме запрос-ответ, только в бинарном варианте и без всяких наворотов вроде методов, заголовков, тела. Это был не наш случай, и больше мы к Thrift не возвращались.

Таким образом, кровеносной системой нашего нового сервера стал Protobuf. Конечно, он выглядел менее функциональным, чем Thrift: меньше языков поддерживается, только один вариант сериализации, несколько раздутые классы сообщений. Но в нем не было ничего лишнего, и он делал ровно то, для чего предназначен. ASIO и Protobuf составили идеальную комбинацию, которую осталось лишь грамотно соединить. Никаких существенных проблем здесь не возникло. Нет, конечно, не все было гладко. C++ не прощает ошибок и наш сервер какое-то время страдал от загадочных падений, случайным образом поедал память. Не обошлось и без багов, которые воспроизводятся только на реальной системе и только в самые неподходящие моменты. Не меньше месяца провели мы плечом к плечу с командой поддержки, тщательно вычищая свое творение.

Тестирование

В конце концов, наш frontend был готов. Он практически не потреблял память и способен был работать беспрерывно неделю до планового рестарта. В процессе нагрузочного тестирования на него обрушился шквал запросов. Руководство сказало: мы должны выдержать 30 тысяч клиентов. И настал момент, когда тридцать тысяч тестовых ботов начали наступление на наши серверы. Пять рабочих дней они заваливали нас запросами. В первый день повалились backend'ы. Во второй тоже. Backend'ы подчистили, но потом стала валиться база данных. В итоге система не выдержала этого теста. Всегда находилось слабое звено. Но frontend'ы не посыпались ни разу.

За кадром остался еще один существенный аспект: обмен данными между frontend'ом и backend'ом. В старой системе, как уже говорилось, использовалсь простая передача http-запроса. В новой системе, мы не стали изобретать велосипед и проложили между ними опять-таки TCP-каналы. Эта схема не выглядит идеальной. Все-таки несколько десятков процессов должны иметь TCP-соединение каждый с каждым – это выглядит не слишком красиво. С другой стороны, конфигурация системы достаточно стабильна, и однажды установленное соединение между backend'ом и frontend’ом может жить очень долго. В отличие от пользовательских подключений, серверные соединения не рвутся внезапно, не переполняются, их не становится слишком много. В общем, такое спорное решение оказалось достаточно беспроблемным в работе и поддержке. На стороне frontend'а мы использовали ASIO, на стороне backend'а изначально были вообще обычные java-сокеты, но несколько позже они были заменены на Apache Mina (в которую к тому времени превратился Netty).

Впоследствии были разговоры, что мы зря ввязались в авантюру с сервером на C++. Нужно было взять сразу Mina и оставить все на java. Это сэкономило бы долгие часы отладки frontend'а, увеличило бы общую стабильность системы… Про стабильность – может быть. Mina превратился в очень удобный фреймворк, но все упирается в производительность. Предварительные тесты показали, что при умеренной нагрузке ASIO работает в 2-3 раза быстрее. Как поведет себя Mina под большой нагрузкой – большой вопрос. Кроме того, на frontend приходится большая работа по SSL-шифрованию, и выполнять ее на java не очень рационально.

Факт остается фактом – новый frontend способен обработать в 10 раз больше пользователей, чем старый. Хотя до десяти тысяч, конечно, не дотянет.

Выпуск

И вот час X настал. Рекламная кампания набрала обороты, браузерная версия вышла в свет. Оценки маркетологов оказались несколько завышенными: в первую неделю количество пользователей удвоилось, но дальше напор ослаб и нагрузка стабилизировалась. Количество работающих frontend'ов уменьшилось в разы (по сравнению со старой системой). Освободилось много серверов. На них размещаются новые backend'ы. Ведь пользователи постепенно добавляются, система растет. Наверное, придет день, когда будет 20-30 новых frontend'ов и руководство вновь решит, что так дальше жить нельзя и начнет новую реформу.

Отказ от ответственности

Все совпадения с реальными событиями считать случайными.

Список литературы

  1. Apache Tomcat, веб-сервер, java http://tomcat.apache.org/
  2. Boost, фрейморк, C++, http://www.boost.org/
  3. ASIO, сетевой фреймворк, C++, http://think-async.com/
  4. Profobuf, http://code.google.com/p/protobuf/
  5. Apache Thrift, http://thrift.apache.org/
  6. Apache Mina, tcp сервер, Java, http://mina.apache.org/


    Сообщений 6    Оценка 255 [+0/-1]         Оценить