BitTorrent для раздачи игр, но не в пиратских целях
С постоянным ростом размеров дистрибутивов игр и увеличением числа игроков по всему миру, разработчики сталкиваются с необходимостью эффективной дистрибуции своих продуктов. Не менее остро этот вопрос встает в процессе разработки игры, особенно в условиях нынешних распределенных команд. Одним из подходов, которые обеспечивают высокую скорость и надежность передачи данных, является использование протокола BitTorrent. В данной статье я хочу рассмотреть как разработчики и операторы онлайн-игр могут воспользоваться (и частенько пользуются) данной технологией для оптимизации процесса распространения и поделиться опытом ее построения.
BitTorrent CDN представляет собой сеть распределенных серверов, способных эффективно обрабатывать высокие нагрузки при передаче данных. В отличие от традиционных централизованных CDN, BitTorrent использует принципы пиринговой передачи данных, что позволяет ускорить загрузку за счет использования ресурсов нескольких узлов одновременно.
Я не очень хочу углубляться в историю создания технологии и ее первого применения в Napster, все это рассказано многократно до меня. Тем не менее, я кратко опишу общий механизм работы и доступные возможности реализации.
Однако начну, пожалуй, с обзора основных принципов, которым должна отвечать CDN для игрового проекта и что bt-сеть может на это предложить.
Очевидно, что выпуск релиза — это, по сути, механизм перехода продукта от разработчиков к конечному пользователю. Изначально, продукт живет в системе контроля версий, где программисты пишут код, а девопсы строят пайплайны. В случае, если релиз должен быть собран в продакшн, необходимо гарантировать его одновременную доступность для всех игроков, причем вместе с серверной частью, ибо в нашем случае речь идет об онлайн-игре.
Не менее остро этот вопрос встает по ходу разработки игры, поскольку релизы собираются до нескольких раз в день, их плейтестят геймдизайнеры, художники, моделлеры. Многие команды имеют отдельные стенды в инфраструктуре для того, чтобы плейтестить тот билд, который нужен сейчас им, и не блокировать работу других отделов. Система доставки должны решать все эти проблемы и отвечать ряду требований
Высокая скорость передачи. BItTorrent способен обеспечивать высокую скорость загрузки благодаря параллельной передаче данных от нескольких источников.
Экономия ресурсов. Распределенная структура BItTorrent позволяет сэкономить ресурсы серверов, так как пользователи, скачивая контент, становятся также источниками для других пользователей. Чем больше игроков, тем больше доступность, выше скорость загрузки и меньше нагрузки на ваши сервера.
Устойчивость к сбоям. За счет децентрализованной природы Bt CDN, система устойчива к сбоям в работе отдельных узлов, обеспечивая непрерывность передачи данных.
Легкий процесс масштабирования. Нужно просто докинуть раздающие узлы в систему.
Геореплицируемость. Как следствие предыдущего, BT сети все равно в какой части света находятся ваши узлы.
Эффективная дистрибуция обновлений и патчей Регулярные обновления и патчи являются неотъемлемой частью современных игр. BItTorrent позволяет снизить нагрузку на центральные серверы при распространении обновлений и, бонусом к этому, система сама решит что является «патчем» и он будет кумулятивен (не надо качать все предыдущие, только актуальный). Ваша задача как оператора — загрузить обновленный дистрибутив и дать к нему доступ. Все участники получат ровно те порции данных, какие им необходимы.
Догрузка. Далеко не факт что ваши игроки смогут скачать весь дистрибутив или патч сразу, необходима возможность остановки и запуска с того же места. BT по своей природе является таковым.
Защита от DDoS-атак Децентрализованная природа BItTorrent делает систему более устойчивой к DDoS-атакам. Поскольку загрузка данных происходит с нескольких источников, а не только с центрального сервера, атаки становятся менее эффективными.
Интеграция существующих технологий Разработчики могут легко интегрировать BitTorrent в свои существующие системы дистрибуции благодаря обширной документации и открытым библиотекам.
Протокол
В ВitTorent данные разбиваются на множество маленьких частей, которые могут быть загружены от разных пиров, у которых уже есть эти части. Сиды (или сидеры) — это пользователи, которые уже скачали полный пакет и продолжают раздавать его другим. Пиры — это участники сети, которые скачивают и одновременно отдают части данных (ну или не отдают и тогда зовутся личами). Трекер — это сервер, который координирует передачу между пирами.
Основной элемент — файл с расширением .torrent, который содержит метаданные о скачиваемом контенте, включая список хешей для каждого блока данных. Эти хеши используются для проверки каждой загруженной части, гарантируя, что не произошло повреждений или изменений во время передачи. Данные разбиваются на блоки, обычно размером от 256 КБ до 2 МБ каждый.
Когда пользователь хочет скачать раздачу, он сначала загружает соответствующий .torrent файл, а затем использует BitTorrent клиент для подключения к трекеру, указанному в этом файле. Каждый раз, когда пир завершает загрузку блока, он может начать передачу этого блока другим пирам, увеличивая скорость всей сети.
Реализация CDN
Что ж, перейдем к какой-никакой общей теории реализации CDN на базе этого. Из предыдущего параграфа вполне вырисовывается схема основных компонент, которые должны присутствовать.
Источник данных.
Нечто, что расскажет участникам сети о релизе.
Трекер.
Хранилище. Релизы нужно куда-то класть, да так чтобы был оперативный доступ у всех раздающих узлов.
Стейбл-ноды. Во-первых, что-то должно быть первоначальным источником для потребителя, во-вторых слишком уж сильно полагаться на компьютеры своих игроков не стоит если вы строите сервис, а не пиратскую бухту.
Клиент, чтобы, собственно, качать (и раздавать впоследствии, если пользователь разрешит).
Общая концепция
Трекер
Трекер функционирует как сервер, предоставляющий информацию о доступных источниках данных и о том, какие части файла у них имеются. Когда пользователь подключается к трекеру, он предоставляет информацию о себе и запрашивает необходимые данные. Трекер, в свою очередь, предоставляет указания о том, где можно получить нужные фрагменты файла.
Устройство его не слишком сложное. Обычно это высокопроизводительный http-сервер, способный обрабатывать большое количество запросов от клиентов. Трекер сохраняет информацию о каждом активном торренте, включая список пиров, их доступность и загруженные фрагменты. Для обеспечения надежной координации трекеры периодически обновляют свои списки, обеспечивая актуальность данных.
Таким образом, регулярное общение с трекером позволяет пользователям оставаться видимыми в сети, а также гарантирует актуальность списка тех, кто готов поделиться данными.
Пожалуй, из специфичного, здесь можно добавить только то, что в такой задаче как построение CDN, трекер приходится делать гораздо более централизованным и роль его в сети несколько выше. Одна из основных атак, которые можно организовать на P2P-сеть — это забивание канала мусорным трафиком. Для предотвращения подобного мы выключаем DHT у клиентов, оставляя источником только трекер, а «отравленные» пиры блокируются на уровне сервера, обеспечивая таким образом более надежную и чистую среду для распространения.
На GitHub есть масса открытых трекеров, под любыми лицензиями. Стоит отметить только то, что трекер стоит выбирать близкий команде по стеку, потому что в случае построения приватной CDN могут потребоваться некоторые расширения, вроде аутентификации при запросе и должна быть возможность безболезненной модификации.
Хранилище
В последние годы Amazon S3 и совместимые с ним системы стали де-факто промышленным стандартом для хранения данных. S3 предоставляет простой и понятный интерфейс для отгрузки и скачивания данных. В то же время, благодаря s3fs, хранилище можно отлично монтировать как директорию, тем самым решая раз и навсегда вопрос с распределенным «жестким» диском для раскиданных по миру нод. Ну, и бонусом, S3 совместимое хранилище предоставляют буквально все облачные провайдеры, а для on-premises решений можно использовать открытый MIN.io. Вы вольны выбрать хранилище, которое соответствует вашим требованиям по стоимости, производительности и другим параметрам не теряя в возможности масштабирования и с кучей готовых инструментов и библиотек.
Cтейбл-ноды (seeds)
Стейбл-нодами мы называем подконтрольных нам, гарантированно доступных сидов.
В архитектуре каждой ноды представлено сочетание обычного торрент-клиента и дополнительного компонента, который мы называем сайдкаром. Эта система начинает свою работу с инициализации, во время которой происходит монтирование необходимого хранилища данных, используя s3fs для подключения к S3 бакетам.
Сайдкар представляет собой простой процесс, регулярно обновляющий конфигурацию, в которой перечислены необходимые UpdateService’ы, о которых чуть дальше. Он опрашивает эти сервисы на предмет наличия актуальных торрент-файлов и передает их торрент-клиенту.
Как только торрент-клиент получает новый релиз, он начинает процесс пересчета хешей для файлов, уже находящихся на диске (фактически в S3, с доступом через s3fs) и сообщает трекеру о себе и своей готовности раздавать 100% необходимых блоков данных.
В нашей системе в качестве торрент-клиента используется transmission, хотя подобным образом может быть задействован любой другой, способный работать в режиме без графического интерфейса.
Мы содержим swarm стейбл-нод, которые задействованы в раздаче абсолютно любых релизов, вне зависимости от стенда или региона, просто общий пулл. Что касается железа — мы арендуем копеечные VPS, максимально придвинутые к пользователю. Им необходимо буквально одно ядро, крохи оперативы и немного дискового кэша (s3fs хорошо умеет оптимизировать походы в хранилище).
Клиент
Ну и клиентское приложение, разумеется. В случае онлайн игры для ПК разработчик так или иначе будет вынужден предоставить конечному пользователю собственный лаунчер. В некоторых случаях удастся отделаться Steam и подобными, но в большинстве ситуаций и они будут использованы лишь как «лаунчер лаунчера».
Мы просто обязаны добавить пользователю свой!
Имея контролируемую точку входа у пользователя, то, какие протоколы будут использованы для загрузки, не слишком важно. Главное чтобы качалось быстро, дистрибутивы не ломало и ресурсы жгло на саму игру, а не средство ее установки и запуска.
Большую роль играет выбор подходящей технологии для реализации клиентской части торрент-протокола. Как заметил один из комментаторов в статье на Хабре, в мире разработки торрент клиентов есть собственный Chrome, и имя ему libtorrent. Эта библиотека постоянно развивается и предлагает разработчикам обширный набор функциональных возможностей и настроек. Все что можно подергать, попереключать и понастраивать в ней есть, и еще немного сверху.
Несмотря на некоторую несогласованность API, которая может вызвать трудности у новых пользователей, более глубокое погружение в документацию и принципы работы библиотеки позволяет понять логику, стоящую за принятыми разработчиками решениями.
Детали архитектуры и поставка релизов
Что ж, мы определились с необходимыми инструментами. А как это все будет работать?
Добавляем в уравнение билд-ферму в качестве изначального источника данных. Билд-ферма — это набор производительных серверов, которые даже с крупными проектами на движках размером с операционную систему справляются за несколько минут. Здесь отрабатывают стандартные механизмы CI/CD:
Пользователь с правами на выпуск релиза инициирует выкатку.
Ферма вытягивает необходимую версию из репозитория.
Компилирует, линкует и собирает в архив.
Далее начинаются этапы чуть более специфичные для нашей CDN:
После сборки снимаем хэш с архива для дальнейшей верификации его целостности.
Отгружаем в Update Service.
Как я говорил, у нас множество стендов (шардов). В случае продакшна это разные регионы. В случае девелопмента — это по стенду на отдел. Каждый из них, в нашей сервисной архитектуре, получил инстанс сущности под названием UpdateService. Задача этого сервиса — принимать релизы, устанавливать их на сервер и сообщать подключенным пользователям что пора бы обновиться. В целом, большая часть работы дальше лежит на нем.
UpdateService принимает архив в память, распаковывает, создает перечень файлов в дистрибутиве и собирает их хэши (об этом чуть далее).
После распаковки UpdateService создает торрент-файл и отгружает в S3-бакет своего стенда.
Отныне любой клиент, спросивший (включая стейбл-ноды, которые сделают запрос по таймеру) у UpdateService о том, что ему качать, получит самый свежий торрент-файл. Стейбл-ноды после апдейта добавят «загрузку» и обнаружат, что данные уже лежат в указанной директории. Мы же помним, что под слоем интерфейсов там на самом деле S3? Дальнейший процесс, я уверен, любой уверенный пользователь ПК видел не раз: просчет хэшей и начало сидирования. Всевозможные мелочи, вроде того, что архив релизов сохраняется, как и торрент-файлы, а значит мы в любой момент можем одним флагом откатить релиз на предыдущий, я опущу, ибо опции достаточно очевидные и специфичные для реализации.
А что насчет пользовательского клиента?
Здесь с точки зрения механики — примитивный интерфейс, фактически состоящий из одной кнопки «Play», кучи красивых картинок и функционала не связанного с системой доставки, так что о нем сейчас не будем.
Большой плюс, что мы одним устройством протокола решаем еще кучу проблем, таких как необходимость проверки неизменности дистрибутива и, как я уже упомянул, возможность кумулятивных апдейтов.
И вот здесь вернемся к вопросу о том, зачем Update Service считал хэши каждого файла, ведь BitTorrent и так весь на бесконечных хэшах ездит.
Дело вот в чем. Протокол можно использовать как патч-систему из коробки, да… Но не совсем. Он, по умолчанию, не подразумевает проверку файлов как отдельных сущностей. Более того, для него вообще не существует понятие файла как чего-то обособленного. Есть сумма данных, которая склеивается в единое целое при разбиении на блоки. Если блок содержит конец одного файла и начало другого, который вы не запрашивали при скачивании, этот кусок тоже будет загружен. Уверен, вы не раз замечали эти кусочки, если доводилось качать раздачу не целиком. Но в случае когда мы загрузку используем для патчинга имеющихся файлов, этот нюанс вылезает с необычной стороны.
Поясню проблему на примере.
У вас есть файл размером 2048 байт. Установленный размер блока — 8 байт. Итого, 256 блоков. Вы его скачали. Затем выходит апдейт релиза, где тот же файл имеет размер 2040 байт, а значит состоит из 255 блоков. Вы его скачиваете поверх старого и… обнаруживаете, что последние 8 байт никуда не делись, а остались лежать на диске, де факто относясь к этому же файлу. Цифры, разумеется, синтентические, никто блоков размеров в 8 байт не делает.
Решений этой проблеме может быть множество. Проверка размеров, но это ненадежно. Честный пересчет хэшей после скачки, что может сильно сократить время загрузки, но сама проверка займет слишком много времени. В конце концов, расширение протокола до проверки того, что после финального, для файла, блока ничего не осталось, однако это решение плохо вяжется с базовым механизмом работы протокола, а значит его будет довольно тяжко поддерживать, в силу обширности кастомизации. Тот же самый механизм I/O библиотеки libtorrent нужно будет переписать.
Здесь нужен баланс, и последняя опция кажется наиболее корректной, однако мы эту проблему, на данный момент, решили в лоб. Клиент получает те самые хэши, что насчитались в момент публикации релиза и при первой скачке, и при апдейте, а потом просто сносит все отличающиеся файлы.
Решение оказалось приемлемым, потому что в наших дистрибутивах почти не бывает ситуаций, чтобы в файле изменилась лишь пара байт. Это бинарники, и даже одна буква в ресурсах приведет к полному изменению итогового файла, таков уж движок. Так что, при выборе между банальным удалением и разработкой сложного плагина, ради экономии едва ли десятков мегабайт трафика, выбор был сделан не в пользу экономии.
Минусы
Я уже упоминал потенциальную атаку с забитием каналов мусором, чем иногда балуются издатели в целях борьбы с пиратством (оцените иронию, в нашем случае угроза прямо противоположна, злоумышленник может атаковать издателя). Решается добавлением аутентификации на трекере и, в целом, его более авторитарным отношением к пользователям, что не страшно, ибо у нас задача не построить самый пиртупирный на свете пиртупир , а донести свой контент до пользователей.
То что присуще торренту в целом — избыточность скачиваемых данных.
Облачные провайдеры не всегда готовы разрешить вам исходящий торрент-трафик или попросту могут быть к нему не готовы, и будут наблюдаться очень странные проблемы. Решается ресерчем подходящего провайдера для стейбл-нод.
Провайдеры у конечного пользователя иногда вставляют палки в колеса торренту. Обычно это закрытые сети или «сложные» регионы, но, в целом, и пользователи относятся к этому с пониманием, потому что перманентно испытывают подобные проблемы, и шифровка трафика частично помогает. Не могу сказать что проблема стоит очень остро.
Пожалуй, из основного, все.
Заключение
Использование BitTorrent CDN для дистрибуции контента действительно представляет собой эффективное решение, особенно в контексте больших файлов или медиа, таких как видеоигры. Этот подход значительно улучшает скорость, надежность и устойчивость процесса распространения.
Экономическая целесообразность такого решения заключается в снижении затрат на инфраструктуру и трафик, поскольку снижается нагрузка на основные сервера. Это делает подход особенно привлекательным для стартапов и компаний с ограниченными ресурсами, а также для тех, кто ищет масштабируемое решение для распределения больших объемов данных.
Технология также упрощает процесс разработки и поддержки лаунчера или клиента, так как наложенные функции протокола устраняют необходимость реализации многих сложных задач, таких как остановка и докачка, проверка целостности, дисковое кэширование, обработка разрывов связи и множество подобных мелочей, а реализовано это в прекрасно поддерживаемых библиотеках.
Интеграция BitTorrent может значительно упростить разработку и поддержку распределенных систем, позволяя командам сосредоточиться на создании основного продукта, а не решении технических проблем связанных с доставкой контента.
Комментарии и критика приветствуются, спасибо за внимание.