Многие игроки в EVE знают, что наша игра написана на языке программирования Python – если быть точнее, Stackless Python. Stackless ― это усовершенствованная версия интерпретатора, позволяющая пользоваться многими преимуществами многопоточного программирования. Но, тем не менее, «питон» остается «питоном», и нам приходится иметь дело с глобальной блокировкой интерпретатора (Global Interpreter Lock, также известной как «этот чертов GIL» или просто GIL).
GIL ограничивает доступ к интерпретатору (и всем соответствующим данным): в каждый конкретный момент времени обрабатывается лишь один поток. Поэтому хотя Stackless Python и обладает многими признаками «многопоточного» языка (такими как разделение задач, каналы, планировщики, совместная память, и т.д.), написанная на нем программа все равно способна обрабатывать лишь одну задачу за раз. Это похоже то, как работали некоторые многозадачные операционные системы в прошлом ― и такая схема позволяет гарантировать, что исполнение каждого потока не будет прервано в какой-то момент времени (за исключением тех случаев, когда поток нарушает принципы проблемы остановки). Это также позволяет нам исходить из многих предпосылок о глобальном состоянии игры при написании кода, что было бы невозможно, если бы мы использовали асинхронную логику и функции обратного вызова. По сути дела, большая часть логических блоков в коде нашей игры следуют принципам процедурной синхронной модели, что позволяет быстро и безболезненно вносить изменения в код.
Проблема заключается в том, что часть «фреймворка» в EVE Online написана на Python, и поэтому нам приходится использовать GIL. Это ограничение распространяется и на написанные на языке C модули, которым необходимо получать данные от интерпретатора Python.
Все задачи, работающие с Python, должны перед выполнением «получить» GIL; программа, по сути дела, выполняется в один поток.
Проще говоря: код, написанный на Stackless Python, исполняется так быстро, как позволяет самое быстрое ядро процессора. В случае четырех- или восьмиядерного процессора использоваться будет лишь одно ядро; впрочем, остальные могут использоваться для обработки других процессов. Это хорошо работает в тех случаях, когда эти процессы не сильно зависят от других компонентов системы, но не подходит для таких задач, как обработка действий игроков, находящихся в космосе или гуляющих по станциям.
Пока один процессор справляется со всей нагрузкой ― а во многих случаях это так и есть ― никаких проблем не возникает. Но не мне вам рассказывать, что в настоящий момент одного процессора уже не хватает для обработки больших боев, несмотря на то, что команда Gridlockи другие разработчики выжимают из системы все возможное, оптимизируя код. В настоящее время процессоры не становятся быстрее ― увеличивается объем кэша и пропускная способность шины, но с точки зрения того, что необходимо нашей игре, их скорость не растет. Задача, которую нам предстоит решить в недалеком (и, возможно, в далеком) будущем ― обеспечить возможность использования кодом игры несколько процессоров одновременно.
В общем и целом, дальнейшее развитие многоядерных процессоров теоретически открывает интересные возможности перед EVE – кластер игры устроен таким образом, что при использовании процессоров с более чем 30-60 ядрами мы получим значительные преимущества по сравнению с текущей архитектурой. Но в настоящий момент перед нами стоит более достижимая задача ― сделать определенные элементы «фреймворка» (сетевое взаимодействие и процессы ввода/вывода данных) независимыми от GIL.
Эта проблема возникла не вчера: необходимость повышения быстродействия важных компонентов нашей игры (для которых не требуется использовать Python) назрела уже давно. CarbonIO – гигантский шаг в этом направлении.
CarbonIO ― естественное развитие StacklessIO. По сути дела это написанный с нуля «движок», при разработке которого перед нами стояла главная задача: сделать сетевой код ― и любой код, написанный на C++ ― полностью независимым от механизма GIL. Вторая часть этой задачи ― большое начинание, на реализацию которого ушел почти год.
Краткий экскурс в историю: предыдущим большим шагом на этом пути являлся механизм StacklessIO. Он позволил обрабатывать сетевые операции на уровне операционной системы и передавать результаты интерпретатору Python по мере необходимости.
Система stacklessIOпозволяет завершать обработку запросов Python, не прибегая к GIL
CarbonIO ― это следующий этап развития этой системы. Многопоточный «движок», отвечающий за коммуникации, был полностью отделен от механизма GIL; он может отправлять и принимать данные без взаимодействия с интерпретатором Python.
Это стоит повторить еще раз: система CarbonIO может отправлять и получать данные без взаимодействия с интерпретатором Python. Параллельно. В обход механизма GIL.
Когда через систему CarbonIO устанавливается соединение, немедленно вызывается функция WSARecv(), и начинается сбор данных. Эти данные расшифровываются, распаковываются и в несколько потоков собираются в пакеты; это происходит параллельно с работой интерпретатора Python. Затем эти пакеты помещаются в очередь, где они ждут вызова от Python.
Когда для работы написанного на Python кода требуется один из таких пакетов, в систему CarbonIO отправляется вызов ― и эти данные, скорее всего, уже там есть. Данные вынимаются из очереди и возвращаются в нее без участия механизма GIL – это происходит практически мгновенно. Вот первое преимущество, которое обеспечивает система CarbonIO – возможность осуществлять параллельное опережающее считывание.
Второе преимущество касается отправки данных. Специальный поток Pythonсобирает и отправляет данные; при этом сжатие, шифрование, сбор их в пакеты и вызов функции WSASend() осуществляются в обход механизма GIL в другом потоке, и операционная система может запускать этот поток на другом процессоре. В принципе, это можно было делать и в рамках StacklessIO ― но это не имело бы никакого смысла без других изменений, которые мы реализовали.
Вернемся чуть-чуть назад. «Эти данные, скорее всего, там уже есть?» Хмм. Что, если создать функцию обратного вызова, которая позволяла бы модулю, написанному не на Python, получать эти данные без использования системы Machonet? Встречайте: система BlueNet.
Система CarbonIO постоянно выполняет цикл recv-loopи может обращаться к написанным на С++ модулям без участия Python.
Система Machonet выполняет ряд важных функций ― маршрутизация, управление сессиями, постановка пакетов данных в очередь и их отправка; по сути дела, это «нервная система» EVE. Она написана на Python, поэтому все данные со всех узлов сервера EVE обязаны в какой-то момент проходить через GIL. Как бы быстро не исполнялся написанный на C++ модуль, его быстродействие все равно будет ограничено этой системой. Поэтому мы не приступали к оптимизации многих написанных на C++ элементов кода ― ведь любой выигрыш сводился бы на нет ограничениями, связанными с механизмом GIL и системой Machonet.
Но это осталось в прошлом.
Теперь написанный на C++ модуль может принимать и отправлять через систему BlueNet пакеты, для обработки которых механизм GIL не требуется. Это нововведение было реализовано для проекта Incarna― ведь для передвижения персонажей требуется передача больших объемов данных. Мы подсчитали, что при использовании старой системы Incarna буквально «положила» бы сервер Tranquility даже при средней длительности игрового «тика». BlueNet позволяет решить эту проблему, направляя сетевой трафик между написанными на C++ модулями (такими как физический «движок») в обход механизма GIL; эти данные не приходится дважды пропускать через систему Machonet, что позволяет добиться значительного выигрыша.
Как именно это работает? BlueNet хранит копию всех необходимых структур Machonetи добавляет ко всем пакетам маленький заголовок (8-10 байт), где содержится информация о маршрутизации. Когда система BlueNet принимает пакет, она может прочесть этот заголовок и направить пакет дальше ― на другой узел или на модуль C++, исполняемый на том же процессоре. Пересылка пакета осуществляется в обход механизма GIL; Machonet/Python никак в этом процессе не участвуют. Это значит, что наши прокси-серверы могут полностью параллельно обрабатывать пакеты BlueNet, вообще не прибегая к связанным с Python процессам. Насколько это эффективно? Мы пока не знаем, какое слово выбрать для того, чтобы описать уменьшение лага и нагрузки на процессоры в ряде случаев ― «потрясающе» или «невероятно». Серьезно ― мы сами не верим своим глазам.
В дополнение к описанному выше, при работе над CarbonIO мы реализовали ряд других нововведений; каждое из них незначительно повышает быстродействие системы, но все вместе они дают хороший результат. Некоторые из которых стоит упомянуть отдельно:
Группировка запросов
В рамках этой статьи сложно рассказать об этом нововведении, но, грубо говоря, система CarbonIO очень эффективно «группирует» запросы перед их обработкой. Для выполнения некоторых операций всегда требуется некоторое дополнительное время; это преимущественно касается сетевых протоколов, но применимо и к другим аспектам программирования. Это время можно сократить, «сгруппировав» эти операции. Например, в EVE и раньше группировались пакеты данных для отправки в рамках единого TCP/IP MTU, но CarbonIO позволяет группировать и другие операции. Хороший пример этого связан с работой механизма GIL.
При возникновении первого потока, обращающегося к механизму GIL, создается очередь; другие потоки добавляют соответствующие вызовы (wake-upcall) в ее конец. Когда GIL освобождается, первый поток очищает всю очередь без необходимости повторно отменять и получать GIL для каждого wake-upcall. Эта ситуация часто возникает при высокой нагрузке на сервер, и использование CarbonIO позволяет получить здесь значительное преимущество.
Интеграция openSSL
Механизм SSL в системе CarbonIO реализован с помощью протокола openSSL, который работает без участия механизма GIL ― вся маршрутизация осуществляется через CarbonIO с использованием так называемых «completion ports». Это позволит нам не только лучше защитить EVE от несанкционированного доступа, но и в перспективе перенести для удобства некоторые функции управления учетной записью непосредственно в игровой клиент.
Интеграция сжатия
CarbonIO осуществляет сжатие и распаковывание пакетов данных с помощью библиотек zlib и snappy; опять же, этот процесс не зависит от механизма GIL.
Полевые испытания
Данные, собранные за 24 часа работы типичного прокси-сервера игры (примерно 1600 пользователей в пиковый период), показывают, что использование CarbonIO позволяет значительно сократить нагрузку на процессор (как абсолютную, так и в расчете на одного пользователя). По мере роста нагрузки на сервер эффективность CarbonIO снижается ― влияние оптимизаций уступает место количеству обрабатываемых операций ― но все равно остается довольно высокой.
Нагрузка на CPUна одного пользователя за 24 часа
Нагрузка на CPU(в %) за 24 часа
На работу серверов, обрабатывающих звездные системы, CarbonIO влияет меньше ― ведь нагрузка на них связана преимущественно с кодом игры, а не с сетевым взаимодействием ― но и здесь нам удалось получить выигрыш в 8-10%.
Стоит отметить, что в ходе этих испытаний мы не использовали новые функции BlueNet (такие как маршрутизация, сжатие и расшифровка). Когда мы начнем их использовать, повышение производительности станет еще более заметным.
Подводя итоги
Что это значит для сервера EVE? После внедрения системы CarbonIO мы сможем лучше использовать возможности современных процессоров ― это непосредственно касается и быстродействия игры. Чем меньше кода обрабатывается с использованием механизма GIL, тем эффективнее работает вся система. CarbonIO и BlueNet открывают перед нами двери в новый мир ― нам еще предстоит узнать, насколько мы сможем повысить быстродействие всей системы в целом, но можно смело сказать, что большой «затор» на этом пути устранен.
- CCP Curt
|