Pull to refresh
0
Plarium
Разработчик мобильных и браузерных игр

Высоконагруженные системы: решение основных проблем

Reading time7 min
Views52K
Привет, Хабр!

Сегодня я хочу рассказать о некоторых решениях проблем, которые возникают во время использования высоконагруженных систем. Все, о чем пойдет речь в этом материале, проверено на собственном опыте: я – Social Games Server Team Lead в компании Plarium, которая занимается разработкой социальных, мобильных, браузерных игр.

Для начала немного статистики. Plarium занимается разработкой игр с 2009 года. На данный момент наши проекты запущены во всех наиболее популярных социальных сетях («Вконтакте», «Мой мир», «Одноклассники», Facebook), несколько игр интегрированы в крупные игровые порталы: games.mail.ru, Kabam. Отдельно существует браузерная и мобильная (iOS) версии стратегии «Правила войны». В базах числятся более 80 миллионов пользователей (5 игр, локализация на 7 языках, 3 миллиона уникальных игроков в день), в итоге все наши серверы получают в среднем около 6500 запросов в секунду и 561 миллион запросов в сутки.

В качестве аппаратной платформы на боевых серверах в основном используются два серверных CPU с 4 ядрами (x2 HT), 32-64 GB RAM, 1-2 TB HDD. Серверы работают на базе Windows Server 2008 R2. Контент раздается через CDN с пропускной способностью до 5 Gbps.
Разработка ведется под .NET Framework 4.5 на языке программирования C#.

Учитывая специфику нашей работы, необходимо не только обеспечить стабильное функционирование систем, но и гарантировать, что они смогут выдержать большой скачок нагрузки. Увы, многие популярные подходы и технологии не выдерживают проверку высокой нагрузкой и быстро становятся узким местом в системе. Поэтому, проанализировав множество проблем, мы нашли оптимальные для нас (как мне кажется) пути их решения. Я расскажу, какие технологии мы выбрали, от каких отказались, и объясню, почему.


NoSQL vs. Relational

В этом сражении чистый NoSQL показал себя слабым бойцом: существовавшие на тот момент решения не поддерживали вменяемую консистентность данных и не обладали достаточной устойчивостью к падениям, что давало о себе знать в процессе работы. Хотя в итоге выбор пал на реляционные СУБД, которые позволяли использовать транзакционность в необходимых местах, в целом в качестве основного подхода используется NoSQL. В частности, таблицы зачастую имеют очень простую структуру типа ключ-значение, где данные представлены в виде JSON, который хранится в упакованном виде в колонке BLOB. В результате схема остается простой и стабильной, при этом структура поля данных может легко расширяться и изменяться. Как ни странно, это дает очень хороший результат – в нашем решении мы объединили преимущества обоих миров.

ORM vs. ADO.NET

Учитывая тот факт, что чистый ADO.NET имеет минимальный оверхед, а все запросы созданы вручную, знакомы и греют душу, он отправляет любые ORM в глубокий нокаут. А всё потому, что объектно-реляционное отображение имеет в нашем случае ряд минусов, таких как низкая производительность и низкий контроль запросов (или его отсутствие). При использовании многих решений ORM приходится долго и часто бороться с библиотекой и терять главное – скорость. А уж если речь заходит о хитром флаге для правильной обработки тайм-аутов клиентской библиотеки или о чем-то аналогичном, то попытки представить установку такого флага с использованием ORM окончательно расстраивают.

Distributed transactions vs. Own Eventual Consistency

Основная задача транзакций – обеспечить согласованность данных после завершения операции. Изменения либо успешно сохраняются, либо полностью откатываются, если что-то пошло не так. И если в случае одной базы мы были рады использовать этот, без сомнения, важный механизм, то распределенные транзакции при высоких нагрузках показали себя не с лучшей стороны. Результат использования – увеличенное время ожидания, усложнение логики (здесь вспоминаем еще про необходимость обновления кэшей в памяти экземпляров приложений, возможность возникновения мертвых блокировок, слабую устойчивость к физическим сбоям).
В итоге мы разработали свой вариант механизма для обеспечения Eventual Consistency, построенный на очередях сообщений. В результате мы получили: масштабируемость, устойчивость к сбоям, подходящее время наступления согласованности, отсутствие мертвых блокировок.

SOAP, WCF, etc. vs. JSON over HTTP

При использовании готовых решений в стиле SOAP (стандартные веб-сервисы .NET, WCF, Web API и т.д.) обнаружилась недостаточная гибкость, возникали сложности с настройкой и поддержкой разными клиентскими технологиями, появлялся лишний инфраструктурный посредник. Для работы с данными мы выбрали передачу JSON по HTTP, не только из-за максимальной простоты, но и потому, что с использованием такого протокола было очень легко диагностировать и устранять проблемы. Также эта простая комбинация наиболее широко охватывает клиентские технологии.

MVC.NET, Spring.NET vs. Naked ASP.NET

Опираясь на опыт работы, могу сказать, что MVC.NET, Spring.NET и им подобные фреймворки создают лишние промежуточные конструкции, которые мешают выжимать максимальную производительность. Наше решение построено на самых базовых возможностях, предоставляемых ASP.NET. Фактически точкой входа являются несколько обычных хендлеров. Мы не используем ни одного стандартного модуля, в приложении нет ни одной активной сессии ASP.NET. Всё понятно и просто.

Немного о велосипедах

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

Чуть больше трети используемого нами времени CPU тратится на сериализацию/десериализацию больших объемов данных в формате JSON, поэтому вопрос эффективности данной задачи является очень важным в контексте производительности системы в целом.
Изначально в работе мы использовали Newtonsoft JSON.NET, но в определенный момент пришли к выводу, что его скорости недостаточно, и мы можем реализовать нужное нам подможество фич более быстрым способом, без необходимости поддержки слишком большого количества вариантов десериализации и «замечательных» фич вроде валидации схем JSON, десериализации в JObject и т.д.

Поэтому мы самостоятельно написали сериализацию с учетом специфики своих данных. При этом на тестах получившееся решение оказалось в 10 раз быстрее JSON.Net и в 3 раза быстрее fastJSON.

Критически важной для нас была совместимость с уже существующими данными, сериализованными с помощью Newtonsoft. Чтобы убедиться в совместимости, перед включением нашей сериализации в продакшн мы провели тестирование на нескольких крупных базах: вычитывали данные в формате JSON, десериализовали с помощью нашей библиотеки, снова выполняли сериализацию и проверяли исходный и конечный JSON на равенство.

Память

Из-за нашего подхода к организации данных мы получили отрицательный эффект в виде слишком большого размера кучи больших объектов (large object heap). Для сравнения, ее размер в среднем составлял порядка 8 гигабайт против 400—500 мегабайт в объектах второго поколения. В итоге эту проблему решили путем разбивки больших блоков данных на блоки меньшего размера с использованием пула ранее выделенных блоков. Благодаря такой схеме куча больших объектов значительно уменьшилась, сборки мусора стали происходить реже и легче. Пользователи довольны, а это главное.

Работая с памятью, мы используем несколько кэшей разных размеров с различными политиками устаревания и обновления, при этом конструкция некоторых кэшей предельно проста, без всяческих излишеств. В результате показатель эффективности всех кэшей – не ниже 90—95%.

Опробовав Memcached, мы решили отложить его на будущее, т.к. пока что в нем нет необходимости. В целом результаты были неплохие, но общий выигрыш по производительности не превысил стоимость дополнительной сериализации/десериализации при помещении данных в кэш.

Дополнительный инструментарий

• Профилировщик
Так как привычные профилировщики значительно замедляют скорость работы приложения при подключении к нему, фактически делая невозможным профилировку достаточно нагруженных приложений, мы используем свою систему счетчиков производительности:



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



Среди положительных сторон:

— счетчики всегда включены;
— минимальные издержки (менее 0,5% ресурса используемого CPU);
— простой и гибкий подход к указанию профилируемых участков;
— автоматическая генерация счетчиков для точек входа (сетевых запросов, методов);
— возможность просмотра и агрегации по принципу parent—child;
— можно оценивать не только real-time данные, но и сохранять значения измерений счетчиков по времени с возможностью дальнейшего просмотра и анализа.

• Логирование
Зачастую это единственный способ диагностики ошибок. В работе мы используем два формата: human readable и JSON, при этом пишем всё, что можно писать, пока хватает места на диске. Собираем логи с серверов и используем для анализа. Всё сделано на базе log4net, поэтому не используется ничего лишнего, решения максимально просты.

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

• Развертывание
С увеличением количества серверов выливать что-либо вручную стало невозможно. Поэтому всего за неделю работы одного программиста мы разработали простейшую систему для автоматизированного обновления серверов. Скрипты были написаны на C#, что позволяло достаточно гибко модифицировать и поддерживать логику развертывания. В результате мы получили очень надежный и простой инструмент, который в критических ситуациях позволяет обновить все продакшн-серверы (порядка 50) за несколько минут.

Выводы

Для того чтобы добиться скорости при высокой нагрузке на серверы, необходимо использовать более простой и тонкий стек технологий, все инструменты должны быть знакомы и предсказуемы. Конструкции должны быть одновременно простыми, достаточными для решения текущих проблем и иметь запас прочности. Оптимально использовать горизонтальное масштабирование, производить кэш-контроль производительности. Логирование и мониторинг состояния системы – must have для жизнеобеспечения любого серьезного проекта. А система развертывания значительно упростит жизнь, поможет сэкономить нервы и время.
Tags:
Hubs:
+18
Comments25

Articles

Change theme settings

Information

Website
company.plarium.com
Registered
Founded
2009
Employees
1,001–5,000 employees
Location
Израиль