Перейти к содержанию

immerse: Модуль фильтра современного формата изображений NGINX

Требуется план Pro (или выше) подписки GetPageSpeed NGINX Extras.

Установка

Вы можете установить этот модуль в любой дистрибутив на базе RHEL, включая, но не ограничиваясь:

  • RedHat Enterprise Linux 7, 8, 9 и 10
  • CentOS 7, 8, 9
  • AlmaLinux 8, 9
  • Rocky Linux 8, 9
  • Amazon Linux 2 и Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install nginx-module-immerse
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install nginx-module-immerse

Включите модуль, добавив следующее в начало файла /etc/nginx/nginx.conf:

load_module modules/ngx_http_immerse_module.so;

Этот документ описывает nginx-module-immerse v1.0.2, выпущенный 5 апреля 2026 года.


Модуль фильтра NGINX для прозрачной доставки изображений в современном формате. Перехватывает ответы с изображениями из любого источника (статические файлы, proxy_pass, FastCGI и т.д.) и преобразует их в WebP или AVIF на основе заголовков Accept клиента. Без переписывания URL, без отдельного сервиса, без изменений в приложении.

Как это работает

ngx_immerse вставляется в цепочку фильтров NGINX. Когда ответ с Content-Type: image/jpeg, image/png или image/gif проходит через модуль, он проверяет заголовок Accept клиента на поддержку современного формата. Если совпадение найдено, он либо обслуживает кэшированное преобразование, либо запускает его через пул потоков - не блокируя процессы рабочих.

Client Request                    NGINX
     |                              |
     |--- GET /photo.jpg ---------> |
     |    Accept: image/avif,       |
     |    image/webp                |
     |                              |--- upstream / static file
     |                              |<-- image/jpeg response
     |                              |
     |                         [ngx_immerse]
     |                              |--- cache hit? serve cached avif
     |                              |--- cache miss + lazy? serve jpeg,
     |                              |    queue background conversion
     |                              |--- cache miss + sync? convert in
     |                              |    thread pool, serve avif
     |                              |
     |<-- 200 image/avif ---------- |
     |    Vary: Accept              |

Особенности

  • Прозрачная согласование формата - анализирует заголовок Accept в соответствии с RFC 7231 с поддержкой коэффициента качества (q=0 отклоняет, наивысший q выигрывает)
  • Вывод в WebP и AVIF - настраиваемое качество, порядок приоритета и условная компиляция (собрать только один, если это необходимо)
  • Кэш на основе файлов - с ключом MD5 и временем изменения исходного файла в ключе, так что кэш автоматически недействителен, когда оригинальное изображение изменяется
  • Два режима преобразования - lazy (обслужить оригинал сейчас, преобразовать в фоновом режиме) и sync (преобразовать встраиваемо, сразу обслужить современный формат)
  • Интеграция с пулом потоков - преобразования выполняются в пулах потоков NGINX, освобождая цикл событий
  • Гладкое резервирование - сбой преобразования, поврежденные изображения, отсутствующий поток пул, полный диск: всегда обслуживает оригинал, никогда не возвращает 500
  • Пороговые значения размера - пропускать изображения ниже immerse_min_size или выше immerse_max_size, чтобы избежать потерь CPU на крошечных значках или огромных ресурсах
  • Безопасный для CDN - добавляет Vary: Accept, чтобы кэши и CDN не обслуживали неправильный формат неправильному клиенту
  • Заголовок отладки - X-Immerse: hit|miss|error|pass показывает, что произошло (можно переключать)
  • Обнаружение магических байтов - определяет входной формат по сигнатуре файла, а не по расширению URL

Конфигурация

Минимальный пример

thread_pool immerse threads=4;

http {
    immerse_cache_path /var/cache/nginx/immerse levels=1:2 max_size=1g;

    server {
        listen 80;

        location /images/ {
            immerse on;
            immerse_thread_pool immerse;
            alias /var/www/images/;
        }
    }
}

С проксируемым контентом

thread_pool immerse threads=4;

http {
    immerse_cache_path /var/cache/nginx/immerse levels=1:2 max_size=1g;

    server {
        listen 80;

        location /api/photos/ {
            immerse on;
            immerse_mode sync;
            immerse_thread_pool immerse;
            proxy_pass http://backend;
        }
    }
}

Полный пример со всеми директивами

thread_pool immerse threads=4;

http {
    immerse_cache_path /var/cache/nginx/immerse levels=1:2
                       max_size=2g inactive=60d;

    # По умолчанию для всех локаций
    immerse_formats avif webp;
    immerse_webp_quality 82;
    immerse_avif_quality 63;

    server {
        listen 80;

        # Статические изображения - режим lazy (по умолчанию)
        location /images/ {
            immerse on;
            immerse_thread_pool immerse;
            immerse_min_size 2k;
            immerse_max_size 5m;
            alias /var/www/images/;
        }

        # Проксируемые изображения - режим sync для немедленного преобразования
        location /api/photos/ {
            immerse on;
            immerse_mode sync;
            immerse_thread_pool immerse;
            proxy_pass http://backend;
        }

        # Только WebP (без AVIF)
        location /thumbnails/ {
            immerse on;
            immerse_formats webp;
            immerse_webp_quality 75;
            immerse_thread_pool immerse;
            alias /var/www/thumbs/;
        }

        # Отключить заголовок отладки в производственной среде
        location /cdn/ {
            immerse on;
            immerse_x_header off;
            immerse_thread_pool immerse;
            alias /var/www/cdn/;
        }
    }
}

Справочник директив

immerse

Синтаксис: immerse on | off;
По умолчанию: off
Контекст: location

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

Требуется, чтобы immerse_cache_path был установлен на уровне http. Если путь к кэшу не настроен, модуль регистрирует ошибку и передает ответ без изменений.

immerse_cache_path

Синтаксис: immerse_cache_path path [levels=levels] [max_size=size] [inactive=time];
По умолчанию: нет (обязательно, когда immerse включен)
Контекст: http

Устанавливает каталог кэша и параметры. Эта директива обязательна - модуль не будет преобразовывать изображения без настроенного пути к кэшу.

Параметры:

  • path - файловая директория для кэшированных преобразований. Создается автоматически, если не существует.
  • levels - глубина иерархии подкаталогов, указанная в виде двоеточия-разделенных цифр (1 или 2). По умолчанию: 1:2. С levels=1:2 ключ кэша a3b1c4d5e6... хранится по адресу path/a/3b/a3b1c4d5e6....webp.
  • max_size - максимальный общий размер кэша. Принимает суффиксы размера (k, m, g). По умолчанию: не установлен (без ограничений).
  • inactive - время, по истечении которого неиспользуемые кэшированные файлы могут быть удалены. Принимает суффиксы времени (s, m, h, d). По умолчанию: 30d.
immerse_cache_path /var/cache/nginx/immerse levels=1:2 max_size=1g inactive=30d;

immerse_formats

Синтаксис: immerse_formats format ...;
По умолчанию: avif webp
Контекст: http, server, location

Устанавливает предпочтительные выходные форматы в порядке приоритета. Когда клиент принимает несколько форматов с равными коэффициентами качества, первый формат, указанный здесь, выигрывает.

Допустимые форматы: avif, webp. По крайней мере один из них должен быть поддержан на этапе компиляции.

# Предпочитать WebP перед AVIF
immerse_formats webp avif;

# Только WebP
immerse_formats webp;

immerse_mode

Синтаксис: immerse_mode lazy | sync;
По умолчанию: lazy
Контекст: location

Устанавливает стратегию преобразования для случаев, когда кэш не найден.

lazy - сразу обслуживает оригинальное изображение без задержки. Если исходный файл поддерживается, ставит в очередь фоновое преобразование в пуле потоков. Преобразованный вариант будет доступен для последующих запросов. Лучше всего подходит для обслуживания статических файлов, где важна задержка первого запроса.

sync - буферизует все тело ответа, преобразует его в пуле потоков и обслуживает преобразованное изображение в том же запросе. Рабочий не блокируется (пул потоков обрабатывает работу). Лучше всего подходит для проксируемого контента или когда вы хотите, чтобы каждый ответ был в современном формате.

# Статические файлы - lazy вполне подходит, кэш быстро заполняется
location /images/ {
    immerse on;
    immerse_mode lazy;
}

# Ответы API - sync гарантирует современный формат при первом запросе
location /api/photos/ {
    immerse on;
    immerse_mode sync;
    proxy_pass http://backend;
}

immerse_webp_quality

Синтаксис: immerse_webp_quality quality;
По умолчанию: 80
Контекст: http, server, location

Качество кодирования WebP (1-100). Более высокие значения обеспечивают лучшее визуальное качество при больших размерах файлов. Значения около 75-85 обеспечивают хороший баланс для большинства контента.

immerse_avif_quality

Синтаксис: immerse_avif_quality quality;
По умолчанию: 60
Контекст: http, server, location

Качество кодирования AVIF (1-100). AVIF достигает хорошего визуального качества при более низких числовых значениях, чем WebP или JPEG. Значения около 50-70 типичны для веб-доставки. Кодировщик использует скорость 6 (сбалансированная скорость/качество).

immerse_min_size

Синтаксис: immerse_min_size size;
По умолчанию: 1k (1024 байта)
Контекст: http, server, location

Минимальный размер тела ответа для преобразования. Изображения меньше этого размера передаются без изменений. Это позволяет избежать потерь CPU на крошечных изображениях (favicon, 1x1 пиксели для отслеживания), где преобразование формата дает незначительную экономию.

immerse_max_size

Синтаксис: immerse_max_size size;
По умолчанию: 10m (10485760 байт)
Контекст: http, server, location

Максимальный размер тела ответа для преобразования. Изображения больше этого размера передаются без изменений. Это предотвращает исчерпание ресурсов из-за очень больших изображений, которые потребляют значительную память и CPU во время декодирования/кодирования.

immerse_thread_pool

Синтаксис: immerse_thread_pool name;
По умолчанию: default
Контекст: http, server, location

Имя пула потоков NGINX, который будет использоваться для задач преобразования. Должно совпадать с директивой thread_pool в основном контексте конфигурации.

# Определите выделенный пул
thread_pool immerse threads=4;

http {
    server {
        location /images/ {
            immerse on;
            immerse_thread_pool immerse;
        }
    }
}

Рекомендации по размеру: начните с числа ядер CPU. Кодирование изображений зависит от CPU, поэтому большее количество потоков, чем ядер, не дает преимущества. Если тот же сервер обрабатывает другую работу пула потоков (aio), рассмотрите возможность создания выделенного пула для immerse.

immerse_x_header

Синтаксис: immerse_x_header on | off;
По умолчанию: on
Контекст: http, server, location

Управляет заголовком ответа X-Immerse. Когда включено, каждый обработанный ответ включает заголовок, указывающий, что произошло:

Значение Значение
hit Обслужено из кэша
miss Промах по кэшу; преобразовано (sync) или оригинал обслужен (lazy)
error Преобразование не удалось; оригинал обслужен в качестве резервного варианта

Отключите это в производственной среде, если не хотите раскрывать внутреннее состояние модуля клиентам.

Заголовки ответа

Когда ngx_immerse обрабатывает ответ, он изменяет или добавляет следующие заголовки:

Заголовок Значение Когда
Content-Type image/webp или image/avif Преобразовано или обслужено из кэша
Content-Length Размер преобразованного изображения Преобразовано или обслужено из кэша
Vary Accept Всегда (даже при пропуске) когда модуль активен
X-Immerse hit, miss или error Когда immerse_x_header включен

Заголовок Vary: Accept критически важен для правильного поведения CDN. Без него CDN может кэшировать ответ WebP и обслуживать его клиенту, который поддерживает только JPEG.

Парсинг заголовка Accept

Модуль анализирует заголовок запроса Accept в соответствии с RFC 7231:

  • Извлекает записи image/webp и image/avif с их коэффициентами качества
  • q=0 означает, что клиент явно отклоняет этот формат
  • q=1 (или отсутствие параметра q) означает полную поддержку
  • Формат с наивысшим значением q выбирается
  • При равных q выигрывает первый формат в immerse_formats
  • Если ни один формат не присутствует или оба имеют q=0, обслуживается оригинал

Примеры:

Заголовок Accept Результат (с умолчательным immerse_formats avif webp)
image/avif, image/webp AVIF (первый в конфигурации, равные q)
image/webp WebP
image/avif;q=0.8, image/webp;q=0.9 WebP (выше q)
image/avif;q=0, image/webp WebP (AVIF отклонен)
text/html, image/jpeg Оригинал (нет современного формата)

Обнаружение входного формата

Исходные изображения определяются по магическим байтам в теле ответа, а не по расширению файла:

Формат Магические байты
JPEG FF D8 FF
PNG 89 50 4E 47 0D 0A 1A 0A
GIF GIF87a или GIF89a

Изображения, уже находящиеся в формате WebP или AVIF, передаются без изменений. Анимированные GIF (несколько кадров) также передаются.

Кэш

Как это работает

Кэш хранит преобразованные изображения в иерархии каталогов, ключ которой - MD5 хэш. Вход для хэша - URI + source_mtime + target_format + quality, так что:

  • Разные форматы (WebP, AVIF) получают отдельные записи в кэше
  • Изменение настроек качества создает новые записи в кэше
  • Модификация оригинального изображения (изменение его времени) автоматически недействительна кэшированное преобразование

Структура каталогов

С levels=1:2 ключ кэша a3b1c4d5... создает:

/var/cache/nginx/immerse/a/3b/a3b1c4d5e6f7890123456789abcdef01.webp

Атомарные записи

Файлы кэша записываются атомарно: данные сначала идут в временный файл, затем rename() перемещает его на место. Это предотвращает обслуживание частично записанных файлов при одновременной нагрузке.

Прогрев кэша

В режиме lazy первый запрос к изображению обслуживает оригинал. Преобразование работает в фоновом режиме, и последующие запросы получают кэшированный современный формат. В режиме sync самый первый запрос запускает преобразование и обслуживает результат.

Ручная очистка кэша

Чтобы очистить весь кэш:

rm -rf /var/cache/nginx/immerse/*

Перезагрузка NGINX не требуется. Модуль воссоздаст каталоги по мере необходимости.

Обработка ошибок

ngx_immerse следует строгой политике "никогда не ломать то, что уже работает":

Условие Поведение
Преобразование не удалось (ошибка кодека) Обслужить оригинал, записать ошибку
Запись в кэш не удалась (диск полон, права доступа) Обслужить преобразованное из памяти, записать предупреждение
Поврежденное или усеченное входное изображение Обслужить оригинал, записать ошибку
Изображение ниже min_size или выше max_size Передать без изменений
Нет современного формата в Accept клиента Передать без изменений, добавить Vary: Accept
Пул потоков не найден Вернуться к синхронному (блокирующему) преобразованию
immerse_cache_path не настроен Передать без изменений, записать ошибку
Неизвестный формат изображения (не JPEG/PNG/GIF) Передать без изменений

Модуль никогда не вернет ошибку 500 из-за сбоя преобразования.

Архитектура

Исходные файлы

Файл Назначение
config Интеграция с системой сборки NGINX, обнаружение библиотек
src/ngx_http_immerse_common.h Общие типы, константы, объявления функций
src/ngx_http_immerse_module.c Точка входа модуля, директивы, жизненный цикл конфигурации
src/ngx_http_immerse_filter.c Цепочка фильтров заголовков и тела, конечный автомат, распределение потоков
src/ngx_http_immerse_convert.c Движок декодирования/кодирования изображений (работает в пуле потоков)
src/ngx_http_immerse_cache.c Генерация ключей кэша, поиск, атомарное хранение
src/ngx_http_immerse_accept.c Парсер заголовка Accept RFC 7231
src/ngx_http_immerse_util.c Отправка ответа, буферизация тела, обнаружение формата, вспомогательные функции заголовков

Безопасность потоков

Вся работа по преобразованию изображений выполняется в рабочих потоках пула NGINX. Код преобразования (ngx_http_immerse_convert.c) использует только malloc/free, POSIX файловый ввод/вывод и вызовы библиотек изображений. Он никогда не обращается к общему состоянию NGINX, пулам запросов или циклу событий.

Результаты передаются обратно в основной цикл событий через стандартный механизм завершения задач потоков NGINX (ngx_thread_task_t).

Конечный автомат

Фильтр тела использует конечный автомат на основе фаз:

START -> READ -> CONVERT -> SEND -> DONE     (режим sync)
PASS -> DONE                                  (режим lazy, первый запрос)
SERVE_CACHE -> DONE                           (попадание в кэш)

Тестирование

На основе Docker (рекомендуется)

# Запустить все тесты (режим HUP для ~10x более быстрой итерации)
make tests

# Запустить конкретный файл теста
make tests T=t/sync.t

# Запустить без режима HUP (чище состояние между тестами)
make tests HUP=0

# Интерактивная оболочка для отладки
make shell

# Пересобрать базовый образ (после изменений в Dockerfile)
make base-image

# Протестировать с другой версией NGINX
make tests NGINX_VERSION=release-1.26.2

CI

GitHub Actions запускает тесты против NGINX 1.26.2, 1.27.3 и 1.28.0 при каждом push и pull request.

Набор тестов

Файл Покрытие
t/accept.t Парсинг заголовка Accept, значения q, выбор формата
t/sync.t Преобразование в режиме sync для JPEG, PNG, GIF в WebP/AVIF
t/lazy.t Режим lazy: оригинал обслуживается первым, кэш заполняется после
t/cache.t Поведение попадания/промаха в кэш
t/limits.t Фильтрация min_size и max_size
t/fallback.t Резервирование при поврежденном изображении
t/passthrough.t Модуль отключен, не изображен контент, нет заголовка Accept
t/config.t Проверка директив, переключение x_header
t/vary.t Наличие заголовка Vary: Accept

Отладка

  • Проверьте test-error.log в корне репозитория для вывода отладки NGINX
  • Используйте make shell, чтобы войти в контейнер и вручную запустить тесты
  • Уровень логирования установлен на debug в тестовой среде Docker