Запросы диапазонов байтов к исходному серверу: раздача больших файлов без настройки запросов диапазонов
- Cloudflare открыла всем тарифам настраиваемую через правила кэширования функцию запросов диапазонов к источнику.
- Новая реализация сократила p99-время до первого байта для R2 примерно вдвое.
- После обновления трафик многоуровневого кэша для R2 и схожих нагрузок снизился примерно на 6% за неделю.
Почему это важно: Функция помогает сократить лишнюю передачу данных и нагрузку на кэш без ручного подбора размера диапазона.
Чтобы перемотать фильм, видеоплееру достаточно запросить несколько мегабайт. При прежнем кэшировании диапазонов один такой запрос мог заставить Cloudflare загрузить с источника 128 МиБ и передать их между центрами обработки данных — иногда уже после того, как зритель ушёл. Запросы диапазонов используются для доставки больших файлов, но при наивном кэшировании в масштабах CDN легко впустую расходуются пропускная способность сети и ресурсы дисковых операций.
Поэтому мы перестроили работу кэша с такими запросами. Для R2 — объектного хранилища Cloudflare — время до получения первого байта на уровне p99 сократилось примерно вдвое, а последующее улучшение уменьшило трафик многоуровневого кэша для R2 и схожих нагрузок примерно на 6%.
Сегодня мы предоставляем новую реализацию всем пользователям в виде функции запросов диапазонов к источнику, доступной для самостоятельной настройки на всех тарифах. Не нужно подбирать размер диапазона или обращаться в службу поддержки. Включите функцию с помощью правила кэширования — остальное сделает Cloudflare.
Запрос части файла
HTTP-запрос диапазона запрашивает часть ресурса, а не ресурс целиком. Клиент отправляет обычный GET с заголовком Range:
GET /movie.mp4 HTTP/1.1
Host: example.com
Range: bytes=1048576-2097151Если сервер поддерживает такой запрос, он возвращает 206 Partial Content и указывает, какие байты переданы, с помощью Content-Range:
HTTP/1.1 206 Partial Content
Content-Range: bytes 1048576-2097151/524288000
Content-Length: 1048576Запросы диапазонов часто используются для больших объектов. Видеоплеер может запросить байты вокруг точки перемотки, а не весь файл. Менеджер загрузок может продолжить загрузку после сбоя. Менеджер пакетов может получить часть артефакта. Клиент может проверить конец файла, не скачивая всё, что находится перед ним.
Клиент сам решает, запрашивать ли диапазон, и Cloudflare не меняет этот запрос. Вместо этого функция «Запросы диапазонов к источнику» меняет способ, которым Cloudflare его обрабатывает.
Проблема размера диапазона
Cloudflare уже много лет поддерживает кэширование запросов диапазонов с помощью статической конфигурации зоны.
Прежняя реализация была похожа на описанное в публикации в блоге nginx разбиение кэша на фрагменты: каждый запрос клиента сопоставлялся с диапазонами файла фиксированного размера, а размер диапазона и расширения файлов настраивались отдельно для каждой зоны.
Размер каждого диапазона требовал компромисса: небольшие диапазоны приводили к множеству мелких запросов к источнику и повышали нагрузку на память Cloudflare, поскольку все запросы объединялись в памяти; большие диапазоны заставляли Cloudflare скачивать намного больше данных, чем запрашивал клиент. Изменение размера диапазона аннулировало записи кэша, поскольку размер входил в ключ кэша.
Из-за такого устройства у пользователей, запрашивавших контент через нашу сеть, могли возникать проблемы с доступом к этим файлам. Параллельные запросы, попадавшие в один диапазон, могли блокировать друг друга — независимо от того, насколько различались запрошенные клиентами диапазоны. Кроме того, наш кэш на базе nginx не мог сочетать кэширование диапазонов с функцией параллельной потоковой передачи, которую мы создали, чтобы несколько клиентов могли читать файл из кэша, пока он ещё записывается. Одновременное включение обеих функций приводило к обрыву ответов: прежде чем кто-либо мог прочитать диапазон, его приходилось скачивать целиком, а для популярных файлов требовался отдельный запрос к источнику для каждого диапазона.
Лишние байты могли пройти через несколько частей нашего распределённого кэша: от источника до одного центра обработки данных, между уровнями кэша, а затем к клиенту. На каждом этапе потери умножались. Не существует единого значения, которое одинаково хорошо подходит для небольшой перемотки, последовательной загрузки, большого медиафайла и любых сочетаний источников и топологий кэша.
Внутренние ограничения нашего кэша напрямую затрагивали клиентов, хотя им не должно требоваться глубокое понимание наших возможностей, чтобы файлы доставлялись эффективно.
К счастью, переход с nginx на кэширующий сервер на базе Pingora дал нам возможность устранить проблемы реализации, из-за которых клиентам приходилось пользоваться сложной настройкой.
Фрагменты кэша фиксированного размера
Решение нашлось в механизме, который Pingora уже использовала. Чтобы можно было легко возобновлять прерванные загрузки, каждый кэшированный файл хранится на диске фрагментами фиксированного размера. Если кэш уже оперирует фрагментами, размер диапазона вовсе не нужно задавать отдельно — достаточно использовать размер фрагмента.
При запросе диапазона Cloudflare проверяет, какие из запрошенных байтов уже есть в кэше, и отдаёт их напрямую. Недостающие байты Cloudflare запрашивает у источника диапазонами, выровненными по границам фрагментов фиксированного размера, сохраняет результат в тех же фрагментах, что и обычные данные кэша, а клиенту возвращает только запрошенные байты.
Например, запрос байтов 150-250 может привести к тому, что Cloudflare загрузит выровненный по границам фрагмент, содержащий эти байты. В ответе клиенту по-прежнему будут только 150-250.
Это не устраняет лишнюю работу для запросов, размер которых меньше размера нашего фрагмента: такой запрос всё равно может потребовать загрузки всего фрагмента, в который он попадает. Кроме того, мы берём на себя дополнительные затраты на обработку диапазонов, не выровненных по границам фрагментов. Разница в том, что эта дополнительная работа ограничена и предсказуема, а её объём больше не зависит от выбранного клиентом размера диапазона, который мог непредсказуемо влиять на нас и источники.
Согласование кэшированных данных и данных, загружаемых в данный момент
Разобрать заголовок Range несложно. Гораздо труднее сформировать корректный ответ, если запрошенные байты распределены между кэшированными данными, ответом источника и уже выполняющимися загрузками.
В общих чертах узел сети:
- Получает заголовки объекта и метаданные о покрытии диапазонов.
- Считывает доступную часть объекта с диска или из потока в памяти.
- Запрашивает недостающие диапазоны у источника, выравнивая их по границам фрагментов.
- Записывает новые байты и отмечает в метаданных, что диапазон загружен полностью.
- Объединяет части и задаёт для ответа клиенту
Content-RangeиContent-Length.

Это не просто отдельный поиск в кэше, а механизм координации. Для одного запроса могут использоваться префикс из кэша, выполняющаяся загрузка и только что полученный суффикс. Внутренний ответ источника может быть больше ответа клиенту, но его статус и заголовки описывают только запрошенный диапазон.
Если уже выполняющаяся загрузка охватывает байты, необходимые для другого запроса, второй запрос может подключиться к этой загрузке вместо того, чтобы запускать ещё один запрос к источнику. Это позволяет совместно использовать ресурсы при всплесках запросов к популярным большим файлам.
В основе решения лежит существующая модель данных кэша. Заголовки идентифицируют версию объекта, тело хранится ограниченными по размеру фрагментами, а метаданные фиксируют, какие диапазоны были успешно записаны.
Проверка перед объединением
Объединённые ответы с диапазонами корректны, только если все байты относятся к одному и тому же представлению ресурса. Механизм координации проверяет, что каждый ответ источника соответствует запрошенному диапазону, общий размер объекта не изменился, а тег сущности остаётся одинаковым для всех подзапросов.
Заголовок If-Range позволяет клиенту запросить диапазон, только если представление по-прежнему соответствует известному валидатору. Если это не так, Cloudflare возвращает представление целиком, а не объединяет байты из разных версий.
Cloudflare также не объединяет диапазоны для представления, которое собирается распаковать: диапазон сжатых байтов будет отличаться от диапазона, выбранного в распакованном ответе. В таких случаях Cloudflare отдаёт преобразованный ответ целиком, а не объявляет поддержку диапазона, который не сможет корректно обработать.
В результате поведение соответствует стандарту HTTP:
- Для удовлетворимого запроса диапазона возвращается
206 Partial Contentс корректным значениемContent-Range. - Для неудовлетворимого запроса диапазона возвращается
416 Range Not Satisfiable. - Если ответ нельзя безопасно разбить на диапазоны, возвращается представление целиком.
Что изменилось в рабочей среде
Прежде чем сделать функцию «Запросы диапазонов к источнику» доступной всем, мы перевели прежних пользователей кэширования диапазонов на новую реализацию, начав с нагрузок, которые создавали для нас наибольшие проблемы.
Для этих нагрузок были характерны многочисленные параллельные запросы диапазонов к очень большим файлам, часто при резких скачках трафика. В старой системе размеры диапазонов варьировались от десятков килобайт до десятков гигабайт. При большом размере диапазона запрос, который не заполнил бы и дискету, мог заставить нас скачать с источника данные объёмом в несколько DVD и передать их через несколько центров обработки данных — иногда уже после того, как клиент сдался и ушёл к другому источнику. При маленьком размере диапазона один запрос превращался в длинную очередь крошечных запросов к источнику, каждый из которых занимал память в ожидании ответа. Оба сценария приводили к серьёзным операционным инцидентам.
Крупнейшим внутренним пользователем была R2. Настроенный размер диапазона составлял 64 МиБ, поэтому при небольшом чтении, пересекавшем границу диапазона, между центрами обработки данных могло передаваться 128 МиБ. После перевода R2 на новую реализацию мы увидели следующие результаты:
- Более быстрые ответы. В регионах хранения R2 время до получения первого байта на уровне p99 сократилось примерно вдвое — как при попадании в кэш, так и при промахе.
- Меньше трафика в нашей сети. В последнем обновлении мы научили механизм координации объединять байты, уже записанные на диск, с байтами, полученными с вышестоящего сервера, в одном ответе. Раньше запрос обслуживался либо полностью из кэша, либо полностью с вышестоящего сервера. После включения этой функции для R2 трафик многоуровневого кэша для R2 и похожих нагрузок снизился примерно на 6% за неделю — это на петабайты меньше данных, ежедневно передаваемых между нашими центрами обработки данных.
Начало работы
Теперь, когда кэш больше не зависит от выбранного клиентом размера диапазона, настроить продукт проще простого. Клиенты выбирают подходящий трафик с помощью правил кэширования. Разбиение на фрагменты и координацию кэша берёт на себя Cloudflare.
Функция «Запросы диапазонов к источнику» предназначена для трафика, при котором клиенты обычно запрашивают части большого объекта, пригодного для кэширования:
- Потоковое и допускающее перемотку видео, например MP4
- Возобновляемые загрузки больших файлов
- Дистрибутивы программного обеспечения и репозитории пакетов
- Образы дисков, архивы и другие объекты размером в несколько гигабайт
- Нагрузки, связанные с обучением и выводами ИИ, при которых передаются большие файлы моделей или запрашиваются отдельные области артефактов моделей
- Клиенты, получающие метаданные или небольшой фрагмент большого файла
Мы планируем сделать этот способ обслуживания стандартным для некоторых типов файлов в нашей сети; его можно будет изменить с помощью правил кэширования.
Эта функция не меняет обычные условия пригодности ответа для кэширования. Её включение не делает кэшируемым ответ, который иначе кэшировать нельзя. Кроме того, для частичной загрузки с источника он должен поддерживать запросы диапазонов байтов.
Чтобы включить функцию, создайте правило кэширования, которое соответствует трафику с большими файлами, и задайте для запросов диапазонов к источнику значение «Включено». Это можно сделать на панели управления в разделе Caching > Cache Rules или через API с помощью origin_range_requests: { "mode": "on" }. Клиенты выбирают трафик, а узел сети обеспечивает координацию кэша и корректность обработки.
