Новинка Виртуальный VDS сервер в Нидерландах от 390р
Выберите продукт

Range‑запросы без сюрпризов: частичная отдача файлов в Nginx/Apache и кэш

Раздаёте видео, большие архивы или аудио? Запросы с Range и ответы 206 — норма. Показываю, как настроить Nginx и Apache: заголовки, кэш (slice/mod_cache), валидаторы, компрессию и тесты — чтобы частичная отдача была стабильной под нагрузкой.
Range‑запросы без сюрпризов: частичная отдача файлов в Nginx/Apache и кэш

Когда сайт начинает отдавать крупные файлы — видео, аудио, образы, большие архивы — запросы с заголовком Range появляются немедленно. Их присылают медиаплееры для перемотки, скачивальщики для докачки и браузеры при нестабильных сетях. Ответ на такой запрос — 206 Partial Content — кажется простым, но на практике часто бьётся о кэш, компрессию, ETag и прокси. Ниже — практический минимум по Nginx и Apache, чтобы частичная отдача работала стабильно и предсказуемо.

Что такое HTTP Range и когда он нужен

Range: bytes=... просит сервер выдать не весь ресурс, а часть. Клиент указывает диапазон байт: от‑до или от‑конца. Сервер отвечает 206 Partial Content с заголовком Content-Range и куском тела. Зачем это нужно:

  • Перемотка в медиаплеере без скачивания всего файла.
  • Докачка после обрыва — клиент запрашивает недостающий хвост.
  • Оптимизация доставки через кэш: можно отдать только востребованный фрагмент.

Сервер, который умеет корректно Range, обычно добавляет Accept-Ranges: bytes в ответы 200/206, тем самым сигнализируя клиентам поддержку частичной отдачи.

Ключевые заголовки и их смысл

  • Range: bytes=0-1023 — запрос первых 1024 байт. Можно просить «с конца»: bytes=-1024.
  • Content-Range: bytes 0-1023/1234567 — что именно отдали: диапазон и полный размер.
  • Accept-Ranges: bytes — сервер поддерживает отдачу по байтам.
  • If-Range — условная частичная отдача: «если контент не изменился (по ETag или Last-Modified), верни 206; иначе отдай целиком 200».
  • ETag и Last-Modified — валидаторы. Для If-Range корректно работает только сильный ETag (без префикса W/).

Правило безопасности: если запрошенный диапазон некорректен (выходит за размер файла), возвращайте 416 Range Not Satisfiable с Content-Range: bytes */size. Это помогает клиентам пересчитать офсеты.

Частичная отдача и кэш: почему возникают проблемы

Кэширование 206 — тонкая тема. Базовые прокси и браузеры не всегда кэшируют частичные ответы, а некоторые серверы по умолчанию не хотят класть 206 в кэш. Отсюда типовые симптомы:

  • Множественные запросы к бэкенду при перемотке — каждый «скачок» лезет на origin.
  • Несовместимость с компрессией: диапазон относится к байтам сжатого тела, а не исходного контента, что ломает воспроизведение.
  • Конфликт валидаторов между нодами: разные ETag вызывают 200 вместо 206, сбивая клиентов.

Есть две рабочие стратегии:

  1. Игнорировать Range на уровне бэкенда и кэшировать полный 200, а частичные ответы отдавать уже из кэша. Так умеет Apache mod_cache (директива CacheIgnoreRange), и так можно организовать в Nginx через модуль slice.
  2. Кэшировать фрагменты как отдельные сущности («шардировать» ресурс на куски фиксированного размера) и собирать ответ из этих кусочков. Это классический подход Nginx с slice для медиаконтента.

Если вы выбираете стратегию кэширования и политику валидаторов, пригодится материал Cache-Control и ETag для статики.

Как запрос с Range проходит через прокси и кэш с шардированием

Nginx: статическая отдача с Range без сюрпризов

Nginx из коробки корректно поддерживает Range для статических файлов. Основная задача — сделать отдачу неблокирующей и ограничить злоупотребления многодиапазонными запросами.

# /etc/nginx/nginx.conf (фрагмент)
http {
    # Включаем сильные ETag для статического контента
    etag on;

    # Ограничение многодиапазонных запросов (защита от лишней нагрузки)
    max_ranges 1;
}
# server/location для больших файлов
location /media/ {
    # Неблокирующая отдача больших файлов
    sendfile on;
    tcp_nopush on;
    aio threads;
    directio 8m;

    # Явно показываем клиентам поддержку Range
    add_header Accept-Ranges bytes;

    # Контроль кэшируемости (пример, подбирайте значения под проект)
    add_header Cache-Control public, max-age=31536000, immutable;
}

Зачем directio и aio threads? Для больших объектов это снижает блокировки на диске: чтение идёт в обход page cache (с выбранного порога) и выполняется пулом потоков, а не рабочими процессами Nginx. max_ranges 1 закрывает старый класс атак и странные кейсы мультидиапазонов, которые большинству медиаплееров не нужны. Если вы только выбираете стек обслуживания статики, посмотрите короткое сравнение Nginx vs Apache.

Проверяем статическую отдачу

# Полный ответ (ожидаем 200 и Accept-Ranges)
curl -I https://example.org/media/big.mp4

# Первый килобайт (ожидаем 206 и Content-Range)
curl -i -H "Range: bytes=0-1023" https://example.org/media/big.mp4

# Хвост с произвольной позиции
curl -i -H "Range: bytes=1048576-" https://example.org/media/big.mp4

Проверьте, что Content-Length в 200‑ответе равен полному размеру, а Content-Range в 206 корректно указывает диапазон и общий размер.

Nginx как прокси: кэш и Range через slice

Если файлы лежат за бэкендом (S3‑совместимое хранилище, приложение, другой сервер) и вы хотите эффективно кэшировать частичные запросы, удобен модуль slice. Он «разрезает» ресурс на фиксированные куски и может кэшировать каждый слайс отдельно.

# Общие настройки кэша
proxy_cache_path /var/cache/nginx/media levels=1:2 keys_zone=media:20m max_size=50g inactive=7d;

server {
    location /video/ {
        # Размер шардирования (подбирайте под типичный bitrate и RTT)
        slice 1m;

        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";

        # Пробрасываем диапазон слайса к origin
        proxy_set_header Range $slice_range;

        # Ключ кэша должен включать диапазон
        proxy_cache media;
        proxy_cache_key $scheme$proxy_host$request_uri$slice_range;
        proxy_cache_valid 200 206 30m;

        # Если origin не умеет Range и отдаёт 200, Nginx сам сформирует 206
        proxy_force_ranges on;

        # Защита и предсказуемость
        max_ranges 1;
        add_header Accept-Ranges bytes;

        # Рекомендованная кэшируемость для медиа
        add_header Cache-Control public, max-age=604800;
    }
}

Как это работает: клиент просит bytes=10m-, Nginx разбивает на сегменты по 1 МБ, делает несколько подзапросов с Range к origin, складывает слайсы в кэш с разными ключами и собирает итоговый 206 для клиента. Повторные перемотки для соседних диапазонов чаще попадут в кэш.

Важно учитывать компрессию: если gzip включён для типа контента, диапазон будет относиться к сжатому телу. Для видео/аудио сжатие обычно не нужно — исключите медиа из gzip_types, иначе Range станет бесполезным для плееров.

Для высокой нагрузки и контроля кеша выделяйте отдельный узел — на собственном VDS можно гибко настроить Nginx, диски и лимиты под профиль медиатрафика.

FastFox VDS
Облачный VDS-сервер
Виртуальные серверы с быстрым запуском и гибкой конфигурацией от 390₽ / мес
Доступные локации
Россия Нидерланды

Типичные ловушки с Nginx и Range

  • Разные ETag на нодах. Для статических файлов оставляйте etag on, но следите, чтобы при выкладке сохранялись время модификации и размер — тогда ETag совпадёт. Синхронизируйте контент так, чтобы сохранялся mtime.
  • Многодиапазонные запросы. Ограничьте max_ranges 1. Этого достаточно для большинства клиентов и экономит CPU/IO.
  • Кэш и 206. Не все прослойки охотно кэшируют 206. С slice складывайте куски как 200/206 и собирайте ответ. Без шардирования кэшировать произвольные диапазоны не стоит — получится фрагментация.
  • Несовместимость с gzip для медиа. Уберите медиа из gzip_types, чтобы диапазоны соответствовали реальным байтам файла.

Концепция настроек Nginx и доставки медиа с Range

Apache: статическая выдача Range и ограничение мультидиапазонов

Apache поддерживает частичную отдачу из коробки. Для больших файлов включаем эффективную выдачу и ограничиваем мультидиапазоны.

# httpd.conf или vhost (фрагмент)
# Сильные ETag без inode (стабильно на кластере)
FileETag MTime Size

# Лимитируем количество диапазонов в запросе
MaxRanges 1

# Рекомендации для статической отдачи
EnableSendfile On
EnableMMAP Off
# Пример контекстных настроек для каталога с медиа
<Directory "/var/www/media">
    # Явно объявляем поддержку Range
    Header set Accept-Ranges bytes

    # Делаем контент публично кэшируемым (подберите сроки хранения)
    Header set Cache-Control "public, max-age=604800"
</Directory>

FileETag MTime Size — важный момент для кластеров: исключаем инод, чтобы ETag был одинаковым на разных серверах при равных размере и времени модификации.

Apache mod_cache: игнорировать Range на backend, отдавать из кэша

Если хотите, чтобы backend всегда получал запросы без Range, а частичные ответы обслуживались фронтендом из кэша, используйте mod_cache с директивой CacheIgnoreRange:

# Диск-кэш для /media, игнорируем Range при запросе к origin
CacheQuickHandler On
CacheLock On
CacheLockAge 5
CacheLockMaxAge 10

CacheEnable disk "/media"
CacheIgnoreRange On
CacheHeader on

Схема такая: первый запрос с Range приведёт к получению полного объекта 200 от origin и его кэшированию; затем Apache сам нарежет 206 для клиентов из локального кэша. Этот подход хорош, если объект имеет разумный размер и часто переиспользуется.

Валидаторы, If-Range и консистентность

If-Range — полезный механизм: клиент запрашивает часть файла только если он не изменился. Если валидатор не совпал, сервер вернёт полный 200. Чтобы всё работало предсказуемо:

  • Обеспечьте сильный ETag (без W/) и стабильный между нодами. В Nginx для статических файлов это достигается сохранением mtime при выкладке. В Apache используйте FileETag MTime Size.
  • Не меняйте ETag по пустякам. Например, не пересобирайте файл «на месте» без изменения содержимого — ETag должен отражать контент.
  • Для длинного хранения в кэше настраивайте Cache-Control и, при необходимости, Vary (например, по Accept-Encoding), чтобы избежать путаницы между сжатыми и несжатыми вариантами.

Важно: диапазоны относятся к конкретному представлению ресурса. Если выдаёте gzipped-версию, диапазон указывает байты сжатого потока. Для медиа обычно избегайте компрессии.

Проверка сценариев с curl

Ниже минимальный набор проверок, который я делаю после настройки:

# 1) Базовая проверка 206
curl -i -H "Range: bytes=0-1023" https://example.org/media/big.mp4

# 2) Диапазон с открытым концом
curl -i -H "Range: bytes=1048576-" https://example.org/media/big.mp4

# 3) Некорректный диапазон (должен быть 416)
curl -i -H "Range: bytes=9999999999-" https://example.org/media/big.mp4

# 4) If-Range с ETag (подставьте реальный ETag)
curl -i -H "If-Range: \"123456789abcdef\"" -H "Range: bytes=0-1023" https://example.org/media/big.mp4

# 5) Повторный запрос — проверяем кэш-хиты (по server headers)
curl -i -H "Range: bytes=0-1048575" https://example.org/video/film.mp4

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

Чеклист для продакшена

  • Nginx статик: etag on, sendfile on, aio threads, порог directio, max_ranges 1, корректные Cache-Control.
  • Nginx прокси: slice, proxy_cache_key с $slice_range, proxy_set_header Range $slice_range, proxy_force_ranges on, ограничение max_ranges.
  • Apache статик: EnableSendfile On, EnableMMAP Off, MaxRanges 1, FileETag MTime Size, заголовок Accept-Ranges.
  • Apache mod_cache: CacheIgnoreRange On для стратегии «кэшируем полный объект».
  • Компрессия: не применять gzip к аудио/видео; аккуратно с Range и сжатием.
  • Валидаторы: сильные ETag и консистентность между нодами; сохранение mtime при выкладке.
  • Тесты: 200/206/416, If-Range, кэш‑хиты, поведение при перемотке.

Практические советы по выбору размеров и таймаутов

  • Размер слайса: начните с 1–4 МБ. Меньше — больше накладных расходов на запросы к origin, больше — хуже «попадание» при перемотке.
  • Кэш: под медиакаталог выделите отдельную keys_zone и лимит по размеру; следите за inactive, чтобы «редкие» куски не вываливались слишком быстро.
  • Таймауты: для origin увеличьте proxy_read_timeout и send_timeout, если сеть до хранилища неидеальна. Отсечка по соединениям клиента — по профилю нагрузки.
  • Грейс: если используете прокси‑кэш с выдачей «устаревшего при ошибке», добавляйте настроенный «stale» для устойчивости к всплескам ошибок origin.

Что сломается, если сделать «не так»

  • Медиаплеер зависает при перемотке — вероятно, получаете сжатое тело и диапазон относится к gz‑потоку, а не к байтам файла.
  • Постоянные 200 вместо 206 при If-Range — валидаторы различаются между нодами, ETag слабый или меняется без изменения содержимого.
  • Бурст запросов на origin при пиковом онлайне — частичные ответы не кэшируются; внедряйте slice или стратегию полного кэша с игнором Range на backend.
  • Неожиданные 416 — клиент просит диапазоны вне размера. Проверьте корректность Content-Length на 200 и Content-Range на 206.

Итоги

Частичная отдача файлов — обязаловка для любого медиапроекта и просто крупных загрузок. Чтобы HTTP Range и 206 Partial Content работали без сюрпризов, настройте сервер и кэш согласованно: включите предсказуемые валидаторы, ограничьте мультидиапазоны, продумайте стратегию кэширования (полные объекты либо шардирование с slice), аккуратно обойдитесь с компрессией. После базовой доводки вы получите экономию трафика к origin, стабильную перемотку и отсутствие «битых» докачек — ровно то, что нужно медиа‑сайтам и серверам скачиваний.

Поделиться статьей

Вам будет интересно

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

После одновременной остановки всех узлов Galera обычного запуска MariaDB недостаточно. Сначала найдите сервер с последней транзакц ...
CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

Разберём, почему CoreDNS перезапускается или отвечает SERVFAIL, как обнаружить замкнутую пересылку DNS-запросов, проверить доступн ...
Apache Kafka KRaft на VDS: отдельные controller и broker

Apache Kafka KRaft на VDS: отдельные controller и broker

Соберём новый Kafka-кластер без ZooKeeper на шести Linux VDS: три узла controller образуют отказоустойчивый metadata quorum, а три ...